javafx.scene.control.skin.SpinnerSkin Maven / Gradle / Ivy
/*
* Copyright (c) 2014, 2021, 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.behavior.BehaviorBase;
import com.sun.javafx.scene.traversal.Algorithm;
import com.sun.javafx.scene.control.FakeFocusTextField;
import com.sun.javafx.scene.traversal.Direction;
import com.sun.javafx.scene.traversal.ParentTraversalEngine;
import javafx.scene.control.Accordion;
import javafx.scene.control.Button;
import javafx.scene.control.Control;
import javafx.scene.control.SkinBase;
import com.sun.javafx.scene.control.behavior.SpinnerBehavior;
import com.sun.javafx.scene.traversal.TraversalContext;
import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.HPos;
import javafx.geometry.VPos;
import javafx.scene.AccessibleAction;
import javafx.scene.AccessibleRole;
import javafx.scene.Node;
import javafx.scene.control.Spinner;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import java.util.List;
/**
* Default skin implementation for the {@link Spinner} control.
*
* @see Spinner
* @since 9
*/
public class SpinnerSkin extends SkinBase> {
/* *************************************************************************
* *
* Private fields *
* *
**************************************************************************/
private TextField textField;
private Region incrementArrow;
private StackPane incrementArrowButton;
private Region decrementArrow;
private StackPane decrementArrowButton;
// rather than create an private enum, lets just use an int, here's the important details:
private static final int ARROWS_ON_RIGHT_VERTICAL = 0;
private static final int ARROWS_ON_LEFT_VERTICAL = 1;
private static final int ARROWS_ON_RIGHT_HORIZONTAL = 2;
private static final int ARROWS_ON_LEFT_HORIZONTAL = 3;
private static final int SPLIT_ARROWS_VERTICAL = 4;
private static final int SPLIT_ARROWS_HORIZONTAL = 5;
private int layoutMode = 0;
private final SpinnerBehavior behavior;
/* *************************************************************************
* *
* Constructors *
* *
**************************************************************************/
/**
* Creates a new SpinnerSkin instance, installing the necessary child
* nodes into the Control {@link Control#getChildren() children} list, as
* well as the necessary input mappings for handling key, mouse, etc events.
*
* @param control The control that this skin should be installed onto.
*/
public SpinnerSkin(Spinner control) {
super(control);
// install default input map for the Button control
behavior = new SpinnerBehavior<>(control);
// control.setInputMap(behavior.getInputMap());
textField = control.getEditor();
getChildren().add(textField);
updateStyleClass();
control.getStyleClass().addListener((ListChangeListener) c -> updateStyleClass());
// increment / decrement arrows
incrementArrow = new Region();
incrementArrow.setFocusTraversable(false);
incrementArrow.getStyleClass().setAll("increment-arrow");
incrementArrow.setMaxWidth(Region.USE_PREF_SIZE);
incrementArrow.setMaxHeight(Region.USE_PREF_SIZE);
incrementArrow.setMouseTransparent(true);
incrementArrowButton = new StackPane() {
public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
switch (action) {
case FIRE: getSkinnable().increment(); break;
default: super.executeAccessibleAction(action, parameters);
}
}
};
incrementArrowButton.setAccessibleRole(AccessibleRole.INCREMENT_BUTTON);
incrementArrowButton.setFocusTraversable(false);
incrementArrowButton.getStyleClass().setAll("increment-arrow-button");
incrementArrowButton.getChildren().add(incrementArrow);
incrementArrowButton.setOnMousePressed(e -> {
getSkinnable().requestFocus();
behavior.startSpinning(true);
});
incrementArrowButton.setOnMouseReleased(e -> behavior.stopSpinning());
decrementArrow = new Region();
decrementArrow.setFocusTraversable(false);
decrementArrow.getStyleClass().setAll("decrement-arrow");
decrementArrow.setMaxWidth(Region.USE_PREF_SIZE);
decrementArrow.setMaxHeight(Region.USE_PREF_SIZE);
decrementArrow.setMouseTransparent(true);
decrementArrowButton = new StackPane() {
public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
switch (action) {
case FIRE: getSkinnable().decrement(); break;
default: super.executeAccessibleAction(action, parameters);
}
}
};
decrementArrowButton.setAccessibleRole(AccessibleRole.DECREMENT_BUTTON);
decrementArrowButton.setFocusTraversable(false);
decrementArrowButton.getStyleClass().setAll("decrement-arrow-button");
decrementArrowButton.getChildren().add(decrementArrow);
decrementArrowButton.setOnMousePressed(e -> {
getSkinnable().requestFocus();
behavior.startSpinning(false);
});
decrementArrowButton.setOnMouseReleased(e -> behavior.stopSpinning());
getChildren().addAll(incrementArrowButton, decrementArrowButton);
// Fixes in the same vein as ComboBoxListViewSkin
// move fake focus in to the textfield if the spinner is editable
control.focusedProperty().addListener((ov, t, hasFocus) -> {
// Fix for the regression noted in a comment in RT-29885.
((FakeFocusTextField)textField).setFakeFocus(hasFocus);
});
control.addEventFilter(KeyEvent.ANY, ke -> {
if (control.isEditable()) {
// 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 RT-38527 which led to a stack overflow
if (ke.getCode() == KeyCode.ESCAPE) return;
// This and the additional check of isIncDecKeyEvent in
// textField's event filter fix JDK-8185937.
if (isIncDecKeyEvent(ke)) 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 Spinner.
textField.fireEvent(ke.copyFor(textField, textField));
if (ke.getCode() == KeyCode.ENTER) return;
ke.consume();
}
});
// This event filter is to enable keyboard events being delivered to the
// spinner when the user has mouse clicked into the TextField area of the
// Spinner control. Without this the up/down/left/right arrow keys don't
// work when you click inside the TextField area (but they do in the case
// of tabbing in).
textField.addEventFilter(KeyEvent.ANY, ke -> {
if (! control.isEditable() || isIncDecKeyEvent(ke)) {
control.fireEvent(ke.copyFor(control, control));
ke.consume();
}
});
textField.focusedProperty().addListener((ov, t, hasFocus) -> {
// Fix for RT-29885
control.getProperties().put("FOCUSED", hasFocus);
// --- end of RT-29885
// RT-21454 starts here
if (! hasFocus) {
pseudoClassStateChanged(CONTAINS_FOCUS_PSEUDOCLASS_STATE, false);
} else {
pseudoClassStateChanged(CONTAINS_FOCUS_PSEUDOCLASS_STATE, true);
}
// --- end of RT-21454
});
// end of comboBox-esque fixes
textField.focusTraversableProperty().bind(control.editableProperty());
// Following code borrowed from ComboBoxPopupControl, to resolve the
// issue initially identified in RT-36902, but specifically (for Spinner)
// identified in RT-40625
ParentHelper.setTraversalEngine(control,
new ParentTraversalEngine(control, 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;
}
}));
}
private boolean isIncDecKeyEvent(KeyEvent ke) {
final KeyCode kc = ke.getCode();
return (kc == KeyCode.UP || kc == KeyCode.DOWN) && behavior.arrowsAreVertical();
}
/* *************************************************************************
* *
* Public API *
* *
**************************************************************************/
/** {@inheritDoc} */
@Override public void dispose() {
super.dispose();
if (behavior != null) {
behavior.dispose();
}
}
/** {@inheritDoc} */
@Override protected void layoutChildren(final double x, final double y,
final double w, final double h) {
final double incrementArrowButtonWidth = incrementArrowButton.snappedLeftInset() +
snapSizeX(incrementArrow.prefWidth(-1)) + incrementArrowButton.snappedRightInset();
final double decrementArrowButtonWidth = decrementArrowButton.snappedLeftInset() +
snapSizeX(decrementArrow.prefWidth(-1)) + decrementArrowButton.snappedRightInset();
final double widestArrowButton = Math.max(incrementArrowButtonWidth, decrementArrowButtonWidth);
// we need to decide on our layout approach, and this depends on
// the presence of style classes in the Spinner styleClass list.
// To be a bit more efficient, we observe the list for changes, so
// here in layoutChildren we can just react to a few booleans.
if (layoutMode == ARROWS_ON_RIGHT_VERTICAL || layoutMode == ARROWS_ON_LEFT_VERTICAL) {
final double textFieldStartX = layoutMode == ARROWS_ON_RIGHT_VERTICAL ? x : x + widestArrowButton;
final double buttonStartX = layoutMode == ARROWS_ON_RIGHT_VERTICAL ? x + w - widestArrowButton : x;
final double halfHeight = Math.floor(h / 2.0);
textField.resizeRelocate(textFieldStartX, y, w - widestArrowButton, h);
incrementArrowButton.resize(widestArrowButton, halfHeight);
positionInArea(incrementArrowButton, buttonStartX, y,
widestArrowButton, halfHeight, 0, HPos.CENTER, VPos.CENTER);
decrementArrowButton.resize(widestArrowButton, halfHeight);
positionInArea(decrementArrowButton, buttonStartX, y + halfHeight,
widestArrowButton, h - halfHeight, 0, HPos.CENTER, VPos.BOTTOM);
} else if (layoutMode == ARROWS_ON_RIGHT_HORIZONTAL || layoutMode == ARROWS_ON_LEFT_HORIZONTAL) {
final double totalButtonWidth = incrementArrowButtonWidth + decrementArrowButtonWidth;
final double textFieldStartX = layoutMode == ARROWS_ON_RIGHT_HORIZONTAL ? x : x + totalButtonWidth;
final double buttonStartX = layoutMode == ARROWS_ON_RIGHT_HORIZONTAL ? x + w - totalButtonWidth : x;
textField.resizeRelocate(textFieldStartX, y, w - totalButtonWidth, h);
// decrement is always on the left
decrementArrowButton.resize(decrementArrowButtonWidth, h);
positionInArea(decrementArrowButton, buttonStartX, y,
decrementArrowButtonWidth, h, 0, HPos.CENTER, VPos.CENTER);
// ... and increment is always on the right
incrementArrowButton.resize(incrementArrowButtonWidth, h);
positionInArea(incrementArrowButton, buttonStartX + decrementArrowButtonWidth, y,
incrementArrowButtonWidth, h, 0, HPos.CENTER, VPos.CENTER);
} else if (layoutMode == SPLIT_ARROWS_VERTICAL) {
final double incrementArrowButtonHeight = incrementArrowButton.snappedTopInset() +
snapSizeY(incrementArrow.prefHeight(-1)) + incrementArrowButton.snappedBottomInset();
final double decrementArrowButtonHeight = decrementArrowButton.snappedTopInset() +
snapSizeY(decrementArrow.prefHeight(-1)) + decrementArrowButton.snappedBottomInset();
final double tallestArrowButton = Math.max(incrementArrowButtonHeight, decrementArrowButtonHeight);
// increment is at the top
incrementArrowButton.resize(w, tallestArrowButton);
positionInArea(incrementArrowButton, x, y,
w, tallestArrowButton, 0, HPos.CENTER, VPos.CENTER);
// textfield in the middle
textField.resizeRelocate(x, y + tallestArrowButton, w, h - (2*tallestArrowButton));
// decrement is at the bottom
decrementArrowButton.resize(w, tallestArrowButton);
positionInArea(decrementArrowButton, x, h - tallestArrowButton,
w, tallestArrowButton, 0, HPos.CENTER, VPos.CENTER);
} else if (layoutMode == SPLIT_ARROWS_HORIZONTAL) {
// decrement is on the left-hand side
decrementArrowButton.resize(widestArrowButton, h);
positionInArea(decrementArrowButton, x, y,
widestArrowButton, h, 0, HPos.CENTER, VPos.CENTER);
// textfield in the middle
textField.resizeRelocate(x + widestArrowButton, y, w - (2*widestArrowButton), h);
// increment is on the right-hand side
incrementArrowButton.resize(widestArrowButton, h);
positionInArea(incrementArrowButton, w - widestArrowButton, y,
widestArrowButton, h, 0, HPos.CENTER, VPos.CENTER);
}
}
@Override
protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return textField.minWidth(height);
}
/** {@inheritDoc} */
@Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
}
/** {@inheritDoc} */
@Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
final double textfieldWidth = textField.prefWidth(height);
return leftInset + textfieldWidth + rightInset;
}
/** {@inheritDoc} */
@Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
double ph;
double textFieldHeight = textField.prefHeight(width);
if (layoutMode == SPLIT_ARROWS_VERTICAL) {
ph = topInset + incrementArrowButton.prefHeight(width) +
textFieldHeight + decrementArrowButton.prefHeight(width) + bottomInset;
} else {
ph = topInset + textFieldHeight + bottomInset;
}
return ph;
}
/** {@inheritDoc} */
@Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return getSkinnable().prefWidth(height);
}
/** {@inheritDoc} */
@Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return getSkinnable().prefHeight(width);
}
// Overridden so that we use the textfield as the baseline, rather than the arrow.
// See RT-30754 for more information.
/** {@inheritDoc} */
@Override protected double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
return textField.getLayoutBounds().getMinY() + textField.getLayoutY() + textField.getBaselineOffset();
}
/* *************************************************************************
* *
* Private implementation *
* *
**************************************************************************/
private void updateStyleClass() {
final List styleClass = getSkinnable().getStyleClass();
if (styleClass.contains(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_VERTICAL)) {
layoutMode = ARROWS_ON_LEFT_VERTICAL;
} else if (styleClass.contains(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_HORIZONTAL)) {
layoutMode = ARROWS_ON_LEFT_HORIZONTAL;
} else if (styleClass.contains(Spinner.STYLE_CLASS_ARROWS_ON_RIGHT_HORIZONTAL)) {
layoutMode = ARROWS_ON_RIGHT_HORIZONTAL;
} else if (styleClass.contains(Spinner.STYLE_CLASS_SPLIT_ARROWS_VERTICAL)) {
layoutMode = SPLIT_ARROWS_VERTICAL;
} else if (styleClass.contains(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL)) {
layoutMode = SPLIT_ARROWS_HORIZONTAL;
} else {
layoutMode = ARROWS_ON_RIGHT_VERTICAL;
}
}
/* *************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
private static PseudoClass CONTAINS_FOCUS_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("contains-focus");
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy