1 /*
   2  * Copyright (c) 2014, 2015, 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 com.sun.javafx.scene.control.skin;
  26 
  27 import com.sun.javafx.scene.control.behavior.SpinnerBehavior;
  28 import com.sun.javafx.scene.traversal.Algorithm;
  29 import com.sun.javafx.scene.traversal.Direction;
  30 import com.sun.javafx.scene.traversal.ParentTraversalEngine;
  31 import com.sun.javafx.scene.traversal.TraversalContext;
  32 import javafx.collections.ListChangeListener;
  33 import javafx.css.PseudoClass;
  34 import javafx.geometry.HPos;
  35 import javafx.geometry.VPos;
  36 import javafx.scene.AccessibleAction;
  37 import javafx.scene.AccessibleRole;
  38 import javafx.scene.Node;
  39 import javafx.scene.control.Spinner;
  40 import javafx.scene.control.TextField;
  41 import javafx.scene.input.KeyCode;
  42 import javafx.scene.input.KeyEvent;
  43 import javafx.scene.layout.Region;
  44 import javafx.scene.layout.StackPane;
  45 
  46 import java.util.List;
  47 
  48 public class SpinnerSkin<T> extends BehaviorSkinBase<Spinner<T>, SpinnerBehavior<T>> {
  49 
  50     private TextField textField;
  51 
  52     private Region incrementArrow;
  53     private StackPane incrementArrowButton;
  54 
  55     private Region decrementArrow;
  56     private StackPane decrementArrowButton;
  57 
  58     // rather than create an private enum, lets just use an int, here's the important details:
  59     private static final int ARROWS_ON_RIGHT_VERTICAL   = 0;
  60     private static final int ARROWS_ON_LEFT_VERTICAL    = 1;
  61     private static final int ARROWS_ON_RIGHT_HORIZONTAL = 2;
  62     private static final int ARROWS_ON_LEFT_HORIZONTAL  = 3;
  63     private static final int SPLIT_ARROWS_VERTICAL      = 4;
  64     private static final int SPLIT_ARROWS_HORIZONTAL    = 5;
  65 
  66     private int layoutMode = 0;
  67 
  68     public SpinnerSkin(Spinner<T> spinner) {
  69         super(spinner, new SpinnerBehavior<T>(spinner));
  70 
  71         textField = spinner.getEditor();
  72         getChildren().add(textField);
  73 
  74         updateStyleClass();
  75         spinner.getStyleClass().addListener((ListChangeListener<String>) c -> updateStyleClass());
  76 
  77         // increment / decrement arrows
  78         incrementArrow = new Region();
  79         incrementArrow.setFocusTraversable(false);
  80         incrementArrow.getStyleClass().setAll("increment-arrow");
  81         incrementArrow.setMaxWidth(Region.USE_PREF_SIZE);
  82         incrementArrow.setMaxHeight(Region.USE_PREF_SIZE);
  83         incrementArrow.setMouseTransparent(true);
  84 
  85         incrementArrowButton = new StackPane() {
  86             public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
  87                 switch (action) {
  88                     case FIRE: getSkinnable().increment();
  89                     default: super.executeAccessibleAction(action, parameters);
  90                 }
  91             }
  92         };
  93         incrementArrowButton.setAccessibleRole(AccessibleRole.INCREMENT_BUTTON);
  94         incrementArrowButton.setFocusTraversable(false);
  95         incrementArrowButton.getStyleClass().setAll("increment-arrow-button");
  96         incrementArrowButton.getChildren().add(incrementArrow);
  97         incrementArrowButton.setOnMousePressed(e -> {
  98             getSkinnable().requestFocus();
  99             getBehavior().startSpinning(true);
 100         });
 101         incrementArrowButton.setOnMouseReleased(e -> getBehavior().stopSpinning());
 102 
 103         decrementArrow = new Region();
 104         decrementArrow.setFocusTraversable(false);
 105         decrementArrow.getStyleClass().setAll("decrement-arrow");
 106         decrementArrow.setMaxWidth(Region.USE_PREF_SIZE);
 107         decrementArrow.setMaxHeight(Region.USE_PREF_SIZE);
 108         decrementArrow.setMouseTransparent(true);
 109 
 110         decrementArrowButton = new StackPane() {
 111             public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
 112                 switch (action) {
 113                     case FIRE: getSkinnable().decrement();
 114                     default: super.executeAccessibleAction(action, parameters);
 115                 }
 116             }
 117         };
 118         decrementArrowButton.setAccessibleRole(AccessibleRole.DECREMENT_BUTTON);
 119         decrementArrowButton.setFocusTraversable(false);
 120         decrementArrowButton.getStyleClass().setAll("decrement-arrow-button");
 121         decrementArrowButton.getChildren().add(decrementArrow);
 122         decrementArrowButton.setOnMousePressed(e -> {
 123             getSkinnable().requestFocus();
 124             getBehavior().startSpinning(false);
 125         });
 126         decrementArrowButton.setOnMouseReleased(e -> getBehavior().stopSpinning());
 127 
 128         getChildren().addAll(incrementArrowButton, decrementArrowButton);
 129 
 130         // Fixes in the same vein as ComboBoxListViewSkin
 131 
 132         // move fake focus in to the textfield if the spinner is editable
 133         spinner.focusedProperty().addListener((ov, t, hasFocus) -> {
 134             // Fix for the regression noted in a comment in RT-29885.
 135             ((ComboBoxListViewSkin.FakeFocusTextField)textField).setFakeFocus(hasFocus);
 136         });
 137 
 138         spinner.addEventFilter(KeyEvent.ANY, ke -> {
 139             if (spinner.isEditable()) {
 140                 // This prevents a stack overflow from our rebroadcasting of the
 141                 // event to the textfield that occurs in the final else statement
 142                 // of the conditions below.
 143                 if (ke.getTarget().equals(textField)) return;
 144 
 145                 // Fix for RT-38527 which led to a stack overflow
 146                 if (ke.getCode() == KeyCode.ESCAPE) return;
 147 
 148                 // Fix for the regression noted in a comment in RT-29885.
 149                 // This forwards the event down into the TextField when
 150                 // the key event is actually received by the Spinner.
 151                 textField.fireEvent(ke.copyFor(textField, textField));
 152                 ke.consume();
 153             }
 154         });
 155 
 156         // This event filter is to enable keyboard events being delivered to the
 157         // spinner when the user has mouse clicked into the TextField area of the
 158         // Spinner control. Without this the up/down/left/right arrow keys don't
 159         // work when you click inside the TextField area (but they do in the case
 160         // of tabbing in).
 161         textField.addEventFilter(KeyEvent.ANY, ke -> {
 162             if (! spinner.isEditable()) {
 163                 spinner.fireEvent(ke.copyFor(spinner, spinner));
 164                 ke.consume();
 165             }
 166         });
 167 
 168         textField.focusedProperty().addListener((ov, t, hasFocus) -> {
 169             // Fix for RT-29885
 170             spinner.getProperties().put("FOCUSED", hasFocus);
 171             // --- end of RT-29885
 172 
 173             // RT-21454 starts here
 174             if (! hasFocus) {
 175                 pseudoClassStateChanged(CONTAINS_FOCUS_PSEUDOCLASS_STATE, false);
 176             } else {
 177                 pseudoClassStateChanged(CONTAINS_FOCUS_PSEUDOCLASS_STATE, true);
 178             }
 179             // --- end of RT-21454
 180         });
 181 
 182         // end of comboBox-esque fixes
 183 
 184         textField.focusTraversableProperty().bind(spinner.editableProperty());
 185 
 186 
 187         // Following code borrowed from ComboBoxPopupControl, to resolve the
 188         // issue initially identified in RT-36902, but specifically (for Spinner)
 189         // identified in RT-40625
 190         spinner.setImpl_traversalEngine(new ParentTraversalEngine(spinner, new Algorithm() {
 191             @Override public Node select(Node owner, Direction dir, TraversalContext context) {
 192                 return null;
 193             }
 194 
 195             @Override public Node selectFirst(TraversalContext context) {
 196                 return null;
 197             }
 198 
 199             @Override public Node selectLast(TraversalContext context) {
 200                 return null;
 201             }
 202         }));
 203     }
 204 
 205     private void updateStyleClass() {
 206         final List<String> styleClass = getSkinnable().getStyleClass();
 207 
 208         if (styleClass.contains(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_VERTICAL)) {
 209             layoutMode = ARROWS_ON_LEFT_VERTICAL;
 210         } else if (styleClass.contains(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_HORIZONTAL)) {
 211             layoutMode = ARROWS_ON_LEFT_HORIZONTAL;
 212         } else if (styleClass.contains(Spinner.STYLE_CLASS_ARROWS_ON_RIGHT_HORIZONTAL)) {
 213             layoutMode = ARROWS_ON_RIGHT_HORIZONTAL;
 214         } else if (styleClass.contains(Spinner.STYLE_CLASS_SPLIT_ARROWS_VERTICAL)) {
 215             layoutMode = SPLIT_ARROWS_VERTICAL;
 216         } else if (styleClass.contains(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL)) {
 217             layoutMode = SPLIT_ARROWS_HORIZONTAL;
 218         } else {
 219             layoutMode = ARROWS_ON_RIGHT_VERTICAL;
 220         }
 221     }
 222 
 223     @Override protected void layoutChildren(final double x, final double y,
 224                                             final double w, final double h) {
 225 
 226         final double incrementArrowButtonWidth = incrementArrowButton.snappedLeftInset() +
 227                 snapSize(incrementArrow.prefWidth(-1)) + incrementArrowButton.snappedRightInset();
 228 
 229         final double decrementArrowButtonWidth = decrementArrowButton.snappedLeftInset() +
 230                 snapSize(decrementArrow.prefWidth(-1)) + decrementArrowButton.snappedRightInset();
 231 
 232         final double widestArrowButton = Math.max(incrementArrowButtonWidth, decrementArrowButtonWidth);
 233 
 234         // we need to decide on our layout approach, and this depends on
 235         // the presence of style classes in the Spinner styleClass list.
 236         // To be a bit more efficient, we observe the list for changes, so
 237         // here in layoutChildren we can just react to a few booleans.
 238         if (layoutMode == ARROWS_ON_RIGHT_VERTICAL || layoutMode == ARROWS_ON_LEFT_VERTICAL) {
 239             final double textFieldStartX = layoutMode == ARROWS_ON_RIGHT_VERTICAL ? x : x + widestArrowButton;
 240             final double buttonStartX = layoutMode == ARROWS_ON_RIGHT_VERTICAL ? x + w - widestArrowButton : x;
 241             final double halfHeight = Math.floor(h / 2.0);
 242 
 243             textField.resizeRelocate(textFieldStartX, y, w - widestArrowButton, h);
 244 
 245             incrementArrowButton.resize(widestArrowButton, halfHeight);
 246             positionInArea(incrementArrowButton, buttonStartX, y,
 247                     widestArrowButton, halfHeight, 0, HPos.CENTER, VPos.CENTER);
 248 
 249             decrementArrowButton.resize(widestArrowButton, halfHeight);
 250             positionInArea(decrementArrowButton, buttonStartX, y + halfHeight,
 251                     widestArrowButton, h - halfHeight, 0, HPos.CENTER, VPos.BOTTOM);
 252         } else if (layoutMode == ARROWS_ON_RIGHT_HORIZONTAL || layoutMode == ARROWS_ON_LEFT_HORIZONTAL) {
 253             final double totalButtonWidth = incrementArrowButtonWidth + decrementArrowButtonWidth;
 254             final double textFieldStartX = layoutMode == ARROWS_ON_RIGHT_HORIZONTAL ? x : x + totalButtonWidth;
 255             final double buttonStartX = layoutMode == ARROWS_ON_RIGHT_HORIZONTAL ? x + w - totalButtonWidth : x;
 256 
 257             textField.resizeRelocate(textFieldStartX, y, w - totalButtonWidth, h);
 258 
 259             // decrement is always on the left
 260             decrementArrowButton.resize(decrementArrowButtonWidth, h);
 261             positionInArea(decrementArrowButton, buttonStartX, y,
 262                     decrementArrowButtonWidth, h, 0, HPos.CENTER, VPos.CENTER);
 263 
 264             // ... and increment is always on the right
 265             incrementArrowButton.resize(incrementArrowButtonWidth, h);
 266             positionInArea(incrementArrowButton, buttonStartX + decrementArrowButtonWidth, y,
 267                     incrementArrowButtonWidth, h, 0, HPos.CENTER, VPos.CENTER);
 268         } else if (layoutMode == SPLIT_ARROWS_VERTICAL) {
 269             final double incrementArrowButtonHeight = incrementArrowButton.snappedTopInset() +
 270                     snapSize(incrementArrow.prefHeight(-1)) + incrementArrowButton.snappedBottomInset();
 271 
 272             final double decrementArrowButtonHeight = decrementArrowButton.snappedTopInset() +
 273                     snapSize(decrementArrow.prefHeight(-1)) + decrementArrowButton.snappedBottomInset();
 274 
 275             final double tallestArrowButton = Math.max(incrementArrowButtonHeight, decrementArrowButtonHeight);
 276 
 277             // increment is at the top
 278             incrementArrowButton.resize(w, tallestArrowButton);
 279             positionInArea(incrementArrowButton, x, y,
 280                     w, tallestArrowButton, 0, HPos.CENTER, VPos.CENTER);
 281 
 282             // textfield in the middle
 283             textField.resizeRelocate(x, y + tallestArrowButton, w, h - (2*tallestArrowButton));
 284 
 285             // decrement is at the bottom
 286             decrementArrowButton.resize(w, tallestArrowButton);
 287             positionInArea(decrementArrowButton, x, h - tallestArrowButton,
 288                     w, tallestArrowButton, 0, HPos.CENTER, VPos.CENTER);
 289         } else if (layoutMode == SPLIT_ARROWS_HORIZONTAL) {
 290             // decrement is on the left-hand side
 291             decrementArrowButton.resize(widestArrowButton, h);
 292             positionInArea(decrementArrowButton, x, y,
 293                     widestArrowButton, h, 0, HPos.CENTER, VPos.CENTER);
 294 
 295             // textfield in the middle
 296             textField.resizeRelocate(x + widestArrowButton, y, w - (2*widestArrowButton), h);
 297 
 298             // increment is on the right-hand side
 299             incrementArrowButton.resize(widestArrowButton, h);
 300             positionInArea(incrementArrowButton, w - widestArrowButton, y,
 301                     widestArrowButton, h, 0, HPos.CENTER, VPos.CENTER);
 302         }
 303     }
 304 
 305     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 306         return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
 307     }
 308 
 309     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 310         final double textfieldWidth = textField.prefWidth(height);
 311         return leftInset + textfieldWidth + rightInset;
 312     }
 313 
 314     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 315         double ph;
 316         double textFieldHeight = textField.prefHeight(width);
 317 
 318         if (layoutMode == SPLIT_ARROWS_VERTICAL) {
 319             ph = topInset + incrementArrowButton.prefHeight(width) +
 320                     textFieldHeight + decrementArrowButton.prefHeight(width) + bottomInset;
 321         } else {
 322             ph = topInset + textFieldHeight + bottomInset;
 323         }
 324 
 325         return ph;
 326     }
 327 
 328     @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 329         return getSkinnable().prefWidth(height);
 330     }
 331 
 332     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 333         return getSkinnable().prefHeight(width);
 334     }
 335 
 336     // Overridden so that we use the textfield as the baseline, rather than the arrow.
 337     // See RT-30754 for more information.
 338     @Override protected double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
 339         return textField.getLayoutBounds().getMinY() + textField.getLayoutY() + textField.getBaselineOffset();
 340     }
 341 
 342 
 343     /***************************************************************************
 344      *                                                                         *
 345      * Stylesheet Handling                                                     *
 346      *                                                                         *
 347      **************************************************************************/
 348 
 349     private static PseudoClass CONTAINS_FOCUS_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("contains-focus");
 350 }