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 }