1 /* 2 * Copyright (c) 2010, 2014, 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 com.sun.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 } 185 } 186 }; 187 188 private ChangeListener<T> selectedItemListener = (ov, t, t1) -> { 189 if (! valueProperty().isBound()) { 190 setValue(t1); 191 } 192 }; 193 194 195 public final void setSelectionModel(SingleSelectionModel<T> value) { selectionModel.set(value); } 196 public final SingleSelectionModel<T> getSelectionModel() { return selectionModel.get(); } 197 public final ObjectProperty<SingleSelectionModel<T>> selectionModelProperty() { return selectionModel; } 198 199 200 /** 201 * Indicates whether the drop down is displaying the list of choices to the 202 * user. This is a readonly property which should be manipulated by means of 203 * the #show and #hide methods. 204 */ 205 private ReadOnlyBooleanWrapper showing = new ReadOnlyBooleanWrapper() { 206 @Override protected void invalidated() { 207 pseudoClassStateChanged(SHOWING_PSEUDOCLASS_STATE, get()); 208 notifyAccessibleAttributeChanged(AccessibleAttribute.EXPANDED); 209 } 210 211 @Override 212 public Object getBean() { 213 return ChoiceBox.this; 214 } 215 216 @Override 217 public String getName() { 218 return "showing"; 219 } 220 }; 221 public final boolean isShowing() { return showing.get(); } 222 public final ReadOnlyBooleanProperty showingProperty() { return showing.getReadOnlyProperty(); } 223 private void setShowing(boolean value) { 224 // these events will not fire if the showing property is bound 225 Event.fireEvent(this, value ? new Event(ComboBoxBase.ON_SHOWING) : 226 new Event(ComboBoxBase.ON_HIDING)); 227 showing.set(value); 228 Event.fireEvent(this, value ? new Event(ComboBoxBase.ON_SHOWN) : 229 new Event(ComboBoxBase.ON_HIDDEN)); 230 } 231 232 /** 233 * The items to display in the choice box. The selected item (as indicated in the 234 * selection model) must always be one of these items. 235 */ 236 private ObjectProperty<ObservableList<T>> items = new ObjectPropertyBase<ObservableList<T>>() { 237 ObservableList<T> old; 238 @Override protected void invalidated() { 239 final ObservableList<T> newItems = get(); 240 if (old != newItems) { 241 // Add and remove listeners 242 if (old != null) old.removeListener(itemsListener); 243 if (newItems != null) newItems.addListener(itemsListener); 244 // Clear the selection model 245 final SingleSelectionModel<T> sm = getSelectionModel(); 246 if (sm != null) { 247 if (newItems != null && newItems.isEmpty()) { 248 // RT-29433 - clear selection. 249 sm.clearSelection(); 250 } else if (sm.getSelectedIndex() == -1 && sm.getSelectedItem() != null) { 251 int newIndex = getItems().indexOf(sm.getSelectedItem()); 252 if (newIndex != -1) { 253 sm.setSelectedIndex(newIndex); 254 } 255 } else sm.clearSelection(); 256 } 257 // if (sm != null) sm.setSelectedIndex(-1); 258 // Save off the old items 259 old = newItems; 260 } 261 } 262 263 @Override 264 public Object getBean() { 265 return ChoiceBox.this; 266 } 267 268 @Override 269 public String getName() { 270 return "items"; 271 } 272 }; 273 public final void setItems(ObservableList<T> value) { items.set(value); } 274 public final ObservableList<T> getItems() { return items.get(); } 275 public final ObjectProperty<ObservableList<T>> itemsProperty() { return items; } 276 277 private final ListChangeListener<T> itemsListener = c -> { 278 final SingleSelectionModel<T> sm = getSelectionModel(); 279 if (sm!= null) { 280 if (getItems() == null || getItems().isEmpty()) { 281 sm.clearSelection(); 282 } else { 283 int newIndex = getItems().indexOf(sm.getSelectedItem()); 284 sm.setSelectedIndex(newIndex); 285 } 286 } 287 if (sm != null) { 288 289 // Look for the selected item as having been removed. If it has been, 290 // then we need to clear the selection in the selection model. 291 final T selectedItem = sm.getSelectedItem(); 292 while (c.next()) { 293 if (selectedItem != null && c.getRemoved().contains(selectedItem)) { 294 sm.clearSelection(); 295 break; 296 } 297 } 298 } 299 }; 300 301 /** 302 * Allows a way to specify how to represent objects in the items list. When 303 * a StringConverter is set, the object toString method is not called and 304 * instead its toString(object T) is called, passing the objects in the items list. 305 * This is useful when using domain objects in a ChoiceBox as this property 306 * allows for customization of the representation. Also, any of the pre-built 307 * Converters available in the {@link javafx.util.converter} package can be set. 308 * @since JavaFX 2.1 309 */ 310 public ObjectProperty<StringConverter<T>> converterProperty() { return converter; } 311 private ObjectProperty<StringConverter<T>> converter = 312 new SimpleObjectProperty<StringConverter<T>>(this, "converter", null); 313 public final void setConverter(StringConverter<T> value) { converterProperty().set(value); } 314 public final StringConverter<T> getConverter() {return converterProperty().get(); } 315 316 /** 317 * The value of this ChoiceBox is defined as the selected item in the ChoiceBox 318 * selection model. The valueProperty is synchronized with the selectedItem. 319 * This property allows for bi-directional binding of external properties to the 320 * ChoiceBox and updates the selection model accordingly. 321 * @since JavaFX 2.1 322 */ 323 public ObjectProperty<T> valueProperty() { return value; } 324 private ObjectProperty<T> value = new SimpleObjectProperty<T>(this, "value") { 325 @Override protected void invalidated() { 326 super.invalidated(); 327 fireEvent(new ActionEvent()); 328 // Update selection 329 final SingleSelectionModel<T> sm = getSelectionModel(); 330 if (sm != null) { 331 sm.select(super.getValue()); 332 } 333 notifyAccessibleAttributeChanged(AccessibleAttribute.TEXT); 334 } 335 }; 336 public final void setValue(T value) { valueProperty().set(value); } 337 public final T getValue() { return valueProperty().get(); } 338 339 340 // --- On Action 341 /** 342 * The ChoiceBox action, which is invoked whenever the ChoiceBox 343 * {@link #valueProperty() value} property is changed. This 344 * may be due to the value property being programmatically changed or when the 345 * user selects an item in a popup menu. 346 * 347 * @since JavaFX 8u60 348 */ 349 public final ObjectProperty<EventHandler<ActionEvent>> onActionProperty() { return onAction; } 350 public final void setOnAction(EventHandler<ActionEvent> value) { onActionProperty().set(value); } 351 public final EventHandler<ActionEvent> getOnAction() { return onActionProperty().get(); } 352 private ObjectProperty<EventHandler<ActionEvent>> onAction = new ObjectPropertyBase<EventHandler<ActionEvent>>() { 353 @Override protected void invalidated() { 354 setEventHandler(ActionEvent.ACTION, get()); 355 } 356 357 @Override 358 public Object getBean() { 359 return ChoiceBox.this; 360 } 361 362 @Override 363 public String getName() { 364 return "onAction"; 365 } 366 }; 367 368 369 // --- On Showing 370 /** 371 * Called just prior to the {@code ChoiceBox} popup being shown. 372 * @since JavaFX 8u60 373 */ 374 public final ObjectProperty<EventHandler<Event>> onShowingProperty() { return onShowing; } 375 public final void setOnShowing(EventHandler<Event> value) { onShowingProperty().set(value); } 376 public final EventHandler<Event> getOnShowing() { return onShowingProperty().get(); } 377 private ObjectProperty<EventHandler<Event>> onShowing = new ObjectPropertyBase<EventHandler<Event>>() { 378 @Override protected void invalidated() { 379 setEventHandler(ON_SHOWING, get()); 380 } 381 382 @Override public Object getBean() { 383 return ChoiceBox.this; 384 } 385 386 @Override public String getName() { 387 return "onShowing"; 388 } 389 }; 390 391 392 // -- On Shown 393 /** 394 * Called just after the {@link ChoiceBox} popup is shown. 395 * @since JavaFX 8u60 396 */ 397 public final ObjectProperty<EventHandler<Event>> onShownProperty() { return onShown; } 398 public final void setOnShown(EventHandler<Event> value) { onShownProperty().set(value); } 399 public final EventHandler<Event> getOnShown() { return onShownProperty().get(); } 400 private ObjectProperty<EventHandler<Event>> onShown = new ObjectPropertyBase<EventHandler<Event>>() { 401 @Override protected void invalidated() { 402 setEventHandler(ON_SHOWN, get()); 403 } 404 405 @Override public Object getBean() { 406 return ChoiceBox.this; 407 } 408 409 @Override public String getName() { 410 return "onShown"; 411 } 412 }; 413 414 415 // --- On Hiding 416 /** 417 * Called just prior to the {@link ChoiceBox} popup being hidden. 418 * @since JavaFX 8u60 419 */ 420 public final ObjectProperty<EventHandler<Event>> onHidingProperty() { return onHiding; } 421 public final void setOnHiding(EventHandler<Event> value) { onHidingProperty().set(value); } 422 public final EventHandler<Event> getOnHiding() { return onHidingProperty().get(); } 423 private ObjectProperty<EventHandler<Event>> onHiding = new ObjectPropertyBase<EventHandler<Event>>() { 424 @Override protected void invalidated() { 425 setEventHandler(ON_HIDING, get()); 426 } 427 428 @Override public Object getBean() { 429 return ChoiceBox.this; 430 } 431 432 @Override public String getName() { 433 return "onHiding"; 434 } 435 }; 436 437 438 // --- On Hidden 439 /** 440 * Called just after the {@link ChoiceBox} popup has been hidden. 441 * @since JavaFX 8u60 442 */ 443 public final ObjectProperty<EventHandler<Event>> onHiddenProperty() { return onHidden; } 444 public final void setOnHidden(EventHandler<Event> value) { onHiddenProperty().set(value); } 445 public final EventHandler<Event> getOnHidden() { return onHiddenProperty().get(); } 446 private ObjectProperty<EventHandler<Event>> onHidden = new ObjectPropertyBase<EventHandler<Event>>() { 447 @Override protected void invalidated() { 448 setEventHandler(ON_HIDDEN, get()); 449 } 450 451 @Override public Object getBean() { 452 return ChoiceBox.this; 453 } 454 455 @Override public String getName() { 456 return "onHidden"; 457 } 458 }; 459 460 /*************************************************************************** 461 * * 462 * Methods * 463 * * 464 **************************************************************************/ 465 466 /** 467 * Opens the list of choices. 468 */ 469 public void show() { 470 if (!isDisabled()) setShowing(true); 471 } 472 473 /** 474 * Closes the list of choices. 475 */ 476 public void hide() { 477 setShowing(false); 478 } 479 480 /** {@inheritDoc} */ 481 @Override protected Skin<?> createDefaultSkin() { 482 return new ChoiceBoxSkin<T>(this); 483 } 484 485 /*************************************************************************** 486 * * 487 * Stylesheet Handling * 488 * * 489 **************************************************************************/ 490 491 private static final PseudoClass SHOWING_PSEUDOCLASS_STATE = 492 PseudoClass.getPseudoClass("showing"); 493 494 // package for testing 495 static class ChoiceBoxSelectionModel<T> extends SingleSelectionModel<T> { 496 private final ChoiceBox<T> choiceBox; 497 498 public ChoiceBoxSelectionModel(final ChoiceBox<T> cb) { 499 if (cb == null) { 500 throw new NullPointerException("ChoiceBox can not be null"); 501 } 502 this.choiceBox = cb; 503 504 /* 505 * The following two listeners are used in conjunction with 506 * SelectionModel.select(T obj) to allow for a developer to select 507 * an item that is not actually in the data model. When this occurs, 508 * we actively try to find an index that matches this object, going 509 * so far as to actually watch for all changes to the items list, 510 * rechecking each time. 511 */ 512 513 // watching for changes to the items list content 514 final ListChangeListener<T> itemsContentObserver = c -> { 515 if (choiceBox.getItems() == null || choiceBox.getItems().isEmpty()) { 516 setSelectedIndex(-1); 517 } else if (getSelectedIndex() == -1 && getSelectedItem() != null) { 518 int newIndex = choiceBox.getItems().indexOf(getSelectedItem()); 519 if (newIndex != -1) { 520 setSelectedIndex(newIndex); 521 } 522 } 523 }; 524 if (this.choiceBox.getItems() != null) { 525 this.choiceBox.getItems().addListener(itemsContentObserver); 526 } 527 528 // watching for changes to the items list 529 ChangeListener<ObservableList<T>> itemsObserver = (valueModel, oldList, newList) -> { 530 if (oldList != null) { 531 oldList.removeListener(itemsContentObserver); 532 } 533 if (newList != null) { 534 newList.addListener(itemsContentObserver); 535 } 536 setSelectedIndex(-1); 537 if (getSelectedItem() != null) { 538 int newIndex = choiceBox.getItems().indexOf(getSelectedItem()); 539 if (newIndex != -1) { 540 setSelectedIndex(newIndex); 541 } 542 } 543 }; 544 this.choiceBox.itemsProperty().addListener(itemsObserver); 545 } 546 547 // API Implementation 548 @Override protected T getModelItem(int index) { 549 final ObservableList<T> items = choiceBox.getItems(); 550 if (items == null) return null; 551 if (index < 0 || index >= items.size()) return null; 552 return items.get(index); 553 } 554 555 @Override protected int getItemCount() { 556 final ObservableList<T> items = choiceBox.getItems(); 557 return items == null ? 0 : items.size(); 558 } 559 560 /** 561 * Selects the given row. Since the SingleSelectionModel can only support having 562 * a single row selected at a time, this also causes any previously selected 563 * row to be unselected. 564 * This method is overridden here so that we can move past a Separator 565 * in a ChoiceBox and select the next valid menuitem. 566 */ 567 @Override public void select(int index) { 568 // this does not sound right, we should let the superclass handle it. 569 final T value = getModelItem(index); 570 if (value instanceof Separator) { 571 select(++index); 572 } else { 573 super.select(index); 574 } 575 576 if (choiceBox.isShowing()) { 577 choiceBox.hide(); 578 } 579 } 580 } 581 582 /*************************************************************************** 583 * * 584 * Accessibility handling * 585 * * 586 **************************************************************************/ 587 588 @Override 589 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 590 switch(attribute) { 591 case TEXT: 592 String accText = getAccessibleText(); 593 if (accText != null && !accText.isEmpty()) return accText; 594 595 //let the skin first. 596 Object title = super.queryAccessibleAttribute(attribute, parameters); 597 if (title != null) return title; 598 StringConverter<T> converter = getConverter(); 599 if (converter == null) { 600 return getValue() != null ? getValue().toString() : ""; 601 } 602 return converter.toString(getValue()); 603 case EXPANDED: return isShowing(); 604 default: return super.queryAccessibleAttribute(attribute, parameters); 605 } 606 } 607 608 @Override 609 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 610 switch (action) { 611 case COLLAPSE: hide(); break; 612 case EXPAND: show(); break; 613 default: super.executeAccessibleAction(action); break; 614 } 615 } 616 617 }