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.