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&lt;Integer&gt;.
 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&lt;Integer&gt;.
 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&lt;Double&gt;.
 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&lt;Double&gt;.
 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&lt;LocalDate&gt;.
 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&lt;LocalDate&gt;.
 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&lt;LocalTime&gt;.
 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&lt;LocalTime&gt;.
 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 }