1 /*
   2  * Copyright (c) 2010, 2016, 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 com.sun.javafx.scene.control.FakeFocusTextField;
  29 import javafx.beans.InvalidationListener;
  30 import javafx.beans.Observable;
  31 import javafx.beans.WeakInvalidationListener;
  32 import javafx.collections.WeakListChangeListener;
  33 import javafx.scene.control.skin.ComboBoxListViewSkin;
  34 import javafx.beans.property.*;
  35 import javafx.beans.value.ChangeListener;
  36 import javafx.beans.value.ObservableValue;
  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  * @param <T> The type of the value that has been selected or otherwise entered
 187  * in to this ComboBox
 188  * @see ComboBoxBase
 189  * @see Cell
 190  * @see ListCell
 191  * @see StringConverter
 192  * @since JavaFX 2.1
 193  */
 194 public class ComboBox<T> extends ComboBoxBase<T> {
 195 
 196     /***************************************************************************
 197      *                                                                         *
 198      * Static properties and methods                                           *
 199      *                                                                         *
 200      **************************************************************************/
 201 
 202     private static <T> StringConverter<T> defaultStringConverter() {
 203         return new StringConverter<T>() {
 204             @Override public String toString(T t) {
 205                 return t == null ? null : t.toString();
 206             }
 207 
 208             @Override public T fromString(String string) {
 209                 return (T) string;
 210             }
 211         };
 212     }
 213 
 214 
 215 
 216     /***************************************************************************
 217      *                                                                         *
 218      * Constructors                                                            *
 219      *                                                                         *
 220      **************************************************************************/
 221 
 222     /**
 223      * Creates a default ComboBox instance with an empty
 224      * {@link #itemsProperty() items} list and default
 225      * {@link #selectionModelProperty() selection model}.
 226      */
 227     public ComboBox() {
 228         this(FXCollections.<T>observableArrayList());
 229     }
 230 
 231     /**
 232      * Creates a default ComboBox instance with the provided items list and
 233      * a default {@link #selectionModelProperty() selection model}.
 234      * @param items the list of items
 235      */
 236     public ComboBox(ObservableList<T> items) {
 237         getStyleClass().add(DEFAULT_STYLE_CLASS);
 238         setAccessibleRole(AccessibleRole.COMBO_BOX);
 239         setItems(items);
 240         setSelectionModel(new ComboBoxSelectionModel<T>(this));
 241 
 242         // listen to the value property input by the user, and if the value is
 243         // set to something that exists in the items list, we should update the
 244         // selection model to indicate that this is the selected item
 245         valueProperty().addListener((ov, t, t1) -> {
 246             if (getItems() == null) return;
 247 
 248             SelectionModel<T> sm = getSelectionModel();
 249             int index = getItems().indexOf(t1);
 250 
 251             if (index == -1) {
 252                 Runnable r = () -> {
 253                     sm.setSelectedIndex(-1);
 254                     sm.setSelectedItem(t1);
 255                 };
 256                 if (sm instanceof ComboBoxSelectionModel) {
 257                     ((ComboBoxSelectionModel)sm).doAtomic(r);
 258                 } else {
 259                     r.run();
 260                 }
 261             } else {
 262                 // we must compare the value here with the currently selected
 263                 // item. If they are different, we overwrite the selection
 264                 // properties to reflect the new value.
 265                 // We do this as there can be circumstances where there are
 266                 // multiple instances of a value in the ComboBox items list,
 267                 // and if we don't check here we may change the selection
 268                 // mistakenly because the indexOf above will return the first
 269                 // instance always, and selection may be on the second or
 270                 // later instances. This is RT-19227.
 271                 T selectedItem = sm.getSelectedItem();
 272                 if (selectedItem == null || ! selectedItem.equals(getValue())) {
 273                     sm.clearAndSelect(index);
 274                 }
 275             }
 276         });
 277 
 278         editableProperty().addListener(o -> {
 279             // When we change from being editable to non-editable, we look for the
 280             // current value in the items list. If it exists, we do not clear selection.
 281             // When we change from being non-editable to editable, we do nothing
 282             if (!isEditable()) {
 283                 // check if value is in items list
 284                 if (getItems() != null && !getItems().contains(getValue())) {
 285                     getSelectionModel().clearSelection();
 286                 }
 287             }
 288         });
 289 
 290         focusedProperty().addListener(o -> {
 291             if (!isFocused()) {
 292                 commitValue();
 293             }
 294         });
 295     }
 296 
 297 
 298 
 299     /***************************************************************************
 300      *                                                                         *
 301      * Properties                                                              *
 302      *                                                                         *
 303      **************************************************************************/
 304 
 305     // --- items
 306     /**
 307      * The list of items to show within the ComboBox popup.
 308      */
 309     private ObjectProperty<ObservableList<T>> items = new SimpleObjectProperty<ObservableList<T>>(this, "items");
 310     public final void setItems(ObservableList<T> value) { itemsProperty().set(value); }
 311     public final ObservableList<T> getItems() {return items.get(); }
 312     public ObjectProperty<ObservableList<T>> itemsProperty() { return items; }
 313 
 314 
 315     // --- string converter
 316     /**
 317      * Converts the user-typed input (when the ComboBox is
 318      * {@link #editableProperty() editable}) to an object of type T, such that
 319      * the input may be retrieved via the  {@link #valueProperty() value} property.
 320      * @return the converter property
 321      */
 322     public ObjectProperty<StringConverter<T>> converterProperty() { return converter; }
 323     private ObjectProperty<StringConverter<T>> converter =
 324             new SimpleObjectProperty<StringConverter<T>>(this, "converter", ComboBox.<T>defaultStringConverter());
 325     public final void setConverter(StringConverter<T> value) { converterProperty().set(value); }
 326     public final StringConverter<T> getConverter() {return converterProperty().get(); }
 327 
 328 
 329     // --- cell factory
 330     /**
 331      * Providing a custom cell factory allows for complete customization of the
 332      * rendering of items in the ComboBox. Refer to the {@link Cell} javadoc
 333      * for more information on cell factories.
 334      */
 335     private ObjectProperty<Callback<ListView<T>, ListCell<T>>> cellFactory =
 336             new SimpleObjectProperty<Callback<ListView<T>, ListCell<T>>>(this, "cellFactory");
 337     public final void setCellFactory(Callback<ListView<T>, ListCell<T>> value) { cellFactoryProperty().set(value); }
 338     public final Callback<ListView<T>, ListCell<T>> getCellFactory() {return cellFactoryProperty().get(); }
 339     public ObjectProperty<Callback<ListView<T>, ListCell<T>>> cellFactoryProperty() { return cellFactory; }
 340 
 341 
 342     // --- button cell
 343     /**
 344      * The button cell is used to render what is shown in the ComboBox 'button'
 345      * area. If a cell is set here, it does not change the rendering of the
 346      * ComboBox popup list - that rendering is controlled via the
 347      * {@link #cellFactoryProperty() cell factory} API.
 348      * @return the button cell property
 349      * @since JavaFX 2.2
 350      */
 351     public ObjectProperty<ListCell<T>> buttonCellProperty() { return buttonCell; }
 352     private ObjectProperty<ListCell<T>> buttonCell =
 353             new SimpleObjectProperty<ListCell<T>>(this, "buttonCell");
 354     public final void setButtonCell(ListCell<T> value) { buttonCellProperty().set(value); }
 355     public final ListCell<T> getButtonCell() {return buttonCellProperty().get(); }
 356 
 357 
 358     // --- Selection Model
 359     /**
 360      * The selection model for the ComboBox. A ComboBox only supports
 361      * single selection.
 362      */
 363     private ObjectProperty<SingleSelectionModel<T>> selectionModel = new SimpleObjectProperty<SingleSelectionModel<T>>(this, "selectionModel") {
 364         private SingleSelectionModel<T> oldSM = null;
 365         @Override protected void invalidated() {
 366             if (oldSM != null) {
 367                 oldSM.selectedItemProperty().removeListener(selectedItemListener);
 368             }
 369             SingleSelectionModel<T> sm = get();
 370             oldSM = sm;
 371             if (sm != null) {
 372                 sm.selectedItemProperty().addListener(selectedItemListener);
 373             }
 374         }
 375     };
 376     public final void setSelectionModel(SingleSelectionModel<T> value) { selectionModel.set(value); }
 377     public final SingleSelectionModel<T> getSelectionModel() { return selectionModel.get(); }
 378     public final ObjectProperty<SingleSelectionModel<T>> selectionModelProperty() { return selectionModel; }
 379 
 380 
 381     // --- Visible Row Count
 382     /**
 383      * The maximum number of rows to be visible in the ComboBox popup when it is
 384      * showing. By default this value is 10, but this can be changed to increase
 385      * or decrease the height of the popup.
 386      */
 387     private IntegerProperty visibleRowCount
 388             = new SimpleIntegerProperty(this, "visibleRowCount", 10);
 389     public final void setVisibleRowCount(int value) { visibleRowCount.set(value); }
 390     public final int getVisibleRowCount() { return visibleRowCount.get(); }
 391     public final IntegerProperty visibleRowCountProperty() { return visibleRowCount; }
 392 
 393 
 394     // --- Editor
 395     private TextField textField;
 396     /**
 397      * The editor for the ComboBox. The editor is null if the ComboBox is not
 398      * {@link #editableProperty() editable}.
 399      * @since JavaFX 2.2
 400      */
 401     private ReadOnlyObjectWrapper<TextField> editor;
 402     public final TextField getEditor() {
 403         return editorProperty().get();
 404     }
 405     public final ReadOnlyObjectProperty<TextField> editorProperty() {
 406         if (editor == null) {
 407             editor = new ReadOnlyObjectWrapper<>(this, "editor");
 408             textField = new FakeFocusTextField();
 409             editor.set(textField);
 410         }
 411         return editor.getReadOnlyProperty();
 412     }
 413 
 414 
 415     // --- Placeholder Node
 416     private ObjectProperty<Node> placeholder;
 417     /**
 418      * This Node is shown to the user when the ComboBox has no content to show.
 419      * The placeholder node is shown in the ComboBox popup area
 420      * when the items list is null or empty.
 421      * @return the placeholder property
 422      * @since JavaFX 8.0
 423      */
 424     public final ObjectProperty<Node> placeholderProperty() {
 425         if (placeholder == null) {
 426             placeholder = new SimpleObjectProperty<Node>(this, "placeholder");
 427         }
 428         return placeholder;
 429     }
 430     public final void setPlaceholder(Node value) {
 431         placeholderProperty().set(value);
 432     }
 433     public final Node getPlaceholder() {
 434         return placeholder == null ? null : placeholder.get();
 435     }
 436 
 437 
 438 
 439     /***************************************************************************
 440      *                                                                         *
 441      * Methods                                                                 *
 442      *                                                                         *
 443      **************************************************************************/
 444 
 445     /** {@inheritDoc} */
 446     @Override protected Skin<?> createDefaultSkin() {
 447         return new ComboBoxListViewSkin<T>(this);
 448     }
 449 
 450     /**
 451      * If the ComboBox is {@link #editableProperty() editable}, calling this method will attempt to
 452      * commit the current text and convert it to a {@link #valueProperty() value}.
 453      * @since 9
 454      */
 455     public final void commitValue() {
 456         if (!isEditable()) return;
 457         String text = getEditor().getText();
 458         StringConverter<T> converter = getConverter();
 459         if (converter != null) {
 460             T value = converter.fromString(text);
 461             setValue(value);
 462         }
 463     }
 464 
 465     /**
 466      * If the ComboBox is {@link #editableProperty() editable}, calling this method will attempt to
 467      * replace the editor text with the last committed {@link #valueProperty() value}.
 468      * @since 9
 469      */
 470     public final void cancelEdit() {
 471         if (!isEditable()) return;
 472         final T committedValue = getValue();
 473         StringConverter<T> converter = getConverter();
 474         if (converter != null) {
 475             String valueString = converter.toString(committedValue);
 476             getEditor().setText(valueString);
 477         }
 478     }
 479 
 480 
 481 
 482     /***************************************************************************
 483      *                                                                         *
 484      * Callbacks and Events                                                    *
 485      *                                                                         *
 486      **************************************************************************/
 487 
 488     // Listen to changes in the selectedItem property of the SelectionModel.
 489     // When it changes, set the selectedItem in the value property.
 490     private ChangeListener<T> selectedItemListener = new ChangeListener<T>() {
 491         @Override public void changed(ObservableValue<? extends T> ov, T t, T t1) {
 492             if (wasSetAllCalled && t1 == null) {
 493                 // no-op: fix for RT-22572 where the developer was completely
 494                 // replacing all items in the ComboBox, and expecting the
 495                 // selection (and ComboBox.value) to remain set. If this isn't
 496                 // here, we would updateValue(null).
 497                 // Additional fix for RT-22937: adding the '&& t1 == null'.
 498                 // Without this, there would be circumstances where the user
 499                 // selecting a new value from the ComboBox would end up in here,
 500                 // when we really should go into the updateValue(t1) call below.
 501                 // We should only ever go into this clause if t1 is null.
 502             } else {
 503                 updateValue(t1);
 504             }
 505 
 506             wasSetAllCalled = false;
 507         }
 508     };
 509 
 510 
 511 
 512     /***************************************************************************
 513      *                                                                         *
 514      * Private methods                                                         *
 515      *                                                                         *
 516      **************************************************************************/
 517 
 518     private void updateValue(T newValue) {
 519         if (! valueProperty().isBound()) {
 520             setValue(newValue);
 521         }
 522     }
 523 
 524 
 525 
 526 
 527     /***************************************************************************
 528      *                                                                         *
 529      * Stylesheet Handling                                                     *
 530      *                                                                         *
 531      **************************************************************************/
 532 
 533     private static final String DEFAULT_STYLE_CLASS = "combo-box";
 534 
 535     private boolean wasSetAllCalled = false;
 536     private int previousItemCount = -1;
 537 
 538     // package for testing
 539     static class ComboBoxSelectionModel<T> extends SingleSelectionModel<T> {
 540         private final ComboBox<T> comboBox;
 541 
 542         private boolean atomic = false;
 543         private void doAtomic(Runnable r) {
 544             atomic = true;
 545             r.run();
 546             atomic = false;
 547         }
 548 
 549         public ComboBoxSelectionModel(final ComboBox<T> cb) {
 550             if (cb == null) {
 551                 throw new NullPointerException("ComboBox can not be null");
 552             }
 553             this.comboBox = cb;
 554             this.comboBox.previousItemCount = getItemCount();
 555 
 556             selectedIndexProperty().addListener(valueModel -> {
 557                 // we used to lazily retrieve the selected item, but now we just
 558                 // do it when the selection changes.
 559                 if (atomic) return;
 560                 setSelectedItem(getModelItem(getSelectedIndex()));
 561             });
 562 
 563             /*
 564              * The following two listeners are used in conjunction with
 565              * SelectionModel.select(T obj) to allow for a developer to select
 566              * an item that is not actually in the data model. When this occurs,
 567              * we actively try to find an index that matches this object, going
 568              * so far as to actually watch for all changes to the items list,
 569              * rechecking each time.
 570              */
 571             itemsObserver = new InvalidationListener() {
 572                 private WeakReference<ObservableList<T>> weakItemsRef = new WeakReference<>(comboBox.getItems());
 573 
 574                 @Override public void invalidated(Observable observable) {
 575                     ObservableList<T> oldItems = weakItemsRef.get();
 576                     weakItemsRef = new WeakReference<>(comboBox.getItems());
 577                     updateItemsObserver(oldItems, comboBox.getItems());
 578                     comboBox.previousItemCount = getItemCount();
 579                 }
 580             };
 581             this.comboBox.itemsProperty().addListener(new WeakInvalidationListener(itemsObserver));
 582             if (comboBox.getItems() != null) {
 583                 this.comboBox.getItems().addListener(weakItemsContentObserver);
 584             }
 585         }
 586 
 587         // watching for changes to the items list content
 588         private final ListChangeListener<T> itemsContentObserver = new ListChangeListener<T>() {
 589             @Override public void onChanged(Change<? extends T> c) {
 590                 if (comboBox.getItems() == null || comboBox.getItems().isEmpty()) {
 591                     setSelectedIndex(-1);
 592                 } else if (getSelectedIndex() == -1 && getSelectedItem() != null) {
 593                     int newIndex = comboBox.getItems().indexOf(getSelectedItem());
 594                     if (newIndex != -1) {
 595                         setSelectedIndex(newIndex);
 596                     }
 597                 }
 598 
 599                 int shift = 0;
 600                 while (c.next()) {
 601                     comboBox.wasSetAllCalled = comboBox.previousItemCount == c.getRemovedSize();
 602 
 603                     if (c.wasReplaced()) {
 604                         // no-op
 605                     } else if (c.wasAdded() || c.wasRemoved()) {
 606                         if (c.getFrom() <= getSelectedIndex() && getSelectedIndex()!= -1) {
 607                             shift += c.wasAdded() ? c.getAddedSize() : -c.getRemovedSize();
 608                         }
 609                     }
 610                 }
 611 
 612                 if (shift != 0) {
 613                     clearAndSelect(getSelectedIndex() + shift);
 614                 } else if (comboBox.wasSetAllCalled && getSelectedIndex() >= 0 && getSelectedItem() != null) {
 615                     // try to find the previously selected item
 616                     T selectedItem = getSelectedItem();
 617                     for (int i = 0; i < comboBox.getItems().size(); i++) {
 618                         if (selectedItem.equals(comboBox.getItems().get(i))) {
 619                             comboBox.setValue(null);
 620                             setSelectedItem(null);
 621                             setSelectedIndex(i);
 622                             break;
 623                         }
 624                     }
 625                 }
 626 
 627                 comboBox.previousItemCount = getItemCount();
 628             }
 629         };
 630 
 631         // watching for changes to the items list
 632         private final InvalidationListener itemsObserver;
 633 
 634         private WeakListChangeListener<T> weakItemsContentObserver =
 635                 new WeakListChangeListener<T>(itemsContentObserver);
 636 
 637 
 638         private void updateItemsObserver(ObservableList<T> oldList, ObservableList<T> newList) {
 639             // update listeners
 640             if (oldList != null) {
 641                 oldList.removeListener(weakItemsContentObserver);
 642             }
 643             if (newList != null) {
 644                 newList.addListener(weakItemsContentObserver);
 645             }
 646 
 647             // when the items list totally changes, we should clear out
 648             // the selection and focus
 649             int newValueIndex = -1;
 650             if (newList != null) {
 651                 T value = comboBox.getValue();
 652                 if (value != null) {
 653                     newValueIndex = newList.indexOf(value);
 654                 }
 655             }
 656             setSelectedIndex(newValueIndex);
 657         }
 658 
 659         // API Implementation
 660         @Override protected T getModelItem(int index) {
 661             final ObservableList<T> items = comboBox.getItems();
 662             if (items == null) return null;
 663             if (index < 0 || index >= items.size()) return null;
 664             return items.get(index);
 665         }
 666 
 667         @Override protected int getItemCount() {
 668             final ObservableList<T> items = comboBox.getItems();
 669             return items == null ? 0 : items.size();
 670         }
 671     }
 672 
 673     /***************************************************************************
 674      *                                                                         *
 675      * Accessibility handling                                                  *
 676      *                                                                         *
 677      **************************************************************************/
 678 
 679     @Override
 680     public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 681         switch(attribute) {
 682             case TEXT:
 683                 String accText = getAccessibleText();
 684                 if (accText != null && !accText.isEmpty()) return accText;
 685 
 686                 //let the skin first.
 687                 Object title = super.queryAccessibleAttribute(attribute, parameters);
 688                 if (title != null) return title;
 689                 StringConverter<T> converter = getConverter();
 690                 if (converter == null) {
 691                     return getValue() != null ? getValue().toString() : "";
 692                 }
 693                 return converter.toString(getValue());
 694             default: return super.queryAccessibleAttribute(attribute, parameters);
 695         }
 696     }
 697 
 698 }