package com.vx.sw.client.jqm4gwt; import java.util.Collection; import java.util.Collections; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.jboss.errai.databinding.client.BindableListChangeHandler; import org.jboss.errai.databinding.client.BindableListWrapper; import com.google.gwt.event.dom.client.ChangeEvent; import com.google.gwt.event.dom.client.ChangeHandler; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.ui.ComplexPanel; import com.google.gwt.user.client.ui.HasValue; import com.sksamuel.jqm4gwt.list.JQMList; import com.sksamuel.jqm4gwt.list.JQMListDivider; import com.sksamuel.jqm4gwt.list.JQMListItem; /** * The same idea as {@link org.jboss.errai.ui.client.widget.ListWidget} but adopted for JQM4GWT. * <br> Binds model's List<M> with UI list items generated by some Renderer implementation. */ public class JQMListBindable<M> extends JQMList implements HasValue<List<M>>, BindableListChangeHandler<M> { public interface JQMListItemHandler { void onDivider(JQMListDivider divider); void onListItem(JQMListItem item); } public static void forEachListItem(List<? extends ComplexPanel> uiItems, JQMListItemHandler handler) { if (handler == null || uiItems == null || uiItems.isEmpty()) return; for (ComplexPanel i : uiItems) { if (i instanceof JQMListItem) { handler.onListItem((JQMListItem) i); } else if (i instanceof JQMListDivider) { handler.onDivider((JQMListDivider) i); } } } public interface Renderer<M> { List<? extends ComplexPanel> addItem(JQMListBindable<M> list, M item); /** * In some cases could be implemented as simple as addItem(). */ List<? extends ComplexPanel> insertItem(JQMListBindable<M> list, int dataIndex, M item); /** * @param uiItems - the same items which have been returned earlier by addItem() / insertItem() * for this particular model item. */ void removeItem(JQMListBindable<M> list, M item, List<? extends ComplexPanel> uiItems); /** * There are many possible reasons why/when this method is called: * <br> 1. data item's content changed, i.e. oldItem == newItem * <br> 2. new data item is replacing old one * <br> 3. sort() is called, so data items positions are exchanging (no real remove/add to the list) * * @param oldUiItems - the same items which have been returned earlier by addItem() / insertItem() * for this particular model item. * * @param oldItemDeleted - means item is not in data/model anymore, otherwise probably * it was just moved to a different position, for example by sort(). * * @param newUiItems - if newItem is already in data/model, then these ui items were returned * earlier by addItem() / insertItem(). Otherwise is just null. * * @return - must return new UI items for this model item. */ List<? extends ComplexPanel> itemChanged(JQMListBindable<M> list, int dataIndex, M oldItem, List<? extends ComplexPanel> oldUiItems, boolean oldItemDeleted, M newItem, List<? extends ComplexPanel> newUiItems); /** * Called after all add/remove operations are finished, right before list.refresh() call. */ void onBeforeRefresh(JQMListBindable<M> list); /** * Called after all add/remove operations and list.refresh() are finished. */ void onAfterRefresh(JQMListBindable<M> list); } public static abstract class BaseRenderer<M> implements Renderer<M> { @Override public List<? extends ComplexPanel> insertItem(JQMListBindable<M> list, int dataIndex, M item) { return addItem(list, item); } @Override public List<? extends ComplexPanel> itemChanged(JQMListBindable<M> list, int dataIndex, M oldItem, List<? extends ComplexPanel> oldUiItems, boolean oldItemDeleted, M newItem, List<? extends ComplexPanel> newUiItems) { if (oldItemDeleted) removeItem(list, oldItem, oldUiItems); if (newUiItems != null) removeItem(list, newItem, newUiItems); return insertItem(list, dataIndex, newItem); } @Override public void onBeforeRefresh(JQMListBindable<M> list) { } @Override public void onAfterRefresh(JQMListBindable<M> list) { } } public static abstract class ListRenderer<M> extends BaseRenderer<M> { private final boolean showEmptyMsg; private JQMListItem emptyMsg = null; private boolean unstableUiIndex; public ListRenderer(boolean showEmptyMsg, boolean unstableUiIndex) { this.showEmptyMsg = showEmptyMsg; this.unstableUiIndex = unstableUiIndex; } public ListRenderer() { this(true/*showEmptyMsg*/, false/*unstableUiIndex*/); } public ListRenderer(boolean showEmptyMsg) { this(showEmptyMsg, false/*unstableUiIndex*/); } protected abstract JQMListItem createListItem(M item); /** * @param item * @return - list of JQMListDivider and/or JQMListItem, needed in case of complex model item * when using createListItem() just is not enough. */ protected List<? extends ComplexPanel> createListItems(M item) { return null; } protected String getEmptyText() { return "-----"; } private void addEmptyMsg(JQMListBindable<M> list) { if (!showEmptyMsg) return; List<JQMListItem> items = list.getItems(); if (items == null || items.isEmpty()) { JQMListDivider d = (JQMListDivider) list.addDivider(""); emptyMsg = list.addItem(getEmptyText()); d.setTag(emptyMsg); } } /** @param list */ private void updateEmptyMsg(JQMListBindable<M> list) { if (!showEmptyMsg || emptyMsg == null) return; String s = getEmptyText(); emptyMsg.setText(s); } private void removeEmptyMsg(JQMListBindable<M> list) { if (!showEmptyMsg || emptyMsg == null) return; list.removeItem(emptyMsg); list.removeDividerByTag(emptyMsg); emptyMsg = null; } private List<? extends ComplexPanel> addItemIntern(final JQMListBindable<M> list, M item, final int position) { removeEmptyMsg(list); JQMListItem li = createListItem(item); if (li != null) { if (position >= 0) list.addItem(position, li); else list.appendItem(li); return Collections.singletonList(li); } else { List<? extends ComplexPanel> lst = createListItems(item); forEachListItem(lst, new JQMListItemHandler() { private int j = position; @Override public void onListItem(JQMListItem item) { if (position >= 0) list.addItem(j++, item); else list.appendItem(item); } @Override public void onDivider(JQMListDivider divider) { if (position >= 0) list.addDivider(j++, divider); else list.appendDivider(divider); } }); return lst; } } @Override public List<? extends ComplexPanel> addItem(final JQMListBindable<M> list, M item) { return addItemIntern(list, item, -1); } @Override public List<? extends ComplexPanel> insertItem(JQMListBindable<M> list, int dataIndex, M item) { if (unstableUiIndex) return addItem(list, item); List<M> data = list.getDataItems(); if (dataIndex < data.size() - 1) { M oldItem = data.get(dataIndex + 1); int oldPos = list.getUiIndex(oldItem); return addItemIntern(list, item, oldPos); } return addItem(list, item); } @Override public List<? extends ComplexPanel> itemChanged(JQMListBindable<M> list, int dataIndex, M oldItem, List<? extends ComplexPanel> oldUiItems, boolean oldItemDeleted, M newItem, List<? extends ComplexPanel> newUiItems) { // in case of sort() oldItem can be already processed as newItem on previous step! final int oldPos = (unstableUiIndex || !oldItemDeleted) ? -1 : list.getUiIndex(oldItem); if (oldItemDeleted) removeItem(list, oldItem, oldUiItems); if (newUiItems != null) removeItem(list, newItem, newUiItems); return addItemIntern(list, newItem, oldPos); } @Override public void removeItem(final JQMListBindable<M> list, M item, List<? extends ComplexPanel> uiItems) { forEachListItem(uiItems, new JQMListItemHandler() { @Override public void onDivider(JQMListDivider divider) { list.removeDivider(divider); } @Override public void onListItem(JQMListItem item) { list.removeItem(item); list.removeDividerByTag(item); }}); addEmptyMsg(list); } @Override public void onBeforeRefresh(JQMListBindable<M> list) { addEmptyMsg(list); list.recreate(); } @Override public void onAfterRefresh(JQMListBindable<M> list) { updateEmptyMsg(list); } public boolean isUnstableUiIndex() { return unstableUiIndex; } /** * Used during insertItem() and itemChanged() processing. * <br>Stable ui index means that there are no manual sort() or items exchange operations * over underlying/binded data items. So previous data item's ui index can be used as * base for current data item visual placement. */ public void setUnstableUiIndex(boolean unstableUiIndex) { this.unstableUiIndex = unstableUiIndex; } } /** * Should be used in case when each UI item is bindable by itself (i.e. actively listens for * its model changes, so should not be recreated each time when data item changed). */ public static abstract class ListRendererBoundUiItems<M> extends ListRenderer<M> { public ListRendererBoundUiItems() { super(true/*showEmptyMsg*/); } public ListRendererBoundUiItems(boolean showEmptyMsg) { super(showEmptyMsg); } @Override public List<? extends ComplexPanel> itemChanged(JQMListBindable<M> list, int dataIndex, M oldItem, List<? extends ComplexPanel> oldUiItems, boolean oldItemDeleted, M newItem, List<? extends ComplexPanel> newUiItems) { if (!oldItemDeleted && oldItem == newItem && newUiItems != null) return newUiItems; return super.itemChanged(list, dataIndex, oldItem, oldUiItems, oldItemDeleted, newItem, newUiItems); } } /** * An "ordered" JQMListBindable (in terms of Html, i.e. only 1, 2, 3, ... position indicator, no real data ordering) */ public static class Ordered<M> extends JQMListBindable<M> { public Ordered() { super(true); } } /** * An "unordered" JQMListBindable (in terms of Html, i.e. only no visual position indicator) */ public static class Unordered<M> extends JQMListBindable<M> { public Unordered() { super(false); } } private Renderer<M> renderer; private BindableListWrapper<M> dataItems; private final Map<M, List<? extends ComplexPanel>> dataToUI = new IdentityHashMap<>(); private boolean valueChangeHandlerInitialized; public JQMListBindable() { this(false/*ordered*/); } public JQMListBindable(boolean ordered) { super(ordered); } @Override public void clear() { super.clear(); dataToUI.clear(); } /** * Sets the list of model objects. * The list will be wrapped in an {@link BindableListWrapper} to make direct changes * to the list observable. * * @param items - The list of model objects. If null or empty all existing items will be removed. */ public void setDataItems(List<M> items) { boolean changed = this.dataItems != items; if (items == null) { this.dataItems = null; } else { this.dataItems = items instanceof BindableListWrapper ? (BindableListWrapper<M>) items : new BindableListWrapper<M>(items); if (changed) this.dataItems.addChangeHandler(this); } addDataItems(); } public List<M> getDataItems() { return dataItems; } private void addDataItems() { clear(); if (dataItems != null && !dataItems.isEmpty()) { for (final M item : dataItems) { addDataItem(item); } } doRefresh(); } private void addDataItem(final M item) { List<? extends ComplexPanel> ui = renderer.addItem(this, item); dataToUI.put(item, ui); } private void insertDataItem(final int index, final M item) { List<? extends ComplexPanel> ui = renderer.insertItem(this, index, item); dataToUI.put(item, ui); } private void removeDataItem(final M item) { List<? extends ComplexPanel> ui = dataToUI.get(item); renderer.removeItem(this, item, ui); dataToUI.remove(item); } private void dataItemChanged(final int index, final M oldItem, final M newItem) { // in case of sort() item can appear as newItem first, then later as oldItem // Example: 0: 7>0, 1: 5>1, 2: 3>2, 3: 0>3, 4: 6>4, 5: 2>5, 6: 1>6, 7: 4>7 // Simple example (size equals 2): // 0: 1>0 (at this step dataItems has two identical elements), 1: 0>1 List<? extends ComplexPanel> oldUi = dataToUI.get(oldItem); List<? extends ComplexPanel> newUi = dataToUI.get(newItem); boolean oldInData = dataItems.contains(oldItem); newUi = renderer.itemChanged(this, index, oldItem, oldUi, !oldInData, newItem, newUi); if (oldItem != newItem && !oldInData) dataToUI.remove(oldItem); dataToUI.put(newItem, newUi); } private void doRefresh() { renderer.onBeforeRefresh(this); refresh(); renderer.onAfterRefresh(this); } public M getDataItem(JQMListItem uiItem) { if (uiItem == null) return null; for (Entry<M, List<? extends ComplexPanel>> i : dataToUI.entrySet()) { List<? extends ComplexPanel> uiItems = i.getValue(); if (uiItems.contains(uiItem)) return i.getKey(); } return null; } public List<? extends ComplexPanel> getUiItems(M dataItem) { if (dataItem == null) return null; return dataToUI.get(dataItem); } /** * @return - first visual position for this data item, or -1 otherwise. */ public int getUiIndex(M dataItem) { List<? extends ComplexPanel> ui = getUiItems(dataItem); if (ui == null || ui.isEmpty()) return -1; ComplexPanel item = ui.get(0); if (item instanceof JQMListItem) { return getItems().indexOf(item); } else if (item instanceof JQMListDivider) { return findDividerIdx((JQMListDivider) item); } else { return -1; } } @Override public HandlerRegistration addValueChangeHandler(ValueChangeHandler<List<M>> handler) { if (!valueChangeHandlerInitialized) { valueChangeHandlerInitialized = true; addDomHandler(new ChangeHandler() { @Override public void onChange(ChangeEvent event) { ValueChangeEvent.fire(JQMListBindable.this, getValue()); } }, ChangeEvent.getType()); } return addHandler(handler, ValueChangeEvent.getType()); } @Override public List<M> getValue() { return dataItems; } @Override public void setValue(List<M> value) { setValue(value, false); } @Override public void setValue(List<M> value, boolean fireEvents) { List<M> oldValue = getValue(); // if list changed, BindibleProxy will call updateWidgetsAndFire() -> this.setValue() // but we may already listen to passed list and in such case should not call setDataItems() if (oldValue != value) setDataItems(value); if (fireEvents) { ValueChangeEvent.fireIfNotEqual(this, oldValue, value); } } @Override public void onItemAdded(List<M> oldList, M item) { addDataItem(item); doRefresh(); } @Override public void onItemAddedAt(List<M> oldList, int index, M item) { insertDataItem(index, item); doRefresh(); } @Override public void onItemsAdded(List<M> oldList, Collection<? extends M> items) { for (M m : items) { addDataItem(m); } doRefresh(); } @Override public void onItemsAddedAt(List<M> oldList, int index, Collection<? extends M> item) { Iterator<? extends M> iter = item.iterator(); int pos = index; while (iter.hasNext()) { M i = iter.next(); insertDataItem(pos, i); pos++; } doRefresh(); } @Override public void onItemsCleared(List<M> oldList) { clear(); doRefresh(); } @Override public void onItemRemovedAt(List<M> oldList, int index) { removeDataItem(oldList.get(index)); doRefresh(); } @Override public void onItemsRemovedAt(List<M> oldList, List<Integer> indexes) { for (Integer index : indexes) { removeDataItem(oldList.get(index)); } doRefresh(); } @Override public void onItemChanged(List<M> oldList, int index, M item) { dataItemChanged(index, oldList.get(index), item); doRefresh(); } /** * Needed in case of renderer changed, so list has to be visually reconstructed by new renderer. */ public void rerender() { clear(); if (dataItems != null) { for (M m : dataItems) { addDataItem(m); } } doRefresh(); } public Renderer<M> getRenderer() { return renderer; } public void setRenderer(Renderer<M> renderer) { this.renderer = renderer; } }