diff options
author | Artur Signell <artur.signell@itmill.com> | 2010-12-07 11:55:09 +0000 |
---|---|---|
committer | Artur Signell <artur.signell@itmill.com> | 2010-12-07 11:55:09 +0000 |
commit | b76d5386b0e0ee8188f1094ec49edbef9e9edd7b (patch) | |
tree | 042d8d4347ce8c318025eeb50d7f8c8a823896fe /src/com/vaadin/ui/AbstractTextField.java | |
parent | 8ef4e5d2f73eb4c599f2f72a719cfa75dffe566e (diff) | |
download | vaadin-framework-b76d5386b0e0ee8188f1094ec49edbef9e9edd7b.tar.gz vaadin-framework-b76d5386b0e0ee8188f1094ec49edbef9e9edd7b.zip |
#3752 - Refactor TextFields
Made PasswordField, TextArea extend AbstractTextField instead of TextField
Moved commonalities "columns", "input prompt", "text change event" and "cursor position" to AbstractTextField
svn changeset:16354/svn branch:6.5
Diffstat (limited to 'src/com/vaadin/ui/AbstractTextField.java')
-rw-r--r-- | src/com/vaadin/ui/AbstractTextField.java | 520 |
1 files changed, 437 insertions, 83 deletions
diff --git a/src/com/vaadin/ui/AbstractTextField.java b/src/com/vaadin/ui/AbstractTextField.java index 7fe1306ea4..cd9d99de18 100644 --- a/src/com/vaadin/ui/AbstractTextField.java +++ b/src/com/vaadin/ui/AbstractTextField.java @@ -1,3 +1,7 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + package com.vaadin.ui; import java.text.Format; @@ -5,16 +9,24 @@ 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.terminal.PaintException; import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.gwt.client.ui.VTextField; -public abstract class AbstractTextField extends AbstractField { +public abstract class AbstractTextField extends AbstractField implements + BlurNotifier, FocusNotifier, TextChangeNotifier { /** * Value formatter used to format the string contents. */ + @Deprecated private Format format; /** @@ -31,7 +43,56 @@ public abstract class AbstractTextField extends AbstractField { */ private int maxLength = -1; - public AbstractTextField() { + /** + * Number of visible columns in the TextField. + */ + private int columns = 0; + + /** + * The prompt to display in an empty field. Null when disabled. + */ + private String inputPrompt = null; + + /** + * The text content when the last messages to the server was sent. + */ + 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 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(); } @@ -43,6 +104,16 @@ public abstract class AbstractTextField extends AbstractField { target.addAttribute("maxLength", getMaxLength()); } + // Adds the number of column and rows + final int columns = getColumns(); + if (columns != 0) { + target.addAttribute("cols", String.valueOf(columns)); + } + + if (getInputPrompt() != null) { + target.addAttribute("prompt", getInputPrompt()); + } + // Adds the content as variable String value = getFormattedValue(); if (value == null) { @@ -53,6 +124,20 @@ public abstract class AbstractTextField extends AbstractField { "Null values are not allowed if the null-representation is null"); } target.addVariable(this, "text", value); + + 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()); + } + } /** @@ -88,97 +173,69 @@ public abstract class AbstractTextField extends AbstractField { @Override public void changeVariables(Object source, Map<String, Object> variables) { + changingVariables = true; - super.changeVariables(source, 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"); + try { + super.changeVariables(source, variables); - // server side check for max length - if (getMaxLength() != -1 && newValue.length() > getMaxLength()) { - newValue = newValue.substring(0, getMaxLength()); - } - final String oldValue = getFormattedValue(); - if (newValue != null - && (oldValue == null || isNullSettingAllowed()) - && newValue.equals(getNullRepresentation())) { - newValue = null; + if (variables.containsKey(VTextField.VAR_CURSOR)) { + Integer object = (Integer) variables.get(VTextField.VAR_CURSOR); + lastKnownCursorPosition = object.intValue(); } - 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 (format != null || wasModified != isModified()) { - requestRepaint(); - } + + 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. + */ + textChangeEventPending = true; + handleInputEventTextChange(variables); } - } - if (variables.containsKey(FocusEvent.EVENT_ID)) { - fireEvent(new FocusEvent(this)); - } - if (variables.containsKey(BlurEvent.EVENT_ID)) { - fireEvent(new BlurEvent(this)); - } + // 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"); - /** - * Sets the height of the {@link TextField} instance. - * - * <p> - * Setting height for {@link TextField} also has a side-effect that puts - * {@link TextField} into multiline mode (aka "textarea"). Multiline mode - * can also be achieved by calling {@link #setRows(int)}. The height value - * overrides the number of rows set by {@link #setRows(int)}. - * <p> - * If you want to set height of single line {@link TextField}, call - * {@link #setRows(int)} with value 0 after setting the height. Setting rows - * to 0 resets the side-effect. - * - * @see com.vaadin.ui.AbstractComponent#setHeight(float, int) - */ - @Override - public void setHeight(float height, int unit) { - super.setHeight(height, unit); - if (height > 1 && this instanceof TextField) { - /* - * In html based terminals we most commonly want to make component - * to be textarea if height is defined. Setting row field above 0 - * will render component as textarea. - */ + // server side check for max length + if (getMaxLength() != -1 && newValue.length() > getMaxLength()) { + newValue = newValue.substring(0, getMaxLength()); + } + final String oldValue = getFormattedValue(); + 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 (format != null || 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; - ((TextField) this).setRows(2); } - } - /** - * Sets the height of the {@link TextField} instance. - * - * <p> - * Setting height for {@link TextField} also has a side-effect that puts - * {@link TextField} into multiline mode (aka "textarea"). Multiline mode - * can also be achieved by calling {@link #setRows(int)}. The height value - * overrides the number of rows set by {@link #setRows(int)}. - * <p> - * If you want to set height of single line {@link TextField}, call - * {@link #setRows(int)} with value 0 after setting the height. Setting rows - * to 0 resets the side-effect. - * - * @see com.vaadin.ui.AbstractComponent#setHeight(java.lang.String) - */ - @Override - public void setHeight(String height) { - // will call setHeight(float, int) the actually does the magic. Method - // is overridden just to document side-effects. - super.setHeight(height); } @Override @@ -328,6 +385,303 @@ public abstract class AbstractTextField extends AbstractField { 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 columns; + } + + /** + * 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; + } + this.columns = columns; + requestRepaint(); + } + + /** + * Gets the current input prompt. + * + * @see #setInputPrompt(String) + * @return the current input prompt, or null if not enabled + */ + public String getInputPrompt() { + return inputPrompt; + } + + /** + * 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) { + this.inputPrompt = inputPrompt; + requestRepaint(); + } + + /* ** Text Change Events ** */ + + private void firePendingTextChangeEvent() { + if (textChangeEventPending) { + fireEvent(new TextChangeEventImpl(this)); + textChangeEventPending = false; + } + } + + @Override + protected void setInternalValue(Object newValue) { + if (changingVariables) { + /* + * Fire text change event before value change event if change is + * coming from the client side. + */ + 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; + } + + if (textChangeEventPending) { + firePendingTextChangeEvent(); + } + } + super.setInternalValue(newValue); + } + + private void handleInputEventTextChange(Map<String, Object> 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. + * <p> + * 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. + * <p> + * This is the default mode. + */ + LAZY + } + + public void addListener(TextChangeListener listener) { + addListener(TextChangeListener.EVENT_ID, TextChangeEvent.class, + listener, TextChangeListener.EVENT_METHOD); + } + + 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. + * <p> + * 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. + * + * <p> + * 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; + } + public void addListener(FocusListener listener) { addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, FocusListener.focusMethod); |