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 }