1 /*
   2  * Copyright (c) 2013, 2016, 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.IsoChronology;
  34 import java.time.format.FormatStyle;
  35 import java.util.ArrayList;
  36 import java.util.Collections;
  37 import java.util.List;
  38 import java.util.Locale;
  39 
  40 import com.sun.javafx.scene.control.FakeFocusTextField;
  41 
  42 import javafx.beans.property.BooleanProperty;
  43 import javafx.beans.property.ObjectProperty;
  44 import javafx.beans.property.ReadOnlyObjectProperty;
  45 import javafx.beans.property.ReadOnlyObjectWrapper;
  46 import javafx.beans.property.SimpleObjectProperty;
  47 import javafx.beans.value.WritableValue;
  48 import javafx.css.CssMetaData;
  49 import javafx.css.Styleable;
  50 import javafx.css.StyleableBooleanProperty;
  51 import javafx.css.StyleableProperty;
  52 import javafx.scene.AccessibleAttribute;
  53 import javafx.scene.AccessibleRole;
  54 import javafx.util.Callback;
  55 import javafx.util.StringConverter;
  56 import javafx.util.converter.LocalDateStringConverter;
  57 
  58 import javafx.css.converter.BooleanConverter;
  59 import javafx.scene.control.skin.DatePickerSkin;
  60 import com.sun.javafx.scene.control.skin.resources.ControlResources;
  61 
  62 
  63 /**
  64  * The DatePicker control allows the user to enter a date as text or
  65  * to select a date from a calendar popup. The calendar is based on
  66  * either the standard ISO-8601 chronology or any of the other
  67  * chronology classes defined in the java.time.chrono package.
  68  *
  69  * <p>The {@link #valueProperty() value} property represents the
  70  * currently selected {@link java.time.LocalDate}.  An initial date can
  71  * be set via the {@link #DatePicker(java.time.LocalDate) constructor}
  72  * or by calling {@link #setValue(java.time.LocalDate) setValue()}.  The
  73  * default value is null.
  74  *
  75  * <pre><code>
  76  * final DatePicker datePicker = new DatePicker();
  77  * datePicker.setOnAction(new EventHandler() {
  78  *     public void handle(Event t) {
  79  *         LocalDate date = datePicker.getValue();
  80  *         System.err.println("Selected date: " + date);
  81  *     }
  82  * });
  83  * </code></pre>
  84  *
  85  * The {@link #chronologyProperty() chronology} property specifies a
  86  * calendar system to be used for parsing, displaying, and choosing
  87  * dates.
  88  * The {@link #valueProperty() value} property is always defined in
  89  * the ISO calendar system, however, so applications based on a
  90  * different chronology may use the conversion methods provided in the
  91  * {@link java.time.chrono.Chronology} API to get or set the
  92  * corresponding {@link java.time.chrono.ChronoLocalDate} value. For
  93  * example:
  94  *
  95  * <pre><code>
  96  * LocalDate isoDate = datePicker.getValue();
  97  * ChronoLocalDate chronoDate =
  98  *     ((isoDate != null) ? datePicker.getChronology().date(isoDate) : null);
  99  * System.err.println("Selected date: " + chronoDate);
 100  * </code></pre>
 101  *
 102  *
 103  * @since JavaFX 8.0
 104  */
 105 public class DatePicker extends ComboBoxBase<LocalDate> {
 106 
 107     private LocalDate lastValidDate = null;
 108     private Chronology lastValidChronology = IsoChronology.INSTANCE;
 109 
 110     /**
 111      * Creates a default DatePicker instance with a <code>null</code> date value set.
 112      */
 113     public DatePicker() {
 114         this(null);
 115 
 116         valueProperty().addListener(observable -> {
 117             LocalDate date = getValue();
 118             Chronology chrono = getChronology();
 119 
 120             if (validateDate(chrono, date)) {
 121                 lastValidDate = date;
 122             } else {
 123                 System.err.println("Restoring value to " +
 124                             ((lastValidDate == null) ? "null" : getConverter().toString(lastValidDate)));
 125                 setValue(lastValidDate);
 126             }
 127         });
 128 
 129         chronologyProperty().addListener(observable -> {
 130             LocalDate date = getValue();
 131             Chronology chrono = getChronology();
 132 
 133             if (validateDate(chrono, date)) {
 134                 lastValidChronology = chrono;
 135                 defaultConverter = new LocalDateStringConverter(FormatStyle.SHORT, null, chrono);
 136             } else {
 137                 System.err.println("Restoring value to " + lastValidChronology);
 138                 setChronology(lastValidChronology);
 139             }
 140         });
 141     }
 142 
 143     private boolean validateDate(Chronology chrono, LocalDate date) {
 144         try {
 145             if (date != null) {
 146                 chrono.date(date);
 147             }
 148             return true;
 149         } catch (DateTimeException ex) {
 150             System.err.println(ex);
 151             return false;
 152         }
 153     }
 154 
 155     /**
 156      * Creates a DatePicker instance and sets the
 157      * {@link #valueProperty() value} to the given date.
 158      *
 159      * @param localDate to be set as the currently selected date in the DatePicker. Can be null.
 160      */
 161     public DatePicker(LocalDate localDate) {
 162         setValue(localDate);
 163         getStyleClass().add(DEFAULT_STYLE_CLASS);
 164         setAccessibleRole(AccessibleRole.DATE_PICKER);
 165         setEditable(true);
 166     }
 167 
 168 
 169     /***************************************************************************
 170      *                                                                         *
 171      * Properties                                                                 *
 172      *                                                                         *
 173      **************************************************************************/
 174 
 175 
 176     /**
 177      * A custom cell factory can be provided to customize individual
 178      * day cells in the DatePicker popup. Refer to {@link DateCell}
 179      * and {@link Cell} for more information on cell factories.
 180      * Example:
 181      *
 182      * <pre><code>
 183      * final Callback&lt;DatePicker, DateCell&gt; dayCellFactory = new Callback&lt;DatePicker, DateCell&gt;() {
 184      *     public DateCell call(final DatePicker datePicker) {
 185      *         return new DateCell() {
 186      *             @Override public void updateItem(LocalDate item, boolean empty) {
 187      *                 super.updateItem(item, empty);
 188      *
 189      *                 if (MonthDay.from(item).equals(MonthDay.of(9, 25))) {
 190      *                     setTooltip(new Tooltip("Happy Birthday!"));
 191      *                     setStyle("-fx-background-color: #ff4444;");
 192      *                 }
 193      *                 if (item.equals(LocalDate.now().plusDays(1))) {
 194      *                     // Tomorrow is too soon.
 195      *                     setDisable(true);
 196      *                 }
 197      *             }
 198      *         };
 199      *     }
 200      * };
 201      * datePicker.setDayCellFactory(dayCellFactory);
 202      * </code></pre>
 203      *
 204      * @defaultValue null
 205      */
 206     private ObjectProperty<Callback<DatePicker, DateCell>> dayCellFactory;
 207     public final void setDayCellFactory(Callback<DatePicker, DateCell> value) {
 208         dayCellFactoryProperty().set(value);
 209     }
 210     public final Callback<DatePicker, DateCell> getDayCellFactory() {
 211         return (dayCellFactory != null) ? dayCellFactory.get() : null;
 212     }
 213     public final ObjectProperty<Callback<DatePicker, DateCell>> dayCellFactoryProperty() {
 214         if (dayCellFactory == null) {
 215             dayCellFactory = new SimpleObjectProperty<Callback<DatePicker, DateCell>>(this, "dayCellFactory");
 216         }
 217         return dayCellFactory;
 218     }
 219 
 220 
 221 
 222     /**
 223      * The calendar system used for parsing, displaying, and choosing
 224      * dates in the DatePicker control.
 225      *
 226      * <p>The default value is returned from a call to
 227      * {@code Chronology.ofLocale(Locale.getDefault(Locale.Category.FORMAT))}.
 228      * The default is usually {@link java.time.chrono.IsoChronology} unless
 229      * provided explicitly in the {@link java.util.Locale} by use of a
 230      * Locale calendar extension.
 231      *
 232      * Setting the value to <code>null</code> will restore the default
 233      * chronology.
 234      * @return the calendar system
 235      */
 236     public final ObjectProperty<Chronology> chronologyProperty() {
 237         return chronology;
 238     }
 239     private ObjectProperty<Chronology> chronology =
 240         new SimpleObjectProperty<Chronology>(this, "chronology", null);
 241     public final Chronology getChronology() {
 242         Chronology chrono = chronology.get();
 243         if (chrono == null) {
 244             try {
 245                 chrono = Chronology.ofLocale(Locale.getDefault(Locale.Category.FORMAT));
 246             } catch (Exception ex) {
 247                 System.err.println(ex);
 248             }
 249             if (chrono == null) {
 250                 chrono = IsoChronology.INSTANCE;
 251             }
 252             //System.err.println(chrono);
 253         }
 254         return chrono;
 255     }
 256     public final void setChronology(Chronology value) {
 257         chronology.setValue(value);
 258     }
 259 
 260 
 261     /**
 262      * Whether the DatePicker popup should display a column showing
 263      * week numbers.
 264      *
 265      * <p>The default value is specified in a resource bundle, and
 266      * depends on the country of the current locale.
 267      * @return true if popup should display a column showing
 268      * week numbers
 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.format.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      *     {@literal @Override public String toString(LocalDate date) {
 326      *         if (date != null) {
 327      *             return dateFormatter.format(date);
 328      *         } else {
 329      *             return "";
 330      *         }
 331      *     }}
 332      *
 333      *     {@literal @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      *       @Override public String toString(LocalDate value) {
 347      *           return defaultConverter.toString(value);
 348      *       }
 349      *
 350      *       @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      *   {@literal @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      * @return the string converter of type LocalDate
 391      * @see javafx.scene.control.ComboBox#converterProperty
 392      */
 393     public final ObjectProperty<StringConverter<LocalDate>> converterProperty() { return converter; }
 394     private ObjectProperty<StringConverter<LocalDate>> converter =
 395             new SimpleObjectProperty<StringConverter<LocalDate>>(this, "converter", null);
 396     public final void setConverter(StringConverter<LocalDate> value) { converterProperty().set(value); }
 397     public final StringConverter<LocalDate> getConverter() {
 398         StringConverter<LocalDate> converter = converterProperty().get();
 399         if (converter != null) {
 400             return converter;
 401         } else {
 402             return defaultConverter;
 403         }
 404     }
 405 
 406     // Create a symmetric (format/parse) converter with the default locale.
 407     private StringConverter<LocalDate> defaultConverter =
 408                 new LocalDateStringConverter(FormatStyle.SHORT, null, getChronology());
 409 
 410 
 411     // --- Editor
 412     /**
 413      * The editor for the DatePicker.
 414      *
 415      * @see javafx.scene.control.ComboBox#editorProperty
 416      */
 417     private ReadOnlyObjectWrapper<TextField> editor;
 418     public final TextField getEditor() {
 419         return editorProperty().get();
 420     }
 421     public final ReadOnlyObjectProperty<TextField> editorProperty() {
 422         if (editor == null) {
 423             editor = new ReadOnlyObjectWrapper<>(this, "editor");
 424             editor.set(new FakeFocusTextField());
 425         }
 426         return editor.getReadOnlyProperty();
 427     }
 428 
 429     /** {@inheritDoc} */
 430     @Override protected Skin<?> createDefaultSkin() {
 431         return new DatePickerSkin(this);
 432     }
 433 
 434 
 435     /***************************************************************************
 436      *                                                                         *
 437      * Stylesheet Handling                                                     *
 438      *                                                                         *
 439      **************************************************************************/
 440 
 441     private static final String DEFAULT_STYLE_CLASS = "date-picker";
 442 
 443     private static class StyleableProperties {
 444         private static final String country =
 445             Locale.getDefault(Locale.Category.FORMAT).getCountry();
 446         private static final CssMetaData<DatePicker, Boolean> SHOW_WEEK_NUMBERS =
 447               new CssMetaData<DatePicker, Boolean>("-fx-show-week-numbers",
 448                    BooleanConverter.getInstance(),
 449                    (!country.isEmpty() &&
 450                     ControlResources.getNonTranslatableString("DatePicker.showWeekNumbers").contains(country))) {
 451             @Override public boolean isSettable(DatePicker n) {
 452                 return n.showWeekNumbers == null || !n.showWeekNumbers.isBound();
 453             }
 454 
 455             @Override public StyleableProperty<Boolean> getStyleableProperty(DatePicker n) {
 456                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)n.showWeekNumbersProperty();
 457             }
 458         };
 459 
 460         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 461 
 462         static {
 463             final List<CssMetaData<? extends Styleable, ?>> styleables =
 464                 new ArrayList<CssMetaData<? extends Styleable, ?>>(Control.getClassCssMetaData());
 465             Collections.addAll(styleables,
 466                 SHOW_WEEK_NUMBERS
 467             );
 468             STYLEABLES = Collections.unmodifiableList(styleables);
 469         }
 470     }
 471 
 472     /**
 473      * @return The CssMetaData associated with this class, which may include the
 474      * CssMetaData of its superclasses.
 475      */
 476     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 477         return StyleableProperties.STYLEABLES;
 478     }
 479 
 480     /**
 481      * {@inheritDoc}
 482      * @since JavaFX 8.0
 483      */
 484     @Override
 485     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
 486         return getClassCssMetaData();
 487     }
 488 
 489     /***************************************************************************
 490      *                                                                         *
 491      * Accessibility handling                                                  *
 492      *                                                                         *
 493      **************************************************************************/
 494 
 495     @Override
 496     public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 497         switch (attribute) {
 498             case DATE: return getValue();
 499             case TEXT: {
 500                 String accText = getAccessibleText();
 501                 if (accText != null && !accText.isEmpty()) return accText;
 502 
 503                 LocalDate date = getValue();
 504                 StringConverter<LocalDate> c = getConverter();
 505                 if (date != null && c != null) {
 506                     return c.toString(date);
 507                 }
 508                 return "";
 509             }
 510             default: return super.queryAccessibleAttribute(attribute, parameters);
 511         }
 512     }
 513 
 514 }