/******************************************************************************* * Copyright (c) 2006-2013 The RCP Company and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * The RCP Company - initial API and implementation *******************************************************************************/ package com.rcpcompany.uibindings.internal.observables; import org.eclipse.core.databinding.observable.Diffs; import org.eclipse.core.databinding.observable.Realm; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.emf.common.notify.Adapter; import org.eclipse.emf.common.notify.Notification; import org.eclipse.emf.common.notify.impl.AdapterImpl; import org.eclipse.jface.databinding.swt.SWTObservables; import org.eclipse.jface.util.Util; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CCombo; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Text; import com.rcpcompany.uibindings.IBinding; import com.rcpcompany.uibindings.IBindingContext; import com.rcpcompany.uibindings.IManager; import com.rcpcompany.uibindings.IUIBindingsPackage; import com.rcpcompany.uibindings.TextCommitStrategy; import com.rcpcompany.utils.logging.LogUtils; /** * {@link IObservableValue} for a number of widgets: {@link Text}, {@link StyledText}, {@link Combo} * and {@link CCombo}. * <p> * It handles all the possible change strategies as defined in * {@link IBindingContext#getTextCommitStrategyCalculated()} . * <p> * Loosely based on classes from the data binding framework. */ public class TextObservableValue extends AbstractSWTObservableValue implements IUpdatableObservable, IDelayedChangeObservable { /** * This interface is used to allow a widget to inform the {@link IObservableValue} that the * value for the widget has been extraordinarily updated. * <p> * The first example is {@link Text} widgets with an associated browse dialog button - when the * browse dialog is closed the interface is used to let the observable know the value has * changed. This is needed when the {@link TextCommitStrategy#ON_FOCUS_OUT} strategy is used. */ public interface IWidgetUpdated { /** * Adds a listener to be called when the value changes. * <p> * The <code>event</code> argument is not needed and will be null. * * @param listener the listener to add */ void addWidgetUpdatedListener(Listener listener); /** * Removes a listener previously added with * {@link IWidgetUpdated#addWidgetUpdatedListener(Listener)}. * * @param listener the listener to remove */ void removeWidgetUpdatedListener(Listener listener); }; /** * The observed widget. */ private final Control myControl; /** * The adapter for the widget. */ private final ControlAdapter myAdapter; /** * <code>true</code> while the widget is updated via {@link #setValue(Object)}. Used to prevent * reporting the changes. */ private boolean updating = false; /** * The last value reported via a change event - also used for ESCAPE. */ private String myOldValue; /** * <code>true</code> if this observable will use ENTER to force the value. */ private final boolean myHandleENTER; /** * The expect value of the text widget at the next modify event - if non-<code>null</code>. * <p> * Used to work around a problem with changes to Text(MULTI) widgets: * <ul> * <li>set text to "a"</li> * <li>select text</li> * <li>enter "b"</li> * </ul> * This results in two modify events: "a" to "" and "" to "b". Though only one verify event... * <p> * Also see <a href="http://jira.marintek.sintef.no/jira/browse/SIMA-623">SIMA-623: Text widget * with Focus out commit strategy, seems to commit early.</a> */ private String myNextModifyValue = null; private final Listener myControlListener = new Listener() { @Override public void handleEvent(Event event) { handleControlEvent(event); } }; /** * Handle all SWT events on the Text (or whatever) control. * * @param event the event */ protected void handleControlEvent(Event event) { // LogUtils.debug(this, "Text='" + myAdapter.getText(myControl) + "'\n" + // ToStringUtils.toString(event)); if (updating) return; /** * Used to provoke an immediate commit of the current value if it has changed */ if (event == null) { forceUpdateValue(); return; } /* * Special handling for some of the event types */ switch (event.type) { case SWT.KeyDown: /* * Handle ENTER and ESCAPE */ switch (event.keyCode) { case SWT.CR: if (myHandleENTER) { forceUpdateValue(); /* * Cannot eat the ENTER as it is also used when the binding is put into a cell * of a viewer. * * event.doit = false; */ } break; case SWT.ESC: doSetValue(myOldValue); /* * Cannot eat the ENTER as it is may also used to shut down a dialog, or whatever * * event.doit = false; */ break; default: break; } return; case SWT.Verify: /* * Predict the new value */ final String v = myAdapter.getText(myControl); myNextModifyValue = v.substring(0, event.start) + event.text + v.substring(event.end); return; case SWT.Modify: if (myNextModifyValue != null && !myAdapter.getText(myControl).equals(myNextModifyValue)) return; myNextModifyValue = null; break; case SWT.FocusOut: forceUpdateValue(); break; case SWT.Selection: forceUpdateValue(); break; default: break; } /* * Handling of the different strategies */ switch (myStrategy) { case ON_MODIFY: if (event.type != SWT.Modify) return; break; case ON_FOCUS_OUT: // if (event.type != SWT.FocusOut) // return; break; case ON_MODIFY_DELAY: if (event.type != SWT.Modify) return; break; default: LogUtils.error(this, "Unknown strategy " + myStrategy); } updateValue(event.type == SWT.Modify, false); } /** * The binding context of this observable... */ private IBindingContext myContext; /** * The current strategy of the observable. */ private TextCommitStrategy myStrategy; /** * Used to suppress the events provoked by strategy changes. */ private boolean mySuppressStrategyChanges = false; /** * <code>true</code> when stale has been fired. */ private boolean isStale = false; private final boolean myHandleSelection; @Override public boolean isStale() { super.isStale(); return isStale; } /** * Returns the current strategy of the observable. * * @return the strategy */ public TextCommitStrategy getStrategy() { return myStrategy; } /** * Sets the wanted strategy of the observable. * * @param strategy the strategy */ public void setStrategy(TextCommitStrategy strategy) { myStrategy = strategy; if (!mySuppressStrategyChanges) { forceUpdateValue(); } } /** * Constructs and returns a new observable for the control. Private! * * @param realm the wanted realm or <code>null</code> * @param control the control in question * @param adapter adapter for the control * @param handleENTER whether ENTER is handled * @param handleSelection TODO */ private TextObservableValue(final Realm realm, Control control, ControlAdapter adapter, boolean handleENTER, boolean handleSelection) { super(realm, control); myControl = control; myAdapter = adapter; myOldValue = adapter.getText(control); myHandleENTER = handleENTER; myHandleSelection = handleSelection; } /** * Constructs and returns a new observable for the Text widget. * * @param text the Text widget */ public TextObservableValue(final Text text) { this(SWTObservables.getRealm(text.getDisplay()), text); } /** * Constructs and returns a new observable for the Text widget. * * @param text the Text widget * @param updater an optional updater interface */ public TextObservableValue(final Text text, IWidgetUpdated updater) { this(SWTObservables.getRealm(text.getDisplay()), text); if (updater != null) { updater.addWidgetUpdatedListener(myControlListener); } } /** * Constructs and returns a new observable for the {@link Text} widget. * * @param realm the wanted realm or <code>null</code> * @param text the Text widget */ public TextObservableValue(final Realm realm, Text text) { this(realm, text, TEXT_ADAPTER, (text.getStyle() & SWT.SINGLE) == SWT.SINGLE, false); } /** * Constructs and returns a new observable for the StyledText widget. * * @param text the StyledText widget */ public TextObservableValue(final StyledText text) { this(SWTObservables.getRealm(text.getDisplay()), text); } /** * Constructs and returns a new observable for the {@link StyledText} widget. * * @param realm the wanted realm or <code>null</code> * @param text the Text widget */ public TextObservableValue(final Realm realm, StyledText text) { this(realm, text, STYLEDTEXT_ADAPTER, false, false); } /** * Constructs and returns a new observable for the Combo widget. * * @param combo the Combo widget */ public TextObservableValue(final Combo combo) { this(SWTObservables.getRealm(combo.getDisplay()), combo); } /** * Constructs and returns a new observable for the {@link Combo} widget. * * @param realm the wanted realm or <code>null</code> * @param combo the Text widget */ public TextObservableValue(final Realm realm, Combo combo) { this(realm, combo, COMBO_ADAPTER, true, true); } /** * Constructs and returns a new observable for the CCombo widget. * * @param combo the CCombo widget */ public TextObservableValue(final CCombo combo) { this(SWTObservables.getRealm(combo.getDisplay()), combo); } /** * Constructs and returns a new observable for the {@link CCombo} widget. * * @param realm the wanted realm or <code>null</code> * @param combo the Text widget */ public TextObservableValue(final Realm realm, CCombo combo) { this(realm, combo, CCOMBO_ADAPTER, true, true); } @Override public synchronized void dispose() { if (hasListeners()) { lastListenerRemoved(); } super.dispose(); } @Override protected void firstListenerAdded() { try { mySuppressStrategyChanges = true; super.firstListenerAdded(); final IBinding binding = IBindingContext.Factory.getBindingForWidget(myControl); if (binding != null) { myContext = binding.getContext(); setStrategy(myContext.getTextCommitStrategyCalculated()); myContext.eAdapters().add(myStrategyListener); } else { setStrategy(IManager.Factory.getManager().getTextCommitStrategy()); IManager.Factory.getManager().eAdapters().add(myStrategyListener); } myControl.addListener(SWT.FocusOut, myControlListener); myControl.addListener(SWT.Modify, myControlListener); myControl.addListener(SWT.KeyDown, myControlListener); myControl.addListener(SWT.Verify, myControlListener); myControl.addListener(SWT.FocusOut, myControlListener); if (myHandleSelection) { myControl.addListener(SWT.Selection, myControlListener); } // LogUtils.debug(this, "" + hashCode()); } finally { mySuppressStrategyChanges = false; } } @Override protected void lastListenerRemoved() { super.lastListenerRemoved(); if (!myControl.isDisposed()) { myControl.removeListener(SWT.FocusOut, myControlListener); myControl.removeListener(SWT.Modify, myControlListener); myControl.removeListener(SWT.KeyDown, myControlListener); myControl.removeListener(SWT.Verify, myControlListener); myControl.removeListener(SWT.FocusOut, myControlListener); if (myHandleSelection) { myControl.removeListener(SWT.Selection, myControlListener); } } if (myContext != null) { myContext.eAdapters().remove(myStrategyListener); } else { IManager.Factory.getManager().eAdapters().remove(myStrategyListener); } // LogUtils.debug(this, "" + hashCode()); } @Override public Object doGetValue() { return myAdapter.getText(myControl); } @Override public Object getValueType() { return String.class; } private ValueUpdater myUpdater = null; /** * Updates the value of this observable. * * @param isModify the value is updated because of a modify event * @param force the update is immediately - i.e. it is not delayed */ protected void updateValue(boolean isModify, boolean force) { final String newValue = myAdapter.getText(myControl); cancelScheduledUpdate(); // if any if (isModify && myStrategy != TextCommitStrategy.ON_MODIFY && !force) { fireDelayedChange(); if (!isStale) { isStale = true; fireStale(); } scheduleUpdate(); return; } if (!Util.equals(myOldValue, newValue)) { isStale = false; fireValueChange(Diffs.createValueDiff(myOldValue, myOldValue = newValue)); } } @Override protected void doSetValue(Object value) { updating = true; try { // Principle of least surprise: setValue overrides any pending // update from observable. isStale = false; cancelScheduledUpdate(); if (value == null) { value = ""; } myAdapter.setText(myControl, value.toString()); if (!Util.equals(myOldValue, value)) { fireValueChange(Diffs.createValueDiff(myOldValue, value)); myOldValue = value.toString(); } } finally { updating = false; } } @Override public void forceUpdateValue() { updateValue(false, true); } /** * Schedules a delayed update of this observable. */ protected void scheduleUpdate() { if (myStrategy != TextCommitStrategy.ON_MODIFY_DELAY) return; myUpdater = new ValueUpdater(); myControl.getDisplay().timerExec(IManager.Factory.getManager().getTextCommitStrategyDelay(), myUpdater); } /** * Cancels an already scheduled update of this observable - if any. */ protected void cancelScheduledUpdate() { if (myUpdater != null) { myUpdater.cancel(); myUpdater = null; } } /** * Private class used to force an delayed update. */ protected class ValueUpdater implements Runnable { private boolean cancel = false; void cancel() { cancel = true; } @Override public void run() { if (isDisposed()) return; if (!cancel) { forceUpdateValue(); } } } /** * Adapter for the preferences that forces an update of the current value. */ private final Adapter myStrategyListener = new AdapterImpl() { @Override public void notifyChanged(Notification msg) { if (msg.isTouch()) return; if (msg.getFeature() == IUIBindingsPackage.Literals.BINDING_CONTEXT__TEXT_COMMIT_STRATEGY_CALCULATED) { setStrategy(myContext.getTextCommitStrategyCalculated()); } if (msg.getFeature() == IUIBindingsPackage.Literals.MANAGER__TEXT_COMMIT_STRATEGY) { setStrategy(IManager.Factory.getManager().getTextCommitStrategy()); } } }; @Override public void addDelayedChangeListener(IDelayedChangeListener listener) { addListener(DelayedChangeEvent.TYPE, listener); } @Override public void removeDelayedChangeListener(IDelayedChangeListener listener) { removeListener(DelayedChangeEvent.TYPE, listener); } /** * Fires a {@link DelayedChangeEvent} to all {@link IDelayedChangeListener relevant listeners}. */ protected void fireDelayedChange() { checkRealm(); fireEvent(new DelayedChangeEvent(this)); } /** * Interface used to iron out differences between supported widget types. */ protected interface ControlAdapter { /** * Returns the text of the widget. * * @param w the widget * @return the current text */ String getText(Control w); /** * Sets the text of the widget. * * @param w the widget * @param newText the new text */ void setText(Control w, String newText); } /** * Access methods for {@link Text} widgets. */ protected static final ControlAdapter TEXT_ADAPTER = new ControlAdapter() { @Override public String getText(Control w) { return ((Text) w).getText(); } @Override public void setText(Control w, String newText) { ((Text) w).setText(newText); } }; /** * Access methods for {@link StyledText} widgets. */ protected static final ControlAdapter STYLEDTEXT_ADAPTER = new ControlAdapter() { @Override public String getText(Control w) { return ((StyledText) w).getText(); } @Override public void setText(Control w, String newText) { ((StyledText) w).setText(newText); } }; /** * Access methods for {@link Combo} widgets. */ protected static final ControlAdapter COMBO_ADAPTER = new ControlAdapter() { @Override public String getText(Control w) { return ((Combo) w).getText(); } @Override public void setText(Control w, String newText) { ((Combo) w).setText(newText); } }; /** * Access methods for {@link CCombo} widgets. */ protected static final ControlAdapter CCOMBO_ADAPTER = new ControlAdapter() { @Override public String getText(Control w) { return ((CCombo) w).getText(); } @Override public void setText(Control w, String newText) { ((CCombo) w).setText(newText); } }; }