MultiListSelectPresenter.java revision 227a7a1d0484dcfa4c6d996a1c10e95437d059ef
1package autotest.common.ui;
2
3import com.google.gwt.event.dom.client.ChangeEvent;
4import com.google.gwt.event.dom.client.ChangeHandler;
5import com.google.gwt.event.dom.client.ClickEvent;
6import com.google.gwt.event.dom.client.ClickHandler;
7import com.google.gwt.event.dom.client.DoubleClickEvent;
8import com.google.gwt.event.dom.client.DoubleClickHandler;
9import com.google.gwt.event.dom.client.HasClickHandlers;
10import com.google.gwt.event.shared.GwtEvent;
11import com.google.gwt.event.shared.HandlerRegistration;
12
13import java.util.ArrayList;
14import java.util.Collections;
15import java.util.HashSet;
16import java.util.List;
17import java.util.Set;
18
19
20public class MultiListSelectPresenter implements ClickHandler, DoubleClickHandler, ChangeHandler {
21    /* Simple display showing two list boxes, one of available items and one of selected items */
22    public interface DoubleListDisplay {
23        public HasClickHandlers getAddAllButton();
24        public HasClickHandlers getAddButton();
25        public HasClickHandlers getRemoveButton();
26        public HasClickHandlers getRemoveAllButton();
27        public HasClickHandlers getMoveUpButton();
28        public HasClickHandlers getMoveDownButton();
29        public SimplifiedList getAvailableList();
30        public SimplifiedList getSelectedList();
31        // ListBoxes don't support DoubleClickEvents themselves, so the display needs to handle them
32        public HandlerRegistration addDoubleClickHandler(DoubleClickHandler handler);
33    }
34
35    /* Optional additional display allowing toggle between a simple ListBox and a
36     * DoubleListSelector
37     */
38    public interface ToggleDisplay {
39        public SimplifiedList getSingleSelector();
40        public ToggleControl getToggleMultipleLink();
41        public void setDoubleListVisible(boolean doubleListVisible);
42    }
43
44    public interface GeneratorHandler {
45        /**
46         * The given generator Item was just selected; create and return a new generated Item.
47         */
48        public Item generateItem(Item generatorItem);
49
50        /**
51         * The given generated Item was just deselected; handle any necessary cleanup.
52         */
53        public void onRemoveGeneratedItem(Item generatedItem);
54    }
55
56    public static class Item implements Comparable<Item> {
57        public String name;
58        public String value;
59        // a generator, when selected, generates a new item and selects that item instead
60        public boolean isGenerator;
61        // a generated item is destroyed when deselected.
62        public boolean isGeneratedItem;
63
64        private boolean selected;
65
66        private Item(String name, String value) {
67            this.name = name;
68            this.value = value;
69        }
70
71        public static Item createItem(String name, String value) {
72            return new Item(name, value);
73        }
74
75        public static Item createGenerator(String name, String value) {
76            Item item = new Item(name, value);
77            item.isGenerator = true;
78            return item;
79        }
80
81        public static Item createGeneratedItem(String name, String value) {
82            Item item = new Item(name, value);
83            item.isGeneratedItem = true;
84            return item;
85        }
86
87        public int compareTo(Item item) {
88            return name.compareTo(item.name);
89        }
90
91        @Override
92        public boolean equals(Object obj) {
93            if (!(obj instanceof Item)) {
94                return false;
95            }
96            Item other = (Item) obj;
97            return name.equals(other.name);
98        }
99
100        @Override
101        public int hashCode() {
102            return name.hashCode();
103        }
104
105        @Override
106        public String toString() {
107            return "Item<" + name + ", " + value + ">";
108        }
109
110        public boolean isSelected() {
111            if (isGenerator) {
112                return false;
113            }
114            if (isGeneratedItem) {
115                return true;
116            }
117            return selected;
118        }
119
120        public void setSelected(boolean selected) {
121            assert !isGenerator && !isGeneratedItem;
122            this.selected = selected;
123        }
124    }
125
126    private static class NullToggleDisplay implements ToggleDisplay {
127        @Override
128        public SimplifiedList getSingleSelector() {
129            return new SimplifiedList() {
130                @Override
131                public void addItem(String name, String value) {
132                    return;
133                }
134
135                @Override
136                public void clear() {
137                    return;
138                }
139
140                @Override
141                public String getSelectedName() {
142                    return "";
143                }
144
145                @Override
146                public void selectByName(String name) {
147                    return;
148                }
149
150                @Override
151                public HandlerRegistration addChangeHandler(ChangeHandler handler) {
152                    throw new UnsupportedOperationException();
153                }
154            };
155        }
156
157        @Override
158        public ToggleControl getToggleMultipleLink() {
159            return new ToggleControl() {
160                @Override
161                public HandlerRegistration addClickHandler(ClickHandler handler) {
162                    throw new UnsupportedOperationException();
163                }
164
165                @Override
166                public void fireEvent(GwtEvent<?> event) {
167                    throw new UnsupportedOperationException();
168                }
169
170                @Override
171                public boolean isActive() {
172                    return true;
173                }
174
175                @Override
176                public void setActive(boolean active) {
177                    return;
178                }
179            };
180        }
181
182        @Override
183        public void setDoubleListVisible(boolean doubleListVisible) {
184            return;
185        }
186    }
187
188    // convenience method
189    public static Set<String> getItemNameSet(List<Item> items) {
190        Set<String> nameSet = new HashSet<String>();
191        for (Item item : items) {
192            nameSet.add(item.name);
193        }
194        return nameSet;
195    }
196
197    private List<Item> items = new ArrayList<Item>();
198    // need a second list to track ordering
199    private List<Item> selectedItems = new ArrayList<Item>();
200    private DoubleListDisplay display;
201    private ToggleDisplay toggleDisplay = new NullToggleDisplay();
202    private GeneratorHandler generatorHandler;
203
204    public void setGeneratorHandler(GeneratorHandler handler) {
205        this.generatorHandler = handler;
206    }
207
208    public void bindDisplay(DoubleListDisplay display) {
209        this.display = display;
210        display.getAddAllButton().addClickHandler(this);
211        display.getAddButton().addClickHandler(this);
212        display.getRemoveButton().addClickHandler(this);
213        display.getRemoveAllButton().addClickHandler(this);
214        display.getMoveUpButton().addClickHandler(this);
215        display.getMoveDownButton().addClickHandler(this);
216        display.addDoubleClickHandler(this);
217    }
218
219    public void bindToggleDisplay(ToggleDisplay toggleDisplay) {
220        this.toggleDisplay = toggleDisplay;
221        toggleDisplay.getSingleSelector().addChangeHandler(this);
222        toggleDisplay.getToggleMultipleLink().addClickHandler(this);
223        toggleDisplay.getToggleMultipleLink().setActive(false);
224    }
225
226    private boolean verifyConsistency() {
227        // check consistency of selectedItems
228        for (Item item : items) {
229            if (item.isSelected() && !selectedItems.contains(item)) {
230                throw new RuntimeException("selectedItems is inconsistent, missing: "
231                                           + item.toString());
232            }
233        }
234        return true;
235    }
236
237    public void addItem(Item item) {
238        if (item.isGenerator) {
239            assert generatorHandler != null : "generator items require a GeneratorHandler";
240        }
241        items.add(item);
242        Collections.sort(items);
243        if (item.isSelected()) {
244            selectedItems.add(item);
245        }
246        assert verifyConsistency();
247        refresh();
248    }
249
250    private void removeItem(Item item) {
251        items.remove(item);
252        if (item.isSelected()) {
253            selectedItems.remove(item);
254        }
255        if (item.isGeneratedItem) {
256            generatorHandler.onRemoveGeneratedItem(item);
257        }
258        assert verifyConsistency();
259        refresh();
260    }
261
262    public void removeItemByName(String name) {
263        removeItem(getItemByName(name));
264    }
265
266    private void refreshSingleSelector() {
267        SimplifiedList selector = toggleDisplay.getSingleSelector();
268
269        boolean isGeneratedItemSelected = false;
270        if (!selectedItems.isEmpty()) {
271            assert selectedItems.size() == 1;
272            isGeneratedItemSelected = selectedItems.get(0).isGeneratedItem;
273        }
274
275        selector.clear();
276        for (Item item : items) {
277            if (item.isGenerator && isGeneratedItemSelected) {
278                continue;
279            }
280            selector.addItem(item.name, item.value);
281            if (item.isSelected()) {
282                selector.selectByName(item.name);
283            }
284        }
285    }
286
287    private void refreshMultipleSelector() {
288        display.getAvailableList().clear();
289        for (Item item : items) {
290            if (!item.isSelected()) {
291                display.getAvailableList().addItem(item.name, item.value);
292            }
293        }
294
295        display.getSelectedList().clear();
296        for (Item item : selectedItems) {
297            display.getSelectedList().addItem(item.name, item.value);
298        }
299    }
300
301    private void refresh() {
302        if (selectedItems.size() > 1) {
303            switchToMultiple();
304        }
305        if (isMultipleSelectActive()) {
306            refreshMultipleSelector();
307        } else {
308            // single selector always needs something selected
309            if (selectedItems.size() == 0) {
310                Item firstItem = getFirstNonGenerator(); // can't default to a generator
311                if (firstItem != null) {
312                    selectItem(items.get(0));
313                }
314            }
315            refreshSingleSelector();
316        }
317    }
318
319    private Item getFirstNonGenerator() {
320        for (Item item : items) {
321            if (!item.isGenerator) {
322                return item;
323            }
324        }
325        return null;
326    }
327
328    private void selectItem(Item item) {
329        if (item.isGenerator) {
330            Item generatedItem = generatorHandler.generateItem(item);
331            addItem(generatedItem);
332        } else {
333            item.setSelected(true);
334            selectedItems.add(item);
335        }
336
337        assert verifyConsistency();
338    }
339
340    public void selectItemByName(String name) {
341        selectItem(getItemByName(name));
342        refresh();
343    }
344
345    public void setSelectedItemsByName(List<String> names) {
346        for (String itemName : names) {
347            Item item = getItemByName(itemName);
348            if (!item.isSelected()) {
349                selectItem(item);
350            }
351        }
352
353        Set<String> selectedNames = new HashSet<String>(names);
354        for (Item item : getItemsCopy()) {
355            if (item.isSelected() && !selectedNames.contains(item.name)) {
356                deselectItem(item);
357            }
358        }
359
360        if (selectedItems.size() < 2) {
361            switchToSingle();
362        }
363        refresh();
364    }
365
366    private void deselectItem(Item item) {
367        if (item.isGeneratedItem) {
368            removeItem(item);
369        } else {
370            item.setSelected(false);
371            selectedItems.remove(item);
372        }
373        assert verifyConsistency();
374    }
375
376    public List<Item> getSelectedItems() {
377        return Collections.unmodifiableList(selectedItems);
378    }
379
380    private boolean isMultipleSelectActive() {
381        return toggleDisplay.getToggleMultipleLink().isActive();
382    }
383
384    private void switchToSingle() {
385        // reduce selection to the first selected item
386        while (selectedItems.size() > 1) {
387            deselectItem(selectedItems.get(1));
388        }
389
390        toggleDisplay.setDoubleListVisible(false);
391        toggleDisplay.getToggleMultipleLink().setActive(false);
392    }
393
394    private void switchToMultiple() {
395        toggleDisplay.setDoubleListVisible(true);
396        toggleDisplay.getToggleMultipleLink().setActive(true);
397    }
398
399    private Item getItemByName(String name) {
400        for (Item item : items) {
401            if (item.name.equals(name)) {
402                return item;
403            }
404        }
405
406        throw new IllegalArgumentException("Item '" + name + "' does not exist in " + items);
407    }
408
409    @Override
410    public void onClick(ClickEvent event) {
411        boolean isItemSelectedOnLeft = (display.getAvailableList().getSelectedName() != null);
412        boolean isItemSelectedOnRight = (display.getSelectedList().getSelectedName() != null);
413        Object source = event.getSource();
414        if (source == display.getAddAllButton()) {
415            addAll();
416        } else if (source == display.getAddButton() && isItemSelectedOnLeft) {
417            doSelect();
418        } else if (source == display.getRemoveButton() && isItemSelectedOnRight) {
419            doDeselect();
420        } else if (source == display.getRemoveAllButton()) {
421            deselectAll();
422        } else if ((source == display.getMoveUpButton() || source == display.getMoveDownButton())
423                   && isItemSelectedOnRight) {
424            reorderItem(source == display.getMoveUpButton());
425            return; // don't refresh again or we'll mess up the user's selection
426        } else if (source == toggleDisplay.getToggleMultipleLink()) {
427            if (toggleDisplay.getToggleMultipleLink().isActive()) {
428                switchToMultiple();
429            } else {
430                switchToSingle();
431            }
432        } else {
433            throw new RuntimeException("Unexpected ClickEvent from " + event.getSource());
434        }
435
436        refresh();
437    }
438
439    @Override
440    public void onDoubleClick(DoubleClickEvent event) {
441        Object source = event.getSource();
442        if (source == display.getAvailableList()) {
443            doSelect();
444        } else if (source == display.getSelectedList()) {
445            doDeselect();
446        } else {
447            // ignore double-clicks on other widgets
448            return;
449        }
450
451        refresh();
452    }
453
454    @Override
455    public void onChange(ChangeEvent event) {
456        assert toggleDisplay != null;
457        SimplifiedList selector = toggleDisplay.getSingleSelector();
458        assert event.getSource() == selector;
459        // events should only come from the single selector when it's active
460        assert !toggleDisplay.getToggleMultipleLink().isActive();
461
462        for (Item item : getItemsCopy()) {
463            if (item.isSelected()) {
464                deselectItem(item);
465            } else if (item.name.equals(selector.getSelectedName())) {
466                selectItem(item);
467            }
468        }
469
470        refresh();
471    }
472
473    /**
474     * Selecting or deselecting items can add or remove items (due to generators), so sometimes we
475     * need to iterate over a copy.
476     */
477    private Iterable<Item> getItemsCopy() {
478        return new ArrayList<Item>(items);
479    }
480
481    private void doSelect() {
482        selectItem(getItemByName(display.getAvailableList().getSelectedName()));
483    }
484
485    private void doDeselect() {
486        deselectItem(getItemByName(display.getSelectedList().getSelectedName()));
487    }
488
489    private void addAll() {
490        for (Item item : items) {
491            if (!item.isSelected() && !item.isGenerator) {
492                selectItem(item);
493            }
494        }
495    }
496
497    public void deselectAll() {
498        for (Item item : getItemsCopy()) {
499            if (item.isSelected()) {
500                deselectItem(item);
501            }
502        }
503    }
504
505    private void reorderItem(boolean moveUp) {
506        Item item = getItemByName(display.getSelectedList().getSelectedName());
507        int positionDelta = moveUp ? -1 : 1;
508        int newPosition = selectedItems.indexOf(item) + positionDelta;
509        newPosition = Math.max(0, Math.min(selectedItems.size() - 1, newPosition));
510        selectedItems.remove(item);
511        selectedItems.add(newPosition, item);
512        refresh();
513        display.getSelectedList().selectByName(item.name);
514    }
515}
516