1 /* 2 * Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.scene.control; 27 28 import javafx.beans.property.ObjectProperty; 29 import javafx.beans.property.ObjectPropertyBase; 30 import javafx.beans.property.SimpleObjectProperty; 31 import javafx.beans.value.ChangeListener; 32 import javafx.collections.FXCollections; 33 import javafx.collections.ListChangeListener; 34 import javafx.collections.ObservableList; 35 import javafx.beans.property.ReadOnlyBooleanProperty; 36 import javafx.beans.property.ReadOnlyBooleanWrapper; 37 import javafx.event.ActionEvent; 38 import javafx.event.Event; 39 import javafx.event.EventHandler; 40 import javafx.event.EventType; 41 import javafx.scene.AccessibleAction; 42 import javafx.scene.AccessibleAttribute; 43 import javafx.scene.AccessibleRole; 44 import javafx.util.StringConverter; 45 import javafx.css.PseudoClass; 46 47 import javafx.scene.control.skin.ChoiceBoxSkin; 48 49 import javafx.beans.DefaultProperty; 50 51 /** 52 * The ChoiceBox is used for presenting the user with a relatively small set of 53 * predefined choices from which they may choose. The ChoiceBox, when "showing", 54 * will display to the user these choices and allow them to pick exactly one 55 * choice. When not showing, the current choice is displayed. 56 * <p> 57 * By default, the ChoiceBox has no item selected unless otherwise specified. 58 * Although the ChoiceBox will only allow a user to select from the predefined 59 * list, it is possible for the developer to specify the selected item to be 60 * something other than what is available in the predefined list. This is 61 * required for several important use cases. 62 * <p> 63 * It means configuration of the ChoiceBox is order independent. You 64 * may either specify the items and then the selected item, or you may 65 * specify the selected item and then the items. Either way will function 66 * correctly. 67 * <p> 68 * ChoiceBox item selection is handled by 69 * {@link javafx.scene.control.SelectionModel SelectionModel} 70 * As with ListView and ComboBox, it is possible to modify the 71 * {@link javafx.scene.control.SelectionModel SelectionModel} that is used, 72 * although this is likely to be rarely changed. ChoiceBox supports only a 73 * single selection model, hence the default used is a {@link SingleSelectionModel}. 74 * 75 * <pre> 76 * import javafx.scene.control.ChoiceBox; 77 * 78 * ChoiceBox cb = new ChoiceBox(); 79 * cb.getItems().addAll("item1", "item2", "item3"); 80 * </pre> 81 * @since JavaFX 2.0 82 */ 83 @DefaultProperty("items") 84 public class ChoiceBox<T> extends Control { 85 86 /*************************************************************************** 87 * * 88 * Static properties and methods * 89 * * 90 **************************************************************************/ 91 92 /** 93 * Called prior to the ChoiceBox showing its popup after the user 94 * has clicked or otherwise interacted with the ChoiceBox. 95 * @since JavaFX 8u60 96 */ 97 public static final EventType<Event> ON_SHOWING = 98 new EventType<Event>(Event.ANY, "CHOICE_BOX_ON_SHOWING"); 99 100 /** 101 * Called after the ChoiceBox has shown its popup. 102 * @since JavaFX 8u60 103 */ 104 public static final EventType<Event> ON_SHOWN = 105 new EventType<Event>(Event.ANY, "CHOICE_BOX_ON_SHOWN"); 106 107 /** 108 * Called when the ChoiceBox popup <b>will</b> be hidden. 109 * @since JavaFX 8u60 110 */ 111 public static final EventType<Event> ON_HIDING = 112 new EventType<Event>(Event.ANY, "CHOICE_BOX_ON_HIDING"); 113 114 /** 115 * Called when the ChoiceBox popup has been hidden. 116 * @since JavaFX 8u60 117 */ 118 public static final EventType<Event> ON_HIDDEN = 119 new EventType<Event>(Event.ANY, "CHOICE_BOX_ON_HIDDEN"); 120 121 122 123 /*************************************************************************** 124 * * 125 * Constructors * 126 * * 127 **************************************************************************/ 128 129 /** 130 * Create a new ChoiceBox which has an empty list of items. 131 */ 132 public ChoiceBox() { 133 this(FXCollections.<T>observableArrayList()); 134 } 135 136 /** 137 * Create a new ChoiceBox with the given set of items. Since it is observable, 138 * the content of this list may change over time and the ChoiceBox will 139 * be updated accordingly. 140 * @param items 141 */ 142 public ChoiceBox(ObservableList<T> items) { 143 getStyleClass().setAll("choice-box"); 144 setAccessibleRole(AccessibleRole.COMBO_BOX); 145 setItems(items); 146 setSelectionModel(new ChoiceBoxSelectionModel<T>(this)); 147 148 // listen to the value property, if the value is 149 // set to something that exists in the items list, update the 150 // selection model to indicate that this is the selected item 151 valueProperty().addListener((ov, t, t1) -> { 152 if (getItems() == null) return; 153 int index = getItems().indexOf(t1); 154 if (index > -1) { 155 getSelectionModel().select(index); 156 } 157 }); 158 } 159 160 /*************************************************************************** 161 * * 162 * Properties * 163 * * 164 **************************************************************************/ 165 166 /** 167 * The selection model for the ChoiceBox. Only a single choice can be made, 168 * hence, the ChoiceBox supports only a SingleSelectionModel. Generally, the 169 * main interaction with the selection model is to explicitly set which item 170 * in the items list should be selected, or to listen to changes in the 171 * selection to know which item has been chosen. 172 */ 173 private ObjectProperty<SingleSelectionModel<T>> selectionModel = 174 new SimpleObjectProperty<SingleSelectionModel<T>>(this, "selectionModel") { 175 private SelectionModel<T> oldSM = null; 176 @Override protected void invalidated() { 177 if (oldSM != null) { 178 oldSM.selectedItemProperty().removeListener(selectedItemListener); 179 } 180 SelectionModel<T> sm = get(); 181 oldSM = sm; 182 if (sm != null) { 183 sm.selectedItemProperty().addListener(selectedItemListener); 184 if (sm.getSelectedItem() != null && ! valueProperty().isBound()) { 185 ChoiceBox.this.setValue(sm.getSelectedItem()); 186 } 187 } 188 } 189 }; 190 191 private ChangeListener<T> selectedItemListener = (ov, t, t1) -> { 192 if (! valueProperty().isBound()) { 193 setValue(t1); 194 } 195 }; 196 197 198 public final void setSelectionModel(SingleSelectionModel<T> value) { selectionModel.set(value); } 199 public final SingleSelectionModel<T> getSelectionModel() { return selectionModel.get(); } 200 public final ObjectProperty<SingleSelectionModel<T>> selectionModelProperty() { return selectionModel; } 201 202 203 /** 204 * Indicates whether the drop down is displaying the list of choices to the 205 * user. This is a readonly property which should be manipulated by means of 206 * the #show and #hide methods. 207 */ 208 private ReadOnlyBooleanWrapper showing = new ReadOnlyBooleanWrapper() { 209 @Override protected void invalidated() { 210 pseudoClassStateChanged(SHOWING_PSEUDOCLASS_STATE, get()); 211 notifyAccessibleAttributeChanged(AccessibleAttribute.EXPANDED); 212 } 213 214 @Override 215 public Object getBean() { 216 return ChoiceBox.this; 217 } 218 219 @Override 220 public String getName() { 221 return "showing"; 222 } 223 }; 224 public final boolean isShowing() { return showing.get(); } 225 public final ReadOnlyBooleanProperty showingProperty() { return showing.getReadOnlyProperty(); } 226 private void setShowing(boolean value) { 227 // these events will not fire if the showing property is bound 228 Event.fireEvent(this, value ? new Event(ComboBoxBase.ON_SHOWING) : 229 new Event(ComboBoxBase.ON_HIDING)); 230 showing.set(value); 231 Event.fireEvent(this, value ? new Event(ComboBoxBase.ON_SHOWN) : 232 new Event(ComboBoxBase.ON_HIDDEN)); 233 } 234 235 /** 236 * The items to display in the choice box. The selected item (as indicated in the 237 * selection model) must always be one of these items. 238 */ 239 private ObjectProperty<ObservableList<T>> items = new ObjectPropertyBase<ObservableList<T>>() { 240 ObservableList<T> old; 241 @Override protected void invalidated() { 242 final ObservableList<T> newItems = get(); 243 if (old != newItems) { 244 // Add and remove listeners 245 if (old != null) old.removeListener(itemsListener); 246 if (newItems != null) newItems.addListener(itemsListener); 247 // Clear the selection model 248 final SingleSelectionModel<T> sm = getSelectionModel(); 249 if (sm != null) { 250 if (newItems != null && newItems.isEmpty()) { 251 // RT-29433 - clear selection. 252 sm.clearSelection(); 253 } else if (sm.getSelectedIndex() == -1 && sm.getSelectedItem() != null) { 254 int newIndex = getItems().indexOf(sm.getSelectedItem()); 255 if (newIndex != -1) { 256 sm.setSelectedIndex(newIndex); 257 } 258 } else sm.clearSelection(); 259 } 260 // if (sm != null) sm.setSelectedIndex(-1); 261 // Save off the old items 262 old = newItems; 263 } 264 } 265 266 @Override 267 public Object getBean() { 268 return ChoiceBox.this; 269 } 270 271 @Override 272 public String getName() { 273 return "items"; 274 } 275 }; 276 public final void setItems(ObservableList<T> value) { items.set(value); } 277 public final ObservableList<T> getItems() { return items.get(); } 278 public final ObjectProperty<ObservableList<T>> itemsProperty() { return items; } 279 280 private final ListChangeListener<T> itemsListener = c -> { 281 final SingleSelectionModel<T> sm = getSelectionModel(); 282 if (sm!= null) { 283 if (getItems() == null || getItems().isEmpty()) { 284 sm.clearSelection(); 285 } else { 286 int newIndex = getItems().indexOf(sm.getSelectedItem()); 287 sm.setSelectedIndex(newIndex); 288 } 289 } 290 if (sm != null) { 291 292 // Look for the selected item as having been removed. If it has been, 293 // then we need to clear the selection in the selection model. 294 final T selectedItem = sm.getSelectedItem(); 295 while (c.next()) { 296 if (selectedItem != null && c.getRemoved().contains(selectedItem)) { 297 sm.clearSelection(); 298 break; 299 } 300 } 301 } 302 }; 303 304 /** 305 * Allows a way to specify how to represent objects in the items list. When 306 * a StringConverter is set, the object toString method is not called and 307 * instead its toString(object T) is called, passing the objects in the items list. 308 * This is useful when using domain objects in a ChoiceBox as this property 309 * allows for customization of the representation. Also, any of the pre-built 310 * Converters available in the {@link javafx.util.converter} package can be set. 311 * @since JavaFX 2.1 312 */ 313 public ObjectProperty<StringConverter<T>> converterProperty() { return converter; } 314 private ObjectProperty<StringConverter<T>> converter = 315 new SimpleObjectProperty<StringConverter<T>>(this, "converter", null); 316 public final void setConverter(StringConverter<T> value) { converterProperty().set(value); } 317 public final StringConverter<T> getConverter() {return converterProperty().get(); } 318 319 /** 320 * The value of this ChoiceBox is defined as the selected item in the ChoiceBox 321 * selection model. The valueProperty is synchronized with the selectedItem. 322 * This property allows for bi-directional binding of external properties to the 323 * ChoiceBox and updates the selection model accordingly. 324 * @since JavaFX 2.1 325 */ 326 public ObjectProperty<T> valueProperty() { return value; } 327 private ObjectProperty<T> value = new SimpleObjectProperty<T>(this, "value") { 328 @Override protected void invalidated() { 329 super.invalidated(); 330 fireEvent(new ActionEvent()); 331 // Update selection 332 final SingleSelectionModel<T> sm = getSelectionModel(); 333 if (sm != null) { 334 sm.select(super.getValue()); 335 } 336 notifyAccessibleAttributeChanged(AccessibleAttribute.TEXT); 337 } 338 }; 339 public final void setValue(T value) { valueProperty().set(value); } 340 public final T getValue() { return valueProperty().get(); } 341 342 343 // --- On Action 344 /** 345 * The ChoiceBox action, which is invoked whenever the ChoiceBox 346 * {@link #valueProperty() value} property is changed. This 347 * may be due to the value property being programmatically changed or when the 348 * user selects an item in a popup menu. 349 * 350 * @since JavaFX 8u60 351 */ 352 public final ObjectProperty<EventHandler<ActionEvent>> onActionProperty() { return onAction; } 353 public final void setOnAction(EventHandler<ActionEvent> value) { onActionProperty().set(value); } 354 public final EventHandler<ActionEvent> getOnAction() { return onActionProperty().get(); } 355 private ObjectProperty<EventHandler<ActionEvent>> onAction = new ObjectPropertyBase<EventHandler<ActionEvent>>() { 356 @Override protected void invalidated() { 357 setEventHandler(ActionEvent.ACTION, get()); 358 } 359 360 @Override 361 public Object getBean() { 362 return ChoiceBox.this; 363 } 364 365 @Override 366 public String getName() { 367 return "onAction"; 368 } 369 }; 370 371 372 // --- On Showing 373 /** 374 * Called just prior to the {@code ChoiceBox} popup being shown. 375 * @since JavaFX 8u60 376 */ 377 public final ObjectProperty<EventHandler<Event>> onShowingProperty() { return onShowing; } 378 public final void setOnShowing(EventHandler<Event> value) { onShowingProperty().set(value); } 379 public final EventHandler<Event> getOnShowing() { return onShowingProperty().get(); } 380 private ObjectProperty<EventHandler<Event>> onShowing = new ObjectPropertyBase<EventHandler<Event>>() { 381 @Override protected void invalidated() { 382 setEventHandler(ON_SHOWING, get()); 383 } 384 385 @Override public Object getBean() { 386 return ChoiceBox.this; 387 } 388 389 @Override public String getName() { 390 return "onShowing"; 391 } 392 }; 393 394 395 // -- On Shown 396 /** 397 * Called just after the {@link ChoiceBox} popup is shown. 398 * @since JavaFX 8u60 399 */ 400 public final ObjectProperty<EventHandler<Event>> onShownProperty() { return onShown; } 401 public final void setOnShown(EventHandler<Event> value) { onShownProperty().set(value); } 402 public final EventHandler<Event> getOnShown() { return onShownProperty().get(); } 403 private ObjectProperty<EventHandler<Event>> onShown = new ObjectPropertyBase<EventHandler<Event>>() { 404 @Override protected void invalidated() { 405 setEventHandler(ON_SHOWN, get()); 406 } 407 408 @Override public Object getBean() { 409 return ChoiceBox.this; 410 } 411 412 @Override public String getName() { 413 return "onShown"; 414 } 415 }; 416 417 418 // --- On Hiding 419 /** 420 * Called just prior to the {@link ChoiceBox} popup being hidden. 421 * @since JavaFX 8u60 422 */ 423 public final ObjectProperty<EventHandler<Event>> onHidingProperty() { return onHiding; } 424 public final void setOnHiding(EventHandler<Event> value) { onHidingProperty().set(value); } 425 public final EventHandler<Event> getOnHiding() { return onHidingProperty().get(); } 426 private ObjectProperty<EventHandler<Event>> onHiding = new ObjectPropertyBase<EventHandler<Event>>() { 427 @Override protected void invalidated() { 428 setEventHandler(ON_HIDING, get()); 429 } 430 431 @Override public Object getBean() { 432 return ChoiceBox.this; 433 } 434 435 @Override public String getName() { 436 return "onHiding"; 437 } 438 }; 439 440 441 // --- On Hidden 442 /** 443 * Called just after the {@link ChoiceBox} popup has been hidden. 444 * @since JavaFX 8u60 445 */ 446 public final ObjectProperty<EventHandler<Event>> onHiddenProperty() { return onHidden; } 447 public final void setOnHidden(EventHandler<Event> value) { onHiddenProperty().set(value); } 448 public final EventHandler<Event> getOnHidden() { return onHiddenProperty().get(); } 449 private ObjectProperty<EventHandler<Event>> onHidden = new ObjectPropertyBase<EventHandler<Event>>() { 450 @Override protected void invalidated() { 451 setEventHandler(ON_HIDDEN, get()); 452 } 453 454 @Override public Object getBean() { 455 return ChoiceBox.this; 456 } 457 458 @Override public String getName() { 459 return "onHidden"; 460 } 461 }; 462 463 /*************************************************************************** 464 * * 465 * Methods * 466 * * 467 **************************************************************************/ 468 469 /** 470 * Opens the list of choices. 471 */ 472 public void show() { 473 if (!isDisabled()) setShowing(true); 474 } 475 476 /** 477 * Closes the list of choices. 478 */ 479 public void hide() { 480 setShowing(false); 481 } 482 483 /** {@inheritDoc} */ 484 @Override protected Skin<?> createDefaultSkin() { 485 return new ChoiceBoxSkin<T>(this); 486 } 487 488 /*************************************************************************** 489 * * 490 * Stylesheet Handling * 491 * * 492 **************************************************************************/ 493 494 private static final PseudoClass SHOWING_PSEUDOCLASS_STATE = 495 PseudoClass.getPseudoClass("showing"); 496 497 // package for testing 498 static class ChoiceBoxSelectionModel<T> extends SingleSelectionModel<T> { 499 private final ChoiceBox<T> choiceBox; 500 501 public ChoiceBoxSelectionModel(final ChoiceBox<T> cb) { 502 if (cb == null) { 503 throw new NullPointerException("ChoiceBox can not be null"); 504 } 505 this.choiceBox = cb; 506 507 /* 508 * The following two listeners are used in conjunction with 509 * SelectionModel.select(T obj) to allow for a developer to select 510 * an item that is not actually in the data model. When this occurs, 511 * we actively try to find an index that matches this object, going 512 * so far as to actually watch for all changes to the items list, 513 * rechecking each time. 514 */ 515 516 // watching for changes to the items list content 517 final ListChangeListener<T> itemsContentObserver = c -> { 518 if (choiceBox.getItems() == null || choiceBox.getItems().isEmpty()) { 519 setSelectedIndex(-1); 520 } else if (getSelectedIndex() == -1 && getSelectedItem() != null) { 521 int newIndex = choiceBox.getItems().indexOf(getSelectedItem()); 522 if (newIndex != -1) { 523 setSelectedIndex(newIndex); 524 } 525 } 526 }; 527 if (this.choiceBox.getItems() != null) { 528 this.choiceBox.getItems().addListener(itemsContentObserver); 529 } 530 531 // watching for changes to the items list 532 ChangeListener<ObservableList<T>> itemsObserver = (valueModel, oldList, newList) -> { 533 if (oldList != null) { 534 oldList.removeListener(itemsContentObserver); 535 } 536 if (newList != null) { 537 newList.addListener(itemsContentObserver); 538 } 539 setSelectedIndex(-1); 540 if (getSelectedItem() != null) { 541 int newIndex = choiceBox.getItems().indexOf(getSelectedItem()); 542 if (newIndex != -1) { 543 setSelectedIndex(newIndex); 544 } 545 } 546 }; 547 this.choiceBox.itemsProperty().addListener(itemsObserver); 548 } 549 550 // API Implementation 551 @Override protected T getModelItem(int index) { 552 final ObservableList<T> items = choiceBox.getItems(); 553 if (items == null) return null; 554 if (index < 0 || index >= items.size()) return null; 555 return items.get(index); 556 } 557 558 @Override protected int getItemCount() { 559 final ObservableList<T> items = choiceBox.getItems(); 560 return items == null ? 0 : items.size(); 561 } 562 563 /** 564 * Selects the given row. Since the SingleSelectionModel can only support having 565 * a single row selected at a time, this also causes any previously selected 566 * row to be unselected. 567 * This method is overridden here so that we can move past a Separator 568 * in a ChoiceBox and select the next valid menuitem. 569 */ 570 @Override public void select(int index) { 571 // this does not sound right, we should let the superclass handle it. 572 super.select(index); 573 574 if (choiceBox.isShowing()) { 575 choiceBox.hide(); 576 } 577 } 578 579 /** {@inheritDoc} */ 580 @Override public void selectPrevious() { 581 // overridden to properly handle Separators 582 int index = getSelectedIndex() - 1; 583 while (index >= 0) { 584 final T value = getModelItem(index); 585 if (value instanceof Separator) { 586 index--; 587 } else { 588 select(index); 589 break; 590 } 591 } 592 } 593 594 /** {@inheritDoc} */ 595 @Override public void selectNext() { 596 // overridden to properly handle Separators 597 int index = getSelectedIndex() + 1; 598 while (index < getItemCount()) { 599 final T value = getModelItem(index); 600 if (value instanceof Separator) { 601 index++; 602 } else { 603 select(index); 604 break; 605 } 606 } 607 } 608 } 609 610 /*************************************************************************** 611 * * 612 * Accessibility handling * 613 * * 614 **************************************************************************/ 615 616 @Override 617 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 618 switch(attribute) { 619 case TEXT: 620 String accText = getAccessibleText(); 621 if (accText != null && !accText.isEmpty()) return accText; 622 623 //let the skin first. 624 Object title = super.queryAccessibleAttribute(attribute, parameters); 625 if (title != null) return title; 626 StringConverter<T> converter = getConverter(); 627 if (converter == null) { 628 return getValue() != null ? getValue().toString() : ""; 629 } 630 return converter.toString(getValue()); 631 case EXPANDED: return isShowing(); 632 default: return super.queryAccessibleAttribute(attribute, parameters); 633 } 634 } 635 636 @Override 637 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 638 switch (action) { 639 case COLLAPSE: hide(); break; 640 case EXPAND: show(); break; 641 default: super.executeAccessibleAction(action); break; 642 } 643 } 644 645 }