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


   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.javafx.scene.control.skin;
  27 


  28 import com.sun.javafx.scene.control.behavior.ComboBoxListViewBehavior;
  29 
  30 import java.util.List;
  31 
  32 import javafx.beans.InvalidationListener;
  33 import javafx.beans.Observable;
  34 import javafx.beans.WeakInvalidationListener;


  35 import javafx.collections.FXCollections;
  36 import javafx.collections.ListChangeListener;
  37 import javafx.collections.ObservableList;
  38 import javafx.collections.WeakListChangeListener;
  39 import javafx.css.PseudoClass;
  40 import javafx.event.ActionEvent;
  41 import javafx.event.EventTarget;
  42 import javafx.scene.AccessibleAttribute;
  43 import javafx.scene.AccessibleRole;
  44 import javafx.scene.Node;
  45 import javafx.scene.Parent;


  46 import javafx.scene.control.ComboBox;
  47 import javafx.scene.control.ComboBoxBase;
  48 import javafx.scene.control.ListCell;
  49 import javafx.scene.control.ListView;
  50 import javafx.scene.control.SelectionMode;
  51 import javafx.scene.control.SelectionModel;
  52 import javafx.scene.control.TextField;
  53 import javafx.scene.input.*;
  54 import javafx.util.Callback;
  55 import javafx.util.StringConverter;
  56 






  57 public class ComboBoxListViewSkin<T> extends ComboBoxPopupControl<T> {
  58     






  59     // By default we measure the width of all cells in the ListView. If this
  60     // is too burdensome, the developer may set a property in the ComboBox
  61     // properties map with this key to specify the number of rows to measure.
  62     // This may one day become a property on the ComboBox itself.
  63     private static final String COMBO_BOX_ROWS_TO_MEASURE_WIDTH_KEY = "comboBoxRowsToMeasureWidth";
  64 
  65     
  66     
  67     /***************************************************************************
  68      *                                                                         *
  69      * Private fields                                                          *
  70      *                                                                         *
  71      **************************************************************************/    
  72     
  73     private final ComboBox<T> comboBox;
  74     private ObservableList<T> comboBoxItems;
  75     
  76     private ListCell<T> buttonCell;
  77     private Callback<ListView<T>, ListCell<T>> cellFactory;
  78     
  79     private final ListView<T> listView;
  80     private ObservableList<T> listViewItems;
  81     
  82     private boolean listSelectionLock = false;
  83     private boolean listViewSelectionDirty = false;
  84 



  85     
  86     /***************************************************************************
  87      *                                                                         *
  88      * Listeners                                                               *
  89      *                                                                         *
  90      **************************************************************************/
  91     
  92     private boolean itemCountDirty;
  93     private final ListChangeListener<T> listViewItemsListener = new ListChangeListener<T>() {
  94         @Override public void onChanged(ListChangeListener.Change<? extends T> c) {
  95             itemCountDirty = true;
  96             getSkinnable().requestLayout();
  97         }
  98     };
  99 
 100     private final InvalidationListener itemsObserver;
 101     
 102     private final WeakListChangeListener<T> weakListViewItemsListener =
 103             new WeakListChangeListener<T>(listViewItemsListener);
 104 
 105     
 106     /***************************************************************************
 107      *                                                                         *
 108      * Constructors                                                            *
 109      *                                                                         *
 110      **************************************************************************/   
 111     
 112     public ComboBoxListViewSkin(final ComboBox<T> comboBox) {
 113         super(comboBox, new ComboBoxListViewBehavior<T>(comboBox));
 114         this.comboBox = comboBox;












 115         updateComboBoxItems();
 116 
 117         itemsObserver = observable -> {
 118             updateComboBoxItems();
 119             updateListViewItems();
 120         };
 121         this.comboBox.itemsProperty().addListener(new WeakInvalidationListener(itemsObserver));
 122         
 123         // listview for popup
 124         this.listView = createListView();
 125         
 126         // Fix for RT-21207. Additional code related to this bug is further below.
 127         this.listView.setManaged(false);
 128         getChildren().add(listView);
 129         // -- end of fix
 130                 
 131         updateListViewItems();
 132         updateCellFactory();
 133         
 134         updateButtonCell();
 135 
 136         // Fix for RT-19431 (also tested via ComboBoxListViewSkinTest)
 137         updateValue();
 138 
 139         registerChangeListener(comboBox.itemsProperty(), "ITEMS");
 140         registerChangeListener(comboBox.promptTextProperty(), "PROMPT_TEXT");
 141         registerChangeListener(comboBox.cellFactoryProperty(), "CELL_FACTORY");
 142         registerChangeListener(comboBox.visibleRowCountProperty(), "VISIBLE_ROW_COUNT");
 143         registerChangeListener(comboBox.converterProperty(), "CONVERTER");
 144         registerChangeListener(comboBox.buttonCellProperty(), "BUTTON_CELL");
 145         registerChangeListener(comboBox.valueProperty(), "VALUE");
 146         registerChangeListener(comboBox.editableProperty(), "EDITABLE");



































 147     }
 148 
 149 
 150 
 151     /***************************************************************************
 152      *                                                                         *
 153      * Public API                                                              *
 154      *                                                                         *
 155      **************************************************************************/  
 156     
 157     /** {@inheritDoc} */
 158     @Override protected void handleControlPropertyChanged(String p) {
 159         super.handleControlPropertyChanged(p);
 160         
 161         if ("ITEMS".equals(p)) {
 162             updateComboBoxItems();
 163             updateListViewItems();
 164         } else if ("PROMPT_TEXT".equals(p)) {
 165             updateDisplayNode();
 166         } else if ("CELL_FACTORY".equals(p)) {
 167             updateCellFactory();
 168         } else if ("VISIBLE_ROW_COUNT".equals(p)) {
 169             if (listView == null) return;
 170             listView.requestLayout();
 171         } else if ("CONVERTER".equals(p)) {
 172             updateListViewItems();
 173         } else if ("EDITOR".equals(p)) {
 174             getEditableInputNode();
 175         } else if ("BUTTON_CELL".equals(p)) {
 176             updateButtonCell();
 177         } else if ("VALUE".equals(p)) {
 178             updateValue();
 179             comboBox.fireEvent(new ActionEvent());
 180         } else if ("EDITABLE".equals(p)) {
 181             updateEditable();
 182         }
 183     }
 184 

 185     @Override protected TextField getEditor() {
 186         // Return null if editable is false, even if the ComboBox has an editor set.
 187         // Use getSkinnable() here because this method is called from the super
 188         // constructor before comboBox is initialized.
 189         return getSkinnable().isEditable() ? ((ComboBox)getSkinnable()).getEditor() : null;
 190     }
 191 

 192     @Override protected StringConverter<T> getConverter() {
 193         return ((ComboBox)getSkinnable()).getConverter();
 194     }
 195 
 196     
 197     /** {@inheritDoc} */
 198     @Override public Node getDisplayNode() {
 199         Node displayNode;
 200         if (comboBox.isEditable()) {
 201             displayNode = getEditableInputNode();
 202         } else {
 203             displayNode = buttonCell;
 204         }
 205         
 206         updateDisplayNode();
 207         
 208         return displayNode;
 209     }
 210     
 211     public void updateComboBoxItems() {
 212         comboBoxItems = comboBox.getItems();
 213         comboBoxItems = comboBoxItems == null ? FXCollections.<T>emptyObservableList() : comboBoxItems;
 214     }
 215     
 216     public void updateListViewItems() {
 217         if (listViewItems != null) {
 218             listViewItems.removeListener(weakListViewItemsListener);
 219         }
 220 
 221         this.listViewItems = comboBoxItems;
 222         listView.setItems(listViewItems);
 223 
 224         if (listViewItems != null) {
 225             listViewItems.addListener(weakListViewItemsListener);
 226         }
 227         
 228         itemCountDirty = true;
 229         getSkinnable().requestLayout();
 230     }
 231     
 232     @Override public Node getPopupContent() {
 233         return listView;
 234     }
 235     

 236     @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 237         reconfigurePopup();
 238         return 50;
 239     }
 240 

 241     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 242         double superPrefWidth = super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset);
 243         double listViewWidth = listView.prefWidth(height);
 244         double pw = Math.max(superPrefWidth, listViewWidth);
 245 
 246         reconfigurePopup();
 247 
 248         return pw;
 249     }
 250 

 251     @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 252         reconfigurePopup();
 253         return super.computeMaxWidth(height, topInset, rightInset, bottomInset, leftInset);
 254     }
 255 

 256     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 257         reconfigurePopup();
 258         return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
 259     }
 260 

 261     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 262         reconfigurePopup();
 263         return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
 264     }
 265 

 266     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 267         reconfigurePopup();
 268         return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
 269     }
 270 

 271     @Override protected void layoutChildren(final double x, final double y,
 272             final double w, final double h) {
 273         if (listViewSelectionDirty) {
 274             try {
 275                 listSelectionLock = true;
 276                 T item = comboBox.getSelectionModel().getSelectedItem();
 277                 listView.getSelectionModel().clearSelection();
 278                 listView.getSelectionModel().select(item);
 279             } finally {
 280                 listSelectionLock = false;
 281                 listViewSelectionDirty = false;
 282             }
 283         }
 284         
 285         super.layoutChildren(x,y,w,h);
 286     }
 287 
 288     // Added to allow subclasses to prevent the popup from hiding when the
 289     // ListView is clicked on (e.g when the list cells have checkboxes).
 290     protected boolean isHideOnClickEnabled() {
 291         return true;
 292     }
 293 
 294     
 295     
 296     /***************************************************************************
 297      *                                                                         *
 298      * Private methods                                                         *
 299      *                                                                         *
 300      **************************************************************************/
 301 






















































 302     private void updateValue() {
 303         T newValue = comboBox.getValue();
 304         
 305         SelectionModel<T> listViewSM = listView.getSelectionModel();
 306         
 307         if (newValue == null) {
 308             listViewSM.clearSelection();
 309         } else {
 310             // RT-22386: We need to test to see if the value is in the comboBox
 311             // items list. If it isn't, then we should clear the listview 
 312             // selection
 313             int indexOfNewValue = getIndexOfComboBoxValueInItemsList();
 314             if (indexOfNewValue == -1) {
 315                 listSelectionLock = true;
 316                 listViewSM.clearSelection();
 317                 listSelectionLock = false;
 318             } else {
 319                 int index = comboBox.getSelectionModel().getSelectedIndex();
 320                 if (index >= 0 && index < comboBoxItems.size()) {
 321                     T itemsObj = comboBoxItems.get(index);
 322                     if (itemsObj != null && itemsObj.equals(newValue)) {
 323                         listViewSM.select(index);
 324                     } else {
 325                         listViewSM.select(newValue);
 326                     }
 327                 } else {
 328                     // just select the first instance of newValue in the list
 329                     int listViewIndex = comboBoxItems.indexOf(newValue);
 330                     if (listViewIndex == -1) {
 331                         // RT-21336 Show the ComboBox value even though it doesn't
 332                         // exist in the ComboBox items list (part one of fix)
 333                         updateDisplayNode();
 334                     } else {
 335                         listViewSM.select(listViewIndex);
 336                     }
 337                 }
 338             }
 339         }
 340     }
 341     
 342     
 343     @Override protected void updateDisplayNode() {
 344         if (getEditor() != null) {
 345             super.updateDisplayNode();
 346         } else {
 347             T value = comboBox.getValue();
 348             int index = getIndexOfComboBoxValueInItemsList();
 349             if (index > -1) {
 350                 buttonCell.setItem(null);
 351                 buttonCell.updateIndex(index);
 352             } else {
 353                 // RT-21336 Show the ComboBox value even though it doesn't
 354                 // exist in the ComboBox items list (part two of fix)
 355                 buttonCell.updateIndex(-1);
 356                 boolean empty = updateDisplayText(buttonCell, value, false);
 357                 
 358                 // Note that empty boolean collected above. This is used to resolve
 359                 // RT-27834, where we were getting different styling based on whether
 360                 // the cell was updated via the updateIndex method above, or just
 361                 // by directly updating the text. We fake the pseudoclass state
 362                 // for empty, filled, and selected here.
 363                 buttonCell.pseudoClassStateChanged(PSEUDO_CLASS_EMPTY,    empty);
 364                 buttonCell.pseudoClassStateChanged(PSEUDO_CLASS_FILLED,   !empty);
 365                 buttonCell.pseudoClassStateChanged(PSEUDO_CLASS_SELECTED, true);
 366             }
 367         }
 368     }
 369     
 370     // return a boolean to indicate that the cell is empty (and therefore not filled)
 371     private boolean updateDisplayText(ListCell<T> cell, T item, boolean empty) {
 372         if (empty) {
 373             if (cell == null) return true;
 374             cell.setGraphic(null);
 375             cell.setText(null);
 376             return true;
 377         } else if (item instanceof Node) {
 378             Node currentNode = cell.getGraphic();
 379             Node newNode = (Node) item;
 380             if (currentNode == null || ! currentNode.equals(newNode)) {
 381                 cell.setText(null);
 382                 cell.setGraphic(newNode);
 383             }
 384             return newNode == null;
 385         } else {
 386             // run item through StringConverter if it isn't null
 387             StringConverter<T> c = comboBox.getConverter();
 388             String s = item == null ? comboBox.getPromptText() : (c == null ? item.toString() : c.toString(item));
 389             cell.setText(s);


 488          
 489         comboBox.getSelectionModel().selectedItemProperty().addListener(o -> {
 490             listViewSelectionDirty = true;
 491         });
 492         
 493         _listView.addEventFilter(MouseEvent.MOUSE_RELEASED, t -> {
 494             // RT-18672: Without checking if the user is clicking in the
 495             // scrollbar area of the ListView, the comboBox will hide. Therefore,
 496             // we add the check below to prevent this from happening.
 497             EventTarget target = t.getTarget();
 498             if (target instanceof Parent) {
 499                 List<String> s = ((Parent) target).getStyleClass();
 500                 if (s.contains("thumb")
 501                         || s.contains("track")
 502                         || s.contains("decrement-arrow")
 503                         || s.contains("increment-arrow")) {
 504                     return;
 505                 }
 506             }
 507 
 508             if (isHideOnClickEnabled()) {
 509                 comboBox.hide();
 510             }
 511         });
 512 
 513         _listView.setOnKeyPressed(t -> {
 514             // TODO move to behavior, when (or if) this class becomes a SkinBase
 515             if (t.getCode() == KeyCode.ENTER ||
 516                     t.getCode() == KeyCode.SPACE ||
 517                     t.getCode() == KeyCode.ESCAPE) {
 518                 comboBox.hide();
 519             }
 520         });
 521         
 522         return _listView;
 523     }
 524     
 525     private double getListViewPrefHeight() {
 526         double ph;
 527         if (listView.getSkin() instanceof VirtualContainerBase) {
 528             int maxRows = comboBox.getVisibleRowCount();
 529             VirtualContainerBase<?,?,?> skin = (VirtualContainerBase<?,?,?>)listView.getSkin();
 530             ph = skin.getVirtualFlowPreferredHeight(maxRows);
 531         } else {
 532             double ch = comboBoxItems.size() * 25;
 533             ph = Math.min(ch, 200);
 534         }
 535         
 536         return ph;
 537     }
 538 
 539 
 540     
 541     /**************************************************************************
 542      * 
 543      * API for testing
 544      * 
 545      *************************************************************************/
 546     
 547     public ListView<T> getListView() {
 548         return listView;
 549     }
 550     
 551     
 552     
 553 
 554     /***************************************************************************
 555      *                                                                         *
 556      * Stylesheet Handling                                                     *
 557      *                                                                         *
 558      **************************************************************************/
 559     
 560     // These three pseudo class states are duplicated from Cell
 561     private static final PseudoClass PSEUDO_CLASS_SELECTED =
 562             PseudoClass.getPseudoClass("selected");
 563     private static final PseudoClass PSEUDO_CLASS_EMPTY =
 564             PseudoClass.getPseudoClass("empty");
 565     private static final PseudoClass PSEUDO_CLASS_FILLED =
 566             PseudoClass.getPseudoClass("filled");
 567 
 568 
 569 
 570     @Override
 571     public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 572         switch (attribute) {
 573             case FOCUS_ITEM: {
 574                 if (comboBox.isShowing()) {
 575                     /* On Mac, for some reason, changing the selection on the list is not
 576                      * reported by VoiceOver the first time it shows.
 577                      * Note that this fix returns a child of the PopupWindow back to the main
 578                      * Stage, which doesn't seem to cause problems.
 579                      */
 580                     return listView.queryAccessibleAttribute(attribute, parameters);
 581                 }
 582                 return null;
 583             }
 584             case TEXT: {
 585                 String accText = comboBox.getAccessibleText();
 586                 if (accText != null && !accText.isEmpty()) return accText;
 587                 String title = comboBox.isEditable() ? getEditor().getText() : buttonCell.getText();
 588                 if (title == null || title.isEmpty()) {
 589                     title = comboBox.getPromptText();
 590                 }
 591                 return title;


   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.control.skin;
  27 
  28 import com.sun.javafx.scene.control.behavior.BehaviorBase;
  29 import com.sun.javafx.scene.control.behavior.ComboBoxBaseBehavior;
  30 import com.sun.javafx.scene.control.behavior.ComboBoxListViewBehavior;
  31 
  32 import java.util.List;
  33 
  34 import javafx.beans.InvalidationListener;

  35 import javafx.beans.WeakInvalidationListener;
  36 import javafx.beans.property.BooleanProperty;
  37 import javafx.beans.property.SimpleBooleanProperty;
  38 import javafx.collections.FXCollections;
  39 import javafx.collections.ListChangeListener;
  40 import javafx.collections.ObservableList;
  41 import javafx.collections.WeakListChangeListener;
  42 import javafx.css.PseudoClass;
  43 import javafx.event.ActionEvent;
  44 import javafx.event.EventTarget;
  45 import javafx.scene.AccessibleAttribute;
  46 import javafx.scene.AccessibleRole;
  47 import javafx.scene.Node;
  48 import javafx.scene.Parent;
  49 import javafx.scene.control.Accordion;
  50 import javafx.scene.control.Button;
  51 import javafx.scene.control.ComboBox;
  52 import javafx.scene.control.Control;
  53 import javafx.scene.control.ListCell;
  54 import javafx.scene.control.ListView;
  55 import javafx.scene.control.SelectionMode;
  56 import javafx.scene.control.SelectionModel;
  57 import javafx.scene.control.TextField;
  58 import javafx.scene.input.*;
  59 import javafx.util.Callback;
  60 import javafx.util.StringConverter;
  61 
  62 /**
  63  * Default skin implementation for the {@link ComboBox} control.
  64  *
  65  * @see ComboBox
  66  * @since 9
  67  */
  68 public class ComboBoxListViewSkin<T> extends ComboBoxPopupControl<T> {
  69 
  70     /***************************************************************************
  71      *                                                                         *
  72      * Static fields                                                           *
  73      *                                                                         *
  74      **************************************************************************/
  75 
  76     // By default we measure the width of all cells in the ListView. If this
  77     // is too burdensome, the developer may set a property in the ComboBox
  78     // properties map with this key to specify the number of rows to measure.
  79     // This may one day become a property on the ComboBox itself.
  80     private static final String COMBO_BOX_ROWS_TO_MEASURE_WIDTH_KEY = "comboBoxRowsToMeasureWidth";
  81 
  82     
  83     
  84     /***************************************************************************
  85      *                                                                         *
  86      * Private fields                                                          *
  87      *                                                                         *
  88      **************************************************************************/    
  89     
  90     private final ComboBox<T> comboBox;
  91     private ObservableList<T> comboBoxItems;
  92     
  93     private ListCell<T> buttonCell;
  94     private Callback<ListView<T>, ListCell<T>> cellFactory;
  95     
  96     private final ListView<T> listView;
  97     private ObservableList<T> listViewItems;
  98     
  99     private boolean listSelectionLock = false;
 100     private boolean listViewSelectionDirty = false;
 101 
 102     private final ComboBoxListViewBehavior behavior;
 103 
 104 
 105 
 106     /***************************************************************************
 107      *                                                                         *
 108      * Listeners                                                               *
 109      *                                                                         *
 110      **************************************************************************/
 111     
 112     private boolean itemCountDirty;
 113     private final ListChangeListener<T> listViewItemsListener = new ListChangeListener<T>() {
 114         @Override public void onChanged(ListChangeListener.Change<? extends T> c) {
 115             itemCountDirty = true;
 116             getSkinnable().requestLayout();
 117         }
 118     };
 119 
 120     private final InvalidationListener itemsObserver;
 121     
 122     private final WeakListChangeListener<T> weakListViewItemsListener =
 123             new WeakListChangeListener<T>(listViewItemsListener);
 124 
 125     
 126     /***************************************************************************
 127      *                                                                         *
 128      * Constructors                                                            *
 129      *                                                                         *
 130      **************************************************************************/
 131 
 132     /**
 133      * Creates a new ComboBoxListViewSkin instance, installing the necessary child
 134      * nodes into the Control {@link Control#getChildren() children} list, as
 135      * well as the necessary input mappings for handling key, mouse, etc events.
 136      *
 137      * @param control The control that this skin should be installed onto.
 138      */
 139     public ComboBoxListViewSkin(final ComboBox<T> control) {
 140         super(control);
 141 
 142         // install default input map for the control
 143         this.behavior = new ComboBoxListViewBehavior<>(control);
 144 //        control.setInputMap(behavior.getInputMap());
 145 
 146         this.comboBox = control;
 147         updateComboBoxItems();
 148 
 149         itemsObserver = observable -> {
 150             updateComboBoxItems();
 151             updateListViewItems();
 152         };
 153         control.itemsProperty().addListener(new WeakInvalidationListener(itemsObserver));
 154         
 155         // listview for popup
 156         this.listView = createListView();
 157         
 158         // Fix for RT-21207. Additional code related to this bug is further below.
 159         this.listView.setManaged(false);
 160         getChildren().add(listView);
 161         // -- end of fix
 162                 
 163         updateListViewItems();
 164         updateCellFactory();
 165         
 166         updateButtonCell();
 167 
 168         // Fix for RT-19431 (also tested via ComboBoxListViewSkinTest)
 169         updateValue();
 170 
 171         registerChangeListener(control.itemsProperty(), e -> {
 172             updateComboBoxItems();
 173             updateListViewItems();
 174         });
 175         registerChangeListener(control.promptTextProperty(), e -> updateDisplayNode());
 176         registerChangeListener(control.cellFactoryProperty(), e -> updateCellFactory());
 177         registerChangeListener(control.visibleRowCountProperty(), e -> {
 178             if (listView == null) return;
 179             listView.requestLayout();
 180         });
 181         registerChangeListener(control.converterProperty(), e -> updateListViewItems());
 182         registerChangeListener(control.buttonCellProperty(), e -> updateButtonCell());
 183         registerChangeListener(control.valueProperty(), e -> {
 184             updateValue();
 185             control.fireEvent(new ActionEvent());
 186         });
 187         registerChangeListener(control.editableProperty(), e -> updateEditable());
 188     }
 189 
 190 
 191 
 192     /***************************************************************************
 193      *                                                                         *
 194      * Properties                                                              *
 195      *                                                                         *
 196      **************************************************************************/
 197 
 198     /**
 199      * By default this skin hides the popup whenever the ListView is clicked in.
 200      * By setting hideOnClick to false, the popup will not be hidden when the
 201      * ListView is clicked in. This is beneficial in some scenarios (for example,
 202      * when the ListView cells have checkboxes).
 203      */
 204     // --- hide on click
 205     private final BooleanProperty hideOnClick = new SimpleBooleanProperty(this, "hideOnClick", true);
 206     public final BooleanProperty hideOnClickProperty() {
 207         return hideOnClick;
 208     }
 209     public final boolean isHideOnClick() {
 210         return hideOnClick.get();
 211     }
 212     public final void setHideOnClick(boolean value) {
 213         hideOnClick.set(value);
 214     }
 215 
 216 
 217 
 218     /***************************************************************************
 219      *                                                                         *
 220      * Public API                                                              *
 221      *                                                                         *
 222      **************************************************************************/
 223 
 224     /** {@inheritDoc} */
 225     @Override public void dispose() {
 226         super.dispose();
 227 
 228         if (behavior != null) {
 229             behavior.dispose();



















 230         }
 231     }
 232 
 233     /** {@inheritDoc} */
 234     @Override protected TextField getEditor() {
 235         // Return null if editable is false, even if the ComboBox has an editor set.
 236         // Use getSkinnable() here because this method is called from the super
 237         // constructor before comboBox is initialized.
 238         return getSkinnable().isEditable() ? ((ComboBox)getSkinnable()).getEditor() : null;
 239     }
 240 
 241     /** {@inheritDoc} */
 242     @Override protected StringConverter<T> getConverter() {
 243         return ((ComboBox)getSkinnable()).getConverter();
 244     }
 245 

 246     /** {@inheritDoc} */
 247     @Override public Node getDisplayNode() {
 248         Node displayNode;
 249         if (comboBox.isEditable()) {
 250             displayNode = getEditableInputNode();
 251         } else {
 252             displayNode = buttonCell;
 253         }
 254         
 255         updateDisplayNode();
 256         
 257         return displayNode;
 258     }
 259 
 260     /** {@inheritDoc} */




















 261     @Override public Node getPopupContent() {
 262         return listView;
 263     }
 264 
 265     /** {@inheritDoc} */
 266     @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 267         reconfigurePopup();
 268         return 50;
 269     }
 270 
 271     /** {@inheritDoc} */
 272     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 273         double superPrefWidth = super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset);
 274         double listViewWidth = listView.prefWidth(height);
 275         double pw = Math.max(superPrefWidth, listViewWidth);
 276 
 277         reconfigurePopup();
 278 
 279         return pw;
 280     }
 281 
 282     /** {@inheritDoc} */
 283     @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 284         reconfigurePopup();
 285         return super.computeMaxWidth(height, topInset, rightInset, bottomInset, leftInset);
 286     }
 287 
 288     /** {@inheritDoc} */
 289     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 290         reconfigurePopup();
 291         return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
 292     }
 293 
 294     /** {@inheritDoc} */
 295     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 296         reconfigurePopup();
 297         return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
 298     }
 299 
 300     /** {@inheritDoc} */
 301     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 302         reconfigurePopup();
 303         return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
 304     }
 305 
 306     /** {@inheritDoc} */
 307     @Override protected void layoutChildren(final double x, final double y,
 308             final double w, final double h) {
 309         if (listViewSelectionDirty) {
 310             try {
 311                 listSelectionLock = true;
 312                 T item = comboBox.getSelectionModel().getSelectedItem();
 313                 listView.getSelectionModel().clearSelection();
 314                 listView.getSelectionModel().select(item);
 315             } finally {
 316                 listSelectionLock = false;
 317                 listViewSelectionDirty = false;
 318             }
 319         }
 320         
 321         super.layoutChildren(x, y, w, h);






 322     }
 323 
 324 
 325 
 326     /***************************************************************************
 327      *                                                                         *
 328      * Private methods                                                         *
 329      *                                                                         *
 330      **************************************************************************/
 331 
 332     /** {@inheritDoc} */
 333     @Override void updateDisplayNode() {
 334         if (getEditor() != null) {
 335             super.updateDisplayNode();
 336         } else {
 337             T value = comboBox.getValue();
 338             int index = getIndexOfComboBoxValueInItemsList();
 339             if (index > -1) {
 340                 buttonCell.setItem(null);
 341                 buttonCell.updateIndex(index);
 342             } else {
 343                 // RT-21336 Show the ComboBox value even though it doesn't
 344                 // exist in the ComboBox items list (part two of fix)
 345                 buttonCell.updateIndex(-1);
 346                 boolean empty = updateDisplayText(buttonCell, value, false);
 347 
 348                 // Note that empty boolean collected above. This is used to resolve
 349                 // RT-27834, where we were getting different styling based on whether
 350                 // the cell was updated via the updateIndex method above, or just
 351                 // by directly updating the text. We fake the pseudoclass state
 352                 // for empty, filled, and selected here.
 353                 buttonCell.pseudoClassStateChanged(PSEUDO_CLASS_EMPTY,    empty);
 354                 buttonCell.pseudoClassStateChanged(PSEUDO_CLASS_FILLED,   !empty);
 355                 buttonCell.pseudoClassStateChanged(PSEUDO_CLASS_SELECTED, true);
 356             }
 357         }
 358     }
 359 
 360     /** {@inheritDoc} */
 361     @Override ComboBoxBaseBehavior getBehavior() {
 362         return behavior;
 363     }
 364 
 365     private void updateComboBoxItems() {
 366         comboBoxItems = comboBox.getItems();
 367         comboBoxItems = comboBoxItems == null ? FXCollections.<T>emptyObservableList() : comboBoxItems;
 368     }
 369 
 370     private void updateListViewItems() {
 371         if (listViewItems != null) {
 372             listViewItems.removeListener(weakListViewItemsListener);
 373         }
 374 
 375         this.listViewItems = comboBoxItems;
 376         listView.setItems(listViewItems);
 377 
 378         if (listViewItems != null) {
 379             listViewItems.addListener(weakListViewItemsListener);
 380         }
 381 
 382         itemCountDirty = true;
 383         getSkinnable().requestLayout();
 384     }
 385 
 386     private void updateValue() {
 387         T newValue = comboBox.getValue();
 388         
 389         SelectionModel<T> listViewSM = listView.getSelectionModel();
 390         
 391         if (newValue == null) {
 392             listViewSM.clearSelection();
 393         } else {
 394             // RT-22386: We need to test to see if the value is in the comboBox
 395             // items list. If it isn't, then we should clear the listview 
 396             // selection
 397             int indexOfNewValue = getIndexOfComboBoxValueInItemsList();
 398             if (indexOfNewValue == -1) {
 399                 listSelectionLock = true;
 400                 listViewSM.clearSelection();
 401                 listSelectionLock = false;
 402             } else {
 403                 int index = comboBox.getSelectionModel().getSelectedIndex();
 404                 if (index >= 0 && index < comboBoxItems.size()) {
 405                     T itemsObj = comboBoxItems.get(index);
 406                     if (itemsObj != null && itemsObj.equals(newValue)) {
 407                         listViewSM.select(index);
 408                     } else {
 409                         listViewSM.select(newValue);
 410                     }
 411                 } else {
 412                     // just select the first instance of newValue in the list
 413                     int listViewIndex = comboBoxItems.indexOf(newValue);
 414                     if (listViewIndex == -1) {
 415                         // RT-21336 Show the ComboBox value even though it doesn't
 416                         // exist in the ComboBox items list (part one of fix)
 417                         updateDisplayNode();
 418                     } else {
 419                         listViewSM.select(listViewIndex);
 420                     }
 421                 }
 422             }
 423         }
 424     }
 425 




























 426     // return a boolean to indicate that the cell is empty (and therefore not filled)
 427     private boolean updateDisplayText(ListCell<T> cell, T item, boolean empty) {
 428         if (empty) {
 429             if (cell == null) return true;
 430             cell.setGraphic(null);
 431             cell.setText(null);
 432             return true;
 433         } else if (item instanceof Node) {
 434             Node currentNode = cell.getGraphic();
 435             Node newNode = (Node) item;
 436             if (currentNode == null || ! currentNode.equals(newNode)) {
 437                 cell.setText(null);
 438                 cell.setGraphic(newNode);
 439             }
 440             return newNode == null;
 441         } else {
 442             // run item through StringConverter if it isn't null
 443             StringConverter<T> c = comboBox.getConverter();
 444             String s = item == null ? comboBox.getPromptText() : (c == null ? item.toString() : c.toString(item));
 445             cell.setText(s);


 544          
 545         comboBox.getSelectionModel().selectedItemProperty().addListener(o -> {
 546             listViewSelectionDirty = true;
 547         });
 548         
 549         _listView.addEventFilter(MouseEvent.MOUSE_RELEASED, t -> {
 550             // RT-18672: Without checking if the user is clicking in the
 551             // scrollbar area of the ListView, the comboBox will hide. Therefore,
 552             // we add the check below to prevent this from happening.
 553             EventTarget target = t.getTarget();
 554             if (target instanceof Parent) {
 555                 List<String> s = ((Parent) target).getStyleClass();
 556                 if (s.contains("thumb")
 557                         || s.contains("track")
 558                         || s.contains("decrement-arrow")
 559                         || s.contains("increment-arrow")) {
 560                     return;
 561                 }
 562             }
 563 
 564             if (isHideOnClick()) {
 565                 comboBox.hide();
 566             }
 567         });
 568 
 569         _listView.setOnKeyPressed(t -> {
 570             // TODO move to behavior, when (or if) this class becomes a SkinBase
 571             if (t.getCode() == KeyCode.ENTER ||
 572                     t.getCode() == KeyCode.SPACE ||
 573                     t.getCode() == KeyCode.ESCAPE) {
 574                 comboBox.hide();
 575             }
 576         });
 577         
 578         return _listView;
 579     }
 580     
 581     private double getListViewPrefHeight() {
 582         double ph;
 583         if (listView.getSkin() instanceof VirtualContainerBase) {
 584             int maxRows = comboBox.getVisibleRowCount();
 585             VirtualContainerBase<?,?> skin = (VirtualContainerBase<?,?>)listView.getSkin();
 586             ph = skin.getVirtualFlowPreferredHeight(maxRows);
 587         } else {
 588             double ch = comboBoxItems.size() * 25;
 589             ph = Math.min(ch, 200);
 590         }
 591         
 592         return ph;
 593     }
 594 
 595 
 596     
 597     /**************************************************************************
 598      * 
 599      * API for testing
 600      * 
 601      *************************************************************************/
 602     
 603     ListView<T> getListView() {
 604         return listView;
 605     }
 606     
 607     
 608     
 609 
 610     /***************************************************************************
 611      *                                                                         *
 612      * Stylesheet Handling                                                     *
 613      *                                                                         *
 614      **************************************************************************/
 615     
 616     // These three pseudo class states are duplicated from Cell
 617     private static final PseudoClass PSEUDO_CLASS_SELECTED =
 618             PseudoClass.getPseudoClass("selected");
 619     private static final PseudoClass PSEUDO_CLASS_EMPTY =
 620             PseudoClass.getPseudoClass("empty");
 621     private static final PseudoClass PSEUDO_CLASS_FILLED =
 622             PseudoClass.getPseudoClass("filled");
 623 
 624 
 625     /** {@inheritDoc} */
 626     @Override public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {

 627         switch (attribute) {
 628             case FOCUS_ITEM: {
 629                 if (comboBox.isShowing()) {
 630                     /* On Mac, for some reason, changing the selection on the list is not
 631                      * reported by VoiceOver the first time it shows.
 632                      * Note that this fix returns a child of the PopupWindow back to the main
 633                      * Stage, which doesn't seem to cause problems.
 634                      */
 635                     return listView.queryAccessibleAttribute(attribute, parameters);
 636                 }
 637                 return null;
 638             }
 639             case TEXT: {
 640                 String accText = comboBox.getAccessibleText();
 641                 if (accText != null && !accText.isEmpty()) return accText;
 642                 String title = comboBox.isEditable() ? getEditor().getText() : buttonCell.getText();
 643                 if (title == null || title.isEmpty()) {
 644                     title = comboBox.getPromptText();
 645                 }
 646                 return title;