
com.calendarfx.view.AgendaView Maven / Gradle / Ivy
/*
* 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.Entry;
import impl.com.calendarfx.view.AgendaViewSkin;
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.geometry.HPos;
import javafx.scene.Node;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Skin;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.shape.Circle;
import javafx.util.Callback;
import org.controlsfx.control.PropertySheet.Item;
import java.text.MessageFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import static java.util.Objects.requireNonNull;
/**
* The agenda view displays calendar entries in a list. The view can be
* configured to look back a given number of days and also to look forward a
* given number of days.
*
* 
*
*/
public class AgendaView extends DateControl {
private static final String DEFAULT_STYLE_CLASS = "agenda-view"; //$NON-NLS-1$
private static final String AGENDA_CATEGORY = "Agenda View"; //$NON-NLS-1$
private final ListView listView = new ListView<>();
private final ObjectProperty formatter = new SimpleObjectProperty<>(
this,
"formatter", //$NON-NLS-1$
DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG));
/**
* Constructs a new agenda view.
*/
public AgendaView() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
listView.setCellFactory(cbListView -> getCellFactory().call(this));
setContextMenu(buildContextMenu());
}
/**
* Returns the list view that will be used to display one cell for each day that
* contains at least one calendar entry.
*
* @return the list view used by this control
*/
public final ListView getListView() {
return listView;
}
@Override
protected Skin> createDefaultSkin() {
return new AgendaViewSkin(this);
}
private ContextMenu buildContextMenu() {
ContextMenu menu = new ContextMenu();
Menu lookBackMenu = new Menu(Messages.getString("AgendaView.MENU_ITEM_LOOK_BACK")); //$NON-NLS-1$
Menu lookAheadMenu = new Menu(Messages.getString("AgendaView.MENU_ITEM_LOOK_AHEAD")); //$NON-NLS-1$
String format = Messages.getString("AgendaView.MENU_ITEM_DAYS"); //$NON-NLS-1$
MenuItem lookBack0 = new MenuItem(MessageFormat.format(format, 0));
MenuItem lookBack10 = new MenuItem(MessageFormat.format(format, 10));
MenuItem lookBack20 = new MenuItem(MessageFormat.format(format, 20));
MenuItem lookBack30 = new MenuItem(MessageFormat.format(format, 30));
MenuItem lookAhead0 = new MenuItem(MessageFormat.format(format, 0));
MenuItem lookAhead10 = new MenuItem(MessageFormat.format(format, 10));
MenuItem lookAhead20 = new MenuItem(MessageFormat.format(format, 20));
MenuItem lookAhead30 = new MenuItem(MessageFormat.format(format, 30));
lookBackMenu.getItems().addAll(lookBack0, lookBack10, lookBack20, lookBack30);
lookAheadMenu.getItems().addAll(lookAhead0, lookAhead10, lookAhead20, lookAhead30);
menu.getItems().addAll(lookBackMenu, lookAheadMenu);
lookBack0.setOnAction(evt -> setLookBackPeriodInDays(0));
lookBack10.setOnAction(evt -> setLookBackPeriodInDays(10));
lookBack20.setOnAction(evt -> setLookBackPeriodInDays(20));
lookBack30.setOnAction(evt -> setLookBackPeriodInDays(30));
lookAhead0.setOnAction(evt -> setLookAheadPeriodInDays(0));
lookAhead10.setOnAction(evt -> setLookAheadPeriodInDays(10));
lookAhead20.setOnAction(evt -> setLookAheadPeriodInDays(20));
lookAhead30.setOnAction(evt -> setLookAheadPeriodInDays(30));
return menu;
}
private final IntegerProperty lookBackPeriodInDays = new SimpleIntegerProperty(this, "lookBackPeriodInDays", 0); //$NON-NLS-1$
/**
* Stores the number of days to "look back" into the past when loading data.
*
* @return the number of days to look back
*/
public final IntegerProperty lookBackPeriodInDaysProperty() {
return lookBackPeriodInDays;
}
/**
* Gets the value of {@link #lookBackPeriodInDaysProperty()}.
*
* @return the number of days to look back
*/
public final int getLookBackPeriodInDays() {
return lookBackPeriodInDaysProperty().get();
}
/**
* Sets the value of {@link #lookBackPeriodInDaysProperty()}.
*
* @param days
* the new number of days to look back
*/
public final void setLookBackPeriodInDays(int days) {
if (days < 0) {
throw new IllegalArgumentException("days must be larger than or equal to 0"); //$NON-NLS-1$
}
lookBackPeriodInDaysProperty().set(days);
}
private final IntegerProperty lookAheadPeriodInDays = new SimpleIntegerProperty(this, "lookAheadPeriodInDays", 30); //$NON-NLS-1$
/**
* Stores the number of days to "look ahead" into the future when loading
* its data.
*
* @return the number of days to "look ahead"
*/
public final IntegerProperty lookAheadPeriodInDaysProperty() {
return lookAheadPeriodInDays;
}
/**
* Returns the value of {@link #lookAheadPeriodInDaysProperty()}.
*
* @return the number of days to look ahead
*/
public final int getLookAheadPeriodInDays() {
return lookAheadPeriodInDaysProperty().get();
}
/**
* Sets the value of {@link #lookAheadPeriodInDaysProperty()}.
*
* @param days
* the number of days to look ahead
*/
public final void setLookAheadPeriodInDays(int days) {
if (days < 0) {
throw new IllegalArgumentException("days must be larger than or equal to 0"); //$NON-NLS-1$
}
lookAheadPeriodInDaysProperty().set(days);
}
private final BooleanProperty showStatusLabel = new SimpleBooleanProperty(this, "showStatusLabel", true);
public final BooleanProperty showStatusLabelProperty() {
return showStatusLabel;
}
public final boolean isShowStatusLabel() {
return showStatusLabelProperty().get();
}
public final void setShowStatusLabel(boolean showStatusLabel) {
showStatusLabelProperty().set(showStatusLabel);
}
private final ObjectProperty> cellFactory = new SimpleObjectProperty>(this, "cellFactory", view -> new AgendaEntryCell(this)) {
@Override
public void set(Callback newValue) {
super.set(Objects.requireNonNull(newValue));
}
};
public final ObjectProperty> cellFactoryProperty() {
return cellFactory;
}
public final Callback getCellFactory() {
return cellFactoryProperty().get();
}
public final void setCellFactory(Callback cellFactory) {
cellFactoryProperty().set(cellFactory);
}
/**
* Gets the DateTimeFormatter property, which is use to provide the format on the TimeScale Labels. By default it
* has a value of {@link FormatStyle#LONG}
*
* @return the date formatter.
*/
public final ObjectProperty dateTimeFormatterProperty() {
return formatter;
}
/**
* Returns the value of {@link #dateTimeFormatterProperty()}
*
* @return a date time formatter
*/
public final DateTimeFormatter getDateTimeFormatter() {
return dateTimeFormatterProperty().get();
}
/**
* Sets the value of {@link #dateTimeFormatterProperty()}
*
* @param formatter a date time formatter, not {@code null}
*/
public final void setDateTimeFormatter(DateTimeFormatter formatter) {
requireNonNull(formatter);
dateTimeFormatterProperty().set(formatter);
}
/**
* Agenda entries are model objects that reference a collection of calendar
* entries for a specific date.
*/
public static class AgendaEntry implements Comparable {
private LocalDate date;
public AgendaEntry(LocalDate date) {
this.date = requireNonNull(date);
}
public LocalDate getDate() {
return date;
}
private final List> entries = new ArrayList<>();
public final List> getEntries() {
return entries;
}
@Override
public int compareTo(AgendaEntry o) {
return getDate().compareTo(o.getDate());
}
}
/**
* A specialized list cell that is capable of displaying all entries currently assigned
* to a given day. Each cell features a header that shows the date information and a body
* that lists all entries. Each entry is visualized with an icon, a title text, and a
* time text.
*
* @see AgendaView#getListView()
* @see ListView#setCellFactory(javafx.util.Callback)
*/
public static class AgendaEntryCell extends ListCell {
private static final String AGENDA_VIEW_LIST_CELL = "agenda-view-list-cell"; //$NON-NLS-1$
private static final String AGENDA_VIEW_TIME_LABEL = "time-label"; //$NON-NLS-1$
private static final String AGENDA_VIEW_TITLE_LABEL = "title-label"; //$NON-NLS-1$
private static final String AGENDA_VIEW_BODY = "body"; //$NON-NLS-1$
private static final String AGENDA_VIEW_DATE_LABEL = "date-label"; //$NON-NLS-1$
private static final String AGENDA_VIEW_DATE_LABEL_TODAY = "today"; //$NON-NLS-1$
private static final String AGENDA_VIEW_WEEKDAY_LABEL = "weekday-label"; //$NON-NLS-1$
private static final String AGENDA_VIEW_WEEKDAY_LABEL_TODAY = "today"; //$NON-NLS-1$
private static final String AGENDA_VIEW_HEADER = "header"; //$NON-NLS-1$
private static final String AGENDA_VIEW_HEADER_TODAY = "today"; //$NON-NLS-1$
private static final String AGENDA_VIEW_BODY_SEPARATOR = "separator";
private DateTimeFormatter weekdayFormatter = DateTimeFormatter.ofPattern(Messages.getString("AgendaEntryCell.WEEKDAY_FORMAT")); //$NON-NLS-1$
private DateTimeFormatter mediumDateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
private DateTimeFormatter shortDateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT);
private DateTimeFormatter timeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
private Label weekdayLabel;
private Label dateLabel;
private GridPane gridPane;
private BorderPane headerPane;
private boolean headerPaneVisible;
private final AgendaView agendaView;
/**
* Constructs a new cell that will work with the given agenda view.
*
* @param view the parent list view
*/
public AgendaEntryCell(AgendaView view) {
this(view, true);
}
/**
* Constructs a new cell that will work with the given agenda view.
*
* @param view the parent list view
* @param headerPaneVisible flag to control the visibility of the cell's header.
*/
public AgendaEntryCell(AgendaView view, boolean headerPaneVisible) {
this.agendaView = Objects.requireNonNull(view);
this.headerPaneVisible = headerPaneVisible;
BorderPane borderPane = new BorderPane();
borderPane.getStyleClass().add("container");
borderPane.setTop(createHeader());
borderPane.setCenter(createBody());
setGraphic(borderPane);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
getStyleClass().add(AGENDA_VIEW_LIST_CELL);
}
/**
* Creates the node used for the body part of each cell.
*
* In this default implementation the body consists of a grid pane with
* three columns. The middle column is used for showing the title of
* calendar entries. This column will get whatever space is left after
* the icon and the time column have used what they need. This means
* that a very long title will automatically be truncated.
*
* @return the body node
*/
protected Node createBody() {
// icon column
ColumnConstraints iconColumn = new ColumnConstraints();
// title column
ColumnConstraints descriptionColumn = new ColumnConstraints();
descriptionColumn.setFillWidth(true);
descriptionColumn.setHgrow(Priority.SOMETIMES);
descriptionColumn.setMinWidth(0);
descriptionColumn.setPrefWidth(0);
// time column
ColumnConstraints timeColumn = new ColumnConstraints();
timeColumn.setHalignment(HPos.RIGHT);
gridPane = new GridPane();
gridPane.setGridLinesVisible(true);
gridPane.setMinWidth(0);
gridPane.setPrefWidth(0);
gridPane.getStyleClass().add(AGENDA_VIEW_BODY);
gridPane.getColumnConstraints().addAll(iconColumn, descriptionColumn, timeColumn);
return gridPane;
}
/**
* Creates the header part for each cell. The header consists of a border pane
* with the weekday label in the "left" position and the date label in the "right"
* position.
*
* @return the header node
*/
protected Node createHeader() {
headerPane = new BorderPane();
headerPane.getStyleClass().add(AGENDA_VIEW_HEADER);
headerPane.setVisible(headerPaneVisible);
headerPane.managedProperty().bind(headerPane.visibleProperty());
weekdayLabel = new Label();
weekdayLabel.getStyleClass().add(AGENDA_VIEW_WEEKDAY_LABEL);
weekdayLabel.setMinWidth(0);
dateLabel = new Label();
dateLabel.setMinWidth(0);
dateLabel.getStyleClass().add(AGENDA_VIEW_DATE_LABEL);
headerPane.setLeft(weekdayLabel);
headerPane.setRight(dateLabel);
return headerPane;
}
@Override
protected void updateItem(AgendaEntry item, boolean empty) {
super.updateItem(item, empty);
gridPane.getChildren().clear();
if (item != null) {
LocalDate date = item.getDate();
if (date.equals(agendaView.getToday())) {
if (!headerPane.getStyleClass().contains(AGENDA_VIEW_HEADER_TODAY)) {
headerPane.getStyleClass().add(AGENDA_VIEW_HEADER_TODAY);
dateLabel.getStyleClass().add(AGENDA_VIEW_DATE_LABEL_TODAY);
weekdayLabel.getStyleClass().add(AGENDA_VIEW_WEEKDAY_LABEL_TODAY);
}
} else {
headerPane.getStyleClass().remove(AGENDA_VIEW_HEADER_TODAY);
dateLabel.getStyleClass().remove(AGENDA_VIEW_DATE_LABEL_TODAY);
weekdayLabel.getStyleClass().remove(AGENDA_VIEW_WEEKDAY_LABEL_TODAY);
}
dateLabel.setText(mediumDateFormatter.format(date));
weekdayLabel.setText(weekdayFormatter.format(date));
int count = item.getEntries().size();
int row = 0;
for (int i = 0; i < count; i++) {
Entry> entry = item.getEntries().get(i);
Node entryGraphic = createEntryGraphic(entry);
gridPane.add(entryGraphic, 0, row);
Label titleLabel = createEntryTitleLabel(entry);
titleLabel.setMinWidth(0);
gridPane.add(titleLabel, 1, row);
Label timeLabel = createEntryTimeLabel(entry);
timeLabel.setMinWidth(0);
gridPane.add(timeLabel, 2, row);
if (count > 1 && i < count - 1) {
Region separator = new Region();
separator.getStyleClass().add(AGENDA_VIEW_BODY_SEPARATOR); //$NON-NLS-1$
row++;
gridPane.add(separator, 0, row);
GridPane.setColumnSpan(separator, 3);
GridPane.setFillWidth(separator, true);
}
row++;
}
getGraphic().setVisible(true);
} else {
getGraphic().setVisible(false);
}
}
/**
* Creates the label used to display the time of the entry. The default implementation of this
* method creates a label and sets the text returned by {@link #getTimeText(Entry)}.
*
* @param entry the entry for which the time will be displayed
* @return a label for displaying the time information on the entry
*/
protected Label createEntryTimeLabel(Entry> entry) {
Label timeLabel = new Label(getTimeText(entry));
timeLabel.getStyleClass().add(AGENDA_VIEW_TIME_LABEL);
if (agendaView.isEnableHyperlinks()) {
timeLabel.setOnMouseClicked(evt -> fireEvent(new RequestEvent(this, this, entry)));
timeLabel.getStyleClass().add("date-hyperlink");
}
return timeLabel;
}
/**
* Creates the label used to display the title of the entry. The default implementation of this
* method creates a label and sets the text found in {@link Entry#getTitle()}.
*
* @param entry the entry for which the title will be displayed
* @return a label for displaying the title of the entry
*/
protected Label createEntryTitleLabel(Entry> entry) {
Label titleLabel = new Label(entry.getTitle());
titleLabel.getStyleClass().add(AGENDA_VIEW_TITLE_LABEL);
if (agendaView.isEnableHyperlinks()) {
titleLabel.setOnMouseClicked(evt -> fireEvent(new RequestEvent(this, this, entry)));
titleLabel.getStyleClass().add("date-hyperlink");
}
return titleLabel;
}
/**
* Creates a node used to display an icon for the entry. The default implementation of this method
* creates a node of type {@link Circle}. The color of the circle will match the color of
* the calendar to which the entry belongs.
*
* Circle circle = new Circle(4);
* circle.getStyleClass().add(entry.getCalendar().getStyle() + "-icon"); //$NON-NLS-1$
*
*
* @param entry the entry for which the icon will be displayed
* @return a node for displaying a graphic for the entry
*/
protected Node createEntryGraphic(Entry> entry) {
Circle circle = new Circle(4);
circle.getStyleClass().add(entry.getCalendar().getStyle() + "-icon"); //$NON-NLS-1$
return circle;
}
/**
* Creates a nicely formatted text that contains the start and end time of
* the given entry. The text can also be something like "full day" if the entry
* is a full-day entry.
*
* @param entry the entry for which the text will be created
* @return a text showing the start and end times of the entry
*/
protected String getTimeText(Entry> entry) {
if (entry.isFullDay()) {
return Messages.getString("AgendaEntryCell.ALL_DAY");//$NON-NLS-1$
}
LocalDate startDate = entry.getStartDate();
LocalDate endDate = entry.getEndDate();
String text;
if (startDate.equals(endDate)) {
text = MessageFormat.format(Messages.getString("AgendaEntryCell.ENTRY_TIME_RANGE"), //$NON-NLS-1$
timeFormatter.format(entry.getStartTime()), timeFormatter.format(entry.getEndTime()));
} else {
text = MessageFormat.format(Messages.getString("AgendaEntryCell.ENTRY_TIME_RANGE_WITH_DATE"), //$NON-NLS-1$
shortDateFormatter.format(entry.getStartDate()), timeFormatter.format(entry.getStartTime()), shortDateFormatter.format(entry.getEndDate()),
timeFormatter.format(entry.getEndTime()));
}
return text;
}
/**
* Sets the Week Formatter, the value by default is 'EEEE' Format.
* @param weekdayFormatter sets the week date time format.
*/
public void setWeekdayFormatter(DateTimeFormatter weekdayFormatter){
this.weekdayFormatter = weekdayFormatter;
}
/**
* Sets the Medium Date Formatter, the value by default is {@link FormatStyle#MEDIUM}.
* Is used to set a format text on the Date Label.
* @param mediumDateFormatter sets medium date time format.
*/
public void setMediumDateFormatter(DateTimeFormatter mediumDateFormatter){
this.mediumDateFormatter = mediumDateFormatter;
}
/**
* Sets the Short Date Formatter, the value by default is {@link FormatStyle#SHORT}.
* Is be used to set a Date format text in {@link #getTimeText(Entry)}
* @param shortDateFormatter sets the short date time format.
*/
public void setShortDateFormatter(DateTimeFormatter shortDateFormatter){
this.shortDateFormatter = shortDateFormatter;
}
/**
* Sets the Time Formatter, the value by default is {@link FormatStyle#SHORT}.
* Is used to set a Time format text in {@link #getTimeText(Entry)}
* @param timeFormatter sets the time format.
*/
public void setTimeFormatter(DateTimeFormatter timeFormatter){
this.timeFormatter = timeFormatter;
}
}
@Override
public ObservableList- getPropertySheetItems() {
ObservableList
- items = super.getPropertySheetItems();
items.add(new Item() {
@Override
public Optional
> getObservableValue() {
return Optional.of(lookAheadPeriodInDaysProperty());
}
@Override
public void setValue(Object value) {
setLookAheadPeriodInDays((int) value);
}
@Override
public Object getValue() {
return getLookAheadPeriodInDays();
}
@Override
public Class> getType() {
return Integer.class;
}
@Override
public String getName() {
return "Look Ahead Period"; //$NON-NLS-1$
}
@Override
public String getDescription() {
return "Look ahead period in days"; //$NON-NLS-1$
}
@Override
public String getCategory() {
return AGENDA_CATEGORY;
}
});
items.add(new Item() {
@Override
public Optional> getObservableValue() {
return Optional.of(lookBackPeriodInDaysProperty());
}
@Override
public void setValue(Object value) {
setLookBackPeriodInDays((int) value);
}
@Override
public Object getValue() {
return getLookBackPeriodInDays();
}
@Override
public Class> getType() {
return Integer.class;
}
@Override
public String getName() {
return "Look Back Period"; //$NON-NLS-1$
}
@Override
public String getDescription() {
return "Look back period in days"; //$NON-NLS-1$
}
@Override
public String getCategory() {
return AGENDA_CATEGORY;
}
});
items.add(new Item() {
@Override
public Optional> getObservableValue() {
return Optional.of(showStatusLabelProperty());
}
@Override
public void setValue(Object value) {
setShowStatusLabel((boolean) value);
}
@Override
public Object getValue() {
return isShowStatusLabel();
}
@Override
public Class> getType() {
return Boolean.class;
}
@Override
public String getName() {
return "Show Status Label"; //$NON-NLS-1$
}
@Override
public String getDescription() {
return "Show Status Label"; //$NON-NLS-1$
}
@Override
public String getCategory() {
return AGENDA_CATEGORY;
}
});
return items;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy