All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.calendarfx.view.MonthSheetView Maven / Gradle / Ivy

There is a newer version: 11.12.7
Show newest version
/*
 *  Copyright (C) 2017 Dirk Lemmermann Software & Consulting (dlsc.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *          http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package com.calendarfx.view;

import com.calendarfx.model.Calendar;
import com.calendarfx.model.Calendar.Style;
import com.calendarfx.model.Entry;
import impl.com.calendarfx.view.MonthSheetViewSkin;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.Skin;
import javafx.scene.control.ToggleGroup;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.PickResult;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.util.Callback;
import org.controlsfx.control.PropertySheet;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Month;
import java.time.Year;
import java.time.YearMonth;
import java.time.ZonedDateTime;
import java.time.format.TextStyle;
import java.time.temporal.WeekFields;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import static java.util.Objects.requireNonNull;

/**
 * A view laying out months and dates in a spreadsheet style. An ideal way of displaying several
 * months or even a year. Each day is represented by a cell (see {@link DateCell}). Cells are
 * created by a cell factory and can be customized to fit an application's needs.
 * 

Screenshot (Using DetailedDateCell)

*
* *

Screenshot (Using "aligned" weekday layout)

*
* * @see #setWeekDayLayout(WeekDayLayoutStrategy) * @see #setCellFactory(Callback) * @see MonthSheetView.DateCell * @see MonthSheetView.SimpleDateCell * @see MonthSheetView.DetailedDateCell * @see MonthSheetView.BadgeDateCell * @see MonthSheetView.UsageDateCell */ public class MonthSheetView extends DateControl { private static final String DEFAULT_STYLE_CLASS = "month-sheet-view"; private static final String USAGE_VERY_LOW = "usage-very-low"; private static final String USAGE_LOW = "usage-low"; private static final String USAGE_MEDIUM = "usage-medium"; private static final String USAGE_HIGH = "usage-high"; private static final String USAGE_VERY_HIGH = "usage-very-high"; private double ctxMenuScreenX; private double ctxMenuScreenY; private DateCell dateCell; /** * Constructs a new month sheet view that is using the {@link SimpleDateCell}. */ public MonthSheetView() { getStyleClass().add(DEFAULT_STYLE_CLASS); setHeaderCellFactory(param -> new MonthHeaderCell(param.getView(), param.getYearMonth())); /* * This view has its own context menu. */ setContextMenu(createContextMenu()); addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, evt -> { final PickResult pickResult = evt.getPickResult(); final Node intersectedNode = pickResult.getIntersectedNode(); if (intersectedNode != null && intersectedNode instanceof DateCell) { this.dateCell = (DateCell) intersectedNode; } else { this.dateCell = null; } ctxMenuScreenX = evt.getScreenX(); ctxMenuScreenY = evt.getScreenY(); }); // Set the factory AFTER the context menu has been created or the cell factory // will be overridden again. setCellFactory(param -> new SimpleDateCell(param.getView(), param.getDate())); } @Override protected Skin createDefaultSkin() { return new MonthSheetViewSkin(this); } private ContextMenu createContextMenu() { ContextMenu contextMenu = new ContextMenu(); MenuItem newEntry = new MenuItem(Messages.getString("MonthSheetView.ADD_NEW_EVENT")); //$NON-NLS-1$ newEntry.setOnAction(evt -> { LocalDate date = getDateSelectionModel().getLastSelected(); Entry entry = createEntryAt(ZonedDateTime.of(date, LocalTime.of(12, 0), getZoneId())); Callback callback = getEntryDetailsCallback(); EntryDetailsParameter param = new EntryDetailsParameter(null, this, entry, dateCell, ctxMenuScreenX, ctxMenuScreenY); callback.call(param); }); contextMenu.getItems().add(newEntry); contextMenu.getItems().add(new SeparatorMenuItem()); RadioMenuItem standardCellItem = new RadioMenuItem(Messages.getString("MonthSheetView.STANDARD_CELLS")); RadioMenuItem detailCellItem = new RadioMenuItem(Messages.getString("MonthSheetView.DETAIL_CELLS")); RadioMenuItem usageCellItem = new RadioMenuItem(Messages.getString("MonthSheetView.USAGE_CELLS")); RadioMenuItem badgeCellItem = new RadioMenuItem(Messages.getString("MonthSheetView.BADGE_CELLS")); standardCellItem.setOnAction(evt -> setCellFactory(param -> new SimpleDateCell(param.getView(), param.getDate()))); detailCellItem.setOnAction(evt -> setCellFactory(param -> new DetailedDateCell(param.getView(), param.getDate()))); usageCellItem.setOnAction(evt -> setCellFactory(param -> new UsageDateCell(param.getView(), param.getDate()))); badgeCellItem.setOnAction(evt -> setCellFactory(param -> new BadgeDateCell(param.getView(), param.getDate()))); ToggleGroup group = new ToggleGroup(); group.getToggles().addAll(standardCellItem, detailCellItem, usageCellItem, badgeCellItem); contextMenu.getItems().addAll(standardCellItem, detailCellItem, usageCellItem, badgeCellItem); standardCellItem.setSelected(true); return contextMenu; } // cell factory support private final ObjectProperty> cellFactory = new SimpleObjectProperty<>(this, "cellFactory"); /** * A property used to store a reference to the cell factory for this view. The default cell factory * simply returns an instance of type {@link SimpleDateCell}. * * @return the cell factory */ public final ObjectProperty> cellFactoryProperty() { return cellFactory; } /** * Returns the value of {@link #cellFactoryProperty()}. * * @return the cell factory */ public final Callback getCellFactory() { return cellFactoryProperty().get(); } /** * Sets the value of {@link #cellFactoryProperty()}. * * @param factory the cell factory */ public final void setCellFactory(Callback factory) { requireNonNull(factory); cellFactoryProperty().set(factory); } // header cell factory support private final ObjectProperty> headerCellFactory = new SimpleObjectProperty<>(this, "headerCellFactory"); /** * A property used to store a reference to the "header" cell factory for this view. The default cell factory * simply returns an instance of type {@link MonthHeaderCell}. * * @return the cell factory */ public final ObjectProperty> headerCellFactoryProperty() { return headerCellFactory; } /** * Returns the value of {@link #headerCellFactoryProperty()}. * * @return the "header" cell factory */ public final Callback getHeaderCellFactory() { return headerCellFactoryProperty().get(); } /** * Sets the value of {@link #headerCellFactoryProperty()}. * * @param factory the "header" cell factory */ public final void setHeaderCellFactory(Callback factory) { requireNonNull(factory); headerCellFactoryProperty().set(factory); } // layout strategy support /** * The different ways the view can align date cells. * * @see #setWeekDayLayout(WeekDayLayoutStrategy) */ public enum WeekDayLayoutStrategy { /** * Aligns the date cells in the traditional ways where * the top-most cell shows the 1st day of the month and the * bottom-most cell shows the last day of the month. */ STANDARD, /** * Aligns the date cells in such a way that all cells * in a given row are dates on the same weekday, e.g. all * cells / dates are located on a "Monday". */ ALIGNED } private final ObjectProperty weekDayLayout = new SimpleObjectProperty<>(this, "weekDayLayout", WeekDayLayoutStrategy.STANDARD); /** * A property used to store the layout strategy used by this view. The month sheet * view is capable of ensuring that the date cells next to each other always show * the same weekday. For this extra empty cells are added at the top of some of the * columns. However, the standard layout is to always show the first day of the month * in the first cell from the top. * * @return the weekday layout strategy property */ public final ObjectProperty weekDayLayoutProperty() { return weekDayLayout; } /** * Returns the value of {@link #weekDayLayoutProperty()}. * * @return the weekday layout strategy */ public final WeekDayLayoutStrategy getWeekDayLayout() { return weekDayLayoutProperty().get(); } /** * Sets the value of {@link #weekDayLayoutProperty()}. * * @param weekDayLayout the weekday layout strategy */ public final void setWeekDayLayout(WeekDayLayoutStrategy weekDayLayout) { weekDayLayoutProperty().set(weekDayLayout); } // view unit support private final ObjectProperty viewUnit = new SimpleObjectProperty<>(this, "viewUnit", ViewUnit.YEAR); /** * A property used to store the unit shown by the view (e.g. "quarters", "year", "month"). * * @return the view unit (e.g. "quarters", "year", "month") */ public final ObjectProperty viewUnitProperty() { return viewUnit; } /** * Returns the value of {@link #viewUnitProperty()}. * * @return the view unit (e.g. "quarters", "year", "month") */ public final ViewUnit getViewUnit() { return viewUnitProperty().get(); } /** * Sets the value of {@link #viewUnitProperty()}. * * @param unit the view unit (e.g. "quarters", "year", "month") */ public final void setViewUnit(ViewUnit unit) { requireNonNull(unit); viewUnitProperty().set(unit); } // extended view unit support private final ObjectProperty extendedViewUnit = new SimpleObjectProperty<>(this, "extendedViewUnit", ViewUnit.MONTH); /** * A property used to store the unit shown by the view in front of or after the * main columns (e.g. "quarters", "year", "month"). So a month showing twelve months * of a year might decide to add some "padding" and show another month in front of * the year and one month after the year. * * @return the view unit (e.g. "quarters", "year", "month") */ public final ObjectProperty extendedViewUnitProperty() { return extendedViewUnit; } /** * Returns the value of {@link #extendedViewUnitProperty()}. * * @return the extended view unit (e.g. "quarters", "year", "month") */ public final ViewUnit getExtendedViewUnit() { return extendedViewUnitProperty().get(); } /** * Sets the value of {@link #extendedViewUnitProperty()}. * * @param unit the extended view unit (e.g. "quarters", "year", "month") */ public final void setExtendedViewUnit(ViewUnit unit) { requireNonNull(unit); extendedViewUnitProperty().set(unit); } // extended units forward support private final IntegerProperty extendedUnitsForward = new SimpleIntegerProperty(this, "extendedUnitsForward"); /** * An integer property that stores the number of units that will be used * to extend the view by. A month showing twelve months of a year might decide * to add some "padding" and show another month at the end of the year. In this * case the number of extended units will be 1. * * @return the number of units used to extend the view at the end (e.g. "3 months") */ public final IntegerProperty extendedUnitsForwardProperty() { return extendedUnitsForward; } /** * Returns the value of {@link #extendedUnitsForward}. * * @return the number of units shown at the end of the view */ public final int getExtendedUnitsForward() { return extendedUnitsForwardProperty().get(); } /** * Sets the value of {@link #extendedUnitsForward}. * * @param units the number of units shown at the end of the view */ public final void setExtendedUnitsForward(int units) { if (units < 0) { throw new IllegalArgumentException("number of units can not be negative but was " + units); } extendedUnitsForwardProperty().set(units); } // extended units backward support private final IntegerProperty extendedUnitsBackward = new SimpleIntegerProperty(this, "extendedUnitsBackward"); /** * An integer property that stores the number of units that will be used * to extend the view by. A month showing twelve months of a year might decide * to put some "padding" in front of the year and show another month. In this * case the number of extended units will be 1. * * @return the number of units used to extend the view at the beginning (e.g. "3 months") */ public final IntegerProperty extendedUnitsBackwardProperty() { return extendedUnitsBackward; } /** * Returns the value of {@link #extendedUnitsBackwardProperty}. * * @return the number of units shown at the beginning of the view */ public final int getExtendedUnitsBackward() { return extendedUnitsBackwardProperty().get(); } /** * Sets the value of {@link #extendedUnitsBackwardProperty}. * * @param units the number of units shown at the beginning of the view */ public final void setExtendedUnitsBackward(final int units) { if (units < 0) { throw new IllegalArgumentException("number of units can not be negative but was " + units); } extendedUnitsBackwardProperty().set(units); } /** * Returns the first "regular" month shown by the view. This method * does not consider the extended months. If the view shows a whole * year then the start month will be January. * * @return the start month */ public final YearMonth getStartMonth() { return getViewUnit().getStartMonth(getDate()); } /** * Returns the first "regular" month shown by the view. This method * does not consider the extended months. If the view shows a whole * year then the end month will be December. * * @return the start month */ public final YearMonth getEndMonth() { return getViewUnit().getEndMonth(getDate()); } /** * Returns the first month shown by the view. This method also takes the extended months into account. If the view shows a whole * year with an extended unit of "month" and and extended unit count of two, then the start month will be November of the previous year. * * @return the start month * @see #setExtendedViewUnit(ViewUnit) * @see #setExtendedUnitsBackward(int) * @see #setExtendedUnitsForward(int) */ public final YearMonth getExtendedStartMonth() { return getStartMonth().minusMonths(getExtendedViewUnit().toMonths(getExtendedUnitsBackward())); } /** * Returns the last month shown by the view. This method also takes the extended months into account. If the view shows a whole * year with an extended unit of "month" and and extended unit count of two, then the end month will be February of the next year. * * @return the start month * @see #setExtendedViewUnit(ViewUnit) * @see #setExtendedUnitsBackward(int) * @see #setExtendedUnitsForward(int) */ public final YearMonth getExtendedEndMonth() { return getEndMonth().plusMonths(getExtendedViewUnit().toMonths(getExtendedUnitsForward())); } /** * A simple check to see if the given month is part of the extended months. * * @param month the month to check * @return true if the given month is part of the extended months * @see #setExtendedViewUnit(ViewUnit) * @see #setExtendedUnitsBackward(int) * @see #setExtendedUnitsForward(int) */ public final boolean isExtendedMonth(YearMonth month) { if (month != null) { YearMonth extendedStart = getExtendedStartMonth(); if ((month.equals(extendedStart) || month.isAfter(extendedStart)) && month.isBefore(getStartMonth())) { return true; } YearMonth extendedEnd = getExtendedEndMonth(); if ((month.equals(extendedEnd) || month.isBefore(extendedEnd)) && month.isAfter(getEndMonth())) { return true; } } return false; } /** * Determines if the given date is currently showing is part of the view. This * method uses the extended start and end months. * * @param date the date to check for visibility * @return true if the date is within the time range of the view * @see #getExtendedStartMonth() * @see #getExtendedEndMonth() */ public final boolean isVisibleDate(LocalDate date) { if (date != null) { YearMonth extendedStart = getExtendedStartMonth(); YearMonth extendedEnd = getExtendedEndMonth(); LocalDate startDate = extendedStart.atDay(1); LocalDate endDate = extendedEnd.atEndOfMonth(); if ((date.equals(startDate) || date.isAfter(startDate)) && (date.equals(endDate) || date.isBefore(endDate))) { return true; } } return false; } // show week number support private final BooleanProperty showWeekNumber = new SimpleBooleanProperty(this, "showWeekNumber", true); /** * A property used to control whether the week numbers should be shown. * * @return true if the numbers should be shown * @see DateControl#weekFieldsProperty() */ public final BooleanProperty showWeekNumberProperty() { return showWeekNumber; } /** * Returns the value of {@link #showWeekNumberProperty()}. * * @return true if the numbers should be shown */ public final boolean isShowWeekNumber() { return showWeekNumberProperty().get(); } /** * Sets the value of {@link #showWeekNumberProperty()}. * * @param show true if the numbers should be shown */ public final void setShowWeekNumber(boolean show) { showWeekNumberProperty().set(show); } // date selection model support private final ObjectProperty dateSelectionModel = new SimpleObjectProperty<>(this, "dateSelectionModel", new DateSelectionModel()); /** * A property used to store a selection model for selecting dates. * * @return the date selection model */ public final ObjectProperty dateSelectionModelProperty() { return dateSelectionModel; } /** * Returns the value of {@link #dateSelectionModelProperty()}. * * @return the date selection model */ public final DateSelectionModel getDateSelectionModel() { return dateSelectionModelProperty().get(); } /** * Sets the value of {@link #dateSelectionModelProperty()}. * * @param model the date selection model */ public final void setDateSelectionModel(DateSelectionModel model) { Objects.requireNonNull(model); dateSelectionModelProperty().set(model); } /** * An enumerator to control the behaviour of the control when the user * clicks on a date. The view supports date selections or the ability to * show a popover that lists the entries on the clicked date. * * @see MonthSheetView#clickBehaviourProperty() */ public enum ClickBehaviour { /** * A value used to make the control select the date on which the user clicked. */ PERFORM_SELECTION, /** * A value used to make the control show some kind of dialog or popover to show * details about the clicked date. */ SHOW_DETAILS, /** * Do nothing when the user clicks on a date. */ NONE } private final ObjectProperty clickBehaviour = new SimpleObjectProperty<>(this, "clickBehaviour", ClickBehaviour.PERFORM_SELECTION); /** * The behaviour used when the user clicks on a date. * * @return the click behaviour */ public final ObjectProperty clickBehaviourProperty() { return clickBehaviour; } /** * Sets the value of {@link #clickBehaviourProperty()}. * * @param behaviour the click behaviour */ public final void setClickBehaviour(ClickBehaviour behaviour) { requireNonNull(behaviour); clickBehaviourProperty().set(behaviour); } /** * Returns the value of {@link #clickBehaviourProperty()}. * * @return the click behaviour */ public final ClickBehaviour getClickBehaviour() { return clickBehaviourProperty().get(); } /** * A view unit describes how many months the view should show. The view * knows about years, semesters (6 months), quarters (3 months), and single * months. When the date property changes the view will make sure to show * the year, semester, quarter, or month where that date is located. * * @see MonthSheetView#setViewUnit(ViewUnit) * @see MonthSheetView#setExtendedViewUnit(ViewUnit) */ public enum ViewUnit { /** * A value used to instruct the {@link MonthSheetView} to display a single * month. * * @see MonthSheetView#setViewUnit(ViewUnit) */ MONTH { @Override public YearMonth getStartMonth(LocalDate date) { return YearMonth.from(date); } @Override public YearMonth getEndMonth(LocalDate date) { return YearMonth.from(date); } @Override public int getMonthsCount() { return 1; } }, /** * A value used to instruct the {@link MonthSheetView} to display a quarter * year (3 months). * * @see MonthSheetView#setViewUnit(ViewUnit) */ QUARTER { @Override public YearMonth getStartMonth(LocalDate date) { return Year.of(date.getYear()).atMonth(QUARTER_START_MONTH[date.getMonthValue() - 1]); } @Override public YearMonth getEndMonth(LocalDate date) { return Year.of(date.getYear()).atMonth(QUARTER_END_MONTH[date.getMonthValue() - 1]); } @Override public int getMonthsCount() { return 3; } }, /** * A value used to instruct the {@link MonthSheetView} to display a semester * (6 months). * * @see MonthSheetView#setViewUnit(ViewUnit) */ SEMESTER { @Override public YearMonth getStartMonth(LocalDate date) { return Year.of(date.getYear()).atMonth(SEMESTER_START_MONTH[date.getMonthValue() - 1]); } @Override public YearMonth getEndMonth(LocalDate date) { return Year.of(date.getYear()).atMonth(SEMESTER_END_MONTH[date.getMonthValue() - 1]); } @Override public int getMonthsCount() { return 6; } }, /** * A value used to instruct the {@link MonthSheetView} to display a whole year. * * @see MonthSheetView#setViewUnit(ViewUnit) */ YEAR { @Override public YearMonth getStartMonth(LocalDate date) { return Year.from(date).atMonth(Month.JANUARY); } @Override public YearMonth getEndMonth(LocalDate date) { return Year.from(date).atMonth(Month.DECEMBER); } @Override public int getMonthsCount() { return 12; } }; private static final int[] QUARTER_START_MONTH = {1, 1, 1, 4, 4, 4, 7, 7, 7, 10, 10, 10}; private static final int[] QUARTER_END_MONTH = {3, 3, 3, 6, 6, 6, 9, 9, 9, 12, 12, 12}; private static final int[] SEMESTER_START_MONTH = {1, 1, 1, 1, 1, 1, 7, 7, 7, 7, 7, 7}; private static final int[] SEMESTER_END_MONTH = {6, 6, 6, 6, 6, 6, 12, 12, 12, 12, 12, 12}; /** * Returns the start month for the given view unit and date. * * @param date the date for which to return the start month * @return the start month for the given view unit and date */ public abstract YearMonth getStartMonth(LocalDate date); /** * Returns the end month for the given view unit and date. * * @param date the date for which to return the end month * @return the end month for the given view unit and date */ public abstract YearMonth getEndMonth(LocalDate date); /** * Returns the number of months represented by the view unit, e.g. "3" for * "quarter". * * @return the month count of the view unit */ public abstract int getMonthsCount(); /** * Calculates the total number of months for the given * number of units, e.g. when "units" is "3" and the "view unit" is * "quarter" then the total number will be "9": three quarters equal * 9 months. * * @param units the number of units * @return the total number of months */ public int toMonths(int units) { return getMonthsCount() * units; } } /** * A parameter object used by the cell factory of the month sheet view. */ public static final class DateParameter { private MonthSheetView view; private LocalDate date; /** * Constructs a new parameter object. * * @param view the view for which a cell has to be created * @param date the date that the cell will represent */ public DateParameter(MonthSheetView view, LocalDate date) { this.view = Objects.requireNonNull(view); this.date = date; } /** * Returns the view for which a cell has to be created. * * @return the month sheet view */ public MonthSheetView getView() { return view; } /** * Returns the date for which a cell has to be created. * * @return the date */ public LocalDate getDate() { return date; } } /** * A parameter object used by the "header" cell factory of the month sheet view. */ public static final class HeaderParameter { private MonthSheetView view; private YearMonth yearMonth; public HeaderParameter(MonthSheetView view, YearMonth yearMonth) { this.view = Objects.requireNonNull(view); this.yearMonth = yearMonth; } public final MonthSheetView getView() { return view; } public final YearMonth getYearMonth() { return yearMonth; } } private static final String MONTH_SHEET_VIEW_CATEGORY = "Month Sheet View"; @Override public ObservableList getPropertySheetItems() { ObservableList items = super.getPropertySheetItems(); items.add(new PropertySheet.Item() { @Override public Optional> getObservableValue() { return Optional.of(clickBehaviourProperty()); } @Override public void setValue(Object value) { setClickBehaviour((ClickBehaviour) value); } @Override public Object getValue() { return getClickBehaviour(); } @Override public Class getType() { return ClickBehaviour.class; } @Override public String getName() { return "Click Behaviour"; //$NON-NLS-1$ } @Override public String getDescription() { return "Click behaviour"; //$NON-NLS-1$ } @Override public String getCategory() { return MONTH_SHEET_VIEW_CATEGORY; } }); items.add(new PropertySheet.Item() { @Override public Class getType() { return WeekDayLayoutStrategy.class; } @Override public String getCategory() { return MONTH_SHEET_VIEW_CATEGORY; } @Override public String getName() { return "Week Day Layout"; } @Override public String getDescription() { return "The layout strategy for the week day on the months"; } @Override public Object getValue() { return getWeekDayLayout(); } @Override public void setValue(Object value) { setWeekDayLayout((WeekDayLayoutStrategy) value); } @Override public Optional> getObservableValue() { return Optional.of(weekDayLayoutProperty()); } }); items.add(new PropertySheet.Item() { @Override public Class getType() { return ViewUnit.class; } @Override public String getCategory() { return MONTH_SHEET_VIEW_CATEGORY; } @Override public String getName() { return "View Unit"; } @Override public String getDescription() { return "View Unit"; } @Override public Object getValue() { return getViewUnit(); } @Override public void setValue(Object value) { setViewUnit((ViewUnit) value); } @Override public Optional> getObservableValue() { return Optional.of(viewUnitProperty()); } }); items.add(new PropertySheet.Item() { @Override public Class getType() { return ViewUnit.class; } @Override public String getCategory() { return MONTH_SHEET_VIEW_CATEGORY; } @Override public String getName() { return "Extended View Unit"; } @Override public String getDescription() { return "Extended View Unit"; } @Override public Object getValue() { return getExtendedViewUnit(); } @Override public void setValue(Object value) { setExtendedViewUnit((ViewUnit) value); } @Override public Optional> getObservableValue() { return Optional.of(extendedViewUnitProperty()); } }); items.add(new PropertySheet.Item() { @Override public Class getType() { return Integer.class; } @Override public String getCategory() { return MONTH_SHEET_VIEW_CATEGORY; } @Override public String getName() { return "Extended Units Forward"; } @Override public String getDescription() { return "Extended Units Forward"; } @Override public Object getValue() { return getExtendedUnitsForward(); } @Override public void setValue(Object value) { setExtendedUnitsForward((Integer) value); } @Override public Optional> getObservableValue() { return Optional.of(extendedUnitsForwardProperty()); } }); items.add(new PropertySheet.Item() { @Override public Class getType() { return Integer.class; } @Override public String getCategory() { return MONTH_SHEET_VIEW_CATEGORY; } @Override public String getName() { return "Extended Units Backward"; } @Override public String getDescription() { return "Extended Units Backward"; } @Override public Object getValue() { return getExtendedUnitsBackward(); } @Override public void setValue(Object value) { setExtendedUnitsBackward((Integer) value); } @Override public Optional> getObservableValue() { return Optional.of(extendedUnitsBackwardProperty()); } }); items.add(new PropertySheet.Item() { @Override public Class getType() { return Boolean.class; } @Override public String getCategory() { return MONTH_SHEET_VIEW_CATEGORY; } @Override public String getName() { return "Show Week Number"; } @Override public String getDescription() { return "Show Week Number"; } @Override public Object getValue() { return isShowWeekNumber(); } @Override public void setValue(Object value) { setShowWeekNumber((Boolean) value); } @Override public Optional> getObservableValue() { return Optional.of(showWeekNumberProperty()); } }); items.add(new PropertySheet.Item() { @Override public Class getType() { return DateSelectionModel.SelectionMode.class; } @Override public String getCategory() { return MONTH_SHEET_VIEW_CATEGORY; } @Override public String getName() { return "Date Selection Mode"; } @Override public String getDescription() { return "Date Selection Mode"; } @Override public Object getValue() { return getDateSelectionModel().getSelectionMode(); } @Override public void setValue(Object value) { getDateSelectionModel().setSelectionMode((DateSelectionModel.SelectionMode) value); } @Override public Optional> getObservableValue() { return Optional.of(getDateSelectionModel().selectionModeProperty()); } }); return items; } /** * The default cell used for month headers. * * @see MonthSheetView#setHeaderCellFactory(Callback) */ public static class MonthHeaderCell extends Label { private final YearMonth yearMonth; private final MonthSheetView view; /** * Constructs a new month header cell. * * @param view the view where the header is needed * @param yearMonth the month to display */ public MonthHeaderCell(MonthSheetView view, YearMonth yearMonth) { this(view, yearMonth, TextStyle.FULL); } /** * Constructs a new month header cell. * * @param view the view where the header is needed * @param yearMonth the month to display * @param textStyle the text style for the month label (full, short, ...) */ public MonthHeaderCell(MonthSheetView view, YearMonth yearMonth, TextStyle textStyle) { this.view = Objects.requireNonNull(view); this.yearMonth = Objects.requireNonNull(yearMonth); setText(yearMonth.getMonth().getDisplayName(textStyle, Locale.getDefault())); setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); setMinWidth(0); } /** * Returns the view where the cell is used. * * @return the view */ public final MonthSheetView getView() { return view; } /** * Returns the month for which the cell is used. * * @return the month */ public final YearMonth getYearMonth() { return yearMonth; } } /** * The base class for all date cells that are used in combination with the * {@link MonthSheetView}. The base class provides support for the pseudo states * "today" and "selected". The cell also adds several stylesheets based on the * location of the cell and based on the date or weekday represented by the cell. *
    *
  • weekend-day - if the date is on a weekend (e.g. a "Saturday" or "Sunday").
  • *
  • extended-date-cell - if the cell is located within an "extension" month.
  • *
  • first-day-of-week - if the day shown by the cell is the first day of the week (e.g. "Monday" in Europe, "Sunday" in the US).
  • *
* The cell also automatically adds the weekday name as a style class to the date ("monday", "tuesday", ...). */ public static abstract class DateCell extends Region { private static final PseudoClass PSEUDO_CLASS_SELECTED = PseudoClass.getPseudoClass("selected"); private static final PseudoClass PSEUDO_CLASS_TODAY = PseudoClass.getPseudoClass("today"); private static final String CELL_STYLE_CLASS = "date-cell"; private static final String EXTENDED_CELL_STYLE_CLASS = "extended-date-cell"; private static final String WEEKEND_DAY = "weekend-day"; private static final String FIRST_DAY_OF_WEEK = "first-day-of-week"; private final LocalDate date; private final MonthSheetView view; private boolean selected; private boolean today; /** * Constructs a new date cell. * * @param view the parent month sheet view * @param date the date shown by the cell (might be null for leading or trailing empty cells) */ public DateCell(MonthSheetView view, LocalDate date) { this.view = Objects.requireNonNull(view); this.date = date; setMaxWidth(Double.MAX_VALUE); getStyleClass().add(CELL_STYLE_CLASS); setFocusTraversable(true); applyStyles(); } /** * Returns the month sheet view to which the cell belongs. * * @return the parent month sheet view */ public final MonthSheetView getView() { return view; } /** * Returns the date shown by the cell or null if it does * not show any date (might be the case when used as a filler * cell at the beginning or end of the month. * * @return the date shown by the cell */ public final LocalDate getDate() { return date; } private void applyStyles() { YearMonth yearMonth; if (date == null) { // date can be null if cell is used to fill the month column return; } yearMonth = YearMonth.from(date); if (view.isExtendedMonth(yearMonth)) { getStyleClass().add(EXTENDED_CELL_STYLE_CLASS); } WeekFields fields = view.getWeekFields(); DayOfWeek firstDayOfWeek = fields.getFirstDayOfWeek(); if (date.getDayOfWeek().equals(firstDayOfWeek)) { getStyleClass().add(FIRST_DAY_OF_WEEK); } if (view.getWeekendDays().contains(date.getDayOfWeek())) { if (!getStyleClass().contains(WEEKEND_DAY)) { getStyleClass().add(0, WEEKEND_DAY); } } else { getStyleClass().remove(WEEKEND_DAY); } getStyleClass().add(date.getDayOfWeek().name().toLowerCase()); } /** * Returns true if the cell is currently selected. * * @return true if the cell is selected */ public boolean isSelected() { return selected; } /** * Tells the cell whether it is currently selected or not. * * @param selected if true the cell will be considered "selected" * @see MonthSheetView#getDateSelectionModel() */ public void setSelected(boolean selected) { /* * Intentionally not made a final method. Cells might show * different content when they are showing today or when they * are not. */ this.selected = selected; pseudoClassStateChanged(PSEUDO_CLASS_SELECTED, selected); } // Today support. /** * Returns true if the cell represents the day / date that is "today". * * @return true if the cell is showing "today" * @see DateControl#todayProperty() */ public boolean isToday() { return today; } /** * Specifies whether or not the cell represents the day / date that is "today". * Applications are free to override this method but should always call * super.setToday(). * * @param today true tells the cell that it is showing "today" * @see DateControl#todayProperty() */ public void setToday(boolean today) { /* * Intentionally not made a final method. Cells might show * different content when they are showing today or when they * are not. */ this.today = today; pseudoClassStateChanged(PSEUDO_CLASS_TODAY, today); } /** * This method gets invoked whenever the {@link MonthSheetView} determines * that the content of the cell might need to be refreshed. This might be the * case when new entries are created or existing entries are deleted. But also * when an entry gets assigned to a new calendar or an entry's time interval * has changed. * * @param entries the current list of entries for the day represented * by the cell */ public void updateEntries(List> entries) { } } /** * A specialization of the {@link SimpleDateCell} that adds a small canvas on the * right-hand side to visualize the utilization of the day (the entries that exist on * that date). */ public static class DetailedDateCell extends SimpleDateCell { private DetailCanvas canvas; private static final Map calendarColors = new HashMap<>(); static { calendarColors.put(Style.STYLE1.name().toLowerCase(), Color.rgb(119, 192, 75, 0.8)); calendarColors.put(Style.STYLE2.name().toLowerCase(), Color.rgb(65, 143, 203, 0.8)); calendarColors.put(Style.STYLE3.name().toLowerCase(), Color.rgb(247, 209, 91, 0.8)); calendarColors.put(Style.STYLE4.name().toLowerCase(), Color.rgb(157, 91, 159, 0.8)); calendarColors.put(Style.STYLE5.name().toLowerCase(), Color.rgb(208, 82, 95, 0.8)); calendarColors.put(Style.STYLE6.name().toLowerCase(), Color.rgb(249, 132, 75, 0.8)); calendarColors.put(Style.STYLE7.name().toLowerCase(), Color.rgb(174, 102, 62, 0.8)); } /** * Constructs a new detailed date cell. * * @param view the parent month sheet view * @param date the date shown by the cell (might be null for leading or trailing empty cells) */ public DetailedDateCell(MonthSheetView view, LocalDate date) { super(view, date); canvas = new DetailCanvas(); canvas.setMouseTransparent(true); getChildren().add(canvas); } /** * Returns the color to be used for the given calendar style. * * @param style the calendar style * @return the color to be used for the given calendar * @see Calendar#setStyle(String) */ public static final Color getCalendarColor(String style) { return calendarColors.get(style); } /** * Sets the color to be used for the given calendar style. * * @param style the calendar style * @param color the color */ public static final void setCalendarColor(String style, Color color) { calendarColors.put(style, color); } @Override protected void layoutChildren() { Insets insets = getInsets(); double top = insets.getTop(); double bottom = insets.getBottom(); double left = insets.getLeft(); double right = insets.getRight(); double w = getWidth(); double h = getHeight(); double availableHeight = h - top - bottom; double ps1 = dayOfMonthLabel.prefWidth(-1); double ps2 = dayOfWeekLabel.prefWidth(-1); double ps3 = weekNumberLabel.prefWidth(-1); double ps4 = 12; // width of canvas dayOfMonthLabel.resizeRelocate(left, top, ps1, availableHeight); dayOfWeekLabel.resizeRelocate(left + ps1, top, ps2, availableHeight); weekNumberLabel.resizeRelocate(w - right - ps3 - ps4, top, ps3, availableHeight); // canvas canvas.resizeRelocate(w - right - ps4, top, ps4, availableHeight); canvas.setWidth(ps4); canvas.setHeight(availableHeight); canvas.draw(); } @Override protected double computePrefWidth(double height) { return dayOfMonthLabel.prefWidth(-1) + dayOfWeekLabel.prefWidth(-1) + weekNumberLabel.prefWidth(-1) + 12; } /* * Had to override because for some weird reason selected date cells were higher than deselected once. */ @Override protected double computePrefHeight(double width) { // for the height we do not care about the canvas double h = Math.max(dayOfMonthLabel.prefHeight(-1), Math.max(dayOfWeekLabel.prefHeight(-1), weekNumberLabel.prefHeight(-1))); return h + getInsets().getTop() + getInsets().getBottom(); } @Override public void updateEntries(List> entries) { canvas.setEntries(entries); } private final class DetailCanvas extends Canvas { private List> entries; private DetailCanvas() { getStyleClass().add("detail-canvas"); draw(); } @Override public boolean isResizable() { return true; } public void setEntries(List> entries) { this.entries = entries; draw(); } public void draw() { final double width = getWidth(); final double height = getHeight(); GraphicsContext gc = getGraphicsContext2D(); gc.clearRect(0, 0, width, height); if (entries != null && !entries.isEmpty()) { for (Entry entry : entries) { com.calendarfx.model.Calendar calendar = entry.getCalendar(); if (calendar == null) { continue; } Color color = getCalendarColor(calendar.getStyle()); gc.setFill(color); if (entry.isFullDay()) { gc.fillRect(0, 0, width, height); } else { LocalTime startTime = entry.getStartTime(); LocalTime endTime = entry.getEndTime(); if (entry.getStartDate().isBefore(getDate())) { startTime = LocalTime.MIN; } if (entry.getEndDate().isAfter(getDate())) { endTime = LocalTime.MAX; } double y = height * (startTime.toSecondOfDay() / (double) LocalTime.MAX.toSecondOfDay()); double h = height * (endTime.toSecondOfDay() / (double) LocalTime.MAX.toSecondOfDay()); gc.fillRect(0, y, width, h - y); } } } } } } /** * The badge date cell extends the {@link SimpleDateCell} and adds another * label to it that is used to display a counter of the number of entries that * exist on that date. */ public static class BadgeDateCell extends SimpleDateCell { private Label counterLabel; /** * Constructs a new badge date cell. * * @param view the parent month sheet view * @param date the date shown by the cell (might be null for leading or trailing empty cells) */ public BadgeDateCell(MonthSheetView view, LocalDate date) { super(view, date); getStyleClass().add("badge-date-cell"); counterLabel = new Label(); counterLabel.getStyleClass().add("badge-label"); counterLabel.setAlignment(Pos.CENTER_RIGHT); counterLabel.setVisible(false); // has to be initially invisible (to work with empty cells) getChildren().add(counterLabel); // this cell type can not display week numbers weekNumberLabel.setVisible(false); } @Override protected void layoutChildren() { Insets insets = getInsets(); double top = insets.getTop(); double bottom = insets.getBottom(); double left = insets.getLeft(); double right = insets.getRight(); double w = getWidth(); double h = getHeight(); double availableHeight = h - top - bottom; double ps1 = dayOfMonthLabel.prefWidth(-1); double ps2 = dayOfWeekLabel.prefWidth(-1); double ps4 = counterLabel.prefWidth(-1); double ph = counterLabel.prefHeight(-1); dayOfMonthLabel.resizeRelocate(left, top, ps1, availableHeight); dayOfWeekLabel.resizeRelocate(left + ps1, top, ps2, availableHeight); // center the counter label, do not let it use the entire height counterLabel.resizeRelocate(w - right - ps4, top + availableHeight / 2 - ph / 2, ps4, Math.min(availableHeight, ph)); } @Override protected double computePrefWidth(double height) { return dayOfMonthLabel.prefWidth(-1) + dayOfWeekLabel.prefWidth(-1) + weekNumberLabel.prefWidth(-1); } /* * Had to override because for some weird reason selected date cells were higher than deselected once. */ @Override protected double computePrefHeight(double width) { double h = Math.max(counterLabel.prefHeight(-1), Math.max(dayOfMonthLabel.prefHeight(-1), Math.max(dayOfWeekLabel.prefHeight(-1), weekNumberLabel.prefHeight(-1)))); return h + getInsets().getTop() + getInsets().getBottom(); } @Override public void updateEntries(List> entries) { super.updateEntries(entries); int entryCount = 0; if (entries != null) { entryCount = entries.size(); } if (entryCount > 0) { counterLabel.setText(Integer.toString(entries.size())); counterLabel.setVisible(true); } else { counterLabel.setVisible(false); } counterLabel.getStyleClass().removeAll(USAGE_VERY_LOW, USAGE_LOW, USAGE_MEDIUM, USAGE_HIGH, USAGE_VERY_HIGH); final Callback usagePolicy = getView().getUsagePolicy(); switch (usagePolicy.call(entryCount)) { case NONE: break; case VERY_LOW: counterLabel.getStyleClass().add(USAGE_VERY_LOW); break; case LOW: counterLabel.getStyleClass().add(USAGE_LOW); break; case MEDIUM: counterLabel.getStyleClass().add(USAGE_MEDIUM); break; case HIGH: counterLabel.getStyleClass().add(USAGE_HIGH); break; case VERY_HIGH: default: counterLabel.getStyleClass().add(USAGE_VERY_HIGH); break; } } } /** * A date cell used to display the day of month, the day of week, and the week of year in * three separate labels. The styles used for these labels are "day-of-month", "day-of-week", * and "week-number". */ public static class SimpleDateCell extends DateCell { private static final String DAY_OF_MONTH_STYLE = "day-of-month-label"; private static final String DAY_OF_WEEK_STYLE = "day-of-week-label"; private static final String WEEK_NUMBER_STYLE = "week-number-label"; protected final Label dayOfMonthLabel = new Label(); protected final Label dayOfWeekLabel = new Label(); protected final Label weekNumberLabel = new Label(); /** * Constructs a new simple date cell. * * @param view the parent month sheet view * @param date the date shown by the cell (might be null for leading or trailing empty cells) */ public SimpleDateCell(MonthSheetView view, LocalDate date) { super(view, date); dayOfMonthLabel.getStyleClass().add(DAY_OF_MONTH_STYLE); dayOfWeekLabel.getStyleClass().add(DAY_OF_WEEK_STYLE); weekNumberLabel.getStyleClass().add(WEEK_NUMBER_STYLE); dayOfMonthLabel.setManaged(false); dayOfWeekLabel.setManaged(false); weekNumberLabel.setManaged(false); dayOfMonthLabel.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); dayOfWeekLabel.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); weekNumberLabel.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); dayOfMonthLabel.setMouseTransparent(!view.isEnableHyperlinks()); dayOfWeekLabel.setMouseTransparent(true); weekNumberLabel.setMouseTransparent(true); if (date != null) { String dayOfWeekName = date.getDayOfWeek().name().toLowerCase(); dayOfMonthLabel.getStyleClass().add(dayOfWeekName + "-label"); dayOfWeekLabel.getStyleClass().add(dayOfWeekName + "-label"); weekNumberLabel.getStyleClass().add(dayOfWeekName + "-label"); } getChildren().addAll(dayOfMonthLabel, dayOfWeekLabel, weekNumberLabel); setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); updateLabels(); if (view.isEnableHyperlinks()) { dayOfMonthLabel.getStyleClass().add("date-hyperlink"); dayOfMonthLabel.setOnMouseClicked(evt -> { if (evt.getButton() == MouseButton.PRIMARY && evt.getClickCount() == 1) { fireEvent(new RequestEvent(this, this, date)); } }); } } @Override protected void layoutChildren() { Insets insets = getInsets(); double top = insets.getTop(); double bottom = insets.getBottom(); double left = insets.getLeft(); double right = insets.getRight(); double w = getWidth(); double h = getHeight(); double availableHeight = h - top - bottom; double ps1 = dayOfMonthLabel.prefWidth(-1); double ps2 = dayOfWeekLabel.prefWidth(-1); double ps3 = weekNumberLabel.prefWidth(-1); dayOfMonthLabel.resizeRelocate(snapPosition(left), snapPosition(top), snapSize(ps1), snapSize(availableHeight)); dayOfWeekLabel.resizeRelocate(snapPosition(left + ps1), snapPosition(top), snapSize(ps2), snapSize(availableHeight)); weekNumberLabel.resizeRelocate(snapPosition(w - right - ps3), snapPosition(top), snapSize(ps3), snapSize(availableHeight)); } @Override protected double computePrefWidth(double height) { return dayOfMonthLabel.prefWidth(-1) + dayOfWeekLabel.prefWidth(-1) + weekNumberLabel.prefWidth(-1); } /* * Had to override because for some weird reason selected date cells were higher than deselected once. */ @Override protected double computePrefHeight(double width) { double h = Math.max(dayOfMonthLabel.prefHeight(-1), Math.max(dayOfWeekLabel.prefHeight(-1), weekNumberLabel.prefHeight(-1))); return h + getInsets().getTop() + getInsets().getBottom(); } private void updateLabels() { LocalDate date = getDate(); if (date == null) { dayOfMonthLabel.setText(""); dayOfWeekLabel.setText(""); weekNumberLabel.setText(""); } else { dayOfMonthLabel.setText(String.valueOf(date.getDayOfMonth())); dayOfWeekLabel.setText(date.getDayOfWeek().getDisplayName(TextStyle.SHORT, Locale.getDefault())); MonthSheetView view = getView(); if (view != null && view.isShowWeekNumber()) { WeekFields fields = view.getWeekFields(); DayOfWeek firstDayOfWeek = fields.getFirstDayOfWeek(); if (date.getDayOfWeek().equals(firstDayOfWeek)) { String weekNumber = Integer.toString(date.get(fields.weekOfYear())); weekNumberLabel.setText(weekNumber); } } } } } /** * A specialization of the {@link SimpleDateCell} that adds utilization information * to the cell by coloring its background differently based on the number of entries * found on that day. The cell uses the "usage policy" of the month sheet view to * determine whether the utilization is low or high. The styles used for visualizing * the different utilizations are "usage-very-low", "usage-low", "usage-medium", * "usage-high", and "usage-very-high". If utilization is zero then none of these styles * will be applied. * * @see DateControl#usagePolicyProperty() */ public static class UsageDateCell extends SimpleDateCell { /** * Constructs a new usage date cell. * * @param view the parent month sheet view * @param date the date shown by the cell (might be null for leading or trailing empty cells) */ public UsageDateCell(MonthSheetView view, LocalDate date) { super(view, date); } @Override public void updateEntries(List> entries) { getStyleClass().removeAll(USAGE_VERY_LOW, USAGE_LOW, USAGE_MEDIUM, USAGE_HIGH, USAGE_VERY_HIGH); int entryCount = 0; if (entries != null) { entryCount = entries.size(); } final Callback usagePolicy = getView().getUsagePolicy(); switch (usagePolicy.call(entryCount)) { case NONE: break; case VERY_LOW: getStyleClass().add(USAGE_VERY_LOW); break; case LOW: getStyleClass().add(USAGE_LOW); break; case MEDIUM: getStyleClass().add(USAGE_MEDIUM); break; case HIGH: getStyleClass().add(USAGE_HIGH); break; case VERY_HIGH: default: getStyleClass().add(USAGE_VERY_HIGH); break; } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy