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

io.guise.framework.component.AbstractTextControl Maven / Gradle / Ivy

There is a newer version: 0.5.3
Show newest version
/*
 * Copyright © 2005-2008 GlobalMentor, Inc. 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.guise.framework.component;

import java.beans.*;
import java.util.regex.Pattern;

import static java.util.Objects.*;

import com.globalmentor.java.Objects;
import com.globalmentor.net.MediaType;
import com.globalmentor.text.Text;

import io.guise.framework.GuiseSession;
import io.guise.framework.converter.*;
import io.guise.framework.model.*;
import io.guise.framework.validator.*;

import static com.globalmentor.java.Classes.*;
import static com.globalmentor.text.Text.*;

/**
 * Control to accept text input from the user representing a particular value type. This control keeps track of literal text entered by the user, distinct from
 * the value stored in the model. The component valid status is updated before any literal text change event is fired. Default converters are available for the
 * following types:
 * 
    *
  • char[]
  • *
  • java.lang.Boolean
  • *
  • java.lang.Float
  • *
  • java.lang.Integer
  • *
  • java.lang.String
  • *
* This control uses a single line feed character to represent each line break. * @param The type of value the input text is to represent. * @author Garret Wilson */ public class AbstractTextControl extends AbstractEditValueControl { /** The auto commit pattern bound property. */ public static final String AUTO_COMMIT_PATTERN_PROPERTY = getPropertyName(AbstractTextControl.class, "autoCommitPattern"); /** The column count bound property. */ public static final String COLUMN_COUNT_PROPERTY = getPropertyName(AbstractTextControl.class, "columnCount"); /** The provisional text literal bound property. */ public static final String PROVISIONAL_TEXT_PROPERTY = getPropertyName(AbstractTextControl.class, "provisionalText"); /** The text literal bound property. */ public static final String TEXT_PROPERTY = getPropertyName(AbstractTextControl.class, "text"); /** The value content type bound property. */ public static final String VALUE_CONTENT_TYPE_PROPERTY = getPropertyName(AbstractTextControl.class, "valueContentType"); /** * The regular expression pattern that will cause the text automatically to be committed immediately, or null if text should not be committed * during entry. */ private Pattern autoCommitPattern = null; /** * @return The regular expression pattern that will cause the text automatically to be committed immediately, or null if text should not be * committed during entry. */ public Pattern getAutoCommitPattern() { return autoCommitPattern; } /** * Sets the The regular expression pattern that will cause the text automatically to be committed immediately. This is a bound property. * @param newAutoCommitPattern The regular expression pattern that will cause the text automatically to be committed immediately, or null if text * should not be committed during entry. * @see #AUTO_COMMIT_PATTERN_PROPERTY */ public void setAutoCommitPattern(final Pattern newAutoCommitPattern) { if(!Objects.equals(autoCommitPattern, newAutoCommitPattern)) { //if the value is really changing (compare their values, rather than identity) final Pattern oldAutoCommitPattern = autoCommitPattern; //get the old value autoCommitPattern = newAutoCommitPattern; //actually change the value firePropertyChange(AUTO_COMMIT_PATTERN_PROPERTY, oldAutoCommitPattern, newAutoCommitPattern); //indicate that the value changed } } /** The estimated number of columns requested to be visible, or -1 if no column count is specified. */ private int columnCount = -1; /** @return The estimated number of columns requested to be visible, or -1 if no column count is specified. */ public int getColumnCount() { return columnCount; } /** * Sets the estimated number of columns requested to be visible. This is a bound property of type Integer. * @param newColumnCount The new requested number of visible columns, or -1 if no column count is specified. * @see #COLUMN_COUNT_PROPERTY */ public void setColumnCount(final int newColumnCount) { if(columnCount != newColumnCount) { //if the value is really changing final int oldColumnCount = columnCount; //get the old value columnCount = newColumnCount; //actually change the value firePropertyChange(COLUMN_COUNT_PROPERTY, new Integer(oldColumnCount), new Integer(newColumnCount)); //indicate that the value changed } } /** The converter for this component. */ private Converter converter; /** @return The converter for this component. */ public Converter getConverter() { return converter; } /** * Sets the converter. This is a bound property * @param newConverter The converter for this component. * @throws NullPointerException if the given converter is null. * @see ValueControl#CONVERTER_PROPERTY */ public void setConverter(final Converter newConverter) { if(converter != newConverter) { //if the value is really changing final Converter oldConverter = converter; //get the old value converter = requireNonNull(newConverter, "Converter cannot be null."); //actually change the value firePropertyChange(CONVERTER_PROPERTY, oldConverter, newConverter); //indicate that the value changed updateText(); //update the text, now that we've installed a new converter } } /** The provisional text literal value, or null if there is no provisional literal value. */ private String provisionalText; /** @return The provisional text literal value, or null if there is no provisional literal value. */ public String getProvisionalText() { return provisionalText; } /** * Sets the provisional text literal value. This method updates the valid status before firing a change event. This is a bound property. * @param newProvisionalText The provisional text literal value. * @see #PROVISIONAL_TEXT_PROPERTY */ public void setProvisionalText(final String newProvisionalText) { if(!Objects.equals(provisionalText, newProvisionalText)) { //if the value is really changing (compare their values, rather than identity) final String oldProvisionalText = provisionalText; //get the old value provisionalText = newProvisionalText; //actually change the value updateValid(); //update the valid status before firing the text change property so that any listeners will know the valid status firePropertyChange(PROVISIONAL_TEXT_PROPERTY, oldProvisionalText, newProvisionalText); //indicate that the value changed } } /** The text literal value displayed in the control, or null if there is no literal value. */ private String text; /** @return The text literal value displayed in the control, or null if there is no literal value. */ public String getText() { return text; } /** * Sets the text literal value displayed in the control. This method updates the provisional text to match and updates the valid status if needed. This is a * bound property. * @param newText The text literal value displayed in the control. * @see #TEXT_PROPERTY */ public void setText(final String newText) { if(!Objects.equals(text, newText)) { //if the value is really changing (compare their values, rather than identity) final String oldText = text; //get the old value text = newText; //actually change the value final String oldProvisionalText = getProvisionalText(); //get the current provisional text final boolean provisionalTextAlreadyMatched = Objects.equals(text, getProvisionalText()); //see if the provisional text already matches our new text value setProvisionalText(text); //update the provisional text before firing a text change property so that the valid state will be updated if(provisionalTextAlreadyMatched) { //if the provisional text already matched the new text, it didn't update validity, so we need to do that here updateValid(); //update the valid status before firing the text change property so that any listeners will know the valid status; this will also update the status, which is important because the text and provisional text values are now the same } firePropertyChange(TEXT_PROPERTY, oldText, newText); //indicate that the value changed } } /** * Sets the text literal value displayed in the control, and then converts the text to an appropriate value and stores it. This is a convenience method. * @param newText The new text literal value to display in the control and then convert and store as a value. * @see #setText(String) * @see #getConverter() * @see Converter#convertLiteral(Object) * @see #setValue(Object) * @throws ConversionException if the literal value cannot be converted. * @throws PropertyVetoException if the provided value is not valid or the change has otherwise been vetoed. */ public void setTextValue(final String newText) throws ConversionException, PropertyVetoException { setText(newText); //update the literal text of the component, which will in turn update the provisional text of the component final Converter converter = getConverter(); //get the component's converter final V value = converter.convertLiteral(newText); //convert the literal text value, throwing an exception if the value cannot be converted setValue(value); //store the value in the model, throwing an exception if the value is invalid } /** The content type of the value. */ private MediaType valueContentType; /** @return The content type of the value. */ public MediaType getValueContentType() { return valueContentType; } /** * Sets the content type of the value. This is a bound property. * @param newValueContentType The new value content type. * @throws NullPointerException if the given content type is null. * @throws IllegalArgumentException if the given content type is not a text content type. * @see #VALUE_CONTENT_TYPE_PROPERTY */ public void setValueContentType(final MediaType newValueContentType) { requireNonNull(newValueContentType, "Content type cannot be null."); if(valueContentType != newValueContentType) { //if the value is really changing final MediaType oldValueContentType = valueContentType; //get the old value if(!isText(newValueContentType)) { //if the new content type is not a text content type throw new IllegalArgumentException("Content type " + newValueContentType + " is not a text content type."); } valueContentType = newValueContentType; //actually change the value firePropertyChange(VALUE_CONTENT_TYPE_PROPERTY, oldValueContentType, newValueContentType); //indicate that the value changed } } /** The property change listener that updates the text in response to a property changing. */ private final PropertyChangeListener updateTextPropertyChangeListener = new PropertyChangeListener() { //create a listener to update the text in response to a property changing @Override public void propertyChange(final PropertyChangeEvent propertyChangeEvent) { //if the property changes updateText(); //update the text with the new value from the model } }; /** * Value class constructor with a default data model to represent a given type and a default converter. * @param valueClass The class indicating the type of value held in the model. * @throws NullPointerException if the given value class is null. */ public AbstractTextControl(final Class valueClass) { this(new DefaultValueModel(valueClass)); //construct the class with a default model } /** * Value model constructor with a default converter. * @param valueModel The component data model. * @throws NullPointerException if the given value model is null. */ public AbstractTextControl(final ValueModel valueModel) { this(valueModel, AbstractStringLiteralConverter.getInstance(valueModel.getValueClass())); //construct the class with a default converter } /** * Value model and converter constructor. * @param valueModel The component value model. * @param converter The converter for this component. * @throws NullPointerException if the given value model and/or converter is null. */ public AbstractTextControl(final ValueModel valueModel, final Converter converter) { super(new DefaultInfoModel(), valueModel, new DefaultEnableable()); //construct the parent class this.valueContentType = Text.PLAIN_MEDIA_TYPE; this.converter = requireNonNull(converter, "Converter cannot be null"); //save the converter updateText(); //initialize the text with the literal form of the initial model value addPropertyChangeListener(VALUE_PROPERTY, updateTextPropertyChangeListener); //listen for the value changing, and update the text in response getSession().addPropertyChangeListener(GuiseSession.LOCALE_PROPERTY, updateTextPropertyChangeListener); //listen for the session locale changing, in case the converter is locale-dependent TODO allow for unregistration to prevent memory leaks } //TODO important; remove this hack and make work with models, compensating for delayed listeners somehow; right now this results in the text being updated twice; once immediately when the value changes, and another when the value property change listener is fired (with delayed events) /*TODO fix; temporarily removing delayed events public void setValue(final V newValue) throws PropertyVetoException { super.setValue(newValue); updateText(); //update the text with the new value from the model } */ /** * Updates the component text with literal form of the given value. * @see Converter#convertValue(Object) * @see #getValue() * @see #setText(String) */ protected void updateText() { final Converter converter = getConverter(); //get the current converter try { final String newText = converter.convertValue(getValue()); //convert the value to text setText(newText); //convert the value to text } catch(final ConversionException conversionException) { //TODO fix better; decide what to do if there is an error here throw new AssertionError(conversionException); } } /** * {@inheritDoc} *

* This version performs no additional checks if the control is disabled. *

*/ @Override protected boolean determineValid() { if(!super.determineValid()) { //if we don't pass the default validity checks return false; //the component isn't valid } if(isEnabled()) { //if the control is enabled try { final V value = getConverter().convertLiteral(getProvisionalText()); //see if the provisional literal text can correctly be converted final Validator validator = getValidator(); //see if there is a validator installed if(validator != null) { //if there is a validator installed if(!validator.isValid(value)) { //if the value represented by the literal text is not valid return false; //the converted value isn't valid } } } catch(final ConversionException conversionException) { //if we can't convert the literal text to a value return false; //the literal isn't valid } } return true; //the values passed all validity checks } /** * {@inheritDoc} *

* This version checks to see if the provisional literal text can be converted to a valid value. If the provisional literal text cannot be converted, the * status is determined to be {@link Status#ERROR}. If the provisional literal text can be converted but the converted value is invalid, the status is * determined to be {@link Status#WARNING} unless the provisional text is the same as the literal text, in which case the status is determined to be * {@link Status#ERROR}. The default value, even if invalid, is considered valid. If the control is disabled no status is given. *

*/ @Override protected Status determineStatus() { Status status = super.determineStatus(); //do the defualt status checks if(status == null && isEnabled()) { //if no status is reported and the control is enabled, check the validity of the text try { final String provisionalText = getProvisionalText(); //get the provisional literal text final V value = getConverter().convertLiteral(provisionalText); //see if the provisional literal text can correctly be converted if(!Objects.equals(value, getDefaultValue())) { //don't count the value as invalid if it is equal to the default value final Validator validator = getValidator(); //see if there is a validator installed if(validator != null) { //if there is a validator installed if(!validator.isValid(value)) { //if the value represented by the provisional literal text is not valid if(Objects.equals(provisionalText, getText())) { //if the invalid provisional literal text is equal to the current literal test status = Status.ERROR; //the invalid value has already been committed, so mark it as an error } else { //if the invalid value isn't equal to the current value status = Status.WARNING; //the invalid value hasn't been committed; mark it as a warning } } } } } catch(final ConversionException conversionException) { //if we can't convert the provisional literal text to a value status = Status.ERROR; //conversion problems are errors } } return status; //return the determined status } /** * {@inheritDoc} *

* This version performs no additional checks if the control is disabled. *

*/ @Override public boolean validate() { V newValue = null; //we'll convert the literal value and store it here Notification newValueNotification = null; //if we have any problems converting the literal value, we'll store a notification here if(isEnabled()) { //if the control is enabled, make sure the value reflects the text value (i.e. make sure the value has been committed) try { newValue = getConverter().convertLiteral(getText()); //see if the literal text can correctly be converted setValue(newValue); //update the value, effectively committing the text (this will have no effect if the value isn't really changing) } catch(final ConversionException conversionException) { //if there is a conversion error newValueNotification = new Notification(conversionException); //indicate that there was a text conversion error } catch(final PropertyVetoException propertyVetoException) { //if there is a veto final Throwable cause = propertyVetoException.getCause(); //get the cause of the veto, if any newValueNotification = new Notification(cause != null ? cause : propertyVetoException); //indicate that there was a commit veto error } } super.validate(); //validate the super class if(isEnabled()) { //if the control is enabled if(newValueNotification != null) { //if we have a text error already setNotification(newValueNotification); //the text conversion error will override other errors (the value may have been invalid, but we want to show that the text's invalidity takes precedence) } else { //if we converted the text to a value with no problems, make sure its value is valid TODO is validation of the new value already taken are of above through setting the value? try { final Validator validator = getValidator(); //see if there is a validator installed if(validator != null) { //if there is a validator installed validator.validate(newValue); //validate the value represented by the literal text } } catch(final ValidationException validationException) { //if there is a validation error setNotification(new Notification(validationException)); //add notificaiton of this error to the component } } } return isValid(); //return the current valid state } /** * {@inheritDoc} *

* This version updates the text to match the new value. *

* @see #updateText() */ @Override public void reset() { super.reset(); //reset normally updateText(); //update the text to match the new value } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy