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<DatePicker, DateCell> dayCellFactory = new Callback<DatePicker, DateCell>() { 185 * public DateCell call(final DatePicker datePicker) { 186 * return new DateCell() { 187 * @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<LocalDate>() { 316 * String pattern = "yyyy-MM-dd"; 317 * DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(pattern); 318 * 319 * { 320 * datePicker.setPromptText(pattern.toLowerCase()); 321 * } 322 * 323 * @Override public String toString(LocalDate date) { 324 * if (date != null) { 325 * return dateFormatter.format(date); 326 * } else { 327 * return ""; 328 * } 329 * } 330 * 331 * @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<LocalDate> defaultConverter = datePicker.getConverter(); 343 * datePicker.setConverter(new StringConverter<LocalDate>() { 344 * @Override public String toString(LocalDate value) { 345 * return defaultConverter.toString(value); 346 * } 347 * 348 * @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 * @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 }