modules/controls/src/main/java/javafx/scene/control/skin/ComboBoxListViewSkin.java

Print this page
rev 9240 : 8076423: JEP 253: Prepare JavaFX UI Controls & CSS APIs for Modularization

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