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,68 **** * 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<T> extends ComboBoxBaseSkin<T> { ! protected PopupControl popup; ! public static final String COMBO_BOX_STYLE_CLASS = "combo-box-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; // editable input node this.textField = getEditor() != null ? getEditableInputNode() : null; // Fix for RT-29565. Without this the textField does not have a correct --- 21,128 ---- * 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 <T> The type of the ComboBox-like control. + * @since 9 + */ public abstract class ComboBoxPopupControl<T> extends ComboBoxBaseSkin<T> { ! /*************************************************************************** ! * * ! * Private fields * ! * * ! **************************************************************************/ ! ! PopupControl popup; private boolean popupNeedsReconfiguring = true; private final ComboBoxBase<T> comboBoxBase; private TextField textField; ! 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,158 **** })); 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"); } --- 196,237 ---- })); 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<T> getConverter(); + /** {@inheritDoc} */ @Override public void show() { if (getSkinnable() == null) { throw new IllegalStateException("ComboBox is null"); }
*** 164,179 **** --- 243,393 ---- 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,261 **** } } 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.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); --- 439,471 ---- } } 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(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);
*** 318,435 **** ((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(); --- 528,537 ----
*** 443,546 **** 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); - } - } - } /*************************************************************************** * * --- 545,579 ---- 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 * * * **************************************************************************/ /*************************************************************************** * *