/* * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Codename One designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ package com.codename1.ui.validation; import com.codename1.components.InteractionDialog; import com.codename1.ui.Button; import com.codename1.ui.CheckBox; import com.codename1.ui.Component; import com.codename1.ui.Display; import com.codename1.ui.FontImage; import com.codename1.ui.Form; import com.codename1.ui.Graphics; import com.codename1.ui.Image; import com.codename1.ui.Label; import com.codename1.ui.List; import com.codename1.ui.Painter; import com.codename1.ui.RadioButton; import com.codename1.ui.TextArea; import com.codename1.ui.TextField; import com.codename1.ui.events.ActionEvent; import com.codename1.ui.events.ActionListener; import com.codename1.ui.events.DataChangedListener; import com.codename1.ui.events.FocusListener; import com.codename1.ui.events.ScrollListener; import com.codename1.ui.geom.Rectangle; import com.codename1.ui.layouts.BorderLayout; import com.codename1.ui.plaf.Style; import com.codename1.ui.spinner.Picker; import java.util.ArrayList; import java.util.HashMap; /** * Binds validation constraints to form elements, when validation fails it can be highlighted directly on * the component via an emblem or change of the UIID (to original UIID name + "Invalid" e.g. "TextFieldInvalid"). * Validators just run thru a set of Constraint objects to decide if validation succeeded or failed. * * @author Shai Almog */ public class Validator { private static final String VALID_MARKER = "cn1$$VALID_MARKER"; private InteractionDialog message = new InteractionDialog(); /** * Error message UIID defaults to DialogBody. Allows customizing the look of the message */ private String errorMessageUIID = "DialogBody"; /** * Indicates the default mode in which validation failures are expressed * @return the defaultValidationFailureHighlightMode */ public static HighlightMode getDefaultValidationFailureHighlightMode() { return defaultValidationFailureHighlightMode; } /** * Indicates the default mode in which validation failures are expressed * @param aDefaultValidationFailureHighlightMode the defaultValidationFailureHighlightMode to set */ public static void setDefaultValidationFailureHighlightMode(HighlightMode aDefaultValidationFailureHighlightMode) { defaultValidationFailureHighlightMode = aDefaultValidationFailureHighlightMode; } /** * The emblem that will be drawn on top of the component to indicate the validation failure * @return the defaultValidationFailedEmblem */ public static Image getDefaultValidationFailedEmblem() { return defaultValidationFailedEmblem; } /** * The emblem that will be drawn on top of the component to indicate the validation failure * @param aDefaultValidationFailedEmblem the defaultValidationFailedEmblem to set */ public static void setDefaultValidationFailedEmblem(Image aDefaultValidationFailedEmblem) { defaultValidationFailedEmblem = aDefaultValidationFailedEmblem; } /** * The position of the validation emblem on the component as X/Y values between 0 and 1 where * 0 indicates the start of the component and 1 indicates its end on the given axis. * @return the defaultValidationEmblemPositionX */ public static float getDefaultValidationEmblemPositionX() { return defaultValidationEmblemPositionX; } /** * The position of the validation emblem on the component as X/Y values between 0 and 1 where * 0 indicates the start of the component and 1 indicates its end on the given axis. * @param aDefaultValidationEmblemPositionX the defaultValidationEmblemPositionX to set */ public static void setDefaultValidationEmblemPositionX(float aDefaultValidationEmblemPositionX) { defaultValidationEmblemPositionX = aDefaultValidationEmblemPositionX; } /** * The position of the validation emblem on the component as X/Y values between 0 and 1 where * 0 indicates the start of the component and 1 indicates its end on the given axis. * @return the defaultValidationEmblemPositionY */ public static float getDefaultValidationEmblemPositionY() { return defaultValidationEmblemPositionY; } /** * The position of the validation emblem on the component as X/Y values between 0 and 1 where * 0 indicates the start of the component and 1 indicates its end on the given axis. * @param aDefaultValidationEmblemPositionY the defaultValidationEmblemPositionY to set */ public static void setDefaultValidationEmblemPositionY(float aDefaultValidationEmblemPositionY) { defaultValidationEmblemPositionY = aDefaultValidationEmblemPositionY; } /** * Indicates whether validation should occur on every key press (data change listener) or * action performed (editing completion) * @return the validateOnEveryKey */ public static boolean isValidateOnEveryKey() { return validateOnEveryKey; } /** * Indicates whether validation should occur on every key press (data change listener) or * action performed (editing completion) * @param aValidateOnEveryKey the validateOnEveryKey to set */ public static void setValidateOnEveryKey(boolean aValidateOnEveryKey) { validateOnEveryKey = aValidateOnEveryKey; } /** * Indicates the default mode in which validation failures are expressed * @return the validationFailureHighlightMode */ public HighlightMode getValidationFailureHighlightMode() { return validationFailureHighlightMode; } /** * Indicates the default mode in which validation failures are expressed * @param validationFailureHighlightMode the validationFailureHighlightMode to set */ public void setValidationFailureHighlightMode(HighlightMode validationFailureHighlightMode) { this.validationFailureHighlightMode = validationFailureHighlightMode; } /** * The emblem that will be drawn on top of the component to indicate the validation failure * @return the validationFailedEmblem */ public Image getValidationFailedEmblem() { return validationFailedEmblem; } /** * The emblem that will be drawn on top of the component to indicate the validation failure * @param validationFailedEmblem the validationFailedEmblem to set */ public void setValidationFailedEmblem(Image validationFailedEmblem) { this.validationFailedEmblem = validationFailedEmblem; } /** * The position of the validation emblem on the component as X/Y values between 0 and 1 where * 0 indicates the start of the component and 1 indicates its end on the given axis. * @return the validationEmblemPositionX */ public float getValidationEmblemPositionX() { return validationEmblemPositionX; } /** * The position of the validation emblem on the component as X/Y values between 0 and 1 where * 0 indicates the start of the component and 1 indicates its end on the given axis. * @param validationEmblemPositionX the validationEmblemPositionX to set */ public void setValidationEmblemPositionX(float validationEmblemPositionX) { this.validationEmblemPositionX = validationEmblemPositionX; } /** * The position of the validation emblem on the component as X/Y values between 0 and 1 where * 0 indicates the start of the component and 1 indicates its end on the given axis. * @return the validationEmblemPositionY */ public float getValidationEmblemPositionY() { return validationEmblemPositionY; } /** * The position of the validation emblem on the component as X/Y values between 0 and 1 where * 0 indicates the start of the component and 1 indicates its end on the given axis. * @param validationEmblemPositionY the validationEmblemPositionY to set */ public void setValidationEmblemPositionY(float validationEmblemPositionY) { this.validationEmblemPositionY = validationEmblemPositionY; } /** * Indicates whether an error message should be shown for the focused component * @return true if the error message should be displayed */ public boolean isShowErrorMessageForFocusedComponent() { return showErrorMessageForFocusedComponent; } /** * Indicates whether an error message should be shown for the focused component * * @param showErrorMessageForFocusedComponent true to show the error message */ public void setShowErrorMessageForFocusedComponent(boolean showErrorMessageForFocusedComponent) { this.showErrorMessageForFocusedComponent = showErrorMessageForFocusedComponent; } /** * Error message UIID defaults to DialogBody. Allows customizing the look of the message * @return the errorMessageUIID */ public String getErrorMessageUIID() { return errorMessageUIID; } /** * Error message UIID defaults to DialogBody. Allows customizing the look of the message * @param errorMessageUIID the errorMessageUIID to set */ public void setErrorMessageUIID(String errorMessageUIID) { this.errorMessageUIID = errorMessageUIID; } /** * Indicates the validation failure modes */ public static enum HighlightMode { UIID, EMBLEM, UIID_AND_EMBLEM, NONE } /** * Indicates the default mode in which validation failures are expressed */ private static HighlightMode defaultValidationFailureHighlightMode = HighlightMode.EMBLEM; /** * Indicates the mode in which validation failures are expressed */ private HighlightMode validationFailureHighlightMode = defaultValidationFailureHighlightMode; /** * The emblem that will be drawn on top of the component to indicate the validation failure */ private static Image defaultValidationFailedEmblem = null; /** * The emblem that will be drawn on top of the component to indicate the validation failure */ private Image validationFailedEmblem = defaultValidationFailedEmblem; /** * The position of the validation emblem on the component as X/Y values between 0 and 1 where * 0 indicates the start of the component and 1 indicates its end on the given axis. */ private static float defaultValidationEmblemPositionX = 1; /** * The position of the validation emblem on the component as X/Y values between 0 and 1 where * 0 indicates the start of the component and 1 indicates its end on the given axis. */ private static float defaultValidationEmblemPositionY = 0.5f; /** * The position of the validation emblem on the component as X/Y values between 0 and 1 where * 0 indicates the start of the component and 1 indicates its end on the given axis. */ private float validationEmblemPositionX = defaultValidationEmblemPositionX; /** * The position of the validation emblem on the component as X/Y values between 0 and 1 where * 0 indicates the start of the component and 1 indicates its end on the given axis. */ private float validationEmblemPositionY = defaultValidationEmblemPositionY; private HashMap<Component, Constraint> constraintList = new HashMap<Component, Constraint>(); private ArrayList<Component> submitButtons = new ArrayList<Component>(); /** * Indicates whether validation should occur on every key press (data change listener) or * action performed (editing completion) */ private static boolean validateOnEveryKey = false; /** * Indicates whether an error message should be shown for the focused component */ private boolean showErrorMessageForFocusedComponent; /** * Default constructor */ public Validator() { if(defaultValidationFailedEmblem == null) { // initialize the default emblem defaultValidationFailedEmblem = FontImage.createMaterial(FontImage.MATERIAL_CANCEL, "InvalidEmblem", 3); validationFailedEmblem = defaultValidationFailedEmblem; } } /** * Places a constraint on the validator, returns this object so constraint additions can be chained. * Notice that only one constraint * @param cmp the component to validate * @param c the constraint or constraints * @return this object so we can write code like v.addConstraint(cmp1, cons).addConstraint(cmp2, otherConstraint); */ public Validator addConstraint(Component cmp, Constraint... c) { if(c.length == 1) { constraintList.put(cmp, c[0]); } else { constraintList.put(cmp, new GroupConstraint(c)); } bindDataListener(cmp); boolean isV = isValid(); for(Component btn : submitButtons) { btn.setEnabled(isV); } return this; } /** * Submit buttons (or any other component type) can be disabled until all components contain a valid value. * Notice that this method should be invoked after all the constraints are added so the initial state of the buttons * will be correct. * * @param cmp set of buttons or components to disable until everything is valid * @return the validator instance so this method can be chained */ public Validator addSubmitButtons(Component... cmp) { boolean isV = isValid(); for(Component c : cmp) { submitButtons.add(c); c.setEnabled(isV); } return this; } /** * Returns the value of the given component, this can be overriden to add support for custom built components * * @param cmp the component * @return the object value */ protected Object getComponentValue(Component cmp) { if(cmp instanceof TextArea) { return ((TextArea)cmp).getText(); } if(cmp instanceof Picker) { ((Picker)cmp).getValue(); } if(cmp instanceof RadioButton || cmp instanceof CheckBox) { if(((Button)cmp).isSelected()) { return Boolean.TRUE; } return Boolean.FALSE; } if(cmp instanceof Label) { return ((Label)cmp).getText(); } if(cmp instanceof List) { return ((List)cmp).getSelectedItem(); } return null; } /** * Binds an event listener to the given component * @param cmp the component to bind the data listener to * @deprecated this method was exposed by accident, constraint implicitly calls it and you don't need to * call it directly. It will be made protected in a future update to Codename One! */ public void bindDataListener(Component cmp) { if(showErrorMessageForFocusedComponent) { cmp.addFocusListener(new FocusListener() { public void focusGained(Component cmp) { // special case. Before the form is showing don't show error dialogs Form p = cmp.getComponentForm(); if(p != Display.getInstance().getCurrent()) { return; } if(message != null) { message.dispose(); } if(!isValid(cmp)) { String err = getErrorMessage(cmp); if(err != null && err.length() > 0) { message = new InteractionDialog(err); message.getTitleComponent().setUIID(errorMessageUIID); message.setAnimateShow(false); if(validationFailureHighlightMode == HighlightMode.EMBLEM || validationFailureHighlightMode == HighlightMode.UIID_AND_EMBLEM) { int xpos = cmp.getAbsoluteX(); int ypos = cmp.getAbsoluteY(); Component scr = cmp.getScrollable(); if(scr != null) { xpos -= scr.getScrollX(); ypos -= scr.getScrollY(); scr.addScrollListener(new ScrollListener() { public void scrollChanged(int scrollX, int scrollY, int oldscrollX, int oldscrollY) { if (message != null) { message.dispose(); } message = null; } }); } float width = cmp.getWidth(); float height = cmp.getHeight(); xpos += Math.round(width * validationEmblemPositionX); ypos += Math.round(height * validationEmblemPositionY); if(message != null) { message.showPopupDialog(new Rectangle(xpos, ypos, validationFailedEmblem.getWidth(), validationFailedEmblem.getHeight())); } } else { message.showPopupDialog(cmp); } } } } public void focusLost(Component cmp) { } }); } if(validateOnEveryKey) { if(cmp instanceof TextField) { ((TextField)cmp).addDataChangedListener(new ComponentListener(cmp)); return; } } if(cmp instanceof TextArea) { ((TextArea)cmp).addActionListener(new ComponentListener(cmp)); return; } if(cmp instanceof List) { ((List)cmp).addActionListener(new ComponentListener(cmp)); return; } if(cmp instanceof CheckBox || cmp instanceof RadioButton) { ((Button)cmp).addActionListener(new ComponentListener(cmp)); return; } } /** * Returns true if all the constraints are currently valid * @return true if the entire validator is valid */ public boolean isValid() { for(Component c : constraintList.keySet()) { if(!isValid(c)) { return false; } } return true; } /** * Validates and highlights an individual component * @param cmp the component to validate */ protected void validate(Component cmp) { Object val = getComponentValue(cmp); setValid(cmp, constraintList.get(cmp).isValid(val)); } boolean isValid(Component cmp) { Boolean b = (Boolean)cmp.getClientProperty(VALID_MARKER); if(b != null) { return b.booleanValue(); } Object val = getComponentValue(cmp); return constraintList.get(cmp).isValid(val); } /** * Returns the validation error message for the given component or null if no such message exists * @param cmp the invalid component * @return a string representing the error message */ public String getErrorMessage(Component cmp) { return constraintList.get(cmp).getDefaultFailMessage(); } void setValid(Component cmp, boolean v) { Boolean b = (Boolean)cmp.getClientProperty(VALID_MARKER); if(b != null && b.booleanValue() == v) { /* if (!v) { for(Component c : submitButtons) { c.setEnabled(false); } } */ return; } cmp.putClientProperty(VALID_MARKER, v); if(!v) { // if one component is invalid... just disable the submit buttons for(Component c : submitButtons) { c.setEnabled(false); } } else { boolean isV = isValid(); for(Component c : submitButtons) { c.setEnabled(isV); } if(message != null && cmp.hasFocus()) { message.dispose(); } } if(validationFailureHighlightMode == HighlightMode.EMBLEM || validationFailureHighlightMode == HighlightMode.UIID_AND_EMBLEM) { if(!(cmp.getComponentForm().getGlassPane() instanceof ComponentListener)) { cmp.getComponentForm().setGlassPane(new ComponentListener(null)); } } if(v) { if(validationFailureHighlightMode == HighlightMode.UIID || validationFailureHighlightMode == HighlightMode.UIID_AND_EMBLEM) { String uiid = cmp.getUIID(); if(uiid.endsWith("Invalid")) { uiid = uiid.substring(0, uiid.length() - 7); cmp.setUIID(uiid); } return; } if(validationFailureHighlightMode == HighlightMode.EMBLEM && validationFailedEmblem != null) { } } else { if(validationFailureHighlightMode == HighlightMode.UIID || validationFailureHighlightMode == HighlightMode.UIID_AND_EMBLEM) { String uiid = cmp.getUIID(); if(!uiid.endsWith("Invalid")) { cmp.setUIID(uiid + "Invalid"); } return; } } } class ComponentListener implements ActionListener, DataChangedListener, Painter { private Component cmp; public ComponentListener(Component cmp) { this.cmp = cmp; } public void actionPerformed(ActionEvent evt) { validate(cmp); } public void dataChanged(int type, int index) { validate(cmp); } /** * Handles the glasspane work just to save a new class object (smaller code) */ @Override public void paint(Graphics g, Rectangle rect) { for(Component c : constraintList.keySet()) { if(!isValid(c)) { int xpos = c.getAbsoluteX(); int ypos = c.getAbsoluteY(); float width = c.getWidth(); float height = c.getHeight(); xpos += Math.round(width * validationEmblemPositionX); ypos += Math.round(height * validationEmblemPositionY); g.drawImage(validationFailedEmblem, xpos - validationFailedEmblem.getWidth() / 2, ypos - validationFailedEmblem.getHeight() / 2); } } } } }