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