diff options
14 files changed, 1154 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..94416b0d 100644 --- a/bundles/org.eclipse.core.databinding/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.core.databinding/META-INF/MANIFEST.MF @@ -11,6 +11,7 @@ Export-Package: org.eclipse.core.databinding, org.eclipse.core.databinding.validation;x-internal:=false, org.eclipse.core.internal.databinding;x-friends:="org.eclipse.core.databinding.beans", org.eclipse.core.internal.databinding.conversion;x-friends:="org.eclipse.jface.tests.databinding", + org.eclipse.core.internal.databinding.provisional.bind;x-internal:=true, org.eclipse.core.internal.databinding.validation;x-friends:="org.eclipse.jface.tests.databinding" Require-Bundle: org.eclipse.equinox.common;bundle-version="[3.2.0,4.0.0)", org.eclipse.core.databinding.observable;bundle-version="[1.3.0,2.0.0)";visibility:=reexport, diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/Bind.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/Bind.java new file mode 100644 index 00000000..0c5ed903 --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/Bind.java @@ -0,0 +1,256 @@ +package org.eclipse.core.internal.databinding.provisional.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.databinding.property.value.IValueProperty; +import org.eclipse.core.runtime.CoreException; + +/** + * @since 1.5 + * @provisional This class has been added as part of a work in progress. It is + * not guaranteed to work or remain the same in future releases. + * For more information contact e4-dev@eclipse.org. + */ +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 final boolean takeOwnership; + + private boolean isModelChanging = false; + + IValueChangeListener<V> modelListener = new IValueChangeListener<V>() { + public void handleValueChange(ValueChangeEvent<V> event) { + if (!isModelChanging) { + targetBinding.setTargetValue(event.diff.getNewValue()); + } + } + }; + + /** + * + * @param modelObservable + * @param takeOwnership + * <code>true</code> if this class is to take ownership of + * the given model observable, which means it has the + * responsibility of disposing of it when this binding is + * disposed, <code>false</code> if the given model observable + * must not be disposed of by this class + */ + public TwoWayModelBinding(IObservableValue<V> modelObservable, + boolean takeOwnership) { + super(true); + this.modelObservable = modelObservable; + this.takeOwnership = takeOwnership; + + 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() { + modelObservable.removeValueChangeListener(modelListener); + + /* + * If we 'own' the observable then we must dispose of it, if we + * don't 'own' the observable then we must not dispose of it. + */ + if (takeOwnership) { + modelObservable.dispose(); + } + } + } + + /** + * 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; + + private final boolean takeOwnership; + + IValueChangeListener<V> modelListener = new IValueChangeListener<V>() { + public void handleValueChange(ValueChangeEvent<V> event) { + targetBinding.setTargetValue(event.diff.getNewValue()); + } + }; + + /** + * + * @param modelObservable + * @param takeOwnership + * <code>true</code> if this class is to take ownership of + * the given model observable, which means it has the + * responsibility of disposing of it when this binding is + * disposed, <code>false</code> if the given model observable + * must not be disposed of by this class + */ + public OneWayModelBinding(IObservableValue<V> modelObservable, + boolean takeOwnership) { + this.modelObservable = modelObservable; + this.takeOwnership = takeOwnership; + + modelObservable.addValueChangeListener(modelListener); + } + + public V getModelValue() { + return modelObservable.getValue(); + } + + public void removeModelListener() { + modelObservable.removeValueChangeListener(modelListener); + + /* + * If we 'own' the observable then we must dispose of it, if we + * don't 'own' the observable then we must not dispose of it. + */ + if (takeOwnership) { + modelObservable.dispose(); + } + } + } + + /** + * Initiates two-way binding. + * <P> + * The model observable is not disposed when this binding is disposed. + * + * @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, false); + } + + /** + * This is a convenience method that creates an observable for the model + * from the given property and source. The observable will be disposed when + * the binding is disposed. + * + * @param modelProperty + * @param source + * @return an object that can chain one-way bindings + */ + public static <S, V> IOneWayBinding<V> oneWay( + IValueProperty<S, V> modelProperty, S source) { + IObservableValue<V> modelObservable = modelProperty.observe(source); + return new OneWayModelBinding<V>(modelObservable, true); + } + + /** + * Initiates two-way binding. + * <P> + * The model observable is not disposed when this binding is disposed. + * + * @param modelObservable + * @return an object that can chain two-way bindings + */ + public static <V> ITwoWayBinding<V> twoWay( + IObservableValue<V> modelObservable) { + return new TwoWayModelBinding<V>(modelObservable, false); + } + + /** + * This is a convenience method that creates an observable for the model + * from the given property and source. The observable will be disposed when + * the binding is disposed. + * + * @param modelProperty + * @param source + * @return an object that can chain two-way bindings + */ + public static <S, V> ITwoWayBinding<V> twoWay( + IValueProperty<S, V> modelProperty, S source) { + IObservableValue<V> modelObservable = modelProperty.observe(source); + 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/internal/databinding/provisional/bind/DefaultValueBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/DefaultValueBinding.java new file mode 100644 index 00000000..d770daa3 --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/DefaultValueBinding.java @@ -0,0 +1,76 @@ +package org.eclipse.core.internal.databinding.provisional.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/internal/databinding/provisional/bind/IBidiConverter.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/IBidiConverter.java new file mode 100644 index 00000000..3024ac34 --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/IBidiConverter.java @@ -0,0 +1,55 @@ +package org.eclipse.core.internal.databinding.provisional.bind; + +import org.eclipse.core.runtime.CoreException; + +/** + * Interface for bi-directional conversions. + * <P> + * Bi-directional conversions encapsulate both the target-to-model and the + * model-to-target conversions in a single converter. This simplifies the API + * for two-way binding as only one converter need be specified. It also helps to + * ensure consistency in the conversions. + * <P> + * The two conversions must be consistent with each other. More specifically, + * for any value of type T1, call modelToTarget to get a type T2, pass that + * value to targetToModel to get a type T1, pass that value to modelToTarget to + * get a type T2. The value returned by the second call to modelToTarget must be + * 'equal' to the value returned by the first call to modelToTarget ('equal' + * meaning the 'equal' method returns 'true'). Likewise, for any value of type + * T2, call targetToModel to get a type T1, pass that value to modelToTarget to + * get a type T2, pass that value to targetToModel to get a type T1. The value + * returned by the second call to targetToModel must be 'equal' to the value + * returned by the first call to targetToModel. + * <P> + * Note that the above rules allow the conversion to start with non-canonical + * forms but the first conversion must always result in a canonical form + * (meaning if the value is conceptually the same then it must be 'equal'. For + * example, "12,345.300" may be allowed as the target, this being converted to a + * BigDecimal with a value of 12345.30 in the model, being converted back to + * "12345.30" in the target. This does not 'equal' the original target value but + * converting this back again to the model data type must result in a BigDecimal + * that 'equals' the BigDecimal obtained from the first conversion. + * + * @since 1.5 + * @param <T1> + * the data type on the model side + * @param <T2> + * the data type on the target side + */ +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/internal/databinding/provisional/bind/IModelBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/IModelBinding.java new file mode 100644 index 00000000..77e3da46 --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/IModelBinding.java @@ -0,0 +1,28 @@ +package org.eclipse.core.internal.databinding.provisional.bind; + +/** + * This interface is used internally by this package to manage two-way binding + * to the model observable. + * + * @since 1.5 + * @noimplement + * @param <T> + */ +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/internal/databinding/provisional/bind/IOneWayBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/IOneWayBinding.java new file mode 100644 index 00000000..00186042 --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/IOneWayBinding.java @@ -0,0 +1,92 @@ +package org.eclipse.core.internal.databinding.provisional.bind; + +import org.eclipse.core.databinding.conversion.IConverter; +import org.eclipse.core.databinding.observable.value.IObservableValue; +import org.eclipse.core.databinding.property.value.IValueProperty; + +/** + * This interface is used to chain together one-way binding. + * <P> + * To create a one-way binding, first create an implementation of this interface + * from an observable on the model value, e.g.: <code> + * Bind.oneWay(modelObservable) + * </code> then call methods on this interface to perform conversions or other + * supported operations. Each of these methods returns an implementation of this + * interface so these operations can be chained together. Finally call the + * <code>to</code> method to bind to the target observable. + * + * @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); + + /** + * When chaining together one-way binding operations, this method must be + * last. It binds to the target. + * <P> + * The target observable is not disposed when this binding is disposed. + * + * @param targetObservable + */ + void to(IObservableValue<T1> targetObservable); + + /** + * When chaining together one-way binding operations, this method must be + * last. It binds to the target. + * + * This is a convenience method that creates an observable for the target + * from the given property and source. The observable will be disposed when + * the binding is disposed. + * + * @param targetProperty + * @param source + */ + <S> void to(IValueProperty<S, T1> targetProperty, S source); + + /** + * 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 (although this is a + * one-way binding, the binding onwards to the target must be + * two-way so that we know when the user changes the target) + */ + ITwoWayBinding<T1> untilTargetChanges(); +} diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/IOneWayModelBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/IOneWayModelBinding.java new file mode 100644 index 00000000..31fc8006 --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/IOneWayModelBinding.java @@ -0,0 +1,23 @@ +package org.eclipse.core.internal.databinding.provisional.bind; + +/** + * This interface is used internally by this package to manage one-way binding + * from the model observable. + * + * @since 1.5 + * @noimplement + * @param <T> + */ +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/internal/databinding/provisional/bind/ITargetBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/ITargetBinding.java new file mode 100644 index 00000000..b8c4bf94 --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/ITargetBinding.java @@ -0,0 +1,22 @@ +package org.eclipse.core.internal.databinding.provisional.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/internal/databinding/provisional/bind/ITwoWayBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/ITwoWayBinding.java new file mode 100644 index 00000000..a6e1245b --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/ITwoWayBinding.java @@ -0,0 +1,73 @@ +package org.eclipse.core.internal.databinding.provisional.bind; + +import org.eclipse.core.databinding.observable.value.IObservableValue; +import org.eclipse.core.databinding.property.value.IValueProperty; + +/** + * This interface is used to chain together two-way binding. + * <P> + * To create a two-way binding, first create an implementation of this interface + * from an observable on the model value, e.g.: <code> + * Bind.twoWay(modelObservable) + * </code> then call methods on this interface to perform conversions or other + * supported operations. Each of these methods returns an implementation of this + * interface so these operations can be chained together. Finally call the + * <code>to</code> method to bind to the target observable. + * + * @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); + + /** + * When chaining together two-way binding operations, this method must be + * last. It binds to the target observable. + * <P> + * The target observable is not disposed when this binding is disposed. + * + * @param targetObservable + */ + void to(IObservableValue<T1> targetObservable); + + /** + * When chaining together two-way binding operations, this method must be + * last. It binds to the target. + * + * This is a convenience method that creates an observable for the target + * from the given property and source. The observable will be disposed when + * the binding is disposed. + * + * @param targetProperty + * @param source + */ + <S> void to(IValueProperty<S, T1> targetProperty, S source); + +} diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/OneWayBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/OneWayBinding.java new file mode 100644 index 00000000..c8538dd5 --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/OneWayBinding.java @@ -0,0 +1,157 @@ +package org.eclipse.core.internal.databinding.provisional.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.databinding.property.value.IValueProperty; +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 one-way binding from model to target that stops if + * someone else edits the target. + * <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(); + } + }); + } + + public <S> void to(IValueProperty<S, T2> targetProperty, S source) { + IObservableValue<T2> targetObservable = targetProperty.observe(source); + to(targetObservable); + // TODO dispose observable if binding is disposed + } + + /** + * @param status + */ + public void setStatus(IStatus status) { + targetBinding.setStatus(status); + } +} diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/OneWayConversionBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/OneWayConversionBinding.java new file mode 100644 index 00000000..dbb85113 --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/OneWayConversionBinding.java @@ -0,0 +1,43 @@ +package org.eclipse.core.internal.databinding.provisional.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/internal/databinding/provisional/bind/TwoWayBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/TwoWayBinding.java new file mode 100644 index 00000000..4577664e --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/TwoWayBinding.java @@ -0,0 +1,177 @@ +package org.eclipse.core.internal.databinding.provisional.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.property.value.IValueProperty; +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 + * <code>true</code> if the initial value from the model is to be + * set into the target, <code>false</code> if the target must not + * be set until the model value changes + */ + 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); + } + + public <S> void to(IValueProperty<S, T2> targetProperty, S source) { + IObservableValue<T2> targetObservable = targetProperty.observe(source); + to(targetObservable); + // TODO dispose observable if binding is disposed + } + + /** + * 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/internal/databinding/provisional/bind/TwoWayConversionBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/TwoWayConversionBinding.java new file mode 100644 index 00000000..2ac9ffec --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/TwoWayConversionBinding.java @@ -0,0 +1,71 @@ +package org.eclipse.core.internal.databinding.provisional.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/internal/databinding/provisional/bind/TwoWayValidationBinding.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/TwoWayValidationBinding.java new file mode 100644 index 00000000..fef7589b --- /dev/null +++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/provisional/bind/TwoWayValidationBinding.java @@ -0,0 +1,80 @@ +package org.eclipse.core.internal.databinding.provisional.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 |