Spec-Zone .ru
спецификации, руководства, описания, API
|
001/* 002 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. 003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 004 * 005 * This code is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU General Public License version 2 only, as 007 * published by the Free Software Foundation. Oracle designates this 008 * particular file as subject to the "Classpath" exception as provided 009 * by Oracle in the LICENSE file that accompanied this code. 010 * 011 * This code is distributed in the hope that it will be useful, but WITHOUT 012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 014 * version 2 for more details (a copy is included in the LICENSE file that 015 * accompanied this code). 016 * 017 * You should have received a copy of the GNU General Public License version 018 * 2 along with this work; if not, write to the Free Software Foundation, 019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 020 * 021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 022 * or visit www.oracle.com if you need additional information or have any 023 * questions. 024 */ 025 026package javafx.scene.control; 027 028import javafx.collections.WeakListChangeListener; 029import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin; 030import javafx.beans.InvalidationListener; 031import javafx.beans.Observable; 032import javafx.beans.property.*; 033import javafx.beans.value.ChangeListener; 034import javafx.beans.value.ObservableValue; 035import javafx.beans.value.WeakChangeListener; 036import javafx.collections.FXCollections; 037import javafx.collections.ListChangeListener; 038import javafx.collections.ObservableList; 039import javafx.scene.Node; 040import javafx.util.Callback; 041import javafx.util.StringConverter; 042 043/** 044 * An implementation of the {@link ComboBoxBase} abstract class for the most common 045 * form of ComboBox, where a popup list is shown to users providing them with 046 * a choice that they may select from. For more information around the general 047 * concepts and API of ComboBox, refer to the {@link ComboBoxBase} class 048 * documentation. 049 * 050 * <p>On top of ComboBoxBase, the ComboBox class introduces additional API. Most 051 * importantly, it adds an {@link #itemsProperty() items} property that works in 052 * much the same way as the ListView {@link ListView#itemsProperty() items} 053 * property. In other words, it is the content of the items list that is displayed 054 * to users when they click on the ComboBox button. 055 * 056 * <p>By default, when the popup list is showing, the maximum number of rows 057 * visible is 10, but this can be changed by modifying the 058 * {@link #visibleRowCountProperty() visibleRowCount} property. If the number of 059 * items in the ComboBox is less than the value of <code>visibleRowCount</code>, 060 * then the items size will be used instead so that the popup list is not 061 * exceedingly long. 062 * 063 * <p>As with ListView, it is possible to modify the 064 * {@link javafx.scene.control.SelectionModel selection model} that is used, 065 * although this is likely to be rarely changed. This is because the ComboBox 066 * enforces the need for a {@link javafx.scene.control.SingleSelectionModel} 067 * instance, and it is not likely that there is much need for alternate 068 * implementations. Nonetheless, the option is there should use cases be found 069 * for switching the selection model. 070 * 071 * <p>As the ComboBox internally renders content with a ListView, API exists in 072 * the ComboBox class to allow for a custom cell factory to be set. For more 073 * information on cell factories, refer to the {@link Cell} and {@link ListCell} 074 * classes. It is important to note that if a cell factory is set on a ComboBox, 075 * cells will only be used in the ListView that shows when the ComboBox is 076 * clicked. If you also want to customize the rendering of the 'button' area 077 * of the ComboBox, you can set a custom {@link ListCell} instance in the 078 * {@link #buttonCellProperty() button cell} property. One way of doing this 079 * is with the following code (note the use of {@code setButtonCell}: 080 * 081 * <pre> 082 * {@code 083 * Callback<ListView<String>, ListCell<String>> cellFactory = ...; 084 * ComboBox comboBox = new ComboBox(); 085 * comboBox.setItems(items); 086 * comboBox.setButtonCell(cellFactory.call(null)); 087 * comboBox.setCellFactory(cellFactory);}</pre> 088 * 089 * <p>Because a ComboBox can be {@link #editableProperty() editable}, and the 090 * default means of allowing user input is via a {@link TextField}, a 091 * {@link #converterProperty() string converter} property is provided to allow 092 * for developers to specify how to translate a users string into an object of 093 * type T, such that the {@link #valueProperty() value} property may contain it. 094 * By default the converter simply returns the String input as the user typed it, 095 * which therefore assumes that the type of the editable ComboBox is String. If 096 * a different type is specified and the ComboBox is to be editable, it is 097 * necessary to specify a custom {@link StringConverter}. 098 * 099 * <h3>A warning about inserting Nodes into the ComboBox items list</h3> 100 * ComboBox allows for the items list to contain elements of any type, including 101 * {@link Node} instances. Putting nodes into 102 * the items list is <strong>strongly not recommended</strong>. This is because 103 * the default {@link #cellFactoryProperty() cell factory} simply inserts Node 104 * items directly into the cell, including in the ComboBox 'button' area too. 105 * Because the scenegraph only allows for Nodes to be in one place at a time, 106 * this means that when an item is selected it becomes removed from the ComboBox 107 * list, and becomes visible in the button area. When selection changes the 108 * previously selected item returns to the list and the new selection is removed. 109 * 110 * <p>The recommended approach, rather than inserting Node instances into the 111 * items list, is to put the relevant information into the ComboBox, and then 112 * provide a custom {@link #cellFactoryProperty() cell factory}. For example, 113 * rather than use the following code: 114 * 115 * <pre> 116 * {@code 117 * ComboBox<Rectangle> cmb = new ComboBox<Rectangle>(); 118 * cmb.getItems().addAll( 119 * new Rectangle(10, 10, Color.RED), 120 * new Rectangle(10, 10, Color.GREEN), 121 * new Rectangle(10, 10, Color.BLUE));}</pre> 122 * 123 * <p>You should do the following:</p> 124 * 125 * <pre><code> 126 * ComboBox<Color> cmb = new ComboBox<Color>(); 127 * cmb.getItems().addAll( 128 * Color.RED, 129 * Color.GREEN, 130 * Color.BLUE); 131 * 132 * cmb.setCellFactory(new Callback<ListView<Color>, ListCell<Color>>() { 133 * @Override public ListCell<Color> call(ListView<Color> p) { 134 * return new ListCell<Color>() { 135 * private final Rectangle rectangle; 136 * { 137 * setContentDisplay(ContentDisplay.GRAPHIC_ONLY); 138 * rectangle = new Rectangle(10, 10); 139 * } 140 * 141 * @Override protected void updateItem(Color item, boolean empty) { 142 * super.updateItem(item, empty); 143 * 144 * if (item == null || empty) { 145 * setGraphic(null); 146 * } else { 147 * rectangle.setFill(item); 148 * setGraphic(rectangle); 149 * } 150 * } 151 * }; 152 * } 153 *});</code></pre> 154 * 155 * <p>Admittedly the above approach is far more verbose, but it offers the 156 * required functionality without encountering the scenegraph constraints. 157 * 158 * @see ComboBoxBase 159 * @see Cell 160 * @see ListCell 161 * @see StringConverter 162 */ 163public class ComboBox<T> extends ComboBoxBase<T> { 164 165 /*************************************************************************** 166 * * 167 * Static properties and methods * 168 * * 169 **************************************************************************/ 170 171 private static <T> StringConverter<T> defaultStringConverter() { 172 return new StringConverter<T>() { 173 @Override public String toString(T t) { 174 return t == null ? null : t.toString(); 175 } 176 177 @Override public T fromString(String string) { 178 return (T) string; 179 } 180 }; 181 } 182 183 184 185 /*************************************************************************** 186 * * 187 * Constructors * 188 * * 189 **************************************************************************/ 190 191 /** 192 * Creates a default ComboBox instance with an empty 193 * {@link #itemsProperty() items} list and default 194 * {@link #selectionModelProperty() selection model}. 195 */ 196 public ComboBox() { 197 this(FXCollections.<T>observableArrayList()); 198 } 199 200 /** 201 * Creates a default ComboBox instance with the provided items list and 202 * a default {@link #selectionModelProperty() selection model}. 203 */ 204 public ComboBox(ObservableList<T> items) { 205 getStyleClass().add(DEFAULT_STYLE_CLASS); 206 setItems(items); 207 setSelectionModel(new ComboBoxSelectionModel<T>(this)); 208 209 // listen to the value property input by the user, and if the value is 210 // set to something that exists in the items list, we should update the 211 // selection model to indicate that this is the selected item 212 valueProperty().addListener(new ChangeListener<T>() { 213 @Override public void changed(ObservableValue<? extends T> ov, T t, T t1) { 214 if (getItems() == null) return; 215 216 SelectionModel<T> sm = getSelectionModel(); 217 int index = getItems().indexOf(t1); 218 219 if (index == -1) { 220 sm.setSelectedItem(t1); 221 } else { 222 // we must compare the value here with the currently selected 223 // item. If they are different, we overwrite the selection 224 // properties to reflect the new value. 225 // We do this as there can be circumstances where there are 226 // multiple instances of a value in the ComboBox items list, 227 // and if we don't check here we may change the selection 228 // mistakenly because the indexOf above will return the first 229 // instance always, and selection may be on the second or 230 // later instances. This is RT-19227. 231 T selectedItem = sm.getSelectedItem(); 232 if (selectedItem == null || ! selectedItem.equals(getValue())) { 233 sm.clearAndSelect(index); 234 } 235 } 236 } 237 }); 238 239 editableProperty().addListener(new InvalidationListener() { 240 @Override public void invalidated(Observable o) { 241 // when editable changes, we reset the selection / value states 242 getSelectionModel().clearSelection(); 243 } 244 }); 245 } 246 247 248 249 /*************************************************************************** 250 * * 251 * Properties * 252 * * 253 **************************************************************************/ 254 255 // --- items 256 /** 257 * The list of items to show within the ComboBox popup. 258 */ 259 private ObjectProperty<ObservableList<T>> items = new SimpleObjectProperty<ObservableList<T>>(this, "items") { 260 @Override protected void invalidated() { 261 // FIXME temporary fix for RT-15793. This will need to be 262 // properly fixed when time permits 263 if (getSelectionModel() instanceof ComboBoxSelectionModel) { 264 ((ComboBoxSelectionModel<T>)getSelectionModel()).updateItemsObserver(null, getItems()); 265 } 266 if (getSkin() instanceof ComboBoxListViewSkin) { 267 ComboBoxListViewSkin<?> skin = (ComboBoxListViewSkin<?>) getSkin(); 268 skin.updateListViewItems(); 269 } 270 } 271 }; 272 public final void setItems(ObservableList<T> value) { itemsProperty().set(value); } 273 public final ObservableList<T> getItems() {return items.get(); } 274 public ObjectProperty<ObservableList<T>> itemsProperty() { return items; } 275 276 277 // --- string converter 278 /** 279 * Converts the user-typed input (when the ComboBox is 280 * {@link #editableProperty() editable}) to an object of type T, such that 281 * the input may be retrieved via the {@link #valueProperty() value} property. 282 */ 283 public ObjectProperty<StringConverter<T>> converterProperty() { return converter; } 284 private ObjectProperty<StringConverter<T>> converter = 285 new SimpleObjectProperty<StringConverter<T>>(this, "converter", ComboBox.<T>defaultStringConverter()); 286 public final void setConverter(StringConverter<T> value) { converterProperty().set(value); } 287 public final StringConverter<T> getConverter() {return converterProperty().get(); } 288 289 290 // --- cell factory 291 /** 292 * Providing a custom cell factory allows for complete customization of the 293 * rendering of items in the ComboBox. Refer to the {@link Cell} javadoc 294 * for more information on cell factories. 295 */ 296 private ObjectProperty<Callback<ListView<T>, ListCell<T>>> cellFactory = 297 new SimpleObjectProperty<Callback<ListView<T>, ListCell<T>>>(this, "cellFactory"); 298 public final void setCellFactory(Callback<ListView<T>, ListCell<T>> value) { cellFactoryProperty().set(value); } 299 public final Callback<ListView<T>, ListCell<T>> getCellFactory() {return cellFactoryProperty().get(); } 300 public ObjectProperty<Callback<ListView<T>, ListCell<T>>> cellFactoryProperty() { return cellFactory; } 301 302 303 // --- button cell 304 /** 305 * The button cell is used to render what is shown in the ComboBox 'button' 306 * area. If a cell is set here, it does not change the rendering of the 307 * ComboBox popup list - that rendering is controlled via the 308 * {@link #cellFactoryProperty() cell factory} API. 309 * @since 2.2 310 */ 311 public ObjectProperty<ListCell<T>> buttonCellProperty() { return buttonCell; } 312 private ObjectProperty<ListCell<T>> buttonCell = 313 new SimpleObjectProperty<ListCell<T>>(this, "buttonCell"); 314 public final void setButtonCell(ListCell<T> value) { buttonCellProperty().set(value); } 315 public final ListCell<T> getButtonCell() {return buttonCellProperty().get(); } 316 317 318 // --- Selection Model 319 /** 320 * The selection model for the ComboBox. A ComboBox only supports 321 * single selection. 322 */ 323 private ObjectProperty<SingleSelectionModel<T>> selectionModel = new SimpleObjectProperty<SingleSelectionModel<T>>(this, "selectionModel") { 324 private SingleSelectionModel<T> oldSM = null; 325 @Override protected void invalidated() { 326 if (oldSM != null) { 327 oldSM.selectedItemProperty().removeListener(selectedItemListener); 328 } 329 SingleSelectionModel<T> sm = get(); 330 oldSM = sm; 331 if (sm != null) { 332 sm.selectedItemProperty().addListener(selectedItemListener); 333 } 334 } 335 }; 336 public final void setSelectionModel(SingleSelectionModel<T> value) { selectionModel.set(value); } 337 public final SingleSelectionModel<T> getSelectionModel() { return selectionModel.get(); } 338 public final ObjectProperty<SingleSelectionModel<T>> selectionModelProperty() { return selectionModel; } 339 340 341 // --- Visible Row Count 342 /** 343 * The maximum number of rows to be visible in the ComboBox popup when it is 344 * showing. By default this value is 10, but this can be changed to increase 345 * or decrease the height of the popup. 346 */ 347 private IntegerProperty visibleRowCount 348 = new SimpleIntegerProperty(this, "visibleRowCount", 10); 349 public final void setVisibleRowCount(int value) { visibleRowCount.set(value); } 350 public final int getVisibleRowCount() { return visibleRowCount.get(); } 351 public final IntegerProperty visibleRowCountProperty() { return visibleRowCount; } 352 353 354 // --- Editor 355 private TextField textField; 356 /** 357 * The editor for the ComboBox. The editor is null if the ComboBox is not 358 * {@link #editableProperty() editable}. 359 * @since 2.2 360 */ 361 private ReadOnlyObjectWrapper<TextField> editor; 362 public final TextField getEditor() { 363 return editorProperty().get(); 364 } 365 public final ReadOnlyObjectProperty<TextField> editorProperty() { 366 if (editor == null) { 367 editor = new ReadOnlyObjectWrapper<TextField>(this, "editor"); 368 textField = new TextField(); 369 editor.set(textField); 370 } 371 return editor.getReadOnlyProperty(); 372 } 373 374 375 // --- Placeholder Node 376 private ObjectProperty<Node> placeholder; 377 /** 378 * This Node is shown to the user when the ComboBox has no content to show. 379 * The placeholder node is shown in the ComboBox popup area 380 * when the items list is null or empty. This is different than the 381 * {@link #emptyTextProperty() emptyText} property, which is shown in the 382 * ComboBox Button / TextField area when there is no user-input value. 383 */ 384 public final ObjectProperty<Node> placeholderProperty() { 385 if (placeholder == null) { 386 placeholder = new SimpleObjectProperty<Node>(this, "placeholder"); 387 } 388 return placeholder; 389 } 390 public final void setPlaceholder(Node value) { 391 placeholderProperty().set(value); 392 } 393 public final Node getPlaceholder() { 394 return placeholder == null ? null : placeholder.get(); 395 } 396 397 398 399 /*************************************************************************** 400 * * 401 * Methods * 402 * * 403 **************************************************************************/ 404 405 /** {@inheritDoc} */ 406 @Override protected Skin<?> createDefaultSkin() { 407 return new ComboBoxListViewSkin<T>(this); 408 } 409 410 411 412 /*************************************************************************** 413 * * 414 * Callbacks and Events * 415 * * 416 **************************************************************************/ 417 418 // Listen to changes in the selectedItem property of the SelectionModel. 419 // When it changes, set the selectedItem in the value property. 420 private ChangeListener<T> selectedItemListener = new ChangeListener<T>() { 421 @Override public void changed(ObservableValue<? extends T> ov, T t, T t1) { 422 if (wasSetAllCalled && t1 == null) { 423 // no-op: fix for RT-22572 where the developer was completely 424 // replacing all items in the ComboBox, and expecting the 425 // selection (and ComboBox.value) to remain set. If this isn't 426 // here, we would updateValue(null). 427 // Additional fix for RT-22937: adding the '&& t1 == null'. 428 // Without this, there would be circumstances where the user 429 // selecting a new value from the ComboBox would end up in here, 430 // when we really should go into the updateValue(t1) call below. 431 // We should only ever go into this clause if t1 is null. 432 wasSetAllCalled = false; 433 } else { 434 updateValue(t1); 435 } 436 } 437 }; 438 439 440 441 /*************************************************************************** 442 * * 443 * Private methods * 444 * * 445 **************************************************************************/ 446 447 private void updateValue(T newValue) { 448 if (! valueProperty().isBound()) { 449 setValue(newValue); 450 } 451 } 452 453 454 455 456 /*************************************************************************** 457 * * 458 * Stylesheet Handling * 459 * * 460 **************************************************************************/ 461 462 private static final String DEFAULT_STYLE_CLASS = "combo-box"; 463 464 private boolean wasSetAllCalled = false; 465 private int previousItemCount = -1; 466 467 // package for testing 468 static class ComboBoxSelectionModel<T> extends SingleSelectionModel<T> { 469 private final ComboBox<T> comboBox; 470 471 public ComboBoxSelectionModel(final ComboBox<T> cb) { 472 if (cb == null) { 473 throw new NullPointerException("ComboBox can not be null"); 474 } 475 this.comboBox = cb; 476 477 selectedIndexProperty().addListener(new InvalidationListener() { 478 @Override public void invalidated(Observable valueModel) { 479 // we used to lazily retrieve the selected item, but now we just 480 // do it when the selection changes. 481 setSelectedItem(getModelItem(getSelectedIndex())); 482 } 483 }); 484 485 /* 486 * The following two listeners are used in conjunction with 487 * SelectionModel.select(T obj) to allow for a developer to select 488 * an item that is not actually in the data model. When this occurs, 489 * we actively try to find an index that matches this object, going 490 * so far as to actually watch for all changes to the items list, 491 * rechecking each time. 492 */ 493 494 this.comboBox.itemsProperty().addListener(weakItemsObserver); 495 if (comboBox.getItems() != null) { 496 this.comboBox.getItems().addListener(weakItemsContentObserver); 497 } 498 } 499 500 // watching for changes to the items list content 501 private final ListChangeListener<T> itemsContentObserver = new ListChangeListener<T>() { 502 @Override public void onChanged(Change<? extends T> c) { 503 if (comboBox.getItems() == null || comboBox.getItems().isEmpty()) { 504 setSelectedIndex(-1); 505 } else if (getSelectedIndex() == -1 && getSelectedItem() != null) { 506 int newIndex = comboBox.getItems().indexOf(getSelectedItem()); 507 if (newIndex != -1) { 508 setSelectedIndex(newIndex); 509 } 510 } 511 512 while (c.next()) { 513 comboBox.wasSetAllCalled = comboBox.previousItemCount == c.getRemovedSize(); 514 515 516 if (c.getFrom() <= getSelectedIndex() && getSelectedIndex()!= -1 && (c.wasAdded() || c.wasRemoved())) { 517 int shift = c.wasAdded() ? c.getAddedSize() : -c.getRemovedSize(); 518 clearAndSelect(getSelectedIndex() + shift); 519 } 520 } 521 522 comboBox.previousItemCount = getItemCount(); 523 } 524 }; 525 526 // watching for changes to the items list 527 private final ChangeListener<ObservableList<T>> itemsObserver = new ChangeListener<ObservableList<T>>() { 528 @Override 529 public void changed(ObservableValue<? extends ObservableList<T>> valueModel, 530 ObservableList<T> oldList, ObservableList<T> newList) { 531 updateItemsObserver(oldList, newList); 532 } 533 }; 534 535 private WeakListChangeListener<T> weakItemsContentObserver = 536 new WeakListChangeListener<T>(itemsContentObserver); 537 538 private WeakChangeListener<ObservableList<T>> weakItemsObserver = 539 new WeakChangeListener<ObservableList<T>>(itemsObserver); 540 541 private void updateItemsObserver(ObservableList<T> oldList, ObservableList<T> newList) { 542 // update listeners 543 if (oldList != null) { 544 oldList.removeListener(weakItemsContentObserver); 545 } 546 if (newList != null) { 547 newList.addListener(weakItemsContentObserver); 548 } 549 550 // when the items list totally changes, we should clear out 551 // the selection and focus 552 setSelectedIndex(-1); 553 } 554 555 // API Implementation 556 @Override protected T getModelItem(int index) { 557 final ObservableList<T> items = comboBox.getItems(); 558 if (items == null) return null; 559 if (index < 0 || index >= items.size()) return null; 560 return items.get(index); 561 } 562 563 @Override protected int getItemCount() { 564 final ObservableList<T> items = comboBox.getItems(); 565 return items == null ? 0 : items.size(); 566 } 567 } 568}