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

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

@@ -21,48 +21,108 @@
  * 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;
+package 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.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 <T> The type of the ComboBox-like control.
+ * @since 9
+ */
 public abstract class ComboBoxPopupControl<T> extends ComboBoxBaseSkin<T> {
     
-    protected PopupControl popup;
-    public static final String COMBO_BOX_STYLE_CLASS = "combo-box-popup";
+    /***************************************************************************
+     *                                                                         *
+     * Private fields                                                          *
+     *                                                                         *
+     **************************************************************************/
+    
+    PopupControl popup;
 
     private boolean popupNeedsReconfiguring = true;
 
     private final ComboBoxBase<T> comboBoxBase;
     private TextField textField;
 
-    public ComboBoxPopupControl(ComboBoxBase<T> comboBoxBase, final ComboBoxBaseBehavior<T> behavior) {
-        super(comboBoxBase, behavior);
-        this.comboBoxBase = comboBoxBase;
+    private String initialTextFieldValue = null;
+
+
+
+    /***************************************************************************
+     *                                                                         *
+     * TextField Listeners                                                     *
+     *                                                                         *
+     **************************************************************************/
+
+    private EventHandler<MouseEvent> textFieldMouseEventHandler = event -> {
+        ComboBoxBase<T> comboBoxBase = getSkinnable();
+        if (!event.getTarget().equals(comboBoxBase)) {
+            comboBoxBase.fireEvent(event.copyFor(comboBoxBase, comboBoxBase));
+            event.consume();
+        }
+    };
+    private EventHandler<DragEvent> textFieldDragEventHandler = event -> {
+        ComboBoxBase<T> 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<T> 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

@@ -136,23 +196,42 @@
         }));
 
         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();
     
-    protected PopupControl getPopup() {
-        if (popup == null) {
-            createPopup();
-        }
-        return popup;
-    }
+    /**
+     * 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<T> getConverter();
 
+    /** {@inheritDoc} */
     @Override public void show() {
         if (getSkinnable() == null) {
             throw new IllegalStateException("ComboBox is null");
         }
         

@@ -164,16 +243,151 @@
         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<T> 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<T> 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() {

@@ -225,37 +439,33 @@
         }
     }
     
     private void createPopup() {
         popup = new PopupControl() {
-
             @Override public Styleable getStyleableParent() {
                 return ComboBoxPopupControl.this.getSkinnable();
             }
             {
                 setSkin(new Skin<Skinnable>() {
                     @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.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.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();
+            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);

@@ -318,118 +528,10 @@
                 ((Region)popupContent).setPrefSize(newWidth, newHeight);
             }
         }
     }
 
-
-
-
-
-    /***************************************************************************
-     *                                                                         *
-     * TextField Listeners                                                     *
-     *                                                                         *
-     **************************************************************************/
-    
-    private EventHandler<MouseEvent> textFieldMouseEventHandler = event -> {
-        ComboBoxBase<T> comboBoxBase = getSkinnable();
-        if (!event.getTarget().equals(comboBoxBase)) {
-            comboBoxBase.fireEvent(event.copyFor(comboBoxBase, comboBoxBase));
-            event.consume();
-        }
-    };
-    private EventHandler<DragEvent> textFieldDragEventHandler = event -> {
-        ComboBoxBase<T> 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<T> 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<T> 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<T> 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();

@@ -443,104 +545,35 @@
             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);
         }
     }
 
-    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);
-            }
-        }
-    }
 
 
 
     /***************************************************************************
      *                                                                         *