/* @VaadinApache2LicenseForJavaFiles@ */ package com.vaadin.ui; import java.util.Map; import com.vaadin.event.FieldEvents.BlurEvent; import com.vaadin.event.FieldEvents.BlurListener; import com.vaadin.event.FieldEvents.BlurNotifier; import com.vaadin.event.FieldEvents.FocusEvent; import com.vaadin.event.FieldEvents.FocusListener; import com.vaadin.event.FieldEvents.FocusNotifier; import com.vaadin.event.FieldEvents.TextChangeEvent; import com.vaadin.event.FieldEvents.TextChangeListener; import com.vaadin.event.FieldEvents.TextChangeNotifier; import com.vaadin.shared.ui.textfield.AbstractTextFieldState; import com.vaadin.terminal.PaintException; import com.vaadin.terminal.PaintTarget; import com.vaadin.terminal.Vaadin6Component; import com.vaadin.terminal.gwt.client.ui.textfield.VTextField; public abstract class AbstractTextField extends AbstractField implements BlurNotifier, FocusNotifier, TextChangeNotifier, Vaadin6Component { /** * Null representation. */ private String nullRepresentation = "null"; /** * Is setting to null from non-null value allowed by setting with null * representation . */ private boolean nullSettingAllowed = false; /** * The text content when the last messages to the server was sent. Cleared * when value is changed. */ private String lastKnownTextContent; /** * The position of the cursor when the last message to the server was sent. */ private int lastKnownCursorPosition; /** * Flag indicating that a text change event is pending to be triggered. * Cleared by {@link #setInternalValue(Object)} and when the event is fired. */ private boolean textChangeEventPending; private boolean isFiringTextChangeEvent = false; private TextChangeEventMode textChangeEventMode = TextChangeEventMode.LAZY; private final int DEFAULT_TEXTCHANGE_TIMEOUT = 400; private int textChangeEventTimeout = DEFAULT_TEXTCHANGE_TIMEOUT; /** * Temporarily holds the new selection position. Cleared on paint. */ private int selectionPosition = -1; /** * Temporarily holds the new selection length. */ private int selectionLength; /** * Flag used to determine whether we are currently handling a state change * triggered by a user. Used to properly fire text change event before value * change event triggered by the client side. */ private boolean changingVariables; protected AbstractTextField() { super(); } @Override public AbstractTextFieldState getState() { return (AbstractTextFieldState) super.getState(); } @Override public void updateState() { super.updateState(); String value = getValue(); if (value == null) { value = getNullRepresentation(); } getState().setText(value); } @Override public void paintContent(PaintTarget target) throws PaintException { if (selectionPosition != -1) { target.addAttribute("selpos", selectionPosition); target.addAttribute("sellen", selectionLength); selectionPosition = -1; } if (hasListeners(TextChangeEvent.class)) { target.addAttribute(VTextField.ATTR_TEXTCHANGE_EVENTMODE, getTextChangeEventMode().toString()); target.addAttribute(VTextField.ATTR_TEXTCHANGE_TIMEOUT, getTextChangeTimeout()); if (lastKnownTextContent != null) { /* * The field has be repainted for some reason (e.g. caption, * size, stylename), but the value has not been changed since * the last text change event. Let the client side know about * the value the server side knows. Client side may then ignore * the actual value, depending on its state. */ target.addAttribute( VTextField.ATTR_NO_VALUE_CHANGE_BETWEEN_PAINTS, true); } } } @Override public void changeVariables(Object source, Map variables) { changingVariables = true; try { if (variables.containsKey(VTextField.VAR_CURSOR)) { Integer object = (Integer) variables.get(VTextField.VAR_CURSOR); lastKnownCursorPosition = object.intValue(); } if (variables.containsKey(VTextField.VAR_CUR_TEXT)) { /* * NOTE, we might want to develop this further so that on a * value change event the whole text content don't need to be * sent from the client to server. Just "commit" the value from * currentText to the value. */ handleInputEventTextChange(variables); } // Sets the text if (variables.containsKey("text") && !isReadOnly()) { // Only do the setting if the string representation of the value // has been updated String newValue = (String) variables.get("text"); // server side check for max length if (getMaxLength() != -1 && newValue.length() > getMaxLength()) { newValue = newValue.substring(0, getMaxLength()); } final String oldValue = getValue(); if (newValue != null && (oldValue == null || isNullSettingAllowed()) && newValue.equals(getNullRepresentation())) { newValue = null; } if (newValue != oldValue && (newValue == null || !newValue.equals(oldValue))) { boolean wasModified = isModified(); setValue(newValue, true); // If the modified status changes, or if we have a // formatter, repaint is needed after all. if (wasModified != isModified()) { requestRepaint(); } } } firePendingTextChangeEvent(); if (variables.containsKey(FocusEvent.EVENT_ID)) { fireEvent(new FocusEvent(this)); } if (variables.containsKey(BlurEvent.EVENT_ID)) { fireEvent(new BlurEvent(this)); } } finally { changingVariables = false; } } @Override public Class getType() { return String.class; } /** * Gets the null-string representation. * *

* The null-valued strings are represented on the user interface by * replacing the null value with this string. If the null representation is * set null (not 'null' string), painting null value throws exception. *

* *

* The default value is string 'null'. *

* * @return the String Textual representation for null strings. * @see TextField#isNullSettingAllowed() */ public String getNullRepresentation() { return nullRepresentation; } /** * Is setting nulls with null-string representation allowed. * *

* If this property is true, writing null-representation string to text * field always sets the field value to real null. If this property is * false, null setting is not made, but the null values are maintained. * Maintenance of null-values is made by only converting the textfield * contents to real null, if the text field matches the null-string * representation and the current value of the field is null. *

* *

* By default this setting is false *

* * @return boolean Should the null-string represenation be always converted * to null-values. * @see TextField#getNullRepresentation() */ public boolean isNullSettingAllowed() { return nullSettingAllowed; } /** * Sets the null-string representation. * *

* The null-valued strings are represented on the user interface by * replacing the null value with this string. If the null representation is * set null (not 'null' string), painting null value throws exception. *

* *

* The default value is string 'null' *

* * @param nullRepresentation * Textual representation for null strings. * @see TextField#setNullSettingAllowed(boolean) */ public void setNullRepresentation(String nullRepresentation) { this.nullRepresentation = nullRepresentation; requestRepaint(); } /** * Sets the null conversion mode. * *

* If this property is true, writing null-representation string to text * field always sets the field value to real null. If this property is * false, null setting is not made, but the null values are maintained. * Maintenance of null-values is made by only converting the textfield * contents to real null, if the text field matches the null-string * representation and the current value of the field is null. *

* *

* By default this setting is false. *

* * @param nullSettingAllowed * Should the null-string representation always be converted to * null-values. * @see TextField#getNullRepresentation() */ public void setNullSettingAllowed(boolean nullSettingAllowed) { this.nullSettingAllowed = nullSettingAllowed; requestRepaint(); } @Override protected boolean isEmpty() { return super.isEmpty() || getValue().length() == 0; } /** * Returns the maximum number of characters in the field. Value -1 is * considered unlimited. Terminal may however have some technical limits. * * @return the maxLength */ public int getMaxLength() { return getState().getMaxLength(); } /** * Sets the maximum number of characters in the field. Value -1 is * considered unlimited. Terminal may however have some technical limits. * * @param maxLength * the maxLength to set */ public void setMaxLength(int maxLength) { getState().setMaxLength(maxLength); requestRepaint(); } /** * Gets the number of columns in the editor. If the number of columns is set * 0, the actual number of displayed columns is determined implicitly by the * adapter. * * @return the number of columns in the editor. */ public int getColumns() { return getState().getColumns(); } /** * Sets the number of columns in the editor. If the number of columns is set * 0, the actual number of displayed columns is determined implicitly by the * adapter. * * @param columns * the number of columns to set. */ public void setColumns(int columns) { if (columns < 0) { columns = 0; } getState().setColumns(columns); requestRepaint(); } /** * Gets the current input prompt. * * @see #setInputPrompt(String) * @return the current input prompt, or null if not enabled */ public String getInputPrompt() { return getState().getInputPrompt(); } /** * Sets the input prompt - a textual prompt that is displayed when the field * would otherwise be empty, to prompt the user for input. * * @param inputPrompt */ public void setInputPrompt(String inputPrompt) { getState().setInputPrompt(inputPrompt); requestRepaint(); } /* ** Text Change Events ** */ private void firePendingTextChangeEvent() { if (textChangeEventPending && !isFiringTextChangeEvent) { isFiringTextChangeEvent = true; textChangeEventPending = false; try { fireEvent(new TextChangeEventImpl(this)); } finally { isFiringTextChangeEvent = false; } } } @Override protected void setInternalValue(String newValue) { if (changingVariables && !textChangeEventPending) { /* * TODO check for possible (minor?) issue (not tested) * * -field with e.g. PropertyFormatter. * * -TextChangeListener and it changes value. * * -if formatter again changes the value, do we get an extra * simulated text change event ? */ /* * Fire a "simulated" text change event before value change event if * change is coming from the client side. * * Iff there is both value change and textChangeEvent in same * variable burst, it is a text field in non immediate mode and the * text change event "flushed" queued value change event. In this * case textChangeEventPending flag is already on and text change * event will be fired after the value change event. */ if (newValue == null && lastKnownTextContent != null && !lastKnownTextContent.equals(getNullRepresentation())) { // Value was changed from something to null representation lastKnownTextContent = getNullRepresentation(); textChangeEventPending = true; } else if (newValue != null && !newValue.toString().equals(lastKnownTextContent)) { // Value was changed to something else than null representation lastKnownTextContent = newValue.toString(); textChangeEventPending = true; } firePendingTextChangeEvent(); } super.setInternalValue(newValue); } @Override public void setValue(Object newValue) throws ReadOnlyException { super.setValue(newValue); /* * Make sure w reset lastKnownTextContent field on value change. The * clearing must happen here as well because TextChangeListener can * revert the original value. Client must respect the value in this * case. AbstractField optimizes value change if the existing value is * reset. Also we need to force repaint if the flag is on. */ if (lastKnownTextContent != null) { lastKnownTextContent = null; requestRepaint(); } } private void handleInputEventTextChange(Map variables) { /* * TODO we could vastly optimize the communication of values by using * some sort of diffs instead of always sending the whole text content. * Also on value change events we could use the mechanism. */ String object = (String) variables.get(VTextField.VAR_CUR_TEXT); lastKnownTextContent = object; textChangeEventPending = true; } /** * Sets the mode how the TextField triggers {@link TextChangeEvent}s. * * @param inputEventMode * the new mode * * @see TextChangeEventMode */ public void setTextChangeEventMode(TextChangeEventMode inputEventMode) { textChangeEventMode = inputEventMode; requestRepaint(); } /** * @return the mode used to trigger {@link TextChangeEvent}s. */ public TextChangeEventMode getTextChangeEventMode() { return textChangeEventMode; } /** * Different modes how the TextField can trigger {@link TextChangeEvent}s. */ public enum TextChangeEventMode { /** * An event is triggered on each text content change, most commonly key * press events. */ EAGER, /** * Each text change event in the UI causes the event to be communicated * to the application after a timeout. The length of the timeout can be * controlled with {@link TextField#setInputEventTimeout(int)}. Only the * last input event is reported to the server side if several text * change events happen during the timeout. *

* In case of a {@link ValueChangeEvent} the schedule is not kept * strictly. Before a {@link ValueChangeEvent} a {@link TextChangeEvent} * is triggered if the text content has changed since the previous * TextChangeEvent regardless of the schedule. */ TIMEOUT, /** * An event is triggered when there is a pause of text modifications. * The length of the pause can be modified with * {@link TextField#setInputEventTimeout(int)}. Like with the * {@link #TIMEOUT} mode, an event is forced before * {@link ValueChangeEvent}s, even if the user did not keep a pause * while entering the text. *

* This is the default mode. */ LAZY } @Override public void addListener(TextChangeListener listener) { addListener(TextChangeListener.EVENT_ID, TextChangeEvent.class, listener, TextChangeListener.EVENT_METHOD); } @Override public void removeListener(TextChangeListener listener) { removeListener(TextChangeListener.EVENT_ID, TextChangeEvent.class, listener); } /** * The text change timeout modifies how often text change events are * communicated to the application when {@link #getTextChangeEventMode()} is * {@link TextChangeEventMode#LAZY} or {@link TextChangeEventMode#TIMEOUT}. * * * @see #getTextChangeEventMode() * * @param timeout * the timeout in milliseconds */ public void setTextChangeTimeout(int timeout) { textChangeEventTimeout = timeout; requestRepaint(); } /** * Gets the timeout used to fire {@link TextChangeEvent}s when the * {@link #getTextChangeEventMode()} is {@link TextChangeEventMode#LAZY} or * {@link TextChangeEventMode#TIMEOUT}. * * @return the timeout value in milliseconds */ public int getTextChangeTimeout() { return textChangeEventTimeout; } public class TextChangeEventImpl extends TextChangeEvent { private String curText; private int cursorPosition; private TextChangeEventImpl(final AbstractTextField tf) { super(tf); curText = tf.getCurrentTextContent(); cursorPosition = tf.getCursorPosition(); } @Override public AbstractTextField getComponent() { return (AbstractTextField) super.getComponent(); } @Override public String getText() { return curText; } @Override public int getCursorPosition() { return cursorPosition; } } /** * Gets the current (or the last known) text content in the field. *

* Note the text returned by this method is not necessary the same that is * returned by the {@link #getValue()} method. The value is updated when the * terminal fires a value change event via e.g. blurring the field or by * pressing enter. The value returned by this method is updated also on * {@link TextChangeEvent}s. Due to this high dependency to the terminal * implementation this method is (at least at this point) not published. * * @return the text which is currently displayed in the field. */ private String getCurrentTextContent() { if (lastKnownTextContent != null) { return lastKnownTextContent; } else { Object text = getValue(); if (text == null) { return getNullRepresentation(); } return text.toString(); } } /** * Selects all text in the field. * * @since 6.4 */ public void selectAll() { String text = getValue() == null ? "" : getValue().toString(); setSelectionRange(0, text.length()); } /** * Sets the range of text to be selected. * * As a side effect the field will become focused. * * @since 6.4 * * @param pos * the position of the first character to be selected * @param length * the number of characters to be selected */ public void setSelectionRange(int pos, int length) { selectionPosition = pos; selectionLength = length; focus(); requestRepaint(); } /** * Sets the cursor position in the field. As a side effect the field will * become focused. * * @since 6.4 * * @param pos * the position for the cursor * */ public void setCursorPosition(int pos) { setSelectionRange(pos, 0); lastKnownCursorPosition = pos; } /** * Returns the last known cursor position of the field. * *

* Note that due to the client server nature or the GWT terminal, Vaadin * cannot provide the exact value of the cursor position in most situations. * The value is updated only when the client side terminal communicates to * TextField, like on {@link ValueChangeEvent}s and {@link TextChangeEvent} * s. This may change later if a deep push integration is built to Vaadin. * * @return the cursor position */ public int getCursorPosition() { return lastKnownCursorPosition; } @Override public void addListener(FocusListener listener) { addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, FocusListener.focusMethod); } @Override public void removeListener(FocusListener listener) { removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); } @Override public void addListener(BlurListener listener) { addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, BlurListener.blurMethod); } @Override public void removeListener(BlurListener listener) { removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); } }