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