/*
* -----------------------------------------------------------------------
* Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/>
* -----------------------------------------------------------------------
* This file (MonthView.java) is part of project Time4J.
*
* Time4J is free software: You can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 2.1 of the License, or
* (at your option) any later version.
*
* Time4J is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Time4J. If not, see <http://www.gnu.org/licenses/>.
* -----------------------------------------------------------------------
*/
package net.time4j.ui.javafx;
import javafx.geometry.HPos;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import net.time4j.PlainDate;
import net.time4j.Weekday;
import net.time4j.Weekmodel;
import net.time4j.engine.CalendarDate;
import net.time4j.engine.CalendarVariant;
import net.time4j.engine.Chronology;
import net.time4j.engine.EpochDays;
import net.time4j.format.DisplayMode;
import net.time4j.format.OutputContext;
import net.time4j.format.TextWidth;
import net.time4j.format.expert.ChronoFormatter;
import net.time4j.format.expert.PatternType;
import net.time4j.range.CalendarMonth;
import net.time4j.range.DateInterval;
import java.util.Locale;
import java.util.Optional;
class MonthView<T extends CalendarDate>
extends TableView<T> {
//~ Statische Felder/Initialisierungen --------------------------------
private static final String CSS_CALENDAR_MONTH_VIEW = "calendar-month-view";
private static final String CSS_CALENDAR_CELL_INSIDE_RANGE = "calendar-cell-inside-range";
private static final String CSS_CALENDAR_CELL_OUT_OF_RANGE = "calendar-cell-out-of-range";
private static final String CSS_CALENDAR_TODAY = "calendar-cell-today";
private static final String CSS_CALENDAR_SELECTED = "calendar-cell-selected";
private static final String CSS_CALENDAR_WEEKDAY_HEADER = "calendar-weekday-header";
private static final String CSS_CALENDAR_WEEK_OF_YEAR = "calendar-week-of-year";
//~ Konstruktoren -----------------------------------------------------
protected MonthView(
CalendarControl<T> control,
FXCalendarSystem<T> calsys,
boolean animationMode
) {
super(control, calsys, animationMode);
getStyleClass().add(CSS_CALENDAR_MONTH_VIEW);
// listen to layout-change
control.showWeeksProperty().addListener(observable -> rebuild());
if (!this.isAnimationMode()) {
// repaint today-cell if necessary
control.selectedDateProperty().addListener(
(observable, oldValue, newValue) -> {
if ((newValue == null) || (newValue.equals(control.today()))) {
updateContent(control.pageDateProperty().getValue());
}
}
);
// listen to arrow keys
this.setEventHandler(
KeyEvent.KEY_PRESSED,
event -> {
KeyCode code = event.getCode();
if (code.isArrowKey()) {
int shift;
switch (code) {
case UP:
shift = -7;
break;
case RIGHT:
shift = 1;
break;
case DOWN:
shift = 7;
break;
case LEFT:
shift = -1;
break;
default:
return;
}
event.consume();
setFocusedDate(shift);
}
}
);
}
}
//~ Methoden ----------------------------------------------------------
@Override
protected void buildContent() {
boolean showWeeks = this.getControl().showWeeksProperty().get();
int extra = (showWeeks ? 1 : 0);
// table header with seven weekday names
for (int colIndex = 0; colIndex < 7 + extra; colIndex++) {
Label label = new Label();
label.setMaxWidth(Double.MAX_VALUE);
label.setAlignment(Pos.CENTER);
if (showWeeks && colIndex == 0) {
label.getStyleClass().add(CSS_CALENDAR_WEEK_OF_YEAR);
add(label, 0, 0);
} else {
label.getStyleClass().add(CSS_CALENDAR_WEEKDAY_HEADER);
add(label, colIndex, 0);
}
}
// we use fixed count of rows (always 6) in order to avoid sudden changes in height when browsing
for (int rowIndex = 0; rowIndex < 6; rowIndex++) {
if (showWeeks) {
Label label = new Label();
label.setMaxWidth(USE_PREF_SIZE);
label.setMaxHeight(Double.MAX_VALUE);
GridPane.setVgrow(label, Priority.ALWAYS);
GridPane.setHgrow(label, Priority.NEVER);
GridPane.setHalignment(label, HPos.RIGHT);
label.getStyleClass().add(CSS_CALENDAR_WEEK_OF_YEAR);
add(label, 0, rowIndex + 1);
}
for (int colIndex = 0; colIndex < 7; colIndex++) {
Button button = new Button();
button.setMaxWidth(Double.MAX_VALUE);
button.setMaxHeight(Double.MAX_VALUE);
GridPane.setVgrow(button, Priority.ALWAYS);
GridPane.setHgrow(button, Priority.ALWAYS);
button.setOnAction(
actionEvent -> {
T selected = getControl().chronology().getChronoType().cast(button.getUserData());
T oldValue = getControl().selectedDateProperty().get();
if ((oldValue == null) || !oldValue.equals(selected)) {
getControl().selectedDateProperty().setValue(selected);
}
}
);
add(button, colIndex + extra, rowIndex + 1);
}
}
}
@Override
protected void updateContent(T date) {
Locale locale = this.getControl().localeProperty().get();
if (locale == null) {
locale = Locale.ROOT;
}
this.updateCells(locale, date);
this.updateColumnHeaders(locale);
if (!this.isAnimationMode()) {
this.updateNavigationTitle(locale, date);
}
}
@Override
protected int getViewIndex() {
return NavigationBar.MONTH_VIEW;
}
private void setFocusedDate(int shift) {
T date = null;
int extra = (this.getControl().showWeeksProperty().get() ? 1 : 0);
// a) search for focused button
for (int rowIndex = 0; rowIndex < 6; rowIndex++) {
int index = (rowIndex + 1) * (7 + extra);
for (int colIndex = 0; colIndex < 7; colIndex++) {
Button button = (Button) this.getChildren().get(index + colIndex + extra);
if (button.isFocused()) {
date = getControl().chronology().getChronoType().cast(button.getUserData());
break;
}
}
if (date != null) {
break;
}
}
if (date == null) {
return;
}
// b) request focus on button with relevant date
date = getCalendarSystem().navigateByDays(date, shift);
for (int rowIndex = 0; rowIndex < 6; rowIndex++) {
int index = (rowIndex + 1) * (7 + extra);
for (int colIndex = 0; colIndex < 7; colIndex++) {
Button button = (Button) this.getChildren().get(index + colIndex + extra);
T test = getControl().chronology().getChronoType().cast(button.getUserData());
if (date.equals(test)) {
if (!button.isDisabled()) {
button.requestFocus();
}
return;
}
}
}
}
private void updateCells(
Locale locale,
T pageDate
) {
boolean showWeeks = this.getControl().showWeeksProperty().get();
int extra = (showWeeks ? 1 : 0);
T selected = this.getControl().selectedDateProperty().getValue();
T today = this.getControl().today();
Weekmodel model = this.getWeekmodel(locale);
int btnCount = 0;
int firstValidDay;
T current;
try {
current = this.getCalendarSystem().withFirstDayOfMonth(pageDate);
int localDayOfWeek = this.getDayOfWeek(current).getValue(model);
if (localDayOfWeek == 1) {
current = this.getCalendarSystem().navigateByDays(current, -7);
} else {
current = this.getCalendarSystem().navigateByDays(current, 1 - localDayOfWeek);
}
firstValidDay = 1;
} catch (ArithmeticException | IllegalArgumentException ex) {
current = null;
Weekday wd = this.getDayOfWeek(this.getCalendarSystem().getChronologicalMinimum());
firstValidDay = wd.getValue(model);
}
Chronology<T> chronology = this.getControl().chronology();
ChronoFormatter<T> woyFormat = ChronoFormatter.ofPattern("w", PatternType.CLDR, locale, chronology);
ChronoFormatter<T> cellFormat = ChronoFormatter.ofPattern("d", PatternType.CLDR, locale, chronology);
ChronoFormatter<T> tooltipFormat = this.getCalendarSystem().createTooltipFormat(locale);
CellCustomizer<T> cellCustomizer = this.getControl().cellCustomizerProperty().get();
for (int rowIndex = 0; rowIndex < 6; rowIndex++) {
int index = (rowIndex + 1) * (7 + extra);
T anyCellDate = null;
for (int colIndex = 0; colIndex < 7; colIndex++) {
Button button = (Button) this.getChildren().get(index + colIndex + extra);
button.getStyleClass().remove(CSS_CALENDAR_CELL_INSIDE_RANGE);
button.getStyleClass().remove(CSS_CALENDAR_CELL_OUT_OF_RANGE);
button.getStyleClass().remove(CSS_CALENDAR_TODAY);
button.getStyleClass().remove(CSS_CALENDAR_SELECTED);
btnCount++;
if ((current == null) && (btnCount == firstValidDay)) {
current = this.getCalendarSystem().getChronologicalMinimum();
}
boolean disabled = (
(current == null)
|| current.isBefore(this.getControl().minDateProperty().get())
|| current.isAfter(this.getControl().maxDateProperty().get())
);
button.setDisable(disabled);
if (disabled) {
button.setOpacity(0); // make button transparent
button.setText(" ");
button.setTooltip(null);
} else {
// resets opacity, too (see calendar.css)
if (this.getCalendarSystem().getMonth(current) == this.getCalendarSystem().getMonth(pageDate)) {
button.getStyleClass().add(CSS_CALENDAR_CELL_INSIDE_RANGE);
} else {
button.getStyleClass().add(CSS_CALENDAR_CELL_OUT_OF_RANGE);
}
if (current.equals(today)) {
button.getStyleClass().add(CSS_CALENDAR_TODAY);
}
if (current.equals(selected)) {
button.getStyleClass().add(CSS_CALENDAR_SELECTED);
}
anyCellDate = current;
button.setText(cellFormat.format(current));
String tooltip = tooltipFormat.format(current);
if (!(current instanceof PlainDate)) {
tooltip = // using LRM-marker shows ISO-date in right order even in RTL-languages
tooltip + " (\u200E" + PlainDate.of(current.getDaysSinceEpochUTC(), EpochDays.UTC) + ")";
}
button.setTooltip(new Tooltip(tooltip));
}
if (cellCustomizer != null) {
Optional<T> dateRef = (disabled ? Optional.<T>empty() : Optional.of(current));
cellCustomizer.customize(button, colIndex, rowIndex, model, dateRef);
if (disabled && !button.isDisabled()) {
button.setDisable(true); // prevents customization of disabled-status if appropriate
}
}
button.setUserData(disabled ? null : current);
try {
if (current != null) {
current = this.getCalendarSystem().navigateByDays(current, 1);
}
} catch (ArithmeticException ex) {
current = null;
}
}
if (showWeeks) {
Label label = (Label) this.getChildren().get(index);
if (anyCellDate == null) {
label.setText(" ");
} else {
String woy = woyFormat.format(anyCellDate);
if (woy.length() == 1) {
woy = " " + woy;
}
label.setText(woy);
}
}
}
}
private void updateColumnHeaders(Locale locale) {
Weekday wd = this.getWeekmodel(locale).getFirstDayOfWeek();
boolean showWeeks = this.getControl().showWeeksProperty().get();
int extra = (showWeeks ? 1 : 0);
for (int colIndex = 0; colIndex < 7; colIndex++) {
Label label = (Label) this.getChildren().get(colIndex + extra);
label.setText(wd.getDisplayName(locale, TextWidth.SHORT, OutputContext.STANDALONE));
wd = wd.next();
}
}
private void updateNavigationTitle(
Locale locale,
T date
) {
String pattern = CalendarMonth.chronology().getFormatPattern(DisplayMode.FULL, locale);
if (!(date instanceof PlainDate)) {
if (this.getControl().chronology().getFormatPattern(DisplayMode.MEDIUM, locale).endsWith("G")) {
pattern = pattern + " G";
} else {
pattern = "G " + pattern;
}
}
this.titleProperty().setValue(
ChronoFormatter.ofPattern(pattern, PatternType.CLDR, locale, getControl().chronology()).format(date)
);
T min = this.getCalendarSystem().withFirstDayOfMonth(date);
T max = this.getCalendarSystem().withLastDayOfMonth(min);
DateInterval range =
DateInterval.between(
PlainDate.of(min.getDaysSinceEpochUTC(), EpochDays.UTC),
PlainDate.of(max.getDaysSinceEpochUTC(), EpochDays.UTC)
);
String prefix;
if (min instanceof CalendarVariant) {
prefix = CalendarVariant.class.cast(min).getVariant();
} else {
prefix = getCalendarSystem().getCalendarType();
}
this.infoProperty().setValue(prefix + ": " + range.toString());
}
private Weekmodel getWeekmodel(Locale locale) {
if (locale.getCountry().isEmpty()) {
return this.getCalendarSystem().getDefaultWeekmodel();
} else {
return Weekmodel.of(locale);
}
}
private Weekday getDayOfWeek(T date) {
return Weekday.valueOf((int) (Math.floorMod(date.getDaysSinceEpochUTC() + 5, 7) + 1));
}
}