1 /*
   2  * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package javafx.scene.control.skin;
  26 
  27 import com.sun.javafx.scene.ParentHelper;
  28 import com.sun.javafx.scene.control.behavior.BehaviorBase;
  29 import com.sun.javafx.scene.traversal.Algorithm;
  30 import com.sun.javafx.scene.control.FakeFocusTextField;
  31 import com.sun.javafx.scene.traversal.Direction;
  32 import com.sun.javafx.scene.traversal.ParentTraversalEngine;
  33 import javafx.scene.control.Accordion;
  34 import javafx.scene.control.Button;
  35 import javafx.scene.control.Control;
  36 import javafx.scene.control.SkinBase;
  37 import com.sun.javafx.scene.control.behavior.SpinnerBehavior;
  38 import com.sun.javafx.scene.traversal.TraversalContext;
  39 import javafx.collections.ListChangeListener;
  40 import javafx.css.PseudoClass;
  41 import javafx.geometry.HPos;
  42 import javafx.geometry.VPos;
  43 import javafx.scene.AccessibleAction;
  44 import javafx.scene.AccessibleRole;
  45 import javafx.scene.Node;
  46 import javafx.scene.control.Spinner;
  47 import javafx.scene.control.TextField;
  48 import javafx.scene.input.KeyCode;
  49 import javafx.scene.input.KeyEvent;
  50 import javafx.scene.layout.Region;
  51 import javafx.scene.layout.StackPane;
  52 
  53 import java.util.List;
  54 
  55 /**
  56  * Default skin implementation for the {@link Spinner} control.
  57  *
  58  * @see Spinner
  59  * @since 9
  60  */
  61 public class SpinnerSkin<T> extends SkinBase<Spinner<T>> {
  62 
  63     /***************************************************************************
  64      *                                                                         *
  65      * Private fields                                                          *
  66      *                                                                         *
  67      **************************************************************************/
  68 
  69     private TextField textField;
  70 
  71     private Region incrementArrow;
  72     private StackPane incrementArrowButton;
  73 
  74     private Region decrementArrow;
  75     private StackPane decrementArrowButton;
  76 
  77     // rather than create an private enum, lets just use an int, here's the important details:
  78     private static final int ARROWS_ON_RIGHT_VERTICAL   = 0;
  79     private static final int ARROWS_ON_LEFT_VERTICAL    = 1;
  80     private static final int ARROWS_ON_RIGHT_HORIZONTAL = 2;
  81     private static final int ARROWS_ON_LEFT_HORIZONTAL  = 3;
  82     private static final int SPLIT_ARROWS_VERTICAL      = 4;
  83     private static final int SPLIT_ARROWS_HORIZONTAL    = 5;
  84 
  85     private int layoutMode = 0;
  86 
  87     private final SpinnerBehavior behavior;
  88 
  89 
  90 
  91     /***************************************************************************
  92      *                                                                         *
  93      * Constructors                                                            *
  94      *                                                                         *
  95      **************************************************************************/
  96 
  97     /**
  98      * Creates a new SpinnerSkin instance, installing the necessary child
  99      * nodes into the Control {@link Control#getChildren() children} list, as
 100      * well as the necessary input mappings for handling key, mouse, etc events.
 101      *
 102      * @param control The control that this skin should be installed onto.
 103      */
 104     public SpinnerSkin(Spinner<T> control) {
 105         super(control);
 106 
 107         // install default input map for the Button control
 108         behavior = new SpinnerBehavior<>(control);
 109 //        control.setInputMap(behavior.getInputMap());
 110 
 111         textField = control.getEditor();
 112         getChildren().add(textField);
 113 
 114         updateStyleClass();
 115         control.getStyleClass().addListener((ListChangeListener<String>) c -> updateStyleClass());
 116 
 117         // increment / decrement arrows
 118         incrementArrow = new Region();
 119         incrementArrow.setFocusTraversable(false);
 120         incrementArrow.getStyleClass().setAll("increment-arrow");
 121         incrementArrow.setMaxWidth(Region.USE_PREF_SIZE);
 122         incrementArrow.setMaxHeight(Region.USE_PREF_SIZE);
 123         incrementArrow.setMouseTransparent(true);
 124 
 125         incrementArrowButton = new StackPane() {
 126             public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
 127                 switch (action) {
 128                     case FIRE: getSkinnable().increment(); break;
 129                     default: super.executeAccessibleAction(action, parameters);
 130                 }
 131             }
 132         };
 133         incrementArrowButton.setAccessibleRole(AccessibleRole.INCREMENT_BUTTON);
 134         incrementArrowButton.setFocusTraversable(false);
 135         incrementArrowButton.getStyleClass().setAll("increment-arrow-button");
 136         incrementArrowButton.getChildren().add(incrementArrow);
 137         incrementArrowButton.setOnMousePressed(e -> {
 138             getSkinnable().requestFocus();
 139             behavior.startSpinning(true);
 140         });
 141         incrementArrowButton.setOnMouseReleased(e -> behavior.stopSpinning());
 142 
 143         decrementArrow = new Region();
 144         decrementArrow.setFocusTraversable(false);
 145         decrementArrow.getStyleClass().setAll("decrement-arrow");
 146         decrementArrow.setMaxWidth(Region.USE_PREF_SIZE);
 147         decrementArrow.setMaxHeight(Region.USE_PREF_SIZE);
 148         decrementArrow.setMouseTransparent(true);
 149 
 150         decrementArrowButton = new StackPane() {
 151             public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
 152                 switch (action) {
 153                     case FIRE: getSkinnable().decrement(); break;
 154                     default: super.executeAccessibleAction(action, parameters);
 155                 }
 156             }
 157         };
 158         decrementArrowButton.setAccessibleRole(AccessibleRole.DECREMENT_BUTTON);
 159         decrementArrowButton.setFocusTraversable(false);
 160         decrementArrowButton.getStyleClass().setAll("decrement-arrow-button");
 161         decrementArrowButton.getChildren().add(decrementArrow);
 162         decrementArrowButton.setOnMousePressed(e -> {
 163             getSkinnable().requestFocus();
 164             behavior.startSpinning(false);
 165         });
 166         decrementArrowButton.setOnMouseReleased(e -> behavior.stopSpinning());
 167 
 168         getChildren().addAll(incrementArrowButton, decrementArrowButton);
 169 
 170         // Fixes in the same vein as ComboBoxListViewSkin
 171 
 172         // move fake focus in to the textfield if the spinner is editable
 173         control.focusedProperty().addListener((ov, t, hasFocus) -> {
 174             // Fix for the regression noted in a comment in RT-29885.
 175             ((FakeFocusTextField)textField).setFakeFocus(hasFocus);
 176         });
 177 
 178         control.addEventFilter(KeyEvent.ANY, ke -> {
 179             if (control.isEditable()) {
 180                 // This prevents a stack overflow from our rebroadcasting of the
 181                 // event to the textfield that occurs in the final else statement
 182                 // of the conditions below.
 183                 if (ke.getTarget().equals(textField)) return;
 184 
 185                 // Fix for RT-38527 which led to a stack overflow
 186                 if (ke.getCode() == KeyCode.ESCAPE) return;
 187 
 188                 // Fix for the regression noted in a comment in RT-29885.
 189                 // This forwards the event down into the TextField when
 190                 // the key event is actually received by the Spinner.
 191                 textField.fireEvent(ke.copyFor(textField, textField));
 192                 ke.consume();
 193             }
 194         });
 195 
 196         // This event filter is to enable keyboard events being delivered to the
 197         // spinner when the user has mouse clicked into the TextField area of the
 198         // Spinner control. Without this the up/down/left/right arrow keys don't
 199         // work when you click inside the TextField area (but they do in the case
 200         // of tabbing in).
 201         textField.addEventFilter(KeyEvent.ANY, ke -> {
 202             if (! control.isEditable()) {
 203                 control.fireEvent(ke.copyFor(control, control));
 204                 ke.consume();
 205             }
 206         });
 207 
 208         textField.focusedProperty().addListener((ov, t, hasFocus) -> {
 209             // Fix for RT-29885
 210             control.getProperties().put("FOCUSED", hasFocus);
 211             // --- end of RT-29885
 212 
 213             // RT-21454 starts here
 214             if (! hasFocus) {
 215                 pseudoClassStateChanged(CONTAINS_FOCUS_PSEUDOCLASS_STATE, false);
 216             } else {
 217                 pseudoClassStateChanged(CONTAINS_FOCUS_PSEUDOCLASS_STATE, true);
 218             }
 219             // --- end of RT-21454
 220         });
 221 
 222         // end of comboBox-esque fixes
 223 
 224         textField.focusTraversableProperty().bind(control.editableProperty());
 225 
 226 
 227         // Following code borrowed from ComboBoxPopupControl, to resolve the
 228         // issue initially identified in RT-36902, but specifically (for Spinner)
 229         // identified in RT-40625
 230         ParentHelper.setTraversalEngine(control,
 231                 new ParentTraversalEngine(control, new Algorithm() {
 232 
 233             @Override public Node select(Node owner, Direction dir, TraversalContext context) {
 234                 return null;
 235             }
 236 
 237             @Override public Node selectFirst(TraversalContext context) {
 238                 return null;
 239             }
 240 
 241             @Override public Node selectLast(TraversalContext context) {
 242                 return null;
 243             }
 244         }));
 245     }
 246 
 247 
 248 
 249     /***************************************************************************
 250      *                                                                         *
 251      * Public API                                                              *
 252      *                                                                         *
 253      **************************************************************************/
 254 
 255     /** {@inheritDoc} */
 256     @Override public void dispose() {
 257         super.dispose();
 258 
 259         if (behavior != null) {
 260             behavior.dispose();
 261         }
 262     }
 263 
 264     /** {@inheritDoc} */
 265     @Override protected void layoutChildren(final double x, final double y,
 266                                             final double w, final double h) {
 267 
 268         final double incrementArrowButtonWidth = incrementArrowButton.snappedLeftInset() +
 269                 snapSizeX(incrementArrow.prefWidth(-1)) + incrementArrowButton.snappedRightInset();
 270 
 271         final double decrementArrowButtonWidth = decrementArrowButton.snappedLeftInset() +
 272                 snapSizeX(decrementArrow.prefWidth(-1)) + decrementArrowButton.snappedRightInset();
 273 
 274         final double widestArrowButton = Math.max(incrementArrowButtonWidth, decrementArrowButtonWidth);
 275 
 276         // we need to decide on our layout approach, and this depends on
 277         // the presence of style classes in the Spinner styleClass list.
 278         // To be a bit more efficient, we observe the list for changes, so
 279         // here in layoutChildren we can just react to a few booleans.
 280         if (layoutMode == ARROWS_ON_RIGHT_VERTICAL || layoutMode == ARROWS_ON_LEFT_VERTICAL) {
 281             final double textFieldStartX = layoutMode == ARROWS_ON_RIGHT_VERTICAL ? x : x + widestArrowButton;
 282             final double buttonStartX = layoutMode == ARROWS_ON_RIGHT_VERTICAL ? x + w - widestArrowButton : x;
 283             final double halfHeight = Math.floor(h / 2.0);
 284 
 285             textField.resizeRelocate(textFieldStartX, y, w - widestArrowButton, h);
 286 
 287             incrementArrowButton.resize(widestArrowButton, halfHeight);
 288             positionInArea(incrementArrowButton, buttonStartX, y,
 289                     widestArrowButton, halfHeight, 0, HPos.CENTER, VPos.CENTER);
 290 
 291             decrementArrowButton.resize(widestArrowButton, halfHeight);
 292             positionInArea(decrementArrowButton, buttonStartX, y + halfHeight,
 293                     widestArrowButton, h - halfHeight, 0, HPos.CENTER, VPos.BOTTOM);
 294         } else if (layoutMode == ARROWS_ON_RIGHT_HORIZONTAL || layoutMode == ARROWS_ON_LEFT_HORIZONTAL) {
 295             final double totalButtonWidth = incrementArrowButtonWidth + decrementArrowButtonWidth;
 296             final double textFieldStartX = layoutMode == ARROWS_ON_RIGHT_HORIZONTAL ? x : x + totalButtonWidth;
 297             final double buttonStartX = layoutMode == ARROWS_ON_RIGHT_HORIZONTAL ? x + w - totalButtonWidth : x;
 298 
 299             textField.resizeRelocate(textFieldStartX, y, w - totalButtonWidth, h);
 300 
 301             // decrement is always on the left
 302             decrementArrowButton.resize(decrementArrowButtonWidth, h);
 303             positionInArea(decrementArrowButton, buttonStartX, y,
 304                     decrementArrowButtonWidth, h, 0, HPos.CENTER, VPos.CENTER);
 305 
 306             // ... and increment is always on the right
 307             incrementArrowButton.resize(incrementArrowButtonWidth, h);
 308             positionInArea(incrementArrowButton, buttonStartX + decrementArrowButtonWidth, y,
 309                     incrementArrowButtonWidth, h, 0, HPos.CENTER, VPos.CENTER);
 310         } else if (layoutMode == SPLIT_ARROWS_VERTICAL) {
 311             final double incrementArrowButtonHeight = incrementArrowButton.snappedTopInset() +
 312                     snapSizeY(incrementArrow.prefHeight(-1)) + incrementArrowButton.snappedBottomInset();
 313 
 314             final double decrementArrowButtonHeight = decrementArrowButton.snappedTopInset() +
 315                     snapSizeY(decrementArrow.prefHeight(-1)) + decrementArrowButton.snappedBottomInset();
 316 
 317             final double tallestArrowButton = Math.max(incrementArrowButtonHeight, decrementArrowButtonHeight);
 318 
 319             // increment is at the top
 320             incrementArrowButton.resize(w, tallestArrowButton);
 321             positionInArea(incrementArrowButton, x, y,
 322                     w, tallestArrowButton, 0, HPos.CENTER, VPos.CENTER);
 323 
 324             // textfield in the middle
 325             textField.resizeRelocate(x, y + tallestArrowButton, w, h - (2*tallestArrowButton));
 326 
 327             // decrement is at the bottom
 328             decrementArrowButton.resize(w, tallestArrowButton);
 329             positionInArea(decrementArrowButton, x, h - tallestArrowButton,
 330                     w, tallestArrowButton, 0, HPos.CENTER, VPos.CENTER);
 331         } else if (layoutMode == SPLIT_ARROWS_HORIZONTAL) {
 332             // decrement is on the left-hand side
 333             decrementArrowButton.resize(widestArrowButton, h);
 334             positionInArea(decrementArrowButton, x, y,
 335                     widestArrowButton, h, 0, HPos.CENTER, VPos.CENTER);
 336 
 337             // textfield in the middle
 338             textField.resizeRelocate(x + widestArrowButton, y, w - (2*widestArrowButton), h);
 339 
 340             // increment is on the right-hand side
 341             incrementArrowButton.resize(widestArrowButton, h);
 342             positionInArea(incrementArrowButton, w - widestArrowButton, y,
 343                     widestArrowButton, h, 0, HPos.CENTER, VPos.CENTER);
 344         }
 345     }
 346 
 347     @Override
 348     protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 349         return textField.minWidth(height);
 350     }
 351 
 352     /** {@inheritDoc} */
 353     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 354         return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
 355     }
 356 
 357     /** {@inheritDoc} */
 358     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 359         final double textfieldWidth = textField.prefWidth(height);
 360         return leftInset + textfieldWidth + rightInset;
 361     }
 362 
 363     /** {@inheritDoc} */
 364     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 365         double ph;
 366         double textFieldHeight = textField.prefHeight(width);
 367 
 368         if (layoutMode == SPLIT_ARROWS_VERTICAL) {
 369             ph = topInset + incrementArrowButton.prefHeight(width) +
 370                     textFieldHeight + decrementArrowButton.prefHeight(width) + bottomInset;
 371         } else {
 372             ph = topInset + textFieldHeight + bottomInset;
 373         }
 374 
 375         return ph;
 376     }
 377 
 378     /** {@inheritDoc} */
 379     @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 380         return getSkinnable().prefWidth(height);
 381     }
 382 
 383     /** {@inheritDoc} */
 384     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 385         return getSkinnable().prefHeight(width);
 386     }
 387 
 388     // Overridden so that we use the textfield as the baseline, rather than the arrow.
 389     // See RT-30754 for more information.
 390     /** {@inheritDoc} */
 391     @Override protected double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
 392         return textField.getLayoutBounds().getMinY() + textField.getLayoutY() + textField.getBaselineOffset();
 393     }
 394 
 395 
 396 
 397     /***************************************************************************
 398      *                                                                         *
 399      * Private implementation                                                  *
 400      *                                                                         *
 401      **************************************************************************/
 402 
 403     private void updateStyleClass() {
 404         final List<String> styleClass = getSkinnable().getStyleClass();
 405 
 406         if (styleClass.contains(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_VERTICAL)) {
 407             layoutMode = ARROWS_ON_LEFT_VERTICAL;
 408         } else if (styleClass.contains(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_HORIZONTAL)) {
 409             layoutMode = ARROWS_ON_LEFT_HORIZONTAL;
 410         } else if (styleClass.contains(Spinner.STYLE_CLASS_ARROWS_ON_RIGHT_HORIZONTAL)) {
 411             layoutMode = ARROWS_ON_RIGHT_HORIZONTAL;
 412         } else if (styleClass.contains(Spinner.STYLE_CLASS_SPLIT_ARROWS_VERTICAL)) {
 413             layoutMode = SPLIT_ARROWS_VERTICAL;
 414         } else if (styleClass.contains(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL)) {
 415             layoutMode = SPLIT_ARROWS_HORIZONTAL;
 416         } else {
 417             layoutMode = ARROWS_ON_RIGHT_VERTICAL;
 418         }
 419     }
 420 
 421 
 422 
 423     /***************************************************************************
 424      *                                                                         *
 425      * Stylesheet Handling                                                     *
 426      *                                                                         *
 427      **************************************************************************/
 428 
 429     private static PseudoClass CONTAINS_FOCUS_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("contains-focus");
 430 }