javafx.scene.control.ChoiceBox Maven / Gradle / Ivy
Show all versions of openjfx-78-backport Show documentation
/*
* Copyright (c) 2010, 2013, 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 javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.event.ActionEvent;
import javafx.util.StringConverter;
import javafx.css.PseudoClass;
import com.sun.javafx.scene.control.skin.ChoiceBoxSkin;
import javafx.beans.DefaultProperty;
/**
* The ChoiceBox is used for presenting the user with a relatively small set of
* predefined choices from which they may choose. The ChoiceBox, when "showing",
* will display to the user these choices and allow them to pick exactly one
* choice. When not showing, the current choice is displayed.
*
* The ChoiceBox can be configured either to support null
as a
* valid choice, or to prohibit it. In the case that it is prohibited, there
* will always be some item that is selected, as long as there is at least
* one item defined. By default, no item is selected unless
* otherwise specified. In the case that null
is acceptable,
* a default entry may be inserted into the list of choices at the top,
* with a name similar to "None" and localized for different Locales.
*
* Although the ChoiceBox will only allow a user to select from the predefined
* list, it is possible for the developer to specify the selected item to be
* something other than what is available in the predefined list. This is
* required for several important use cases.
*
* It means configuration of the ChoiceBox is order independent. You
* may either specify the items and then the selected item, or you may
* specify the selected item and then the items. Either way will function
* correctly.
*
* ChoiceBox item selection is handled by
* {@link javafx.scene.control.SelectionModel SelectionModel}
* As with ListView and ComboBox, it is possible to modify the
* {@link javafx.scene.control.SelectionModel SelectionModel} that is used,
* although this is likely to be rarely changed. ChoiceBox supports only a
* single selection model, hence the default used is a {@link SingleSelectionModel}.
*
*
* import javafx.scene.control.ChoiceBox;
*
* ChoiceBox cb = new ChoiceBox();
* cb.getItems().addAll("item1", "item2", "item3");
*
* @since JavaFX 2.0
*/
@DefaultProperty("items")
public class ChoiceBox extends Control {
/***************************************************************************
* *
* Constructors *
* *
**************************************************************************/
/**
* Create a new ChoiceBox which has an empty list of items.
*/
public ChoiceBox() {
this(FXCollections.observableArrayList());
}
/**
* Create a new ChoiceBox with the given set of items. Since it is observable,
* the content of this list may change over time and the ChoiceBox will
* be updated accordingly.
* @param items
*/
public ChoiceBox(ObservableList items) {
getStyleClass().setAll("choice-box");
setItems(items);
setSelectionModel(new ChoiceBoxSelectionModel(this));
// listen to the value property, if the value is
// set to something that exists in the items list, update the
// selection model to indicate that this is the selected item
valueProperty().addListener(new ChangeListener() {
@Override public void changed(ObservableValue extends T> ov, T t, T t1) {
if (getItems() == null) return;
int index = getItems().indexOf(t1);
if (index > -1) {
getSelectionModel().select(index);
}
}
});
}
/***************************************************************************
* *
* Properties *
* *
**************************************************************************/
/**
* The selection model for the ChoiceBox. Only a single choice can be made,
* hence, the ChoiceBox supports only a SingleSelectionModel. Generally, the
* main interaction with the selection model is to explicitly set which item
* in the items list should be selected, or to listen to changes in the
* selection to know which item has been chosen.
*/
private ObjectProperty> selectionModel =
new SimpleObjectProperty>(this, "selectionModel") {
private SelectionModel oldSM = null;
@Override protected void invalidated() {
if (oldSM != null) {
oldSM.selectedItemProperty().removeListener(selectedItemListener);
}
SelectionModel sm = get();
oldSM = sm;
if (sm != null) {
sm.selectedItemProperty().addListener(selectedItemListener);
}
}
};
private ChangeListener selectedItemListener = new ChangeListener() {
@Override public void changed(ObservableValue extends T> ov, T t, T t1) {
if (! valueProperty().isBound()) {
setValue(t1);
}
}
};
public final void setSelectionModel(SingleSelectionModel value) { selectionModel.set(value); }
public final SingleSelectionModel getSelectionModel() { return selectionModel.get(); }
public final ObjectProperty> selectionModelProperty() { return selectionModel; }
/**
* Indicates whether the drop down is displaying the list of choices to the
* user. This is a readonly property which should be manipulated by means of
* the #show and #hide methods.
*/
private ReadOnlyBooleanWrapper showing = new ReadOnlyBooleanWrapper() {
@Override protected void invalidated() {
pseudoClassStateChanged(SHOWING_PSEUDOCLASS_STATE, get());
}
@Override
public Object getBean() {
return ChoiceBox.this;
}
@Override
public String getName() {
return "showing";
}
};
public final boolean isShowing() { return showing.get(); }
public final ReadOnlyBooleanProperty showingProperty() { return showing.getReadOnlyProperty(); }
/**
* The items to display in the choice box. The selected item (as indicated in the
* selection model) must always be one of these items.
*/
private ObjectProperty> items = new ObjectPropertyBase>() {
ObservableList old;
@Override protected void invalidated() {
final ObservableList newItems = get();
if (old != newItems) {
// Add and remove listeners
if (old != null) old.removeListener(itemsListener);
if (newItems != null) newItems.addListener(itemsListener);
// Clear the selection model
final SingleSelectionModel sm = getSelectionModel();
if (sm != null) {
if (newItems != null && newItems.isEmpty()) {
sm.setSelectedIndex(-1);
} else if (sm.getSelectedIndex() == -1 && sm.getSelectedItem() != null) {
int newIndex = getItems().indexOf(sm.getSelectedItem());
if (newIndex != -1) {
sm.setSelectedIndex(newIndex);
}
} else sm.clearSelection();
}
// if (sm != null) sm.setSelectedIndex(-1);
// Save off the old items
old = newItems;
}
}
@Override
public Object getBean() {
return ChoiceBox.this;
}
@Override
public String getName() {
return "items";
}
};
public final void setItems(ObservableList value) { items.set(value); }
public final ObservableList getItems() { return items.get(); }
public final ObjectProperty> itemsProperty() { return items; }
private final ListChangeListener itemsListener = new ListChangeListener() {
@Override public void onChanged(Change extends T> c) {
final SingleSelectionModel sm = getSelectionModel();
if (sm!= null) {
if (getItems() == null || getItems().isEmpty()) {
sm.clearSelection();
} else {
int newIndex = getItems().indexOf(sm.getSelectedItem());
sm.setSelectedIndex(newIndex);
}
}
if (sm != null) {
// Look for the selected item as having been removed. If it has been,
// then we need to clear the selection in the selection model.
final T selectedItem = sm.getSelectedItem();
while (c.next()) {
if (selectedItem != null && c.getRemoved().contains(selectedItem)) {
sm.clearSelection();
break;
}
}
}
}
};
/**
* Allows a way to specify how to represent objects in the items list. When
* a StringConverter is set, the object toString method is not called and
* instead its toString(object T) is called, passing the objects in the items list.
* This is useful when using domain objects in a ChoiceBox as this property
* allows for customization of the representation. Also, any of the pre-built
* Converters available in the {@link javafx.util.converter} package can be set.
* @since JavaFX 2.1
*/
public ObjectProperty> converterProperty() { return converter; }
private ObjectProperty> converter =
new SimpleObjectProperty>(this, "converter", null);
public final void setConverter(StringConverter value) { converterProperty().set(value); }
public final StringConverter getConverter() {return converterProperty().get(); }
/**
* The value of this ChoiceBox is defined as the selected item in the ChoiceBox
* selection model. The valueProperty is synchronized with the selectedItem.
* This property allows for bi-directional binding of external properties to the
* ChoiceBox and updates the selection model accordingly.
* @since JavaFX 2.1
*/
public ObjectProperty valueProperty() { return value; }
private ObjectProperty value = new SimpleObjectProperty(this, "value") {
@Override protected void invalidated() {
super.invalidated();
fireEvent(new ActionEvent());
// Update selection
final SingleSelectionModel sm = getSelectionModel();
if (sm != null) {
sm.select(super.getValue());
}
}
};
public final void setValue(T value) { valueProperty().set(value); }
public final T getValue() { return valueProperty().get(); }
/***************************************************************************
* *
* Methods *
* *
**************************************************************************/
/**
* Opens the list of choices.
*/
public void show() {
if (!isDisabled()) showing.set(true);
}
/**
* Closes the list of choices.
*/
public void hide() {
showing.set(false);
}
/** {@inheritDoc} */
@Override protected Skin> createDefaultSkin() {
return new ChoiceBoxSkin(this);
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
private static final PseudoClass SHOWING_PSEUDOCLASS_STATE =
PseudoClass.getPseudoClass("showing");
// package for testing
static class ChoiceBoxSelectionModel extends SingleSelectionModel {
private final ChoiceBox choiceBox;
public ChoiceBoxSelectionModel(final ChoiceBox cb) {
if (cb == null) {
throw new NullPointerException("ChoiceBox can not be null");
}
this.choiceBox = cb;
/*
* 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.
*/
// watching for changes to the items list content
final ListChangeListener itemsContentObserver = new ListChangeListener() {
@Override public void onChanged(Change extends T> c) {
if (choiceBox.getItems() == null || choiceBox.getItems().isEmpty()) {
setSelectedIndex(-1);
} else if (getSelectedIndex() == -1 && getSelectedItem() != null) {
int newIndex = choiceBox.getItems().indexOf(getSelectedItem());
if (newIndex != -1) {
setSelectedIndex(newIndex);
}
}
}
};
if (this.choiceBox.getItems() != null) {
this.choiceBox.getItems().addListener(itemsContentObserver);
}
// watching for changes to the items list
ChangeListener> itemsObserver = new ChangeListener>() {
@Override
public void changed(ObservableValue extends ObservableList> valueModel, ObservableList oldList, ObservableList newList) {
if (oldList != null) {
oldList.removeListener(itemsContentObserver);
}
if (newList != null) {
newList.addListener(itemsContentObserver);
}
setSelectedIndex(-1);
if (getSelectedItem() != null) {
int newIndex = choiceBox.getItems().indexOf(getSelectedItem());
if (newIndex != -1) {
setSelectedIndex(newIndex);
}
}
}
};
this.choiceBox.itemsProperty().addListener(itemsObserver);
}
// API Implementation
@Override protected T getModelItem(int index) {
final ObservableList items = choiceBox.getItems();
if (items == null) return null;
if (index < 0 || index >= items.size()) return null;
return items.get(index);
}
@Override protected int getItemCount() {
final ObservableList items = choiceBox.getItems();
return items == null ? 0 : items.size();
}
/**
* Selects the given row. Since the SingleSelectionModel can only support having
* a single row selected at a time, this also causes any previously selected
* row to be unselected.
* This method is overridden here so that we can move past a Separator
* in a ChoiceBox and select the next valid menuitem.
*/
@Override public void select(int index) {
// this does not sound right, we should let the superclass handle it.
final T value = getModelItem(index);
if (value instanceof Separator) {
select(++index);
} else {
super.select(index);
}
if (choiceBox.isShowing()) {
choiceBox.hide();
}
}
}
}