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 = snapSpaceX(getHgap()); 147 final double left = snapSpaceX(getInsets().getLeft()); 148 final double right = snapSpaceX(getInsets().getRight()); 149 final double hgaps = snaphgap * (nCols - 1); 150 final double contentWidth = width - left - right - hgaps; 151 return ((snapSizeX(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 monthLabel.fontProperty().addListener((o, ov, nv) -> { 333 updateMonthLabelWidth(); 334 }); 335 336 forwardMonthButton.setOnAction(t -> { 337 forward(1, MONTHS, false); 338 }); 339 340 monthSpinner.getChildren().addAll(backMonthButton, monthLabel, forwardMonthButton); 341 monthYearPane.setLeft(monthSpinner); 342 343 // Year spinner 344 345 HBox yearSpinner = new HBox(); 346 yearSpinner.getStyleClass().add("spinner"); 347 348 backYearButton = new Button(); 349 backYearButton.getStyleClass().add("left-button"); 350 351 forwardYearButton = new Button(); 352 forwardYearButton.getStyleClass().add("right-button"); 353 354 StackPane leftYearArrow = new StackPane(); 355 leftYearArrow.getStyleClass().add("left-arrow"); 356 leftYearArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE); 357 backYearButton.setGraphic(leftYearArrow); 358 359 StackPane rightYearArrow = new StackPane(); 360 rightYearArrow.getStyleClass().add("right-arrow"); 361 rightYearArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE); 362 forwardYearButton.setGraphic(rightYearArrow); 363 364 365 backYearButton.setOnAction(t -> { 366 forward(-1, YEARS, false); 367 }); 368 369 yearLabel = new Label(); 370 yearLabel.getStyleClass().add("spinner-label"); 371 372 forwardYearButton.setOnAction(t -> { 373 forward(1, YEARS, false); 374 }); 375 376 yearSpinner.getChildren().addAll(backYearButton, yearLabel, forwardYearButton); 377 yearSpinner.setFillHeight(false); 378 monthYearPane.setRight(yearSpinner); 379 380 return monthYearPane; 381 } 382 383 private void refresh() { 384 updateMonthLabelWidth(); 385 updateDayNameCells(); 386 updateValues(); 387 } 388 389 public void updateValues() { 390 // Note: Preserve this order, as DatePickerHijrahContent needs 391 // updateDayCells before updateMonthYearPane(). 392 updateWeeknumberDateCells(); 393 updateDayCells(); 394 updateMonthYearPane(); 395 } 396 397 public void updateGrid() { 398 gridPane.getColumnConstraints().clear(); 399 gridPane.getChildren().clear(); 400 401 int nCols = daysPerWeek + (datePicker.isShowWeekNumbers() ? 1 : 0); 402 403 ColumnConstraints columnConstraints = new ColumnConstraints(); 404 columnConstraints.setPercentWidth(100); // Treated as weight 405 for (int i = 0; i < nCols; i++) { 406 gridPane.getColumnConstraints().add(columnConstraints); 407 } 408 409 for (int i = 0; i < daysPerWeek; i++) { 410 gridPane.add(dayNameCells.get(i), i + nCols - daysPerWeek, 1); // col, row 411 } 412 413 // Week number column 414 if (datePicker.isShowWeekNumbers()) { 415 for (int i = 0; i < 6; i++) { 416 gridPane.add(weekNumberCells.get(i), 0, i + 2); // col, row 417 } 418 } 419 420 // setup: 6 rows of daysPerWeek (which is the maximum number of cells required in the worst case layout) 421 for (int row = 0; row < 6; row++) { 422 for (int col = 0; col < daysPerWeek; col++) { 423 gridPane.add(dayCells.get(row*daysPerWeek+col), col + nCols - daysPerWeek, row + 2); 424 } 425 } 426 } 427 428 public void updateDayNameCells() { 429 // first day of week, 1 = monday, 7 = sunday 430 int firstDayOfWeek = WeekFields.of(getLocale()).getFirstDayOfWeek().getValue(); 431 432 // july 13th 2009 is a Monday, so a firstDayOfWeek=1 must come out of the 13th 433 LocalDate date = LocalDate.of(2009, 7, 12 + firstDayOfWeek); 434 for (int i = 0; i < daysPerWeek; i++) { 435 String name = weekDayNameFormatter.withLocale(getLocale()).format(date.plus(i, DAYS)); 436 dayNameCells.get(i).setText(titleCaseWord(name)); 437 } 438 } 439 440 public void updateWeeknumberDateCells() { 441 if (datePicker.isShowWeekNumbers()) { 442 final Locale locale = getLocale(); 443 final int maxWeeksPerMonth = 6; // TODO: Get this from chronology? 444 445 LocalDate firstOfMonth = displayedYearMonth.get().atDay(1); 446 for (int i = 0; i < maxWeeksPerMonth; i++) { 447 LocalDate date = firstOfMonth.plus(i, WEEKS); 448 // Use a formatter to ensure correct localization, 449 // such as when Thai numerals are required. 450 String cellText = 451 weekNumberFormatter.withLocale(locale) 452 .withDecimalStyle(DecimalStyle.of(locale)) 453 .format(date); 454 weekNumberCells.get(i).setText(cellText); 455 } 456 } 457 } 458 459 public void updateDayCells() { 460 Locale locale = getLocale(); 461 Chronology chrono = getPrimaryChronology(); 462 int firstOfMonthIdx = determineFirstOfMonthDayOfWeek(); 463 YearMonth curMonth = displayedYearMonth.get(); 464 465 // RT-31075: The following are now set in the try-catch block. 466 YearMonth prevMonth = null; 467 YearMonth nextMonth = null; 468 int daysInCurMonth = -1; 469 int daysInPrevMonth = -1; 470 int daysInNextMonth = -1; 471 472 for (int i = 0; i < 6 * daysPerWeek; i++) { 473 DateCell dayCell = dayCells.get(i); 474 dayCell.getStyleClass().setAll("cell", "date-cell", "day-cell"); 475 dayCell.setDisable(false); 476 dayCell.setStyle(null); 477 dayCell.setGraphic(null); 478 dayCell.setTooltip(null); 479 480 try { 481 if (daysInCurMonth == -1) { 482 daysInCurMonth = curMonth.lengthOfMonth(); 483 } 484 YearMonth month = curMonth; 485 int day = i - firstOfMonthIdx + 1; 486 //int index = firstOfMonthIdx + i - 1; 487 if (i < firstOfMonthIdx) { 488 if (prevMonth == null) { 489 prevMonth = curMonth.minusMonths(1); 490 daysInPrevMonth = prevMonth.lengthOfMonth(); 491 } 492 month = prevMonth; 493 day = i + daysInPrevMonth - firstOfMonthIdx + 1; 494 dayCell.getStyleClass().add("previous-month"); 495 } else if (i >= firstOfMonthIdx + daysInCurMonth) { 496 if (nextMonth == null) { 497 nextMonth = curMonth.plusMonths(1); 498 daysInNextMonth = nextMonth.lengthOfMonth(); 499 } 500 month = nextMonth; 501 day = i - daysInCurMonth - firstOfMonthIdx + 1; 502 dayCell.getStyleClass().add("next-month"); 503 } 504 LocalDate date = month.atDay(day); 505 dayCellDates[i] = date; 506 ChronoLocalDate cDate = chrono.date(date); 507 508 dayCell.setDisable(false); 509 510 if (isToday(date)) { 511 dayCell.getStyleClass().add("today"); 512 } 513 514 if (date.equals(datePicker.getValue())) { 515 dayCell.getStyleClass().add("selected"); 516 } 517 518 String cellText = 519 dayCellFormatter.withLocale(locale) 520 .withChronology(chrono) 521 .withDecimalStyle(DecimalStyle.of(locale)) 522 .format(cDate); 523 dayCell.setText(cellText); 524 525 dayCell.updateItem(date, false); 526 } catch (DateTimeException ex) { 527 // Date is out of range. 528 // System.err.println(dayCellDate(dayCell) + " " + ex); 529 dayCell.setText(" "); 530 dayCell.setDisable(true); 531 } 532 } 533 } 534 535 private int getDaysPerWeek() { 536 ValueRange range = getPrimaryChronology().range(DAY_OF_WEEK); 537 return (int)(range.getMaximum() - range.getMinimum() + 1); 538 } 539 540 private int getMonthsPerYear() { 541 ValueRange range = getPrimaryChronology().range(MONTH_OF_YEAR); 542 return (int)(range.getMaximum() - range.getMinimum() + 1); 543 } 544 545 private void updateMonthLabelWidth() { 546 if (monthLabel != null) { 547 int monthsPerYear = getMonthsPerYear(); 548 double width = 0; 549 for (int i = 0; i < monthsPerYear; i++) { 550 YearMonth yearMonth = displayedYearMonth.get().withMonth(i + 1); 551 String name = monthFormatterSO.withLocale(getLocale()).format(yearMonth); 552 if (Character.isDigit(name.charAt(0))) { 553 // Fallback. The standalone format returned a number, so use standard format instead. 554 name = monthFormatter.withLocale(getLocale()).format(yearMonth); 555 } 556 width = Math.max(width, Utils.computeTextWidth(monthLabel.getFont(), name, 0)); 557 } 558 monthLabel.setMinWidth(width); 559 } 560 } 561 562 protected void updateMonthYearPane() { 563 YearMonth yearMonth = displayedYearMonth.get(); 564 String str = formatMonth(yearMonth); 565 monthLabel.setText(str); 566 567 str = formatYear(yearMonth); 568 yearLabel.setText(str); 569 double width = Utils.computeTextWidth(yearLabel.getFont(), str, 0); 570 if (width > yearLabel.getMinWidth()) { 571 yearLabel.setMinWidth(width); 572 } 573 574 Chronology chrono = datePicker.getChronology(); 575 LocalDate firstDayOfMonth = yearMonth.atDay(1); 576 backMonthButton.setDisable(!isValidDate(chrono, firstDayOfMonth, -1, DAYS)); 577 forwardMonthButton.setDisable(!isValidDate(chrono, firstDayOfMonth, +1, MONTHS)); 578 backYearButton.setDisable(!isValidDate(chrono, firstDayOfMonth, -1, YEARS)); 579 forwardYearButton.setDisable(!isValidDate(chrono, firstDayOfMonth, +1, YEARS)); 580 } 581 582 private String formatMonth(YearMonth yearMonth) { 583 Chronology chrono = getPrimaryChronology(); 584 try { 585 ChronoLocalDate cDate = chrono.date(yearMonth.atDay(1)); 586 587 String str = monthFormatterSO.withLocale(getLocale()) 588 .withChronology(chrono) 589 .format(cDate); 590 if (Character.isDigit(str.charAt(0))) { 591 // Fallback. The standalone format returned a number, so use standard format instead. 592 str = monthFormatter.withLocale(getLocale()) 593 .withChronology(chrono) 594 .format(cDate); 595 } 596 return titleCaseWord(str); 597 } catch (DateTimeException ex) { 598 // Date is out of range. 599 return ""; 600 } 601 } 602 603 private String formatYear(YearMonth yearMonth) { 604 Chronology chrono = getPrimaryChronology(); 605 try { 606 DateTimeFormatter formatter = yearFormatter; 607 ChronoLocalDate cDate = chrono.date(yearMonth.atDay(1)); 608 int era = cDate.getEra().getValue(); 609 int nEras = chrono.eras().size(); 610 611 /*if (cDate.get(YEAR) < 0) { 612 formatter = yearForNegYearFormatter; 613 } else */ 614 if ((nEras == 2 && era == 0) || nEras > 2) { 615 formatter = yearWithEraFormatter; 616 } 617 618 // Fixme: Format Japanese era names with Japanese text. 619 String str = formatter.withLocale(getLocale()) 620 .withChronology(chrono) 621 .withDecimalStyle(DecimalStyle.of(getLocale())) 622 .format(cDate); 623 624 return str; 625 } catch (DateTimeException ex) { 626 // Date is out of range. 627 return ""; 628 } 629 } 630 631 // Ensures that month and day names are titlecased (capitalized). 632 private String titleCaseWord(String str) { 633 if (str.length() > 0) { 634 int firstChar = str.codePointAt(0); 635 if (!Character.isTitleCase(firstChar)) { 636 str = new String(new int[] { Character.toTitleCase(firstChar) }, 0, 1) + 637 str.substring(Character.offsetByCodePoints(str, 0, 1)); 638 } 639 } 640 return str; 641 } 642 643 644 645 /** 646 * determine on which day of week idx the first of the months is 647 */ 648 private int determineFirstOfMonthDayOfWeek() { 649 // determine with which cell to start 650 int firstDayOfWeek = WeekFields.of(getLocale()).getFirstDayOfWeek().getValue(); 651 int firstOfMonthIdx = displayedYearMonth.get().atDay(1).getDayOfWeek().getValue() - firstDayOfWeek; 652 if (firstOfMonthIdx < 0) { 653 firstOfMonthIdx += daysPerWeek; 654 } 655 return firstOfMonthIdx; 656 } 657 658 private boolean isToday(LocalDate localDate) { 659 return (localDate.equals(LocalDate.now())); 660 } 661 662 protected LocalDate dayCellDate(DateCell dateCell) { 663 assert (dayCellDates != null); 664 return dayCellDates[dayCells.indexOf(dateCell)]; 665 } 666 667 // public for behavior class 668 public void goToDayCell(DateCell dateCell, int offset, ChronoUnit unit, boolean focusDayCell) { 669 goToDate(dayCellDate(dateCell).plus(offset, unit), focusDayCell); 670 } 671 672 protected void forward(int offset, ChronoUnit unit, boolean focusDayCell) { 673 YearMonth yearMonth = displayedYearMonth.get(); 674 DateCell dateCell = lastFocusedDayCell; 675 if (dateCell == null || !dayCellDate(dateCell).getMonth().equals(yearMonth.getMonth())) { 676 dateCell = findDayCellForDate(yearMonth.atDay(1)); 677 } 678 goToDayCell(dateCell, offset, unit, focusDayCell); 679 } 680 681 // public for behavior class 682 public void goToDate(LocalDate date, boolean focusDayCell) { 683 if (isValidDate(datePicker.getChronology(), date)) { 684 displayedYearMonth.set(YearMonth.from(date)); 685 if (focusDayCell) { 686 findDayCellForDate(date).requestFocus(); 687 } 688 } 689 } 690 691 // public for behavior class 692 public void selectDayCell(DateCell dateCell) { 693 datePicker.setValue(dayCellDate(dateCell)); 694 datePicker.hide(); 695 } 696 697 private DateCell findDayCellForDate(LocalDate date) { 698 for (int i = 0; i < dayCellDates.length; i++) { 699 if (date.equals(dayCellDates[i])) { 700 return dayCells.get(i); 701 } 702 } 703 return dayCells.get(dayCells.size()/2+1); 704 } 705 706 public void clearFocus() { 707 LocalDate focusDate = datePicker.getValue(); 708 if (focusDate == null) { 709 focusDate = LocalDate.now(); 710 } 711 if (YearMonth.from(focusDate).equals(displayedYearMonth.get())) { 712 // focus date 713 goToDate(focusDate, true); 714 } else { 715 // focus month spinner (should not happen) 716 backMonthButton.requestFocus(); 717 } 718 719 // RT-31857 720 if (backMonthButton.getWidth() == 0) { 721 backMonthButton.requestLayout(); 722 forwardMonthButton.requestLayout(); 723 backYearButton.requestLayout(); 724 forwardYearButton.requestLayout(); 725 } 726 } 727 728 protected void createDayCells() { 729 final EventHandler<MouseEvent> dayCellActionHandler = ev -> { 730 if (ev.getButton() != MouseButton.PRIMARY) { 731 return; 732 } 733 734 DateCell dayCell = (DateCell)ev.getSource(); 735 selectDayCell(dayCell); 736 lastFocusedDayCell = dayCell; 737 }; 738 739 for (int row = 0; row < 6; row++) { 740 for (int col = 0; col < daysPerWeek; col++) { 741 DateCell dayCell = createDayCell(); 742 dayCell.addEventHandler(MouseEvent.MOUSE_CLICKED, dayCellActionHandler); 743 dayCells.add(dayCell); 744 } 745 } 746 747 dayCellDates = new LocalDate[6 * daysPerWeek]; 748 } 749 750 private DateCell createDayCell() { 751 DateCell cell = null; 752 if (datePicker.getDayCellFactory() != null) { 753 cell = datePicker.getDayCellFactory().call(datePicker); 754 } 755 if (cell == null) { 756 cell = new DateCell(); 757 } 758 759 return cell; 760 } 761 762 protected Locale getLocale() { 763 return Locale.getDefault(Locale.Category.FORMAT); 764 } 765 766 /** 767 * The primary chronology for display. This may be overridden to 768 * be different than the DatePicker chronology. For example 769 * DatePickerHijrahContent uses ISO as primary and Hijrah as a 770 * secondary chronology. 771 */ 772 protected Chronology getPrimaryChronology() { 773 return datePicker.getChronology(); 774 } 775 776 protected boolean isValidDate(Chronology chrono, LocalDate date, int offset, ChronoUnit unit) { 777 if (date != null) { 778 try { 779 return isValidDate(chrono, date.plus(offset, unit)); 780 } catch (DateTimeException ex) { 781 } 782 } 783 return false; 784 } 785 786 protected boolean isValidDate(Chronology chrono, LocalDate date) { 787 try { 788 if (date != null) { 789 chrono.date(date); 790 } 791 return true; 792 } catch (DateTimeException ex) { 793 return false; 794 } 795 } 796 }