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