All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.codename1.ui.validation.Validator Maven / Gradle / Ivy

There is a newer version: 7.0.167
Show newest version
/*
 * 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.Container;
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.InputComponent;
import com.codename1.ui.PickerComponent;
import com.codename1.ui.TextComponent;
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.plaf.UIManager;
import com.codename1.ui.spinner.Picker;
import com.codename1.ui.util.UITimer;
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.

* *

It's possible to create any custom logic of validation. Example (see * this * discussion on StackOverflow):

* * * * @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 constraintList = new HashMap(); private ArrayList submitButtons = new ArrayList(); /** * 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. Shows validation errors messages even when the * TextModeLayout is not {@code onTopMode} (it's possible to disable this * functionality setting to false the theme constant * {@code showValidationErrorsIfNotOnTopMode}: basically, the error * message is shown for two second in place of the label on the left of the * InputComponent (or on right of the InputComponent for RTL languages); * this solution never breaks the layout, because the error message is * trimmed to fit the available space. The error message UIID is * "ErrorLabel" when it's not onTopMode. * * @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) { Constraint constraint = null; if (c.length == 1) { constraint = c[0]; constraintList.put(cmp, constraint); } else if (c.length > 1) { constraint = new GroupConstraint(c); constraintList.put(cmp, constraint); } if (constraint == null) { throw new IllegalArgumentException("addConstraint needs at least a Constraint, but the Constraint array in empty"); } bindDataListener(cmp); boolean isV = isValid(); for (Component btn : submitButtons) { btn.setEnabled(isV); } // Show validation error on iPhone if (UIManager.getInstance().isThemeConstant("showValidationErrorsIfNotOnTopMode", true) && cmp instanceof InputComponent) { final InputComponent inputComponent = (InputComponent) cmp; if (!inputComponent.isOnTopMode()) { Label labelForComponent = null; if (inputComponent instanceof TextComponent) { labelForComponent = ((TextComponent) inputComponent).getField().getLabelForComponent(); } else if (inputComponent instanceof PickerComponent) { labelForComponent = ((PickerComponent) inputComponent).getPicker().getLabelForComponent(); } if (labelForComponent != null) { final Label myLabel = labelForComponent; final String originalText = myLabel.getText(); final String originalUIID = myLabel.getUIID(); final Constraint myConstraint = constraint; final Runnable showError = new Runnable() { @Override public void run() { boolean isValid = false; if (inputComponent instanceof TextComponent) { isValid = myConstraint.isValid(((TextComponent) inputComponent).getField().getText()); } else if (inputComponent instanceof PickerComponent) { isValid = myConstraint.isValid(((PickerComponent) inputComponent).getPicker().getValue()); } String errorMessage = trimLongString(UIManager.getInstance().localize(myConstraint.getDefaultFailMessage(), myConstraint.getDefaultFailMessage()), "ErrorLabel", myLabel.getWidth()); if (errorMessage != null && errorMessage.length() > 0 && !isValid) { // show the error in place of the label for component myLabel.setUIID("ErrorLabel"); myLabel.setText(errorMessage); UITimer.timer(2000, false, Display.getInstance().getCurrent(), new Runnable() { @Override public void run() { myLabel.setUIID(originalUIID); myLabel.setText(originalText); } }); } else { // show the label for component without the error myLabel.setUIID(originalUIID); myLabel.setText(originalText); } } }; FocusListener myFocusListener = new FocusListener() { @Override public void focusLost(Component cmp) { showError.run(); } @Override public void focusGained(Component cmp) { // no code here } }; if (inputComponent instanceof TextComponent) { ((TextComponent) inputComponent).getField().addFocusListener(myFocusListener); } else if (inputComponent instanceof PickerComponent) { ((PickerComponent) inputComponent).getPicker().addFocusListener(myFocusListener); } } } } return this; } /** * Long error messages are trimmed to fit the available space in the layout * * @param errorMessage the string to be trimmed * @param uiid the uiid of the errorMessage * @param width the maximum width * @return the new String trimmed to fit the available width */ private String trimLongString(String errorMessage, String uiid, int width) { Label errorLabel = new Label(errorMessage, uiid); while (errorLabel.getPreferredW() > width && errorMessage.length() > 1) { errorMessage = errorMessage.substring(0, errorMessage.length() - 1); errorLabel.setText(errorMessage); } return errorMessage; } /** * 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 InputComponent) { cmp = ((InputComponent)cmp).getEditor(); } if(cmp instanceof TextArea) { return ((TextArea)cmp).getText(); } if(cmp instanceof Picker) { return ((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) { if(!(cmp instanceof InputComponent && ((InputComponent)cmp).isOnTopMode())) { 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 TextComponent) { ((TextComponent)cmp).getField().addDataChangedListener(new ComponentListener(cmp)); return; } if(cmp instanceof TextField) { ((TextField)cmp).addDataChangedListener(new ComponentListener(cmp)); return; } } if(cmp instanceof TextComponent) { ((TextComponent)cmp).getField().addActionListener(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; } if(cmp instanceof Picker) { ((Picker)cmp).addActionListener(new ComponentListener(cmp)); return; } if(cmp instanceof PickerComponent) { ((PickerComponent)cmp).getPicker().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); Constraint c = constraintList.get(cmp); if(c != null) { setValid(cmp, c.isValid(val)); } } boolean isValid(Component cmp) { Boolean b = (Boolean)cmp.getClientProperty(VALID_MARKER); if(b != null) { return b.booleanValue(); } Object val = getComponentValue(cmp); Constraint c = constraintList.get(cmp); if(c != null) { return c.isValid(val); } return true; } /** * 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) { 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(cmp instanceof InputComponent && ((InputComponent)cmp).isOnTopMode()) { InputComponent tc = (InputComponent)cmp; if(v) { tc.errorMessage(null); } else { tc.errorMessage(getErrorMessage(cmp)); } } if (cmp.getComponentForm() != null) { 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; private Rectangle visibleRect = new Rectangle(); 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)) { if(c instanceof InputComponent && ((InputComponent)c).isOnTopMode()) { continue; } 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); Container parent = c.getParent(); visibleRect = parent.getVisibleBounds(visibleRect); Container grandParent = parent.getParent(); if (grandParent != null){ visibleRect.setX(visibleRect.getX() + grandParent.getAbsoluteX()); visibleRect.setY(visibleRect.getY() + grandParent.getAbsoluteY()); } int[] originalClip = g.getClip(); g.setClip(visibleRect); if(xpos + validationFailedEmblem.getWidth() > Display.getInstance().getDisplayWidth()) { g.drawImage(validationFailedEmblem, xpos - validationFailedEmblem.getWidth(), ypos - validationFailedEmblem.getHeight() / 2); } else { g.drawImage(validationFailedEmblem, xpos - validationFailedEmblem.getWidth() / 2, ypos - validationFailedEmblem.getHeight() / 2); } g.setClip(originalClip); } } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy