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