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