Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.scene.control;
027
028// editor and converter code in sync with ComboBox 2708:a3e606ef6ead
029
030import java.time.LocalDate;
031import java.time.DateTimeException;
032import java.time.chrono.Chronology;
033import java.time.chrono.ChronoLocalDate;
034import java.time.chrono.IsoChronology;
035// import java.time.format.DateTimeFormatSymbols;
036import java.time.format.DateTimeFormatter;
037import java.time.format.DateTimeParseException;
038import java.time.format.FormatStyle;
039import java.time.temporal.TemporalAccessor;
040import java.util.ArrayList;
041import java.util.Collections;
042import java.util.List;
043import java.util.Locale;
044
045import javafx.beans.property.BooleanProperty;
046import javafx.beans.property.ObjectProperty;
047import javafx.beans.property.ReadOnlyObjectProperty;
048import javafx.beans.property.ReadOnlyObjectWrapper;
049import javafx.beans.property.SimpleObjectProperty;
050import javafx.beans.value.ChangeListener;
051import javafx.beans.value.ObservableValue;
052import javafx.beans.value.WritableValue;
053import javafx.collections.FXCollections;
054import javafx.collections.ListChangeListener;
055import javafx.collections.ObservableList;
056import javafx.css.CssMetaData;
057import javafx.css.Styleable;
058import javafx.css.StyleableBooleanProperty;
059import javafx.css.StyleableProperty;
060import javafx.util.Callback;
061import javafx.util.StringConverter;
062
063import com.sun.javafx.css.converters.BooleanConverter;
064import com.sun.javafx.scene.control.skin.DatePickerSkin;
065import com.sun.javafx.scene.control.skin.resources.ControlResources;
066import sun.util.locale.provider.LocaleProviderAdapter;
067
068
069
070/**
071 * The DatePicker control allows the user to enter a date as text or
072 * to select a date from a calendar popup. The calendar is based on
073 * either the standard ISO-8601 chronology or any of the other
074 * chronology classes defined in the java.time.chrono package.
075 *
076 * <p>The {@link #valueProperty() value} property represents the
077 * currently selected {@link java.time.LocalDate}.  An initial date can
078 * be set via the {@link #DatePicker(java.time.LocalDate) constructor}
079 * or by calling {@link #setValue(java.time.LocalDate) setValue()}.  The
080 * default value is null.
081 *
082 * <pre><code>
083 * final DatePicker datePicker = new DatePicker();
084 * datePicker.setOnAction(new EventHandler() {
085 *     public void handle(Event t) {
086 *         LocalDate date = datePicker.getValue();
087 *         System.err.println("Selected date: " + date);
088 *     }
089 * });
090 * </code></pre>
091 *
092 * The {@link #chronologyProperty() chronology} property specifies a
093 * calendar system to be used for parsing, displaying, and choosing
094 * dates.
095 * The {@link #valueProperty() value} property is always defined in
096 * the ISO calendar system, however, so applications based on a
097 * different chronology may use the conversion methods provided in the
098 * {@link java.time.chrono.Chronology} API to get or set the
099 * corresponding {@link java.time.chrono.ChronoLocalDate} value. For
100 * example:
101 *
102 * <pre><code>
103 * LocalDate isoDate = datePicker.getValue();
104 * ChronoLocalDate chronoDate =
105 *     ((isoDate != null) ? datePicker.getChronology().date(isoDate) : null);
106 * System.err.println("Selected date: " + chronoDate);
107 * </code></pre>
108 *
109 *
110 * @since 8.0
111 */
112public class DatePicker extends ComboBoxBase<LocalDate> {
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
121    /**
122     * Creates a DatePicker instance and sets the
123     * {@link #valueProperty() value} to the given date.
124     *
125     * @param localDate to be set as the currently selected date in the DatePicker. Can be null.
126     */
127    public DatePicker(LocalDate localDate) {
128        setValue(localDate);
129        getStyleClass().add(DEFAULT_STYLE_CLASS);
130        setEditable(true);
131    }
132
133
134    /***************************************************************************
135     *                                                                         *
136     * Properties                                                                 *
137     *                                                                         *
138     **************************************************************************/
139
140
141    /**
142     * A custom cell factory can be provided to customize individual
143     * day cells in the DatePicker popup. Refer to {@link DateCell}
144     * and {@link Cell} for more information on cell factories.
145     * Example:
146     *
147     * <pre><code>
148     * final Callback&lt;DatePicker, DateCell&gt; dayCellFactory = new Callback&lt;DatePicker, DateCell&gt;() {
149     *     public DateCell call(final DatePicker datePicker) {
150     *         return new DateCell() {
151     *             &#064;Override public void updateItem(LocalDate item, boolean empty) {
152     *                 super.updateItem(item, empty);
153     *
154     *                 if (MonthDay.from(item).equals(MonthDay.of(9, 25))) {
155     *                     setTooltip(new Tooltip("Happy Birthday!"));
156     *                     setStyle("-fx-background-color: #ff4444;");
157     *                 }
158     *                 if (item.equals(LocalDate.now().plusDays(1))) {
159     *                     // Tomorrow is too soon.
160     *                     setDisable(true);
161     *                 }
162     *             }
163     *         };
164     *     }
165     * };
166     * datePicker.setDayCellFactory(dayCellFactory);
167     * </code></pre>
168     *
169     * @defaultValue null
170     */
171    private ObjectProperty<Callback<DatePicker, DateCell>> dayCellFactory;
172    public final void setDayCellFactory(Callback<DatePicker, DateCell> value) {
173        dayCellFactoryProperty().set(value);
174    }
175    public final Callback<DatePicker, DateCell> getDayCellFactory() {
176        return (dayCellFactory != null) ? dayCellFactory.get() : null;
177    }
178    public final ObjectProperty<Callback<DatePicker, DateCell>> dayCellFactoryProperty() {
179        if (dayCellFactory == null) {
180            dayCellFactory = new SimpleObjectProperty<Callback<DatePicker, DateCell>>(this, "dayCellFactory");
181        }
182        return dayCellFactory;
183    }
184
185
186
187    /**
188     * The calendar system used for parsing, displaying, and choosing
189     * dates in the DatePicker control.
190     *
191     * <p>The default value is returned from a call to
192     * {@code Chronology.ofLocale(Locale.getDefault(Locale.Category.FORMAT))}.
193     * The default is usually {@link java.time.chrono.IsoChronology} unless
194     * provided explicitly in the {@link java.util.Locale} by use of a
195     * Locale calendar extension.
196     *
197     * Setting the value to <code>null</code> will restore the default
198     * chronology.
199     */
200    public final ObjectProperty<Chronology> chronologyProperty() {
201        return chronology;
202    }
203    private ObjectProperty<Chronology> chronology =
204        new SimpleObjectProperty<Chronology>(this, "chronology", null);
205    public final Chronology getChronology() {
206        Chronology chrono = chronology.get();
207        if (chrono == null) {
208            try {
209                chrono = Chronology.ofLocale(Locale.getDefault(Locale.Category.FORMAT));
210            } catch (Exception ex) {
211                System.err.println(ex);
212            }
213            if (chrono == null) {
214                chrono = IsoChronology.INSTANCE;
215            }
216            //System.err.println(chrono);
217        }
218        return chrono;
219    }
220    public final void setChronology(Chronology value) {
221        chronology.setValue(value);
222    }
223
224
225    /**
226     * Whether the DatePicker popup should display a column showing
227     * week numbers.
228     *
229     * <p>The default value is false unless otherwise defined in a
230     * resource bundle for the current locale.
231     *
232     * <p>This property may be toggled by the end user by using a
233     * context menu in the DatePicker popup, so it is recommended that
234     * applications save and restore the value between sessions.
235     */
236    public final BooleanProperty showWeekNumbersProperty() {
237        if (showWeekNumbers == null) {
238            String country = Locale.getDefault(Locale.Category.FORMAT).getCountry();
239            boolean localizedDefault =
240                (!country.isEmpty() &&
241                 ControlResources.getNonTranslatableString("DatePicker.showWeekNumbers").contains(country));
242            showWeekNumbers = new StyleableBooleanProperty(localizedDefault) {
243                @Override public CssMetaData getCssMetaData() {
244                    return StyleableProperties.SHOW_WEEK_NUMBERS;
245                }
246
247                @Override public Object getBean() {
248                    return DatePicker.this;
249                }
250
251                @Override public String getName() {
252                    return "showWeekNumbers";
253                }
254            };
255        }
256        return showWeekNumbers;
257    }
258    private BooleanProperty showWeekNumbers;
259    public final void setShowWeekNumbers(boolean value) {
260        showWeekNumbersProperty().setValue(value);
261    }
262    public final boolean isShowWeekNumbers() {
263        return showWeekNumbersProperty().getValue();
264    }
265
266
267    // --- string converter
268    /**
269     * Converts the input text to an object of type LocalDate and vice
270     * versa.
271     *
272     * <p>If not set by the application, the DatePicker skin class will
273     * set a converter based on a {@link java.time.DateTimeFormatter}
274     * for the current {@link java.util.Locale} and
275     * {@link #chronologyProperty() chronology}. This formatter is
276     * then used to parse and display the current date value.
277     *
278     * Setting the value to <code>null</code> will restore the default
279     * converter.
280     *
281     * <p>Example using an explicit formatter:
282     * <pre><code>
283     * datePicker.setConverter(new StringConverter&lt;LocalDate&gt;() {
284     *     String pattern = "yyyy-MM-dd";
285     *     DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(pattern);
286     *
287     *     {
288     *         datePicker.setPromptText(pattern.toLowerCase());
289     *     }
290     *
291     *     &#064;Override public String toString(LocalDate date) {
292     *         if (date != null) {
293     *             return dateFormatter.format(date);
294     *         } else {
295     *             return "";
296     *         }
297     *     }
298     *
299     *     &#064;Override public LocalDate fromString(String string) {
300     *         if (string != null && !string.isEmpty()) {
301     *             return LocalDate.parse(string, dateFormatter);
302     *         } else {
303     *             return null;
304     *         }
305     *     }
306     * });
307     * </code></pre>
308     * <p>Example that wraps the default formatter and catches parse exceptions:
309     * <pre><code>
310     *   final StringConverter&lt;LocalDate&gt; defaultConverter = datePicker.getConverter();
311     *   datePicker.setConverter(new StringConverter&lt;LocalDate&gt;() {
312     *       &#064;Override public String toString(LocalDate value) {
313     *           return defaultConverter.toString(value);
314     *       }
315     *
316     *       &#064;Override public LocalDate fromString(String text) {
317     *           try {
318     *               return defaultConverter.fromString(text);
319     *           } catch (DateTimeParseException ex) {
320     *               System.err.println("HelloDatePicker: "+ex.getMessage());
321     *               throw ex;
322     *           }
323     *       }
324     *   });
325     * </code></pre>
326     *
327     * @see javafx.scene.control.ComboBox#converterProperty
328     */
329    public final ObjectProperty<StringConverter<LocalDate>> converterProperty() { return converter; }
330    private ObjectProperty<StringConverter<LocalDate>> converter =
331            new SimpleObjectProperty<StringConverter<LocalDate>>(this, "converter", null);
332    public final void setConverter(StringConverter<LocalDate> value) { converterProperty().set(value); }
333    public final StringConverter<LocalDate> getConverter() {
334        StringConverter<LocalDate> converter = converterProperty().get();
335        if (converter != null) {
336            return converter;
337        } else {
338            return defaultConverter;
339        }
340    }
341
342    private StringConverter<LocalDate> defaultConverter = new StringConverter<LocalDate>() {
343        @Override public String toString(LocalDate value) {
344            if (value != null) {
345                Locale locale = Locale.getDefault(Locale.Category.FORMAT);
346                Chronology chrono = getChronology();
347                ChronoLocalDate cDate;
348                try {
349                    cDate = chrono.date(value);
350                } catch (DateTimeException ex) {
351                    System.err.println(ex);
352                    chrono = IsoChronology.INSTANCE;
353                    cDate = value;
354                }
355                DateTimeFormatter dateFormatter =
356                    DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
357                                     .withLocale(locale)
358                                     .withChronology(chrono);
359//                                      .withSymbols(DateTimeFormatSymbols.of(locale));
360
361                String pattern = getPattern();
362                //System.err.println("pattern = "+pattern);
363                if (pattern.contains("yy") && !pattern.contains("yyy")) {
364                    // Modify pattern to show four-digit year, including leading zeros.
365                    String newPattern = pattern.replace("yy", "yyyy");
366                    //System.err.println("Fixing pattern ("+forParsing+"): "+pattern+" -> "+newPattern);
367                    dateFormatter = DateTimeFormatter.ofPattern(newPattern);
368                }
369
370                return dateFormatter.format(cDate);
371            } else {
372                return "";
373            }
374        }
375
376        @Override public LocalDate fromString(String text) {
377            if (text != null && !text.isEmpty()) {
378                Locale locale = Locale.getDefault(Locale.Category.FORMAT);
379                Chronology chrono = getChronology();
380
381                String pattern = getPattern();
382                if (pattern.contains("yy") && !pattern.contains("yyy")) {
383                    // First try a pattern using four-digit year
384                    pattern = pattern.replace("yy", "yyyy");
385                    DateTimeFormatter df = DateTimeFormatter.ofPattern(pattern);
386                    try {
387                        TemporalAccessor temporal = df.parse(text);
388                        try {
389                            ChronoLocalDate cDate = chrono.date(temporal);
390                            return LocalDate.from(cDate);
391                        } catch (DateTimeException ex) {
392                            System.err.println(ex);
393                            return null;
394                        }
395                    } catch (DateTimeParseException ex) {
396                        // Didn't work, so continue with original two-digit pattern
397                    }
398                }
399
400                DateTimeFormatter df =
401                    DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
402                                     .withLocale(locale)
403                                     .withChronology(chrono);
404//                                      .withSymbols(DateTimeFormatSymbols.of(locale));
405                TemporalAccessor temporal = df.parse(text);
406                ChronoLocalDate cDate = chrono.date(temporal);
407                return LocalDate.from(cDate);
408            }
409            return null;
410        }
411    };
412
413    private String getPattern() {
414        Locale locale = Locale.getDefault(Locale.Category.FORMAT);
415        Chronology chrono = getChronology();
416
417        return LocaleProviderAdapter.getResourceBundleBased()
418                                    .getLocaleResources(locale)
419                                    .getJavaTimeDateTimePattern(-1,
420                                                                FormatStyle.SHORT.ordinal(),
421                                                                chrono.getCalendarType());
422    }
423
424
425
426    // --- Editor
427    /**
428     * The editor for the DatePicker.
429     *
430     * @see javafx.scene.control.ComboBox#editorProperty
431     */
432    private ReadOnlyObjectWrapper<TextField> editor;
433    public final TextField getEditor() {
434        return editorProperty().get();
435    }
436    public final ReadOnlyObjectProperty<TextField> editorProperty() {
437        if (editor == null) {
438            editor = new ReadOnlyObjectWrapper<TextField>(this, "editor");
439            editor.set(new TextField());
440        }
441        return editor.getReadOnlyProperty();
442    }
443
444    /** {@inheritDoc} */
445    @Override protected Skin<?> createDefaultSkin() {
446        return new DatePickerSkin(this);
447    }
448
449
450    /***************************************************************************
451     *                                                                         *
452     * Stylesheet Handling                                                     *
453     *                                                                         *
454     **************************************************************************/
455
456    private static final String DEFAULT_STYLE_CLASS = "date-picker";
457
458     /**
459      * @treatAsPrivate implementation detail
460      */
461    private static class StyleableProperties {
462        private static final String country =
463            Locale.getDefault(Locale.Category.FORMAT).getCountry();
464        private static final CssMetaData<DatePicker, Boolean> SHOW_WEEK_NUMBERS =
465              new CssMetaData<DatePicker, Boolean>("-fx-show-week-numbers",
466                   BooleanConverter.getInstance(),
467                   (!country.isEmpty() &&
468                    ControlResources.getNonTranslatableString("DatePicker.showWeekNumbers").contains(country))) {
469            @Override public boolean isSettable(DatePicker n) {
470                return n.showWeekNumbers == null || !n.showWeekNumbers.isBound();
471            }
472
473            @Override public StyleableProperty<Boolean> getStyleableProperty(DatePicker n) {
474                return (StyleableProperty)n.showWeekNumbersProperty();
475            }
476        };
477
478        private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
479
480        static {
481            final List<CssMetaData<? extends Styleable, ?>> styleables =
482                new ArrayList<CssMetaData<? extends Styleable, ?>>(Control.getClassCssMetaData());
483            Collections.addAll(styleables,
484                SHOW_WEEK_NUMBERS
485            );
486            STYLEABLES = Collections.unmodifiableList(styleables);
487        }
488    }
489
490    /**
491     * @return The CssMetaData associated with this class, which may include the
492     * CssMetaData of its super classes.
493     */
494    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
495        return StyleableProperties.STYLEABLES;
496    }
497
498    /**
499     * {@inheritDoc}
500     */
501    @Override
502    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
503        return getClassCssMetaData();
504    }
505}