1 /*
   2  * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   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 import com.sun.javafx.scene.input.ExtendedInputMethodRequests;
  30 
  31 import java.lang.ref.WeakReference;
  32 import java.util.List;
  33 
  34 import com.sun.javafx.scene.traversal.Algorithm;
  35 import com.sun.javafx.scene.traversal.Direction;
  36 import com.sun.javafx.scene.traversal.ParentTraversalEngine;
  37 import com.sun.javafx.scene.traversal.TraversalContext;
  38 import javafx.beans.InvalidationListener;
  39 import javafx.beans.Observable;
  40 import javafx.beans.WeakInvalidationListener;
  41 import javafx.collections.FXCollections;
  42 import javafx.collections.ListChangeListener;
  43 import javafx.collections.ObservableList;
  44 import javafx.collections.WeakListChangeListener;
  45 import javafx.css.PseudoClass;
  46 import javafx.event.ActionEvent;
  47 import javafx.event.EventHandler;
  48 import javafx.event.EventTarget;
  49 import javafx.geometry.Point2D;
  50 import javafx.scene.AccessibleAttribute;
  51 import javafx.scene.AccessibleRole;
  52 import javafx.scene.Node;
  53 import javafx.scene.Parent;
  54 import javafx.scene.control.ComboBox;
  55 import javafx.scene.control.ComboBoxBase;
  56 import javafx.scene.control.ListCell;
  57 import javafx.scene.control.ListView;
  58 import javafx.scene.control.SelectionMode;
  59 import javafx.scene.control.SelectionModel;
  60 import javafx.scene.control.TextField;
  61 import javafx.scene.input.*;
  62 import javafx.util.Callback;
  63 import javafx.util.StringConverter;
  64 
  65 public class ComboBoxListViewSkin<T> extends ComboBoxPopupControl<T> {
  66     
  67     // By default we measure the width of all cells in the ListView. If this
  68     // is too burdensome, the developer may set a property in the ComboBox
  69     // properties map with this key to specify the number of rows to measure.
  70     // This may one day become a property on the ComboBox itself.
  71     private static final String COMBO_BOX_ROWS_TO_MEASURE_WIDTH_KEY = "comboBoxRowsToMeasureWidth";
  72 
  73     
  74     
  75     /***************************************************************************
  76      *                                                                         *
  77      * Private fields                                                          *
  78      *                                                                         *
  79      **************************************************************************/    
  80     
  81     private final ComboBox<T> comboBox;
  82     private ObservableList<T> comboBoxItems;
  83     
  84     private ListCell<T> buttonCell;
  85     private Callback<ListView<T>, ListCell<T>> cellFactory;
  86     private TextField textField;
  87     
  88     private final ListView<T> listView;
  89     private ObservableList<T> listViewItems;
  90     
  91     private boolean listSelectionLock = false;
  92     private boolean listViewSelectionDirty = false;
  93 
  94     
  95     /***************************************************************************
  96      *                                                                         *
  97      * Listeners                                                               *
  98      *                                                                         *
  99      **************************************************************************/
 100     
 101     private boolean itemCountDirty;
 102     private final ListChangeListener<T> listViewItemsListener = new ListChangeListener<T>() {
 103         @Override public void onChanged(ListChangeListener.Change<? extends T> c) {
 104             itemCountDirty = true;
 105             getSkinnable().requestLayout();
 106         }
 107     };
 108 
 109     private final InvalidationListener itemsObserver;
 110     
 111     private final WeakListChangeListener<T> weakListViewItemsListener =
 112             new WeakListChangeListener<T>(listViewItemsListener);
 113 
 114     private EventHandler<KeyEvent> textFieldKeyEventHandler = event -> {
 115         if (textField == null || ! getSkinnable().isEditable()) return;
 116         handleKeyEvent(event, true);
 117     };
 118     private EventHandler<MouseEvent> textFieldMouseEventHandler = event -> {
 119         ComboBoxBase<T> comboBox = getSkinnable();
 120         if (event.getTarget().equals(comboBox)) return;
 121         comboBox.fireEvent(event.copyFor(comboBox, comboBox));
 122         event.consume();
 123     };
 124     private EventHandler<DragEvent> textFieldDragEventHandler = event -> {
 125         ComboBoxBase<T> comboBox = getSkinnable();
 126         if (event.getTarget().equals(comboBox)) return;
 127         comboBox.fireEvent(event.copyFor(comboBox, comboBox));
 128         event.consume();
 129     };
 130     
 131     
 132     
 133     /***************************************************************************
 134      *                                                                         *
 135      * Constructors                                                            *
 136      *                                                                         *
 137      **************************************************************************/   
 138     
 139     public ComboBoxListViewSkin(final ComboBox<T> comboBox) {
 140         super(comboBox, new ComboBoxListViewBehavior<T>(comboBox));
 141         this.comboBox = comboBox;
 142         updateComboBoxItems();
 143 
 144         itemsObserver = observable -> {
 145             updateComboBoxItems();
 146             updateListViewItems();
 147         };
 148         this.comboBox.itemsProperty().addListener(new WeakInvalidationListener(itemsObserver));
 149         
 150         // editable input node
 151         this.textField = comboBox.isEditable() ? getEditableInputNode() : null;
 152         
 153         // Fix for RT-29565. Without this the textField does not have a correct
 154         // pref width at startup, as it is not part of the scenegraph (and therefore
 155         // has no pref width until after the first measurements have been taken).
 156         if (this.textField != null) {
 157             getChildren().add(textField);
 158         }
 159         
 160         // listview for popup
 161         this.listView = createListView();
 162         
 163         // Fix for RT-21207. Additional code related to this bug is further below.
 164         this.listView.setManaged(false);
 165         getChildren().add(listView);
 166         // -- end of fix
 167                 
 168         updateListViewItems();
 169         updateCellFactory();
 170         
 171         updateButtonCell();
 172 
 173         // move fake focus in to the textfield if the comboBox is editable
 174         comboBox.focusedProperty().addListener((ov, t, hasFocus) -> {
 175             if (comboBox.isEditable()) {
 176                 // Fix for the regression noted in a comment in RT-29885.
 177                 ((FakeFocusTextField)textField).setFakeFocus(hasFocus);
 178             }
 179         });
 180 
 181         comboBox.addEventFilter(KeyEvent.ANY, ke -> {
 182             if (textField == null || ! comboBox.isEditable()) {
 183                 handleKeyEvent(ke, false);
 184             } else {
 185                 // This prevents a stack overflow from our rebroadcasting of the
 186                 // event to the textfield that occurs in the final else statement
 187                 // of the conditions below.
 188                 if (ke.getTarget().equals(textField)) return;
 189 
 190                 // Fix for the regression noted in a comment in RT-29885.
 191                 // This forwards the event down into the TextField when
 192                 // the key event is actually received by the ComboBox.
 193                 textField.fireEvent(ke.copyFor(textField, textField));
 194                 ke.consume();
 195             }
 196         });
 197 
 198         // RT-38978: Forward input method events to TextField if editable.
 199         if (comboBox.getOnInputMethodTextChanged() == null) {
 200             comboBox.setOnInputMethodTextChanged(event -> {
 201                 if (textField != null && comboBox.isEditable() && comboBox.getScene().getFocusOwner() == comboBox) {
 202                     if (textField.getOnInputMethodTextChanged() != null) {
 203                         textField.getOnInputMethodTextChanged().handle(event);
 204                     }
 205                 }
 206             });
 207         }
 208 
 209         updateEditable();
 210         
 211         // Fix for RT-19431 (also tested via ComboBoxListViewSkinTest)
 212         updateValue();
 213 
 214         // Fix for RT-36902, where focus traversal was getting stuck inside the ComboBox
 215         comboBox.setImpl_traversalEngine(new ParentTraversalEngine(comboBox, new Algorithm() {
 216             @Override public Node select(Node owner, Direction dir, TraversalContext context) {
 217                 return null;
 218             }
 219 
 220             @Override public Node selectFirst(TraversalContext context) {
 221                 return null;
 222             }
 223 
 224             @Override public Node selectLast(TraversalContext context) {
 225                 return null;
 226             }
 227         }));
 228         
 229         registerChangeListener(comboBox.itemsProperty(), "ITEMS");
 230         registerChangeListener(comboBox.promptTextProperty(), "PROMPT_TEXT");
 231         registerChangeListener(comboBox.cellFactoryProperty(), "CELL_FACTORY");
 232         registerChangeListener(comboBox.visibleRowCountProperty(), "VISIBLE_ROW_COUNT");
 233         registerChangeListener(comboBox.converterProperty(), "CONVERTER");
 234         registerChangeListener(comboBox.buttonCellProperty(), "BUTTON_CELL");
 235         registerChangeListener(comboBox.valueProperty(), "VALUE");
 236         registerChangeListener(comboBox.editableProperty(), "EDITABLE");
 237     }
 238 
 239 
 240 
 241     /***************************************************************************
 242      *                                                                         *
 243      * Public API                                                              *
 244      *                                                                         *
 245      **************************************************************************/  
 246     
 247     /** {@inheritDoc} */
 248     @Override protected void handleControlPropertyChanged(String p) {
 249         super.handleControlPropertyChanged(p);
 250         
 251         if ("ITEMS".equals(p)) {
 252             updateComboBoxItems();
 253             updateListViewItems();
 254         } else if ("PROMPT_TEXT".equals(p)) {
 255             updateDisplayNode();
 256         } else if ("CELL_FACTORY".equals(p)) {
 257             updateCellFactory();
 258         } else if ("VISIBLE_ROW_COUNT".equals(p)) {
 259             if (listView == null) return;
 260             listView.requestLayout();
 261         } else if ("CONVERTER".equals(p)) {
 262             updateListViewItems();
 263         } else if ("EDITOR".equals(p)) {
 264             getEditableInputNode();
 265         } else if ("BUTTON_CELL".equals(p)) {
 266             updateButtonCell();
 267         } else if ("VALUE".equals(p)) {
 268             updateValue();
 269             comboBox.fireEvent(new ActionEvent());
 270         } else if ("EDITABLE".equals(p)) {
 271             updateEditable();
 272         }
 273     }
 274 
 275     private void updateEditable() {
 276         TextField newTextField = comboBox.getEditor();
 277 
 278         if (!comboBox.isEditable()) {
 279             // remove event filters
 280             if (textField != null) {
 281                 textField.removeEventFilter(KeyEvent.ANY, textFieldKeyEventHandler);
 282                 textField.removeEventFilter(MouseEvent.DRAG_DETECTED, textFieldMouseEventHandler);
 283                 textField.removeEventFilter(DragEvent.ANY, textFieldDragEventHandler);
 284 
 285                 comboBox.setInputMethodRequests(null);
 286             }
 287         } else if (newTextField != null) {
 288             // add event filters
 289             newTextField.addEventFilter(KeyEvent.ANY, textFieldKeyEventHandler);
 290 
 291             // Fix for RT-31093 - drag events from the textfield were not surfacing
 292             // properly for the ComboBox.
 293             newTextField.addEventFilter(MouseEvent.DRAG_DETECTED, textFieldMouseEventHandler);
 294             newTextField.addEventFilter(DragEvent.ANY, textFieldDragEventHandler);
 295 
 296             // RT-38978: Forward input method requests to TextField.
 297             comboBox.setInputMethodRequests(new ExtendedInputMethodRequests() {
 298                 @Override public Point2D getTextLocation(int offset) {
 299                     return newTextField.getInputMethodRequests().getTextLocation(offset);
 300                 }
 301 
 302                 @Override public int getLocationOffset(int x, int y) {
 303                     return newTextField.getInputMethodRequests().getLocationOffset(x, y);
 304                 }
 305 
 306                 @Override public void cancelLatestCommittedText() {
 307                     newTextField.getInputMethodRequests().cancelLatestCommittedText();
 308                 }
 309 
 310                 @Override public String getSelectedText() {
 311                     return newTextField.getInputMethodRequests().getSelectedText();
 312                 }
 313 
 314                 @Override public int getInsertPositionOffset() {
 315                     return ((ExtendedInputMethodRequests)newTextField.getInputMethodRequests()).getInsertPositionOffset();
 316                 }
 317 
 318                 @Override public String getCommittedText(int begin, int end) {
 319                     return ((ExtendedInputMethodRequests)newTextField.getInputMethodRequests()).getCommittedText(begin, end);
 320                 }
 321 
 322                 @Override public int getCommittedTextLength() {
 323                     return ((ExtendedInputMethodRequests)newTextField.getInputMethodRequests()).getCommittedTextLength();
 324                 }
 325             });
 326         }
 327 
 328         textField = newTextField;
 329     }
 330     
 331     /** {@inheritDoc} */
 332     @Override public Node getDisplayNode() {
 333         Node displayNode;
 334         if (comboBox.isEditable()) {
 335             displayNode = getEditableInputNode();
 336         } else {
 337             displayNode = buttonCell;
 338         }
 339         
 340         updateDisplayNode();
 341         
 342         return displayNode;
 343     }
 344     
 345     public void updateComboBoxItems() {
 346         comboBoxItems = comboBox.getItems();
 347         comboBoxItems = comboBoxItems == null ? FXCollections.<T>emptyObservableList() : comboBoxItems;
 348     }
 349     
 350     public void updateListViewItems() {
 351         if (listViewItems != null) {
 352             listViewItems.removeListener(weakListViewItemsListener);
 353         }
 354 
 355         this.listViewItems = comboBoxItems;
 356         listView.setItems(listViewItems);
 357 
 358         if (listViewItems != null) {
 359             listViewItems.addListener(weakListViewItemsListener);
 360         }
 361         
 362         itemCountDirty = true;
 363         getSkinnable().requestLayout();
 364     }
 365     
 366     @Override public Node getPopupContent() {
 367         return listView;
 368     }
 369     
 370     @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 371         reconfigurePopup();
 372         return 50;
 373     }
 374 
 375     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 376         double superPrefWidth = super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset);
 377         double listViewWidth = listView.prefWidth(height);
 378         double pw = Math.max(superPrefWidth, listViewWidth);
 379 
 380         reconfigurePopup();
 381 
 382         return pw;
 383     }
 384 
 385     @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 386         reconfigurePopup();
 387         return super.computeMaxWidth(height, topInset, rightInset, bottomInset, leftInset);
 388     }
 389 
 390     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 391         reconfigurePopup();
 392         return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
 393     }
 394 
 395     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 396         reconfigurePopup();
 397         return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
 398     }
 399 
 400     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 401         reconfigurePopup();
 402         return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
 403     }
 404 
 405     @Override protected void layoutChildren(final double x, final double y,
 406             final double w, final double h) {
 407         if (listViewSelectionDirty) {
 408             try {
 409                 listSelectionLock = true;
 410                 T item = comboBox.getSelectionModel().getSelectedItem();
 411                 listView.getSelectionModel().clearSelection();
 412                 listView.getSelectionModel().select(item);
 413             } finally {
 414                 listSelectionLock = false;
 415                 listViewSelectionDirty = false;
 416             }
 417         }
 418         
 419         super.layoutChildren(x,y,w,h);
 420     }
 421 
 422     // Added to allow subclasses to prevent the popup from hiding when the
 423     // ListView is clicked on (e.g when the list cells have checkboxes).
 424     protected boolean isHideOnClickEnabled() {
 425         return true;
 426     }
 427 
 428     
 429     
 430     /***************************************************************************
 431      *                                                                         *
 432      * Private methods                                                         *
 433      *                                                                         *
 434      **************************************************************************/
 435 
 436     private void handleKeyEvent(KeyEvent ke, boolean doConsume) {
 437         // When the user hits the enter or F4 keys, we respond before
 438         // ever giving the event to the TextField.
 439         if (ke.getCode() == KeyCode.ENTER) {
 440             setTextFromTextFieldIntoComboBoxValue();
 441 
 442             if (doConsume) ke.consume();
 443         } else if (ke.getCode() == KeyCode.F4) {
 444             if (ke.getEventType() == KeyEvent.KEY_RELEASED) {
 445                 if (comboBox.isShowing()) comboBox.hide();
 446                 else comboBox.show();
 447             }
 448             ke.consume(); // we always do a consume here (otherwise unit tests fail)
 449         } else if (ke.getCode() == KeyCode.F10 || ke.getCode() == KeyCode.ESCAPE) {
 450             // RT-23275: The TextField fires F10 and ESCAPE key events
 451             // up to the parent, which are then fired back at the
 452             // TextField, and this ends up in an infinite loop until
 453             // the stack overflows. So, here we consume these two
 454             // events and stop them from going any further.
 455             if (doConsume) ke.consume();
 456         }
 457     }
 458 
 459     private void updateValue() {
 460         T newValue = comboBox.getValue();
 461         
 462         SelectionModel<T> listViewSM = listView.getSelectionModel();
 463         
 464         if (newValue == null) {
 465             listViewSM.clearSelection();
 466         } else {
 467             // RT-22386: We need to test to see if the value is in the comboBox
 468             // items list. If it isn't, then we should clear the listview 
 469             // selection
 470             int indexOfNewValue = getIndexOfComboBoxValueInItemsList();
 471             if (indexOfNewValue == -1) {
 472                 listSelectionLock = true;
 473                 listViewSM.clearSelection();
 474                 listSelectionLock = false;
 475             } else {
 476                 int index = comboBox.getSelectionModel().getSelectedIndex();
 477                 if (index >= 0 && index < comboBoxItems.size()) {
 478                     T itemsObj = comboBoxItems.get(index);
 479                     if (itemsObj != null && itemsObj.equals(newValue)) {
 480                         listViewSM.select(index);
 481                     } else {
 482                         listViewSM.select(newValue);
 483                     }
 484                 } else {
 485                     // just select the first instance of newValue in the list
 486                     int listViewIndex = comboBoxItems.indexOf(newValue);
 487                     if (listViewIndex == -1) {
 488                         // RT-21336 Show the ComboBox value even though it doesn't
 489                         // exist in the ComboBox items list (part one of fix)
 490                         updateDisplayNode();
 491                     } else {
 492                         listViewSM.select(listViewIndex);
 493                     }
 494                 }
 495             }
 496         }
 497     }
 498     
 499     private String initialTextFieldValue = null;
 500     private TextField getEditableInputNode() {
 501         if (textField != null) return textField;
 502         
 503         textField = comboBox.getEditor();
 504         textField.focusTraversableProperty().bindBidirectional(comboBox.focusTraversableProperty());
 505         textField.promptTextProperty().bind(comboBox.promptTextProperty());
 506         textField.tooltipProperty().bind(comboBox.tooltipProperty());
 507 
 508         // Fix for RT-21406: ComboBox do not show initial text value
 509         initialTextFieldValue = textField.getText();
 510         // End of fix (see updateDisplayNode below for the related code)
 511 
 512         textField.focusedProperty().addListener((ov, t, hasFocus) -> {
 513             if (! comboBox.isEditable()) return;
 514 
 515             // Fix for RT-29885
 516             comboBox.getProperties().put("FOCUSED", hasFocus);
 517             // --- end of RT-29885
 518 
 519             // RT-21454 starts here
 520             if (! hasFocus) {
 521                 setTextFromTextFieldIntoComboBoxValue();
 522                 pseudoClassStateChanged(CONTAINS_FOCUS_PSEUDOCLASS_STATE, false);
 523             } else {
 524                 pseudoClassStateChanged(CONTAINS_FOCUS_PSEUDOCLASS_STATE, true);
 525             }
 526             // --- end of RT-21454
 527         });
 528 
 529         return textField;
 530     }
 531     
 532     private void updateDisplayNode() {
 533         StringConverter<T> c = comboBox.getConverter();
 534         if (c == null) return;
 535               
 536         T value = comboBox.getValue();
 537         if (comboBox.isEditable()) {
 538             if (initialTextFieldValue != null && ! initialTextFieldValue.isEmpty()) {
 539                 // Remainder of fix for RT-21406: ComboBox do not show initial text value
 540                 textField.setText(initialTextFieldValue);
 541                 initialTextFieldValue = null;
 542                 // end of fix
 543             } else {
 544                 String stringValue = c.toString(value);
 545                 if (value == null || stringValue == null) {
 546                     textField.setText("");
 547                 } else if (! stringValue.equals(textField.getText())) {
 548                     textField.setText(stringValue);
 549                 }
 550             }
 551         } else {
 552             int index = getIndexOfComboBoxValueInItemsList();
 553             if (index > -1) {
 554                 buttonCell.setItem(null);
 555                 buttonCell.updateIndex(index);
 556             } else {
 557                 // RT-21336 Show the ComboBox value even though it doesn't
 558                 // exist in the ComboBox items list (part two of fix)
 559                 buttonCell.updateIndex(-1);
 560                 boolean empty = updateDisplayText(buttonCell, value, false);
 561                 
 562                 // Note that empty boolean collected above. This is used to resolve
 563                 // RT-27834, where we were getting different styling based on whether
 564                 // the cell was updated via the updateIndex method above, or just
 565                 // by directly updating the text. We fake the pseudoclass state
 566                 // for empty, filled, and selected here.
 567                 buttonCell.pseudoClassStateChanged(PSEUDO_CLASS_EMPTY,    empty);
 568                 buttonCell.pseudoClassStateChanged(PSEUDO_CLASS_FILLED,   !empty);
 569                 buttonCell.pseudoClassStateChanged(PSEUDO_CLASS_SELECTED, true);
 570             }
 571         }
 572     }
 573     
 574     // return a boolean to indicate that the cell is empty (and therefore not filled)
 575     private boolean updateDisplayText(ListCell<T> cell, T item, boolean empty) {
 576         if (empty) {
 577             if (cell == null) return true;
 578             cell.setGraphic(null);
 579             cell.setText(null);
 580             return true;
 581         } else if (item instanceof Node) {
 582             Node currentNode = cell.getGraphic();
 583             Node newNode = (Node) item;
 584             if (currentNode == null || ! currentNode.equals(newNode)) {
 585                 cell.setText(null);
 586                 cell.setGraphic(newNode);
 587             }
 588             return newNode == null;
 589         } else {
 590             // run item through StringConverter if it isn't null
 591             StringConverter<T> c = comboBox.getConverter();
 592             String s = item == null ? comboBox.getPromptText() : (c == null ? item.toString() : c.toString(item));
 593             cell.setText(s);
 594             cell.setGraphic(null);
 595             return s == null || s.isEmpty();
 596         }
 597     }
 598     
 599     private void setTextFromTextFieldIntoComboBoxValue() {
 600         if (! comboBox.isEditable()) return;
 601         
 602         StringConverter<T> c = comboBox.getConverter();
 603         if (c == null) return;
 604         
 605         T oldValue = comboBox.getValue();
 606         String text = textField.getText();
 607         
 608         // conditional check here added due to RT-28245
 609         T value = oldValue == null && (text == null || text.isEmpty()) ? null : c.fromString(textField.getText());
 610         
 611         if ((value == null && oldValue == null) || (value != null && value.equals(oldValue))) {
 612             // no point updating values needlessly (as they are the same)
 613             return;
 614         }
 615         
 616         comboBox.setValue(value);
 617     }
 618     
 619     private int getIndexOfComboBoxValueInItemsList() {
 620         T value = comboBox.getValue();
 621         int index = comboBoxItems.indexOf(value);
 622         return index;
 623     }
 624     
 625     private void updateButtonCell() {
 626         buttonCell = comboBox.getButtonCell() != null ? 
 627                 comboBox.getButtonCell() : getDefaultCellFactory().call(listView);
 628         buttonCell.setMouseTransparent(true);
 629         buttonCell.updateListView(listView);
 630         updateDisplayArea();
 631         // As long as the screen-reader is concerned this node is not a list item.
 632         // This matters because the screen-reader counts the number of list item
 633         // within combo and speaks it to the user.
 634         buttonCell.setAccessibleRole(AccessibleRole.NODE);
 635     }
 636 
 637     private void updateCellFactory() {
 638         Callback<ListView<T>, ListCell<T>> cf = comboBox.getCellFactory();
 639         cellFactory = cf != null ? cf : getDefaultCellFactory();
 640         listView.setCellFactory(cellFactory);
 641     }
 642     
 643     private Callback<ListView<T>, ListCell<T>> getDefaultCellFactory() {
 644         return new Callback<ListView<T>, ListCell<T>>() {
 645             @Override public ListCell<T> call(ListView<T> listView) {
 646                 return new ListCell<T>() {
 647                     @Override public void updateItem(T item, boolean empty) {
 648                         super.updateItem(item, empty);
 649                         updateDisplayText(this, item, empty);
 650                     }
 651                 };
 652             }
 653         };
 654     }
 655     
 656     private ListView<T> createListView() {
 657         final ListView<T> _listView = new ListView<T>() {
 658 
 659             {
 660                 getProperties().put("selectFirstRowByDefault", false);
 661             }
 662 
 663             @Override protected double computeMinHeight(double width) {
 664                 return 30;
 665             }
 666             
 667             @Override protected double computePrefWidth(double height) {
 668                 double pw;
 669                 if (getSkin() instanceof ListViewSkin) {
 670                     ListViewSkin<?> skin = (ListViewSkin<?>)getSkin();
 671                     if (itemCountDirty) {
 672                         skin.updateRowCount();
 673                         itemCountDirty = false;
 674                     }
 675                     
 676                     int rowsToMeasure = -1;
 677                     if (comboBox.getProperties().containsKey(COMBO_BOX_ROWS_TO_MEASURE_WIDTH_KEY)) {
 678                         rowsToMeasure = (Integer) comboBox.getProperties().get(COMBO_BOX_ROWS_TO_MEASURE_WIDTH_KEY);
 679                     }
 680                     
 681                     pw = Math.max(comboBox.getWidth(), skin.getMaxCellWidth(rowsToMeasure) + 30);
 682                 } else {
 683                     pw = Math.max(100, comboBox.getWidth());
 684                 }
 685 
 686                 // need to check the ListView pref height in the case that the
 687                 // placeholder node is showing
 688                 if (getItems().isEmpty() && getPlaceholder() != null) {
 689                     pw = Math.max(super.computePrefWidth(height), pw);
 690                 }
 691 
 692                 return Math.max(50, pw);
 693             }
 694 
 695             @Override protected double computePrefHeight(double width) {
 696                 return getListViewPrefHeight();
 697             }
 698         };
 699 
 700         _listView.setId("list-view");
 701         _listView.placeholderProperty().bind(comboBox.placeholderProperty());
 702         _listView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
 703         _listView.setFocusTraversable(false);
 704 
 705         _listView.getSelectionModel().selectedIndexProperty().addListener(o -> {
 706             if (listSelectionLock) return;
 707             int index = listView.getSelectionModel().getSelectedIndex();
 708             comboBox.getSelectionModel().select(index);
 709             updateDisplayNode();
 710             comboBox.notifyAccessibleAttributeChanged(AccessibleAttribute.TEXT);
 711         });
 712          
 713         comboBox.getSelectionModel().selectedItemProperty().addListener(o -> {
 714             listViewSelectionDirty = true;
 715         });
 716         
 717         _listView.addEventFilter(MouseEvent.MOUSE_RELEASED, t -> {
 718             // RT-18672: Without checking if the user is clicking in the
 719             // scrollbar area of the ListView, the comboBox will hide. Therefore,
 720             // we add the check below to prevent this from happening.
 721             EventTarget target = t.getTarget();
 722             if (target instanceof Parent) {
 723                 List<String> s = ((Parent) target).getStyleClass();
 724                 if (s.contains("thumb")
 725                         || s.contains("track")
 726                         || s.contains("decrement-arrow")
 727                         || s.contains("increment-arrow")) {
 728                     return;
 729                 }
 730             }
 731 
 732             if (isHideOnClickEnabled()) {
 733                 comboBox.hide();
 734             }
 735         });
 736 
 737         _listView.setOnKeyPressed(t -> {
 738             // TODO move to behavior, when (or if) this class becomes a SkinBase
 739             if (t.getCode() == KeyCode.ENTER ||
 740                     t.getCode() == KeyCode.SPACE ||
 741                     t.getCode() == KeyCode.ESCAPE) {
 742                 comboBox.hide();
 743             }
 744         });
 745         
 746         return _listView;
 747     }
 748     
 749     private double getListViewPrefHeight() {
 750         double ph;
 751         if (listView.getSkin() instanceof VirtualContainerBase) {
 752             int maxRows = comboBox.getVisibleRowCount();
 753             VirtualContainerBase<?,?,?> skin = (VirtualContainerBase<?,?,?>)listView.getSkin();
 754             ph = skin.getVirtualFlowPreferredHeight(maxRows);
 755         } else {
 756             double ch = comboBoxItems.size() * 25;
 757             ph = Math.min(ch, 200);
 758         }
 759         
 760         return ph;
 761     }
 762 
 763 
 764     
 765     /**************************************************************************
 766      * 
 767      * API for testing
 768      * 
 769      *************************************************************************/
 770     
 771     public ListView<T> getListView() {
 772         return listView;
 773     }
 774     
 775     
 776     
 777 
 778     /***************************************************************************
 779      *                                                                         *
 780      * Stylesheet Handling                                                     *
 781      *                                                                         *
 782      **************************************************************************/
 783     
 784     private static PseudoClass CONTAINS_FOCUS_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("contains-focus");    
 785     
 786     // These three pseudo class states are duplicated from Cell
 787     private static final PseudoClass PSEUDO_CLASS_SELECTED =
 788             PseudoClass.getPseudoClass("selected");
 789     private static final PseudoClass PSEUDO_CLASS_EMPTY =
 790             PseudoClass.getPseudoClass("empty");
 791     private static final PseudoClass PSEUDO_CLASS_FILLED =
 792             PseudoClass.getPseudoClass("filled");
 793 
 794 
 795 
 796     /***************************************************************************
 797      *                                                                         *
 798      * Support classes                                                         *
 799      *                                                                         *
 800      **************************************************************************/
 801 
 802     public static final class FakeFocusTextField extends TextField {
 803 
 804         public void setFakeFocus(boolean b) {
 805             setFocused(b);
 806         }
 807 
 808         @Override
 809         public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 810             switch (attribute) {
 811                 case FOCUS_ITEM: 
 812                     /* Internally comboBox reassign its focus the text field.
 813                      * For the accessibility perspective it is more meaningful
 814                      * if the focus stays with the comboBox control.
 815                      */
 816                     return getParent();
 817                 default: return super.queryAccessibleAttribute(attribute, parameters);
 818             }
 819         }
 820     }
 821 
 822     @Override
 823     public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 824         switch (attribute) {
 825             case FOCUS_ITEM: {
 826                 if (comboBox.isShowing()) {
 827                     /* On Mac, for some reason, changing the selection on the list is not
 828                      * reported by VoiceOver the first time it shows.
 829                      * Note that this fix returns a child of the PopupWindow back to the main
 830                      * Stage, which doesn't seem to cause problems.
 831                      */
 832                     return listView.queryAccessibleAttribute(attribute, parameters);
 833                 }
 834                 return null;
 835             }
 836             case TEXT: {
 837                 String accText = comboBox.getAccessibleText();
 838                 if (accText != null && !accText.isEmpty()) return accText;
 839                 String title = comboBox.isEditable() ? textField.getText() : buttonCell.getText();
 840                 if (title == null || title.isEmpty()) {
 841                     title = comboBox.getPromptText();
 842                 }
 843                 return title;
 844             }
 845             case SELECTION_START: return textField.getSelection().getStart();
 846             case SELECTION_END: return textField.getSelection().getEnd();
 847             default: return super.queryAccessibleAttribute(attribute, parameters);
 848         }
 849     }
 850 }
 851