1 /* 2 * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 package javafx.scene.control; 26 27 import com.sun.javafx.scene.control.skin.ListViewSkin; 28 import javafx.beans.NamedArg; 29 import javafx.beans.property.BooleanProperty; 30 import javafx.beans.property.DoubleProperty; 31 import javafx.beans.property.IntegerProperty; 32 import javafx.beans.property.LongProperty; 33 import javafx.beans.property.ObjectProperty; 34 import javafx.beans.property.SimpleBooleanProperty; 35 import javafx.beans.property.SimpleDoubleProperty; 36 import javafx.beans.property.SimpleIntegerProperty; 37 import javafx.beans.property.SimpleLongProperty; 38 import javafx.beans.property.SimpleObjectProperty; 39 import javafx.beans.value.ChangeListener; 40 import javafx.beans.value.WeakChangeListener; 41 import javafx.collections.ListChangeListener; 42 import javafx.collections.ObservableList; 43 import javafx.collections.WeakListChangeListener; 44 import javafx.util.StringConverter; 45 import javafx.util.converter.IntegerStringConverter; 46 47 import java.lang.ref.WeakReference; 48 import java.math.BigDecimal; 49 import java.text.DecimalFormat; 50 import java.text.NumberFormat; 51 import java.text.ParseException; 52 import java.time.Duration; 53 import java.time.Instant; 54 import java.time.LocalDate; 55 import java.time.LocalTime; 56 import java.time.format.DateTimeFormatter; 57 import java.time.format.FormatStyle; 58 import java.time.temporal.ChronoUnit; 59 import java.time.temporal.Temporal; 60 import java.time.temporal.TemporalField; 61 import java.time.temporal.TemporalUnit; 62 import java.util.List; 63 64 /** 65 * The SpinnerValueFactory is the model behind the JavaFX 66 * {@link Spinner Spinner control} - without a value factory installed a 67 * Spinner is unusable. It is the role of the value factory to handle almost all 68 * aspects of the Spinner, including: 69 * 70 * <ul> 71 * <li>Representing the current state of the {@link javafx.scene.control.SpinnerValueFactory#valueProperty() value},</li> 72 * <li>{@link SpinnerValueFactory#increment(int) Incrementing} 73 * and {@link SpinnerValueFactory#decrement(int) decrementing} the 74 * value, with one or more steps per call,</li> 75 * <li>{@link javafx.scene.control.SpinnerValueFactory#converterProperty() Converting} text input 76 * from the user (via the Spinner {@link Spinner#editorProperty() editor},</li> 77 * <li>Converting {@link javafx.scene.control.SpinnerValueFactory#converterProperty() objects to user-readable strings} 78 * for display on screen</li> 79 * </ul> 80 * 81 * <p>SpinnerValueFactory classes for some common types are provided with JavaFX, including: 82 * 83 * <br/> 84 * 85 * <ul> 86 * <li>{@link SpinnerValueFactory.IntegerSpinnerValueFactory}</li> 87 * <li>{@link SpinnerValueFactory.DoubleSpinnerValueFactory}</li> 88 * <li>{@link SpinnerValueFactory.ListSpinnerValueFactory}</li> 89 * </ul> 90 * 91 * @param <T> The type of the data this value factory deals with, which must 92 * coincide with the type of the Spinner that the value factory is set on. 93 * @see Spinner 94 * @see SpinnerValueFactory.IntegerSpinnerValueFactory 95 * @see SpinnerValueFactory.DoubleSpinnerValueFactory 96 * @see SpinnerValueFactory.ListSpinnerValueFactory 97 * @since JavaFX 8u40 98 */ 99 public abstract class SpinnerValueFactory<T> { 100 101 /*************************************************************************** 102 * * 103 * Private fields * 104 * * 105 **************************************************************************/ 106 107 108 109 /*************************************************************************** 110 * * 111 * Abstract methods * 112 * * 113 **************************************************************************/ 114 115 /** 116 * Attempts to decrement the {@link #valueProperty() value} by the given 117 * number of steps. 118 * 119 * @param steps The number of decrements that should be performed on the value. 120 */ 121 public abstract void decrement(int steps); 122 123 124 /** 125 * Attempts to omcrement the {@link #valueProperty() value} by the given 126 * number of steps. 127 * 128 * @param steps The number of increments that should be performed on the value. 129 */ 130 public abstract void increment(int steps); 131 132 133 134 /*************************************************************************** 135 * * 136 * Properties * 137 * * 138 **************************************************************************/ 139 140 // --- value 141 /** 142 * Represents the current value of the SpinnerValueFactory, or null if no 143 * value has been set. 144 */ 145 private ObjectProperty<T> value = new SimpleObjectProperty<>(this, "value"); 146 public final T getValue() { 147 return value.get(); 148 } 149 public final void setValue(T newValue) { 150 value.set(newValue); 151 } 152 public final ObjectProperty<T> valueProperty() { 153 return value; 154 } 155 156 157 // --- converter 158 /** 159 * Converts the user-typed input (when the Spinner is 160 * {@link Spinner#editableProperty() editable}) to an object of type T, 161 * such that the input may be retrieved via the {@link #valueProperty() value} 162 * property. 163 */ 164 private ObjectProperty<StringConverter<T>> converter = new SimpleObjectProperty<>(this, "converter"); 165 public final StringConverter<T> getConverter() { 166 return converter.get(); 167 } 168 public final void setConverter(StringConverter<T> newValue) { 169 converter.set(newValue); 170 } 171 public final ObjectProperty<StringConverter<T>> converterProperty() { 172 return converter; 173 } 174 175 176 // --- wrapAround 177 /** 178 * The wrapAround property is used to specify whether the value factory should 179 * be circular. For example, should an integer-based value model increment 180 * from the maximum value back to the minimum value (and vice versa). 181 */ 182 private BooleanProperty wrapAround; 183 public final void setWrapAround(boolean value) { 184 wrapAroundProperty().set(value); 185 } 186 public final boolean isWrapAround() { 187 return wrapAround == null ? false : wrapAround.get(); 188 } 189 public final BooleanProperty wrapAroundProperty() { 190 if (wrapAround == null) { 191 wrapAround = new SimpleBooleanProperty(this, "wrapAround", false); 192 } 193 return wrapAround; 194 } 195 196 197 198 /*************************************************************************** 199 * * 200 * Subclasses of SpinnerValueFactory * 201 * * 202 **************************************************************************/ 203 204 /** 205 * A {@link javafx.scene.control.SpinnerValueFactory} implementation designed to iterate through 206 * a list of values. 207 * 208 * <p>Note that the default {@link #converterProperty() converter} is implemented 209 * simply as shown below, which may be adequate in many cases, but it is important 210 * for users to ensure that this suits their needs (and adjust when necessary): 211 * 212 * <pre> 213 * setConverter(new StringConverter<T>() { 214 * @Override public String toString(T value) { 215 * if (value == null) { 216 * return ""; 217 * } 218 * return value.toString(); 219 * } 220 * 221 * @Override public T fromString(String string) { 222 * return (T) string; 223 * } 224 * });</pre> 225 * 226 * @param <T> The type of the elements in the {@link java.util.List}. 227 * @since JavaFX 8u40 228 */ 229 public static class ListSpinnerValueFactory<T> extends SpinnerValueFactory<T> { 230 231 /*********************************************************************** 232 * * 233 * Private fields * 234 * * 235 **********************************************************************/ 236 237 private int currentIndex = 0; 238 239 private final ListChangeListener<T> itemsContentObserver = c -> { 240 // the items content has changed. We do not try to find the current 241 // item, instead we remain at the currentIndex, if possible, or else 242 // we go back to index 0, and if that fails, we go to null 243 updateCurrentIndex(); 244 }; 245 246 private WeakListChangeListener<T> weakItemsContentObserver = 247 new WeakListChangeListener<T>(itemsContentObserver); 248 249 250 251 /*********************************************************************** 252 * * 253 * Constructors * 254 * * 255 **********************************************************************/ 256 257 /** 258 * Creates a new instance of the ListSpinnerValueFactory with the given 259 * list used as the list to step through. 260 * 261 * @param items The list of items to step through with the Spinner. 262 */ 263 public ListSpinnerValueFactory(@NamedArg("items") ObservableList<T> items) { 264 setItems(items); 265 setConverter(new StringConverter<T>() { 266 @Override public String toString(T value) { 267 if (value == null) { 268 return ""; 269 } 270 return value.toString(); 271 } 272 273 @Override public T fromString(String string) { 274 return (T) string; 275 } 276 }); 277 278 valueProperty().addListener((o, oldValue, newValue) -> { 279 // when the value is set, we need to react to ensure it is a 280 // valid value (and if not, blow up appropriately) 281 int newIndex = -1; 282 if (items.contains(newValue)) { 283 newIndex = items.indexOf(newValue); 284 } else { 285 // add newValue to list 286 items.add(newValue); 287 newIndex = items.indexOf(newValue); 288 } 289 currentIndex = newIndex; 290 }); 291 setValue(_getValue(currentIndex)); 292 } 293 294 295 296 /*********************************************************************** 297 * * 298 * Properties * 299 * * 300 **********************************************************************/ 301 // --- Items 302 private ObjectProperty<ObservableList<T>> items; 303 304 /** 305 * Sets the underlying data model for the ListSpinnerValueFactory. Note that it has a generic 306 * type that must match the type of the Spinner itself. 307 */ 308 public final void setItems(ObservableList<T> value) { 309 itemsProperty().set(value); 310 } 311 312 /** 313 * Returns an {@link javafx.collections.ObservableList} that contains the items currently able 314 * to be iterated through by the user. This may be null if 315 * {@link #setItems(javafx.collections.ObservableList)} has previously been 316 * called, however, by default it is an empty ObservableList. 317 * 318 * @return An ObservableList containing the items to be shown to the user, or 319 * null if the items have previously been set to null. 320 */ 321 public final ObservableList<T> getItems() { 322 return items == null ? null : items.get(); 323 } 324 325 /** 326 * The underlying data model for the ListView. Note that it has a generic 327 * type that must match the type of the ListView itself. 328 */ 329 public final ObjectProperty<ObservableList<T>> itemsProperty() { 330 if (items == null) { 331 items = new SimpleObjectProperty<ObservableList<T>>(this, "items") { 332 WeakReference<ObservableList<T>> oldItemsRef; 333 334 @Override protected void invalidated() { 335 ObservableList<T> oldItems = oldItemsRef == null ? null : oldItemsRef.get(); 336 ObservableList<T> newItems = getItems(); 337 338 // update listeners 339 if (oldItems != null) { 340 oldItems.removeListener(weakItemsContentObserver); 341 } 342 if (newItems != null) { 343 newItems.addListener(weakItemsContentObserver); 344 } 345 346 // update the current value based on the index 347 updateCurrentIndex(); 348 349 oldItemsRef = new WeakReference<>(getItems()); 350 } 351 }; 352 } 353 return items; 354 } 355 356 357 358 /*********************************************************************** 359 * * 360 * Overridden methods * 361 * * 362 **********************************************************************/ 363 364 /** {@inheritDoc} */ 365 @Override public void decrement(int steps) { 366 final int max = getItemsSize() - 1; 367 int newIndex = currentIndex - steps; 368 currentIndex = newIndex >= 0 ? newIndex : (isWrapAround() ? Spinner.wrapValue(newIndex, 0, max + 1) : 0); 369 setValue(_getValue(currentIndex)); 370 } 371 372 /** {@inheritDoc} */ 373 @Override public void increment(int steps) { 374 final int max = getItemsSize() - 1; 375 int newIndex = currentIndex + steps; 376 currentIndex = newIndex <= max ? newIndex : (isWrapAround() ? Spinner.wrapValue(newIndex, 0, max + 1) : max); 377 setValue(_getValue(currentIndex)); 378 } 379 380 381 382 /*********************************************************************** 383 * * 384 * Private implementation * 385 * * 386 **********************************************************************/ 387 private int getItemsSize() { 388 List<T> items = getItems(); 389 return items == null ? 0 : items.size(); 390 } 391 392 private void updateCurrentIndex() { 393 int itemsSize = getItemsSize(); 394 if (currentIndex < 0 || currentIndex >= itemsSize) { 395 currentIndex = 0; 396 } 397 setValue(_getValue(currentIndex)); 398 } 399 400 private T _getValue(int index) { 401 List<T> items = getItems(); 402 return items == null ? null : (index >= 0 && index < items.size()) ? items.get(index) : null; 403 } 404 } 405 406 407 408 /** 409 * A {@link javafx.scene.control.SpinnerValueFactory} implementation designed to iterate through 410 * integer values. 411 * 412 * <p>Note that the default {@link #converterProperty() converter} is implemented 413 * as an {@link javafx.util.converter.IntegerStringConverter} instance. 414 * 415 * @since JavaFX 8u40 416 */ 417 public static class IntegerSpinnerValueFactory extends SpinnerValueFactory<Integer> { 418 419 /*********************************************************************** 420 * * 421 * Constructors * 422 * * 423 **********************************************************************/ 424 425 /** 426 * Constructs a new IntegerSpinnerValueFactory that sets the initial value 427 * to be equal to the min value, and a default {@code amountToStepBy} of one. 428 * 429 * @param min The minimum allowed integer value for the Spinner. 430 * @param max The maximum allowed integer value for the Spinner. 431 */ 432 public IntegerSpinnerValueFactory(@NamedArg("min") int min, 433 @NamedArg("max") int max) { 434 this(min, max, min); 435 } 436 437 /** 438 * Constructs a new IntegerSpinnerValueFactory with a default 439 * {@code amountToStepBy} of one. 440 * 441 * @param min The minimum allowed integer value for the Spinner. 442 * @param max The maximum allowed integer value for the Spinner. 443 * @param initialValue The value of the Spinner when first instantiated, must 444 * be within the bounds of the min and max arguments, or 445 * else the min value will be used. 446 */ 447 public IntegerSpinnerValueFactory(@NamedArg("min") int min, 448 @NamedArg("max") int max, 449 @NamedArg("initialValue") int initialValue) { 450 this(min, max, initialValue, 1); 451 } 452 453 /** 454 * Constructs a new IntegerSpinnerValueFactory. 455 * 456 * @param min The minimum allowed integer value for the Spinner. 457 * @param max The maximum allowed integer value for the Spinner. 458 * @param initialValue The value of the Spinner when first instantiated, must 459 * be within the bounds of the min and max arguments, or 460 * else the min value will be used. 461 * @param amountToStepBy The amount to increment or decrement by, per step. 462 */ 463 public IntegerSpinnerValueFactory(@NamedArg("min") int min, 464 @NamedArg("max") int max, 465 @NamedArg("initialValue") int initialValue, 466 @NamedArg("amountToStepBy") int amountToStepBy) { 467 setMin(min); 468 setMax(max); 469 setAmountToStepBy(amountToStepBy); 470 setConverter(new IntegerStringConverter()); 471 472 valueProperty().addListener((o, oldValue, newValue) -> { 473 // when the value is set, we need to react to ensure it is a 474 // valid value (and if not, blow up appropriately) 475 if (newValue < getMin()) { 476 setValue(getMin()); 477 } else if (newValue > getMax()) { 478 setValue(getMax()); 479 } 480 }); 481 setValue(initialValue >= min && initialValue <= max ? initialValue : min); 482 } 483 484 485 /*********************************************************************** 486 * * 487 * Properties * 488 * * 489 **********************************************************************/ 490 491 // --- min 492 private IntegerProperty min = new SimpleIntegerProperty(this, "min") { 493 @Override protected void invalidated() { 494 Integer currentValue = IntegerSpinnerValueFactory.this.getValue(); 495 if (currentValue == null) { 496 return; 497 } 498 499 int newMin = get(); 500 if (newMin > getMax()) { 501 setMin(getMax()); 502 return; 503 } 504 505 if (currentValue < newMin) { 506 IntegerSpinnerValueFactory.this.setValue(newMin); 507 } 508 } 509 }; 510 511 public final void setMin(int value) { 512 min.set(value); 513 } 514 public final int getMin() { 515 return min.get(); 516 } 517 /** 518 * Sets the minimum allowable value for this value factory 519 */ 520 public final IntegerProperty minProperty() { 521 return min; 522 } 523 524 // --- max 525 private IntegerProperty max = new SimpleIntegerProperty(this, "max") { 526 @Override protected void invalidated() { 527 Integer currentValue = IntegerSpinnerValueFactory.this.getValue(); 528 if (currentValue == null) { 529 return; 530 } 531 532 int newMax = get(); 533 if (newMax < getMin()) { 534 setMax(getMin()); 535 return; 536 } 537 538 if (currentValue > newMax) { 539 IntegerSpinnerValueFactory.this.setValue(newMax); 540 } 541 } 542 }; 543 544 public final void setMax(int value) { 545 max.set(value); 546 } 547 public final int getMax() { 548 return max.get(); 549 } 550 /** 551 * Sets the maximum allowable value for this value factory 552 */ 553 public final IntegerProperty maxProperty() { 554 return max; 555 } 556 557 // --- amountToStepBy 558 private IntegerProperty amountToStepBy = new SimpleIntegerProperty(this, "amountToStepBy"); 559 public final void setAmountToStepBy(int value) { 560 amountToStepBy.set(value); 561 } 562 public final int getAmountToStepBy() { 563 return amountToStepBy.get(); 564 } 565 /** 566 * Sets the amount to increment or decrement by, per step. 567 */ 568 public final IntegerProperty amountToStepByProperty() { 569 return amountToStepBy; 570 } 571 572 573 574 /*********************************************************************** 575 * * 576 * Overridden methods * 577 * * 578 **********************************************************************/ 579 580 /** {@inheritDoc} */ 581 @Override public void decrement(int steps) { 582 final int min = getMin(); 583 final int max = getMax(); 584 final int newIndex = getValue() - steps * getAmountToStepBy(); 585 setValue(newIndex >= min ? newIndex : (isWrapAround() ? Spinner.wrapValue(newIndex, min, max) + 1 : min)); 586 } 587 588 /** {@inheritDoc} */ 589 @Override public void increment(int steps) { 590 final int min = getMin(); 591 final int max = getMax(); 592 final int currentValue = getValue(); 593 final int newIndex = currentValue + steps * getAmountToStepBy(); 594 setValue(newIndex <= max ? newIndex : (isWrapAround() ? Spinner.wrapValue(newIndex, min, max) - 1 : max)); 595 } 596 } 597 598 599 600 /** 601 * A {@link javafx.scene.control.SpinnerValueFactory} implementation designed to iterate through 602 * double values. 603 * 604 * <p>Note that the default {@link #converterProperty() converter} is implemented 605 * simply as shown below, which may be adequate in many cases, but it is important 606 * for users to ensure that this suits their needs (and adjust when necessary). The 607 * main point to note is that this {@link javafx.util.StringConverter} embeds 608 * within it a {@link java.text.DecimalFormat} instance that shows the Double 609 * to two decimal places. This is used for both the toString and fromString 610 * methods: 611 * 612 * <pre> 613 * setConverter(new StringConverter<Double>() { 614 * private final DecimalFormat df = new DecimalFormat("#.##"); 615 * 616 * @Override public String toString(Double value) { 617 * // If the specified value is null, return a zero-length String 618 * if (value == null) { 619 * return ""; 620 * } 621 * 622 * return df.format(value); 623 * } 624 * 625 * @Override public Double fromString(String value) { 626 * try { 627 * // If the specified value is null or zero-length, return null 628 * if (value == null) { 629 * return null; 630 * } 631 * 632 * value = value.trim(); 633 * 634 * if (value.length() < 1) { 635 * return null; 636 * } 637 * 638 * // Perform the requested parsing 639 * return df.parse(value).doubleValue(); 640 * } catch (ParseException ex) { 641 * throw new RuntimeException(ex); 642 * } 643 * } 644 * });</pre> 645 * 646 * @since JavaFX 8u40 647 */ 648 public static class DoubleSpinnerValueFactory extends SpinnerValueFactory<Double> { 649 650 /** 651 * Constructs a new DoubleSpinnerValueFactory that sets the initial value 652 * to be equal to the min value, and a default {@code amountToStepBy} of 653 * one. 654 * 655 * @param min The minimum allowed double value for the Spinner. 656 * @param max The maximum allowed double value for the Spinner. 657 */ 658 public DoubleSpinnerValueFactory(@NamedArg("min") double min, 659 @NamedArg("max") double max) { 660 this(min, max, min); 661 } 662 663 /** 664 * Constructs a new DoubleSpinnerValueFactory with a default 665 * {@code amountToStepBy} of one. 666 * 667 * @param min The minimum allowed double value for the Spinner. 668 * @param max The maximum allowed double value for the Spinner. 669 * @param initialValue The value of the Spinner when first instantiated, must 670 * be within the bounds of the min and max arguments, or 671 * else the min value will be used. 672 */ 673 public DoubleSpinnerValueFactory(@NamedArg("min") double min, 674 @NamedArg("max") double max, 675 @NamedArg("initialValue") double initialValue) { 676 this(min, max, initialValue, 1); 677 } 678 679 /** 680 * Constructs a new DoubleSpinnerValueFactory. 681 * 682 * @param min The minimum allowed double value for the Spinner. 683 * @param max The maximum allowed double value for the Spinner. 684 * @param initialValue The value of the Spinner when first instantiated, must 685 * be within the bounds of the min and max arguments, or 686 * else the min value will be used. 687 * @param amountToStepBy The amount to increment or decrement by, per step. 688 */ 689 public DoubleSpinnerValueFactory(@NamedArg("min") double min, 690 @NamedArg("max") double max, 691 @NamedArg("initialValue") double initialValue, 692 @NamedArg("amountToStepBy") double amountToStepBy) { 693 setMin(min); 694 setMax(max); 695 setAmountToStepBy(amountToStepBy); 696 setConverter(new StringConverter<Double>() { 697 private final DecimalFormat df = new DecimalFormat("#.##"); 698 699 @Override public String toString(Double value) { 700 // If the specified value is null, return a zero-length String 701 if (value == null) { 702 return ""; 703 } 704 705 return df.format(value); 706 } 707 708 @Override public Double fromString(String value) { 709 try { 710 // If the specified value is null or zero-length, return null 711 if (value == null) { 712 return null; 713 } 714 715 value = value.trim(); 716 717 if (value.length() < 1) { 718 return null; 719 } 720 721 // Perform the requested parsing 722 return df.parse(value).doubleValue(); 723 } catch (ParseException ex) { 724 throw new RuntimeException(ex); 725 } 726 } 727 }); 728 729 valueProperty().addListener((o, oldValue, newValue) -> { 730 // when the value is set, we need to react to ensure it is a 731 // valid value (and if not, blow up appropriately) 732 if (newValue < getMin()) { 733 setValue(getMin()); 734 } else if (newValue > getMax()) { 735 setValue(getMax()); 736 } 737 }); 738 setValue(initialValue >= min && initialValue <= max ? initialValue : min); 739 } 740 741 742 743 /*********************************************************************** 744 * * 745 * Properties * 746 * * 747 **********************************************************************/ 748 749 // --- min 750 private DoubleProperty min = new SimpleDoubleProperty(this, "min") { 751 @Override protected void invalidated() { 752 Double currentValue = DoubleSpinnerValueFactory.this.getValue(); 753 if (currentValue == null) { 754 return; 755 } 756 757 final double newMin = get(); 758 if (newMin > getMax()) { 759 setMin(getMax()); 760 return; 761 } 762 763 if (currentValue < newMin) { 764 DoubleSpinnerValueFactory.this.setValue(newMin); 765 } 766 } 767 }; 768 769 public final void setMin(double value) { 770 min.set(value); 771 } 772 public final double getMin() { 773 return min.get(); 774 } 775 /** 776 * Sets the minimum allowable value for this value factory 777 */ 778 public final DoubleProperty minProperty() { 779 return min; 780 } 781 782 // --- max 783 private DoubleProperty max = new SimpleDoubleProperty(this, "max") { 784 @Override protected void invalidated() { 785 Double currentValue = DoubleSpinnerValueFactory.this.getValue(); 786 if (currentValue == null) { 787 return; 788 } 789 790 final double newMax = get(); 791 if (newMax < getMin()) { 792 setMax(getMin()); 793 return; 794 } 795 796 if (currentValue > newMax) { 797 DoubleSpinnerValueFactory.this.setValue(newMax); 798 } 799 } 800 }; 801 802 public final void setMax(double value) { 803 max.set(value); 804 } 805 public final double getMax() { 806 return max.get(); 807 } 808 /** 809 * Sets the maximum allowable value for this value factory 810 */ 811 public final DoubleProperty maxProperty() { 812 return max; 813 } 814 815 // --- amountToStepBy 816 private DoubleProperty amountToStepBy = new SimpleDoubleProperty(this, "amountToStepBy"); 817 public final void setAmountToStepBy(double value) { 818 amountToStepBy.set(value); 819 } 820 public final double getAmountToStepBy() { 821 return amountToStepBy.get(); 822 } 823 /** 824 * Sets the amount to increment or decrement by, per step. 825 */ 826 public final DoubleProperty amountToStepByProperty() { 827 return amountToStepBy; 828 } 829 830 831 832 /** {@inheritDoc} */ 833 @Override public void decrement(int steps) { 834 final BigDecimal currentValue = BigDecimal.valueOf(getValue()); 835 final BigDecimal minBigDecimal = BigDecimal.valueOf(getMin()); 836 final BigDecimal maxBigDecimal = BigDecimal.valueOf(getMax()); 837 final BigDecimal amountToStepByBigDecimal = BigDecimal.valueOf(getAmountToStepBy()); 838 BigDecimal newValue = currentValue.subtract(amountToStepByBigDecimal.multiply(BigDecimal.valueOf(steps))); 839 setValue(newValue.compareTo(minBigDecimal) >= 0 ? newValue.doubleValue() : 840 (isWrapAround() ? Spinner.wrapValue(newValue, minBigDecimal, maxBigDecimal).doubleValue() : getMin())); 841 } 842 843 /** {@inheritDoc} */ 844 @Override public void increment(int steps) { 845 final BigDecimal currentValue = BigDecimal.valueOf(getValue()); 846 final BigDecimal minBigDecimal = BigDecimal.valueOf(getMin()); 847 final BigDecimal maxBigDecimal = BigDecimal.valueOf(getMax()); 848 final BigDecimal amountToStepByBigDecimal = BigDecimal.valueOf(getAmountToStepBy()); 849 BigDecimal newValue = currentValue.add(amountToStepByBigDecimal.multiply(BigDecimal.valueOf(steps))); 850 setValue(newValue.compareTo(maxBigDecimal) <= 0 ? newValue.doubleValue() : 851 (isWrapAround() ? Spinner.wrapValue(newValue, minBigDecimal, maxBigDecimal).doubleValue() : getMax())); 852 } 853 } 854 855 /** 856 * A {@link javafx.scene.control.SpinnerValueFactory} implementation designed to iterate through 857 * {@link java.time.LocalDate} values. 858 * 859 * <p>Note that the default {@link #converterProperty() converter} is implemented 860 * simply as shown below, which may be adequate in many cases, but it is important 861 * for users to ensure that this suits their needs (and adjust when necessary): 862 * 863 * <pre> 864 * setConverter(new StringConverter<LocalDate>() { 865 * @Override public String toString(LocalDate object) { 866 * if (object == null) { 867 * return ""; 868 * } 869 * return object.toString(); 870 * } 871 * 872 * @Override public LocalDate fromString(String string) { 873 * return LocalDate.parse(string); 874 * } 875 * });</pre> 876 */ 877 static class LocalDateSpinnerValueFactory extends SpinnerValueFactory<LocalDate> { 878 879 /** 880 * Creates a new instance of the LocalDateSpinnerValueFactory, using the 881 * value returned by calling {@code LocalDate#now()} as the initial value, 882 * and using a stepping amount of one day. 883 */ 884 public LocalDateSpinnerValueFactory() { 885 this(LocalDate.now()); 886 } 887 888 /** 889 * Creates a new instance of the LocalDateSpinnerValueFactory, using the 890 * provided initial value, and a stepping amount of one day. 891 * 892 * @param initialValue The value of the Spinner when first instantiated. 893 */ 894 public LocalDateSpinnerValueFactory(@NamedArg("initialValue") LocalDate initialValue) { 895 this(LocalDate.MIN, LocalDate.MAX, initialValue); 896 } 897 898 /** 899 * Creates a new instance of the LocalDateSpinnerValueFactory, using the 900 * provided initial value, and a stepping amount of one day. 901 * 902 * @param min The minimum allowed double value for the Spinner. 903 * @param max The maximum allowed double value for the Spinner. 904 * @param initialValue The value of the Spinner when first instantiated. 905 */ 906 public LocalDateSpinnerValueFactory(@NamedArg("min") LocalDate min, 907 @NamedArg("min") LocalDate max, 908 @NamedArg("initialValue") LocalDate initialValue) { 909 this(min, max, initialValue, 1, ChronoUnit.DAYS); 910 } 911 912 /** 913 * Creates a new instance of the LocalDateSpinnerValueFactory, using the 914 * provided min, max, and initial values, as well as the amount to step 915 * by and {@link java.time.temporal.TemporalUnit}. 916 * 917 * <p>To better understand, here are a few examples: 918 * 919 * <ul> 920 * <li><strong>To step by one day from today: </strong> {@code new LocalDateSpinnerValueFactory(LocalDate.MIN, LocalDate.MAX, LocalDate.now(), 1, ChronoUnit.DAYS)}</li> 921 * <li><strong>To step by one month from today: </strong> {@code new LocalDateSpinnerValueFactory(LocalDate.MIN, LocalDate.MAX, LocalDate.now(), 1, ChronoUnit.MONTHS)}</li> 922 * <li><strong>To step by one year from today: </strong> {@code new LocalDateSpinnerValueFactory(LocalDate.MIN, LocalDate.MAX, LocalDate.now(), 1, ChronoUnit.YEARS)}</li> 923 * </ul> 924 * 925 * @param min The minimum allowed double value for the Spinner. 926 * @param max The maximum allowed double value for the Spinner. 927 * @param initialValue The value of the Spinner when first instantiated. 928 * @param amountToStepBy The amount to increment or decrement by, per step. 929 * @param temporalUnit The size of each step (e.g. day, week, month, year, etc) 930 */ 931 public LocalDateSpinnerValueFactory(@NamedArg("min") LocalDate min, 932 @NamedArg("min") LocalDate max, 933 @NamedArg("initialValue") LocalDate initialValue, 934 @NamedArg("amountToStepBy") long amountToStepBy, 935 @NamedArg("temporalUnit") TemporalUnit temporalUnit) { 936 setMin(min); 937 setMax(max); 938 setAmountToStepBy(amountToStepBy); 939 setTemporalUnit(temporalUnit); 940 setConverter(new StringConverter<LocalDate>() { 941 @Override public String toString(LocalDate object) { 942 if (object == null) { 943 return ""; 944 } 945 return object.toString(); 946 } 947 948 @Override public LocalDate fromString(String string) { 949 return LocalDate.parse(string); 950 } 951 }); 952 953 valueProperty().addListener((o, oldValue, newValue) -> { 954 // when the value is set, we need to react to ensure it is a 955 // valid value (and if not, blow up appropriately) 956 if (getMin() != null && newValue.isBefore(getMin())) { 957 setValue(getMin()); 958 } else if (getMax() != null && newValue.isAfter(getMax())) { 959 setValue(getMax()); 960 } 961 }); 962 setValue(initialValue != null ? initialValue : LocalDate.now()); 963 } 964 965 966 967 /*********************************************************************** 968 * * 969 * Properties * 970 * * 971 **********************************************************************/ 972 973 // --- min 974 private ObjectProperty<LocalDate> min = new SimpleObjectProperty<LocalDate>(this, "min") { 975 @Override protected void invalidated() { 976 LocalDate currentValue = LocalDateSpinnerValueFactory.this.getValue(); 977 if (currentValue == null) { 978 return; 979 } 980 981 final LocalDate newMin = get(); 982 if (newMin.isAfter(getMax())) { 983 setMin(getMax()); 984 return; 985 } 986 987 if (currentValue.isBefore(newMin)) { 988 LocalDateSpinnerValueFactory.this.setValue(newMin); 989 } 990 } 991 }; 992 993 public final void setMin(LocalDate value) { 994 min.set(value); 995 } 996 public final LocalDate getMin() { 997 return min.get(); 998 } 999 /** 1000 * Sets the minimum allowable value for this value factory 1001 */ 1002 public final ObjectProperty<LocalDate> minProperty() { 1003 return min; 1004 } 1005 1006 // --- max 1007 private ObjectProperty<LocalDate> max = new SimpleObjectProperty<LocalDate>(this, "max") { 1008 @Override protected void invalidated() { 1009 LocalDate currentValue = LocalDateSpinnerValueFactory.this.getValue(); 1010 if (currentValue == null) { 1011 return; 1012 } 1013 1014 final LocalDate newMax = get(); 1015 if (newMax.isBefore(getMin())) { 1016 setMax(getMin()); 1017 return; 1018 } 1019 1020 if (currentValue.isAfter(newMax)) { 1021 LocalDateSpinnerValueFactory.this.setValue(newMax); 1022 } 1023 } 1024 }; 1025 1026 public final void setMax(LocalDate value) { 1027 max.set(value); 1028 } 1029 public final LocalDate getMax() { 1030 return max.get(); 1031 } 1032 /** 1033 * Sets the maximum allowable value for this value factory 1034 */ 1035 public final ObjectProperty<LocalDate> maxProperty() { 1036 return max; 1037 } 1038 1039 // --- temporalUnit 1040 private ObjectProperty<TemporalUnit> temporalUnit = new SimpleObjectProperty<>(this, "temporalUnit"); 1041 public final void setTemporalUnit(TemporalUnit value) { 1042 temporalUnit.set(value); 1043 } 1044 public final TemporalUnit getTemporalUnit() { 1045 return temporalUnit.get(); 1046 } 1047 /** 1048 * The size of each step (e.g. day, week, month, year, etc). 1049 */ 1050 public final ObjectProperty<TemporalUnit> temporalUnitProperty() { 1051 return temporalUnit; 1052 } 1053 1054 // --- amountToStepBy 1055 private LongProperty amountToStepBy = new SimpleLongProperty(this, "amountToStepBy"); 1056 public final void setAmountToStepBy(long value) { 1057 amountToStepBy.set(value); 1058 } 1059 public final long getAmountToStepBy() { 1060 return amountToStepBy.get(); 1061 } 1062 /** 1063 * Sets the amount to increment or decrement by, per step. 1064 */ 1065 public final LongProperty amountToStepByProperty() { 1066 return amountToStepBy; 1067 } 1068 1069 1070 1071 /*********************************************************************** 1072 * * 1073 * Overridden methods * 1074 * * 1075 **********************************************************************/ 1076 1077 /** {@inheritDoc} */ 1078 @Override public void decrement(int steps) { 1079 final LocalDate currentValue = getValue(); 1080 final LocalDate min = getMin(); 1081 LocalDate newValue = currentValue.minus(getAmountToStepBy() * steps, getTemporalUnit()); 1082 1083 if (min != null && isWrapAround() && newValue.isBefore(min)) { 1084 // we need to wrap around 1085 newValue = getMax(); 1086 } 1087 1088 setValue(newValue); 1089 } 1090 1091 /** {@inheritDoc} */ 1092 @Override public void increment(int steps) { 1093 final LocalDate currentValue = getValue(); 1094 final LocalDate max = getMax(); 1095 LocalDate newValue = currentValue.plus(getAmountToStepBy() * steps, getTemporalUnit()); 1096 1097 if (max != null && isWrapAround() && newValue.isAfter(max)) { 1098 // we need to wrap around 1099 newValue = getMin(); 1100 } 1101 1102 setValue(newValue); 1103 } 1104 } 1105 1106 1107 1108 1109 1110 /** 1111 * A {@link javafx.scene.control.SpinnerValueFactory} implementation designed to iterate through 1112 * {@link java.time.LocalTime} values. 1113 * 1114 * <p>Note that the default {@link #converterProperty() converter} is implemented 1115 * simply as shown below, which may be adequate in many cases, but it is important 1116 * for users to ensure that this suits their needs (and adjust when necessary): 1117 * 1118 * <pre> 1119 * setConverter(new StringConverter<LocalTime>() { 1120 * @Override public String toString(LocalTime object) { 1121 * if (object == null) { 1122 * return ""; 1123 * } 1124 * return object.toString(); 1125 * } 1126 * 1127 * @Override public LocalTime fromString(String string) { 1128 * return LocalTime.parse(string); 1129 * } 1130 * });</pre> 1131 */ 1132 static class LocalTimeSpinnerValueFactory extends SpinnerValueFactory<LocalTime> { 1133 1134 /** 1135 * Creates a new instance of the LocalTimepinnerValueFactory, using the 1136 * value returned by calling {@code LocalTime#now()} as the initial value, 1137 * and using a stepping amount of one day. 1138 */ 1139 public LocalTimeSpinnerValueFactory() { 1140 this(LocalTime.now()); 1141 } 1142 1143 /** 1144 * Creates a new instance of the LocalTimeSpinnerValueFactory, using the 1145 * provided initial value, and a stepping amount of one hour. 1146 * 1147 * @param initialValue The value of the Spinner when first instantiated. 1148 */ 1149 public LocalTimeSpinnerValueFactory(@NamedArg("initialValue") LocalTime initialValue) { 1150 this(LocalTime.MIN, LocalTime.MAX, initialValue); 1151 } 1152 1153 /** 1154 * Creates a new instance of the LocalTimeSpinnerValueFactory, using the 1155 * provided initial value, and a stepping amount of one hour. 1156 * 1157 * @param min The minimum allowed double value for the Spinner. 1158 * @param max The maximum allowed double value for the Spinner. 1159 * @param initialValue The value of the Spinner when first instantiated. 1160 */ 1161 public LocalTimeSpinnerValueFactory(@NamedArg("min") LocalTime min, 1162 @NamedArg("min") LocalTime max, 1163 @NamedArg("initialValue") LocalTime initialValue) { 1164 this(min, max, initialValue, 1, ChronoUnit.HOURS); 1165 } 1166 1167 /** 1168 * Creates a new instance of the LocalTimeSpinnerValueFactory, using the 1169 * provided min, max, and initial values, as well as the amount to step 1170 * by and {@link java.time.temporal.TemporalUnit}. 1171 * 1172 * <p>To better understand, here are a few examples: 1173 * 1174 * <ul> 1175 * <li><strong>To step by one hour from the current time: </strong> {@code new LocalTimeSpinnerValueFactory(LocalTime.MIN, LocalTime.MAX, LocalTime.now(), 1, ChronoUnit.HOURS)}</li> 1176 * <li><strong>To step by one minute from the current time: </strong> {@code new LocalTimeSpinnerValueFactory(LocalTime.MIN, LocalTime.MAX, LocalTime.now(), 1, ChronoUnit.MINUTES)}</li> 1177 * </ul> 1178 * 1179 * @param min The minimum allowed double value for the Spinner. 1180 * @param max The maximum allowed double value for the Spinner. 1181 * @param initialValue The value of the Spinner when first instantiated. 1182 * @param amountToStepBy The amount to increment or decrement by, per step. 1183 * @param temporalUnit The size of each step (e.g. day, week, month, year, etc) 1184 */ 1185 public LocalTimeSpinnerValueFactory(@NamedArg("min") LocalTime min, 1186 @NamedArg("min") LocalTime max, 1187 @NamedArg("initialValue") LocalTime initialValue, 1188 @NamedArg("amountToStepBy") long amountToStepBy, 1189 @NamedArg("temporalUnit") TemporalUnit temporalUnit) { 1190 setMin(min); 1191 setMax(max); 1192 setAmountToStepBy(amountToStepBy); 1193 setTemporalUnit(temporalUnit); 1194 setConverter(new StringConverter<LocalTime>() { 1195 private DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT); 1196 1197 @Override public String toString(LocalTime localTime) { 1198 if (localTime == null) { 1199 return ""; 1200 } 1201 return localTime.format(dtf); 1202 } 1203 1204 @Override public LocalTime fromString(String string) { 1205 return LocalTime.parse(string); 1206 } 1207 }); 1208 1209 valueProperty().addListener((o, oldValue, newValue) -> { 1210 // when the value is set, we need to react to ensure it is a 1211 // valid value (and if not, blow up appropriately) 1212 if (getMin() != null && newValue.isBefore(getMin())) { 1213 setValue(getMin()); 1214 } else if (getMax() != null && newValue.isAfter(getMax())) { 1215 setValue(getMax()); 1216 } 1217 }); 1218 setValue(initialValue != null ? initialValue : LocalTime.now()); 1219 } 1220 1221 1222 1223 /*********************************************************************** 1224 * * 1225 * Properties * 1226 * * 1227 **********************************************************************/ 1228 1229 // --- min 1230 private ObjectProperty<LocalTime> min = new SimpleObjectProperty<LocalTime>(this, "min") { 1231 @Override protected void invalidated() { 1232 LocalTime currentValue = LocalTimeSpinnerValueFactory.this.getValue(); 1233 if (currentValue == null) { 1234 return; 1235 } 1236 1237 final LocalTime newMin = get(); 1238 if (newMin.isAfter(getMax())) { 1239 setMin(getMax()); 1240 return; 1241 } 1242 1243 if (currentValue.isBefore(newMin)) { 1244 LocalTimeSpinnerValueFactory.this.setValue(newMin); 1245 } 1246 } 1247 }; 1248 1249 public final void setMin(LocalTime value) { 1250 min.set(value); 1251 } 1252 public final LocalTime getMin() { 1253 return min.get(); 1254 } 1255 /** 1256 * Sets the minimum allowable value for this value factory 1257 */ 1258 public final ObjectProperty<LocalTime> minProperty() { 1259 return min; 1260 } 1261 1262 // --- max 1263 private ObjectProperty<LocalTime> max = new SimpleObjectProperty<LocalTime>(this, "max") { 1264 @Override protected void invalidated() { 1265 LocalTime currentValue = LocalTimeSpinnerValueFactory.this.getValue(); 1266 if (currentValue == null) { 1267 return; 1268 } 1269 1270 final LocalTime newMax = get(); 1271 if (newMax.isBefore(getMin())) { 1272 setMax(getMin()); 1273 return; 1274 } 1275 1276 if (currentValue.isAfter(newMax)) { 1277 LocalTimeSpinnerValueFactory.this.setValue(newMax); 1278 } 1279 } 1280 }; 1281 1282 public final void setMax(LocalTime value) { 1283 max.set(value); 1284 } 1285 public final LocalTime getMax() { 1286 return max.get(); 1287 } 1288 /** 1289 * Sets the maximum allowable value for this value factory 1290 */ 1291 public final ObjectProperty<LocalTime> maxProperty() { 1292 return max; 1293 } 1294 1295 // --- temporalUnit 1296 private ObjectProperty<TemporalUnit> temporalUnit = new SimpleObjectProperty<>(this, "temporalUnit"); 1297 public final void setTemporalUnit(TemporalUnit value) { 1298 temporalUnit.set(value); 1299 } 1300 public final TemporalUnit getTemporalUnit() { 1301 return temporalUnit.get(); 1302 } 1303 /** 1304 * The size of each step (e.g. day, week, month, year, etc). 1305 */ 1306 public final ObjectProperty<TemporalUnit> temporalUnitProperty() { 1307 return temporalUnit; 1308 } 1309 1310 // --- amountToStepBy 1311 private LongProperty amountToStepBy = new SimpleLongProperty(this, "amountToStepBy"); 1312 public final void setAmountToStepBy(long value) { 1313 amountToStepBy.set(value); 1314 } 1315 public final long getAmountToStepBy() { 1316 return amountToStepBy.get(); 1317 } 1318 /** 1319 * Sets the amount to increment or decrement by, per step. 1320 */ 1321 public final LongProperty amountToStepByProperty() { 1322 return amountToStepBy; 1323 } 1324 1325 1326 1327 /*********************************************************************** 1328 * * 1329 * Overridden methods * 1330 * * 1331 **********************************************************************/ 1332 1333 /** {@inheritDoc} */ 1334 @Override public void decrement(int steps) { 1335 final LocalTime currentValue = getValue(); 1336 final LocalTime min = getMin(); 1337 1338 final Duration duration = Duration.of(getAmountToStepBy() * steps, getTemporalUnit()); 1339 1340 final long durationInSeconds = duration.toMinutes() * 60; 1341 final long currentValueInSeconds = currentValue.toSecondOfDay(); 1342 1343 if (! isWrapAround() && durationInSeconds > currentValueInSeconds) { 1344 setValue(min == null ? LocalTime.MIN : min); 1345 } else { 1346 setValue(currentValue.minus(duration)); 1347 } 1348 } 1349 1350 /** {@inheritDoc} */ 1351 @Override public void increment(int steps) { 1352 final LocalTime currentValue = getValue(); 1353 final LocalTime max = getMax(); 1354 1355 final Duration duration = Duration.of(getAmountToStepBy() * steps, getTemporalUnit()); 1356 1357 final long durationInSeconds = duration.toMinutes() * 60; 1358 final long currentValueInSeconds = currentValue.toSecondOfDay(); 1359 1360 if (! isWrapAround() && durationInSeconds > (LocalTime.MAX.toSecondOfDay() - currentValueInSeconds)) { 1361 setValue(max == null ? LocalTime.MAX : max); 1362 } else { 1363 setValue(currentValue.plus(duration)); 1364 } 1365 } 1366 } 1367 }