/*
 * Copyright (c) 2010, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package javafx.scene.control;

import com.sun.javafx.scene.control.FakeFocusTextField;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.collections.WeakListChangeListener;
import javafx.scene.control.skin.ComboBoxListViewSkin;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.AccessibleAttribute;
import javafx.scene.AccessibleRole;
import javafx.scene.Node;
import javafx.util.Callback;
import javafx.util.StringConverter;

import java.lang.ref.WeakReference;

An implementation of the ComboBoxBase abstract class for the most common form of ComboBox, where a popup list is shown to users providing them with a choice that they may select from. For more information around the general concepts and API of ComboBox, refer to the ComboBoxBase class documentation.

On top of ComboBoxBase, the ComboBox class introduces additional API. Most importantly, it adds an items property that works in much the same way as the ListView items property. In other words, it is the content of the items list that is displayed to users when they click on the ComboBox button.

The ComboBox exposes the ComboBoxBase.valueProperty() from ComboBoxBase, but there are some important points of the value property that need to be understood in relation to ComboBox. These include:

  1. The value property is not constrained to items contained within the items list - it can be anything as long as it is a valid value of type T.
  2. If the value property is set to a non-null object, and subsequently the items list is cleared, the value property is not nulled out.
  3. Clearing the selection in the selection model does not null the value property - it remains the same as before.
  4. It is valid for the selection model to have a selection set to a given index even if there is no items in the list (or less items in the list than the given index). Once the items list is further populated, such that the list contains enough items to have an item in the given index, both the selection model SelectionModel.selectedItemProperty() and value property will be updated to have this value. This is inconsistent with other controls that use a selection model, but done intentionally for ComboBox.

By default, when the popup list is showing, the maximum number of rows visible is 10, but this can be changed by modifying the visibleRowCount property. If the number of items in the ComboBox is less than the value of visibleRowCount, then the items size will be used instead so that the popup list is not exceedingly long.

As with ListView, it is possible to modify the selection model that is used, although this is likely to be rarely changed. This is because the ComboBox enforces the need for a SingleSelectionModel instance, and it is not likely that there is much need for alternate implementations. Nonetheless, the option is there should use cases be found for switching the selection model.

As the ComboBox internally renders content with a ListView, API exists in the ComboBox class to allow for a custom cell factory to be set. For more information on cell factories, refer to the Cell and ListCell classes. It is important to note that if a cell factory is set on a ComboBox, cells will only be used in the ListView that shows when the ComboBox is clicked. If you also want to customize the rendering of the 'button' area of the ComboBox, you can set a custom ListCell instance in the button cell property. One way of doing this is with the following code (note the use of setButtonCell:


Callback<ListView<String>, ListCell<String>> cellFactory = ...;
ComboBox comboBox = new ComboBox();
comboBox.setItems(items);
comboBox.setButtonCell(cellFactory.call(null));
comboBox.setCellFactory(cellFactory);

Because a ComboBox can be editable, and the default means of allowing user input is via a TextField, a string converter property is provided to allow for developers to specify how to translate a users string into an object of type T, such that the value property may contain it. By default the converter simply returns the String input as the user typed it, which therefore assumes that the type of the editable ComboBox is String. If a different type is specified and the ComboBox is to be editable, it is necessary to specify a custom StringConverter.

A warning about inserting Nodes into the ComboBox items list

ComboBox allows for the items list to contain elements of any type, including Node instances. Putting nodes into the items list is strongly not recommended. This is because the default cell factory simply inserts Node items directly into the cell, including in the ComboBox 'button' area too. Because the scenegraph only allows for Nodes to be in one place at a time, this means that when an item is selected it becomes removed from the ComboBox list, and becomes visible in the button area. When selection changes the previously selected item returns to the list and the new selection is removed.

The recommended approach, rather than inserting Node instances into the items list, is to put the relevant information into the ComboBox, and then provide a custom cell factory. For example, rather than use the following code:


ComboBox<Rectangle> cmb = new ComboBox<Rectangle>();
cmb.getItems().addAll(
    new Rectangle(10, 10, Color.RED),
    new Rectangle(10, 10, Color.GREEN),
    new Rectangle(10, 10, Color.BLUE));

You should do the following:


ComboBox<Color> cmb = new ComboBox<Color>();
cmb.getItems().addAll(
    Color.RED,
    Color.GREEN,
    Color.BLUE);
cmb.setCellFactory(new Callback<ListView<Color>, ListCell<Color>>() {
    @Override public ListCell<Color> call(ListView<Color> p) {
        return new ListCell<Color>() {
            private final Rectangle rectangle;
            {
                setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
                rectangle = new Rectangle(10, 10);
            }
            @Override protected void updateItem(Color item, boolean empty) {
                super.updateItem(item, empty);
                if (item == null || empty) {
                    setGraphic(null);
                } else {
                    rectangle.setFill(item);
                    setGraphic(rectangle);
                }
           }
      };
  }
});

Admittedly the above approach is far more verbose, but it offers the required functionality without encountering the scenegraph constraints.

Type parameters:
  • <T> – The type of the value that has been selected or otherwise entered in to this ComboBox
See Also:
Since:JavaFX 2.1
/** * An implementation of the {@link ComboBoxBase} abstract class for the most common * form of ComboBox, where a popup list is shown to users providing them with * a choice that they may select from. For more information around the general * concepts and API of ComboBox, refer to the {@link ComboBoxBase} class * documentation. * * <p>On top of ComboBoxBase, the ComboBox class introduces additional API. Most * importantly, it adds an {@link #itemsProperty() items} property that works in * much the same way as the ListView {@link ListView#itemsProperty() items} * property. In other words, it is the content of the items list that is displayed * to users when they click on the ComboBox button. * * <p>The ComboBox exposes the {@link #valueProperty()} from * {@link javafx.scene.control.ComboBoxBase}, but there are some important points * of the value property that need to be understood in relation to ComboBox. * These include: * * <ol> * <li>The value property <strong>is not</strong> constrained to items contained * within the items list - it can be anything as long as it is a valid value * of type T.</li> * <li>If the value property is set to a non-null object, and subsequently the * items list is cleared, the value property <strong>is not</strong> nulled out.</li> * <li>Clearing the {@link javafx.scene.control.SelectionModel#clearSelection() * selection} in the selection model <strong>does not</strong> null the value * property - it remains the same as before.</li> * <li>It is valid for the selection model to have a selection set to a given * index even if there is no items in the list (or less items in the list than * the given index). Once the items list is further populated, such that the * list contains enough items to have an item in the given index, both the * selection model {@link SelectionModel#selectedItemProperty()} and * value property will be updated to have this value. This is inconsistent with * other controls that use a selection model, but done intentionally for ComboBox.</li> * </ol> * * <p>By default, when the popup list is showing, the maximum number of rows * visible is 10, but this can be changed by modifying the * {@link #visibleRowCountProperty() visibleRowCount} property. If the number of * items in the ComboBox is less than the value of <code>visibleRowCount</code>, * then the items size will be used instead so that the popup list is not * exceedingly long. * * <p>As with ListView, it is possible to modify the * {@link javafx.scene.control.SelectionModel selection model} that is used, * although this is likely to be rarely changed. This is because the ComboBox * enforces the need for a {@link javafx.scene.control.SingleSelectionModel} * instance, and it is not likely that there is much need for alternate * implementations. Nonetheless, the option is there should use cases be found * for switching the selection model. * * <p>As the ComboBox internally renders content with a ListView, API exists in * the ComboBox class to allow for a custom cell factory to be set. For more * information on cell factories, refer to the {@link Cell} and {@link ListCell} * classes. It is important to note that if a cell factory is set on a ComboBox, * cells will only be used in the ListView that shows when the ComboBox is * clicked. If you also want to customize the rendering of the 'button' area * of the ComboBox, you can set a custom {@link ListCell} instance in the * {@link #buttonCellProperty() button cell} property. One way of doing this * is with the following code (note the use of {@code setButtonCell}: * * <pre> * {@code * Callback<ListView<String>, ListCell<String>> cellFactory = ...; * ComboBox comboBox = new ComboBox(); * comboBox.setItems(items); * comboBox.setButtonCell(cellFactory.call(null)); * comboBox.setCellFactory(cellFactory);}</pre> * * <p>Because a ComboBox can be {@link #editableProperty() editable}, and the * default means of allowing user input is via a {@link TextField}, a * {@link #converterProperty() string converter} property is provided to allow * for developers to specify how to translate a users string into an object of * type T, such that the {@link #valueProperty() value} property may contain it. * By default the converter simply returns the String input as the user typed it, * which therefore assumes that the type of the editable ComboBox is String. If * a different type is specified and the ComboBox is to be editable, it is * necessary to specify a custom {@link StringConverter}. * * <h3>A warning about inserting Nodes into the ComboBox items list</h3> * ComboBox allows for the items list to contain elements of any type, including * {@link Node} instances. Putting nodes into * the items list is <strong>strongly not recommended</strong>. This is because * the default {@link #cellFactoryProperty() cell factory} simply inserts Node * items directly into the cell, including in the ComboBox 'button' area too. * Because the scenegraph only allows for Nodes to be in one place at a time, * this means that when an item is selected it becomes removed from the ComboBox * list, and becomes visible in the button area. When selection changes the * previously selected item returns to the list and the new selection is removed. * * <p>The recommended approach, rather than inserting Node instances into the * items list, is to put the relevant information into the ComboBox, and then * provide a custom {@link #cellFactoryProperty() cell factory}. For example, * rather than use the following code: * * <pre> * {@code * ComboBox<Rectangle> cmb = new ComboBox<Rectangle>(); * cmb.getItems().addAll( * new Rectangle(10, 10, Color.RED), * new Rectangle(10, 10, Color.GREEN), * new Rectangle(10, 10, Color.BLUE));}</pre> * * <p>You should do the following:</p> * * <pre><code> * ComboBox&lt;Color&gt; cmb = new ComboBox&lt;Color&gt;(); * cmb.getItems().addAll( * Color.RED, * Color.GREEN, * Color.BLUE); * * cmb.setCellFactory(new Callback&lt;ListView&lt;Color&gt;, ListCell&lt;Color&gt;&gt;() { * &#064;Override public ListCell&lt;Color&gt; call(ListView&lt;Color&gt; p) { * return new ListCell&lt;Color&gt;() { * private final Rectangle rectangle; * { * setContentDisplay(ContentDisplay.GRAPHIC_ONLY); * rectangle = new Rectangle(10, 10); * } * * &#064;Override protected void updateItem(Color item, boolean empty) { * super.updateItem(item, empty); * * if (item == null || empty) { * setGraphic(null); * } else { * rectangle.setFill(item); * setGraphic(rectangle); * } * } * }; * } *});</code></pre> * * <p>Admittedly the above approach is far more verbose, but it offers the * required functionality without encountering the scenegraph constraints. * * @param <T> The type of the value that has been selected or otherwise entered * in to this ComboBox * @see ComboBoxBase * @see Cell * @see ListCell * @see StringConverter * @since JavaFX 2.1 */
public class ComboBox<T> extends ComboBoxBase<T> {
* Static properties and methods * *
/*************************************************************************** * * * Static properties and methods * * * **************************************************************************/
private static <T> StringConverter<T> defaultStringConverter() { return new StringConverter<T>() { @Override public String toString(T t) { return t == null ? null : t.toString(); } @Override public T fromString(String string) { return (T) string; } }; } /*************************************************************************** * * * Constructors * * * **************************************************************************/
Creates a default ComboBox instance with an empty items list and default selection model.
/** * Creates a default ComboBox instance with an empty * {@link #itemsProperty() items} list and default * {@link #selectionModelProperty() selection model}. */
public ComboBox() { this(FXCollections.<T>observableArrayList()); }
Creates a default ComboBox instance with the provided items list and a default selection model.
Params:
  • items – the list of items
/** * Creates a default ComboBox instance with the provided items list and * a default {@link #selectionModelProperty() selection model}. * @param items the list of items */
public ComboBox(ObservableList<T> items) { getStyleClass().add(DEFAULT_STYLE_CLASS); setAccessibleRole(AccessibleRole.COMBO_BOX); setItems(items); setSelectionModel(new ComboBoxSelectionModel<T>(this)); // listen to the value property input by the user, and if the value is // set to something that exists in the items list, we should update the // selection model to indicate that this is the selected item valueProperty().addListener((ov, t, t1) -> { if (getItems() == null) return; SelectionModel<T> sm = getSelectionModel(); int index = getItems().indexOf(t1); if (index == -1) { Runnable r = () -> { sm.setSelectedIndex(-1); sm.setSelectedItem(t1); }; if (sm instanceof ComboBoxSelectionModel) { ((ComboBoxSelectionModel)sm).doAtomic(r); } else { r.run(); } } else { // we must compare the value here with the currently selected // item. If they are different, we overwrite the selection // properties to reflect the new value. // We do this as there can be circumstances where there are // multiple instances of a value in the ComboBox items list, // and if we don't check here we may change the selection // mistakenly because the indexOf above will return the first // instance always, and selection may be on the second or // later instances. This is RT-19227. T selectedItem = sm.getSelectedItem(); if (selectedItem == null || ! selectedItem.equals(getValue())) { sm.clearAndSelect(index); } } }); editableProperty().addListener(o -> { // When we change from being editable to non-editable, we look for the // current value in the items list. If it exists, we do not clear selection. // When we change from being non-editable to editable, we do nothing if (!isEditable()) { // check if value is in items list if (getItems() != null && !getItems().contains(getValue())) { getSelectionModel().clearSelection(); } } }); focusedProperty().addListener(o -> { if (!isFocused()) { commitValue(); } }); } /*************************************************************************** * * * Properties * * * **************************************************************************/ // --- items
The list of items to show within the ComboBox popup.
/** * The list of items to show within the ComboBox popup. */
private ObjectProperty<ObservableList<T>> items = new SimpleObjectProperty<ObservableList<T>>(this, "items"); public final void setItems(ObservableList<T> value) { itemsProperty().set(value); } public final ObservableList<T> getItems() {return items.get(); } public ObjectProperty<ObservableList<T>> itemsProperty() { return items; } // --- string converter
Converts the user-typed input (when the ComboBox is editable) to an object of type T, such that the input may be retrieved via the value property.
Returns:the converter property
/** * Converts the user-typed input (when the ComboBox is * {@link #editableProperty() editable}) to an object of type T, such that * the input may be retrieved via the {@link #valueProperty() value} property. * @return the converter property */
public ObjectProperty<StringConverter<T>> converterProperty() { return converter; } private ObjectProperty<StringConverter<T>> converter = new SimpleObjectProperty<StringConverter<T>>(this, "converter", ComboBox.<T>defaultStringConverter()); public final void setConverter(StringConverter<T> value) { converterProperty().set(value); } public final StringConverter<T> getConverter() {return converterProperty().get(); } // --- cell factory
Providing a custom cell factory allows for complete customization of the rendering of items in the ComboBox. Refer to the Cell javadoc for more information on cell factories.
/** * Providing a custom cell factory allows for complete customization of the * rendering of items in the ComboBox. Refer to the {@link Cell} javadoc * for more information on cell factories. */
private ObjectProperty<Callback<ListView<T>, ListCell<T>>> cellFactory = new SimpleObjectProperty<Callback<ListView<T>, ListCell<T>>>(this, "cellFactory"); public final void setCellFactory(Callback<ListView<T>, ListCell<T>> value) { cellFactoryProperty().set(value); } public final Callback<ListView<T>, ListCell<T>> getCellFactory() {return cellFactoryProperty().get(); } public ObjectProperty<Callback<ListView<T>, ListCell<T>>> cellFactoryProperty() { return cellFactory; } // --- button cell
The button cell is used to render what is shown in the ComboBox 'button' area. If a cell is set here, it does not change the rendering of the ComboBox popup list - that rendering is controlled via the cell factory API.
Returns:the button cell property
Since:JavaFX 2.2
/** * The button cell is used to render what is shown in the ComboBox 'button' * area. If a cell is set here, it does not change the rendering of the * ComboBox popup list - that rendering is controlled via the * {@link #cellFactoryProperty() cell factory} API. * @return the button cell property * @since JavaFX 2.2 */
public ObjectProperty<ListCell<T>> buttonCellProperty() { return buttonCell; } private ObjectProperty<ListCell<T>> buttonCell = new SimpleObjectProperty<ListCell<T>>(this, "buttonCell"); public final void setButtonCell(ListCell<T> value) { buttonCellProperty().set(value); } public final ListCell<T> getButtonCell() {return buttonCellProperty().get(); } // --- Selection Model
The selection model for the ComboBox. A ComboBox only supports single selection.
/** * The selection model for the ComboBox. A ComboBox only supports * single selection. */
private ObjectProperty<SingleSelectionModel<T>> selectionModel = new SimpleObjectProperty<SingleSelectionModel<T>>(this, "selectionModel") { private SingleSelectionModel<T> oldSM = null; @Override protected void invalidated() { if (oldSM != null) { oldSM.selectedItemProperty().removeListener(selectedItemListener); } SingleSelectionModel<T> sm = get(); oldSM = sm; if (sm != null) { sm.selectedItemProperty().addListener(selectedItemListener); } } }; public final void setSelectionModel(SingleSelectionModel<T> value) { selectionModel.set(value); } public final SingleSelectionModel<T> getSelectionModel() { return selectionModel.get(); } public final ObjectProperty<SingleSelectionModel<T>> selectionModelProperty() { return selectionModel; } // --- Visible Row Count
The maximum number of rows to be visible in the ComboBox popup when it is showing. By default this value is 10, but this can be changed to increase or decrease the height of the popup.
/** * The maximum number of rows to be visible in the ComboBox popup when it is * showing. By default this value is 10, but this can be changed to increase * or decrease the height of the popup. */
private IntegerProperty visibleRowCount = new SimpleIntegerProperty(this, "visibleRowCount", 10); public final void setVisibleRowCount(int value) { visibleRowCount.set(value); } public final int getVisibleRowCount() { return visibleRowCount.get(); } public final IntegerProperty visibleRowCountProperty() { return visibleRowCount; } // --- Editor private TextField textField;
The editor for the ComboBox. The editor is null if the ComboBox is not editable.
Since:JavaFX 2.2
/** * The editor for the ComboBox. The editor is null if the ComboBox is not * {@link #editableProperty() editable}. * @since JavaFX 2.2 */
private ReadOnlyObjectWrapper<TextField> editor; public final TextField getEditor() { return editorProperty().get(); } public final ReadOnlyObjectProperty<TextField> editorProperty() { if (editor == null) { editor = new ReadOnlyObjectWrapper<>(this, "editor"); textField = new FakeFocusTextField(); editor.set(textField); } return editor.getReadOnlyProperty(); } // --- Placeholder Node private ObjectProperty<Node> placeholder;
This Node is shown to the user when the ComboBox has no content to show. The placeholder node is shown in the ComboBox popup area when the items list is null or empty.
Returns:the placeholder property
Since:JavaFX 8.0
/** * This Node is shown to the user when the ComboBox has no content to show. * The placeholder node is shown in the ComboBox popup area * when the items list is null or empty. * @return the placeholder property * @since JavaFX 8.0 */
public final ObjectProperty<Node> placeholderProperty() { if (placeholder == null) { placeholder = new SimpleObjectProperty<Node>(this, "placeholder"); } return placeholder; } public final void setPlaceholder(Node value) { placeholderProperty().set(value); } public final Node getPlaceholder() { return placeholder == null ? null : placeholder.get(); } /*************************************************************************** * * * Methods * * * **************************************************************************/
{@inheritDoc}
/** {@inheritDoc} */
@Override protected Skin<?> createDefaultSkin() { return new ComboBoxListViewSkin<T>(this); }
If the ComboBox is editable, calling this method will attempt to commit the current text and convert it to a value.
Since:9
/** * If the ComboBox is {@link #editableProperty() editable}, calling this method will attempt to * commit the current text and convert it to a {@link #valueProperty() value}. * @since 9 */
public final void commitValue() { if (!isEditable()) return; String text = getEditor().getText(); StringConverter<T> converter = getConverter(); if (converter != null) { T value = converter.fromString(text); setValue(value); } }
If the ComboBox is editable, calling this method will attempt to replace the editor text with the last committed value.
Since:9
/** * If the ComboBox is {@link #editableProperty() editable}, calling this method will attempt to * replace the editor text with the last committed {@link #valueProperty() value}. * @since 9 */
public final void cancelEdit() { if (!isEditable()) return; final T committedValue = getValue(); StringConverter<T> converter = getConverter(); if (converter != null) { String valueString = converter.toString(committedValue); getEditor().setText(valueString); } }
* Callbacks and Events * *
/*************************************************************************** * * * Callbacks and Events * * * **************************************************************************/
// Listen to changes in the selectedItem property of the SelectionModel. // When it changes, set the selectedItem in the value property. private ChangeListener<T> selectedItemListener = new ChangeListener<T>() { @Override public void changed(ObservableValue<? extends T> ov, T t, T t1) { if (wasSetAllCalled && t1 == null) { // no-op: fix for RT-22572 where the developer was completely // replacing all items in the ComboBox, and expecting the // selection (and ComboBox.value) to remain set. If this isn't // here, we would updateValue(null). // Additional fix for RT-22937: adding the '&& t1 == null'. // Without this, there would be circumstances where the user // selecting a new value from the ComboBox would end up in here, // when we really should go into the updateValue(t1) call below. // We should only ever go into this clause if t1 is null. } else { updateValue(t1); } wasSetAllCalled = false; } };
* Private methods * *
/*************************************************************************** * * * Private methods * * * **************************************************************************/
private void updateValue(T newValue) { if (! valueProperty().isBound()) { setValue(newValue); } }
* Stylesheet Handling * *
/*************************************************************************** * * * Stylesheet Handling * * * **************************************************************************/
private static final String DEFAULT_STYLE_CLASS = "combo-box"; private boolean wasSetAllCalled = false; private int previousItemCount = -1; // package for testing static class ComboBoxSelectionModel<T> extends SingleSelectionModel<T> { private final ComboBox<T> comboBox; private boolean atomic = false; private void doAtomic(Runnable r) { atomic = true; r.run(); atomic = false; } public ComboBoxSelectionModel(final ComboBox<T> cb) { if (cb == null) { throw new NullPointerException("ComboBox can not be null"); } this.comboBox = cb; this.comboBox.previousItemCount = getItemCount(); selectedIndexProperty().addListener(valueModel -> { // we used to lazily retrieve the selected item, but now we just // do it when the selection changes. if (atomic) return; setSelectedItem(getModelItem(getSelectedIndex())); }); /* * The following two listeners are used in conjunction with * SelectionModel.select(T obj) to allow for a developer to select * an item that is not actually in the data model. When this occurs, * we actively try to find an index that matches this object, going * so far as to actually watch for all changes to the items list, * rechecking each time. */ itemsObserver = new InvalidationListener() { private WeakReference<ObservableList<T>> weakItemsRef = new WeakReference<>(comboBox.getItems()); @Override public void invalidated(Observable observable) { ObservableList<T> oldItems = weakItemsRef.get(); weakItemsRef = new WeakReference<>(comboBox.getItems()); updateItemsObserver(oldItems, comboBox.getItems()); comboBox.previousItemCount = getItemCount(); } }; this.comboBox.itemsProperty().addListener(new WeakInvalidationListener(itemsObserver)); if (comboBox.getItems() != null) { this.comboBox.getItems().addListener(weakItemsContentObserver); } } // watching for changes to the items list content private final ListChangeListener<T> itemsContentObserver = new ListChangeListener<T>() { @Override public void onChanged(Change<? extends T> c) { if (comboBox.getItems() == null || comboBox.getItems().isEmpty()) { setSelectedIndex(-1); } else if (getSelectedIndex() == -1 && getSelectedItem() != null) { int newIndex = comboBox.getItems().indexOf(getSelectedItem()); if (newIndex != -1) { setSelectedIndex(newIndex); } } int shift = 0; while (c.next()) { comboBox.wasSetAllCalled = comboBox.previousItemCount == c.getRemovedSize(); if (c.wasReplaced()) { // no-op } else if (c.wasAdded() || c.wasRemoved()) { if (c.getFrom() <= getSelectedIndex() && getSelectedIndex()!= -1) { shift += c.wasAdded() ? c.getAddedSize() : -c.getRemovedSize(); } } } if (shift != 0) { clearAndSelect(getSelectedIndex() + shift); } else if (comboBox.wasSetAllCalled && getSelectedIndex() >= 0 && getSelectedItem() != null) { // try to find the previously selected item T selectedItem = getSelectedItem(); for (int i = 0; i < comboBox.getItems().size(); i++) { if (selectedItem.equals(comboBox.getItems().get(i))) { comboBox.setValue(null); setSelectedItem(null); setSelectedIndex(i); break; } } } comboBox.previousItemCount = getItemCount(); } }; // watching for changes to the items list private final InvalidationListener itemsObserver; private WeakListChangeListener<T> weakItemsContentObserver = new WeakListChangeListener<T>(itemsContentObserver); private void updateItemsObserver(ObservableList<T> oldList, ObservableList<T> newList) { // update listeners if (oldList != null) { oldList.removeListener(weakItemsContentObserver); } if (newList != null) { newList.addListener(weakItemsContentObserver); } // when the items list totally changes, we should clear out // the selection and focus int newValueIndex = -1; if (newList != null) { T value = comboBox.getValue(); if (value != null) { newValueIndex = newList.indexOf(value); } } setSelectedIndex(newValueIndex); } // API Implementation @Override protected T getModelItem(int index) { final ObservableList<T> items = comboBox.getItems(); if (items == null) return null; if (index < 0 || index >= items.size()) return null; return items.get(index); } @Override protected int getItemCount() { final ObservableList<T> items = comboBox.getItems(); return items == null ? 0 : items.size(); } } /*************************************************************************** * * * Accessibility handling * * * **************************************************************************/
{@inheritDoc}
/** {@inheritDoc} */
@Override public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { switch(attribute) { case TEXT: String accText = getAccessibleText(); if (accText != null && !accText.isEmpty()) return accText; //let the skin first. Object title = super.queryAccessibleAttribute(attribute, parameters); if (title != null) return title; StringConverter<T> converter = getConverter(); if (converter == null) { return getValue() != null ? getValue().toString() : ""; } return converter.toString(getValue()); default: return super.queryAccessibleAttribute(attribute, parameters); } } }