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