package org.tessell.model.dsl;
import static com.google.gwt.event.dom.client.KeyCodes.KEY_TAB;
import static org.tessell.model.properties.NewProperty.derivedProperty;
import java.util.Arrays;
import org.tessell.bus.AbstractBound;
import org.tessell.gwt.user.client.ui.IsWidget;
import org.tessell.model.commands.UiCommand;
import org.tessell.model.events.HasMemberChangedHandlers;
import org.tessell.model.properties.*;
import org.tessell.model.validation.rules.Rule;
import org.tessell.model.values.LambdaValue;
import com.google.gwt.event.dom.client.*;
import com.google.gwt.event.logical.shared.HasAttachHandlers;
import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.ui.HasValue;
/**
* Provides a fluent interface for binding properties to widgets.
*
* Very heavily influenced by gwt-pectin.
*/
public class Binder extends AbstractBound {
/** @return a fluent {@link PropertyBinder} against {@code property}. */
public <P> PropertyBinder<P> bind(Property<P> property) {
return new PropertyBinder<P>(this, property);
}
/** @return a fluent {@link PropertyBinder} against {@code property}. */
public <P> PropertyBinder<P> bind(LambdaValue<P> value) {
return new PropertyBinder<P>(this, derivedProperty(value));
}
/** @return a fluent {@link ListPropertyBinder} against {@code property}. */
public <P> ListPropertyBinder<P> bind(ListProperty<P> property) {
return new ListPropertyBinder<P>(this, property);
}
/** @return a fluent {@link RuleBinder} against {@code rule}. */
public <P> RuleBinder<P> bind(Rule<P> rule) {
return new RuleBinder<P>(this, rule);
}
/** @return a fluent {@link StringPropertyBinder} against {@code property}. */
public <P> StringPropertyBinder bind(StringProperty property) {
return new StringPropertyBinder(this, property);
}
/** @return a fluent {@link BooleanPropertyBinder} against {@code property}. */
public <P> BooleanPropertyBinder bind(BooleanProperty property) {
return new BooleanPropertyBinder(this, property);
}
/** @return a fluent {@link EnumPropertyBinder} against {@code property}. */
public <E extends Enum<E>> EnumPropertyBinder<E> bind(EnumProperty<E> property) {
return new EnumPropertyBinder<E>(this, property);
}
/** @return a fluent {@link UiCommandBinder} against {@code command}. */
public UiCommandBinder bind(UiCommand command) {
return new UiCommandBinder(this, command);
}
/** @return a fluent {@link WhenBinder} against {@code property}. */
public <P> WhenBinder<P> when(Property<P> property) {
return new WhenBinder<P>(this, property);
}
/** @return a fluent {@link WhenBinder} against {@code lambda}. */
public <P> WhenBinder<P> when(LambdaValue<P> lambda) {
return new WhenBinder<P>(this, derivedProperty(lambda));
}
/** @return a fluent {@link EventBinder} against {@code clickable}. */
public EventBinder onClick(IsWidget clickable) {
return new ClickBinder(this, clickable);
}
/** @return a fluent {@link EventBinder} against {@code clickable}. */
public EventBinder onDoubleClick(HasDoubleClickHandlers clickable) {
return new DoubleClickBinder(this, clickable);
}
/** @return a fluent {@link EventBinder} against {@code blurable}. */
public EventBinder onBlur(HasBlurHandlers blurable) {
return new BlurBinder(this, blurable);
}
/** @return a fluent {@link EventBinder} against {@code attachable}. */
public EventBinder onAttach(HasAttachHandlers attachable) {
return new AttachBinder(this, attachable, true);
}
/** @return a fluent {@link EventBinder} against {@code attachable}. */
public EventBinder onDetach(HasAttachHandlers attachable) {
return new AttachBinder(this, attachable, false);
}
/** @return a fluent {@link EventBinder} against {@code changable}. */
@SuppressWarnings("unchecked")
public EventBinder onChange(HasValueChangeHandlers<?> changable) {
return new ChangeBinder(this, (HasValueChangeHandlers<Object>) changable);
}
/** @return a fluent {@link EventBinder} against {@code property}. */
@SuppressWarnings("unchecked")
public EventBinder onChange(Property<?> property) {
return new PropertyChangeBinder(this, (Property<Object>) property);
}
/** @return a fluent {@link EventBinder} against {@code hasMembers}. */
public EventBinder onMemberChange(HasMemberChangedHandlers hasMembers) {
return new MemberChangeBinder(this, hasMembers);
}
/** @return a fluent {@link EventBinder} against {@code keyDownable}. */
public EventBinder onKeyDown(IsWidget keyDownable) {
return new KeyDownBinder(this, keyDownable, null);
}
/** @return a fluent {@link EventBinder} against {@code keyDownable}, when {@code char} is pressed. */
public EventBinder onKeyDown(IsWidget keyDownable, Integer... filter) {
return new KeyDownBinder(this, keyDownable, Arrays.asList(filter));
}
/** Enhances each {@code source} to fire change events on key up and blur. */
public <P, S extends HasKeyUpHandlers & HasBlurHandlers & HasValue<P>> void enhance(S... sources) {
fireChangeOnBlur(sources);
fireChangeOnKeyUp(sources);
}
/** Forces a {@link ValueChangeEvent} (and hence touching) of each {@code source} on blur. */
public <P, S extends HasBlurHandlers & HasValue<P>> void fireChangeOnBlur(S... sources) {
for (final S source : sources) {
add(source.addBlurHandler(e -> ValueChangeEvent.fire(source, source.getValue())));
}
}
/** Forces a {@link ValueChangeEvent} (and hence touching) of each {@code source} on key up. */
public <P, S extends HasKeyUpHandlers & HasValue<P>> void fireChangeOnKeyUp(S... sources) {
for (final S source : sources) {
add(source.addKeyUpHandler(e -> {
if (e.getNativeKeyCode() == KEY_TAB) {
return; // ignore tabbing into the field
}
ValueChangeEvent.fire(source, source.getValue());
}));
}
}
/*
* Properties might need two kinds of values--current and changing. E.g. property.changing()
* is the same value, except that it includes results from key up events. Then derivative
* properties can be made off of either the current or changing property.
*
* Think of the login email box case:
*
* 1. User starts typing "foo@" -- do not do validation.
* 1a. The 'remaining' box is counting down from 100, 99, 98... chars left
* 2. User finishes typing "foo@bar.com!" -- stays in the box -- (auto-fire changed after X seconds? fancy, probably not)
* 3. User tabs out of the email box, blur/change fires, do validation
* 4. User fixes email by removing "!", redo validation as soon as key up is fired
*
* Or is 'remaining' just a view artifact, that is driven directly by key up,
* textBox.getValue and not by the backing model property?
*
* Maybe that makes more sense--the model is only updated when the user
* is no longer in input mode. While in input mode, the view can display
* various view-specific content, like length of input remaining. But
* there is no reason for the in-progress state to leak back into the model.
*
* ...but what about in error-mode correction? After a field/model have
* been touched, and determined invalid, don't we want to let the user
* know they've remedied the error ASAP, e.g. after key up? After a pause?
*/
/** Forces a {@link ValueChangeEvent} of each {@code source} on key up, after it's been touched. */
public <P, S extends HasKeyUpHandlers & HasBlurHandlers & HasValue<P>> void fireChangeOnBlurThenKeyUp(S... sources) {
for (final S source : sources) {
BlurThenKeyUp<P> h = new BlurThenKeyUp<P>(source);
add(source.addKeyUpHandler(h));
add(source.addBlurHandler(h));
}
}
/** Fires {@link ValueChangeEvent} on blur (always) and key up (after a blur has been fired). */
private static class BlurThenKeyUp<P> implements KeyUpHandler, BlurHandler {
private final HasValue<P> source;
private boolean hasBlurred = false;
/*
* Instead of only firing key-up-change if post-hasBlurred, it should be
* only firing key-up-change is the property is currenty invalid. Then
* a currently valid property would not eagerly invalidated as the user is
* changing it from 1 valid value to another valid value. It's only when
* properties are invalid that we want to fix things as soon as possible.
*
* For validation anyway...for characters left, that seems better suited to
* a separate "property.changing" derived property.
*/
private BlurThenKeyUp(HasValue<P> source) {
this.source = source;
}
@Override
public void onBlur(BlurEvent event) {
hasBlurred = true;
ValueChangeEvent.fire(source, source.getValue());
}
@Override
public void onKeyUp(KeyUpEvent event) {
if (!hasBlurred) {
return;
}
if (event.getNativeKeyCode() == KEY_TAB) {
return; // ignore tabbing into the field
}
ValueChangeEvent.fire(source, source.getValue());
}
}
/** Adds {@code registration} to be removed when this binder is unbound. */
public void add(HandlerRegistration registration) {
super.registerHandler(registration);
}
/** Adds {@code registration} to be removed when this binder is unbound. */
public void add(com.google.web.bindery.event.shared.HandlerRegistration registration) {
super.registerHandler(registration);
}
boolean canSetInitialValue(Property<?> property) {
return !property.isReadOnly() && !property.isTouched() && property.get() == null;
}
}