1 /* 2 * Copyright (c) 2014, 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; 26 27 import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin; 28 import com.sun.javafx.scene.control.skin.SpinnerSkin; 29 import javafx.beans.NamedArg; 30 import javafx.beans.property.BooleanProperty; 31 import javafx.beans.property.ObjectProperty; 32 import javafx.beans.property.ReadOnlyObjectProperty; 33 import javafx.beans.property.ReadOnlyObjectWrapper; 34 import javafx.beans.property.SimpleBooleanProperty; 35 import javafx.beans.property.SimpleObjectProperty; 36 import javafx.collections.MapChangeListener; 37 import javafx.collections.ObservableList; 38 import javafx.scene.AccessibleAction; 39 import javafx.scene.AccessibleAttribute; 40 import javafx.scene.AccessibleRole; 41 import javafx.util.StringConverter; 42 43 import java.math.BigDecimal; 44 import java.time.LocalDate; 45 import java.time.LocalTime; 46 import java.time.temporal.TemporalUnit; 47 48 /** 49 * A single line text field that lets the user select a number or an object 50 * value from an ordered sequence. Spinners typically provide a pair of tiny 51 * arrow buttons for stepping through the elements of the sequence. The keyboard 52 * up/down arrow keys also cycle through the elements. The user may also be 53 * allowed to type a (legal) value directly into the spinner. Although combo 54 * boxes provide similar functionality, spinners are sometimes preferred because 55 * they don't require a drop down list that can obscure important data, and also 56 * because they allow for features such as 57 * {@link SpinnerValueFactory#wrapAroundProperty() wrapping} 58 * and simpler specification of 'infinite' data models (the 59 * {@link SpinnerValueFactory SpinnerValueFactory}, rather than using a 60 * {@link javafx.collections.ObservableList ObservableList} data model like many 61 * other JavaFX UI controls. 62 * 63 * <p>A Spinner's sequence value is defined by its 64 * {@link SpinnerValueFactory SpinnerValueFactory}. The value factory 65 * can be specified as a constructor argument and changed with the 66 * {@link #valueFactoryProperty() value factory property}. SpinnerValueFactory 67 * classes for some common types are provided with JavaFX, including: 68 * 69 * <br/> 70 * 71 * <ul> 72 * <li>{@link SpinnerValueFactory.IntegerSpinnerValueFactory}</li> 73 * <li>{@link SpinnerValueFactory.DoubleSpinnerValueFactory}</li> 74 * <li>{@link SpinnerValueFactory.ListSpinnerValueFactory}</li> 75 * </ul> 76 * 77 * <br/> 78 * 79 * <p>A Spinner has a TextField child component that is responsible for displaying 80 * and potentially changing the current {@link #valueProperty() value} of the 81 * Spinner, which is called the {@link #editorProperty() editor}. By default the 82 * Spinner is non-editable, but input can be accepted if the 83 * {@link #editableProperty() editable property} is set to true. The Spinner 84 * editor stays in sync with the value factory by listening for changes to the 85 * {@link SpinnerValueFactory#valueProperty() value property} of the value factory. 86 * If the user has changed the value displayed in the editor it is possible for 87 * the Spinner {@link #valueProperty() value} to differ from that of the editor. 88 * To make sure the model has the same value as the editor, the user must commit 89 * the edit using the Enter key. 90 * 91 * @see SpinnerValueFactory 92 * @param <T> The type of all values that can be iterated through in the Spinner. 93 * Common types include Integer and String. 94 * @since JavaFX 8u40 95 */ 96 public class Spinner<T> extends Control { 97 98 // default style class, puts arrows on right, stacked vertically 99 private static final String DEFAULT_STYLE_CLASS = "spinner"; 100 101 /** The arrows are placed on the right of the Spinner, pointing horizontally (i.e. left and right). */ 102 public static final String STYLE_CLASS_ARROWS_ON_RIGHT_HORIZONTAL = "arrows-on-right-horizontal"; 103 104 /** The arrows are placed on the left of the Spinner, pointing vertically (i.e. up and down). */ 105 public static final String STYLE_CLASS_ARROWS_ON_LEFT_VERTICAL = "arrows-on-left-vertical"; 106 107 /** The arrows are placed on the left of the Spinner, pointing horizontally (i.e. left and right). */ 108 public static final String STYLE_CLASS_ARROWS_ON_LEFT_HORIZONTAL = "arrows-on-left-horizontal"; 109 110 /** The arrows are placed above and beneath the spinner, stretching to take the entire width. */ 111 public static final String STYLE_CLASS_SPLIT_ARROWS_VERTICAL = "split-arrows-vertical"; 112 113 /** The decrement arrow is placed on the left of the Spinner, and the increment on the right. */ 114 public static final String STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL = "split-arrows-horizontal"; 115 116 117 118 /*************************************************************************** 119 * * 120 * Constructors * 121 * * 122 **************************************************************************/ 123 124 /** 125 * Constructs a default Spinner instance, with the default 'spinner' style 126 * class and a non-editable editor. 127 */ 128 public Spinner() { 129 getStyleClass().add(DEFAULT_STYLE_CLASS); 130 setAccessibleRole(AccessibleRole.SPINNER); 131 132 getEditor().setOnAction(action -> { 133 String text = getEditor().getText(); 134 SpinnerValueFactory<T> valueFactory = getValueFactory(); 135 if (valueFactory != null) { 136 StringConverter<T> converter = valueFactory.getConverter(); 137 if (converter != null) { 138 T value = converter.fromString(text); 139 valueFactory.setValue(value); 140 } 141 } 142 }); 143 144 getEditor().editableProperty().bind(editableProperty()); 145 146 value.addListener((o, oldValue, newValue) -> setText(newValue)); 147 148 // Fix for RT-29885 149 getProperties().addListener((MapChangeListener<Object, Object>) change -> { 150 if (change.wasAdded()) { 151 if (change.getKey() == "FOCUSED") { 152 setFocused((Boolean)change.getValueAdded()); 153 getProperties().remove("FOCUSED"); 154 } 155 } 156 }); 157 // End of fix for RT-29885 158 } 159 160 /** 161 * Creates a Spinner instance with the 162 * {@link #valueFactoryProperty() value factory} set to be an instance 163 * of {@link SpinnerValueFactory.IntegerSpinnerValueFactory}. Note that 164 * if this constructor is called, the only valid generic type for the 165 * Spinner instance is Integer, i.e. Spinner<Integer>. 166 * 167 * @param min The minimum allowed integer value for the Spinner. 168 * @param max The maximum allowed integer value for the Spinner. 169 * @param initialValue The value of the Spinner when first instantiated, must 170 * be within the bounds of the min and max arguments, or 171 * else the min value will be used. 172 */ 173 public Spinner(@NamedArg("min") int min, 174 @NamedArg("max") int max, 175 @NamedArg("initialValue") int initialValue) { 176 // This only works if the Spinner is of type Integer 177 this((SpinnerValueFactory<T>)new SpinnerValueFactory.IntegerSpinnerValueFactory(min, max, initialValue)); 178 } 179 180 /** 181 * Creates a Spinner instance with the 182 * {@link #valueFactoryProperty() value factory} set to be an instance 183 * of {@link SpinnerValueFactory.IntegerSpinnerValueFactory}. Note that 184 * if this constructor is called, the only valid generic type for the 185 * Spinner instance is Integer, i.e. Spinner<Integer>. 186 * 187 * @param min The minimum allowed integer value for the Spinner. 188 * @param max The maximum allowed integer value for the Spinner. 189 * @param initialValue The value of the Spinner when first instantiated, must 190 * be within the bounds of the min and max arguments, or 191 * else the min value will be used. 192 * @param amountToStepBy The amount to increment or decrement by, per step. 193 */ 194 public Spinner(@NamedArg("min") int min, 195 @NamedArg("max") int max, 196 @NamedArg("initialValue") int initialValue, 197 @NamedArg("amountToStepBy") int amountToStepBy) { 198 // This only works if the Spinner is of type Integer 199 this((SpinnerValueFactory<T>)new SpinnerValueFactory.IntegerSpinnerValueFactory(min, max, initialValue, amountToStepBy)); 200 } 201 202 /** 203 * Creates a Spinner instance with the 204 * {@link #valueFactoryProperty() value factory} set to be an instance 205 * of {@link SpinnerValueFactory.DoubleSpinnerValueFactory}. Note that 206 * if this constructor is called, the only valid generic type for the 207 * Spinner instance is Double, i.e. Spinner<Double>. 208 * 209 * @param min The minimum allowed double value for the Spinner. 210 * @param max The maximum allowed double value for the Spinner. 211 * @param initialValue The value of the Spinner when first instantiated, must 212 * be within the bounds of the min and max arguments, or 213 * else the min value will be used. 214 */ 215 public Spinner(@NamedArg("min") double min, 216 @NamedArg("max") double max, 217 @NamedArg("initialValue") double initialValue) { 218 // This only works if the Spinner is of type Double 219 this((SpinnerValueFactory<T>)new SpinnerValueFactory.DoubleSpinnerValueFactory(min, max, initialValue)); 220 } 221 222 /** 223 * Creates a Spinner instance with the 224 * {@link #valueFactoryProperty() value factory} set to be an instance 225 * of {@link SpinnerValueFactory.DoubleSpinnerValueFactory}. Note that 226 * if this constructor is called, the only valid generic type for the 227 * Spinner instance is Double, i.e. Spinner<Double>. 228 * 229 * @param min The minimum allowed double value for the Spinner. 230 * @param max The maximum allowed double value for the Spinner. 231 * @param initialValue The value of the Spinner when first instantiated, must 232 * be within the bounds of the min and max arguments, or 233 * else the min value will be used. 234 * @param amountToStepBy The amount to increment or decrement by, per step. 235 */ 236 public Spinner(@NamedArg("min") double min, 237 @NamedArg("max") double max, 238 @NamedArg("initialValue") double initialValue, 239 @NamedArg("amountToStepBy") double amountToStepBy) { 240 // This only works if the Spinner is of type Double 241 this((SpinnerValueFactory<T>)new SpinnerValueFactory.DoubleSpinnerValueFactory(min, max, initialValue, amountToStepBy)); 242 } 243 244 /** 245 * Creates a Spinner instance with the 246 * {@link #valueFactoryProperty() value factory} set to be an instance 247 * of {@link SpinnerValueFactory.LocalDateSpinnerValueFactory}. Note that 248 * if this constructor is called, the only valid generic type for the 249 * Spinner instance is LocalDate, i.e. Spinner<LocalDate>. 250 * 251 * @param min The minimum allowed LocalDate value for the Spinner. 252 * @param max The maximum allowed LocalDate value for the Spinner. 253 * @param initialValue The value of the Spinner when first instantiated, must 254 * be within the bounds of the min and max arguments, or 255 * else the min value will be used. 256 */ 257 Spinner(@NamedArg("min") LocalDate min, 258 @NamedArg("max") LocalDate max, 259 @NamedArg("initialValue") LocalDate initialValue) { 260 // This only works if the Spinner is of type LocalDate 261 this((SpinnerValueFactory<T>)new SpinnerValueFactory.LocalDateSpinnerValueFactory(min, max, initialValue)); 262 } 263 264 /** 265 * Creates a Spinner instance with the 266 * {@link #valueFactoryProperty() value factory} set to be an instance 267 * of {@link SpinnerValueFactory.LocalDateSpinnerValueFactory}. Note that 268 * if this constructor is called, the only valid generic type for the 269 * Spinner instance is LocalDate, i.e. Spinner<LocalDate>. 270 * 271 * @param min The minimum allowed LocalDate value for the Spinner. 272 * @param max The maximum allowed LocalDate value for the Spinner. 273 * @param initialValue The value of the Spinner when first instantiated, must 274 * be within the bounds of the min and max arguments, or 275 * else the min value will be used. 276 * @param amountToStepBy The amount to increment or decrement by, per step. 277 * @param temporalUnit The size of each step (e.g. day, week, month, year, etc). 278 */ 279 Spinner(@NamedArg("min") LocalDate min, 280 @NamedArg("max") LocalDate max, 281 @NamedArg("initialValue") LocalDate initialValue, 282 @NamedArg("amountToStepBy") long amountToStepBy, 283 @NamedArg("temporalUnit") TemporalUnit temporalUnit) { 284 // This only works if the Spinner is of type LocalDate 285 this((SpinnerValueFactory<T>)new SpinnerValueFactory.LocalDateSpinnerValueFactory(min, max, initialValue, amountToStepBy, temporalUnit)); 286 } 287 288 /** 289 * Creates a Spinner instance with the 290 * {@link #valueFactoryProperty() value factory} set to be an instance 291 * of {@link SpinnerValueFactory.LocalTimeSpinnerValueFactory}. Note that 292 * if this constructor is called, the only valid generic type for the 293 * Spinner instance is LocalTime, i.e. Spinner<LocalTime>. 294 * 295 * @param min The minimum allowed LocalTime value for the Spinner. 296 * @param max The maximum allowed LocalTime value for the Spinner. 297 * @param initialValue The value of the Spinner when first instantiated, must 298 * be within the bounds of the min and max arguments, or 299 * else the min value will be used. 300 */ 301 Spinner(@NamedArg("min") LocalTime min, 302 @NamedArg("max") LocalTime max, 303 @NamedArg("initialValue") LocalTime initialValue) { 304 // This only works if the Spinner is of type LocalTime 305 this((SpinnerValueFactory<T>)new SpinnerValueFactory.LocalTimeSpinnerValueFactory(min, max, initialValue)); 306 } 307 308 /** 309 * Creates a Spinner instance with the 310 * {@link #valueFactoryProperty() value factory} set to be an instance 311 * of {@link SpinnerValueFactory.LocalTimeSpinnerValueFactory}. Note that 312 * if this constructor is called, the only valid generic type for the 313 * Spinner instance is LocalTime, i.e. Spinner<LocalTime>. 314 * 315 * @param min The minimum allowed LocalTime value for the Spinner. 316 * @param max The maximum allowed LocalTime value for the Spinner. 317 * @param initialValue The value of the Spinner when first instantiated, must 318 * be within the bounds of the min and max arguments, or 319 * else the min value will be used. 320 * @param amountToStepBy The amount to increment or decrement by, per step. 321 * @param temporalUnit The size of each step (e.g. hour, minute, second, etc). 322 */ 323 Spinner(@NamedArg("min") LocalTime min, 324 @NamedArg("max") LocalTime max, 325 @NamedArg("initialValue") LocalTime initialValue, 326 @NamedArg("amountToStepBy") long amountToStepBy, 327 @NamedArg("temporalUnit") TemporalUnit temporalUnit) { 328 // This only works if the Spinner is of type LocalTime 329 this((SpinnerValueFactory<T>)new SpinnerValueFactory.LocalTimeSpinnerValueFactory(min, max, initialValue, amountToStepBy, temporalUnit)); 330 } 331 332 /** 333 * Creates a Spinner instance with the 334 * {@link #valueFactoryProperty() value factory} set to be an instance 335 * of {@link SpinnerValueFactory.ListSpinnerValueFactory}. The 336 * Spinner {@link #valueProperty() value property} will be set to the first 337 * element of the list, if an element exists, or null otherwise. 338 * 339 * @param items A list of items that will be stepped through in the Spinner. 340 */ 341 public Spinner(@NamedArg("items") ObservableList<T> items) { 342 this(new SpinnerValueFactory.ListSpinnerValueFactory<T>(items)); 343 } 344 345 /** 346 * Creates a Spinner instance with the given value factory set. 347 * 348 * @param valueFactory The {@link #valueFactoryProperty() value factory} to use. 349 */ 350 public Spinner(@NamedArg("valueFactory") SpinnerValueFactory<T> valueFactory) { 351 this(); 352 353 setValueFactory(valueFactory); 354 } 355 356 357 358 /*************************************************************************** 359 * * 360 * Public API * 361 * * 362 **************************************************************************/ 363 364 /** 365 * Attempts to increment the {@link #valueFactoryProperty() value factory} 366 * by one step, by calling the {@link SpinnerValueFactory#increment(int)} 367 * method with an argument of one. If the value factory is null, an 368 * IllegalStateException is thrown. 369 * 370 * @throws IllegalStateException if the value factory returned by 371 * calling {@link #getValueFactory()} is null. 372 */ 373 public void increment() { 374 increment(1); 375 } 376 377 /** 378 * Attempts to increment the {@link #valueFactoryProperty() value factory} 379 * by the given number of steps, by calling the 380 * {@link SpinnerValueFactory#increment(int)} 381 * method and forwarding the steps argument to it. If the value factory is 382 * null, an IllegalStateException is thrown. 383 * 384 * @param steps The number of increments that should be performed on the value. 385 * @throws IllegalStateException if the value factory returned by 386 * calling {@link #getValueFactory()} is null. 387 */ 388 public void increment(int steps) { 389 SpinnerValueFactory<T> valueFactory = getValueFactory(); 390 if (valueFactory == null) { 391 throw new IllegalStateException("Can't increment Spinner with a null SpinnerValueFactory"); 392 } 393 commitEditorText(); 394 valueFactory.increment(steps); 395 } 396 397 /** 398 * Attempts to decrement the {@link #valueFactoryProperty() value factory} 399 * by one step, by calling the {@link SpinnerValueFactory#decrement(int)} 400 * method with an argument of one. If the value factory is null, an 401 * IllegalStateException is thrown. 402 * 403 * @throws IllegalStateException if the value factory returned by 404 * calling {@link #getValueFactory()} is null. 405 */ 406 public void decrement() { 407 decrement(1); 408 } 409 410 /** 411 * Attempts to decrement the {@link #valueFactoryProperty() value factory} 412 * by the given number of steps, by calling the 413 * {@link SpinnerValueFactory#decrement(int)} 414 * method and forwarding the steps argument to it. If the value factory is 415 * null, an IllegalStateException is thrown. 416 * 417 * @param steps The number of decrements that should be performed on the value. 418 * @throws IllegalStateException if the value factory returned by 419 * calling {@link #getValueFactory()} is null. 420 */ 421 public void decrement(int steps) { 422 SpinnerValueFactory<T> valueFactory = getValueFactory(); 423 if (valueFactory == null) { 424 throw new IllegalStateException("Can't decrement Spinner with a null SpinnerValueFactory"); 425 } 426 commitEditorText(); 427 valueFactory.decrement(steps); 428 } 429 430 /** {@inheritDoc} */ 431 @Override protected Skin<?> createDefaultSkin() { 432 return new SpinnerSkin<>(this); 433 } 434 435 436 437 /*************************************************************************** 438 * * 439 * Properties * 440 * * 441 **************************************************************************/ 442 443 // --- value (a read only, bound property to the value factory value property) 444 /** 445 * The value property on Spinner is a read-only property, as it is bound to 446 * the SpinnerValueFactory 447 * {@link SpinnerValueFactory#valueProperty() value property}. Should the 448 * {@link #valueFactoryProperty() value factory} change, this value property 449 * will be unbound from the old value factory and bound to the new one. 450 * 451 * <p>If developers wish to modify the value property, they may do so with 452 * code in the following form: 453 * 454 * <pre> 455 * {@code 456 * Object newValue = ...; 457 * spinner.getValueFactory().setValue(newValue); 458 * }</pre> 459 */ 460 private ReadOnlyObjectWrapper<T> value = new ReadOnlyObjectWrapper<T>(this, "value"); 461 public final T getValue() { 462 return value.get(); 463 } 464 public final ReadOnlyObjectProperty<T> valueProperty() { 465 return value; 466 } 467 468 469 // --- valueFactory 470 /** 471 * The value factory is the model behind the JavaFX Spinner control - without 472 * a value factory installed a Spinner is unusable. It is the role of the 473 * value factory to handle almost all aspects of the Spinner, including: 474 * 475 * <ul> 476 * <li>Representing the current state of the {@link SpinnerValueFactory#valueProperty() value},</li> 477 * <li>{@link SpinnerValueFactory#increment(int) Incrementing} 478 * and {@link SpinnerValueFactory#decrement(int) decrementing} the 479 * value, with one or more steps per call,</li> 480 * <li>{@link SpinnerValueFactory#converterProperty() Converting} text input 481 * from the user (via the Spinner {@link #editorProperty() editor},</li> 482 * <li>Converting {@link SpinnerValueFactory#converterProperty() objects to user-readable strings} 483 * for display on screen</li> 484 * </ul> 485 */ 486 private ObjectProperty<SpinnerValueFactory<T>> valueFactory = 487 new SimpleObjectProperty<SpinnerValueFactory<T>>(this, "valueFactory") { 488 @Override protected void invalidated() { 489 value.unbind(); 490 491 SpinnerValueFactory<T> newFactory = get(); 492 if (newFactory != null) { 493 // this binding is what ensures the Spinner.valueProperty() 494 // properly represents the value in the value factory 495 value.bind(newFactory.valueProperty()); 496 } 497 } 498 }; 499 public final void setValueFactory(SpinnerValueFactory<T> value) { 500 valueFactory.setValue(value); 501 } 502 public final SpinnerValueFactory<T> getValueFactory() { 503 return valueFactory.get(); 504 } 505 public final ObjectProperty<SpinnerValueFactory<T>> valueFactoryProperty() { 506 return valueFactory; 507 } 508 509 510 // --- editable 511 /** 512 * The editable property is used to specify whether user input is able to 513 * be typed into the Spinner {@link #editorProperty() editor}. If editable 514 * is true, user input will be received once the user types and presses 515 * the Enter key. At this point the input is passed to the 516 * SpinnerValueFactory {@link SpinnerValueFactory#converterProperty() converter} 517 * {@link javafx.util.StringConverter#fromString(String)} method. 518 * The returned value from this call (of type T) is then sent to the 519 * {@link SpinnerValueFactory#setValue(Object)} method. If the value 520 * is valid, it will remain as the value. If it is invalid, the value factory 521 * will need to react accordingly and back out this change. 522 */ 523 private BooleanProperty editable; 524 public final void setEditable(boolean value) { 525 editableProperty().set(value); 526 } 527 public final boolean isEditable() { 528 return editable == null ? true : editable.get(); 529 } 530 public final BooleanProperty editableProperty() { 531 if (editable == null) { 532 editable = new SimpleBooleanProperty(this, "editable", false); 533 } 534 return editable; 535 } 536 537 538 // --- editor 539 /** 540 * The editor used by the Spinner control. 541 */ 542 public final ReadOnlyObjectProperty<TextField> editorProperty() { 543 if (editor == null) { 544 editor = new ReadOnlyObjectWrapper<TextField>(this, "editor"); 545 textField = new ComboBoxListViewSkin.FakeFocusTextField(); 546 editor.set(textField); 547 } 548 return editor.getReadOnlyProperty(); 549 } 550 private TextField textField; 551 private ReadOnlyObjectWrapper<TextField> editor; 552 public final TextField getEditor() { 553 return editorProperty().get(); 554 } 555 556 557 558 /*************************************************************************** 559 * * 560 * Implementation * 561 * * 562 **************************************************************************/ 563 564 /* 565 * Update the TextField based on the current value 566 */ 567 private void setText(T value) { 568 String text = null; 569 570 SpinnerValueFactory<T> valueFactory = getValueFactory(); 571 if (valueFactory != null) { 572 StringConverter<T> converter = valueFactory.getConverter(); 573 if (converter != null) { 574 text = converter.toString(value); 575 } 576 } 577 578 notifyAccessibleAttributeChanged(AccessibleAttribute.TEXT); 579 if (text == null) { 580 if (value == null) { 581 getEditor().clear(); 582 return; 583 } else { 584 text = value.toString(); 585 } 586 } 587 588 getEditor().setText(text); 589 } 590 591 /* 592 * Convenience method to support wrapping values around their min / max 593 * constraints. Used by the SpinnerValueFactory implementations when 594 * the Spinner wrapAround property is true. 595 */ 596 static int wrapValue(int value, int min, int max) { 597 if (max == 0) { 598 throw new RuntimeException(); 599 } 600 601 int r = value % max; 602 if (r > min && max < min) { 603 r = r + max - min; 604 } else if (r < min && max > min) { 605 r = r + max - min; 606 } 607 return r; 608 } 609 610 /* 611 * Convenience method to support wrapping values around their min / max 612 * constraints. Used by the SpinnerValueFactory implementations when 613 * the Spinner wrapAround property is true. 614 */ 615 static BigDecimal wrapValue(BigDecimal value, BigDecimal min, BigDecimal max) { 616 if (max.doubleValue() == 0) { 617 throw new RuntimeException(); 618 } 619 620 // note that this wrap method differs from the others where we take the 621 // difference - in this approach we wrap to the min or max - it feels better 622 // to go from 1 to 0, rather than 1 to 0.05 (where max is 1 and step is 0.05). 623 if (value.compareTo(min) < 0) { 624 return max; 625 } else if (value.compareTo(max) > 0) { 626 return min; 627 } 628 return value; 629 } 630 631 private void commitEditorText() { 632 if (!isEditable()) return; 633 String text = getEditor().getText(); 634 SpinnerValueFactory<T> valueFactory = getValueFactory(); 635 if (valueFactory != null) { 636 StringConverter<T> converter = valueFactory.getConverter(); 637 if (converter != null) { 638 T value = converter.fromString(text); 639 valueFactory.setValue(value); 640 } 641 } 642 } 643 644 645 /*************************************************************************** 646 * * 647 * Accessibility handling * 648 * * 649 **************************************************************************/ 650 651 @Override 652 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 653 switch (attribute) { 654 case TEXT: { 655 T value = getValue(); 656 SpinnerValueFactory<T> factory = getValueFactory(); 657 if (factory != null) { 658 StringConverter<T> converter = factory.getConverter(); 659 if (converter != null) { 660 return converter.toString(value); 661 } 662 } 663 return value != null ? value.toString() : ""; 664 } 665 default: return super.queryAccessibleAttribute(attribute, parameters); 666 } 667 } 668 669 @Override 670 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 671 switch (action) { 672 case INCREMENT: 673 increment(); 674 break; 675 case DECREMENT: 676 decrement(); 677 break; 678 default: super.executeAccessibleAction(action); 679 } 680 } 681 682 }