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);
- }
- }
- }
/***************************************************************************
* *