--- old/modules/controls/src/main/java/com/sun/javafx/scene/control/skin/ComboBoxPopupControl.java 2015-09-03 15:18:58.130091900 -0700 +++ /dev/null 2015-09-03 15:18:59.000000000 -0700 @@ -1,553 +0,0 @@ -/* - * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * 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 javafx.beans.value.ObservableValue; -import javafx.css.PseudoClass; -import javafx.css.Styleable; -import javafx.geometry.*; -import javafx.scene.control.*; -import com.sun.javafx.scene.control.behavior.ComboBoxBaseBehavior; -import com.sun.javafx.scene.input.ExtendedInputMethodRequests; -import com.sun.javafx.scene.traversal.Algorithm; -import com.sun.javafx.scene.traversal.Direction; -import com.sun.javafx.scene.traversal.ParentTraversalEngine; -import com.sun.javafx.scene.traversal.TraversalContext; -import javafx.beans.InvalidationListener; -import javafx.event.EventHandler; -import javafx.scene.AccessibleAttribute; -import javafx.scene.Node; -import javafx.scene.input.DragEvent; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.Region; -import javafx.stage.WindowEvent; -import javafx.util.StringConverter; - -public abstract class ComboBoxPopupControl extends ComboBoxBaseSkin { - - protected PopupControl popup; - public static final String COMBO_BOX_STYLE_CLASS = "combo-box-popup"; - - private boolean popupNeedsReconfiguring = true; - - private final ComboBoxBase comboBoxBase; - private TextField textField; - - public ComboBoxPopupControl(ComboBoxBase comboBoxBase, final ComboBoxBaseBehavior behavior) { - super(comboBoxBase, behavior); - this.comboBoxBase = comboBoxBase; - - // editable input node - this.textField = getEditor() != null ? getEditableInputNode() : null; - - // Fix for RT-29565. Without this the textField does not have a correct - // pref width at startup, as it is not part of the scenegraph (and therefore - // has no pref width until after the first measurements have been taken). - if (this.textField != null) { - getChildren().add(textField); - } - - // move fake focus in to the textfield if the comboBox is editable - comboBoxBase.focusedProperty().addListener((ov, t, hasFocus) -> { - if (getEditor() != null) { - // Fix for the regression noted in a comment in RT-29885. - ((FakeFocusTextField)textField).setFakeFocus(hasFocus); - } - }); - - comboBoxBase.addEventFilter(KeyEvent.ANY, ke -> { - if (textField == null || getEditor() == null) { - handleKeyEvent(ke, false); - } else { - // This prevents a stack overflow from our rebroadcasting of the - // event to the textfield that occurs in the final else statement - // of the conditions below. - if (ke.getTarget().equals(textField)) return; - - switch (ke.getCode()) { - case ESCAPE: - case F10: - // Allow to bubble up. - break; - - case ENTER: - handleKeyEvent(ke, true); - break; - - default: - // Fix for the regression noted in a comment in RT-29885. - // This forwards the event down into the TextField when - // the key event is actually received by the ComboBox. - textField.fireEvent(ke.copyFor(textField, textField)); - ke.consume(); - } - } - }); - - // RT-38978: Forward input method events to TextField if editable. - if (comboBoxBase.getOnInputMethodTextChanged() == null) { - comboBoxBase.setOnInputMethodTextChanged(event -> { - if (textField != null && getEditor() != null && comboBoxBase.getScene().getFocusOwner() == comboBoxBase) { - if (textField.getOnInputMethodTextChanged() != null) { - textField.getOnInputMethodTextChanged().handle(event); - } - } - }); - } - - // Fix for RT-36902, where focus traversal was getting stuck inside the ComboBox - comboBoxBase.setImpl_traversalEngine(new ParentTraversalEngine(comboBoxBase, new Algorithm() { - @Override public Node select(Node owner, Direction dir, TraversalContext context) { - return null; - } - - @Override public Node selectFirst(TraversalContext context) { - return null; - } - - @Override public Node selectLast(TraversalContext context) { - return null; - } - })); - - updateEditable(); - } - - /** - * This method should return the Node that will be displayed when the user - * clicks on the ComboBox 'button' area. - */ - protected abstract Node getPopupContent(); - - protected PopupControl getPopup() { - if (popup == null) { - createPopup(); - } - return popup; - } - - @Override public void show() { - if (getSkinnable() == null) { - throw new IllegalStateException("ComboBox is null"); - } - - Node content = getPopupContent(); - if (content == null) { - throw new IllegalStateException("Popup node is null"); - } - - if (getPopup().isShowing()) return; - - positionAndShowPopup(); - } - - @Override public void hide() { - if (popup != null && popup.isShowing()) { - popup.hide(); - } - } - - private Point2D getPrefPopupPosition() { - return com.sun.javafx.util.Utils.pointRelativeTo(getSkinnable(), getPopupContent(), HPos.CENTER, VPos.BOTTOM, 0, 0, true); - } - - private void positionAndShowPopup() { - final PopupControl _popup = getPopup(); - _popup.getScene().setNodeOrientation(getSkinnable().getEffectiveNodeOrientation()); - - - final Node popupContent = getPopupContent(); - sizePopup(); - - Point2D p = getPrefPopupPosition(); - - popupNeedsReconfiguring = true; - reconfigurePopup(); - - final ComboBoxBase comboBoxBase = getSkinnable(); - _popup.show(comboBoxBase.getScene().getWindow(), - snapPosition(p.getX()), - snapPosition(p.getY())); - - popupContent.requestFocus(); - - // second call to sizePopup here to enable proper sizing _after_ the popup - // has been displayed. See RT-37622 for more detail. - sizePopup(); - } - - private void sizePopup() { - final Node popupContent = getPopupContent(); - - if (popupContent instanceof Region) { - // snap to pixel - final Region r = (Region) popupContent; - - // 0 is used here for the width due to RT-46097 - double prefHeight = snapSize(r.prefHeight(0)); - double minHeight = snapSize(r.minHeight(0)); - double maxHeight = snapSize(r.maxHeight(0)); - double h = snapSize(Math.min(Math.max(prefHeight, minHeight), Math.max(minHeight, maxHeight))); - - double prefWidth = snapSize(r.prefWidth(h)); - double minWidth = snapSize(r.minWidth(h)); - double maxWidth = snapSize(r.maxWidth(h)); - double w = snapSize(Math.min(Math.max(prefWidth, minWidth), Math.max(minWidth, maxWidth))); - - popupContent.resize(w, h); - } else { - popupContent.autosize(); - } - } - - private void createPopup() { - popup = new PopupControl() { - - @Override public Styleable getStyleableParent() { - return ComboBoxPopupControl.this.getSkinnable(); - } - { - setSkin(new Skin() { - @Override public Skinnable getSkinnable() { return ComboBoxPopupControl.this.getSkinnable(); } - @Override public Node getNode() { return getPopupContent(); } - @Override public void dispose() { } - }); - } - - }; - popup.getStyleClass().add(COMBO_BOX_STYLE_CLASS); - popup.setConsumeAutoHidingEvents(false); - popup.setAutoHide(true); - popup.setAutoFix(true); - popup.setHideOnEscape(true); - popup.setOnAutoHide(e -> { - getBehavior().onAutoHide(); - }); - popup.addEventHandler(MouseEvent.MOUSE_CLICKED, t -> { - // RT-18529: We listen to mouse input that is received by the popup - // but that is not consumed, and assume that this is due to the mouse - // clicking outside of the node, but in areas such as the - // dropshadow. - getBehavior().onAutoHide(); - }); - popup.addEventHandler(WindowEvent.WINDOW_HIDDEN, t -> { - // Make sure the accessibility focus returns to the combo box - // after the window closes. - getSkinnable().notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_NODE); - }); - - // Fix for RT-21207 - InvalidationListener layoutPosListener = o -> { - popupNeedsReconfiguring = true; - reconfigurePopup(); - }; - getSkinnable().layoutXProperty().addListener(layoutPosListener); - getSkinnable().layoutYProperty().addListener(layoutPosListener); - getSkinnable().widthProperty().addListener(layoutPosListener); - getSkinnable().heightProperty().addListener(layoutPosListener); - - // RT-36966 - if skinnable's scene becomes null, ensure popup is closed - getSkinnable().sceneProperty().addListener(o -> { - if (((ObservableValue)o).getValue() == null) { - hide(); - } - }); - - } - - void reconfigurePopup() { - // RT-26861. Don't call getPopup() here because it may cause the popup - // to be created too early, which leads to memory leaks like those noted - // in RT-32827. - if (popup == null) return; - - final boolean isShowing = popup.isShowing(); - if (! isShowing) return; - - if (! popupNeedsReconfiguring) return; - popupNeedsReconfiguring = false; - - final Point2D p = getPrefPopupPosition(); - - final Node popupContent = getPopupContent(); - final double minWidth = popupContent.prefWidth(Region.USE_COMPUTED_SIZE); - final double minHeight = popupContent.prefHeight(Region.USE_COMPUTED_SIZE); - - if (p.getX() > -1) popup.setAnchorX(p.getX()); - if (p.getY() > -1) popup.setAnchorY(p.getY()); - if (minWidth > -1) popup.setMinWidth(minWidth); - if (minHeight > -1) popup.setMinHeight(minHeight); - - final Bounds b = popupContent.getLayoutBounds(); - final double currentWidth = b.getWidth(); - final double currentHeight = b.getHeight(); - final double newWidth = currentWidth < minWidth ? minWidth : currentWidth; - final double newHeight = currentHeight < minHeight ? minHeight : currentHeight; - - if (newWidth != currentWidth || newHeight != currentHeight) { - // Resizing content to resolve issues such as RT-32582 and RT-33700 - // (where RT-33700 was introduced due to a previous fix for RT-32582) - popupContent.resize(newWidth, newHeight); - if (popupContent instanceof Region) { - ((Region)popupContent).setMinSize(newWidth, newHeight); - ((Region)popupContent).setPrefSize(newWidth, newHeight); - } - } - } - - - - - - /*************************************************************************** - * * - * TextField Listeners * - * * - **************************************************************************/ - - private EventHandler textFieldMouseEventHandler = event -> { - ComboBoxBase comboBoxBase = getSkinnable(); - if (!event.getTarget().equals(comboBoxBase)) { - comboBoxBase.fireEvent(event.copyFor(comboBoxBase, comboBoxBase)); - event.consume(); - } - }; - private EventHandler textFieldDragEventHandler = event -> { - ComboBoxBase comboBoxBase = getSkinnable(); - if (!event.getTarget().equals(comboBoxBase)) { - comboBoxBase.fireEvent(event.copyFor(comboBoxBase, comboBoxBase)); - event.consume(); - } - }; - - - /** - * Subclasses are responsible for getting the editor. This will be removed - * in FX 9 when the editor property is moved up to ComboBoxBase. - * - * Note: ComboBoxListViewSkin should return null if editable is false, even - * if the ComboBox does have an editor set. - */ - protected abstract TextField getEditor(); - - /** - * Subclasses are responsible for getting the converter. This will be - * removed in FX 9 when the converter property is moved up to ComboBoxBase. - */ - protected abstract StringConverter getConverter(); - - private String initialTextFieldValue = null; - protected TextField getEditableInputNode() { - if (textField == null && getEditor() != null) { - textField = getEditor(); - textField.setFocusTraversable(false); - textField.promptTextProperty().bind(comboBoxBase.promptTextProperty()); - textField.tooltipProperty().bind(comboBoxBase.tooltipProperty()); - - // Fix for RT-21406: ComboBox do not show initial text value - initialTextFieldValue = textField.getText(); - // End of fix (see updateDisplayNode below for the related code) - } - - return textField; - } - - protected void setTextFromTextFieldIntoComboBoxValue() { - if (getEditor() != null) { - StringConverter c = getConverter(); - if (c != null) { - T oldValue = comboBoxBase.getValue(); - T value = oldValue; - String text = textField.getText(); - - // conditional check here added due to RT-28245 - if (oldValue == null && (text == null || text.isEmpty())) { - value = null; - } else { - try { - value = c.fromString(text); - } catch (Exception ex) { - // Most likely a parsing error, such as DateTimeParseException - } - } - - if ((value != null || oldValue != null) && (value == null || !value.equals(oldValue))) { - // no point updating values needlessly if they are the same - comboBoxBase.setValue(value); - } - - updateDisplayNode(); - } - } - } - - protected void updateDisplayNode() { - if (textField != null && getEditor() != null) { - T value = comboBoxBase.getValue(); - StringConverter c = getConverter(); - - if (initialTextFieldValue != null && ! initialTextFieldValue.isEmpty()) { - // Remainder of fix for RT-21406: ComboBox do not show initial text value - textField.setText(initialTextFieldValue); - initialTextFieldValue = null; - // end of fix - } else { - String stringValue = c.toString(value); - if (value == null || stringValue == null) { - textField.setText(""); - } else if (! stringValue.equals(textField.getText())) { - textField.setText(stringValue); - } - } - } - } - - - private void handleKeyEvent(KeyEvent ke, boolean doConsume) { - // When the user hits the enter or F4 keys, we respond before - // ever giving the event to the TextField. - if (ke.getCode() == KeyCode.ENTER) { - setTextFromTextFieldIntoComboBoxValue(); - - if (doConsume && comboBoxBase.getOnAction() != null) { - ke.consume(); - } else { - forwardToParent(ke); - } - } else if (ke.getCode() == KeyCode.F4) { - if (ke.getEventType() == KeyEvent.KEY_RELEASED) { - if (comboBoxBase.isShowing()) comboBoxBase.hide(); - else comboBoxBase.show(); - } - ke.consume(); // we always do a consume here (otherwise unit tests fail) - } - } - - private void forwardToParent(KeyEvent event) { - if (comboBoxBase.getParent() != null) { - comboBoxBase.getParent().fireEvent(event); - } - } - - protected void updateEditable() { - TextField newTextField = getEditor(); - - if (getEditor() == null) { - // remove event filters - if (textField != null) { - textField.removeEventFilter(MouseEvent.DRAG_DETECTED, textFieldMouseEventHandler); - textField.removeEventFilter(DragEvent.ANY, textFieldDragEventHandler); - - comboBoxBase.setInputMethodRequests(null); - } - } else if (newTextField != null) { - // add event filters - - // Fix for RT-31093 - drag events from the textfield were not surfacing - // properly for the ComboBox. - newTextField.addEventFilter(MouseEvent.DRAG_DETECTED, textFieldMouseEventHandler); - newTextField.addEventFilter(DragEvent.ANY, textFieldDragEventHandler); - - // RT-38978: Forward input method requests to TextField. - comboBoxBase.setInputMethodRequests(new ExtendedInputMethodRequests() { - @Override public Point2D getTextLocation(int offset) { - return newTextField.getInputMethodRequests().getTextLocation(offset); - } - - @Override public int getLocationOffset(int x, int y) { - return newTextField.getInputMethodRequests().getLocationOffset(x, y); - } - - @Override public void cancelLatestCommittedText() { - newTextField.getInputMethodRequests().cancelLatestCommittedText(); - } - - @Override public String getSelectedText() { - return newTextField.getInputMethodRequests().getSelectedText(); - } - - @Override public int getInsertPositionOffset() { - return ((ExtendedInputMethodRequests)newTextField.getInputMethodRequests()).getInsertPositionOffset(); - } - - @Override public String getCommittedText(int begin, int end) { - return ((ExtendedInputMethodRequests)newTextField.getInputMethodRequests()).getCommittedText(begin, end); - } - - @Override public int getCommittedTextLength() { - return ((ExtendedInputMethodRequests)newTextField.getInputMethodRequests()).getCommittedTextLength(); - } - }); - } - - textField = newTextField; - } - - /*************************************************************************** - * * - * Support classes * - * * - **************************************************************************/ - - public static final class FakeFocusTextField extends TextField { - - @Override public void requestFocus() { - if (getParent() != null) { - getParent().requestFocus(); - } - } - - public void setFakeFocus(boolean b) { - setFocused(b); - } - - @Override - public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { - switch (attribute) { - case FOCUS_ITEM: - /* Internally comboBox reassign its focus the text field. - * For the accessibility perspective it is more meaningful - * if the focus stays with the comboBox control. - */ - return getParent(); - default: return super.queryAccessibleAttribute(attribute, parameters); - } - } - } - - - - /*************************************************************************** - * * - * Stylesheet Handling * - * * - **************************************************************************/ - - private static PseudoClass CONTAINS_FOCUS_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("contains-focus"); - -} --- /dev/null 2015-09-03 15:18:59.000000000 -0700 +++ new/modules/controls/src/main/java/javafx/scene/control/skin/ComboBoxPopupControl.java 2015-09-03 15:18:57.415051000 -0700 @@ -0,0 +1,586 @@ +/* + * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 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.FakeFocusTextField; +import com.sun.javafx.scene.control.Properties; +import com.sun.javafx.scene.input.ExtendedInputMethodRequests; +import com.sun.javafx.scene.traversal.Algorithm; +import com.sun.javafx.scene.traversal.Direction; +import com.sun.javafx.scene.traversal.ParentTraversalEngine; +import com.sun.javafx.scene.traversal.TraversalContext; +import javafx.beans.InvalidationListener; +import javafx.beans.value.ObservableValue; +import javafx.css.PseudoClass; +import javafx.css.Styleable; +import javafx.event.EventHandler; +import javafx.geometry.Bounds; +import javafx.geometry.HPos; +import javafx.geometry.Point2D; +import javafx.geometry.VPos; +import javafx.scene.AccessibleAttribute; +import javafx.scene.Node; +import javafx.scene.control.ComboBoxBase; +import javafx.scene.control.PopupControl; +import javafx.scene.control.Skin; +import javafx.scene.control.Skinnable; +import javafx.scene.control.TextField; +import javafx.scene.input.DragEvent; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Region; +import javafx.stage.WindowEvent; +import javafx.util.StringConverter; + +/** + * An abstract class that extends the functionality of {@link ComboBoxBaseSkin} + * to include API related to showing ComboBox-like controls as popups. + * + * @param The type of the ComboBox-like control. + * @since 9 + */ +public abstract class ComboBoxPopupControl extends ComboBoxBaseSkin { + + /*************************************************************************** + * * + * Private fields * + * * + **************************************************************************/ + + PopupControl popup; + + private boolean popupNeedsReconfiguring = true; + + private final ComboBoxBase comboBoxBase; + private TextField textField; + + private String initialTextFieldValue = null; + + + + /*************************************************************************** + * * + * TextField Listeners * + * * + **************************************************************************/ + + private EventHandler textFieldMouseEventHandler = event -> { + ComboBoxBase comboBoxBase = getSkinnable(); + if (!event.getTarget().equals(comboBoxBase)) { + comboBoxBase.fireEvent(event.copyFor(comboBoxBase, comboBoxBase)); + event.consume(); + } + }; + private EventHandler textFieldDragEventHandler = event -> { + ComboBoxBase comboBoxBase = getSkinnable(); + if (!event.getTarget().equals(comboBoxBase)) { + comboBoxBase.fireEvent(event.copyFor(comboBoxBase, comboBoxBase)); + event.consume(); + } + }; + + + + /*************************************************************************** + * * + * Constructors * + * * + **************************************************************************/ + + /** + * Creates a new instance of ComboBoxPopupControl, although note that this + * instance does not handle any behavior / input mappings - this needs to be + * handled appropriately by subclasses. + * + * @param control The control that this skin should be installed onto. + */ + public ComboBoxPopupControl(ComboBoxBase control) { + super(control); + this.comboBoxBase = control; + + // editable input node + this.textField = getEditor() != null ? getEditableInputNode() : null; + + // Fix for RT-29565. Without this the textField does not have a correct + // pref width at startup, as it is not part of the scenegraph (and therefore + // has no pref width until after the first measurements have been taken). + if (this.textField != null) { + getChildren().add(textField); + } + + // move fake focus in to the textfield if the comboBox is editable + comboBoxBase.focusedProperty().addListener((ov, t, hasFocus) -> { + if (getEditor() != null) { + // Fix for the regression noted in a comment in RT-29885. + ((FakeFocusTextField)textField).setFakeFocus(hasFocus); + } + }); + + comboBoxBase.addEventFilter(KeyEvent.ANY, ke -> { + if (textField == null || getEditor() == null) { + handleKeyEvent(ke, false); + } else { + // This prevents a stack overflow from our rebroadcasting of the + // event to the textfield that occurs in the final else statement + // of the conditions below. + if (ke.getTarget().equals(textField)) return; + + switch (ke.getCode()) { + case ESCAPE: + case F10: + // Allow to bubble up. + break; + + case ENTER: + handleKeyEvent(ke, true); + break; + + default: + // Fix for the regression noted in a comment in RT-29885. + // This forwards the event down into the TextField when + // the key event is actually received by the ComboBox. + textField.fireEvent(ke.copyFor(textField, textField)); + ke.consume(); + } + } + }); + + // RT-38978: Forward input method events to TextField if editable. + if (comboBoxBase.getOnInputMethodTextChanged() == null) { + comboBoxBase.setOnInputMethodTextChanged(event -> { + if (textField != null && getEditor() != null && comboBoxBase.getScene().getFocusOwner() == comboBoxBase) { + if (textField.getOnInputMethodTextChanged() != null) { + textField.getOnInputMethodTextChanged().handle(event); + } + } + }); + } + + // Fix for RT-36902, where focus traversal was getting stuck inside the ComboBox + comboBoxBase.setImpl_traversalEngine(new ParentTraversalEngine(comboBoxBase, new Algorithm() { + @Override public Node select(Node owner, Direction dir, TraversalContext context) { + return null; + } + + @Override public Node selectFirst(TraversalContext context) { + return null; + } + + @Override public Node selectLast(TraversalContext context) { + return null; + } + })); + + updateEditable(); + } + + + + /*************************************************************************** + * * + * Public API * + * * + **************************************************************************/ + + /** + * This method should return the Node that will be displayed when the user + * clicks on the ComboBox 'button' area. + */ + protected abstract Node getPopupContent(); + + /** + * Subclasses are responsible for getting the editor. This will be removed + * in FX 9 when the editor property is moved up to ComboBoxBase with + * JDK-8130354 + * + * Note: ComboBoxListViewSkin should return null if editable is false, even + * if the ComboBox does have an editor set. + */ + protected abstract TextField getEditor(); + + /** + * Subclasses are responsible for getting the converter. This will be + * removed in FX 9 when the converter property is moved up to ComboBoxBase + * with JDK-8130354. + */ + protected abstract StringConverter getConverter(); + + /** {@inheritDoc} */ + @Override public void show() { + if (getSkinnable() == null) { + throw new IllegalStateException("ComboBox is null"); + } + + Node content = getPopupContent(); + if (content == null) { + throw new IllegalStateException("Popup node is null"); + } + + if (getPopup().isShowing()) return; + + positionAndShowPopup(); + } + + /** {@inheritDoc} */ + @Override public void hide() { + if (popup != null && popup.isShowing()) { + popup.hide(); + } + } + + + + /*************************************************************************** + * * + * Private implementation * + * * + **************************************************************************/ + + PopupControl getPopup() { + if (popup == null) { + createPopup(); + } + return popup; + } + + TextField getEditableInputNode() { + if (textField == null && getEditor() != null) { + textField = getEditor(); + textField.setFocusTraversable(false); + textField.promptTextProperty().bind(comboBoxBase.promptTextProperty()); + textField.tooltipProperty().bind(comboBoxBase.tooltipProperty()); + + // Fix for RT-21406: ComboBox do not show initial text value + initialTextFieldValue = textField.getText(); + // End of fix (see updateDisplayNode below for the related code) + } + + return textField; + } + + void setTextFromTextFieldIntoComboBoxValue() { + if (getEditor() != null) { + StringConverter c = getConverter(); + if (c != null) { + T oldValue = comboBoxBase.getValue(); + T value = oldValue; + String text = textField.getText(); + + // conditional check here added due to RT-28245 + if (oldValue == null && (text == null || text.isEmpty())) { + value = null; + } else { + try { + value = c.fromString(text); + } catch (Exception ex) { + // Most likely a parsing error, such as DateTimeParseException + } + } + + if ((value != null || oldValue != null) && (value == null || !value.equals(oldValue))) { + // no point updating values needlessly if they are the same + comboBoxBase.setValue(value); + } + + updateDisplayNode(); + } + } + } + + void updateDisplayNode() { + if (textField != null && getEditor() != null) { + T value = comboBoxBase.getValue(); + StringConverter c = getConverter(); + + if (initialTextFieldValue != null && ! initialTextFieldValue.isEmpty()) { + // Remainder of fix for RT-21406: ComboBox do not show initial text value + textField.setText(initialTextFieldValue); + initialTextFieldValue = null; + // end of fix + } else { + String stringValue = c.toString(value); + if (value == null || stringValue == null) { + textField.setText(""); + } else if (! stringValue.equals(textField.getText())) { + textField.setText(stringValue); + } + } + } + } + + void updateEditable() { + TextField newTextField = getEditor(); + + if (getEditor() == null) { + // remove event filters + if (textField != null) { + textField.removeEventFilter(MouseEvent.DRAG_DETECTED, textFieldMouseEventHandler); + textField.removeEventFilter(DragEvent.ANY, textFieldDragEventHandler); + + comboBoxBase.setInputMethodRequests(null); + } + } else if (newTextField != null) { + // add event filters + + // Fix for RT-31093 - drag events from the textfield were not surfacing + // properly for the ComboBox. + newTextField.addEventFilter(MouseEvent.DRAG_DETECTED, textFieldMouseEventHandler); + newTextField.addEventFilter(DragEvent.ANY, textFieldDragEventHandler); + + // RT-38978: Forward input method requests to TextField. + comboBoxBase.setInputMethodRequests(new ExtendedInputMethodRequests() { + @Override public Point2D getTextLocation(int offset) { + return newTextField.getInputMethodRequests().getTextLocation(offset); + } + + @Override public int getLocationOffset(int x, int y) { + return newTextField.getInputMethodRequests().getLocationOffset(x, y); + } + + @Override public void cancelLatestCommittedText() { + newTextField.getInputMethodRequests().cancelLatestCommittedText(); + } + + @Override public String getSelectedText() { + return newTextField.getInputMethodRequests().getSelectedText(); + } + + @Override public int getInsertPositionOffset() { + return ((ExtendedInputMethodRequests)newTextField.getInputMethodRequests()).getInsertPositionOffset(); + } + + @Override public String getCommittedText(int begin, int end) { + return ((ExtendedInputMethodRequests)newTextField.getInputMethodRequests()).getCommittedText(begin, end); + } + + @Override public int getCommittedTextLength() { + return ((ExtendedInputMethodRequests)newTextField.getInputMethodRequests()).getCommittedTextLength(); + } + }); + } + + textField = newTextField; + } + + private Point2D getPrefPopupPosition() { + return com.sun.javafx.util.Utils.pointRelativeTo(getSkinnable(), getPopupContent(), HPos.CENTER, VPos.BOTTOM, 0, 0, true); + } + + private void positionAndShowPopup() { + final PopupControl _popup = getPopup(); + _popup.getScene().setNodeOrientation(getSkinnable().getEffectiveNodeOrientation()); + + + final Node popupContent = getPopupContent(); + sizePopup(); + + Point2D p = getPrefPopupPosition(); + + popupNeedsReconfiguring = true; + reconfigurePopup(); + + final ComboBoxBase comboBoxBase = getSkinnable(); + _popup.show(comboBoxBase.getScene().getWindow(), + snapPosition(p.getX()), + snapPosition(p.getY())); + + popupContent.requestFocus(); + + // second call to sizePopup here to enable proper sizing _after_ the popup + // has been displayed. See RT-37622 for more detail. + sizePopup(); + } + + private void sizePopup() { + final Node popupContent = getPopupContent(); + + if (popupContent instanceof Region) { + // snap to pixel + final Region r = (Region) popupContent; + + // 0 is used here for the width due to RT-46097 + double prefHeight = snapSize(r.prefHeight(0)); + double minHeight = snapSize(r.minHeight(0)); + double maxHeight = snapSize(r.maxHeight(0)); + double h = snapSize(Math.min(Math.max(prefHeight, minHeight), Math.max(minHeight, maxHeight))); + + double prefWidth = snapSize(r.prefWidth(h)); + double minWidth = snapSize(r.minWidth(h)); + double maxWidth = snapSize(r.maxWidth(h)); + double w = snapSize(Math.min(Math.max(prefWidth, minWidth), Math.max(minWidth, maxWidth))); + + popupContent.resize(w, h); + } else { + popupContent.autosize(); + } + } + + private void createPopup() { + popup = new PopupControl() { + @Override public Styleable getStyleableParent() { + return ComboBoxPopupControl.this.getSkinnable(); + } + { + setSkin(new Skin() { + @Override public Skinnable getSkinnable() { return ComboBoxPopupControl.this.getSkinnable(); } + @Override public Node getNode() { return getPopupContent(); } + @Override public void dispose() { } + }); + } + }; + popup.getStyleClass().add(Properties.COMBO_BOX_STYLE_CLASS); + popup.setConsumeAutoHidingEvents(false); + popup.setAutoHide(true); + popup.setAutoFix(true); + popup.setHideOnEscape(true); + popup.setOnAutoHide(e -> getBehavior().onAutoHide(popup)); + popup.addEventHandler(MouseEvent.MOUSE_CLICKED, t -> { + // RT-18529: We listen to mouse input that is received by the popup + // but that is not consumed, and assume that this is due to the mouse + // clicking outside of the node, but in areas such as the + // dropshadow. + getBehavior().onAutoHide(popup); + }); + popup.addEventHandler(WindowEvent.WINDOW_HIDDEN, t -> { + // Make sure the accessibility focus returns to the combo box + // after the window closes. + getSkinnable().notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_NODE); + }); + + // Fix for RT-21207 + InvalidationListener layoutPosListener = o -> { + popupNeedsReconfiguring = true; + reconfigurePopup(); + }; + getSkinnable().layoutXProperty().addListener(layoutPosListener); + getSkinnable().layoutYProperty().addListener(layoutPosListener); + getSkinnable().widthProperty().addListener(layoutPosListener); + getSkinnable().heightProperty().addListener(layoutPosListener); + + // RT-36966 - if skinnable's scene becomes null, ensure popup is closed + getSkinnable().sceneProperty().addListener(o -> { + if (((ObservableValue)o).getValue() == null) { + hide(); + } + }); + + } + + void reconfigurePopup() { + // RT-26861. Don't call getPopup() here because it may cause the popup + // to be created too early, which leads to memory leaks like those noted + // in RT-32827. + if (popup == null) return; + + final boolean isShowing = popup.isShowing(); + if (! isShowing) return; + + if (! popupNeedsReconfiguring) return; + popupNeedsReconfiguring = false; + + final Point2D p = getPrefPopupPosition(); + + final Node popupContent = getPopupContent(); + final double minWidth = popupContent.prefWidth(Region.USE_COMPUTED_SIZE); + final double minHeight = popupContent.prefHeight(Region.USE_COMPUTED_SIZE); + + if (p.getX() > -1) popup.setAnchorX(p.getX()); + if (p.getY() > -1) popup.setAnchorY(p.getY()); + if (minWidth > -1) popup.setMinWidth(minWidth); + if (minHeight > -1) popup.setMinHeight(minHeight); + + final Bounds b = popupContent.getLayoutBounds(); + final double currentWidth = b.getWidth(); + final double currentHeight = b.getHeight(); + final double newWidth = currentWidth < minWidth ? minWidth : currentWidth; + final double newHeight = currentHeight < minHeight ? minHeight : currentHeight; + + if (newWidth != currentWidth || newHeight != currentHeight) { + // Resizing content to resolve issues such as RT-32582 and RT-33700 + // (where RT-33700 was introduced due to a previous fix for RT-32582) + popupContent.resize(newWidth, newHeight); + if (popupContent instanceof Region) { + ((Region)popupContent).setMinSize(newWidth, newHeight); + ((Region)popupContent).setPrefSize(newWidth, newHeight); + } + } + } + + private void handleKeyEvent(KeyEvent ke, boolean doConsume) { + // When the user hits the enter or F4 keys, we respond before + // ever giving the event to the TextField. + if (ke.getCode() == KeyCode.ENTER) { + setTextFromTextFieldIntoComboBoxValue(); + + if (doConsume && comboBoxBase.getOnAction() != null) { + ke.consume(); + } else { + forwardToParent(ke); + } + } else if (ke.getCode() == KeyCode.F4) { + if (ke.getEventType() == KeyEvent.KEY_RELEASED) { + if (comboBoxBase.isShowing()) comboBoxBase.hide(); + else comboBoxBase.show(); + } + ke.consume(); // we always do a consume here (otherwise unit tests fail) + } else if (ke.getCode() == KeyCode.F10 || ke.getCode() == KeyCode.ESCAPE) { + // RT-23275: The TextField fires F10 and ESCAPE key events + // up to the parent, which are then fired back at the + // TextField, and this ends up in an infinite loop until + // the stack overflows. So, here we consume these two + // events and stop them from going any further. + if (doConsume) ke.consume(); + } + } + + private void forwardToParent(KeyEvent event) { + if (comboBoxBase.getParent() != null) { + comboBoxBase.getParent().fireEvent(event); + } + } + + + + /*************************************************************************** + * * + * Support classes * + * * + **************************************************************************/ + + + + + + /*************************************************************************** + * * + * Stylesheet Handling * + * * + **************************************************************************/ + + private static PseudoClass CONTAINS_FOCUS_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("contains-focus"); + +}