All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javafx.scene.control.ChoiceBox Maven / Gradle / Ivy

There is a newer version: 24-ea+15
Show newest version
/*
 * Copyright (c) 2010, 2022, 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.WeakChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.WeakListChangeListener;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.AccessibleAction;
import javafx.scene.AccessibleAttribute;
import javafx.scene.AccessibleRole;
import javafx.util.StringConverter;
import javafx.css.PseudoClass;

import 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.
 * 

* By default, the ChoiceBox has no item selected unless otherwise specified. * 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. 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}. * *

* Example: *

 ChoiceBox cb = new ChoiceBox();
 * cb.getItems().addAll("item1", "item2", "item3");
* * Image of the ChoiceBox control * * @since JavaFX 2.0 */ @DefaultProperty("items") public class ChoiceBox extends Control { /* ************************************************************************* * * * Static properties and methods * * * **************************************************************************/ /** * Called prior to the ChoiceBox showing its popup after the user * has clicked or otherwise interacted with the ChoiceBox. * @since JavaFX 8u60 */ public static final EventType ON_SHOWING = new EventType<>(Event.ANY, "CHOICE_BOX_ON_SHOWING"); /** * Called after the ChoiceBox has shown its popup. * @since JavaFX 8u60 */ public static final EventType ON_SHOWN = new EventType<>(Event.ANY, "CHOICE_BOX_ON_SHOWN"); /** * Called when the ChoiceBox popup will be hidden. * @since JavaFX 8u60 */ public static final EventType ON_HIDING = new EventType<>(Event.ANY, "CHOICE_BOX_ON_HIDING"); /** * Called when the ChoiceBox popup has been hidden. * @since JavaFX 8u60 */ public static final EventType ON_HIDDEN = new EventType<>(Event.ANY, "CHOICE_BOX_ON_HIDDEN"); /* ************************************************************************* * * * 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 the set of items */ public ChoiceBox(ObservableList items) { getStyleClass().setAll("choice-box"); setAccessibleRole(AccessibleRole.COMBO_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((ov, t, t1) -> { if (getItems() == null) return; SingleSelectionModel sm = getSelectionModel(); if (sm == null) return; int index = getItems().indexOf(t1); if (index > -1) { sm.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); if (!valueProperty().isBound()) { ChoiceBox.this.setValue(sm.getSelectedItem()); } } } }; private ChangeListener selectedItemListener = (ov, 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()); notifyAccessibleAttributeChanged(AccessibleAttribute.EXPANDED); } @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(); } private void setShowing(boolean value) { // these events will not fire if the showing property is bound Event.fireEvent(this, value ? new Event(ON_SHOWING) : new Event(ON_HIDING)); showing.set(value); Event.fireEvent(this, value ? new Event(ON_SHOWN) : new Event(ON_HIDDEN)); } /** * 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()) { // RT-29433 - clear selection. sm.clearSelection(); } 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 = 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. * @return the string converter property * @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. * @return the value property * @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()); } notifyAccessibleAttributeChanged(AccessibleAttribute.TEXT); } }; public final void setValue(T value) { valueProperty().set(value); } public final T getValue() { return valueProperty().get(); } // --- On Action /** * The ChoiceBox action, which is invoked whenever the ChoiceBox * {@link #valueProperty() value} property is changed. This * may be due to the value property being programmatically changed or when the * user selects an item in a popup menu. * * @return the on action property * @since JavaFX 8u60 */ public final ObjectProperty> onActionProperty() { return onAction; } public final void setOnAction(EventHandler value) { onActionProperty().set(value); } public final EventHandler getOnAction() { return onActionProperty().get(); } private ObjectProperty> onAction = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(ActionEvent.ACTION, get()); } @Override public Object getBean() { return ChoiceBox.this; } @Override public String getName() { return "onAction"; } }; // --- On Showing /** * Called just prior to the {@code ChoiceBox} popup being shown. * @return the on showing property * @since JavaFX 8u60 */ public final ObjectProperty> onShowingProperty() { return onShowing; } public final void setOnShowing(EventHandler value) { onShowingProperty().set(value); } public final EventHandler getOnShowing() { return onShowingProperty().get(); } private ObjectProperty> onShowing = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(ON_SHOWING, get()); } @Override public Object getBean() { return ChoiceBox.this; } @Override public String getName() { return "onShowing"; } }; // -- On Shown /** * Called just after the {@code ChoiceBox} popup is shown. * @return the on shown property * @since JavaFX 8u60 */ public final ObjectProperty> onShownProperty() { return onShown; } public final void setOnShown(EventHandler value) { onShownProperty().set(value); } public final EventHandler getOnShown() { return onShownProperty().get(); } private ObjectProperty> onShown = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(ON_SHOWN, get()); } @Override public Object getBean() { return ChoiceBox.this; } @Override public String getName() { return "onShown"; } }; // --- On Hiding /** * Called just prior to the {@code ChoiceBox} popup being hidden. * @return the on hiding property * @since JavaFX 8u60 */ public final ObjectProperty> onHidingProperty() { return onHiding; } public final void setOnHiding(EventHandler value) { onHidingProperty().set(value); } public final EventHandler getOnHiding() { return onHidingProperty().get(); } private ObjectProperty> onHiding = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(ON_HIDING, get()); } @Override public Object getBean() { return ChoiceBox.this; } @Override public String getName() { return "onHiding"; } }; // --- On Hidden /** * Called just after the {@code ChoiceBox} popup has been hidden. * @return the on hidden property * @since JavaFX 8u60 */ public final ObjectProperty> onHiddenProperty() { return onHidden; } public final void setOnHidden(EventHandler value) { onHiddenProperty().set(value); } public final EventHandler getOnHidden() { return onHiddenProperty().get(); } private ObjectProperty> onHidden = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(ON_HIDDEN, get()); } @Override public Object getBean() { return ChoiceBox.this; } @Override public String getName() { return "onHidden"; } }; /* ************************************************************************* * * * Methods * * * **************************************************************************/ /** * Opens the list of choices. */ public void show() { if (!isDisabled()) setShowing(true); } /** * Closes the list of choices. */ public void hide() { setShowing(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; private ChangeListener> itemsObserver; private ListChangeListener itemsContentObserver; private WeakListChangeListener weakItemsContentObserver; 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 itemsContentObserver = 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); } } }; weakItemsContentObserver = new WeakListChangeListener<>(itemsContentObserver); if (this.choiceBox.getItems() != null) { this.choiceBox.getItems().addListener(weakItemsContentObserver); } // watching for changes to the items list itemsObserver = (valueModel, oldList, newList) -> { if (oldList != null) { oldList.removeListener(weakItemsContentObserver); } if (newList != null) { newList.addListener(weakItemsContentObserver); } setSelectedIndex(-1); if (getSelectedItem() != null) { int newIndex = choiceBox.getItems().indexOf(getSelectedItem()); if (newIndex != -1) { setSelectedIndex(newIndex); } } }; // TBD: use pattern as f.i. in listView selectionModel (invalidationListener to really // get all changes - including list of same content - of the list-valued property) this.choiceBox.itemsProperty().addListener(new WeakChangeListener<>(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. super.select(index); if (choiceBox.isShowing()) { choiceBox.hide(); } } /** * {@inheritDoc}

* * Overridden to clear selectedIndex if selectedItem is not contained * in the items. */ @Override public void select(T obj) { super.select(obj); if (obj != null && !choiceBox.getItems().contains(obj)) { setSelectedIndex(-1); } } /** {@inheritDoc} */ @Override public void selectPrevious() { // overridden to properly handle Separators int index = getSelectedIndex() - 1; while (index >= 0) { final T value = getModelItem(index); if (value instanceof Separator) { index--; } else { select(index); break; } } } /** {@inheritDoc} */ @Override public void selectNext() { // overridden to properly handle Separators int index = getSelectedIndex() + 1; while (index < getItemCount()) { final T value = getModelItem(index); if (value instanceof Separator) { index++; } else { select(index); break; } } } } /* ************************************************************************* * * * Accessibility handling * * * **************************************************************************/ /** {@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 converter = getConverter(); if (converter == null) { return getValue() != null ? getValue().toString() : ""; } return converter.toString(getValue()); case EXPANDED: return isShowing(); default: return super.queryAccessibleAttribute(attribute, parameters); } } /** {@inheritDoc} */ @Override public void executeAccessibleAction(AccessibleAction action, Object... parameters) { switch (action) { case COLLAPSE: hide(); break; case EXPAND: show(); break; default: super.executeAccessibleAction(action); break; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy