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