package com.sun.javafx.scene.control;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DecimalStyle;
import java.time.chrono.Chronology;
import java.time.chrono.ChronoLocalDate;
import java.time.temporal.ChronoUnit;
import java.time.temporal.ValueRange;
import java.time.temporal.WeekFields;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import static java.time.temporal.ChronoField.*;
import static java.time.temporal.ChronoUnit.*;
import com.sun.javafx.scene.control.skin.*;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.WeakChangeListener;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.DatePicker;
import javafx.scene.control.DateCell;
import javafx.scene.control.Label;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.layout.StackPane;
import com.sun.javafx.scene.control.skin.resources.ControlResources;
import com.sun.javafx.scene.traversal.Direction;
import static com.sun.javafx.PlatformUtil.*;
import com.sun.javafx.scene.NodeHelper;
public class DatePickerContent extends VBox {
protected DatePicker datePicker;
private Button backMonthButton;
private Button forwardMonthButton;
private Button backYearButton;
private Button forwardYearButton;
private Label monthLabel;
private Label yearLabel;
protected GridPane gridPane;
private int daysPerWeek;
private List<DateCell> dayNameCells = new ArrayList<DateCell>();
private List<DateCell> weekNumberCells = new ArrayList<DateCell>();
protected List<DateCell> dayCells = new ArrayList<DateCell>();
private LocalDate[] dayCellDates;
private DateCell lastFocusedDayCell = null;
final DateTimeFormatter monthFormatter =
DateTimeFormatter.ofPattern("MMMM");
final DateTimeFormatter monthFormatterSO =
DateTimeFormatter.ofPattern("LLLL");
final DateTimeFormatter yearFormatter =
DateTimeFormatter.ofPattern("y");
final DateTimeFormatter yearWithEraFormatter =
DateTimeFormatter.ofPattern("GGGGy");
final DateTimeFormatter weekNumberFormatter =
DateTimeFormatter.ofPattern("w");
final DateTimeFormatter weekDayNameFormatter =
DateTimeFormatter.ofPattern("ccc");
final DateTimeFormatter dayCellFormatter =
DateTimeFormatter.ofPattern("d");
static String getString(String key) {
return ControlResources.getString("DatePicker."+key);
}
public DatePickerContent(final DatePicker datePicker) {
this.datePicker = datePicker;
getStyleClass().add("date-picker-popup");
daysPerWeek = getDaysPerWeek();
{
LocalDate date = datePicker.getValue();
displayedYearMonth.set((date != null) ? YearMonth.from(date) : YearMonth.now());
}
displayedYearMonth.addListener((observable, oldValue, newValue) -> {
updateValues();
});
getChildren().add(createMonthYearPane());
gridPane = new GridPane() {
@Override protected double computePrefWidth(double height) {
final double width = super.computePrefWidth(height);
final int nCols = daysPerWeek + (datePicker.isShowWeekNumbers() ? 1 : 0);
final double snaphgap = snapSpaceX(getHgap());
final double left = snapSpaceX(getInsets().getLeft());
final double right = snapSpaceX(getInsets().getRight());
final double hgaps = snaphgap * (nCols - 1);
final double contentWidth = width - left - right - hgaps;
return ((snapSizeX(contentWidth / nCols)) * nCols) + left + right + hgaps;
}
@Override protected void layoutChildren() {
if (getWidth() > 0 && getHeight() > 0) {
super.layoutChildren();
}
}
};
gridPane.setFocusTraversable(true);
gridPane.getStyleClass().add("calendar-grid");
gridPane.setVgap(-1);
gridPane.setHgap(-1);
final WeakChangeListener<Node> weakFocusOwnerListener =
new WeakChangeListener<Node>((ov2, oldFocusOwner, newFocusOwner) -> {
if (newFocusOwner == gridPane) {
if (oldFocusOwner instanceof DateCell) {
NodeHelper.traverse(gridPane, Direction.PREVIOUS);
} else {
if (lastFocusedDayCell != null) {
Platform.runLater(() -> {
lastFocusedDayCell.requestFocus();
});
} else {
clearFocus();
}
}
}
});
gridPane.sceneProperty().addListener(new WeakChangeListener<Scene>((ov, oldScene, newScene) -> {
if (oldScene != null) {
oldScene.focusOwnerProperty().removeListener(weakFocusOwnerListener);
}
if (newScene != null) {
Platform.runLater(() -> {
newScene.focusOwnerProperty().addListener(weakFocusOwnerListener);
});
}
}));
if (gridPane.getScene() != null) {
gridPane.getScene().focusOwnerProperty().addListener(weakFocusOwnerListener);
}
for (int i = 0; i < daysPerWeek; i++) {
DateCell cell = new DateCell();
cell.getStyleClass().add("day-name-cell");
dayNameCells.add(cell);
}
for (int i = 0; i < 6; i++) {
DateCell cell = new DateCell();
cell.getStyleClass().add("week-number-cell");
weekNumberCells.add(cell);
}
createDayCells();
updateGrid();
getChildren().add(gridPane);
refresh();
addEventHandler(KeyEvent.ANY, e -> {
Node node = getScene().getFocusOwner();
if (node instanceof DateCell) {
lastFocusedDayCell = (DateCell)node;
}
if (e.getEventType() == KeyEvent.KEY_PRESSED) {
switch (e.getCode()) {
case HOME:
goToDate(LocalDate.now(), true);
e.consume();
break;
case PAGE_UP:
if ((isMac() && e.isMetaDown()) || (!isMac() && e.isControlDown())) {
if (!backYearButton.isDisabled()) {
forward(-1, YEARS, true);
}
} else {
if (!backMonthButton.isDisabled()) {
forward(-1, MONTHS, true);
}
}
e.consume();
break;
case PAGE_DOWN:
if ((isMac() && e.isMetaDown()) || (!isMac() && e.isControlDown())) {
if (!forwardYearButton.isDisabled()) {
forward(1, YEARS, true);
}
} else {
if (!forwardMonthButton.isDisabled()) {
forward(1, MONTHS, true);
}
}
e.consume();
break;
}
node = getScene().getFocusOwner();
if (node instanceof DateCell) {
lastFocusedDayCell = (DateCell)node;
}
}
switch (e.getCode()) {
case F4:
case F10:
case UP:
case DOWN:
case LEFT:
case RIGHT:
case TAB:
break;
case ESCAPE:
datePicker.hide();
e.consume();
break;
default:
e.consume();
}
});
}
private ObjectProperty<YearMonth> displayedYearMonth =
new SimpleObjectProperty<YearMonth>(this, "displayedYearMonth");
public ObjectProperty<YearMonth> displayedYearMonthProperty() {
return displayedYearMonth;
}
protected BorderPane createMonthYearPane() {
BorderPane monthYearPane = new BorderPane();
monthYearPane.getStyleClass().add("month-year-pane");
HBox monthSpinner = new HBox();
monthSpinner.getStyleClass().add("spinner");
backMonthButton = new Button();
backMonthButton.getStyleClass().add("left-button");
forwardMonthButton = new Button();
forwardMonthButton.getStyleClass().add("right-button");
StackPane leftMonthArrow = new StackPane();
leftMonthArrow.getStyleClass().add("left-arrow");
leftMonthArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
backMonthButton.setGraphic(leftMonthArrow);
StackPane rightMonthArrow = new StackPane();
rightMonthArrow.getStyleClass().add("right-arrow");
rightMonthArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
forwardMonthButton.setGraphic(rightMonthArrow);
backMonthButton.setOnAction(t -> {
forward(-1, MONTHS, false);
});
monthLabel = new Label();
monthLabel.getStyleClass().add("spinner-label");
monthLabel.fontProperty().addListener((o, ov, nv) -> {
updateMonthLabelWidth();
});
forwardMonthButton.setOnAction(t -> {
forward(1, MONTHS, false);
});
monthSpinner.getChildren().addAll(backMonthButton, monthLabel, forwardMonthButton);
monthYearPane.setLeft(monthSpinner);
HBox yearSpinner = new HBox();
yearSpinner.getStyleClass().add("spinner");
backYearButton = new Button();
backYearButton.getStyleClass().add("left-button");
forwardYearButton = new Button();
forwardYearButton.getStyleClass().add("right-button");
StackPane leftYearArrow = new StackPane();
leftYearArrow.getStyleClass().add("left-arrow");
leftYearArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
backYearButton.setGraphic(leftYearArrow);
StackPane rightYearArrow = new StackPane();
rightYearArrow.getStyleClass().add("right-arrow");
rightYearArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
forwardYearButton.setGraphic(rightYearArrow);
backYearButton.setOnAction(t -> {
forward(-1, YEARS, false);
});
yearLabel = new Label();
yearLabel.getStyleClass().add("spinner-label");
forwardYearButton.setOnAction(t -> {
forward(1, YEARS, false);
});
yearSpinner.getChildren().addAll(backYearButton, yearLabel, forwardYearButton);
yearSpinner.setFillHeight(false);
monthYearPane.setRight(yearSpinner);
return monthYearPane;
}
private void refresh() {
updateMonthLabelWidth();
updateDayNameCells();
updateValues();
}
public void updateValues() {
updateWeeknumberDateCells();
updateDayCells();
updateMonthYearPane();
}
public void updateGrid() {
gridPane.getColumnConstraints().clear();
gridPane.getChildren().clear();
int nCols = daysPerWeek + (datePicker.isShowWeekNumbers() ? 1 : 0);
ColumnConstraints columnConstraints = new ColumnConstraints();
columnConstraints.setPercentWidth(100);
for (int i = 0; i < nCols; i++) {
gridPane.getColumnConstraints().add(columnConstraints);
}
for (int i = 0; i < daysPerWeek; i++) {
gridPane.add(dayNameCells.get(i), i + nCols - daysPerWeek, 1);
}
if (datePicker.isShowWeekNumbers()) {
for (int i = 0; i < 6; i++) {
gridPane.add(weekNumberCells.get(i), 0, i + 2);
}
}
for (int row = 0; row < 6; row++) {
for (int col = 0; col < daysPerWeek; col++) {
gridPane.add(dayCells.get(row*daysPerWeek+col), col + nCols - daysPerWeek, row + 2);
}
}
}
public void updateDayNameCells() {
int firstDayOfWeek = WeekFields.of(getLocale()).getFirstDayOfWeek().getValue();
LocalDate date = LocalDate.of(2009, 7, 12 + firstDayOfWeek);
for (int i = 0; i < daysPerWeek; i++) {
String name = weekDayNameFormatter.withLocale(getLocale()).format(date.plus(i, DAYS));
dayNameCells.get(i).setText(titleCaseWord(name));
}
}
public void updateWeeknumberDateCells() {
if (datePicker.isShowWeekNumbers()) {
final Locale locale = getLocale();
final int maxWeeksPerMonth = 6;
LocalDate firstOfMonth = displayedYearMonth.get().atDay(1);
for (int i = 0; i < maxWeeksPerMonth; i++) {
LocalDate date = firstOfMonth.plus(i, WEEKS);
String cellText =
weekNumberFormatter.withLocale(locale)
.withDecimalStyle(DecimalStyle.of(locale))
.format(date);
weekNumberCells.get(i).setText(cellText);
}
}
}
public void updateDayCells() {
Locale locale = getLocale();
Chronology chrono = getPrimaryChronology();
int firstOfMonthIdx = determineFirstOfMonthDayOfWeek();
YearMonth curMonth = displayedYearMonth.get();
YearMonth prevMonth = null;
YearMonth nextMonth = null;
int daysInCurMonth = -1;
int daysInPrevMonth = -1;
int daysInNextMonth = -1;
for (int i = 0; i < 6 * daysPerWeek; i++) {
DateCell dayCell = dayCells.get(i);
dayCell.getStyleClass().setAll("cell", "date-cell", "day-cell");
dayCell.setDisable(false);
dayCell.setStyle(null);
dayCell.setGraphic(null);
dayCell.setTooltip(null);
try {
if (daysInCurMonth == -1) {
daysInCurMonth = curMonth.lengthOfMonth();
}
YearMonth month = curMonth;
int day = i - firstOfMonthIdx + 1;
if (i < firstOfMonthIdx) {
if (prevMonth == null) {
prevMonth = curMonth.minusMonths(1);
daysInPrevMonth = prevMonth.lengthOfMonth();
}
month = prevMonth;
day = i + daysInPrevMonth - firstOfMonthIdx + 1;
dayCell.getStyleClass().add("previous-month");
} else if (i >= firstOfMonthIdx + daysInCurMonth) {
if (nextMonth == null) {
nextMonth = curMonth.plusMonths(1);
daysInNextMonth = nextMonth.lengthOfMonth();
}
month = nextMonth;
day = i - daysInCurMonth - firstOfMonthIdx + 1;
dayCell.getStyleClass().add("next-month");
}
LocalDate date = month.atDay(day);
dayCellDates[i] = date;
ChronoLocalDate cDate = chrono.date(date);
dayCell.setDisable(false);
if (isToday(date)) {
dayCell.getStyleClass().add("today");
}
if (date.equals(datePicker.getValue())) {
dayCell.getStyleClass().add("selected");
}
String cellText =
dayCellFormatter.withLocale(locale)
.withChronology(chrono)
.withDecimalStyle(DecimalStyle.of(locale))
.format(cDate);
dayCell.setText(cellText);
dayCell.updateItem(date, false);
} catch (DateTimeException ex) {
dayCell.setText(" ");
dayCell.setDisable(true);
}
}
}
private int getDaysPerWeek() {
ValueRange range = getPrimaryChronology().range(DAY_OF_WEEK);
return (int)(range.getMaximum() - range.getMinimum() + 1);
}
private int getMonthsPerYear() {
ValueRange range = getPrimaryChronology().range(MONTH_OF_YEAR);
return (int)(range.getMaximum() - range.getMinimum() + 1);
}
private void updateMonthLabelWidth() {
if (monthLabel != null) {
int monthsPerYear = getMonthsPerYear();
double width = 0;
for (int i = 0; i < monthsPerYear; i++) {
YearMonth yearMonth = displayedYearMonth.get().withMonth(i + 1);
String name = monthFormatterSO.withLocale(getLocale()).format(yearMonth);
if (Character.isDigit(name.charAt(0))) {
name = monthFormatter.withLocale(getLocale()).format(yearMonth);
}
width = Math.max(width, Utils.computeTextWidth(monthLabel.getFont(), name, 0));
}
monthLabel.setMinWidth(width);
}
}
protected void updateMonthYearPane() {
YearMonth yearMonth = displayedYearMonth.get();
String str = formatMonth(yearMonth);
monthLabel.setText(str);
str = formatYear(yearMonth);
yearLabel.setText(str);
double width = Utils.computeTextWidth(yearLabel.getFont(), str, 0);
if (width > yearLabel.getMinWidth()) {
yearLabel.setMinWidth(width);
}
Chronology chrono = datePicker.getChronology();
LocalDate firstDayOfMonth = yearMonth.atDay(1);
backMonthButton.setDisable(!isValidDate(chrono, firstDayOfMonth, -1, DAYS));
forwardMonthButton.setDisable(!isValidDate(chrono, firstDayOfMonth, +1, MONTHS));
backYearButton.setDisable(!isValidDate(chrono, firstDayOfMonth, -1, YEARS));
forwardYearButton.setDisable(!isValidDate(chrono, firstDayOfMonth, +1, YEARS));
}
private String formatMonth(YearMonth yearMonth) {
Chronology chrono = getPrimaryChronology();
try {
ChronoLocalDate cDate = chrono.date(yearMonth.atDay(1));
String str = monthFormatterSO.withLocale(getLocale())
.withChronology(chrono)
.format(cDate);
if (Character.isDigit(str.charAt(0))) {
str = monthFormatter.withLocale(getLocale())
.withChronology(chrono)
.format(cDate);
}
return titleCaseWord(str);
} catch (DateTimeException ex) {
return "";
}
}
private String formatYear(YearMonth yearMonth) {
Chronology chrono = getPrimaryChronology();
try {
DateTimeFormatter formatter = yearFormatter;
ChronoLocalDate cDate = chrono.date(yearMonth.atDay(1));
int era = cDate.getEra().getValue();
int nEras = chrono.eras().size();
if ((nEras == 2 && era == 0) || nEras > 2) {
formatter = yearWithEraFormatter;
}
String str = formatter.withLocale(getLocale())
.withChronology(chrono)
.withDecimalStyle(DecimalStyle.of(getLocale()))
.format(cDate);
return str;
} catch (DateTimeException ex) {
return "";
}
}
private String titleCaseWord(String str) {
if (str.length() > 0) {
int firstChar = str.codePointAt(0);
if (!Character.isTitleCase(firstChar)) {
str = new String(new int[] { Character.toTitleCase(firstChar) }, 0, 1) +
str.substring(Character.offsetByCodePoints(str, 0, 1));
}
}
return str;
}
private int determineFirstOfMonthDayOfWeek() {
int firstDayOfWeek = WeekFields.of(getLocale()).getFirstDayOfWeek().getValue();
int firstOfMonthIdx = displayedYearMonth.get().atDay(1).getDayOfWeek().getValue() - firstDayOfWeek;
if (firstOfMonthIdx < 0) {
firstOfMonthIdx += daysPerWeek;
}
return firstOfMonthIdx;
}
private boolean isToday(LocalDate localDate) {
return (localDate.equals(LocalDate.now()));
}
protected LocalDate dayCellDate(DateCell dateCell) {
assert (dayCellDates != null);
return dayCellDates[dayCells.indexOf(dateCell)];
}
public void goToDayCell(DateCell dateCell, int offset, ChronoUnit unit, boolean focusDayCell) {
goToDate(dayCellDate(dateCell).plus(offset, unit), focusDayCell);
}
protected void forward(int offset, ChronoUnit unit, boolean focusDayCell) {
YearMonth yearMonth = displayedYearMonth.get();
DateCell dateCell = lastFocusedDayCell;
if (dateCell == null || !dayCellDate(dateCell).getMonth().equals(yearMonth.getMonth())) {
dateCell = findDayCellForDate(yearMonth.atDay(1));
}
goToDayCell(dateCell, offset, unit, focusDayCell);
}
public void goToDate(LocalDate date, boolean focusDayCell) {
if (isValidDate(datePicker.getChronology(), date)) {
displayedYearMonth.set(YearMonth.from(date));
if (focusDayCell) {
findDayCellForDate(date).requestFocus();
}
}
}
public void selectDayCell(DateCell dateCell) {
datePicker.setValue(dayCellDate(dateCell));
datePicker.hide();
}
private DateCell findDayCellForDate(LocalDate date) {
for (int i = 0; i < dayCellDates.length; i++) {
if (date.equals(dayCellDates[i])) {
return dayCells.get(i);
}
}
return dayCells.get(dayCells.size()/2+1);
}
public void clearFocus() {
LocalDate focusDate = datePicker.getValue();
if (focusDate == null) {
focusDate = LocalDate.now();
}
if (YearMonth.from(focusDate).equals(displayedYearMonth.get())) {
goToDate(focusDate, true);
} else {
backMonthButton.requestFocus();
}
if (backMonthButton.getWidth() == 0) {
backMonthButton.requestLayout();
forwardMonthButton.requestLayout();
backYearButton.requestLayout();
forwardYearButton.requestLayout();
}
}
protected void createDayCells() {
final EventHandler<MouseEvent> dayCellActionHandler = ev -> {
if (ev.getButton() != MouseButton.PRIMARY) {
return;
}
DateCell dayCell = (DateCell)ev.getSource();
selectDayCell(dayCell);
lastFocusedDayCell = dayCell;
};
for (int row = 0; row < 6; row++) {
for (int col = 0; col < daysPerWeek; col++) {
DateCell dayCell = createDayCell();
dayCell.addEventHandler(MouseEvent.MOUSE_CLICKED, dayCellActionHandler);
dayCells.add(dayCell);
}
}
dayCellDates = new LocalDate[6 * daysPerWeek];
}
private DateCell createDayCell() {
DateCell cell = null;
if (datePicker.getDayCellFactory() != null) {
cell = datePicker.getDayCellFactory().call(datePicker);
}
if (cell == null) {
cell = new DateCell();
}
return cell;
}
protected Locale getLocale() {
return Locale.getDefault(Locale.Category.FORMAT);
}
protected Chronology getPrimaryChronology() {
return datePicker.getChronology();
}
protected boolean isValidDate(Chronology chrono, LocalDate date, int offset, ChronoUnit unit) {
if (date != null) {
try {
return isValidDate(chrono, date.plus(offset, unit));
} catch (DateTimeException ex) {
}
}
return false;
}
protected boolean isValidDate(Chronology chrono, LocalDate date) {
try {
if (date != null) {
chrono.date(date);
}
return true;
} catch (DateTimeException ex) {
return false;
}
}
}