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