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&lt;Color&gt; cmb = new ComboBox&lt;Color&gt;();
 155  * cmb.getItems().addAll(
 156  *     Color.RED,
 157  *     Color.GREEN,
 158  *     Color.BLUE);
 159  *
 160  * cmb.setCellFactory(new Callback&lt;ListView&lt;Color&gt;, ListCell&lt;Color&gt;&gt;() {
 161  *     @Override public ListCell&lt;Color&gt; call(ListView&lt;Color&gt; p) {
 162  *         return new ListCell&lt;Color&gt;() {
 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 }