1 /*
   2  * Copyright (c) 2013, 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 
  26 package javafx.scene.control;
  27 
  28 // editor and converter code in sync with ComboBox 4858:e60e9a5396e6
  29 
  30 import java.time.LocalDate;
  31 import java.time.DateTimeException;
  32 import java.time.chrono.Chronology;
  33 import java.time.chrono.ChronoLocalDate;
  34 import java.time.chrono.IsoChronology;
  35 import java.time.format.DateTimeFormatter;
  36 import java.time.format.DateTimeFormatterBuilder;
  37 import java.time.format.DecimalStyle;
  38 import java.time.format.FormatStyle;
  39 import java.time.temporal.TemporalAccessor;
  40 import java.util.ArrayList;
  41 import java.util.Collections;
  42 import java.util.List;
  43 import java.util.Locale;
  44 
  45 import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
  46 
  47 import javafx.beans.property.BooleanProperty;
  48 import javafx.beans.property.ObjectProperty;
  49 import javafx.beans.property.ReadOnlyObjectProperty;
  50 import javafx.beans.property.ReadOnlyObjectWrapper;
  51 import javafx.beans.property.SimpleObjectProperty;
  52 import javafx.beans.value.WritableValue;
  53 import javafx.css.CssMetaData;
  54 import javafx.css.Styleable;
  55 import javafx.css.StyleableBooleanProperty;
  56 import javafx.css.StyleableProperty;
  57 import javafx.scene.AccessibleAttribute;
  58 import javafx.scene.AccessibleRole;
  59 import javafx.util.Callback;
  60 import javafx.util.StringConverter;
  61 
  62 import com.sun.javafx.css.converters.BooleanConverter;
  63 import com.sun.javafx.scene.control.skin.DatePickerSkin;
  64 import com.sun.javafx.scene.control.skin.resources.ControlResources;
  65 
  66 
  67 /**
  68  * The DatePicker control allows the user to enter a date as text or
  69  * to select a date from a calendar popup. The calendar is based on
  70  * either the standard ISO-8601 chronology or any of the other
  71  * chronology classes defined in the java.time.chrono package.
  72  *
  73  * <p>The {@link #valueProperty() value} property represents the
  74  * currently selected {@link java.time.LocalDate}.  An initial date can
  75  * be set via the {@link #DatePicker(java.time.LocalDate) constructor}
  76  * or by calling {@link #setValue(java.time.LocalDate) setValue()}.  The
  77  * default value is null.
  78  *
  79  * <pre><code>
  80  * final DatePicker datePicker = new DatePicker();
  81  * datePicker.setOnAction(new EventHandler() {
  82  *     public void handle(Event t) {
  83  *         LocalDate date = datePicker.getValue();
  84  *         System.err.println("Selected date: " + date);
  85  *     }
  86  * });
  87  * </code></pre>
  88  *
  89  * The {@link #chronologyProperty() chronology} property specifies a
  90  * calendar system to be used for parsing, displaying, and choosing
  91  * dates.
  92  * The {@link #valueProperty() value} property is always defined in
  93  * the ISO calendar system, however, so applications based on a
  94  * different chronology may use the conversion methods provided in the
  95  * {@link java.time.chrono.Chronology} API to get or set the
  96  * corresponding {@link java.time.chrono.ChronoLocalDate} value. For
  97  * example:
  98  *
  99  * <pre><code>
 100  * LocalDate isoDate = datePicker.getValue();
 101  * ChronoLocalDate chronoDate =
 102  *     ((isoDate != null) ? datePicker.getChronology().date(isoDate) : null);
 103  * System.err.println("Selected date: " + chronoDate);
 104  * </code></pre>
 105  *
 106  *
 107  * @since JavaFX 8.0
 108  */
 109 public class DatePicker extends ComboBoxBase<LocalDate> {
 110 
 111     private LocalDate lastValidDate = null;
 112     private Chronology lastValidChronology = IsoChronology.INSTANCE;
 113 
 114     /**
 115      * Creates a default DatePicker instance with a <code>null</code> date value set.
 116      */
 117     public DatePicker() {
 118         this(null);
 119 
 120         valueProperty().addListener(observable -> {
 121             LocalDate date = getValue();
 122             Chronology chrono = getChronology();
 123 
 124             if (validateDate(chrono, date)) {
 125                 lastValidDate = date;
 126             } else {
 127                 System.err.println("Restoring value to " +
 128                             ((lastValidDate == null) ? "null" : getConverter().toString(lastValidDate)));
 129                 setValue(lastValidDate);
 130             }
 131         });
 132 
 133         chronologyProperty().addListener(observable -> {
 134             LocalDate date = getValue();
 135             Chronology chrono = getChronology();
 136 
 137             if (validateDate(chrono, date)) {
 138                 lastValidChronology = chrono;
 139             } else {
 140                 System.err.println("Restoring value to " + lastValidChronology);
 141                 setChronology(lastValidChronology);
 142             }
 143         });
 144     }
 145 
 146     private boolean validateDate(Chronology chrono, LocalDate date) {
 147         try {
 148             if (date != null) {
 149                 chrono.date(date);
 150             }
 151             return true;
 152         } catch (DateTimeException ex) {
 153             System.err.println(ex);
 154             return false;
 155         }
 156     }
 157 
 158     /**
 159      * Creates a DatePicker instance and sets the
 160      * {@link #valueProperty() value} to the given date.
 161      *
 162      * @param localDate to be set as the currently selected date in the DatePicker. Can be null.
 163      */
 164     public DatePicker(LocalDate localDate) {
 165         setValue(localDate);
 166         getStyleClass().add(DEFAULT_STYLE_CLASS);
 167         setRole(AccessibleRole.DATE_PICKER);
 168         setEditable(true);
 169     }
 170 
 171 
 172     /***************************************************************************
 173      *                                                                         *
 174      * Properties                                                                 *
 175      *                                                                         *
 176      **************************************************************************/
 177 
 178 
 179     /**
 180      * A custom cell factory can be provided to customize individual
 181      * day cells in the DatePicker popup. Refer to {@link DateCell}
 182      * and {@link Cell} for more information on cell factories.
 183      * Example:
 184      *
 185      * <pre><code>
 186      * final Callback&lt;DatePicker, DateCell&gt; dayCellFactory = new Callback&lt;DatePicker, DateCell&gt;() {
 187      *     public DateCell call(final DatePicker datePicker) {
 188      *         return new DateCell() {
 189      *             &#064;Override public void updateItem(LocalDate item, boolean empty) {
 190      *                 super.updateItem(item, empty);
 191      *
 192      *                 if (MonthDay.from(item).equals(MonthDay.of(9, 25))) {
 193      *                     setTooltip(new Tooltip("Happy Birthday!"));
 194      *                     setStyle("-fx-background-color: #ff4444;");
 195      *                 }
 196      *                 if (item.equals(LocalDate.now().plusDays(1))) {
 197      *                     // Tomorrow is too soon.
 198      *                     setDisable(true);
 199      *                 }
 200      *             }
 201      *         };
 202      *     }
 203      * };
 204      * datePicker.setDayCellFactory(dayCellFactory);
 205      * </code></pre>
 206      *
 207      * @defaultValue null
 208      */
 209     private ObjectProperty<Callback<DatePicker, DateCell>> dayCellFactory;
 210     public final void setDayCellFactory(Callback<DatePicker, DateCell> value) {
 211         dayCellFactoryProperty().set(value);
 212     }
 213     public final Callback<DatePicker, DateCell> getDayCellFactory() {
 214         return (dayCellFactory != null) ? dayCellFactory.get() : null;
 215     }
 216     public final ObjectProperty<Callback<DatePicker, DateCell>> dayCellFactoryProperty() {
 217         if (dayCellFactory == null) {
 218             dayCellFactory = new SimpleObjectProperty<Callback<DatePicker, DateCell>>(this, "dayCellFactory");
 219         }
 220         return dayCellFactory;
 221     }
 222 
 223 
 224 
 225     /**
 226      * The calendar system used for parsing, displaying, and choosing
 227      * dates in the DatePicker control.
 228      *
 229      * <p>The default value is returned from a call to
 230      * {@code Chronology.ofLocale(Locale.getDefault(Locale.Category.FORMAT))}.
 231      * The default is usually {@link java.time.chrono.IsoChronology} unless
 232      * provided explicitly in the {@link java.util.Locale} by use of a
 233      * Locale calendar extension.
 234      *
 235      * Setting the value to <code>null</code> will restore the default
 236      * chronology.
 237      */
 238     public final ObjectProperty<Chronology> chronologyProperty() {
 239         return chronology;
 240     }
 241     private ObjectProperty<Chronology> chronology =
 242         new SimpleObjectProperty<Chronology>(this, "chronology", null);
 243     public final Chronology getChronology() {
 244         Chronology chrono = chronology.get();
 245         if (chrono == null) {
 246             try {
 247                 chrono = Chronology.ofLocale(Locale.getDefault(Locale.Category.FORMAT));
 248             } catch (Exception ex) {
 249                 System.err.println(ex);
 250             }
 251             if (chrono == null) {
 252                 chrono = IsoChronology.INSTANCE;
 253             }
 254             //System.err.println(chrono);
 255         }
 256         return chrono;
 257     }
 258     public final void setChronology(Chronology value) {
 259         chronology.setValue(value);
 260     }
 261 
 262 
 263     /**
 264      * Whether the DatePicker popup should display a column showing
 265      * week numbers.
 266      *
 267      * <p>The default value is specified in a resource bundle, and
 268      * depends on the country of the current locale.
 269      */
 270     public final BooleanProperty showWeekNumbersProperty() {
 271         if (showWeekNumbers == null) {
 272             String country = Locale.getDefault(Locale.Category.FORMAT).getCountry();
 273             boolean localizedDefault =
 274                 (!country.isEmpty() &&
 275                  ControlResources.getNonTranslatableString("DatePicker.showWeekNumbers").contains(country));
 276             showWeekNumbers = new StyleableBooleanProperty(localizedDefault) {
 277                 @Override public CssMetaData<DatePicker,Boolean> getCssMetaData() {
 278                     return StyleableProperties.SHOW_WEEK_NUMBERS;
 279                 }
 280 
 281                 @Override public Object getBean() {
 282                     return DatePicker.this;
 283                 }
 284 
 285                 @Override public String getName() {
 286                     return "showWeekNumbers";
 287                 }
 288             };
 289         }
 290         return showWeekNumbers;
 291     }
 292     private BooleanProperty showWeekNumbers;
 293     public final void setShowWeekNumbers(boolean value) {
 294         showWeekNumbersProperty().setValue(value);
 295     }
 296     public final boolean isShowWeekNumbers() {
 297         return showWeekNumbersProperty().getValue();
 298     }
 299 
 300 
 301     // --- string converter
 302     /**
 303      * Converts the input text to an object of type LocalDate and vice
 304      * versa.
 305      *
 306      * <p>If not set by the application, the DatePicker skin class will
 307      * set a converter based on a {@link java.time.DateTimeFormatter}
 308      * for the current {@link java.util.Locale} and
 309      * {@link #chronologyProperty() chronology}. This formatter is
 310      * then used to parse and display the current date value.
 311      *
 312      * Setting the value to <code>null</code> will restore the default
 313      * converter.
 314      *
 315      * <p>Example using an explicit formatter:
 316      * <pre><code>
 317      * datePicker.setConverter(new StringConverter&lt;LocalDate&gt;() {
 318      *     String pattern = "yyyy-MM-dd";
 319      *     DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(pattern);
 320      *
 321      *     {
 322      *         datePicker.setPromptText(pattern.toLowerCase());
 323      *     }
 324      *
 325      *     &#064;Override public String toString(LocalDate date) {
 326      *         if (date != null) {
 327      *             return dateFormatter.format(date);
 328      *         } else {
 329      *             return "";
 330      *         }
 331      *     }
 332      *
 333      *     &#064;Override public LocalDate fromString(String string) {
 334      *         if (string != null && !string.isEmpty()) {
 335      *             return LocalDate.parse(string, dateFormatter);
 336      *         } else {
 337      *             return null;
 338      *         }
 339      *     }
 340      * });
 341      * </code></pre>
 342      * <p>Example that wraps the default formatter and catches parse exceptions:
 343      * <pre><code>
 344      *   final StringConverter&lt;LocalDate&gt; defaultConverter = datePicker.getConverter();
 345      *   datePicker.setConverter(new StringConverter&lt;LocalDate&gt;() {
 346      *       &#064;Override public String toString(LocalDate value) {
 347      *           return defaultConverter.toString(value);
 348      *       }
 349      *
 350      *       &#064;Override public LocalDate fromString(String text) {
 351      *           try {
 352      *               return defaultConverter.fromString(text);
 353      *           } catch (DateTimeParseException ex) {
 354      *               System.err.println("HelloDatePicker: "+ex.getMessage());
 355      *               throw ex;
 356      *           }
 357      *       }
 358      *   });
 359      * </code></pre>
 360      *
 361      * <p>The default base year for parsing input containing only two digits for
 362      * the year is 2000 (see {@link java.time.format.DateTimeFormatter}).  This
 363      * default is not useful for allowing a person's date of birth to be typed.
 364      * The following example modifies the converter's fromString() method to
 365      * allow a two digit year for birth dates up to 99 years in the past.
 366      * <pre><code>
 367      *   &#064;Override public LocalDate fromString(String text) {
 368      *       if (text != null && !text.isEmpty()) {
 369      *           Locale locale = Locale.getDefault(Locale.Category.FORMAT);
 370      *           Chronology chrono = datePicker.getChronology();
 371      *           String pattern =
 372      *               DateTimeFormatterBuilder.getLocalizedDateTimePattern(FormatStyle.SHORT,
 373      *                                                                    null, chrono, locale);
 374      *           String prePattern = pattern.substring(0, pattern.indexOf("y"));
 375      *           String postPattern = pattern.substring(pattern.lastIndexOf("y")+1);
 376      *           int baseYear = LocalDate.now().getYear() - 99;
 377      *           DateTimeFormatter df = new DateTimeFormatterBuilder()
 378      *                       .parseLenient()
 379      *                       .appendPattern(prePattern)
 380      *                       .appendValueReduced(ChronoField.YEAR, 2, 2, baseYear)
 381      *                       .appendPattern(postPattern)
 382      *                       .toFormatter();
 383      *           return LocalDate.from(chrono.date(df.parse(text)));
 384      *       } else {
 385      *           return null;
 386      *       }
 387      *   }
 388      * </code></pre>
 389      *
 390      * @see javafx.scene.control.ComboBox#converterProperty
 391      */
 392     public final ObjectProperty<StringConverter<LocalDate>> converterProperty() { return converter; }
 393     private ObjectProperty<StringConverter<LocalDate>> converter =
 394             new SimpleObjectProperty<StringConverter<LocalDate>>(this, "converter", null);
 395     public final void setConverter(StringConverter<LocalDate> value) { converterProperty().set(value); }
 396     public final StringConverter<LocalDate> getConverter() {
 397         StringConverter<LocalDate> converter = converterProperty().get();
 398         if (converter != null) {
 399             return converter;
 400         } else {
 401             return defaultConverter;
 402         }
 403     }
 404 
 405     private StringConverter<LocalDate> defaultConverter = new StringConverter<LocalDate>() {
 406         @Override public String toString(LocalDate value) {
 407             if (value != null) {
 408                 Locale locale = Locale.getDefault(Locale.Category.FORMAT);
 409                 Chronology chrono = getChronology();
 410                 ChronoLocalDate cDate;
 411                 try {
 412                     cDate = chrono.date(value);
 413                 } catch (DateTimeException ex) {
 414                     System.err.println(ex);
 415                     chrono = IsoChronology.INSTANCE;
 416                     cDate = value;
 417                 }
 418                 DateTimeFormatter dateFormatter =
 419                     DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
 420                                      .withLocale(locale)
 421                                      .withChronology(chrono)
 422                                      .withDecimalStyle(DecimalStyle.of(locale));
 423 
 424                 String pattern =
 425                     DateTimeFormatterBuilder.getLocalizedDateTimePattern(FormatStyle.SHORT,
 426                                                                          null, chrono, locale);
 427 
 428                 if (pattern.contains("yy") && !pattern.contains("yyy")) {
 429                     // Modify pattern to show four-digit year, including leading zeros.
 430                     String newPattern = pattern.replace("yy", "yyyy");
 431                     //System.err.println("Fixing pattern ("+forParsing+"): "+pattern+" -> "+newPattern);
 432                     dateFormatter = DateTimeFormatter.ofPattern(newPattern)
 433                                                      .withDecimalStyle(DecimalStyle.of(locale));
 434                 }
 435 
 436                 return dateFormatter.format(cDate);
 437             } else {
 438                 return "";
 439             }
 440         }
 441 
 442         @Override public LocalDate fromString(String text) {
 443             if (text != null && !text.isEmpty()) {
 444                 Locale locale = Locale.getDefault(Locale.Category.FORMAT);
 445                 Chronology chrono = getChronology();
 446 
 447                 String pattern =
 448                     DateTimeFormatterBuilder.getLocalizedDateTimePattern(FormatStyle.SHORT,
 449                                                                          null, chrono, locale);
 450                 DateTimeFormatter df =
 451                     new DateTimeFormatterBuilder().parseLenient()
 452                                                   .appendPattern(pattern)
 453                                                   .toFormatter()
 454                                                   .withChronology(chrono)
 455                                                   .withDecimalStyle(DecimalStyle.of(locale));
 456                 TemporalAccessor temporal = df.parse(text);
 457                 ChronoLocalDate cDate = chrono.date(temporal);
 458                 return LocalDate.from(cDate);
 459             }
 460             return null;
 461         }
 462     };
 463 
 464 
 465     // --- Editor
 466     /**
 467      * The editor for the DatePicker.
 468      *
 469      * @see javafx.scene.control.ComboBox#editorProperty
 470      */
 471     private ReadOnlyObjectWrapper<TextField> editor;
 472     public final TextField getEditor() {
 473         return editorProperty().get();
 474     }
 475     public final ReadOnlyObjectProperty<TextField> editorProperty() {
 476         if (editor == null) {
 477             editor = new ReadOnlyObjectWrapper<TextField>(this, "editor");
 478             editor.set(new ComboBoxListViewSkin.FakeFocusTextField());
 479         }
 480         return editor.getReadOnlyProperty();
 481     }
 482 
 483     /** {@inheritDoc} */
 484     @Override protected Skin<?> createDefaultSkin() {
 485         return new DatePickerSkin(this);
 486     }
 487 
 488 
 489     /***************************************************************************
 490      *                                                                         *
 491      * Stylesheet Handling                                                     *
 492      *                                                                         *
 493      **************************************************************************/
 494 
 495     private static final String DEFAULT_STYLE_CLASS = "date-picker";
 496 
 497      /**
 498       * @treatAsPrivate implementation detail
 499       */
 500     private static class StyleableProperties {
 501         private static final String country =
 502             Locale.getDefault(Locale.Category.FORMAT).getCountry();
 503         private static final CssMetaData<DatePicker, Boolean> SHOW_WEEK_NUMBERS =
 504               new CssMetaData<DatePicker, Boolean>("-fx-show-week-numbers",
 505                    BooleanConverter.getInstance(),
 506                    (!country.isEmpty() &&
 507                     ControlResources.getNonTranslatableString("DatePicker.showWeekNumbers").contains(country))) {
 508             @Override public boolean isSettable(DatePicker n) {
 509                 return n.showWeekNumbers == null || !n.showWeekNumbers.isBound();
 510             }
 511 
 512             @Override public StyleableProperty<Boolean> getStyleableProperty(DatePicker n) {
 513                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)n.showWeekNumbersProperty();
 514             }
 515         };
 516 
 517         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 518 
 519         static {
 520             final List<CssMetaData<? extends Styleable, ?>> styleables =
 521                 new ArrayList<CssMetaData<? extends Styleable, ?>>(Control.getClassCssMetaData());
 522             Collections.addAll(styleables,
 523                 SHOW_WEEK_NUMBERS
 524             );
 525             STYLEABLES = Collections.unmodifiableList(styleables);
 526         }
 527     }
 528 
 529     /**
 530      * @return The CssMetaData associated with this class, which may include the
 531      * CssMetaData of its super classes.
 532      */
 533     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 534         return StyleableProperties.STYLEABLES;
 535     }
 536 
 537     /**
 538      * {@inheritDoc}
 539      */
 540     @Override
 541     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
 542         return getClassCssMetaData();
 543     }
 544 
 545     /***************************************************************************
 546      *                                                                         *
 547      * Accessibility handling                                                  *
 548      *                                                                         *
 549      **************************************************************************/
 550 
 551     @Override
 552     public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 553         switch (attribute) {
 554             case DATE: return getValue();
 555             case TITLE: {
 556                 String accText = getAccessibleText();
 557                 if (accText != null && !accText.isEmpty()) return accText;
 558 
 559                 LocalDate date = getValue();
 560                 StringConverter<LocalDate> c = getConverter();
 561                 if (date != null && c != null) {
 562                     return c.toString(date);
 563                 }
 564                 return "";
 565             }
 566             default: return super.queryAccessibleAttribute(attribute, parameters);
 567         }
 568     }
 569 
 570 }