diff options
author | Nigel Westbury | 2013-07-08 11:50:36 +0000 |
---|---|---|
committer | Nigel Westbury | 2013-07-08 21:39:19 +0000 |
commit | 9ac8f9392c3863c13088aca4b6b681a2dbb18e7b (patch) | |
tree | b82c8eeee29bfdc03852555acda55e38d7287ffd | |
parent | e6947e901ed8ec083ede35cd95f7630c6fcee80b (diff) | |
download | org.eclipse.e4.databinding-bind.tar.gz org.eclipse.e4.databinding-bind.tar.xz org.eclipse.e4.databinding-bind.zip |
14 files changed, 962 insertions, 0 deletions
diff --git a/bundles/org.eclipse.core.databinding/META-INF/MANIFEST.MF b/bundles/org.eclipse.core.databinding/META-INF/MANIFEST.MF index f2fdfc8a..688997f5 100644 --- a/bundles/org.eclipse.core.databinding/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.core.databinding/META-INF/MANIFEST.MF @@ -7,6 +7,7 @@ Bundle-ClassPath: . Bundle-Vendor: %providerName Bundle-Localization: plugin Export-Package: org.eclipse.core.databinding, + org.eclipse.core.databinding.bind, org.eclipse.core.databinding.conversion;x-internal:=false, org.eclipse.core.databinding.validation;x-internal:=false, org.eclipse.core.internal.databinding;x-friends:="org.eclipse.core.databinding.beans", diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/Bind.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/Bind.java new file mode 100644 index 00000000..d4ac643a --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/Bind.java @@ -0,0 +1,176 @@ +package org.eclipse.core.databinding.bind; + +import org.eclipse.core.databinding.observable.value.IObservableValue; +import org.eclipse.core.databinding.observable.value.IValueChangeListener; +import org.eclipse.core.databinding.observable.value.ValueChangeEvent; +import org.eclipse.core.runtime.CoreException; + +/** + * @since 1.5 + * + */ +public class Bind { + + /** + * This is the ITwoWayBinding that sits immediately on top on the model + * observable. + * + * @param <V> + */ + static class TwoWayModelBinding<V> extends TwoWayBinding<V> { + private final IObservableValue<V> modelObservable; + + private boolean isModelChanging = false; + + IValueChangeListener<V> modelListener = new IValueChangeListener<V>() { + public void handleValueChange(ValueChangeEvent<V> event) { + if (!isModelChanging) { + targetBinding.setTargetValue(event.diff.getNewValue()); + } + } + }; + + public TwoWayModelBinding(IObservableValue<V> modelObservable, + boolean pullInitialValue) { + super(pullInitialValue); + this.modelObservable = modelObservable; + + modelObservable.addValueChangeListener(modelListener); + } + + public V getModelValue() { + return modelObservable.getValue(); + } + + public void setModelValue(V newValue) { + isModelChanging = true; + try { + modelObservable.setValue(newValue); + } finally { + isModelChanging = false; + } + } + + public void removeModelListener() { + // Actually we don't own this observable so don't dispose, + // just remove our listener. + modelObservable.removeValueChangeListener(modelListener); + } + } + + /** + * This is the IOneWayBinding that sits immediately on top on the model + * observable. + * + * @param <V> + */ + static class OneWayModelBinding<V> extends OneWayBinding<V> { + private final IObservableValue<V> modelObservable; + + IValueChangeListener<V> modelListener = new IValueChangeListener<V>() { + public void handleValueChange(ValueChangeEvent<V> event) { + targetBinding.setTargetValue(event.diff.getNewValue()); + } + }; + + public OneWayModelBinding(IObservableValue<V> modelObservable) { + this.modelObservable = modelObservable; + + modelObservable.addValueChangeListener(modelListener); + } + + public V getModelValue() { + return modelObservable.getValue(); + } + + public void removeModelListener() { + // Actually we don't own this observable so don't dispose, + // just remove our listener. + modelObservable.removeValueChangeListener(modelListener); + } + } + + /** + * @param modelObservable + * @return an object that can chain one-way bindings + */ + public static <V> IOneWayBinding<V> oneWay( + IObservableValue<V> modelObservable) { + return new OneWayModelBinding<V>(modelObservable); + } + + /** + * @param modelObservable + * @return an object that can chain two-way bindings + */ + public static <V> ITwoWayBinding<V> twoWay( + final IObservableValue<V> modelObservable) { + return new TwoWayModelBinding<V>(modelObservable, true); + } + + /** + * This method is used to 'bounce back' a value from the target. + * Specifically this means whenever the target value changes, the value is + * converted using the given converter (targetToModel). The resulting value + * is the converted back using the same converter (modelToTarget). + * <P> + * A use case for this method is as follows. You have a number that is + * stored in the model as an Integer. You want to display the number in a + * text box with separators, so 1234567 would be displayed as 1,234,567 or + * 1.234.567 depending on your regional settings. You want to allow more + * flexibility on what the user can enter. For example you may want to allow + * the user to miss out the separators. As the user types, the value is + * updated in the model. When the control loses focus you want the + * separators to be inserted in the Text control in the proper positions. + * <P> + * To do this you create two bindings. One is a two-way binding that + * observes the Text control with SWT.Modify. The other is a 'bounce back' + * that observes the control with SWT.FocusOut. It might be coded as + * follows: + * <P> + * <code> + * Bind.bounceBack(myIntegerToTextConverter) + .to(SWTObservables.observeText(textControl, SWT.FocusOut)); + * </code> + * + * @param converter + * @return an object that can chain two-way bindings + */ + public static <T1, T2> ITwoWayBinding<T2> bounceBack( + final IBidiConverter<T1, T2> converter) { + return new TwoWayBinding<T2>(false) { + + public T2 getModelValue() { + /* + * This method should never be called because pullInitialValue + * is set to false. + */ + throw new UnsupportedOperationException(); + } + + public void setModelValue(T2 valueFromTarget) { + try { + T1 modelSideValue = converter + .targetToModel(valueFromTarget); + T2 valueBackToTarget = converter + .modelToTarget(modelSideValue); + this.targetBinding.setTargetValue(valueBackToTarget); + } catch (CoreException e) { + /* + * No bounce-back occurs if the value from the target side + * cannot be converted. We do nothing because the user will + * typically have an error indicator anyway. + */ + } + } + + public void removeModelListener() { + /* + * Nothing to do here because nothing originates from the model + * side. + */ + } + }; + } + +} diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/DefaultValueBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/DefaultValueBinding.java new file mode 100644 index 00000000..f5dacf44 --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/DefaultValueBinding.java @@ -0,0 +1,76 @@ +package org.eclipse.core.databinding.bind; + +import org.eclipse.core.runtime.IStatus; + +/** + * @since 1.5 + * + * @param <T1> + */ +public class DefaultValueBinding<T1> extends TwoWayBinding<T1> implements + ITargetBinding<T1> { + + private final IOneWayModelBinding<T1> modelBinding; + + private boolean stopped = false; + + /** + * @param modelBinding + */ + public DefaultValueBinding(IOneWayModelBinding<T1> modelBinding) { + super(true); + this.modelBinding = modelBinding; + } + + public T1 getModelValue() { + return modelBinding.getModelValue(); + } + + public void setTargetValue(T1 valueOnModelSide) { + targetBinding.setTargetValue(valueOnModelSide); + } + + public void setStatus(IStatus status) { + /* + * Generally there are no status values sent from the model to the + * target because the model is generally valid. However there may be + * cases when error or warning statuses come from the model. For example + * when using JSR-303 validations the validation is done on the value in + * the model object. In any case we just pass it on. + */ + targetBinding.setStatus(status); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.core.databinding.bind.IModelBinding#setModelValue(java.lang + * .Object) + */ + public void setModelValue(T1 newValue) { + /* + * The target has changed so stop this binding. The target will continue + * to notify us of changes for as long as it exists so we need to set a + * flag to indicate that this binding is in a stopped state. + */ + if (!stopped) { + stopped = true; + modelBinding.removeModelListener(); + } + } + + public void removeModelListener() { + /* + * Pass the request back to the next link in the binding chain so + * eventually the request gets back to the model observable. + * + * Note that if we are in a 'stopped' state then the listener has + * already been removed from the model and we should not attempt to + * remove it again. + */ + if (!stopped) { + modelBinding.removeModelListener(); + } + } +}
\ No newline at end of file diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/IBidiConverter.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/IBidiConverter.java new file mode 100644 index 00000000..183ca3b3 --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/IBidiConverter.java @@ -0,0 +1,27 @@ +package org.eclipse.core.databinding.bind; + +import org.eclipse.core.runtime.CoreException; + +/** + * @since 1.5 + * + * @param <T1> + * @param <T2> + */ +public interface IBidiConverter<T1, T2> { + + /** + * @param fromObject + * @return the value converted for use on the target side + */ + T2 modelToTarget(T1 fromObject); + + /** + * @param fromObject + * @return the value converted for use on the model side + * @throws CoreException + * if the value cannot be converted + */ + T1 targetToModel(T2 fromObject) throws CoreException; + +} diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/IModelBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/IModelBinding.java new file mode 100644 index 00000000..0f03e748 --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/IModelBinding.java @@ -0,0 +1,25 @@ +package org.eclipse.core.databinding.bind; + +/** + * @since 1.5 + * + * @param <T> + */ +public interface IModelBinding<T> { + + /** + * @return the model from the model side + */ + T getModelValue(); + + /** + * @param newValue + */ + void setModelValue(T newValue); + + /** + * Removes the listener from the model. This method is called when the + * target is disposed. + */ + void removeModelListener(); +} diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/IOneWayBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/IOneWayBinding.java new file mode 100644 index 00000000..eeac0571 --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/IOneWayBinding.java @@ -0,0 +1,62 @@ +package org.eclipse.core.databinding.bind; + +import org.eclipse.core.databinding.conversion.IConverter; +import org.eclipse.core.databinding.observable.value.IObservableValue; + +/** + * @since 1.5 + * + * @param <T1> + */ +public interface IOneWayBinding<T1> { + + /** + * @param converter + * @return the value converted to the type expected by the next part of the + * binding chain + */ + <T2> IOneWayBinding<T2> convert(IConverter<T1, T2> converter); + + /** + * This method is similar to <code>convert</code>. However if any + * observables are read during the conversion then listeners are added to + * these observables and the conversion is done again. + * <P> + * The conversion is always repeated keeping the same value of the model. It + * is assumed that the tracked observables affect the target. For example + * suppose a time widget contains a time which is bound to a Date property + * in the model. The time zone to use is a preference and an observable + * exists for the time zone (which would implement + * IObservableValue<TimeZone>). If the user changes the time zone in the + * preferences then the text in the time widget will change to show the same + * time but in a different time zone. The time in the model will not change + * when the time zone is changed. If the user edits the time in the time + * widget then that time will be interpreted using the new time zone and + * converted to a Date object for the model. + * + * @param converter + * @return an object that can chain one-way bindings + */ + <T2> IOneWayBinding<T2> convertWithTracking(IConverter<T1, T2> converter); + + /** + * @param targetObservable + */ + void to(IObservableValue<T1> targetObservable); + + /** + * This method is used to create a one-way binding from the model to the + * target but the binding stops if something else changes the target. + * <P> + * A use case is when a default value is provided in a UI control. For + * example suppose you have two fields, 'amount' and 'sales tax'. When the + * user enters an amount, the requirement is that the 'sales tax' field is + * completed based on the amount using a given tax rate. If the user edits + * the amount, the sales tax amount changes accordingly. However once the + * user edits the sales tax field then changes to the amount field no longer + * affect the sales tax field. + * + * @return an object that can chain two-way bindings + */ + ITwoWayBinding<T1> untilTargetChanges(); +} diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/IOneWayModelBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/IOneWayModelBinding.java new file mode 100644 index 00000000..98be6c79 --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/IOneWayModelBinding.java @@ -0,0 +1,20 @@ +package org.eclipse.core.databinding.bind; + +/** + * @since 1.5 + * + * @param <T> + */ +public interface IOneWayModelBinding<T> { + + /** + * @return the value from the model side + */ + T getModelValue(); + + /** + * Removes the listener from the model. This method is called when the + * target is disposed. + */ + void removeModelListener(); +} diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/ITargetBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/ITargetBinding.java new file mode 100644 index 00000000..3fc6d6cb --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/ITargetBinding.java @@ -0,0 +1,22 @@ +package org.eclipse.core.databinding.bind; + +import org.eclipse.core.runtime.IStatus; + +/** + * @since 1.5 + * + * @param <F> + */ +public interface ITargetBinding<F> { + /** + * @param targetValue + */ + void setTargetValue(F targetValue); + + /** + * Push the error status back to the target + * + * @param status + */ + void setStatus(IStatus status); +} diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/ITwoWayBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/ITwoWayBinding.java new file mode 100644 index 00000000..124066eb --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/ITwoWayBinding.java @@ -0,0 +1,43 @@ +package org.eclipse.core.databinding.bind; + +import org.eclipse.core.databinding.observable.value.IObservableValue; + +/** + * @since 1.5 + * @param <T1> + */ +public interface ITwoWayBinding<T1> { + + /** + * @param converter + * @return an object that can chain two-way bindings + */ + <T2> ITwoWayBinding<T2> convert(final IBidiConverter<T1, T2> converter); + + /** + * This method is similar to <code>convert</code>. However if any + * observables are read during the conversion then listeners are added to + * these observables and the conversion is done again. + * <P> + * The conversion is always repeated keeping the same value of the model. It + * is assumed that the tracked observables affect the target. For example + * suppose a time widget contains a time which is bound to a Date property + * in the model. The time zone to use is a preference and an observable + * exists for the time zone (which would implement + * IObservableValue<TimeZone>). If the user changes the time zone in the + * preferences then the text in the time widget will change to show the same + * time but in a different time zone. The time in the model will not change + * when the time zone is changed. If the user edits the time in the time + * widget then that time will be interpreted using the new time zone and + * converted to a Date object for the model. + * + * @param converter + * @return an object that can chain two-way bindings + */ + <T2> ITwoWayBinding<T2> convertWithTracking(IBidiConverter<T1, T2> converter); + + /** + * @param targetObservable + */ + void to(IObservableValue<T1> targetObservable); +} diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/OneWayBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/OneWayBinding.java new file mode 100644 index 00000000..8c82d1a1 --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/OneWayBinding.java @@ -0,0 +1,149 @@ +package org.eclipse.core.databinding.bind; + +import org.eclipse.core.databinding.conversion.IConverter; +import org.eclipse.core.databinding.observable.DisposeEvent; +import org.eclipse.core.databinding.observable.IDisposeListener; +import org.eclipse.core.databinding.observable.value.IObservableValue; +import org.eclipse.core.runtime.IStatus; + +/** + * @since 1.5 + * + * @param <T2> + */ +public abstract class OneWayBinding<T2> implements IOneWayBinding<T2>, + IOneWayModelBinding<T2> { + + protected ITargetBinding<T2> targetBinding; + + public <T3> IOneWayBinding<T3> convert(final IConverter<T2, T3> converter) { + if (targetBinding != null) { + throw new RuntimeException( + "When chaining together a binding, you cannot chain more than one target."); //$NON-NLS-1$ + } + + OneWayConversionBinding<T3, T2> nextBinding = new OneWayConversionBinding<T3, T2>( + this, converter); + targetBinding = nextBinding; + return nextBinding; + } + + /** + * This method is similar to <code>convert</code>. However if any + * observables are read during the conversion then listeners are added to + * these observables and the conversion is done again. + * <P> + * The conversion is always repeated keeping the same value of the model. It + * is assumed that the tracked observables affect the target. For example + * suppose a time widget contains a time which is bound to a Date property + * in the model. The time zone to use is a preference and an observable + * exists for the time zone (which would implement + * IObservableValue<TimeZone>). If the user changes the time zone in the + * preferences then the text in the time widget will change to show the same + * time but in a different time zone. The time in the model will not change + * when the time zone is changed. If the user edits the time in the time + * widget then that time will be interpreted using the new time zone and + * converted to a Date object for the model. + * + * @param converter + * @return an object that can chain one-way bindings + */ + public <T3> IOneWayBinding<T3> convertWithTracking( + final IConverter<T2, T3> converter) { + if (targetBinding != null) { + throw new RuntimeException( + "When chaining together a binding, you cannot chain more than one target."); //$NON-NLS-1$ + } + + OneWayConversionBinding<T3, T2> nextBinding = new OneWayConversionBinding<T3, T2>( + this, converter); + targetBinding = nextBinding; + return nextBinding; + } + + /** + * This method creates a binding where + * <P> + * This method is used to provide a default value. The default value is a + * one-way binding, typically a one-way binding from a ComputedValue. This + * default value is the receiver of this method. The target may be either an + * observable on the model or an observable on a UI control. (If you have + * two-way binding between the model and the UI control then you can bind a + * default value to either but you will typically have fewer conversions to + * do if you bind to the model). + * <P> + * The binding on to the target is a two-way binding. The default value is + * passed on to the target until such time that the target is changed by + * someone other than ourselves. At that point the binding stops and changes + * in the default value are no longer set into the target. + * <P> + * An example use is as follows: There is a field in which the user can + * enter the price of an item. There is another field in which the user can + * enter the sales tax for the sale of the item. We want the sales tax field + * to automatically calculate to be 5% of the price. If the user edits the + * price then the sales tax field should change too. The user may edit the + * sales tax field. If the user does this then the sales tax field will no + * longer be updated as the price changes. + */ + public ITwoWayBinding<T2> untilTargetChanges() { + if (targetBinding != null) { + throw new RuntimeException( + "When chaining together a binding, you cannot chain more than one target."); //$NON-NLS-1$ + } + + DefaultValueBinding<T2> nextBinding = new DefaultValueBinding<T2>(this); + + targetBinding = nextBinding; + return nextBinding; + } + + public void to(final IObservableValue<T2> targetObservable) { + /* + * We have finally made it to the target observable. + * + * Initially set the target observable to the current value from the + * model. + */ + targetObservable.setValue(getModelValue()); + + /* + * The target binding contains a method that is called whenever a new + * value comes from the model side. We simply set a target binding that + * sets that value into the target observable. + */ + targetBinding = new ITargetBinding<T2>() { + public void setTargetValue(T2 targetValue) { + targetObservable.setValue(targetValue); + } + + public void setStatus(IStatus status) { + /* + * Generally there are no status values sent from the model to + * the target because the model is generally valid. However + * there may be cases when error or warning statuses come from + * the model. For example when using JSR-303 validations the + * validation is done on the value in the model object. In any + * case, there is no status observable provided by the user so + * we drop it. + */ + } + }; + + /* + * If the target is disposed, be sure to remove the listener from the + * model. + */ + targetObservable.addDisposeListener(new IDisposeListener() { + public void handleDispose(DisposeEvent event) { + removeModelListener(); + } + }); + } + + /** + * @param status + */ + public void setStatus(IStatus status) { + targetBinding.setStatus(status); + } +} diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/OneWayConversionBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/OneWayConversionBinding.java new file mode 100644 index 00000000..f5fe2019 --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/OneWayConversionBinding.java @@ -0,0 +1,43 @@ +package org.eclipse.core.databinding.bind; + +import org.eclipse.core.databinding.conversion.IConverter; + +/** + * @since 1.5 + * + * @param <T2> + * @param <T1> + */ +public class OneWayConversionBinding<T2, T1> extends OneWayBinding<T2> + implements ITargetBinding<T1> { + private final IOneWayModelBinding<T1> modelBinding; + private final IConverter<T1, T2> converter; + + /** + * @param modelBinding + * @param converter + */ + public OneWayConversionBinding(IOneWayModelBinding<T1> modelBinding, + IConverter<T1, T2> converter) { + this.modelBinding = modelBinding; + this.converter = converter; + } + + public T2 getModelValue() { + T1 modelValue = modelBinding.getModelValue(); + return converter.convert(modelValue); + } + + public void setTargetValue(T1 valueOnModelSide) { + T2 valueOnTargetSide = converter.convert(valueOnModelSide); + targetBinding.setTargetValue(valueOnTargetSide); + } + + public void removeModelListener() { + /* + * Pass the request back to the next link in the binding chain so + * eventually the request gets back to the model observable. + */ + modelBinding.removeModelListener(); + } +}
\ No newline at end of file diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/TwoWayBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/TwoWayBinding.java new file mode 100644 index 00000000..f7860ac5 --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/TwoWayBinding.java @@ -0,0 +1,167 @@ +package org.eclipse.core.databinding.bind; + +import org.eclipse.core.databinding.observable.DisposeEvent; +import org.eclipse.core.databinding.observable.IDisposeListener; +import org.eclipse.core.databinding.observable.value.IObservableValue; +import org.eclipse.core.databinding.observable.value.IValueChangeListener; +import org.eclipse.core.databinding.observable.value.ValueChangeEvent; +import org.eclipse.core.databinding.validation.IValidator; +import org.eclipse.core.runtime.IStatus; + +/** + * @since 1.5 + * + * @param <T2> + */ +public abstract class TwoWayBinding<T2> implements ITwoWayBinding<T2>, + IModelBinding<T2> { + + /** + * <code>true</code> if the target observable bound by the <code>to</code> + * method is to be initially set to the value from the model side, + * <code>false</code> if its value is to be set only when changes are pushed + * from the target side + */ + protected boolean pullInitialValue; + + protected ITargetBinding<T2> targetBinding; + + /** + * @param pullInitialValue + */ + public TwoWayBinding(boolean pullInitialValue) { + this.pullInitialValue = pullInitialValue; + } + + public <T3> ITwoWayBinding<T3> convert( + final IBidiConverter<T2, T3> converter) { + if (targetBinding != null) { + throw new RuntimeException( + "When chaining together a binding, you cannot chain more than one target."); //$NON-NLS-1$ + } + + TwoWayConversionBinding<T3, T2> nextBinding = new TwoWayConversionBinding<T3, T2>( + this, converter, pullInitialValue); + targetBinding = nextBinding; + return nextBinding; + } + + /** + * This method is similar to <code>convert</code>. However if any + * observables are read during the conversion then listeners are added to + * these observables and the conversion is done again. + * <P> + * The conversion is always repeated keeping the same value of the model. It + * is assumed that the tracked observables affect the target. For example + * suppose a time widget contains a time which is bound to a Date property + * in the model. The time zone to use is a preference and an observable + * exists for the time zone (which would implement + * IObservableValue<TimeZone>). If the user changes the time zone in the + * preferences then the text in the time widget will change to show the same + * time but in a different time zone. The time in the model will not change + * when the time zone is changed. If the user edits the time in the time + * widget then that time will be interpreted using the new time zone and + * converted to a Date object for the model. + * + * @param converter + * @return an object that can chain two-way bindings + */ + public <T3> ITwoWayBinding<T3> convertWithTracking( + final IBidiConverter<T2, T3> converter) { + if (targetBinding != null) { + throw new RuntimeException( + "When chaining together a binding, you cannot chain more than one target."); //$NON-NLS-1$ + } + + TwoWayConversionBinding<T3, T2> nextBinding = new TwoWayConversionBinding<T3, T2>( + this, converter, pullInitialValue); + targetBinding = nextBinding; + return nextBinding; + } + + /** + * @param validator + * @return an object that can chain two-way bindings + */ + public ITwoWayBinding<T2> validate(final IValidator<T2> validator) { + if (targetBinding != null) { + throw new RuntimeException( + "When chaining together a binding, you cannot chain more than one target."); //$NON-NLS-1$ + } + + TwoWayValidationBinding<T2> nextBinding = new TwoWayValidationBinding<T2>( + this, validator, pullInitialValue); + targetBinding = nextBinding; + return nextBinding; + } + + public void to(final IObservableValue<T2> targetObservable) { + to(targetObservable, null); + } + + /** + * We have finally made it to the target observable. + * + * Initially set the target observable to the current value from the model + * (if the pullInitialValue flag is set which it will be in most cases). + * + * @param targetObservable + * @param statusObservable + */ + public void to(final IObservableValue<T2> targetObservable, + final IObservableValue<IStatus> statusObservable) { + if (pullInitialValue) { + targetObservable.setValue(getModelValue()); + } + + final boolean[] isChanging = new boolean[] { false }; + + /* + * The target binding contains a method that is called whenever a new + * value comes from the model side. We simply set a target binding that + * sets that value into the target observable. + */ + targetBinding = new ITargetBinding<T2>() { + public void setTargetValue(T2 targetValue) { + try { + isChanging[0] = true; + targetObservable.setValue(targetValue); + } finally { + isChanging[0] = false; + } + } + + public void setStatus(IStatus status) { + /* + * If there is a target for the status, set it. Otherwise drop + * it. + */ + if (statusObservable != null) { + statusObservable.setValue(status); + } + } + }; + + /* + * Listen for changes originating from the target observable, and send + * those back through to the model side. + */ + targetObservable.addValueChangeListener(new IValueChangeListener<T2>() { + public void handleValueChange(ValueChangeEvent<T2> event) { + if (!isChanging[0]) { + setModelValue(event.diff.getNewValue()); + } + } + }); + + /* + * If the target is disposed, be sure to remove the listener from the + * model. + */ + targetObservable.addDisposeListener(new IDisposeListener() { + public void handleDispose(DisposeEvent event) { + removeModelListener(); + } + }); + } +} diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/TwoWayConversionBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/TwoWayConversionBinding.java new file mode 100644 index 00000000..e9856920 --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/TwoWayConversionBinding.java @@ -0,0 +1,71 @@ +package org.eclipse.core.databinding.bind; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; + +/** + * @since 1.5 + * + * @param <T2> + * @param <T1> + */ +public class TwoWayConversionBinding<T2, T1> extends TwoWayBinding<T2> + implements ITargetBinding<T1> { + private final IModelBinding<T1> modelBinding; + private final IBidiConverter<T1, T2> converter; + + /** + * @param modelBinding + * @param converter + * @param pullInitialValue + */ + public TwoWayConversionBinding(IModelBinding<T1> modelBinding, + IBidiConverter<T1, T2> converter, boolean pullInitialValue) { + super(pullInitialValue); + this.modelBinding = modelBinding; + this.converter = converter; + } + + /** + * @return the value from the model, converted for use on the target side + */ + public T2 getModelValue() { + T1 modelValue = modelBinding.getModelValue(); + return converter.modelToTarget(modelValue); + } + + /** + * @param valueOnTargetSide + */ + public void setModelValue(T2 valueOnTargetSide) { + try { + T1 valueOnModelSide = converter.targetToModel(valueOnTargetSide); + modelBinding.setModelValue(valueOnModelSide); + targetBinding.setStatus(Status.OK_STATUS); + } catch (CoreException e) { + targetBinding.setStatus(e.getStatus()); + } + } + + public void setTargetValue(T1 valueOnModelSide) { + T2 valueOnTargetSide = converter.modelToTarget(valueOnModelSide); + targetBinding.setTargetValue(valueOnTargetSide); + } + + public void setStatus(IStatus status) { + /* + * The error occurred somewhere on the model side. We push this error + * back to the target. + */ + targetBinding.setStatus(status); + } + + public void removeModelListener() { + /* + * Pass the request back to the next link in the binding chain so + * eventually the request gets back to the model observable. + */ + modelBinding.removeModelListener(); + } +}
\ No newline at end of file diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/TwoWayValidationBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/TwoWayValidationBinding.java new file mode 100644 index 00000000..a7821109 --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/bind/TwoWayValidationBinding.java @@ -0,0 +1,80 @@ +package org.eclipse.core.databinding.bind; + +import org.eclipse.core.databinding.validation.IValidator; +import org.eclipse.core.runtime.IStatus; + +/** + * @since 1.5 + * + * @param <T> + */ +public class TwoWayValidationBinding<T> extends TwoWayBinding<T> implements + ITargetBinding<T> { + private final IModelBinding<T> modelBinding; + private final IValidator<T> validator; + + /** + * @param modelBinding + * @param validator + * @param pullInitialValue + */ + public TwoWayValidationBinding(IModelBinding<T> modelBinding, + IValidator<T> validator, boolean pullInitialValue) { + super(pullInitialValue); + this.modelBinding = modelBinding; + this.validator = validator; + } + + /** + * The default behavior is to validate only when going from target to model. + * The error status observable is assumed to be there to show user errors. + * Therefore no validation is done in this method. + * + * @return the value from the model + */ + public T getModelValue() { + return modelBinding.getModelValue(); + } + + /** + * @param valueOnTargetSide + */ + public void setModelValue(T valueOnTargetSide) { + IStatus status = validator.validate(valueOnTargetSide); + targetBinding.setStatus(status); + + /* + * We pass on the value towards the model only if a warning or better. + * We block if an error or worse. This may or may not be the behavior + * expected by the users. + */ + if (status.getSeverity() >= IStatus.WARNING) { + modelBinding.setModelValue(valueOnTargetSide); + } + } + + /** + * The default behavior is to validate only when going from target to model. + * The error status observable is assumed to be there to show user errors. + * Therefore no validation is done in this method. + */ + public void setTargetValue(T valueOnModelSide) { + targetBinding.setTargetValue(valueOnModelSide); + } + + public void setStatus(IStatus status) { + /* + * The error occurred somewhere on the model side. We push this error + * back to the target. + */ + targetBinding.setStatus(status); + } + + public void removeModelListener() { + /* + * Pass the request back to the next link in the binding chain so + * eventually the request gets back to the model observable. + */ + modelBinding.removeModelListener(); + } +}
\ No newline at end of file |