1 /* 2 * Copyright (c) 2013, 2015, 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 com.sun.javafx.scene.control; 27 28 import java.time.DateTimeException; 29 import java.time.LocalDate; 30 import java.time.format.DateTimeFormatter; 31 import java.time.format.DecimalStyle; 32 import java.time.chrono.Chronology; 33 import java.time.chrono.ChronoLocalDate; 34 import java.time.temporal.ChronoUnit; 35 import java.time.temporal.ValueRange; 36 import java.time.temporal.WeekFields; 37 import java.time.YearMonth; 38 import java.util.ArrayList; 39 import java.util.List; 40 import java.util.Locale; 41 42 import static java.time.temporal.ChronoField.*; 43 import static java.time.temporal.ChronoUnit.*; 44 45 import com.sun.javafx.scene.control.skin.*; 46 import javafx.application.Platform; 47 import javafx.beans.property.ObjectProperty; 48 import javafx.beans.property.SimpleObjectProperty; 49 import javafx.beans.value.WeakChangeListener; 50 import javafx.event.EventHandler; 51 import javafx.scene.Node; 52 import javafx.scene.Scene; 53 import javafx.scene.control.Button; 54 import javafx.scene.control.DatePicker; 55 import javafx.scene.control.DateCell; 56 import javafx.scene.control.Label; 57 import javafx.scene.input.KeyEvent; 58 import javafx.scene.input.MouseButton; 59 import javafx.scene.input.MouseEvent; 60 import javafx.scene.layout.BorderPane; 61 import javafx.scene.layout.ColumnConstraints; 62 import javafx.scene.layout.GridPane; 63 import javafx.scene.layout.HBox; 64 import javafx.scene.layout.VBox; 65 import javafx.scene.layout.StackPane; 66 67 import com.sun.javafx.scene.control.skin.resources.ControlResources; 68 import com.sun.javafx.scene.traversal.Direction; 69 70 import static com.sun.javafx.PlatformUtil.*; 71 72 /** 73 * The full content for the DatePicker popup. This class could 74 * probably be used more or less as-is with an embeddable type of date 75 * picker that doesn't use a popup. 76 */ 77 public class DatePickerContent extends VBox { 78 protected DatePicker datePicker; 79 private Button backMonthButton; 80 private Button forwardMonthButton; 81 private Button backYearButton; 82 private Button forwardYearButton; 83 private Label monthLabel; 84 private Label yearLabel; 85 protected GridPane gridPane; 86 87 private int daysPerWeek; 88 private List<DateCell> dayNameCells = new ArrayList<DateCell>(); 89 private List<DateCell> weekNumberCells = new ArrayList<DateCell>(); 90 protected List<DateCell> dayCells = new ArrayList<DateCell>(); 91 private LocalDate[] dayCellDates; 92 private DateCell lastFocusedDayCell = null; 93 94 final DateTimeFormatter monthFormatter = 95 DateTimeFormatter.ofPattern("MMMM"); 96 97 final DateTimeFormatter monthFormatterSO = 98 DateTimeFormatter.ofPattern("LLLL"); // Standalone month name 99 100 final DateTimeFormatter yearFormatter = 101 DateTimeFormatter.ofPattern("y"); 102 103 final DateTimeFormatter yearWithEraFormatter = 104 DateTimeFormatter.ofPattern("GGGGy"); // For Japanese. What to use for others?? 105 106 final DateTimeFormatter weekNumberFormatter = 107 DateTimeFormatter.ofPattern("w"); 108 109 final DateTimeFormatter weekDayNameFormatter = 110 DateTimeFormatter.ofPattern("ccc"); // Standalone day name 111 112 final DateTimeFormatter dayCellFormatter = 113 DateTimeFormatter.ofPattern("d"); 114 115 static String getString(String key) { 116 return ControlResources.getString("DatePicker."+key); 117 } 118 119 public DatePickerContent(final DatePicker datePicker) { 120 this.datePicker = datePicker; 121 122 getStyleClass().add("date-picker-popup"); 123 124 daysPerWeek = getDaysPerWeek(); 125 126 { 127 LocalDate date = datePicker.getValue(); 128 displayedYearMonth.set((date != null) ? YearMonth.from(date) : YearMonth.now()); 129 } 130 131 displayedYearMonth.addListener((observable, oldValue, newValue) -> { 132 updateValues(); 133 }); 134 135 136 getChildren().add(createMonthYearPane()); 137 138 gridPane = new GridPane() { 139 @Override protected double computePrefWidth(double height) { 140 final double width = super.computePrefWidth(height); 141 142 // RT-30903: Make sure width snaps to pixel when divided by 143 // number of columns. GridPane doesn't do this with percentage 144 // width constraints. See GridPane.adjustColumnWidths(). 145 final int nCols = daysPerWeek + (datePicker.isShowWeekNumbers() ? 1 : 0); 146 final double snaphgap = snapSpace(getHgap()); 147 final double left = snapSpace(getInsets().getLeft()); 148 final double right = snapSpace(getInsets().getRight()); 149 final double hgaps = snaphgap * (nCols - 1); 150 final double contentWidth = width - left - right - hgaps; 151 return ((snapSize(contentWidth / nCols)) * nCols) + left + right + hgaps; 152 } 153 154 @Override protected void layoutChildren() { 155 // Prevent AssertionError in GridPane 156 if (getWidth() > 0 && getHeight() > 0) { 157 super.layoutChildren(); 158 } 159 } 160 }; 161 gridPane.setFocusTraversable(true); 162 gridPane.getStyleClass().add("calendar-grid"); 163 gridPane.setVgap(-1); 164 gridPane.setHgap(-1); 165 166 // Add a focus owner listener to Scene when it becomes available. 167 final WeakChangeListener<Node> weakFocusOwnerListener = 168 new WeakChangeListener<Node>((ov2, oldFocusOwner, newFocusOwner) -> { 169 if (newFocusOwner == gridPane) { 170 if (oldFocusOwner instanceof DateCell) { 171 // Backwards traversal, skip gridPane. 172 gridPane.impl_traverse(Direction.PREVIOUS); 173 } else { 174 // Forwards traversal, pass focus to day cell. 175 if (lastFocusedDayCell != null) { 176 Platform.runLater(() -> { 177 lastFocusedDayCell.requestFocus(); 178 }); 179 } else { 180 clearFocus(); 181 } 182 } 183 } 184 }); 185 gridPane.sceneProperty().addListener(new WeakChangeListener<Scene>((ov, oldScene, newScene) -> { 186 if (oldScene != null) { 187 oldScene.focusOwnerProperty().removeListener(weakFocusOwnerListener); 188 } 189 if (newScene != null) { 190 Platform.runLater(() -> { 191 newScene.focusOwnerProperty().addListener(weakFocusOwnerListener); 192 }); 193 } 194 })); 195 if (gridPane.getScene() != null) { 196 gridPane.getScene().focusOwnerProperty().addListener(weakFocusOwnerListener); 197 } 198 199 // get the weekday labels starting with the weekday that is the 200 // first-day-of-the-week according to the locale in the 201 // displayed LocalDate 202 for (int i = 0; i < daysPerWeek; i++) { 203 DateCell cell = new DateCell(); 204 cell.getStyleClass().add("day-name-cell"); 205 dayNameCells.add(cell); 206 } 207 208 // Week number column 209 for (int i = 0; i < 6; i++) { 210 DateCell cell = new DateCell(); 211 cell.getStyleClass().add("week-number-cell"); 212 weekNumberCells.add(cell); 213 } 214 215 createDayCells(); 216 updateGrid(); 217 getChildren().add(gridPane); 218 219 refresh(); 220 221 // RT-30511: This prevents key events from reaching the popup's owner. 222 addEventHandler(KeyEvent.ANY, e -> { 223 Node node = getScene().getFocusOwner(); 224 if (node instanceof DateCell) { 225 lastFocusedDayCell = (DateCell)node; 226 } 227 228 if (e.getEventType() == KeyEvent.KEY_PRESSED) { 229 switch (e.getCode()) { 230 case HOME: 231 goToDate(LocalDate.now(), true); 232 e.consume(); 233 break; 234 235 236 case PAGE_UP: 237 if ((isMac() && e.isMetaDown()) || (!isMac() && e.isControlDown())) { 238 if (!backYearButton.isDisabled()) { 239 forward(-1, YEARS, true); 240 } 241 } else { 242 if (!backMonthButton.isDisabled()) { 243 forward(-1, MONTHS, true); 244 } 245 } 246 e.consume(); 247 break; 248 249 case PAGE_DOWN: 250 if ((isMac() && e.isMetaDown()) || (!isMac() && e.isControlDown())) { 251 if (!forwardYearButton.isDisabled()) { 252 forward(1, YEARS, true); 253 } 254 } else { 255 if (!forwardMonthButton.isDisabled()) { 256 forward(1, MONTHS, true); 257 } 258 } 259 e.consume(); 260 break; 261 } 262 263 node = getScene().getFocusOwner(); 264 if (node instanceof DateCell) { 265 lastFocusedDayCell = (DateCell)node; 266 } 267 } 268 269 // Consume all key events except those that control 270 // showing the popup and traversal. 271 switch (e.getCode()) { 272 case F4: 273 case F10: 274 case UP: 275 case DOWN: 276 case LEFT: 277 case RIGHT: 278 case TAB: 279 break; 280 281 case ESCAPE: 282 datePicker.hide(); 283 e.consume(); 284 break; 285 286 default: 287 e.consume(); 288 } 289 }); 290 } 291 292 private ObjectProperty<YearMonth> displayedYearMonth = 293 new SimpleObjectProperty<YearMonth>(this, "displayedYearMonth"); 294 295 public ObjectProperty<YearMonth> displayedYearMonthProperty() { 296 return displayedYearMonth; 297 } 298 299 300 protected BorderPane createMonthYearPane() { 301 BorderPane monthYearPane = new BorderPane(); 302 monthYearPane.getStyleClass().add("month-year-pane"); 303 304 // Month spinner 305 306 HBox monthSpinner = new HBox(); 307 monthSpinner.getStyleClass().add("spinner"); 308 309 backMonthButton = new Button(); 310 backMonthButton.getStyleClass().add("left-button"); 311 312 forwardMonthButton = new Button(); 313 forwardMonthButton.getStyleClass().add("right-button"); 314 315 StackPane leftMonthArrow = new StackPane(); 316 leftMonthArrow.getStyleClass().add("left-arrow"); 317 leftMonthArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE); 318 backMonthButton.setGraphic(leftMonthArrow); 319 320 StackPane rightMonthArrow = new StackPane(); 321 rightMonthArrow.getStyleClass().add("right-arrow"); 322 rightMonthArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE); 323 forwardMonthButton.setGraphic(rightMonthArrow); 324 325 326 backMonthButton.setOnAction(t -> { 327 forward(-1, MONTHS, false); 328 }); 329 330 monthLabel = new Label(); 331 monthLabel.getStyleClass().add("spinner-label"); 332 333 forwardMonthButton.setOnAction(t -> { 334 forward(1, MONTHS, false); 335 }); 336 337 monthSpinner.getChildren().addAll(backMonthButton, monthLabel, forwardMonthButton); 338 monthYearPane.setLeft(monthSpinner); 339 340 // Year spinner 341 342 HBox yearSpinner = new HBox(); 343 yearSpinner.getStyleClass().add("spinner"); 344 345 backYearButton = new Button(); 346 backYearButton.getStyleClass().add("left-button"); 347 348 forwardYearButton = new Button(); 349 forwardYearButton.getStyleClass().add("right-button"); 350 351 StackPane leftYearArrow = new StackPane(); 352 leftYearArrow.getStyleClass().add("left-arrow"); 353 leftYearArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE); 354 backYearButton.setGraphic(leftYearArrow); 355 356 StackPane rightYearArrow = new StackPane(); 357 rightYearArrow.getStyleClass().add("right-arrow"); 358 rightYearArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE); 359 forwardYearButton.setGraphic(rightYearArrow); 360 361 362 backYearButton.setOnAction(t -> { 363 forward(-1, YEARS, false); 364 }); 365 366 yearLabel = new Label(); 367 yearLabel.getStyleClass().add("spinner-label"); 368 369 forwardYearButton.setOnAction(t -> { 370 forward(1, YEARS, false); 371 }); 372 373 yearSpinner.getChildren().addAll(backYearButton, yearLabel, forwardYearButton); 374 yearSpinner.setFillHeight(false); 375 monthYearPane.setRight(yearSpinner); 376 377 return monthYearPane; 378 } 379 380 private void refresh() { 381 updateMonthLabelWidth(); 382 updateDayNameCells(); 383 updateValues(); 384 } 385 386 public void updateValues() { 387 // Note: Preserve this order, as DatePickerHijrahContent needs 388 // updateDayCells before updateMonthYearPane(). 389 updateWeeknumberDateCells(); 390 updateDayCells(); 391 updateMonthYearPane(); 392 } 393 394 public void updateGrid() { 395 gridPane.getColumnConstraints().clear(); 396 gridPane.getChildren().clear(); 397 398 int nCols = daysPerWeek + (datePicker.isShowWeekNumbers() ? 1 : 0); 399 400 ColumnConstraints columnConstraints = new ColumnConstraints(); 401 columnConstraints.setPercentWidth(100); // Treated as weight 402 for (int i = 0; i < nCols; i++) { 403 gridPane.getColumnConstraints().add(columnConstraints); 404 } 405 406 for (int i = 0; i < daysPerWeek; i++) { 407 gridPane.add(dayNameCells.get(i), i + nCols - daysPerWeek, 1); // col, row 408 } 409 410 // Week number column 411 if (datePicker.isShowWeekNumbers()) { 412 for (int i = 0; i < 6; i++) { 413 gridPane.add(weekNumberCells.get(i), 0, i + 2); // col, row 414 } 415 } 416 417 // setup: 6 rows of daysPerWeek (which is the maximum number of cells required in the worst case layout) 418 for (int row = 0; row < 6; row++) { 419 for (int col = 0; col < daysPerWeek; col++) { 420 gridPane.add(dayCells.get(row*daysPerWeek+col), col + nCols - daysPerWeek, row + 2); 421 } 422 } 423 } 424 425 public void updateDayNameCells() { 426 // first day of week, 1 = monday, 7 = sunday 427 int firstDayOfWeek = WeekFields.of(getLocale()).getFirstDayOfWeek().getValue(); 428 429 // july 13th 2009 is a Monday, so a firstDayOfWeek=1 must come out of the 13th 430 LocalDate date = LocalDate.of(2009, 7, 12 + firstDayOfWeek); 431 for (int i = 0; i < daysPerWeek; i++) { 432 String name = weekDayNameFormatter.withLocale(getLocale()).format(date.plus(i, DAYS)); 433 dayNameCells.get(i).setText(titleCaseWord(name)); 434 } 435 } 436 437 public void updateWeeknumberDateCells() { 438 if (datePicker.isShowWeekNumbers()) { 439 final Locale locale = getLocale(); 440 final int maxWeeksPerMonth = 6; // TODO: Get this from chronology? 441 442 LocalDate firstOfMonth = displayedYearMonth.get().atDay(1); 443 for (int i = 0; i < maxWeeksPerMonth; i++) { 444 LocalDate date = firstOfMonth.plus(i, WEEKS); 445 // Use a formatter to ensure correct localization, 446 // such as when Thai numerals are required. 447 String cellText = 448 weekNumberFormatter.withLocale(locale) 449 .withDecimalStyle(DecimalStyle.of(locale)) 450 .format(date); 451 weekNumberCells.get(i).setText(cellText); 452 } 453 } 454 } 455 456 public void updateDayCells() { 457 Locale locale = getLocale(); 458 Chronology chrono = getPrimaryChronology(); 459 int firstOfMonthIdx = determineFirstOfMonthDayOfWeek(); 460 YearMonth curMonth = displayedYearMonth.get(); 461 462 // RT-31075: The following are now set in the try-catch block. 463 YearMonth prevMonth = null; 464 YearMonth nextMonth = null; 465 int daysInCurMonth = -1; 466 int daysInPrevMonth = -1; 467 int daysInNextMonth = -1; 468 469 for (int i = 0; i < 6 * daysPerWeek; i++) { 470 DateCell dayCell = dayCells.get(i); 471 dayCell.getStyleClass().setAll("cell", "date-cell", "day-cell"); 472 dayCell.setDisable(false); 473 dayCell.setStyle(null); 474 dayCell.setGraphic(null); 475 dayCell.setTooltip(null); 476 477 try { 478 if (daysInCurMonth == -1) { 479 daysInCurMonth = curMonth.lengthOfMonth(); 480 } 481 YearMonth month = curMonth; 482 int day = i - firstOfMonthIdx + 1; 483 //int index = firstOfMonthIdx + i - 1; 484 if (i < firstOfMonthIdx) { 485 if (prevMonth == null) { 486 prevMonth = curMonth.minusMonths(1); 487 daysInPrevMonth = prevMonth.lengthOfMonth(); 488 } 489 month = prevMonth; 490 day = i + daysInPrevMonth - firstOfMonthIdx + 1; 491 dayCell.getStyleClass().add("previous-month"); 492 } else if (i >= firstOfMonthIdx + daysInCurMonth) { 493 if (nextMonth == null) { 494 nextMonth = curMonth.plusMonths(1); 495 daysInNextMonth = nextMonth.lengthOfMonth(); 496 } 497 month = nextMonth; 498 day = i - daysInCurMonth - firstOfMonthIdx + 1; 499 dayCell.getStyleClass().add("next-month"); 500 } 501 LocalDate date = month.atDay(day); 502 dayCellDates[i] = date; 503 ChronoLocalDate cDate = chrono.date(date); 504 505 dayCell.setDisable(false); 506 507 if (isToday(date)) { 508 dayCell.getStyleClass().add("today"); 509 } 510 511 if (date.equals(datePicker.getValue())) { 512 dayCell.getStyleClass().add("selected"); 513 } 514 515 String cellText = 516 dayCellFormatter.withLocale(locale) 517 .withChronology(chrono) 518 .withDecimalStyle(DecimalStyle.of(locale)) 519 .format(cDate); 520 dayCell.setText(cellText); 521 522 dayCell.updateItem(date, false); 523 } catch (DateTimeException ex) { 524 // Date is out of range. 525 // System.err.println(dayCellDate(dayCell) + " " + ex); 526 dayCell.setText(" "); 527 dayCell.setDisable(true); 528 } 529 } 530 } 531 532 private int getDaysPerWeek() { 533 ValueRange range = getPrimaryChronology().range(DAY_OF_WEEK); 534 return (int)(range.getMaximum() - range.getMinimum() + 1); 535 } 536 537 private int getMonthsPerYear() { 538 ValueRange range = getPrimaryChronology().range(MONTH_OF_YEAR); 539 return (int)(range.getMaximum() - range.getMinimum() + 1); 540 } 541 542 private void updateMonthLabelWidth() { 543 if (monthLabel != null) { 544 int monthsPerYear = getMonthsPerYear(); 545 double width = 0; 546 for (int i = 0; i < monthsPerYear; i++) { 547 YearMonth yearMonth = displayedYearMonth.get().withMonth(i + 1); 548 String name = monthFormatterSO.withLocale(getLocale()).format(yearMonth); 549 if (Character.isDigit(name.charAt(0))) { 550 // Fallback. The standalone format returned a number, so use standard format instead. 551 name = monthFormatter.withLocale(getLocale()).format(yearMonth); 552 } 553 width = Math.max(width, Utils.computeTextWidth(monthLabel.getFont(), name, 0)); 554 } 555 monthLabel.setMinWidth(width); 556 } 557 } 558 559 protected void updateMonthYearPane() { 560 YearMonth yearMonth = displayedYearMonth.get(); 561 String str = formatMonth(yearMonth); 562 monthLabel.setText(str); 563 564 str = formatYear(yearMonth); 565 yearLabel.setText(str); 566 double width = Utils.computeTextWidth(yearLabel.getFont(), str, 0); 567 if (width > yearLabel.getMinWidth()) { 568 yearLabel.setMinWidth(width); 569 } 570 571 Chronology chrono = datePicker.getChronology(); 572 LocalDate firstDayOfMonth = yearMonth.atDay(1); 573 backMonthButton.setDisable(!isValidDate(chrono, firstDayOfMonth, -1, DAYS)); 574 forwardMonthButton.setDisable(!isValidDate(chrono, firstDayOfMonth, +1, MONTHS)); 575 backYearButton.setDisable(!isValidDate(chrono, firstDayOfMonth, -1, YEARS)); 576 forwardYearButton.setDisable(!isValidDate(chrono, firstDayOfMonth, +1, YEARS)); 577 } 578 579 private String formatMonth(YearMonth yearMonth) { 580 Locale locale = getLocale(); 581 Chronology chrono = getPrimaryChronology(); 582 try { 583 ChronoLocalDate cDate = chrono.date(yearMonth.atDay(1)); 584 585 String str = monthFormatterSO.withLocale(getLocale()) 586 .withChronology(chrono) 587 .format(cDate); 588 if (Character.isDigit(str.charAt(0))) { 589 // Fallback. The standalone format returned a number, so use standard format instead. 590 str = monthFormatter.withLocale(getLocale()) 591 .withChronology(chrono) 592 .format(cDate); 593 } 594 return titleCaseWord(str); 595 } catch (DateTimeException ex) { 596 // Date is out of range. 597 return ""; 598 } 599 } 600 601 private String formatYear(YearMonth yearMonth) { 602 Locale locale = getLocale(); 603 Chronology chrono = getPrimaryChronology(); 604 try { 605 DateTimeFormatter formatter = yearFormatter; 606 ChronoLocalDate cDate = chrono.date(yearMonth.atDay(1)); 607 int era = cDate.getEra().getValue(); 608 int nEras = chrono.eras().size(); 609 610 /*if (cDate.get(YEAR) < 0) { 611 formatter = yearForNegYearFormatter; 612 } else */ 613 if ((nEras == 2 && era == 0) || nEras > 2) { 614 formatter = yearWithEraFormatter; 615 } 616 617 // Fixme: Format Japanese era names with Japanese text. 618 String str = formatter.withLocale(getLocale()) 619 .withChronology(chrono) 620 .withDecimalStyle(DecimalStyle.of(getLocale())) 621 .format(cDate); 622 623 return str; 624 } catch (DateTimeException ex) { 625 // Date is out of range. 626 return ""; 627 } 628 } 629 630 // Ensures that month and day names are titlecased (capitalized). 631 private String titleCaseWord(String str) { 632 if (str.length() > 0) { 633 int firstChar = str.codePointAt(0); 634 if (!Character.isTitleCase(firstChar)) { 635 str = new String(new int[] { Character.toTitleCase(firstChar) }, 0, 1) + 636 str.substring(Character.offsetByCodePoints(str, 0, 1)); 637 } 638 } 639 return str; 640 } 641 642 643 644 /** 645 * determine on which day of week idx the first of the months is 646 */ 647 private int determineFirstOfMonthDayOfWeek() { 648 // determine with which cell to start 649 int firstDayOfWeek = WeekFields.of(getLocale()).getFirstDayOfWeek().getValue(); 650 int firstOfMonthIdx = displayedYearMonth.get().atDay(1).getDayOfWeek().getValue() - firstDayOfWeek; 651 if (firstOfMonthIdx < 0) { 652 firstOfMonthIdx += daysPerWeek; 653 } 654 return firstOfMonthIdx; 655 } 656 657 private boolean isToday(LocalDate localDate) { 658 return (localDate.equals(LocalDate.now())); 659 } 660 661 protected LocalDate dayCellDate(DateCell dateCell) { 662 assert (dayCellDates != null); 663 return dayCellDates[dayCells.indexOf(dateCell)]; 664 } 665 666 // public for behavior class 667 public void goToDayCell(DateCell dateCell, int offset, ChronoUnit unit, boolean focusDayCell) { 668 goToDate(dayCellDate(dateCell).plus(offset, unit), focusDayCell); 669 } 670 671 protected void forward(int offset, ChronoUnit unit, boolean focusDayCell) { 672 YearMonth yearMonth = displayedYearMonth.get(); 673 DateCell dateCell = lastFocusedDayCell; 674 if (dateCell == null || !dayCellDate(dateCell).getMonth().equals(yearMonth.getMonth())) { 675 dateCell = findDayCellForDate(yearMonth.atDay(1)); 676 } 677 goToDayCell(dateCell, offset, unit, focusDayCell); 678 } 679 680 // public for behavior class 681 public void goToDate(LocalDate date, boolean focusDayCell) { 682 if (isValidDate(datePicker.getChronology(), date)) { 683 displayedYearMonth.set(YearMonth.from(date)); 684 if (focusDayCell) { 685 findDayCellForDate(date).requestFocus(); 686 } 687 } 688 } 689 690 // public for behavior class 691 public void selectDayCell(DateCell dateCell) { 692 datePicker.setValue(dayCellDate(dateCell)); 693 datePicker.hide(); 694 } 695 696 private DateCell findDayCellForDate(LocalDate date) { 697 for (int i = 0; i < dayCellDates.length; i++) { 698 if (date.equals(dayCellDates[i])) { 699 return dayCells.get(i); 700 } 701 } 702 return dayCells.get(dayCells.size()/2+1); 703 } 704 705 public void clearFocus() { 706 LocalDate focusDate = datePicker.getValue(); 707 if (focusDate == null) { 708 focusDate = LocalDate.now(); 709 } 710 if (YearMonth.from(focusDate).equals(displayedYearMonth.get())) { 711 // focus date 712 goToDate(focusDate, true); 713 } else { 714 // focus month spinner (should not happen) 715 backMonthButton.requestFocus(); 716 } 717 718 // RT-31857 719 if (backMonthButton.getWidth() == 0) { 720 backMonthButton.requestLayout(); 721 forwardMonthButton.requestLayout(); 722 backYearButton.requestLayout(); 723 forwardYearButton.requestLayout(); 724 } 725 } 726 727 protected void createDayCells() { 728 final EventHandler<MouseEvent> dayCellActionHandler = ev -> { 729 if (ev.getButton() != MouseButton.PRIMARY) { 730 return; 731 } 732 733 DateCell dayCell = (DateCell)ev.getSource(); 734 selectDayCell(dayCell); 735 lastFocusedDayCell = dayCell; 736 }; 737 738 for (int row = 0; row < 6; row++) { 739 for (int col = 0; col < daysPerWeek; col++) { 740 DateCell dayCell = createDayCell(); 741 dayCell.addEventHandler(MouseEvent.MOUSE_CLICKED, dayCellActionHandler); 742 dayCells.add(dayCell); 743 } 744 } 745 746 dayCellDates = new LocalDate[6 * daysPerWeek]; 747 } 748 749 private DateCell createDayCell() { 750 DateCell cell = null; 751 if (datePicker.getDayCellFactory() != null) { 752 cell = datePicker.getDayCellFactory().call(datePicker); 753 } 754 if (cell == null) { 755 cell = new DateCell(); 756 } 757 758 return cell; 759 } 760 761 protected Locale getLocale() { 762 return Locale.getDefault(Locale.Category.FORMAT); 763 } 764 765 /** 766 * The primary chronology for display. This may be overridden to 767 * be different than the DatePicker chronology. For example 768 * DatePickerHijrahContent uses ISO as primary and Hijrah as a 769 * secondary chronology. 770 */ 771 protected Chronology getPrimaryChronology() { 772 return datePicker.getChronology(); 773 } 774 775 protected boolean isValidDate(Chronology chrono, LocalDate date, int offset, ChronoUnit unit) { 776 if (date != null) { 777 try { 778 return isValidDate(chrono, date.plus(offset, unit)); 779 } catch (DateTimeException ex) { 780 } 781 } 782 return false; 783 } 784 785 protected boolean isValidDate(Chronology chrono, LocalDate date) { 786 try { 787 if (date != null) { 788 chrono.date(date); 789 } 790 return true; 791 } catch (DateTimeException ex) { 792 return false; 793 } 794 } 795 }