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 */ 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<LocalDate>() { 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<LocalDate> defaultConverter = datePicker.getConverter(); 342 * datePicker.setConverter(new StringConverter<LocalDate>() { 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 super classes. 471 */ 472 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 473 return StyleableProperties.STYLEABLES; 474 } 475 476 /** 477 * {@inheritDoc} 478 */ 479 @Override 480 public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() { 481 return getClassCssMetaData(); 482 } 483 484 /*************************************************************************** 485 * * 486 * Accessibility handling * 487 * * 488 **************************************************************************/ 489 490 @Override 491 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 492 switch (attribute) { 493 case DATE: return getValue(); 494 case TEXT: { 495 String accText = getAccessibleText(); 496 if (accText != null && !accText.isEmpty()) return accText; 497 498 LocalDate date = getValue(); 499 StringConverter<LocalDate> c = getConverter(); 500 if (date != null && c != null) { 501 return c.toString(date); 502 } 503 return ""; 504 } 505 default: return super.queryAccessibleAttribute(attribute, parameters); 506 } 507 } 508 509 }