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