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      */
 235     public final ObjectProperty<Chronology> chronologyProperty() {
 236         return chronology;
 237     }
 238     private ObjectProperty<Chronology> chronology =
 239         new SimpleObjectProperty<Chronology>(this, "chronology", null);
 240     public final Chronology getChronology() {
 241         Chronology chrono = chronology.get();
 242         if (chrono == null) {
 243             try {
 244                 chrono = Chronology.ofLocale(Locale.getDefault(Locale.Category.FORMAT));
 245             } catch (Exception ex) {
 246                 System.err.println(ex);
 247             }
 248             if (chrono == null) {
 249                 chrono = IsoChronology.INSTANCE;
 250             }
 251             //System.err.println(chrono);
 252         }
 253         return chrono;
 254     }
 255     public final void setChronology(Chronology value) {
 256         chronology.setValue(value);
 257     }
 258 
 259 
 260     /**
 261      * Whether the DatePicker popup should display a column showing
 262      * week numbers.
 263      *
 264      * <p>The default value is specified in a resource bundle, and
 265      * depends on the country of the current locale.
 266      */
 267     public final BooleanProperty showWeekNumbersProperty() {
 268         if (showWeekNumbers == null) {
 269             String country = Locale.getDefault(Locale.Category.FORMAT).getCountry();
 270             boolean localizedDefault =
 271                 (!country.isEmpty() &&
 272                  ControlResources.getNonTranslatableString("DatePicker.showWeekNumbers").contains(country));
 273             showWeekNumbers = new StyleableBooleanProperty(localizedDefault) {
 274                 @Override public CssMetaData<DatePicker,Boolean> getCssMetaData() {
 275                     return StyleableProperties.SHOW_WEEK_NUMBERS;
 276                 }
 277 
 278                 @Override public Object getBean() {
 279                     return DatePicker.this;
 280                 }
 281 
 282                 @Override public String getName() {
 283                     return "showWeekNumbers";
 284                 }
 285             };
 286         }
 287         return showWeekNumbers;
 288     }
 289     private BooleanProperty showWeekNumbers;
 290     public final void setShowWeekNumbers(boolean value) {
 291         showWeekNumbersProperty().setValue(value);
 292     }
 293     public final boolean isShowWeekNumbers() {
 294         return showWeekNumbersProperty().getValue();
 295     }
 296 
 297 
 298     // --- string converter
 299     /**
 300      * Converts the input text to an object of type LocalDate and vice
 301      * versa.
 302      *
 303      * <p>If not set by the application, the DatePicker skin class will
 304      * set a converter based on a {@link java.time.format.DateTimeFormatter}
 305      * for the current {@link java.util.Locale} and
 306      * {@link #chronologyProperty() chronology}. This formatter is
 307      * then used to parse and display the current date value.
 308      *
 309      * Setting the value to <code>null</code> will restore the default
 310      * converter.
 311      *
 312      * <p>Example using an explicit formatter:
 313      * <pre><code>
 314      * datePicker.setConverter(new StringConverter&lt;LocalDate&gt;() {
 315      *     String pattern = "yyyy-MM-dd";
 316      *     DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(pattern);
 317      *
 318      *     {
 319      *         datePicker.setPromptText(pattern.toLowerCase());
 320      *     }
 321      *
 322      *     @Override public String toString(LocalDate date) {
 323      *         if (date != null) {
 324      *             return dateFormatter.format(date);
 325      *         } else {
 326      *             return "";
 327      *         }
 328      *     }
 329      *
 330      *     @Override public LocalDate fromString(String string) {
 331      *         if (string != null && !string.isEmpty()) {
 332      *             return LocalDate.parse(string, dateFormatter);
 333      *         } else {
 334      *             return null;
 335      *         }
 336      *     }
 337      * });
 338      * </code></pre>
 339      * <p>Example that wraps the default formatter and catches parse exceptions:
 340      * <pre><code>
 341      *   final StringConverter&lt;LocalDate&gt; defaultConverter = datePicker.getConverter();
 342      *   datePicker.setConverter(new StringConverter&lt;LocalDate&gt;() {
 343      *       @Override public String toString(LocalDate value) {
 344      *           return defaultConverter.toString(value);
 345      *       }
 346      *
 347      *       @Override public LocalDate fromString(String text) {
 348      *           try {
 349      *               return defaultConverter.fromString(text);
 350      *           } catch (DateTimeParseException ex) {
 351      *               System.err.println("HelloDatePicker: "+ex.getMessage());
 352      *               throw ex;
 353      *           }
 354      *       }
 355      *   });
 356      * </code></pre>
 357      *
 358      * <p>The default base year for parsing input containing only two digits for
 359      * the year is 2000 (see {@link java.time.format.DateTimeFormatter}).  This
 360      * default is not useful for allowing a person's date of birth to be typed.
 361      * The following example modifies the converter's fromString() method to
 362      * allow a two digit year for birth dates up to 99 years in the past.
 363      * <pre><code>
 364      *   @Override public LocalDate fromString(String text) {
 365      *       if (text != null && !text.isEmpty()) {
 366      *           Locale locale = Locale.getDefault(Locale.Category.FORMAT);
 367      *           Chronology chrono = datePicker.getChronology();
 368      *           String pattern =
 369      *               DateTimeFormatterBuilder.getLocalizedDateTimePattern(FormatStyle.SHORT,
 370      *                                                                    null, chrono, locale);
 371      *           String prePattern = pattern.substring(0, pattern.indexOf("y"));
 372      *           String postPattern = pattern.substring(pattern.lastIndexOf("y")+1);
 373      *           int baseYear = LocalDate.now().getYear() - 99;
 374      *           DateTimeFormatter df = new DateTimeFormatterBuilder()
 375      *                       .parseLenient()
 376      *                       .appendPattern(prePattern)
 377      *                       .appendValueReduced(ChronoField.YEAR, 2, 2, baseYear)
 378      *                       .appendPattern(postPattern)
 379      *                       .toFormatter();
 380      *           return LocalDate.from(chrono.date(df.parse(text)));
 381      *       } else {
 382      *           return null;
 383      *       }
 384      *   }
 385      * </code></pre>
 386      *
 387      * @see javafx.scene.control.ComboBox#converterProperty
 388      */
 389     public final ObjectProperty<StringConverter<LocalDate>> converterProperty() { return converter; }
 390     private ObjectProperty<StringConverter<LocalDate>> converter =
 391             new SimpleObjectProperty<StringConverter<LocalDate>>(this, "converter", null);
 392     public final void setConverter(StringConverter<LocalDate> value) { converterProperty().set(value); }
 393     public final StringConverter<LocalDate> getConverter() {
 394         StringConverter<LocalDate> converter = converterProperty().get();
 395         if (converter != null) {
 396             return converter;
 397         } else {
 398             return defaultConverter;
 399         }
 400     }
 401 
 402     // Create a symmetric (format/parse) converter with the default locale.
 403     private StringConverter<LocalDate> defaultConverter =
 404                 new LocalDateStringConverter(FormatStyle.SHORT, null, getChronology());
 405 
 406 
 407     // --- Editor
 408     /**
 409      * The editor for the DatePicker.
 410      *
 411      * @see javafx.scene.control.ComboBox#editorProperty
 412      */
 413     private ReadOnlyObjectWrapper<TextField> editor;
 414     public final TextField getEditor() {
 415         return editorProperty().get();
 416     }
 417     public final ReadOnlyObjectProperty<TextField> editorProperty() {
 418         if (editor == null) {
 419             editor = new ReadOnlyObjectWrapper<>(this, "editor");
 420             editor.set(new FakeFocusTextField());
 421         }
 422         return editor.getReadOnlyProperty();
 423     }
 424 
 425     /** {@inheritDoc} */
 426     @Override protected Skin<?> createDefaultSkin() {
 427         return new DatePickerSkin(this);
 428     }
 429 
 430 
 431     /***************************************************************************
 432      *                                                                         *
 433      * Stylesheet Handling                                                     *
 434      *                                                                         *
 435      **************************************************************************/
 436 
 437     private static final String DEFAULT_STYLE_CLASS = "date-picker";
 438 
 439     private static class StyleableProperties {
 440         private static final String country =
 441             Locale.getDefault(Locale.Category.FORMAT).getCountry();
 442         private static final CssMetaData<DatePicker, Boolean> SHOW_WEEK_NUMBERS =
 443               new CssMetaData<DatePicker, Boolean>("-fx-show-week-numbers",
 444                    BooleanConverter.getInstance(),
 445                    (!country.isEmpty() &&
 446                     ControlResources.getNonTranslatableString("DatePicker.showWeekNumbers").contains(country))) {
 447             @Override public boolean isSettable(DatePicker n) {
 448                 return n.showWeekNumbers == null || !n.showWeekNumbers.isBound();
 449             }
 450 
 451             @Override public StyleableProperty<Boolean> getStyleableProperty(DatePicker n) {
 452                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)n.showWeekNumbersProperty();
 453             }
 454         };
 455 
 456         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 457 
 458         static {
 459             final List<CssMetaData<? extends Styleable, ?>> styleables =
 460                 new ArrayList<CssMetaData<? extends Styleable, ?>>(Control.getClassCssMetaData());
 461             Collections.addAll(styleables,
 462                 SHOW_WEEK_NUMBERS
 463             );
 464             STYLEABLES = Collections.unmodifiableList(styleables);
 465         }
 466     }
 467 
 468     /**
 469      * @return The CssMetaData associated with this class, which may include the
 470      * CssMetaData of its superclasses.
 471      */
 472     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 473         return StyleableProperties.STYLEABLES;
 474     }
 475 
 476     /**
 477      * {@inheritDoc}
 478      * @since JavaFX 8.0
 479      */
 480     @Override
 481     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
 482         return getClassCssMetaData();
 483     }
 484 
 485     /***************************************************************************
 486      *                                                                         *
 487      * Accessibility handling                                                  *
 488      *                                                                         *
 489      **************************************************************************/
 490 
 491     @Override
 492     public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 493         switch (attribute) {
 494             case DATE: return getValue();
 495             case TEXT: {
 496                 String accText = getAccessibleText();
 497                 if (accText != null && !accText.isEmpty()) return accText;
 498 
 499                 LocalDate date = getValue();
 500                 StringConverter<LocalDate> c = getConverter();
 501                 if (date != null && c != null) {
 502                     return c.toString(date);
 503                 }
 504                 return "";
 505             }
 506             default: return super.queryAccessibleAttribute(attribute, parameters);
 507         }
 508     }
 509 
 510 }