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