modules/controls/src/main/java/javafx/scene/control/skin/ComboBoxListViewSkin.java
Print this page
rev 9240 : 8076423: JEP 253: Prepare JavaFX UI Controls & CSS APIs for Modularization
@@ -21,19 +21,22 @@
* 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 com.sun.javafx.scene.control.skin;
+package javafx.scene.control.skin;
+import com.sun.javafx.scene.control.behavior.BehaviorBase;
+import com.sun.javafx.scene.control.behavior.ComboBoxBaseBehavior;
import com.sun.javafx.scene.control.behavior.ComboBoxListViewBehavior;
import java.util.List;
import javafx.beans.InvalidationListener;
-import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.WeakListChangeListener;
import javafx.css.PseudoClass;
@@ -41,23 +44,37 @@
import javafx.event.EventTarget;
import javafx.scene.AccessibleAttribute;
import javafx.scene.AccessibleRole;
import javafx.scene.Node;
import javafx.scene.Parent;
+import javafx.scene.control.Accordion;
+import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
-import javafx.scene.control.ComboBoxBase;
+import javafx.scene.control.Control;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SelectionModel;
import javafx.scene.control.TextField;
import javafx.scene.input.*;
import javafx.util.Callback;
import javafx.util.StringConverter;
+/**
+ * Default skin implementation for the {@link ComboBox} control.
+ *
+ * @see ComboBox
+ * @since 9
+ */
public class ComboBoxListViewSkin<T> extends ComboBoxPopupControl<T> {
+ /***************************************************************************
+ * *
+ * Static fields *
+ * *
+ **************************************************************************/
+
// By default we measure the width of all cells in the ListView. If this
// is too burdensome, the developer may set a property in the ComboBox
// properties map with this key to specify the number of rows to measure.
// This may one day become a property on the ComboBox itself.
private static final String COMBO_BOX_ROWS_TO_MEASURE_WIDTH_KEY = "comboBoxRowsToMeasureWidth";
@@ -80,10 +97,13 @@
private ObservableList<T> listViewItems;
private boolean listSelectionLock = false;
private boolean listViewSelectionDirty = false;
+ private final ComboBoxListViewBehavior behavior;
+
+
/***************************************************************************
* *
* Listeners *
* *
@@ -107,20 +127,32 @@
* *
* Constructors *
* *
**************************************************************************/
- public ComboBoxListViewSkin(final ComboBox<T> comboBox) {
- super(comboBox, new ComboBoxListViewBehavior<T>(comboBox));
- this.comboBox = comboBox;
+ /**
+ * Creates a new ComboBoxListViewSkin instance, installing the necessary child
+ * nodes into the Control {@link Control#getChildren() children} list, as
+ * well as the necessary input mappings for handling key, mouse, etc events.
+ *
+ * @param control The control that this skin should be installed onto.
+ */
+ public ComboBoxListViewSkin(final ComboBox<T> control) {
+ super(control);
+
+ // install default input map for the control
+ this.behavior = new ComboBoxListViewBehavior<>(control);
+// control.setInputMap(behavior.getInputMap());
+
+ this.comboBox = control;
updateComboBoxItems();
itemsObserver = observable -> {
updateComboBoxItems();
updateListViewItems();
};
- this.comboBox.itemsProperty().addListener(new WeakInvalidationListener(itemsObserver));
+ control.itemsProperty().addListener(new WeakInvalidationListener(itemsObserver));
// listview for popup
this.listView = createListView();
// Fix for RT-21207. Additional code related to this bug is further below.
@@ -134,18 +166,53 @@
updateButtonCell();
// Fix for RT-19431 (also tested via ComboBoxListViewSkinTest)
updateValue();
- registerChangeListener(comboBox.itemsProperty(), "ITEMS");
- registerChangeListener(comboBox.promptTextProperty(), "PROMPT_TEXT");
- registerChangeListener(comboBox.cellFactoryProperty(), "CELL_FACTORY");
- registerChangeListener(comboBox.visibleRowCountProperty(), "VISIBLE_ROW_COUNT");
- registerChangeListener(comboBox.converterProperty(), "CONVERTER");
- registerChangeListener(comboBox.buttonCellProperty(), "BUTTON_CELL");
- registerChangeListener(comboBox.valueProperty(), "VALUE");
- registerChangeListener(comboBox.editableProperty(), "EDITABLE");
+ registerChangeListener(control.itemsProperty(), e -> {
+ updateComboBoxItems();
+ updateListViewItems();
+ });
+ registerChangeListener(control.promptTextProperty(), e -> updateDisplayNode());
+ registerChangeListener(control.cellFactoryProperty(), e -> updateCellFactory());
+ registerChangeListener(control.visibleRowCountProperty(), e -> {
+ if (listView == null) return;
+ listView.requestLayout();
+ });
+ registerChangeListener(control.converterProperty(), e -> updateListViewItems());
+ registerChangeListener(control.buttonCellProperty(), e -> updateButtonCell());
+ registerChangeListener(control.valueProperty(), e -> {
+ updateValue();
+ control.fireEvent(new ActionEvent());
+ });
+ registerChangeListener(control.editableProperty(), e -> updateEditable());
+ }
+
+
+
+ /***************************************************************************
+ * *
+ * Properties *
+ * *
+ **************************************************************************/
+
+ /**
+ * By default this skin hides the popup whenever the ListView is clicked in.
+ * By setting hideOnClick to false, the popup will not be hidden when the
+ * ListView is clicked in. This is beneficial in some scenarios (for example,
+ * when the ListView cells have checkboxes).
+ */
+ // --- hide on click
+ private final BooleanProperty hideOnClick = new SimpleBooleanProperty(this, "hideOnClick", true);
+ public final BooleanProperty hideOnClickProperty() {
+ return hideOnClick;
+ }
+ public final boolean isHideOnClick() {
+ return hideOnClick.get();
+ }
+ public final void setHideOnClick(boolean value) {
+ hideOnClick.set(value);
}
/***************************************************************************
@@ -153,49 +220,31 @@
* Public API *
* *
**************************************************************************/
/** {@inheritDoc} */
- @Override protected void handleControlPropertyChanged(String p) {
- super.handleControlPropertyChanged(p);
+ @Override public void dispose() {
+ super.dispose();
- if ("ITEMS".equals(p)) {
- updateComboBoxItems();
- updateListViewItems();
- } else if ("PROMPT_TEXT".equals(p)) {
- updateDisplayNode();
- } else if ("CELL_FACTORY".equals(p)) {
- updateCellFactory();
- } else if ("VISIBLE_ROW_COUNT".equals(p)) {
- if (listView == null) return;
- listView.requestLayout();
- } else if ("CONVERTER".equals(p)) {
- updateListViewItems();
- } else if ("EDITOR".equals(p)) {
- getEditableInputNode();
- } else if ("BUTTON_CELL".equals(p)) {
- updateButtonCell();
- } else if ("VALUE".equals(p)) {
- updateValue();
- comboBox.fireEvent(new ActionEvent());
- } else if ("EDITABLE".equals(p)) {
- updateEditable();
+ if (behavior != null) {
+ behavior.dispose();
}
}
+ /** {@inheritDoc} */
@Override protected TextField getEditor() {
// Return null if editable is false, even if the ComboBox has an editor set.
// Use getSkinnable() here because this method is called from the super
// constructor before comboBox is initialized.
return getSkinnable().isEditable() ? ((ComboBox)getSkinnable()).getEditor() : null;
}
+ /** {@inheritDoc} */
@Override protected StringConverter<T> getConverter() {
return ((ComboBox)getSkinnable()).getConverter();
}
-
/** {@inheritDoc} */
@Override public Node getDisplayNode() {
Node displayNode;
if (comboBox.isEditable()) {
displayNode = getEditableInputNode();
@@ -206,70 +255,57 @@
updateDisplayNode();
return displayNode;
}
- public void updateComboBoxItems() {
- comboBoxItems = comboBox.getItems();
- comboBoxItems = comboBoxItems == null ? FXCollections.<T>emptyObservableList() : comboBoxItems;
- }
-
- public void updateListViewItems() {
- if (listViewItems != null) {
- listViewItems.removeListener(weakListViewItemsListener);
- }
-
- this.listViewItems = comboBoxItems;
- listView.setItems(listViewItems);
-
- if (listViewItems != null) {
- listViewItems.addListener(weakListViewItemsListener);
- }
-
- itemCountDirty = true;
- getSkinnable().requestLayout();
- }
-
+ /** {@inheritDoc} */
@Override public Node getPopupContent() {
return listView;
}
+ /** {@inheritDoc} */
@Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
reconfigurePopup();
return 50;
}
+ /** {@inheritDoc} */
@Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
double superPrefWidth = super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset);
double listViewWidth = listView.prefWidth(height);
double pw = Math.max(superPrefWidth, listViewWidth);
reconfigurePopup();
return pw;
}
+ /** {@inheritDoc} */
@Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
reconfigurePopup();
return super.computeMaxWidth(height, topInset, rightInset, bottomInset, leftInset);
}
+ /** {@inheritDoc} */
@Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
reconfigurePopup();
return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
}
+ /** {@inheritDoc} */
@Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
reconfigurePopup();
return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
}
+ /** {@inheritDoc} */
@Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
reconfigurePopup();
return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
}
+ /** {@inheritDoc} */
@Override protected void layoutChildren(final double x, final double y,
final double w, final double h) {
if (listViewSelectionDirty) {
try {
listSelectionLock = true;
@@ -280,27 +316,75 @@
listSelectionLock = false;
listViewSelectionDirty = false;
}
}
- super.layoutChildren(x,y,w,h);
- }
-
- // Added to allow subclasses to prevent the popup from hiding when the
- // ListView is clicked on (e.g when the list cells have checkboxes).
- protected boolean isHideOnClickEnabled() {
- return true;
+ super.layoutChildren(x, y, w, h);
}
/***************************************************************************
* *
* Private methods *
* *
**************************************************************************/
+ /** {@inheritDoc} */
+ @Override void updateDisplayNode() {
+ if (getEditor() != null) {
+ super.updateDisplayNode();
+ } else {
+ T value = comboBox.getValue();
+ int index = getIndexOfComboBoxValueInItemsList();
+ if (index > -1) {
+ buttonCell.setItem(null);
+ buttonCell.updateIndex(index);
+ } else {
+ // RT-21336 Show the ComboBox value even though it doesn't
+ // exist in the ComboBox items list (part two of fix)
+ buttonCell.updateIndex(-1);
+ boolean empty = updateDisplayText(buttonCell, value, false);
+
+ // Note that empty boolean collected above. This is used to resolve
+ // RT-27834, where we were getting different styling based on whether
+ // the cell was updated via the updateIndex method above, or just
+ // by directly updating the text. We fake the pseudoclass state
+ // for empty, filled, and selected here.
+ buttonCell.pseudoClassStateChanged(PSEUDO_CLASS_EMPTY, empty);
+ buttonCell.pseudoClassStateChanged(PSEUDO_CLASS_FILLED, !empty);
+ buttonCell.pseudoClassStateChanged(PSEUDO_CLASS_SELECTED, true);
+ }
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override ComboBoxBaseBehavior getBehavior() {
+ return behavior;
+ }
+
+ private void updateComboBoxItems() {
+ comboBoxItems = comboBox.getItems();
+ comboBoxItems = comboBoxItems == null ? FXCollections.<T>emptyObservableList() : comboBoxItems;
+ }
+
+ private void updateListViewItems() {
+ if (listViewItems != null) {
+ listViewItems.removeListener(weakListViewItemsListener);
+ }
+
+ this.listViewItems = comboBoxItems;
+ listView.setItems(listViewItems);
+
+ if (listViewItems != null) {
+ listViewItems.addListener(weakListViewItemsListener);
+ }
+
+ itemCountDirty = true;
+ getSkinnable().requestLayout();
+ }
+
private void updateValue() {
T newValue = comboBox.getValue();
SelectionModel<T> listViewSM = listView.getSelectionModel();
@@ -337,38 +421,10 @@
}
}
}
}
-
- @Override protected void updateDisplayNode() {
- if (getEditor() != null) {
- super.updateDisplayNode();
- } else {
- T value = comboBox.getValue();
- int index = getIndexOfComboBoxValueInItemsList();
- if (index > -1) {
- buttonCell.setItem(null);
- buttonCell.updateIndex(index);
- } else {
- // RT-21336 Show the ComboBox value even though it doesn't
- // exist in the ComboBox items list (part two of fix)
- buttonCell.updateIndex(-1);
- boolean empty = updateDisplayText(buttonCell, value, false);
-
- // Note that empty boolean collected above. This is used to resolve
- // RT-27834, where we were getting different styling based on whether
- // the cell was updated via the updateIndex method above, or just
- // by directly updating the text. We fake the pseudoclass state
- // for empty, filled, and selected here.
- buttonCell.pseudoClassStateChanged(PSEUDO_CLASS_EMPTY, empty);
- buttonCell.pseudoClassStateChanged(PSEUDO_CLASS_FILLED, !empty);
- buttonCell.pseudoClassStateChanged(PSEUDO_CLASS_SELECTED, true);
- }
- }
- }
-
// return a boolean to indicate that the cell is empty (and therefore not filled)
private boolean updateDisplayText(ListCell<T> cell, T item, boolean empty) {
if (empty) {
if (cell == null) return true;
cell.setGraphic(null);
@@ -503,11 +559,11 @@
|| s.contains("increment-arrow")) {
return;
}
}
- if (isHideOnClickEnabled()) {
+ if (isHideOnClick()) {
comboBox.hide();
}
});
_listView.setOnKeyPressed(t -> {
@@ -524,11 +580,11 @@
private double getListViewPrefHeight() {
double ph;
if (listView.getSkin() instanceof VirtualContainerBase) {
int maxRows = comboBox.getVisibleRowCount();
- VirtualContainerBase<?,?,?> skin = (VirtualContainerBase<?,?,?>)listView.getSkin();
+ VirtualContainerBase<?,?> skin = (VirtualContainerBase<?,?>)listView.getSkin();
ph = skin.getVirtualFlowPreferredHeight(maxRows);
} else {
double ch = comboBoxItems.size() * 25;
ph = Math.min(ch, 200);
}
@@ -542,11 +598,11 @@
*
* API for testing
*
*************************************************************************/
- public ListView<T> getListView() {
+ ListView<T> getListView() {
return listView;
}
@@ -564,13 +620,12 @@
PseudoClass.getPseudoClass("empty");
private static final PseudoClass PSEUDO_CLASS_FILLED =
PseudoClass.getPseudoClass("filled");
-
- @Override
- public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
+ /** {@inheritDoc} */
+ @Override public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
switch (attribute) {
case FOCUS_ITEM: {
if (comboBox.isShowing()) {
/* On Mac, for some reason, changing the selection on the list is not
* reported by VoiceOver the first time it shows.