1 package autotest.common.ui; 2 3 import com.google.gwt.event.dom.client.ChangeEvent; 4 import com.google.gwt.event.dom.client.ChangeHandler; 5 import com.google.gwt.event.dom.client.ClickEvent; 6 import com.google.gwt.event.dom.client.ClickHandler; 7 import com.google.gwt.event.dom.client.DoubleClickEvent; 8 import com.google.gwt.event.dom.client.DoubleClickHandler; 9 import com.google.gwt.event.dom.client.HasClickHandlers; 10 import com.google.gwt.event.shared.GwtEvent; 11 import com.google.gwt.event.shared.HandlerRegistration; 12 13 import java.util.ArrayList; 14 import java.util.Collections; 15 import java.util.HashSet; 16 import java.util.List; 17 import java.util.Set; 18 19 20 public 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 { getAddAllButton()23 public HasClickHandlers getAddAllButton(); getAddButton()24 public HasClickHandlers getAddButton(); getRemoveButton()25 public HasClickHandlers getRemoveButton(); getRemoveAllButton()26 public HasClickHandlers getRemoveAllButton(); getMoveUpButton()27 public HasClickHandlers getMoveUpButton(); getMoveDownButton()28 public HasClickHandlers getMoveDownButton(); getAvailableList()29 public SimplifiedList getAvailableList(); getSelectedList()30 public SimplifiedList getSelectedList(); 31 // ListBoxes don't support DoubleClickEvents themselves, so the display needs to handle them addDoubleClickHandler(DoubleClickHandler handler)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 { getSingleSelector()39 public SimplifiedList getSingleSelector(); getToggleMultipleLink()40 public ToggleControl getToggleMultipleLink(); setDoubleListVisible(boolean doubleListVisible)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 */ onRemoveGeneratedItem(Item generatedItem)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 Item(String name, String value)59 private Item(String name, String value) { 60 this.name = name; 61 this.value = value; 62 } 63 createItem(String name, String value)64 public static Item createItem(String name, String value) { 65 return new Item(name, value); 66 } 67 createGeneratedItem(String name, String value)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 compareTo(Item item)74 public int compareTo(Item item) { 75 return name.compareTo(item.name); 76 } 77 78 @Override equals(Object obj)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 hashCode()88 public int hashCode() { 89 return name.hashCode(); 90 } 91 92 @Override toString()93 public String toString() { 94 return "Item<" + name + ", " + value + ">"; 95 } 96 isSelected()97 private boolean isSelected() { 98 if (isGeneratedItem) { 99 return true; 100 } 101 return selected; 102 } 103 setSelected(boolean selected)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 getSingleSelector()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 getToggleMultipleLink()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