summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMatti Tahvonen <matti.tahvonen@itmill.com>2010-11-09 09:37:00 +0000
committerMatti Tahvonen <matti.tahvonen@itmill.com>2010-11-09 09:37:00 +0000
commit204748a6bdaeb46483ec1e225df43aeb10bdc99a (patch)
treecbc77a354571dd3cc6bad7807fd3ca5a79c0518e /src
parentd9759417af2859995f4c3eaf407edc904a3bfa0b (diff)
downloadvaadin-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.java74
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/VTextField.java141
-rw-r--r--src/com/vaadin/ui/TextField.java282
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;
+ }
+
}
}