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

Print this page
rev 8726 : [mq]: rt40280

*** 24,53 **** */ package com.sun.javafx.scene.control.skin; import javafx.beans.value.ObservableValue; import javafx.css.Styleable; import javafx.geometry.*; import javafx.scene.control.*; import com.sun.javafx.scene.control.behavior.ComboBoxBaseBehavior; import javafx.beans.InvalidationListener; import javafx.scene.AccessibleAttribute; import javafx.scene.Node; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Region; import javafx.stage.WindowEvent; 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; ! public ComboBoxPopupControl(ComboBoxBase<T> comboBox, final ComboBoxBaseBehavior<T> behavior) { ! super(comboBox, behavior); } /** * This method should return the Node that will be displayed when the user * clicks on the ComboBox 'button' area. --- 24,131 ---- */ 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 ! // 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; ! ! // 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.
*** 226,231 **** --- 304,554 ---- ((Region)popupContent).setMinSize(newWidth, newHeight); ((Region)popupContent).setPrefSize(newWidth, newHeight); } } } + + + + + + /*************************************************************************** + * * + * TextField Listeners * + * * + **************************************************************************/ + + private EventHandler<KeyEvent> textFieldKeyEventHandler = event -> { + if (getEditor() != null && textField != null) { + handleKeyEvent(event, true); + } + }; + 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.focusTraversableProperty().bindBidirectional(comboBoxBase.focusTraversableProperty()); + 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) + + textField.focusedProperty().addListener((ov, t, hasFocus) -> { + if (getEditor() != null) { + // Fix for RT-29885 + comboBoxBase.getProperties().put("FOCUSED", hasFocus); + // --- end of RT-29885 + + // RT-21454 starts here + if (!hasFocus) { + setTextFromTextFieldIntoComboBoxValue(); + } + pseudoClassStateChanged(CONTAINS_FOCUS_PSEUDOCLASS_STATE, hasFocus); + // --- end of RT-21454 + } + }); + } + + 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 (/*DateTimeParseException*/ Exception ex) { + } + } + + if ((value == null && oldValue == null) || (value != null && value.equals(oldValue))) { + // no point updating values needlessly (as they are the same) + return; + } + + 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(); + + if (doConsume) ke.consume(); + } 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(); + } + } + + + protected void updateEditable() { + TextField newTextField = getEditor(); + + if (getEditor() == null) { + // remove event filters + if (textField != null) { + textField.removeEventFilter(KeyEvent.ANY, textFieldKeyEventHandler); + textField.removeEventFilter(MouseEvent.DRAG_DETECTED, textFieldMouseEventHandler); + textField.removeEventFilter(DragEvent.ANY, textFieldDragEventHandler); + + comboBoxBase.setInputMethodRequests(null); + } + } else if (newTextField != null) { + // add event filters + newTextField.addEventFilter(KeyEvent.ANY, textFieldKeyEventHandler); + + // 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 { + + 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"); + }