1 /* 2 * Copyright (c) 2014, 2018, 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 193 if (ke.getCode() == KeyCode.ENTER) return; 194 195 ke.consume(); 196 } 197 }); 198 199 // This event filter is to enable keyboard events being delivered to the 200 // spinner when the user has mouse clicked into the TextField area of the 201 // Spinner control. Without this the up/down/left/right arrow keys don't 202 // work when you click inside the TextField area (but they do in the case 203 // of tabbing in). 204 textField.addEventFilter(KeyEvent.ANY, ke -> { 205 if (! control.isEditable()) { 206 control.fireEvent(ke.copyFor(control, control)); 207 ke.consume(); 208 } 209 }); 210 211 textField.focusedProperty().addListener((ov, t, hasFocus) -> { 212 // Fix for RT-29885 213 control.getProperties().put("FOCUSED", hasFocus); 214 // --- end of RT-29885 215 216 // RT-21454 starts here 217 if (! hasFocus) { 218 pseudoClassStateChanged(CONTAINS_FOCUS_PSEUDOCLASS_STATE, false); 219 } else { 220 pseudoClassStateChanged(CONTAINS_FOCUS_PSEUDOCLASS_STATE, true); 221 } 222 // --- end of RT-21454 223 }); 224 225 // end of comboBox-esque fixes 226 227 textField.focusTraversableProperty().bind(control.editableProperty()); 228 229 230 // Following code borrowed from ComboBoxPopupControl, to resolve the 231 // issue initially identified in RT-36902, but specifically (for Spinner) 232 // identified in RT-40625 233 ParentHelper.setTraversalEngine(control, 234 new ParentTraversalEngine(control, new Algorithm() { 235 236 @Override public Node select(Node owner, Direction dir, TraversalContext context) { 237 return null; 238 } 239 240 @Override public Node selectFirst(TraversalContext context) { 241 return null; 242 } 243 244 @Override public Node selectLast(TraversalContext context) { 245 return null; 246 } 247 })); 248 } 249 250 251 252 /*************************************************************************** 253 * * 254 * Public API * 255 * * 256 **************************************************************************/ 257 258 /** {@inheritDoc} */ 259 @Override public void dispose() { 260 super.dispose(); 261 262 if (behavior != null) { 263 behavior.dispose(); 264 } 265 } 266 267 /** {@inheritDoc} */ 268 @Override protected void layoutChildren(final double x, final double y, 269 final double w, final double h) { 270 271 final double incrementArrowButtonWidth = incrementArrowButton.snappedLeftInset() + 272 snapSizeX(incrementArrow.prefWidth(-1)) + incrementArrowButton.snappedRightInset(); 273 274 final double decrementArrowButtonWidth = decrementArrowButton.snappedLeftInset() + 275 snapSizeX(decrementArrow.prefWidth(-1)) + decrementArrowButton.snappedRightInset(); 276 277 final double widestArrowButton = Math.max(incrementArrowButtonWidth, decrementArrowButtonWidth); 278 279 // we need to decide on our layout approach, and this depends on 280 // the presence of style classes in the Spinner styleClass list. 281 // To be a bit more efficient, we observe the list for changes, so 282 // here in layoutChildren we can just react to a few booleans. 283 if (layoutMode == ARROWS_ON_RIGHT_VERTICAL || layoutMode == ARROWS_ON_LEFT_VERTICAL) { 284 final double textFieldStartX = layoutMode == ARROWS_ON_RIGHT_VERTICAL ? x : x + widestArrowButton; 285 final double buttonStartX = layoutMode == ARROWS_ON_RIGHT_VERTICAL ? x + w - widestArrowButton : x; 286 final double halfHeight = Math.floor(h / 2.0); 287 288 textField.resizeRelocate(textFieldStartX, y, w - widestArrowButton, h); 289 290 incrementArrowButton.resize(widestArrowButton, halfHeight); 291 positionInArea(incrementArrowButton, buttonStartX, y, 292 widestArrowButton, halfHeight, 0, HPos.CENTER, VPos.CENTER); 293 294 decrementArrowButton.resize(widestArrowButton, halfHeight); 295 positionInArea(decrementArrowButton, buttonStartX, y + halfHeight, 296 widestArrowButton, h - halfHeight, 0, HPos.CENTER, VPos.BOTTOM); 297 } else if (layoutMode == ARROWS_ON_RIGHT_HORIZONTAL || layoutMode == ARROWS_ON_LEFT_HORIZONTAL) { 298 final double totalButtonWidth = incrementArrowButtonWidth + decrementArrowButtonWidth; 299 final double textFieldStartX = layoutMode == ARROWS_ON_RIGHT_HORIZONTAL ? x : x + totalButtonWidth; 300 final double buttonStartX = layoutMode == ARROWS_ON_RIGHT_HORIZONTAL ? x + w - totalButtonWidth : x; 301 302 textField.resizeRelocate(textFieldStartX, y, w - totalButtonWidth, h); 303 304 // decrement is always on the left 305 decrementArrowButton.resize(decrementArrowButtonWidth, h); 306 positionInArea(decrementArrowButton, buttonStartX, y, 307 decrementArrowButtonWidth, h, 0, HPos.CENTER, VPos.CENTER); 308 309 // ... and increment is always on the right 310 incrementArrowButton.resize(incrementArrowButtonWidth, h); 311 positionInArea(incrementArrowButton, buttonStartX + decrementArrowButtonWidth, y, 312 incrementArrowButtonWidth, h, 0, HPos.CENTER, VPos.CENTER); 313 } else if (layoutMode == SPLIT_ARROWS_VERTICAL) { 314 final double incrementArrowButtonHeight = incrementArrowButton.snappedTopInset() + 315 snapSizeY(incrementArrow.prefHeight(-1)) + incrementArrowButton.snappedBottomInset(); 316 317 final double decrementArrowButtonHeight = decrementArrowButton.snappedTopInset() + 318 snapSizeY(decrementArrow.prefHeight(-1)) + decrementArrowButton.snappedBottomInset(); 319 320 final double tallestArrowButton = Math.max(incrementArrowButtonHeight, decrementArrowButtonHeight); 321 322 // increment is at the top 323 incrementArrowButton.resize(w, tallestArrowButton); 324 positionInArea(incrementArrowButton, x, y, 325 w, tallestArrowButton, 0, HPos.CENTER, VPos.CENTER); 326 327 // textfield in the middle 328 textField.resizeRelocate(x, y + tallestArrowButton, w, h - (2*tallestArrowButton)); 329 330 // decrement is at the bottom 331 decrementArrowButton.resize(w, tallestArrowButton); 332 positionInArea(decrementArrowButton, x, h - tallestArrowButton, 333 w, tallestArrowButton, 0, HPos.CENTER, VPos.CENTER); 334 } else if (layoutMode == SPLIT_ARROWS_HORIZONTAL) { 335 // decrement is on the left-hand side 336 decrementArrowButton.resize(widestArrowButton, h); 337 positionInArea(decrementArrowButton, x, y, 338 widestArrowButton, h, 0, HPos.CENTER, VPos.CENTER); 339 340 // textfield in the middle 341 textField.resizeRelocate(x + widestArrowButton, y, w - (2*widestArrowButton), h); 342 343 // increment is on the right-hand side 344 incrementArrowButton.resize(widestArrowButton, h); 345 positionInArea(incrementArrowButton, w - widestArrowButton, y, 346 widestArrowButton, h, 0, HPos.CENTER, VPos.CENTER); 347 } 348 } 349 350 @Override 351 protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 352 return textField.minWidth(height); 353 } 354 355 /** {@inheritDoc} */ 356 @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 357 return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset); 358 } 359 360 /** {@inheritDoc} */ 361 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 362 final double textfieldWidth = textField.prefWidth(height); 363 return leftInset + textfieldWidth + rightInset; 364 } 365 366 /** {@inheritDoc} */ 367 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 368 double ph; 369 double textFieldHeight = textField.prefHeight(width); 370 371 if (layoutMode == SPLIT_ARROWS_VERTICAL) { 372 ph = topInset + incrementArrowButton.prefHeight(width) + 373 textFieldHeight + decrementArrowButton.prefHeight(width) + bottomInset; 374 } else { 375 ph = topInset + textFieldHeight + bottomInset; 376 } 377 378 return ph; 379 } 380 381 /** {@inheritDoc} */ 382 @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 383 return getSkinnable().prefWidth(height); 384 } 385 386 /** {@inheritDoc} */ 387 @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 388 return getSkinnable().prefHeight(width); 389 } 390 391 // Overridden so that we use the textfield as the baseline, rather than the arrow. 392 // See RT-30754 for more information. 393 /** {@inheritDoc} */ 394 @Override protected double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) { 395 return textField.getLayoutBounds().getMinY() + textField.getLayoutY() + textField.getBaselineOffset(); 396 } 397 398 399 400 /*************************************************************************** 401 * * 402 * Private implementation * 403 * * 404 **************************************************************************/ 405 406 private void updateStyleClass() { 407 final List<String> styleClass = getSkinnable().getStyleClass(); 408 409 if (styleClass.contains(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_VERTICAL)) { 410 layoutMode = ARROWS_ON_LEFT_VERTICAL; 411 } else if (styleClass.contains(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_HORIZONTAL)) { 412 layoutMode = ARROWS_ON_LEFT_HORIZONTAL; 413 } else if (styleClass.contains(Spinner.STYLE_CLASS_ARROWS_ON_RIGHT_HORIZONTAL)) { 414 layoutMode = ARROWS_ON_RIGHT_HORIZONTAL; 415 } else if (styleClass.contains(Spinner.STYLE_CLASS_SPLIT_ARROWS_VERTICAL)) { 416 layoutMode = SPLIT_ARROWS_VERTICAL; 417 } else if (styleClass.contains(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL)) { 418 layoutMode = SPLIT_ARROWS_HORIZONTAL; 419 } else { 420 layoutMode = ARROWS_ON_RIGHT_VERTICAL; 421 } 422 } 423 424 425 426 /*************************************************************************** 427 * * 428 * Stylesheet Handling * 429 * * 430 **************************************************************************/ 431 432 private static PseudoClass CONTAINS_FOCUS_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("contains-focus"); 433 }