/*
* Copyright (c) 2010, 2019, 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.ParentHelper;
import com.sun.javafx.scene.control.FakeFocusTextField;
import com.sun.javafx.scene.control.Properties;
import com.sun.javafx.scene.control.behavior.TextInputControlBehavior;
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.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 ComboBoxBaseSkin
to include API related to showing ComboBox-like controls as popups. Type parameters: - <T> – The type of the ComboBox-like control.
Since: 9
/**
* 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 *
*
/***************************************************************************
* *
* Private fields *
* *
**************************************************************************/
PopupControl popup;
private boolean popupNeedsReconfiguring = true;
private final ComboBoxBase<T> comboBoxBase;
private TextField textField;
private String initialTextFieldValue = null;
*
TextField Listeners *
*
/***************************************************************************
* *
* 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.
Params: - control – The control that this skin should be installed onto.
/**
* 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
// 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
ParentHelper.setTraversalEngine(comboBoxBase,
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.
Returns: the Node that will be displayed when the user clicks on the
ComboBox 'button' area
/**
* This method should return the Node that will be displayed when the user
* clicks on the ComboBox 'button' area.
* @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.
Returns: the editor
/**
* 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.
* @return the editor
*/
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.
Returns: the string converter
/**
* 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.
* @return the string converter
*/
protected abstract StringConverter<T> getConverter();
{@inheritDoc} /** {@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} /** {@inheritDoc} */
@Override public void hide() {
if (popup != null && popup.isShowing()) {
popup.hide();
}
}
*
Private implementation *
*
/***************************************************************************
* *
* 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 JDK-8145515 - in short the ComboBox was firing the event down to
// the TextField, and then the TextField was firing it back up to the
// ComboBox, resulting in stack overflows.
textField.getProperties().put(TextInputControlBehavior.DISABLE_FORWARD_TO_PARENT, true);
// 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() {
final ComboBoxBase<T> comboBoxBase = getSkinnable();
if (comboBoxBase.getScene() == null) {
return;
}
final PopupControl _popup = getPopup();
_popup.getScene().setNodeOrientation(getSkinnable().getEffectiveNodeOrientation());
final Node popupContent = getPopupContent();
sizePopup();
Point2D p = getPrefPopupPosition();
popupNeedsReconfiguring = true;
reconfigurePopup();
_popup.show(comboBoxBase.getScene().getWindow(),
snapPositionX(p.getX()),
snapPositionY(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 = snapSizeY(r.prefHeight(0));
double minHeight = snapSizeY(r.minHeight(0));
double maxHeight = snapSizeY(r.maxHeight(0));
double h = snapSizeY(Math.min(Math.max(prefHeight, minHeight), Math.max(minHeight, maxHeight)));
double prefWidth = snapSizeX(r.prefWidth(h));
double minWidth = snapSizeX(r.minWidth(h));
double maxWidth = snapSizeX(r.maxWidth(h));
double w = snapSizeX(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<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);
});
// 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();
} else if (getSkinnable().isShowing()) {
show();
}
});
}
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 key, we respond before
// ever giving the event to the TextField.
if (ke.getCode() == KeyCode.ENTER) {
if (ke.isConsumed() || ke.getEventType() != KeyEvent.KEY_RELEASED) {
return;
}
setTextFromTextFieldIntoComboBoxValue();
if (doConsume && comboBoxBase.getOnAction() != null) {
ke.consume();
} else if (textField != null) {
textField.fireEvent(ke);
}
} 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();
}
}
/***************************************************************************
* *
* Support classes *
* *
**************************************************************************/
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
}