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