package org.marketcetera.photon.commons.ui.databinding; import org.eclipse.core.databinding.Binding; import org.eclipse.core.databinding.DataBindingContext; import org.eclipse.core.databinding.observable.IObservable; import org.eclipse.core.databinding.observable.IObservableCollection; import org.eclipse.core.databinding.observable.Realm; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.databinding.validation.MultiValidator; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.databinding.swt.ISWTObservable; import org.eclipse.jface.databinding.viewers.IViewerObservable; import org.eclipse.jface.fieldassist.ControlDecoration; import org.eclipse.jface.fieldassist.FieldDecorationRegistry; import org.eclipse.jface.databinding.fieldassist.ControlDecorationSupport; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.marketcetera.photon.commons.Validate; import org.marketcetera.photon.commons.ui.CommonsUI; import org.marketcetera.photon.commons.ui.databinding.DataBindingUtils.CaptureUpdater; import org.marketcetera.util.misc.ClassVersion; /* $License$ */ /** * Support class for decorating fields that are required. See * {@link #initFor(DataBindingContext, IObservable, String, boolean, Binding)} * for use cases. * * @author <a href="mailto:will@marketcetera.com">Will Horn</a> * @version $Id: RequiredFieldSupport.java 16841 2014-02-20 19:59:04Z colin $ * @since 2.0.0 */ @ClassVersion("$Id: RequiredFieldSupport.java 16841 2014-02-20 19:59:04Z colin $") public final class RequiredFieldSupport { /** * Initialized required field UI for the given target observable. Equivalent * to * {@link #initFor(DataBindingContext, IObservable, String, boolean, int, Binding)} * except the position is defaulted to SWT.TOP | SWT.LEFT (the decoration is * positioned at the top left of the control). * * * @param context * the databinding context that manages validation status * @param target * the observable to validate and decorate * @param description * a description of the observable (for error messages) * @param showRequiredDecoration * controls whether a decoration (black asterisk) should be shown * when a required field is missing * @param binding * a binding that also contributes validation status, can be null * @throws IllegalArgumentException * if context, target, or description is null * @throws IllegalStateException * if the context validation realm or the target realm is not * {@link Realm#isCurrent() current} */ public static void initFor(DataBindingContext context, IObservable target, String description, boolean showRequiredDecoration, Binding binding) { initFor(context, target, description, showRequiredDecoration, SWT.TOP | SWT.LEFT, binding); } /** * Initialized required field UI for the given target observable. Two use * cases are supported: * <ol> * <li>String based {@link IObservableValue} - when the String value is the * empty string (""), an error status with a message such as * "<description> is required" will be generated.</li> * <li>{@link IObservableCollection} - when the collection is empty, an * error status with a message such as "At least one <description> must * be selected" will be generated.</li> * </ol> * The actual text of the error is subject to localization. * <p> * In both cases, if the observable is an {@link ISWTObservable} or * {@link IViewerObservable} (or decoration thereof), and if * showRequiredDecoration is true, then when the error status is generated, * a {@link ControlDecoration} will be added to the observable's control. * The decoration will display the * {@link FieldDecorationRegistry#DEC_REQUIRED} (black asterisk) icon with * the error message as the icon tooltip. The decoration will be positioned * according to controlDecorationPosition and will be accessible from the * control using * <code>control.getData(DataBindingUtils.CONTROL_DECORATION)</code>. * <p> * If the optional binding is provided, the binding status will be queried * first and the required field validation will only kick in when the * binding is ok. * * @param context * the databinding context that manages validation status * @param target * the observable to validate and decorate * @param description * a description of the observable (for error messages) * @param showRequiredDecoration * controls whether a decoration (black asterisk) should be shown * when a required field is missing * @param controlDecorationPosition * the position of the control decoration, e.g. SWT.TOP | * SWT.LEFT * @param binding * a binding that also contributes validation status, can be null * @throws IllegalArgumentException * if context, target, or description is null * @throws IllegalStateException * if the context validation realm or the target realm is not * {@link Realm#isCurrent() current} */ public static void initFor(DataBindingContext context, IObservable target, String description, boolean showRequiredDecoration, int controlDecorationPosition, Binding binding) { Validate.notNull(context, "context", //$NON-NLS-1$ target, "target", //$NON-NLS-1$ description, "description"); //$NON-NLS-1$ if (!context.getValidationRealm().isCurrent()) { throw new IllegalStateException( "must be called from the validation realm of context"); //$NON-NLS-1$ } if (!target.getRealm().isCurrent()) { throw new IllegalStateException( "must be called from the realm of target"); //$NON-NLS-1$ } MultiValidator validator = new RequiredFieldValidator(target, description, binding); context.addValidationStatusProvider(validator); ControlDecorationSupport.create(validator, SWT.LEFT | SWT.TOP, null, showRequiredDecoration ? new RequiredDecorationUpdater() : new HideRequiredUpdater()); } /** * Updates control decorations. If the status is {@link RequiredStatus}, * {@link FieldDecorationRegistry#DEC_REQUIRED} will be used for the icon * instead of {@link FieldDecorationRegistry#DEC_ERROR}. */ @ClassVersion("$Id: RequiredFieldSupport.java 16841 2014-02-20 19:59:04Z colin $") private static final class RequiredDecorationUpdater extends CaptureUpdater { @Override protected Image getImage(IStatus status) { if (status instanceof RequiredStatus) { return FieldDecorationRegistry.getDefault().getFieldDecoration( FieldDecorationRegistry.DEC_REQUIRED).getImage(); } return super.getImage(status); } } /** * Updates control decorations. If the status is {@link RequiredStatus}, the * decoration is hidden. */ @ClassVersion("$Id: RequiredFieldSupport.java 16841 2014-02-20 19:59:04Z colin $") private static final class HideRequiredUpdater extends CaptureUpdater { @Override protected Image getImage(IStatus status) { if (status instanceof RequiredStatus) { return null; } return super.getImage(status); } } /** * A custom status for indicating a required field is missing. */ @ClassVersion("$Id: RequiredFieldSupport.java 16841 2014-02-20 19:59:04Z colin $") public static class RequiredStatus extends Status { /** * Constructor. * * @param message * a human-readable message, localized to the current locale */ public RequiredStatus(String message) { super(IStatus.ERROR, CommonsUI.PLUGIN_ID, 0, message, null); } } /** * Validates required fields. For {@link IObservableValue}, it validates * that the value is not null and not an empty string. For * {@link IObservableCollection}, it validates that the collection is not * empty. * <p> * If a binding is provided, the binding status will be queried first and * the required field validation will only kick in when the binding is ok. */ @ClassVersion("$Id: RequiredFieldSupport.java 16841 2014-02-20 19:59:04Z colin $") private static final class RequiredFieldValidator extends MultiValidator { private final IObservable mTargetObservable; private final String mDescription; private final Binding mBinding; /** * Constructor. * * @param observable * the observable to validate * @param description * the description of the field for error messages * @param binding * a binding that also contributes validation status, can be * null * @throws IllegalArgumentException * if observable or description is null */ public RequiredFieldValidator(IObservable observable, String description, Binding binding) { Validate.notNull(observable, "observable", //$NON-NLS-1$ description, "description"); //$NON-NLS-1$ mTargetObservable = observable; mDescription = description; mBinding = binding; } @Override protected IStatus validate() { // first query observables to ensure they are tracked String message = null; if (mTargetObservable instanceof IObservableValue) { final Object value = ((IObservableValue) mTargetObservable) .getValue(); if (value == null || String.valueOf(value).isEmpty()) { message = Messages.REQUIRED_FIELD_SUPPORT_MISSING_VALUE .getText(mDescription); } } else if (mTargetObservable instanceof IObservableCollection) { if (((IObservableCollection) mTargetObservable).isEmpty()) { message = Messages.REQUIRED_FIELD_SUPPORT_MISSING_COLLECTION .getText(mDescription); } } // even if the field is missing, prefer the binding status if (mBinding != null) { final IStatus status = (IStatus) mBinding.getValidationStatus() .getValue(); if (!status.isOK()) { return status; } } // binding is null or its status is ok so return result of // observable validation if (message == null) { return Status.OK_STATUS; } else { return new RequiredStatus(message); } } } }