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<DatePicker, DateCell> dayCellFactory = new Callback<DatePicker, DateCell>() { 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<LocalDate>() { 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<LocalDate> defaultConverter = datePicker.getConverter(); 345 * datePicker.setConverter(new StringConverter<LocalDate>() { 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 }