1 /* 2 * Copyright (c) 2010, 2015, 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.InvalidationListener; 29 import javafx.beans.Observable; 30 import javafx.beans.WeakInvalidationListener; 31 import javafx.collections.WeakListChangeListener; 32 import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin; 33 import javafx.beans.property.*; 34 import javafx.beans.value.ChangeListener; 35 import javafx.beans.value.ObservableValue; 36 import javafx.beans.value.WeakChangeListener; 37 import javafx.collections.FXCollections; 38 import javafx.collections.ListChangeListener; 39 import javafx.collections.ObservableList; 40 import javafx.scene.AccessibleAttribute; 41 import javafx.scene.AccessibleRole; 42 import javafx.scene.Node; 43 import javafx.util.Callback; 44 import javafx.util.StringConverter; 45 46 import java.lang.ref.WeakReference; 47 48 /** 49 * An implementation of the {@link ComboBoxBase} abstract class for the most common 50 * form of ComboBox, where a popup list is shown to users providing them with 51 * a choice that they may select from. For more information around the general 52 * concepts and API of ComboBox, refer to the {@link ComboBoxBase} class 53 * documentation. 54 * 55 * <p>On top of ComboBoxBase, the ComboBox class introduces additional API. Most 56 * importantly, it adds an {@link #itemsProperty() items} property that works in 57 * much the same way as the ListView {@link ListView#itemsProperty() items} 58 * property. In other words, it is the content of the items list that is displayed 59 * to users when they click on the ComboBox button. 60 * 61 * <p>The ComboBox exposes the {@link #valueProperty()} from 62 * {@link javafx.scene.control.ComboBoxBase}, but there are some important points 63 * of the value property that need to be understood in relation to ComboBox. 64 * These include: 65 * 66 * <ol> 67 * <li>The value property <strong>is not</strong> constrained to items contained 68 * within the items list - it can be anything as long as it is a valid value 69 * of type T.</li> 70 * <li>If the value property is set to a non-null object, and subsequently the 71 * items list is cleared, the value property <strong>is not</strong> nulled out.</li> 72 * <li>Clearing the {@link javafx.scene.control.SelectionModel#clearSelection() 73 * selection} in the selection model <strong>does not</strong> null the value 74 * property - it remains the same as before.</li> 75 * <li>It is valid for the selection model to have a selection set to a given 76 * index even if there is no items in the list (or less items in the list than 77 * the given index). Once the items list is further populated, such that the 78 * list contains enough items to have an item in the given index, both the 79 * selection model {@link SelectionModel#selectedItemProperty()} and 80 * value property will be updated to have this value. This is inconsistent with 81 * other controls that use a selection model, but done intentionally for ComboBox.</li> 82 * </ol> 83 * 84 * <p>By default, when the popup list is showing, the maximum number of rows 85 * visible is 10, but this can be changed by modifying the 86 * {@link #visibleRowCountProperty() visibleRowCount} property. If the number of 87 * items in the ComboBox is less than the value of <code>visibleRowCount</code>, 88 * then the items size will be used instead so that the popup list is not 89 * exceedingly long. 90 * 91 * <p>As with ListView, it is possible to modify the 92 * {@link javafx.scene.control.SelectionModel selection model} that is used, 93 * although this is likely to be rarely changed. This is because the ComboBox 94 * enforces the need for a {@link javafx.scene.control.SingleSelectionModel} 95 * instance, and it is not likely that there is much need for alternate 96 * implementations. Nonetheless, the option is there should use cases be found 97 * for switching the selection model. 98 * 99 * <p>As the ComboBox internally renders content with a ListView, API exists in 100 * the ComboBox class to allow for a custom cell factory to be set. For more 101 * information on cell factories, refer to the {@link Cell} and {@link ListCell} 102 * classes. It is important to note that if a cell factory is set on a ComboBox, 103 * cells will only be used in the ListView that shows when the ComboBox is 104 * clicked. If you also want to customize the rendering of the 'button' area 105 * of the ComboBox, you can set a custom {@link ListCell} instance in the 106 * {@link #buttonCellProperty() button cell} property. One way of doing this 107 * is with the following code (note the use of {@code setButtonCell}: 108 * 109 * <pre> 110 * {@code 111 * Callback<ListView<String>, ListCell<String>> cellFactory = ...; 112 * ComboBox comboBox = new ComboBox(); 113 * comboBox.setItems(items); 114 * comboBox.setButtonCell(cellFactory.call(null)); 115 * comboBox.setCellFactory(cellFactory);}</pre> 116 * 117 * <p>Because a ComboBox can be {@link #editableProperty() editable}, and the 118 * default means of allowing user input is via a {@link TextField}, a 119 * {@link #converterProperty() string converter} property is provided to allow 120 * for developers to specify how to translate a users string into an object of 121 * type T, such that the {@link #valueProperty() value} property may contain it. 122 * By default the converter simply returns the String input as the user typed it, 123 * which therefore assumes that the type of the editable ComboBox is String. If 124 * a different type is specified and the ComboBox is to be editable, it is 125 * necessary to specify a custom {@link StringConverter}. 126 * 127 * <h3>A warning about inserting Nodes into the ComboBox items list</h3> 128 * ComboBox allows for the items list to contain elements of any type, including 129 * {@link Node} instances. Putting nodes into 130 * the items list is <strong>strongly not recommended</strong>. This is because 131 * the default {@link #cellFactoryProperty() cell factory} simply inserts Node 132 * items directly into the cell, including in the ComboBox 'button' area too. 133 * Because the scenegraph only allows for Nodes to be in one place at a time, 134 * this means that when an item is selected it becomes removed from the ComboBox 135 * list, and becomes visible in the button area. When selection changes the 136 * previously selected item returns to the list and the new selection is removed. 137 * 138 * <p>The recommended approach, rather than inserting Node instances into the 139 * items list, is to put the relevant information into the ComboBox, and then 140 * provide a custom {@link #cellFactoryProperty() cell factory}. For example, 141 * rather than use the following code: 142 * 143 * <pre> 144 * {@code 145 * ComboBox<Rectangle> cmb = new ComboBox<Rectangle>(); 146 * cmb.getItems().addAll( 147 * new Rectangle(10, 10, Color.RED), 148 * new Rectangle(10, 10, Color.GREEN), 149 * new Rectangle(10, 10, Color.BLUE));}</pre> 150 * 151 * <p>You should do the following:</p> 152 * 153 * <pre><code> 154 * ComboBox<Color> cmb = new ComboBox<Color>(); 155 * cmb.getItems().addAll( 156 * Color.RED, 157 * Color.GREEN, 158 * Color.BLUE); 159 * 160 * cmb.setCellFactory(new Callback<ListView<Color>, ListCell<Color>>() { 161 * @Override public ListCell<Color> call(ListView<Color> p) { 162 * return new ListCell<Color>() { 163 * private final Rectangle rectangle; 164 * { 165 * setContentDisplay(ContentDisplay.GRAPHIC_ONLY); 166 * rectangle = new Rectangle(10, 10); 167 * } 168 * 169 * @Override protected void updateItem(Color item, boolean empty) { 170 * super.updateItem(item, empty); 171 * 172 * if (item == null || empty) { 173 * setGraphic(null); 174 * } else { 175 * rectangle.setFill(item); 176 * setGraphic(rectangle); 177 * } 178 * } 179 * }; 180 * } 181 *});</code></pre> 182 * 183 * <p>Admittedly the above approach is far more verbose, but it offers the 184 * required functionality without encountering the scenegraph constraints. 185 * 186 * @see ComboBoxBase 187 * @see Cell 188 * @see ListCell 189 * @see StringConverter 190 * @since JavaFX 2.1 191 */ 192 public class ComboBox<T> extends ComboBoxBase<T> { 193 194 /*************************************************************************** 195 * * 196 * Static properties and methods * 197 * * 198 **************************************************************************/ 199 200 private static <T> StringConverter<T> defaultStringConverter() { 201 return new StringConverter<T>() { 202 @Override public String toString(T t) { 203 return t == null ? null : t.toString(); 204 } 205 206 @Override public T fromString(String string) { 207 return (T) string; 208 } 209 }; 210 } 211 212 213 214 /*************************************************************************** 215 * * 216 * Constructors * 217 * * 218 **************************************************************************/ 219 220 /** 221 * Creates a default ComboBox instance with an empty 222 * {@link #itemsProperty() items} list and default 223 * {@link #selectionModelProperty() selection model}. 224 */ 225 public ComboBox() { 226 this(FXCollections.<T>observableArrayList()); 227 } 228 229 /** 230 * Creates a default ComboBox instance with the provided items list and 231 * a default {@link #selectionModelProperty() selection model}. 232 */ 233 public ComboBox(ObservableList<T> items) { 234 getStyleClass().add(DEFAULT_STYLE_CLASS); 235 setAccessibleRole(AccessibleRole.COMBO_BOX); 236 setItems(items); 237 setSelectionModel(new ComboBoxSelectionModel<T>(this)); 238 239 // listen to the value property input by the user, and if the value is 240 // set to something that exists in the items list, we should update the 241 // selection model to indicate that this is the selected item 242 valueProperty().addListener((ov, t, t1) -> { 243 if (getItems() == null) return; 244 245 SelectionModel<T> sm = getSelectionModel(); 246 int index = getItems().indexOf(t1); 247 248 if (index == -1) { 249 sm.setSelectedItem(t1); 250 } else { 251 // we must compare the value here with the currently selected 252 // item. If they are different, we overwrite the selection 253 // properties to reflect the new value. 254 // We do this as there can be circumstances where there are 255 // multiple instances of a value in the ComboBox items list, 256 // and if we don't check here we may change the selection 257 // mistakenly because the indexOf above will return the first 258 // instance always, and selection may be on the second or 259 // later instances. This is RT-19227. 260 T selectedItem = sm.getSelectedItem(); 261 if (selectedItem == null || ! selectedItem.equals(getValue())) { 262 sm.clearAndSelect(index); 263 } 264 } 265 }); 266 267 editableProperty().addListener(o -> { 268 // when editable changes, we reset the selection / value states 269 getSelectionModel().clearSelection(); 270 }); 271 } 272 273 274 275 /*************************************************************************** 276 * * 277 * Properties * 278 * * 279 **************************************************************************/ 280 281 // --- items 282 /** 283 * The list of items to show within the ComboBox popup. 284 */ 285 private ObjectProperty<ObservableList<T>> items = new SimpleObjectProperty<ObservableList<T>>(this, "items"); 286 public final void setItems(ObservableList<T> value) { itemsProperty().set(value); } 287 public final ObservableList<T> getItems() {return items.get(); } 288 public ObjectProperty<ObservableList<T>> itemsProperty() { return items; } 289 290 291 // --- string converter 292 /** 293 * Converts the user-typed input (when the ComboBox is 294 * {@link #editableProperty() editable}) to an object of type T, such that 295 * the input may be retrieved via the {@link #valueProperty() value} property. 296 */ 297 public ObjectProperty<StringConverter<T>> converterProperty() { return converter; } 298 private ObjectProperty<StringConverter<T>> converter = 299 new SimpleObjectProperty<StringConverter<T>>(this, "converter", ComboBox.<T>defaultStringConverter()); 300 public final void setConverter(StringConverter<T> value) { converterProperty().set(value); } 301 public final StringConverter<T> getConverter() {return converterProperty().get(); } 302 303 304 // --- cell factory 305 /** 306 * Providing a custom cell factory allows for complete customization of the 307 * rendering of items in the ComboBox. Refer to the {@link Cell} javadoc 308 * for more information on cell factories. 309 */ 310 private ObjectProperty<Callback<ListView<T>, ListCell<T>>> cellFactory = 311 new SimpleObjectProperty<Callback<ListView<T>, ListCell<T>>>(this, "cellFactory"); 312 public final void setCellFactory(Callback<ListView<T>, ListCell<T>> value) { cellFactoryProperty().set(value); } 313 public final Callback<ListView<T>, ListCell<T>> getCellFactory() {return cellFactoryProperty().get(); } 314 public ObjectProperty<Callback<ListView<T>, ListCell<T>>> cellFactoryProperty() { return cellFactory; } 315 316 317 // --- button cell 318 /** 319 * The button cell is used to render what is shown in the ComboBox 'button' 320 * area. If a cell is set here, it does not change the rendering of the 321 * ComboBox popup list - that rendering is controlled via the 322 * {@link #cellFactoryProperty() cell factory} API. 323 * @since JavaFX 2.2 324 */ 325 public ObjectProperty<ListCell<T>> buttonCellProperty() { return buttonCell; } 326 private ObjectProperty<ListCell<T>> buttonCell = 327 new SimpleObjectProperty<ListCell<T>>(this, "buttonCell"); 328 public final void setButtonCell(ListCell<T> value) { buttonCellProperty().set(value); } 329 public final ListCell<T> getButtonCell() {return buttonCellProperty().get(); } 330 331 332 // --- Selection Model 333 /** 334 * The selection model for the ComboBox. A ComboBox only supports 335 * single selection. 336 */ 337 private ObjectProperty<SingleSelectionModel<T>> selectionModel = new SimpleObjectProperty<SingleSelectionModel<T>>(this, "selectionModel") { 338 private SingleSelectionModel<T> oldSM = null; 339 @Override protected void invalidated() { 340 if (oldSM != null) { 341 oldSM.selectedItemProperty().removeListener(selectedItemListener); 342 } 343 SingleSelectionModel<T> sm = get(); 344 oldSM = sm; 345 if (sm != null) { 346 sm.selectedItemProperty().addListener(selectedItemListener); 347 } 348 } 349 }; 350 public final void setSelectionModel(SingleSelectionModel<T> value) { selectionModel.set(value); } 351 public final SingleSelectionModel<T> getSelectionModel() { return selectionModel.get(); } 352 public final ObjectProperty<SingleSelectionModel<T>> selectionModelProperty() { return selectionModel; } 353 354 355 // --- Visible Row Count 356 /** 357 * The maximum number of rows to be visible in the ComboBox popup when it is 358 * showing. By default this value is 10, but this can be changed to increase 359 * or decrease the height of the popup. 360 */ 361 private IntegerProperty visibleRowCount 362 = new SimpleIntegerProperty(this, "visibleRowCount", 10); 363 public final void setVisibleRowCount(int value) { visibleRowCount.set(value); } 364 public final int getVisibleRowCount() { return visibleRowCount.get(); } 365 public final IntegerProperty visibleRowCountProperty() { return visibleRowCount; } 366 367 368 // --- Editor 369 private TextField textField; 370 /** 371 * The editor for the ComboBox. The editor is null if the ComboBox is not 372 * {@link #editableProperty() editable}. 373 * @since JavaFX 2.2 374 */ 375 private ReadOnlyObjectWrapper<TextField> editor; 376 public final TextField getEditor() { 377 return editorProperty().get(); 378 } 379 public final ReadOnlyObjectProperty<TextField> editorProperty() { 380 if (editor == null) { 381 editor = new ReadOnlyObjectWrapper<TextField>(this, "editor"); 382 textField = new ComboBoxListViewSkin.FakeFocusTextField(); 383 editor.set(textField); 384 } 385 return editor.getReadOnlyProperty(); 386 } 387 388 389 // --- Placeholder Node 390 private ObjectProperty<Node> placeholder; 391 /** 392 * This Node is shown to the user when the ComboBox has no content to show. 393 * The placeholder node is shown in the ComboBox popup area 394 * when the items list is null or empty. 395 * @since JavaFX 8.0 396 */ 397 public final ObjectProperty<Node> placeholderProperty() { 398 if (placeholder == null) { 399 placeholder = new SimpleObjectProperty<Node>(this, "placeholder"); 400 } 401 return placeholder; 402 } 403 public final void setPlaceholder(Node value) { 404 placeholderProperty().set(value); 405 } 406 public final Node getPlaceholder() { 407 return placeholder == null ? null : placeholder.get(); 408 } 409 410 411 412 /*************************************************************************** 413 * * 414 * Methods * 415 * * 416 **************************************************************************/ 417 418 /** {@inheritDoc} */ 419 @Override protected Skin<?> createDefaultSkin() { 420 return new ComboBoxListViewSkin<T>(this); 421 } 422 423 424 425 /*************************************************************************** 426 * * 427 * Callbacks and Events * 428 * * 429 **************************************************************************/ 430 431 // Listen to changes in the selectedItem property of the SelectionModel. 432 // When it changes, set the selectedItem in the value property. 433 private ChangeListener<T> selectedItemListener = new ChangeListener<T>() { 434 @Override public void changed(ObservableValue<? extends T> ov, T t, T t1) { 435 if (wasSetAllCalled && t1 == null) { 436 // no-op: fix for RT-22572 where the developer was completely 437 // replacing all items in the ComboBox, and expecting the 438 // selection (and ComboBox.value) to remain set. If this isn't 439 // here, we would updateValue(null). 440 // Additional fix for RT-22937: adding the '&& t1 == null'. 441 // Without this, there would be circumstances where the user 442 // selecting a new value from the ComboBox would end up in here, 443 // when we really should go into the updateValue(t1) call below. 444 // We should only ever go into this clause if t1 is null. 445 } else { 446 updateValue(t1); 447 } 448 449 wasSetAllCalled = false; 450 } 451 }; 452 453 454 455 /*************************************************************************** 456 * * 457 * Private methods * 458 * * 459 **************************************************************************/ 460 461 private void updateValue(T newValue) { 462 if (! valueProperty().isBound()) { 463 setValue(newValue); 464 } 465 } 466 467 468 469 470 /*************************************************************************** 471 * * 472 * Stylesheet Handling * 473 * * 474 **************************************************************************/ 475 476 private static final String DEFAULT_STYLE_CLASS = "combo-box"; 477 478 private boolean wasSetAllCalled = false; 479 private int previousItemCount = -1; 480 481 // package for testing 482 static class ComboBoxSelectionModel<T> extends SingleSelectionModel<T> { 483 private final ComboBox<T> comboBox; 484 485 public ComboBoxSelectionModel(final ComboBox<T> cb) { 486 if (cb == null) { 487 throw new NullPointerException("ComboBox can not be null"); 488 } 489 this.comboBox = cb; 490 491 selectedIndexProperty().addListener(valueModel -> { 492 // we used to lazily retrieve the selected item, but now we just 493 // do it when the selection changes. 494 setSelectedItem(getModelItem(getSelectedIndex())); 495 }); 496 497 /* 498 * The following two listeners are used in conjunction with 499 * SelectionModel.select(T obj) to allow for a developer to select 500 * an item that is not actually in the data model. When this occurs, 501 * we actively try to find an index that matches this object, going 502 * so far as to actually watch for all changes to the items list, 503 * rechecking each time. 504 */ 505 itemsObserver = new InvalidationListener() { 506 private WeakReference<ObservableList<T>> weakItemsRef = new WeakReference<>(comboBox.getItems()); 507 508 @Override public void invalidated(Observable observable) { 509 ObservableList<T> oldItems = weakItemsRef.get(); 510 weakItemsRef = new WeakReference<>(comboBox.getItems()); 511 updateItemsObserver(oldItems, comboBox.getItems()); 512 } 513 }; 514 this.comboBox.itemsProperty().addListener(new WeakInvalidationListener(itemsObserver)); 515 if (comboBox.getItems() != null) { 516 this.comboBox.getItems().addListener(weakItemsContentObserver); 517 } 518 } 519 520 // watching for changes to the items list content 521 private final ListChangeListener<T> itemsContentObserver = new ListChangeListener<T>() { 522 @Override public void onChanged(Change<? extends T> c) { 523 if (comboBox.getItems() == null || comboBox.getItems().isEmpty()) { 524 setSelectedIndex(-1); 525 } else if (getSelectedIndex() == -1 && getSelectedItem() != null) { 526 int newIndex = comboBox.getItems().indexOf(getSelectedItem()); 527 if (newIndex != -1) { 528 setSelectedIndex(newIndex); 529 } 530 } 531 532 int shift = 0; 533 while (c.next()) { 534 comboBox.wasSetAllCalled = comboBox.previousItemCount == c.getRemovedSize(); 535 536 if (c.wasReplaced()) { 537 // no-op 538 } else if (c.wasAdded() || c.wasRemoved()) { 539 if (c.getFrom() <= getSelectedIndex() && getSelectedIndex()!= -1) { 540 shift += c.wasAdded() ? c.getAddedSize() : -c.getRemovedSize(); 541 } 542 } 543 } 544 545 if (shift != 0) { 546 clearAndSelect(getSelectedIndex() + shift); 547 } 548 549 comboBox.previousItemCount = getItemCount(); 550 } 551 }; 552 553 // watching for changes to the items list 554 private final InvalidationListener itemsObserver; 555 556 private WeakListChangeListener<T> weakItemsContentObserver = 557 new WeakListChangeListener<T>(itemsContentObserver); 558 559 560 private void updateItemsObserver(ObservableList<T> oldList, ObservableList<T> newList) { 561 // update listeners 562 if (oldList != null) { 563 oldList.removeListener(weakItemsContentObserver); 564 } 565 if (newList != null) { 566 newList.addListener(weakItemsContentObserver); 567 } 568 569 // when the items list totally changes, we should clear out 570 // the selection and focus 571 int newValueIndex = -1; 572 if (newList != null) { 573 T value = comboBox.getValue(); 574 if (value != null) { 575 newValueIndex = newList.indexOf(value); 576 } 577 } 578 setSelectedIndex(newValueIndex); 579 } 580 581 // API Implementation 582 @Override protected T getModelItem(int index) { 583 final ObservableList<T> items = comboBox.getItems(); 584 if (items == null) return null; 585 if (index < 0 || index >= items.size()) return null; 586 return items.get(index); 587 } 588 589 @Override protected int getItemCount() { 590 final ObservableList<T> items = comboBox.getItems(); 591 return items == null ? 0 : items.size(); 592 } 593 } 594 595 /*************************************************************************** 596 * * 597 * Accessibility handling * 598 * * 599 **************************************************************************/ 600 601 @Override 602 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 603 switch(attribute) { 604 case TEXT: 605 String accText = getAccessibleText(); 606 if (accText != null && !accText.isEmpty()) return accText; 607 608 //let the skin first. 609 Object title = super.queryAccessibleAttribute(attribute, parameters); 610 if (title != null) return title; 611 StringConverter<T> converter = getConverter(); 612 if (converter == null) { 613 return getValue() != null ? getValue().toString() : ""; 614 } 615 return converter.toString(getValue()); 616 default: return super.queryAccessibleAttribute(attribute, parameters); 617 } 618 } 619 620 }