1 /* 2 * Copyright (c) 2010, 2015, 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