diff options
author | Matti Tahvonen <matti.tahvonen@itmill.com> | 2010-11-09 09:37:00 +0000 |
---|---|---|
committer | Matti Tahvonen <matti.tahvonen@itmill.com> | 2010-11-09 09:37:00 +0000 |
commit | 204748a6bdaeb46483ec1e225df43aeb10bdc99a (patch) | |
tree | cbc77a354571dd3cc6bad7807fd3ca5a79c0518e /src | |
parent | d9759417af2859995f4c3eaf407edc904a3bfa0b (diff) | |
download | vaadin-framework-204748a6bdaeb46483ec1e225df43aeb10bdc99a.tar.gz vaadin-framework-204748a6bdaeb46483ec1e225df43aeb10bdc99a.zip |
fixes #2387: added TextChangeEvent that can be used to listen text changes while the user is typing or otherwise modifying the text
svn changeset:15923/svn branch:6.5
Diffstat (limited to 'src')
-rw-r--r-- | src/com/vaadin/event/FieldEvents.java | 74 | ||||
-rw-r--r-- | src/com/vaadin/terminal/gwt/client/ui/VTextField.java | 141 | ||||
-rw-r--r-- | src/com/vaadin/ui/TextField.java | 282 |
3 files changed, 482 insertions, 15 deletions
diff --git a/src/com/vaadin/event/FieldEvents.java b/src/com/vaadin/event/FieldEvents.java index 202b079536..818341a5d1 100644 --- a/src/com/vaadin/event/FieldEvents.java +++ b/src/com/vaadin/event/FieldEvents.java @@ -11,6 +11,8 @@ import com.vaadin.terminal.gwt.client.EventId; import com.vaadin.tools.ReflectTools;
import com.vaadin.ui.Component;
import com.vaadin.ui.Field;
+import com.vaadin.ui.Field.ValueChangeEvent;
+import com.vaadin.ui.TextField;
/**
* Interface that serves as a wrapper for {@link Field} related events.
@@ -173,4 +175,76 @@ public interface FieldEvents { public void blur(BlurEvent event);
}
+ /**
+ * TextChangeEvents are fired when the user is editing the text content of a
+ * field. Most commonly text change events are triggered by typing text with
+ * keyboard, but e.g. pasting content from clip board to a text field also
+ * triggers an event.
+ * <p>
+ * TextChangeEvents differ from {@link ValueChangeEvent}s so that they are
+ * triggered repeatedly while the end user is filling the field.
+ * ValueChangeEvents are not fired until the user for example hits enter or
+ * focuses another field. Also note the difference that TextChangeEvents are
+ * only fired if the change is triggered from the user, while
+ * ValueChangeEvents are also fired if the field value is set by the
+ * application code.
+ * <p>
+ * The {@link TextChangeNotifier}s implementation may decide when exactly
+ * TextChangeEvents are fired. TextChangeEvents are not necessary fire for
+ * example on each key press, but buffered with a small delay. The
+ * {@link TextField} component supports different modes for triggering
+ * TextChangeEvents.
+ *
+ * @see TextChangeListener
+ * @see TextChangeNotifier
+ * @see TextField#setTextChangeEventMode(com.vaadin.ui.TextField.TextChangeEventMode)
+ * @since 6.5
+ */
+ public static abstract class TextChangeEvent extends Component.Event {
+ public TextChangeEvent(Component source) {
+ super(source);
+ }
+
+ /**
+ * @return the text content of the field after the
+ * {@link TextChangeEvent}
+ */
+ public abstract String getCurrentTextContent();
+
+ /**
+ * @return the cursor position during after the {@link TextChangeEvent}
+ */
+ public abstract int getCursorPosition();
+ }
+
+ /**
+ * A listener for {@link TextChangeEvent}s.
+ *
+ * @since 6.5
+ */
+ public interface TextChangeListener extends ComponentEventListener {
+
+ public static String EVENT_ID = "ie";
+ public static Method EVENT_METHOD = ReflectTools.findMethod(
+ TextChangeListener.class, "textChange", TextChangeEvent.class);
+
+ /**
+ * This method is called repeatedly while the text is edited by a user.
+ *
+ * @param event
+ * the event providing details of the text change
+ */
+ public void textChange(TextChangeEvent event);
+ }
+
+ /**
+ * An interface implemented by a {@link Field} supporting
+ * {@link TextChangeEvent}s. An example a {@link TextField} supports
+ * {@link TextChangeListener}s.
+ */
+ public interface TextChangeNotifier extends Serializable {
+ public void addListener(TextChangeListener listener);
+
+ public void removeListener(TextChangeListener listener);
+ }
}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/VTextField.java b/src/com/vaadin/terminal/gwt/client/ui/VTextField.java index 3311eef1c5..4c6f06c992 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/VTextField.java +++ b/src/com/vaadin/terminal/gwt/client/ui/VTextField.java @@ -15,6 +15,7 @@ import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.DeferredCommand; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.TextBoxBase; import com.vaadin.terminal.gwt.client.ApplicationConnection; import com.vaadin.terminal.gwt.client.BrowserInfo; @@ -34,6 +35,7 @@ import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.BeforeShortcutAct public class VTextField extends TextBoxBase implements Paintable, Field, ChangeHandler, FocusHandler, BlurHandler, BeforeShortcutActionListener { + public static final String VAR_CUR_TEXT = "curText"; /** * The input node CSS classname. */ @@ -56,7 +58,11 @@ public class VTextField extends TextBoxBase implements Paintable, Field, private static final String CLASSNAME_PROMPT = "prompt"; private static final String ATTR_INPUTPROMPT = "prompt"; - private static final String VAR_CURSOR = "c"; + public static final String ATTR_TEXTCHANGE_TIMEOUT = "iet"; + public static final String VAR_CURSOR = "c"; + public static final String ATTR_TEXTCHANGE_EVENTMODE = "iem"; + private static final String TEXTCHANGE_MODE_EAGER = "EAGER"; + private static final String TEXTCHANGE_MODE_TIMEOUT = "TIMEOUT"; private String inputPrompt = null; private boolean prompting = false; @@ -81,12 +87,83 @@ public class VTextField extends TextBoxBase implements Paintable, Field, sinkEvents(VTooltip.TOOLTIP_EVENTS); } + /* + * TODO When GWT adds ONCUT, add it there and remove workaround. See + * http://code.google.com/p/google-web-toolkit/issues/detail?id=4030 + * + * Also note that the cut/paste are not totally crossbrowsers compatible. + * E.g. in Opera mac works via context menu, but on via File->Paste/Cut. + * Opera might need the polling method for 100% working textchanceevents. + * Eager polling for a change is bit dum and heavy operation, so I guess we + * should first try to survive without. + */ + private static final int TEXTCHANGE_EVENTS = Event.ONPASTE + | Event.KEYEVENTS | Event.ONMOUSEUP; + @Override public void onBrowserEvent(Event event) { super.onBrowserEvent(event); if (client != null) { client.handleTooltipEvent(event, this); } + + if (listenTextChangeEvents + && (event.getTypeInt() & TEXTCHANGE_EVENTS) == event + .getTypeInt()) { + deferTextChangeEvent(); + } + + } + + /* + * TODO optimize this so that only changes are sent + make the value change + * event just a flag that moves the current text to value + */ + private String lastTextChangeString = null; + + private String getLastCommunicatedString() { + return lastTextChangeString; + } + + private boolean communicateTextValueToServer() { + String text = getText(); + if (!text.equals(getLastCommunicatedString())) { + lastTextChangeString = text; + client.updateVariable(id, VAR_CUR_TEXT, text, true); + return true; + } + return false; + } + + private Timer textChangeEventTrigger = new Timer() { + + @Override + public void run() { + updateCursorPosition(); + boolean textChanged = communicateTextValueToServer(); + if (textChanged) { + client.sendPendingVariableChanges(); + } + scheduled = false; + } + }; + private boolean scheduled = false; + private boolean listenTextChangeEvents; + private String textChangeEventMode; + private int textChangeEventTimeout; + + private void deferTextChangeEvent() { + if (textChangeEventMode.equals(TEXTCHANGE_MODE_TIMEOUT) && scheduled) { + return; + } else { + textChangeEventTrigger.cancel(); + } + textChangeEventTrigger.schedule(getInputEventTimeout()); + scheduled = true; + } + + private int getInputEventTimeout() { + return textChangeEventTimeout; } @Override @@ -127,6 +204,20 @@ public class VTextField extends TextBoxBase implements Paintable, Field, immediate = uidl.getBooleanAttribute("immediate"); + listenTextChangeEvents = client.hasEventListeners(this, "ie"); + if (listenTextChangeEvents) { + textChangeEventMode = uidl + .getStringAttribute(ATTR_TEXTCHANGE_EVENTMODE); + if (textChangeEventMode.equals(TEXTCHANGE_MODE_EAGER)) { + textChangeEventTimeout = 1; + } else { + textChangeEventTimeout = uidl + .getIntAttribute(ATTR_TEXTCHANGE_TIMEOUT); + } + sinkEvents(TEXTCHANGE_EVENTS); + attachCutEventListener(getElement()); + } + if (uidl.hasAttribute("cols")) { setColumns(new Integer(uidl.getStringAttribute("cols")).intValue()); } @@ -154,7 +245,13 @@ public class VTextField extends TextBoxBase implements Paintable, Field, fieldValue = text; removeStyleDependentName(CLASSNAME_PROMPT); } - setText(fieldValue); + /* + * Avoid resetting the old value. Prevents cursor flickering + * which then again happens due to this Gecko hack. + */ + if (!getText().equals(fieldValue)) { + setText(fieldValue); + } } }); } else { @@ -169,7 +266,7 @@ public class VTextField extends TextBoxBase implements Paintable, Field, setText(fieldValue); } - valueBeforeEdit = uidl.getStringVariable("text"); + lastTextChangeString = valueBeforeEdit = uidl.getStringVariable("text"); if (uidl.hasAttribute("selpos")) { final int pos = uidl.getIntAttribute("selpos"); @@ -185,6 +282,39 @@ public class VTextField extends TextBoxBase implements Paintable, Field, } } + protected void onCut() { + if (listenTextChangeEvents) { + deferTextChangeEvent(); + } + } + + protected native void attachCutEventListener(Element el) + /*-{ + var me = this; + el.oncut = function() { + me.@com.vaadin.terminal.gwt.client.ui.VTextField::onCut()(); + }; + }-*/; + + protected native void detachCutEventListener(Element el) + /*-{ + el.oncut = null; + }-*/; + + @Override + protected void onDetach() { + super.onDetach(); + detachCutEventListener(getElement()); + } + + @Override + protected void onAttach() { + super.onAttach(); + if (listenTextChangeEvents) { + detachCutEventListener(getElement()); + } + } + private void setMaxLength(int newMaxLength) { if (newMaxLength > 0) { maxLength = newMaxLength; @@ -245,6 +375,11 @@ public class VTextField extends TextBoxBase implements Paintable, Field, updateCursorPosition(); if (sendBlurEvent || sendValueChange) { + /* + * Avoid sending text change event as we will simulate it on the + * server side before value change events. + */ + textChangeEventTrigger.cancel(); client.sendPendingVariableChanges(); } } diff --git a/src/com/vaadin/ui/TextField.java b/src/com/vaadin/ui/TextField.java index 81e586a505..03693a1160 100644 --- a/src/com/vaadin/ui/TextField.java +++ b/src/com/vaadin/ui/TextField.java @@ -4,8 +4,12 @@ package com.vaadin.ui; +import java.util.Map; + import com.vaadin.data.Property; import com.vaadin.event.FieldEvents; +import com.vaadin.event.FieldEvents.TextChangeEvent; +import com.vaadin.event.FieldEvents.TextChangeListener; import com.vaadin.terminal.PaintException; import com.vaadin.terminal.PaintTarget; import com.vaadin.terminal.gwt.client.ui.VTextField; @@ -34,7 +38,10 @@ import com.vaadin.ui.ClientWidget.LoadStyle; @SuppressWarnings("serial") @ClientWidget(value = VTextField.class, loadStyle = LoadStyle.EAGER) public class TextField extends AbstractTextField implements - FieldEvents.BlurNotifier, FieldEvents.FocusNotifier { + FieldEvents.BlurNotifier, FieldEvents.FocusNotifier, + FieldEvents.TextChangeNotifier { + + private static final String VAR_TEXT_CONTENT_DIFF = "tcd"; /** * Tells if input is used to enter sensitive information that is not echoed @@ -63,6 +70,29 @@ public class TextField extends AbstractTextField implements private int selectionPosition = -1; private int selectionLength; + private int lastKnownCursorPosition; + + private String lastKnownTextContent; + + /** + * 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; + + /** + * Flag used to determine wheter 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; + + private TextChangeEventMode textChangeEventMode = TextChangeEventMode.LAZY; + + private final int DEFAULT_TEXTCHANGE_TIMEOUT = 400; + + private int textChangeEventTimeout = DEFAULT_TEXTCHANGE_TIMEOUT; + /** * Constructs an empty <code>TextField</code> with no caption. */ @@ -130,7 +160,7 @@ public class TextField extends AbstractTextField implements * @return <code>true</code> if the field is used to enter secret * information, <code>false</code> otherwise. * - * @deprecated use {@link PasswordField} instead + * @deprecated in 6.5 use {@link PasswordField} instead */ @Deprecated public boolean isSecret() { @@ -144,7 +174,7 @@ public class TextField extends AbstractTextField implements * @param secret * the value specifying if the field is used to enter secret * information. - * @deprecated use {@link PasswordField} instead + * @deprecated in 6.5 use {@link PasswordField} instead */ @Deprecated public void setSecret(boolean secret) { @@ -182,8 +212,15 @@ public class TextField extends AbstractTextField implements 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()); + } super.paintContent(target); + } /** @@ -208,9 +245,9 @@ public class TextField extends AbstractTextField implements * @param rows * the number of rows for this editor. * - * @deprecated use {@link TextArea} component and the same method there. - * This method will be removed from TextField that is to be used - * for one line text input only in the next versions. + * @deprecated in 6.5 use {@link TextArea} component and the same method + * there. This method will be removed from TextField that is to + * be used for one line text input only in the next versions. */ @Deprecated public void setRows(int rows) { @@ -228,9 +265,9 @@ public class TextField extends AbstractTextField implements * * @return <code>true</code> if the component is in the word-wrap mode, * <code>false</code> if not. - * @deprecated use {@link TextArea} component and the same method there. - * This method will be removed from TextField that is to be used - * for one line text input only in the next versions. + * @deprecated in 6.5 use {@link TextArea} component and the same method + * there. This method will be removed from TextField that is to + * be used for one line text input only in the next versions. */ @Deprecated public boolean isWordwrap() { @@ -244,9 +281,9 @@ public class TextField extends AbstractTextField implements * the boolean value specifying if the editor should be in * word-wrap mode after the call or not. * - * @deprecated use {@link TextArea} component and the same method there. - * This method will be removed from TextField that is to be used - * for one line text input only in the next versions. + * @deprecated in 6.5 use {@link TextArea} component and the same method + * there. This method will be removed from TextField that is to + * be used for one line text input only in the next versions. */ @Deprecated public void setWordwrap(boolean wordwrap) { @@ -344,6 +381,227 @@ public class TextField extends AbstractTextField implements * */ 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; + } + + /** + * 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 what 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(); + } + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + if (variables.containsKey(VTextField.VAR_CURSOR)) { + Integer object = (Integer) variables.get(VTextField.VAR_CURSOR); + lastKnownCursorPosition = object.intValue(); + textChangeEventPending = true; + } + 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); + } + + changingVariables = true; + try { + super.changeVariables(source, variables); + } finally { + changingVariables = false; + } + firePendingTextChangeEvent(); + } + + private void firePendingTextChangeEvent() { + if (textChangeEventPending) { + fireEvent(new TextChangeEventImpl(this)); + textChangeEventPending = false; + } + } + + @Override + protected void setInternalValue(Object newValue) { + if (changingVariables + && !newValue.toString().equals(lastKnownTextContent)) { + /* + * Fire text change event before value change event if change is + * coming from the client side. + */ + lastKnownTextContent = newValue.toString(); + textChangeEventPending = true; + 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; + } + + /* ** Text Change Events ** */ + + /** + * Sets the mode how the TextField triggers {@link TextChangeEvent}s. + * + * @param inputEventMode + * the new mode + * + * @see TextChangeEventMode + */ + public void setTextChangeEventMode(TextChangeEventMode inputEventMode) { + textChangeEventMode = inputEventMode; + } + + /** + * @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; + } + + /** + * 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 TextField tf) { + super(tf); + curText = tf.getCurrentTextContent(); + cursorPosition = tf.getCursorPosition(); + } + + @Override + public TextField getComponent() { + return (TextField) super.getComponent(); + } + + @Override + public String getCurrentTextContent() { + return curText; + } + + @Override + public int getCursorPosition() { + return cursorPosition; + } + } } |