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