package org.tessell.model.dsl; import static org.tessell.util.ObjectUtils.eq; import static org.tessell.util.StringUtils.sanitizeIfString; import java.util.HashMap; import java.util.List; import java.util.Map; import org.tessell.gwt.user.client.ui.HasCss; import org.tessell.gwt.user.client.ui.IsListBox; import org.tessell.gwt.user.client.ui.IsTextBox; import org.tessell.model.properties.HasMaxLength; import org.tessell.model.properties.ListProperty; import org.tessell.model.properties.Property; import org.tessell.util.Function; import org.tessell.util.ObjectUtils; import org.tessell.widgets.IsTextList; import com.google.gwt.event.dom.client.HasBlurHandlers; import com.google.gwt.event.dom.client.HasFocusHandlers; import com.google.gwt.event.dom.client.HasKeyUpHandlers; import com.google.gwt.user.client.TakesValue; import com.google.gwt.user.client.ui.HasValue; /** Binds properties to widgets. */ public class PropertyBinder<P> { protected final Binder b; protected final Property<P> p; private final boolean[] active = { false }; public PropertyBinder(final Binder b, final Property<P> p) { this.b = b; this.p = p; } /** Binds our property to {@code value} (one-way). */ public void to(final SetsValue<P> value) { b.add(p.addPropertyChangedHandler(e -> value.setValue(p.get()))); value.setValue(p.get()); } /** Binds our property to {@code value} (one-way). */ public void to(final TakesValue<P> value) { b.add(p.addPropertyChangedHandler(e -> value.setValue(p.get()))); // Set initial value. Even though this is one-way, if value is a cookie/etc., // we may want to set the initial value of our property back to the current // value of value. Do this only one, and only if the property looks unset // (non-touched and null). if (b.canSetInitialValue(p) && value.getValue() != null) { p.setInitialValue(value.getValue()); } else { value.setValue(p.get()); } } /** Binds our property to {@code source} (two-way). */ public void to(final HasValue<P> source) { final boolean[] isFocusing = { false }; // set initial value if (b.canSetInitialValue(p) && sanitizeIfString(source.getValue()) != null) { p.setInitialValue(sanitizeIfString(source.getValue())); } else { source.setValue(p.get(), true); } // after we've set the initial value (which fired ValueChangeEvent and // would have messed up our 'touched' state), listen for others changes if (!p.isReadOnly()) { b.add(source.addValueChangeHandler(e -> { p.set(sanitizeIfString(source.getValue())); })); if (source instanceof HasKeyUpHandlers) { b.add(((HasKeyUpHandlers) source).addKeyUpHandler(e -> { p.set(sanitizeIfString(source.getValue()), false); })); } if (source instanceof HasFocusHandlers && source instanceof HasBlurHandlers) { b.add(((HasFocusHandlers) source).addFocusHandler(e -> isFocusing[0] = true)); b.add(((HasBlurHandlers) source).addBlurHandler(e -> { isFocusing[0] = false; if (p.isValid()) { source.setValue(p.get(), true); } p.touch(); })); } } b.add(p.addPropertyChangedHandler(e -> { // if we're focusing by the source value is empty, go ahead and over write if (!isFocusing[0] || sanitizeIfString(source.getValue()) == null) { source.setValue(e.getProperty().get(), true); } })); if (p instanceof HasMaxLength && source instanceof IsTextBox) { final Integer length = ((HasMaxLength) p).getMaxLength(); if (length != null) { ((IsTextBox) source).setMaxLength(length.intValue()); } } } /** Binds our {@code p} to the selection in {@code source}, given the {@code options}. */ public void to(final IsListBox source, final List<P> options) { to(source, options, new ListBoxIdentityAdaptor<P>()); } public void to(final IsListBox source, final List<P> options, final Function<P, String> optionToDisplay) { to(source, options, new ListBoxAdaptor<P, P>() { @Override public String toDisplay(P option) { return optionToDisplay.get(option); } @Override public P toValue(P option) { return option; } }); } public <O> void to(// final IsListBox source, final List<O> options, final Function<O, String> optionToDisplay, final Function<O, P> optionToValue) { to(source, options, new ListBoxFunctionsAdaptor<P, O>(optionToDisplay, optionToValue)); } /** Binds our {@code p} to the selection in {@code source}, given the {@code options}. */ public <O> void to(final IsListBox source, final List<O> options, final ListBoxAdaptor<P, O> adaptor) { addOptionsAndSetIfNull(source, options, adaptor); b.add(source.addChangeHandler(e -> { final int i = source.getSelectedIndex(); // getSelectedIndex within an onchange should never be -1, but check just in case if (i != -1) { p.set(adaptor.toValue(options.get(i))); } })); b.add(p.addPropertyChangedHandler(e -> { setToFirstIfNull(options, adaptor); source.setSelectedIndex(indexInOptions(adaptor, options)); })); } /** Binds our {@code p} to the selection in {@code source}, given the {@code options}. */ public void to(final IsListBox source, final ListProperty<P> options) { to(source, options, new ListBoxIdentityAdaptor<P>()); } /** Binds our {@code p} to the selection in {@code source}, given the {@code options}. */ public <O> void to( final IsListBox source, final ListProperty<O> options, final Function<O, String> optionToDisplay, final Function<O, P> optionToValue) { to(source, options, new ListBoxFunctionsAdaptor<P, O>(optionToDisplay, optionToValue)); } /** Binds our {@code p} to the selection in {@code source}, given the {@code options}. */ public <O> void to(final IsListBox source, final ListProperty<O> options, final ListBoxAdaptor<P, O> adaptor) { if (options.get() != null) { addOptionsAndSetIfNull(source, options.get(), adaptor); } b.add(source.addChangeHandler(e -> { final int i = source.getSelectedIndex(); // getSelectedIndex within an onchange should never be -1, but check just in case if (i != -1) { p.set(adaptor.toValue(options.get().get(i))); } })); b.add(p.addPropertyChangedHandler(e -> { setToFirstIfNull(options.get(), adaptor); source.setSelectedIndex(indexInOptions(adaptor, options.get())); })); options.addPropertyChangedHandler(e -> { // it looks like this does not cause an onchange in the browser source.clear(); if (options.get() != null) { addOptionsAndSetIfNull(source, options.get(), adaptor); // reselect the 1st value if p's current value is not available. if (p.get() != null && indexInOptions(adaptor, options.get()) == -1 && !options.get().isEmpty()) { p.setInitialValue(adaptor.toValue(options.get().get(0))); } } }); } /** Binds errors for our property to {@code errors}. */ public void errorsTo(final IsTextList errors) { final TextListOnError i = new TextListOnError(errors); b.add(p.addRuleTriggeredHandler(i)); b.add(p.addRuleUntriggeredHandler(i)); i.addExisting(p); } /** Binds our property to {@code source} and its errors to {@code errors}. */ public void to(final HasValue<P> source, final IsTextList errors) { to(source); errorsTo(errors); } /** Binds our property to a list of radio buttons. */ public MoreRadioButtons to(HasValue<Boolean> button, P value) { return new MoreRadioButtons().and(button, value); } /** Binds our property to a list of radio buttons, and a view. */ public MoreRadioButtons to(HasValue<Boolean> button, P value, HasCss view) { return new MoreRadioButtons().and(button, value, view); } public class MoreRadioButtons { private final Map<HasValue<Boolean>, P> buttons = new HashMap<HasValue<Boolean>, P>(); private MoreRadioButtons() { // any time p changes, update all of the buttons. Granted, the browser // does this implicitly for radio buttons, but implementing the logic // like this means it will work for the stubs too. b.add(p.addPropertyChangedHandler(event -> { for (Map.Entry<HasValue<Boolean>, P> e : buttons.entrySet()) { boolean isForThisValue = eq(e.getValue(), event.getNewValue()); e.getKey().setValue(isForThisValue, true); } })); } public MoreRadioButtons and(final HasValue<Boolean> button, final P value) { return and(button, value, null); } public MoreRadioButtons and(final HasValue<Boolean> button, final P value, final HasCss view) { buttons.put(button, value); b.add(button.addValueChangeHandler(e -> { if (e.getValue()) { p.set(value); } })); button.setValue(eq(p.get(), value), true); // set initial if (view != null) { b.when(p).is(value).show(view); } return this; } } // can't use indexOf because we can't map value -> option, only option -> value private <O> int indexInOptions(ListBoxAdaptor<P, O> adaptor, List<O> options) { int i = 0; for (final O option : options) { if (ObjectUtils.eq(adaptor.toValue(option), p.get())) { return i; } i++; } return -1; } private <O> void addOptionsAndSetIfNull(final IsListBox source, final List<O> options, final ListBoxAdaptor<P, O> adaptor) { int i = 0; for (final O option : options) { source.addItem(adaptor.toDisplay(option), Integer.toString(i++)); } setToFirstIfNull(options, adaptor); source.setSelectedIndex(indexInOptions(adaptor, options)); } /** If our property is null, but the list of options doesn't contain {@code null}, auto-select the first valid value. */ private <O> void setToFirstIfNull(List<O> options, final ListBoxAdaptor<P, O> adaptor) { // Just to be cautious, call setInitialValue with an active check to prevent stack overflows // if the application code has a handler that tries to keep setting it back to null if (!active[0]) { active[0] = true; if (p.get() == null && !options.contains(null) && !options.isEmpty()) { p.setInitialValue(adaptor.toValue(options.get(0))); } active[0] = false; } } private static class ListBoxFunctionsAdaptor<P, O> implements ListBoxAdaptor<P, O> { private final Function<O, String> optionToDisplay; private final Function<O, P> optionToValue; private ListBoxFunctionsAdaptor(Function<O, String> optionToDisplay, Function<O, P> optionToValue) { this.optionToDisplay = optionToDisplay; this.optionToValue = optionToValue; } @Override public String toDisplay(O option) { // don't null check option, so that the lambda can provide it's own default/null value return optionToDisplay.get(option); } @Override public P toValue(O option) { return option == null ? null : optionToValue.get(option); } } }