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