package org.tessell.model.dsl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.tessell.gwt.user.client.ui.IsInsertPanel;
import org.tessell.gwt.user.client.ui.IsListBox;
import org.tessell.gwt.user.client.ui.IsWidget;
import org.tessell.model.properties.ListProperty;
import org.tessell.presenter.BasicPresenter;
import org.tessell.presenter.Presenter;
import org.tessell.util.ListDiff;
import org.tessell.util.ListDiff.ListLike;
import org.tessell.util.ListDiff.Location;
/** Fluent binding methods for {@link ListProperty}s. */
public class ListPropertyBinder<P> extends PropertyBinder<List<P>> {
/** Factory for creating {@link IsWidget}s for each item in a list. */
public interface ListViewFactory<P> {
IsWidget create(P value);
}
/** Factory for creating {@link Presenter}s for each item in a list. */
public interface ListPresenterFactory<P> {
Presenter create(P value);
}
private final ListProperty<P> p;
private final boolean[] active = { false };
public ListPropertyBinder(final Binder b, final ListProperty<P> p) {
super(b, p);
this.p = p;
}
/** Binds our {@code p} to the selection in {@code source}, given the {@code options}. */
public void toMultiple(final IsListBox source, final List<P> options) {
toMultiple(source, options, new ListBoxIdentityAdaptor<P>());
}
public void toMultiple(final IsListBox source, final List<P> options, final ListBoxLambdaAdaptor<P> adaptor) {
toMultiple(source, options, (ListBoxAdaptor<P, P>) adaptor);
}
/** Binds our {@code p} to the selection in {@code source}, given the {@code options}. */
public <O> void toMultiple(final IsListBox source, final List<O> options, final ListBoxAdaptor<P, O> adaptor) {
source.setMultipleSelect(true);
addOptionsAndSetIfNull(source, options, adaptor);
b.add(source.addChangeHandler(e -> {
if (!active[0]) {
active[0] = true;
// collect all currently-selected options
List<P> newOptions = new ArrayList<P>();
for (int i = 0; i < source.getItemCount(); i++) {
if (source.isItemSelected(i)) {
newOptions.add(adaptor.toValue(options.get(i)));
}
}
p.set(newOptions);
active[0] = false;
}
}));
b.add(p.addPropertyChangedHandler(e -> {
if (!active[0]) {
active[0] = true;
setToFirstIfNull(options, adaptor);
for (int i = 0; i < options.size(); i++) {
boolean contains = p.get().contains(adaptor.toValue(options.get(i)));
source.setItemSelected(i, contains);
}
active[0] = false;
}
}));
}
/** Binds each value in {@code p} to a view created by {@code factory}. */
public void to(final IsInsertPanel panel, final ListViewFactory<P> factory) {
to(new InsertPanelListLikeAdapter(panel), factory);
}
/** Binds each value in {@code p} to a view created by {@code factory}. */
public void to(final ListLike<IsWidget> panel, final ListViewFactory<P> factory) {
if (p.get() != null) {
int i = 0;
for (P value : p.get()) {
panel.add(i++, factory.create(value));
}
}
b.add(p.addListChangedHandler(e -> e.getDiff().apply(panel, a -> factory.create(a))));
}
public void to(final ListLike<P> panel) {
if (p.get() != null) {
int i = 0;
for (P value : p.get()) {
panel.add(i++, value);
}
}
b.add(p.addListChangedHandler(e -> e.getDiff().apply(panel, a -> a)));
}
/**
* Binds each value in {@code p} to a presenter created by {@code factory}.
*
* Also adds/removes the child presenters to the {@code parent} presenter for proper binding/unbinding.
*/
public void to(final BasicPresenter<?> parent, final IsInsertPanel panel, final ListPresenterFactory<P> factory) {
final InsertPanelListLikeAdapter adapter = new InsertPanelListLikeAdapter(panel);
// map to remember the model->presenter mapping so we know which view to remove later
final Map<P, Presenter> views = new HashMap<P, Presenter>();
if (p.get() != null) {
for (P value : p.get()) {
Presenter child = factory.create(value);
parent.addPresenter(child);
views.put(value, child);
panel.add(child.getView());
}
}
b.add(p.addListChangedHandler(e -> {
e.getDiff().apply(adapter, value -> {
Presenter child = factory.create(value);
parent.addPresenter(child);
views.put(value, child);
return child.getView();
});
for (Location<P> remove : e.getDiff().removed) {
Presenter child = views.remove(remove.element);
if (child != null) {
parent.removePresenter(child);
}
}
}));
}
private static class InsertPanelListLikeAdapter implements ListDiff.ListLike<IsWidget> {
private final IsInsertPanel panel;
private final int offsetForExistingContent;
private InsertPanelListLikeAdapter(IsInsertPanel panel) {
this.panel = panel;
this.offsetForExistingContent = panel.getWidgetCount();
}
@Override
public IsWidget remove(int index) {
IsWidget w = panel.getIsWidget(index + offsetForExistingContent);
panel.remove(index + offsetForExistingContent);
return w;
}
@Override
public void add(int index, IsWidget a) {
panel.insert(a, index + offsetForExistingContent);
}
}
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);
for (int j = 0; j < options.size(); j++) {
boolean contains = p.get().contains(adaptor.toValue(options.get(j)));
source.setItemSelected(j, contains);
}
}
/** 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()) {
List<P> initial = new ArrayList<P>();
initial.add(adaptor.toValue(options.get(0)));
p.setInitialValue(initial);
}
active[0] = false;
}
}
}