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 }