handler = evt -> foo(evt);
calendar.addEventHandler(handler);
----
=== Load Events
Load events are used by the framework to signal to the application that the UI requires data for a specific time interval. This
can be very useful for implementing a lazy loading strategy. If the user switches from one month to another then an event of this
type will be fired and the time bounds on this event will be the first and the last day of that month. The `LoadEvent` type only
supports a single event type called `LOAD`.
Listeners for this event type can be registered on any date control:
[source,java,linenums]
----
DayView view = new DayView();
view.addEventHandler(LoadEvent.LOAD, evt -> foo(evt));
----
=== Request Events
A unique event class is `RequestEvent`. It is used by the controls of the framework to signal to other framework controls that
the user wants to "jump" to another view. For example: the user clicks on the date shown for a day in the `MonthView` then the month view will fire a request event that informs the framework that the user wants to switch to the `DayView` to see more detail for that day.
== DateControl
A calendar user interface hardly ever consists of just a single control. They are composed of several views, some showing a single day or a week or
a month. In _CalendarFX_ the `CalendarView` control consists of dedicated "pages" for a day, a week, a month, or a full year. Each one of these pages consists of one or more subtypes of DateControl. The following image shows a simplified view of the scene graph / the containment hierarchy.
[.thumb]
image::hierarchy.png[Hierarchy View,align="center"]
To make all of these controls work together in harmony it is important that they share many properties. This is accomplished by JavaFX property binding. The class `DateControl` features a method called "bind" that ensures the dates and times shown by the controls are synchronized. But also that many of the customization features (e.g. node factories) are shared.
The following listing shows the implementation of the `DateControl.bind()` method to give you an idea how much is bound within _CalendarFX_.
[source,java,linenums]
----
public final void bind(DateControl otherControl, boolean bindDate) {
// bind lists
Bindings.bindContentBidirectional(otherControl.getCalendarSources(),
getCalendarSources());
Bindings.bindContentBidirectional(otherControl.getSelections(),
getSelections());
Bindings.bindContentBidirectional(otherControl.getWeekendDays(),
getWeekendDays());
// bind properties
Bindings.bindBidirectional(otherControl.entryFactoryProperty(),
entryFactoryProperty());
Bindings.bindBidirectional(otherControl.defaultCalendarProviderProperty(),
defaultCalendarProviderProperty());
Bindings.bindBidirectional(otherControl.virtualGridProperty(),
virtualGridProperty());
Bindings.bindBidirectional(otherControl.draggedEntryProperty(),
draggedEntryProperty());
Bindings.bindBidirectional(otherControl.requestedTimeProperty(),
requestedTimeProperty());
Bindings.bindBidirectional(otherControl.selectionModeProperty(),
selectionModeProperty());
Bindings.bindBidirectional(otherControl.selectionModeProperty(),
selectionModeProperty());
Bindings.bindBidirectional(otherControl.weekFieldsProperty(),
weekFieldsProperty());
Bindings.bindBidirectional(otherControl.layoutProperty(),
layoutProperty());
if (bindDate) {
Bindings.bindBidirectional(otherControl.dateProperty(), dateProperty());
}
Bindings.bindBidirectional(otherControl.todayProperty(),
todayProperty());
Bindings.bindBidirectional(otherControl.zoneIdProperty(),
zoneIdProperty());
// edit callbacks
Bindings.bindBidirectional(
otherControl.entryDetailsCallbackProperty(),
entryDetailsCallbackProperty());
Bindings.bindBidirectional(
otherControl.dateDetailsCallbackProperty(),
dateDetailsCallbackProperty());
Bindings.bindBidirectional(
otherControl.contextMenuCallbackProperty(),
contextMenuCallbackProperty());
Bindings.bindBidirectional(
otherControl.entryContextMenuCallbackProperty(),
entryContextMenuCallbackProperty());
Bindings.bindBidirectional(
otherControl.calendarSourceFactoryProperty(),
calendarSourceFactoryProperty());
Bindings.bindBidirectional(
otherControl.entryDetailsPopOverContentCallbackProperty(),
entryDetailsPopOverContentCallbackProperty());
}
----
=== Class Hierarchy
_CalendarFX_ ships with many built-in views for displaying calendar information. All of these views inherit from `DateControl`. The class
hierarchy can be seen in the following image:
image::datecontrol.png[Class Hierarchy,align="center"]
=== Current Date, Time, and Today
Each `DateControl` keeps track of the "current date" and "today". The current date is the date that the control is supposed to display to the user.
"Today" is the date that the control assumes to be the actual date. "Today" defaults to the current system date (provided by the operating system), but
it can be any date.
[IMPORTANT]
.Updating today and current time
====
The "today" and "time" properties do not get updated by themselves. See the daemon thread created in the listing shown in the "Quick Start" section.
====
`DateControl` defines utility methods that allow for easy modification of the "current" date.
[source,java,linenums]
----
public void goToday();
public void goForward();
public void goBack();
----
=== Adding Calendars / Sources
Even though the `DateControl` class provides a `getCalendars()` method this is not the place where calendars are being added. Instead,
always create calendar sources, add calendars to them, and then add the sources to the control. The "calendars" list is a read-only
flat list representation of all calendars in all calendar sources. The "calendars" list gets updated by the framework.
[source,java,linenums]
.Adding Calendars
----
Calendar katja = new Calendar("Katja");
Calendar dirk = new Calendar("Dirk");
CalendarSource familyCalendarSource = new CalendarSource("Family");
familyCalendarSource.getCalendars().addAll(katja, dirk);
CalendarView calendarView = new CalendarView();
calendarView.getCalendarSources().setAll(familyCalendarSource);
----
=== Customizing or Replacing the PopOver
The `DateControl` class has built-in support for displaying a `PopOver` control when the user double-clicks on a calendar entry. The content node of this `PopOver` can be replaced. It is normally used to show some basic entry details (e.g. start / end date, title, event location) but applications might have defined specialized entries with custom properties that require additional UI elements. This can be accomplished by the help of the `PopOver` content node factory.
[source,java,linenums]
.PopOver Content Node Factory
----
CalendarView calendarView = new CalendarView();
calendarView.setEntryDetailsPopOverContentCallback(param -> new MyCustomPopOverContentNode());
----
If an application does not want to use the `PopOver` at all but instead display a standard dialog then there is a way of doing that, too. Simply
register an entry details callback.
[source,java,linenums]
.Entry Details Callback
----
CalendarView calendarView = new CalendarView();
calendarView.setEntryDetailsCallback(param -> new MyCustomEntryDialog());
----
These two callbacks normally work hand in hand. The default implementation of the entry details callback is producing a `PopOver` and sets the content
node on the PopOver via the help of the content node callback.
=== Context Menu Support
A common place for customization are context menus. The `DateControl` class produces a context menu via specialized callbacks. One callback is used
to produce a menu for a given calendar entry, the second callback is used when the user triggers the context menu by clicking in the background
of a `DateControl`.
[source,java,linenums]
.PopOver Content Node Factory
----
CalendarView calendarView = new CalendarView();
calendarView.setEntryContextMenuCallback(param -> new MyEntryContextMenu());
calendarView.setContextMenuCallback(param -> new MyContextMenu());
----
[IMPORTANT]
.Context Menus
====
The context menu callbacks are automatically shared among all date controls that are bound to each other. The same context menu code will execute for
different views, the `DayView`, the `MonthView`, and so on. This means that the code that builds the context menu will need to check the parameter object
that was passed to the callback to configure itself appropriately.
The same is true for basically all callbacks used by the DateControl.
====
=== Creating Entries
The user can create new entries by double-clicking anywhere inside a `DateControl`. The actual work of creating a new entry instance is then delegated to a specialized entry factory that can be set on `DateControl`.
[source,java,linenums]
.Entry Factory
----
CalendarView calendarView = new CalendarView();
calendarView.setEntryFactory(param -> new MyEntryFactory());
----
Once the entry factory has returned the new entry it will be added to the calendar that is being returned by the "default calendar" provider. This provider is also customizable via a callback.
[source,java,linenums]
.Default Calendar Provider
----
CalendarView calendarView = new CalendarView();
calendarView.setDefaultCalendarProvider(param -> new MyDefaultCalendarProvider());
----
Besides the double click creation the application can also programmatically request the DateControl to create a new entry at a given point in time. Two methods are available for this: createEntryAt(ZonedDateTime) and createEntryAt(ZonedDateTime, Calendar). The second method will ensure that the entry will be added to the given calendar while the first method will invoke the default calendar provider.
=== Creating Calendar Sources
The user might also wish to add another calendar source to the application. In this case the DateControl will invoke the calendar source factory. The default implementation of this factory does nothing more than to create a new instance of the standard CalendarSource class. Applications are free to return a specialization of CalendarSource instead (e.g. GoogleCalendarAccount). A custom factory might even prompt the user first with a dialog, e.g. to request user credentials.
[source,java,linenums]
.Default Calendar Provider
----
CalendarView calendarView = new CalendarView();
calendarView.setCalendarSourceFactory(param -> new MyCalendarSource());
----
The calendar source factory gets invoked when the method `DateControl.createCalendarSource()` gets invoked. The `CalendarView` class already provides a button
in its toolbar that will call this method.
== Entry Views
Entry views are JavaFX nodes that are representing calendar entries. There are several types, all extending `EntryViewBase`:
Day Entry View:: Shown inside a `DayView` or `WeekDayView` control. These views can be customized by subclassing `DayEntryViewSkin` and overriding the `createContent()` method.
All Day Entry View:: Shown inside the `AllDayView` control.
Month Entry View:: Shown inside the `MonthView` control.
== Standard Calendar Views
The most fundamental views inside _CalendarFX_ are of course the views used to display a day (24 hours), an entire week, a month, and a year.
DayView:: Shows a 24-hour time period vertically. The control has several options that can be used to influence the layout of the hours. E.g.: it is possible to define hour ranges where the time will be compressed in order to save space on the screen (early and late hours are often not relevant). The view can also specify whether it wants to always show a fixed number of hours or a fixed height for each hour.
[.thumb]
image::day-view.png[Day View,align="center"]
DetailedDayView:: wraps the `DayView` control with several additional controls: an `AllDayView`, a `TimeScaleView`, a `CalendarHeaderView`, a `ScrollBar` and an (optional)
`AgendaView`.
[.thumb]
image::detailed-day-view-agenda.png[Detailed Day View,align="center"]
WeekView:: The name of this control is somewhat misleading, because it can show any number of `WeekDayView` instances, not just 5 or 7 but also 14 (two weeks) or 21 (three weeks). In this view entries can be easily edited to span multiple days.
[.thumb]
image::week-view.png[Week View,align="center"]
DetailedWeekView:: same concept as the `DetailedDayView`. This view wraps the `WeekView` and adds several other controls.
[.thumb]
image::detailed-week-view.png[Detailed Week View,align="center"]
MonthView:: Shows up to 31 days for the current month plus some days of the previous and the next month.
[.thumb]
image::month-view.png[Month View,align="center"]
MonthSheetView:: Shows several months in a column layout. Weekdays can be aligned so that the same weekdays are always next to each other. A customizable
cell factory is used to create the date cells. Several default implementations are included in _CalendarFX_: simple date cell, usage date cell, badge date cell,
detail date cell.
[.thumb]
image::month-sheet-view.png[Month Sheet View,align="center"]
[.thumb]
image::month-sheet-view-aligned.png[Month Sheet View Aligned,align="center"]
YearView:: Shows twelve `YearMonthView` instances.
[.thumb]
image::year-view.png[Year View,align="center"]
YearMonthView:: Sort of a date picker control. 12 instances of this control are used to build up the `YearPage` control. This control provides many properties for easy
customization. The month label, the year label, and the arrow buttons can be hidden. A cell factory can be set to customize the appearance of each day, and so on.
[.thumb]
image::date-picker.png[Year Month View,align="center"]
AllDayView:: Just like the `WeekView` this control can also span multiple days. It is being used as a header for the `DayView` inside the `DayPage` and also for the `WeekView` inside the `WeekPage`. The control displays calendar entries that have their "full day" property set to true.
[.thumb]
image::all-day-view.png[All Day View,align="center"]
CalendarHeaderView:: Displays the names of all currently visible calendars, but only when the `DateControl` has its layout set to `SWIMLANE` and not to `STANDARD`.
[.thumb]
image::calendar-header-view.png[Calendar Header View,align="center"]
== Calendar Pages
Calendar pages are complex controls that are composed of several controls, many of them `DateControl` instances. All pages provide controls to navigate to different
dates or to quickly jump to "Today". Each page also shows a title with the current date shown. The `CalendarView` class manages one instance of each page type to let the
user switch from a day, to a week, to a month, to a year.
DayPage:: Shows an `AgendaView`, a `DetailedDayView`, and a `YearMonthView`. This page is designed to give the user a quick overview of what is going on today and
in the near future (agenda).
[.thumb]
image::day-page.png[Day Page,align="center",border="1"]
WeekPage:: Composed of a `DetailedWeekView`.
[.thumb]
image::week-page.png[Week Page,align="center"]
MonthPage:: Shows a single `MonthView` control.
[.thumb]
image::month-page.png[Month Page,align="center"]
YearPage:: Shows a `YearView` with twelve `YearMonthView` sub-controls. Alternatively can switch to a `MonthSheetView`.
[.thumb]
image::year-page.png[Year Page using YearView,align="center"]
[.thumb]
image::year-page-2.png[Year Page using MonthSheetView,align="center"]
== Resource Scheduling Views
Another category of views is used for scheduling resource allocations. These are commonly used by scheduling software, e.g. a customer appointment application for a garage, a hairdresser, and so on. The
=== ResourcesView
The class `ResourcesView` displays one or more days for one or more resources. The view can either display one or more resources for a given
day (`ResourceView.Type.RESOURCES_OVER_DATES`) or one or more days for a given resource (`ResourceView.Type.DATES_OVER_RESOURCE`). Each one of
these options can be configured to show one or more dates and one or more resources.
This screenshot shows the resources view when the type of the view has been set to "resources over dates".
[.thumb]
image::resources-view-resources-over-dates.png[ResourcesView - Resources over Dates,align="center"]
The next screenshot shows the resources view when the type of the view has been set to "dates over resources".
[.thumb]
image::resources-view-dates-over-resources.png[esourcesView - Dates over Resources,align="center"]
By default, the calendar entries will become semi-transparent when the user switches to the "edit availability" mode. This behaviour can be configured so that either the entries stay completely visible or they are completely hidden. The following screenshot shows the situation where the user is editing the resources' availability and the already existing calendar entries become semi-transparent.
[.thumb]
image::resources-view-availability.png[esourcesView - Availability Editing,align="center"]
== Developer Console
_CalendarFX_ supports a special system property called `calendarfx.developer`. If this property is set to `true` then a developer console is being added to the skin of `CalendarView`. The console can be made visible by pressing `SHORTCUT-D`. The console is a standard _CalendarFX_ control and you can also add it directly to your application for development purposes.
[.thumb]
image::developer-console.png[Developer Console,align="center"]
== Logging
_CalendarFX_ uses the standard java logging api for its logging. The logging settings and the available loggers can be found inside the file `logging.properties`. _CalendarFX_ uses domains for logging and not packages or classes. Several domains are available: view, model, editing, recurrence, etc...
== Internationalization (i18n)
The default resource bundle of _CalendarFX_ is English. Additional bundles include German, Spanish, French, Italian, Portuguese (Brazil), and Czech.
All can be found in the distribution (misc/messages.properties, misc/messages_de.properties, etc...). Please submit a pull request to add another
language to _CalendarFX_.
== Known Issues
* There is currently no support for defining exceptions for recurrence rules. In most calendar applications, when the user edits a recurrent entry, the user will be asked whether he wants to change just this one recurrence or the whole series. This feature is currently not supported but will be in one of the next releases.
* In `SwimLane` layout it would be nice if the user could drag an entry horizontally from one column / calendar to another. This is currently not supported. We will investigate if this can be added in one of the next releases.
================================================
FILE: CalendarFXView/src/main/java/com/calendarfx/model/Calendar.java
================================================
/*
* 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.model;
import com.calendarfx.view.DateControl;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.event.EventDispatchChain;
import javafx.event.EventHandler;
import javafx.event.EventTarget;
import net.fortuna.ical4j.model.Recur;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import static com.calendarfx.model.CalendarEvent.CALENDAR_CHANGED;
import static com.calendarfx.model.CalendarEvent.ENTRY_CHANGED;
import static com.calendarfx.util.LoggingDomain.MODEL;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.FINER;
/**
* A calendar is responsible for storing calendar entries. It provides methods
* for adding, removing, and querying entries. A calendar also defines a visual
* style / color theme that will be used throughout the UI controls. Calendars
* fire events whenever entries are added or removed. Calendars are grouped together
* inside a {@link CalendarSource}. These calendar sources are then added to
* {@link DateControl#getCalendarSources()}.
*
* Example
*
* {@code
* // create the calendar and listen to all changes
* Calendar calendar = new Calendar("Home");
* calendar.addEventHandler(CalendarEvent.ANY, evt -> handleEvent(evt));
*
* // create the calendar source and attach the calendar
* CalendarSource source = new CalendarSource("Online Calendars");
* source.getCalendars().add(calendar);
*
* // attach the source to the date control / calendar view.
* CalendarView view = new CalendarView();
* view.getCalendarSources().add(source);
* }
*
*
* @param the type of the (optional) user object
*/
public class Calendar implements EventTarget {
/**
* Predefined visual styles for calendars. The actual CSS settings for these
* styles can be found in the framework stylesheet, prefixed with "style1-",
* "style2-", etc. The picture below shows the colors used for the various
* styles.
*
*
*
* @see Calendar#setStyle(Style)
*/
public enum Style {
/**
* Default style "1".
*/
STYLE1,
/**
* Default style "2".
*/
STYLE2,
/**
* Default style "3".
*/
STYLE3,
/**
* Default style "4".
*/
STYLE4,
/**
* Default style "5".
*/
STYLE5,
/**
* Default style "6".
*/
STYLE6,
/**
* Default style "7".
*/
STYLE7;
/**
* Returns a style for the given ordinal. This method is implemented
* with a roll over strategy: the final ordinal value is the given
* ordinal value modulo the number of elements in this enum.
*
* @param ordinal the ordinal value for which to return a style
* @return a style, guaranteed to be non-null
*/
public static Style getStyle(int ordinal) {
return Style.values()[ordinal % Style.values().length];
}
}
private final IntervalTree> intervalTree = new IntervalTree<>();
/**
* Constructs a new calendar.
*/
public Calendar() {
addEventHandler(evt -> {
Entry> entry = evt.getEntry();
if (evt.getEventType().getSuperType().equals(ENTRY_CHANGED) && entry.isRecurrence()) {
updateRecurrenceSourceEntry(evt, entry.getRecurrenceSourceEntry());
}
});
}
/**
* Constructs a new calendar with the given name.
*
* @param name the name of the calendar
* @param userObject an optional user object
*/
public Calendar(String name, T userObject) {
this();
setName(name);
if (name != null) {
setShortName(!name.isEmpty() ? name.substring(0, 1) : "");
}
setUserObject(userObject);
}
/**
* Constructs a new calendar with the given name.
*
* @param name the name of the calendar
*/
public Calendar(String name) {
this(name, null);
}
@SuppressWarnings({"rawtypes", "unchecked"})
private void updateRecurrenceSourceEntry(CalendarEvent evt, Entry source) {
Entry recurrence = evt.getEntry();
if (evt.getEventType().equals(CalendarEvent.ENTRY_INTERVAL_CHANGED)) {
Interval oldInterval = evt.getOldInterval();
Interval newInterval = calculateSourceBoundsFromRecurrenceBounds(source, recurrence, oldInterval);
source.setInterval(newInterval);
} else if (evt.getEventType().equals(CalendarEvent.ENTRY_LOCATION_CHANGED)) {
source.setLocation(recurrence.getLocation());
} else if (evt.getEventType().equals(CalendarEvent.ENTRY_RECURRENCE_RULE_CHANGED)) {
source.setRecurrenceRule(recurrence.getRecurrenceRule());
} else if (evt.getEventType().equals(CalendarEvent.ENTRY_TITLE_CHANGED)) {
source.setTitle(recurrence.getTitle());
} else if (evt.getEventType().equals(CalendarEvent.ENTRY_USER_OBJECT_CHANGED)) {
source.setUserObject(recurrence.getUserObject());
} else if (evt.getEventType().equals(CalendarEvent.ENTRY_CALENDAR_CHANGED)) {
source.setCalendar(recurrence.getCalendar());
} else if (evt.getEventType().equals(CalendarEvent.ENTRY_FULL_DAY_CHANGED)) {
source.setFullDay(recurrence.isFullDay());
}
}
private Interval calculateSourceBoundsFromRecurrenceBounds(Entry> source, Entry> recurrence, Interval oldInterval) {
ZonedDateTime recurrenceStart = recurrence.getStartAsZonedDateTime();
ZonedDateTime recurrenceEnd = recurrence.getEndAsZonedDateTime();
Duration startDelta = Duration.between(oldInterval.getStartZonedDateTime(), recurrenceStart);
Duration endDelta = Duration.between(oldInterval.getEndZonedDateTime(), recurrenceEnd);
ZonedDateTime sourceStart = source.getStartAsZonedDateTime();
ZonedDateTime sourceEnd = source.getEndAsZonedDateTime();
sourceStart = sourceStart.plus(startDelta);
sourceEnd = sourceEnd.plus(endDelta);
return new Interval(sourceStart.toLocalDate(), sourceStart.toLocalTime(), sourceEnd.toLocalDate(), sourceEnd.toLocalTime(), recurrence.getZoneId());
}
/**
* Gets the earliest time used by this calendar, that means the start of the
* first entry stored.
*
* @return An instant representing the earliest time, can be null if no
* entries are contained.
*/
public final Instant getEarliestTimeUsed() {
return intervalTree.getEarliestTimeUsed();
}
/**
* Gets the latest time used by this calendar, that means the end of the
* last entry stored.
*
* @return An instant representing the latest time, can be null if no
* entries are contained.
*/
public final Instant getLatestTimeUsed() {
return intervalTree.getLatestTimeUsed();
}
private boolean batchUpdates;
private boolean dirty;
/**
* Tells the calendar that the application will perform a large number of changes.
* While batch updates in progress the calendar will stop to fire events. To finish
* this mode the application has to call {@link #stopBatchUpdates()}.
*/
public final void startBatchUpdates() {
batchUpdates = true;
dirty = false;
}
/**
* Tells the calendar that the application is done making big changes. Invoking
* this method will trigger a calendar event of type {@link CalendarEvent#CALENDAR_CHANGED} which
* will then force an update of the views.
*/
public final void stopBatchUpdates() {
batchUpdates = false;
if (dirty) {
dirty = false;
fireEvent(new CalendarEvent(CalendarEvent.CALENDAR_CHANGED, this));
}
}
/**
* Queries the calendar for all entries within the time interval defined by
* the start date and end date.
*
* @param startDate the start of the time interval
* @param endDate the end of the time interval
* @param zoneId the time zone for which to find entries
* @return a map filled with list of entries for given days
*/
public final Map>> findEntries(LocalDate startDate, LocalDate endDate, ZoneId zoneId) {
fireEvents = false;
Map>> result;
try {
result = doGetEntries(startDate, endDate, zoneId);
} finally {
fireEvents = true;
}
return result;
}
@SuppressWarnings({"rawtypes", "unchecked"})
private Map>> doGetEntries(LocalDate startDate, LocalDate endDate, ZoneId zoneId) {
if (MODEL.isLoggable(FINE)) {
MODEL.fine(getName() + ": getting entries from " + startDate + " until " + endDate + ", zone = " + zoneId);
}
ZonedDateTime st = ZonedDateTime.of(startDate, LocalTime.MIN, zoneId);
ZonedDateTime et = ZonedDateTime.of(endDate, LocalTime.MAX, zoneId);
Collection> intersectingEntries = intervalTree.getIntersectingObjects(st.toInstant(), et.toInstant());
if (intersectingEntries.isEmpty()) {
if (MODEL.isLoggable(FINE)) {
MODEL.fine(getName() + ": found no entries");
}
return Collections.emptyMap();
}
if (MODEL.isLoggable(FINE)) {
MODEL.fine(getName() + ": found " + intersectingEntries.size() + " entries");
}
Map>> result = new HashMap<>();
for (Entry> entry : intersectingEntries) {
if (entry.isRecurring()) {
String recurrenceRule = entry.getRecurrenceRule().replaceFirst("^RRULE:", "");
LocalDate utilStartDate = entry.getStartDate();
try {
LocalDate utilEndDate = et.toLocalDate();
List dateList = new Recur(recurrenceRule).getDates(utilStartDate, utilEndDate);
for (LocalDate repeatingDate : dateList) {
ZonedDateTime zonedDateTime = ZonedDateTime.of(repeatingDate, LocalTime.MIN, zoneId);
Entry recurrence = entry.createRecurrence();
recurrence.setId(entry.getId());
recurrence.getProperties().put("com.calendarfx.recurrence.source", entry);
recurrence.getProperties().put("com.calendarfx.recurrence.id", zonedDateTime.toString());
recurrence.setRecurrenceRule(entry.getRecurrenceRule());
// update the recurrence interval
LocalDate recurrenceStartDate = zonedDateTime.toLocalDate();
LocalDate recurrenceEndDate = recurrenceStartDate.plus(entry.getStartDate().until(entry.getEndDate()));
recurrence.setInterval(entry.getInterval().withDates(recurrenceStartDate, recurrenceEndDate));
recurrence.setUserObject(entry.getUserObject());
recurrence.setTitle(entry.getTitle());
recurrence.setMinimumDuration(entry.getMinimumDuration());
recurrence.setFullDay(entry.isFullDay());
recurrence.setLocation(entry.getLocation());
recurrence.setCalendar(this);
addEntryToResult(result, recurrence, startDate, endDate);
}
} catch (IllegalArgumentException | DateTimeParseException e) {
e.printStackTrace();
}
} else {
addEntryToResult(result, entry, startDate, endDate);
}
}
if (MODEL.isLoggable(FINE)) {
MODEL.fine(getName() + ": found entries for " + result.size() + " different days");
}
result.values().forEach(Collections::sort);
return result;
}
/*
* Assign the given entry to each date that it intersects with in the given search interval.
*/
private void addEntryToResult(Map>> result, Entry> entry, LocalDate startDate, LocalDate endDate) {
LocalDate entryStartDate = entry.getStartDate();
LocalDate entryEndDate = entry.getEndDate();
// entry does not intersect with time interval
if (entryEndDate.isBefore(startDate) || entryStartDate.isAfter(endDate)) {
return;
}
if (entryStartDate.isAfter(startDate)) {
startDate = entryStartDate;
}
if (entryEndDate.isBefore(endDate)) {
endDate = entryEndDate;
}
LocalDate date = startDate;
do {
result.computeIfAbsent(date, it -> new ArrayList<>()).add(entry);
date = date.plusDays(1);
} while (!date.isAfter(endDate));
}
private final ObjectProperty lookAheadDuration = new SimpleObjectProperty<>(this, "lookAheadDuration", Duration.ofDays(730));
/**
* Stores a time duration used for the entry search functionality of this
* calendar. The look ahead and the look back durations limit the search to
* the time interval [now - lookBackDuration, now + lookAheadDuration]. The
* default value of this property is 730 days (2 years).
*
* @return the look ahead duration
* @see #findEntries(String)
*/
public final ObjectProperty lookAheadDurationProperty() {
return lookAheadDuration;
}
/**
* Sets the value of {@link #lookAheadDurationProperty()}.
*
* @param duration the look ahead duration
*/
public final void setLookAheadDuration(Duration duration) {
requireNonNull(duration);
lookAheadDurationProperty().set(duration);
}
/**
* Returns the value of {@link #lookAheadDurationProperty()}.
*
* @return the look ahead duration
*/
public final Duration getLookAheadDuration() {
return lookAheadDurationProperty().get();
}
private final ObjectProperty lookBackDuration = new SimpleObjectProperty<>(this, "lookBackDuration", Duration.ofDays(730));
/**
* Stores a time duration used for the entry search functionality of this
* calendar. The look ahead and the look back durations limit the search to
* the time interval [now - lookBackDuration, now + lookAheadDuration]. The
* default value of this property is 730 days (2 years).
*
* @return the look back duration
* @see #findEntries(String)
*/
public final ObjectProperty lookBackDurationProperty() {
return lookBackDuration;
}
/**
* Sets the value of {@link #lookBackDurationProperty()}.
*
* @param duration the look back duration
*/
public final void setLookBackDuration(Duration duration) {
requireNonNull(duration);
lookBackDurationProperty().set(duration);
}
/**
* Returns the value of {@link #lookBackDurationProperty()}.
*
* @return the look back duration
*/
public final Duration getLookBackDuration() {
return lookBackDurationProperty().get();
}
/**
* Queries the calendar for entries that match the given search text. The method
* can be overridden to implement custom find / search strategies.
*
* @param searchText the search text
* @return a list of entries that match the search
* @see Entry#matches(String)
*/
public List> findEntries(String searchText) {
if (MODEL.isLoggable(FINE)) {
MODEL.fine(getName() + ": getting entries for search term: "
+ searchText);
}
Instant horizonStart = Instant.now().minus(getLookBackDuration());
Instant horizonEnd = Instant.now().plus(getLookAheadDuration());
ZoneId zoneId = ZoneId.systemDefault();
ZonedDateTime st = ZonedDateTime.ofInstant(horizonStart, zoneId);
ZonedDateTime et = ZonedDateTime.ofInstant(horizonEnd, zoneId);
List> result = new ArrayList<>();
Map>> map = findEntries(st.toLocalDate(), et.toLocalDate(), zoneId);
for (List> list : map.values()) {
for (Entry> entry : list) {
if (entry.matches(searchText)) {
result.add(entry);
}
}
}
if (MODEL.isLoggable(FINE)) {
MODEL.fine(getName() + ": found " + result.size() + " entries");
}
return result;
}
/**
* Removes all entries from the calendar. Fires an
* {@link CalendarEvent#CALENDAR_CHANGED} event.
*/
public final void clear() {
intervalTree.clear();
fireEvent(new CalendarEvent(CALENDAR_CHANGED, this));
}
// support for adding entries
/**
* Adds the given entry to the calendar. This is basically just a convenience
* method as the actual work of adding an entry to a calendar is done inside
* {@link Entry#setCalendar(Calendar)}.
*
* @param entry the entry to add
*/
public final void addEntry(Entry> entry) {
addEntries(entry);
}
/**
* Adds the given entries to the calendar. This is basically just a convenience
* method as the actual work of adding an entry to a calendar is done inside
* {@link Entry#setCalendar(Calendar)}.
*
* @param entries the entries to add
*/
public final void addEntries(Entry>... entries) {
if (entries != null) {
for (Entry> entry : entries) {
entry.setCalendar(this);
}
}
}
/**
* Adds the given entries to the calendar. This is basically just a convenience
* method as the actual work of adding an entry to a calendar is done inside
* {@link Entry#setCalendar(Calendar)}.
*
* @param entries the collection of entries to add
*/
public final void addEntries(Collection> entries) {
if (entries != null) {
entries.forEach(this::addEntry);
}
}
/**
* Adds the entries returned by the iterator to the calendar. This is basically just a convenience
* method as the actual work of adding an entry to a calendar is done inside {@link Entry#setCalendar(Calendar)}.
*
* @param entries the entries to add
*/
public final void addEntries(Iterator> entries) {
if (entries != null) {
while (entries.hasNext()) {
addEntry(entries.next());
}
}
}
/**
* Adds the entries returned by the iterable to the calendar. This is basically just a convenience
* method as the actual work of adding an entry to a calendar is done inside {@link Entry#setCalendar(Calendar)}.
*
* @param entries the entries to add
*/
public final void addEntries(Iterable> entries) {
if (entries != null) {
addEntries(entries.iterator());
}
}
// support for removing entries
/**
* Removes the given entry from the calendar. This is basically just a convenience
* method as the actual work of removing an entry from a calendar is done inside
* {@link Entry#setCalendar(Calendar)}.
*
* @param entry the entry to remove
*/
public final void removeEntry(Entry> entry) {
removeEntries(entry);
}
/**
* Removes the given entries from the calendar. This is basically just a convenience
* method as the actual work of removing an entry from a calendar is done inside
* {@link Entry#setCalendar(Calendar)}.
*
* @param entries the entries to remove
*/
public final void removeEntries(Entry>... entries) {
if (entries != null) {
for (Entry> entry : entries) {
entry.setCalendar(null);
}
}
}
/**
* Removes the given entries from the calendar. This is basically just a convenience
* method as the actual work of removing an entry from a calendar is done inside
* {@link Entry#setCalendar(Calendar)}.
*
* @param entries the collection of entries to remove
*/
public final void removeEntries(Collection> entries) {
if (entries != null) {
entries.forEach(this::removeEntry);
}
}
/**
* Removes the entries returned by the iterator from the calendar. This is basically just a convenience
* method as the actual work of removing an entry from a calendar is done inside {@link Entry#setCalendar(Calendar)}.
*
* @param entries the entries to remove
*/
public final void removeEntries(Iterator> entries) {
if (entries != null) {
while (entries.hasNext()) {
removeEntry(entries.next());
}
}
}
/**
* Adds the entries returned by the iterable to the calendar. This is basically just a convenience
* method as the actual work of adding an entry to a calendar is done inside {@link Entry#setCalendar(Calendar)}.
*
* @param entries the entries to add
*/
public final void removeEntries(Iterable> entries) {
if (entries != null) {
removeEntries(entries.iterator());
}
}
final void impl_addEntry(Entry> entry) {
if (entry.isRecurrence()) {
throw new IllegalArgumentException("a recurrence entry can not be added to a calendar");
}
dirty = true;
intervalTree.add(entry);
}
final void impl_removeEntry(Entry> entry) {
if (entry.isRecurrence()) {
throw new IllegalArgumentException("a recurrence entry can not be added to a calendar");
}
dirty = true;
intervalTree.remove(entry);
}
// Name support.
private final StringProperty name = new SimpleStringProperty(this, "name", "Untitled");
/**
* A property used to store the name of the calendar.
*
* @return the property used for storing the calendar name
*/
public final StringProperty nameProperty() {
return name;
}
/**
* Sets the value of {@link #nameProperty()}.
*
* @param name the new name for the calendar
*/
public final void setName(String name) {
nameProperty().set(name);
}
/**
* Returns the value of {@link #nameProperty()}.
*
* @return the name of the calendar
*/
public final String getName() {
return nameProperty().get();
}
// Short name support.
private final StringProperty shortName = new SimpleStringProperty(this, "shortName", "Unt.");
/**
* A property used to store the short name of the calendar.
*
* @return the property used for storing the calendar short name
*/
public final StringProperty shortNameProperty() {
return shortName;
}
/**
* Sets the value of {@link #shortNameProperty()}.
*
* @param name the new short name for the calendar
*/
public final void setShortName(String name) {
shortNameProperty().set(name);
}
/**
* Returns the value of {@link #shortNameProperty()}.
*
* @return the short name of the calendar
*/
public final String getShortName() {
return shortNameProperty().get();
}
// Style prefix support.
private final StringProperty style = new SimpleStringProperty(this, "style",
Style.STYLE1.name().toLowerCase());
/**
* A property used to store the visual style that will be used for the
* calendar in the UI. A style can be any arbitrary name. The style will be
* used as a prefix to find the styles in the stylesheet. For examples
* please search the standard framework stylesheet for the predefined styles
* "style1-", "style2-", etc.
*
* @return the visual calendar style
*/
public final StringProperty styleProperty() {
return style;
}
/**
* Sets the value of {@link #styleProperty()} based on one of the predefined
* styles (see also the enum {@link Style}). The image below shows how the
* styles appear in the UI.
*
*
*
* @param style the calendar style
*/
public final void setStyle(Style style) {
MODEL.finer(getName() + ": setting style to: " + style);
setStyle(style.name().toLowerCase());
}
/**
* Sets the value of {@link #styleProperty()}.
*
* @param stylePrefix the calendar style
*/
public final void setStyle(String stylePrefix) {
requireNonNull(stylePrefix);
MODEL.finer(getName() + ": setting style to: " + style);
styleProperty().set(stylePrefix);
}
/**
* Returns the value of {@link #styleProperty()}.
*
* @return the current calendar style
*/
public final String getStyle() {
return styleProperty().get();
}
// Read only support.
private final BooleanProperty readOnly = new SimpleBooleanProperty(this, "readOnly", false);
/**
* A property used to control if the calendar is read-only or not.
*
* @return true if the calendar is read-only (default is false)
*/
public final BooleanProperty readOnlyProperty() {
return readOnly;
}
/**
* Returns the value of {@link #readOnlyProperty()}.
*
* @return true if the calendar can not be edited by the user
*/
public final boolean isReadOnly() {
return readOnlyProperty().get();
}
/**
* Sets the value of {@link #readOnlyProperty()}.
*
* @param readOnly the calendar can not be edited by the user if true
*/
public final void setReadOnly(boolean readOnly) {
MODEL.finer(getName() + ": setting read only to: " + readOnly);
readOnlyProperty().set(readOnly);
}
private final ObservableList> eventHandlers = FXCollections.observableArrayList();
/**
* Adds an event handler for calendar events. Handlers will be called when
* an entry gets added, removed, changes, etc.
*
* @param l the event handler to add
*/
public final void addEventHandler(EventHandler l) {
if (l != null) {
if (MODEL.isLoggable(FINER)) {
MODEL.finer(getName() + ": adding event handler: " + l);
}
eventHandlers.add(l);
}
}
/**
* Removes an event handler from the calendar.
*
* @param l the event handler to remove
*/
public final void removeEventHandler(EventHandler l) {
if (l != null) {
if (MODEL.isLoggable(FINER)) {
MODEL.finer(getName() + ": removing event handler: " + l);
}
eventHandlers.remove(l);
}
}
private boolean fireEvents = true;
/**
* Fires the given calendar event to all event handlers currently registered
* with this calendar.
*
* @param evt the event to fire
*/
public final void fireEvent(CalendarEvent evt) {
if (fireEvents && !batchUpdates) {
if (MODEL.isLoggable(FINER)) {
MODEL.finer(getName() + ": firing event: " + evt);
}
requireNonNull(evt);
Event.fireEvent(this, evt);
}
}
@Override
public final EventDispatchChain buildEventDispatchChain(EventDispatchChain givenTail) {
return givenTail.append((event, tail) -> {
if (event instanceof CalendarEvent) {
for (EventHandler handler : eventHandlers) {
handler.handle((CalendarEvent) event);
}
}
return event;
});
}
private final ObjectProperty userObject = new SimpleObjectProperty<>(this, "userObject");
public final T getUserObject() {
return userObject.get();
}
/**
* An (optional) user object that can be used to link this calendar to the source
* of its data or the business object that it represents.
*
* @return a user object
*/
public final ObjectProperty userObjectProperty() {
return userObject;
}
public final void setUserObject(T userObject) {
this.userObject.set(userObject);
}
@Override
public String toString() {
return "Calendar [name=" + getName() + ", style=" + getStyle() + ", readOnly=" + isReadOnly() + ", " + (getUserObject() != null ? getUserObject().toString() : "null") + "]";
}
}
================================================
FILE: CalendarFXView/src/main/java/com/calendarfx/model/CalendarEvent.java
================================================
/*
* 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.model;
import javafx.event.Event;
import javafx.event.EventType;
import java.time.ZonedDateTime;
import static java.util.Objects.requireNonNull;
/**
* An event class used to signal changes done within a calendar or changes done
* to a calendar entry. Events of this type can be received by adding an event
* handler to a calendar.
*
*
* Example
*
*
* {@code
* Calendar calendar = new Calendar("Home");
* calendar.addEventHandler(CalendarEvent.ENTRY_ADDED, evt -> {...});
* }
*
*
* @see Calendar#addEventHandler(javafx.event.EventHandler)
*/
public class CalendarEvent extends Event {
private static final long serialVersionUID = 4279597664476680474L;
/**
* The supertype of all event types in this event class.
*/
public static final EventType ANY = new EventType<>(Event.ANY, "CALENDAR");
/**
* An event type used to inform the application that "something" inside the
* calendar has changed and that the views need to update their visuals
* accordingly (brute force update).
*/
public static final EventType CALENDAR_CHANGED = new EventType<>(CalendarEvent.ANY, "CALENDAR_CHANGED");
/**
* The supertype of all events that a related to an entry itself and not the
* calendar.
*/
public static final EventType ENTRY_CHANGED = new EventType<>(CalendarEvent.ANY, "ENTRY_CHANGED");
/**
* An event type used to inform the application that an entry has been moved
* from one calendar to another.
*/
public static final EventType ENTRY_CALENDAR_CHANGED = new EventType<>(CalendarEvent.ENTRY_CHANGED, "ENTRY_CALENDAR_CHANGED");
/**
* An event type used to inform the application that an entry has become a
* "full day" entry, meaning its start and end time are no longer relevant.
* The entry should be visualized in a way that signals that the entry will
* take all day (e.g. a birthday).
*/
public static final EventType ENTRY_FULL_DAY_CHANGED = new EventType<>(CalendarEvent.ENTRY_CHANGED, "ENTRY_FULL_DAY_CHANGED");
/**
* An event type used to inform the application that an entry has been
* assigned a new user object.
*/
public static final EventType ENTRY_RECURRENCE_RULE_CHANGED = new EventType<>(CalendarEvent.ENTRY_CHANGED, "ENTRY_RECURRENCE_RULE_CHANGED");
/**
* An event type used to inform the application that an entry has been
* assigned a new title.
*/
public static final EventType ENTRY_TITLE_CHANGED = new EventType<>(CalendarEvent.ENTRY_CHANGED, "ENTRY_TITLE_CHANGED");
/**
* An event type used to inform the application that an entry has been
* assigned a new user object.
*/
public static final EventType ENTRY_USER_OBJECT_CHANGED = new EventType<>(CalendarEvent.ENTRY_CHANGED, "ENTRY_USER_OBJECT_CHANGED");
/**
* An event type used to inform the application that an entry has been
* assigned a new user object.
*/
public static final EventType ENTRY_LOCATION_CHANGED = new EventType<>(CalendarEvent.ENTRY_CHANGED, "ENTRY_LOCATION_CHANGED");
/**
* An event type used to inform the application that the time bounds of an
* entry have been changed. One or several of start / end date, start / end
* time.
*/
public static final EventType ENTRY_INTERVAL_CHANGED = new EventType<>(CalendarEvent.ENTRY_CHANGED, "ENTRY_INTERVAL_CHANGED");
private Entry> entry;
private final Calendar calendar;
private boolean oldFullDay;
private String oldText;
private Calendar oldCalendar;
private Interval oldInterval;
private Object oldUserObject;
/**
* Constructs a new event for subclass.
*
* @param eventType the event type
* @param calendar the calendar where the event occurred.
*/
protected CalendarEvent(EventType extends CalendarEvent> eventType, Calendar calendar) {
super(calendar, calendar, eventType);
this.calendar = requireNonNull(calendar);
}
/**
* Constructs a new event.
*
* @param eventType the event type
* @param calendar the calendar where the event occured
* @param entry the affected entry
*/
public CalendarEvent(EventType extends CalendarEvent> eventType, Calendar calendar, Entry> entry) {
super(calendar, calendar, eventType);
this.calendar = calendar;
this.entry = requireNonNull(entry);
}
/**
* Constructs a new event used for signalling that an entry was assigned to
* a new calendar. The entry already carries a reference to new calendar and
* the event object will know the old calendar.
*
* @param eventType the event type
* @param calendar the calendar where the event occured
* @param entry the affected entry
* @param oldCalendar the calendar to which the event belonged before
*/
public CalendarEvent(EventType eventType, Calendar calendar, Entry> entry, Calendar oldCalendar) {
this(eventType, calendar, entry);
this.oldCalendar = oldCalendar;
}
/**
* Constructs a new event used for signalling that an entry has been
* assigned a new user object. The entry already carries a reference to the
* new user object and the event object will know the old user object.
*
* @param eventType the event type
* @param calendar the calendar where the event occured
* @param entry the affected entry
* @param oldUserObject the calendar to which the event belonged before
*/
public CalendarEvent(EventType eventType, Calendar calendar, Entry> entry, Object oldUserObject) {
this(eventType, calendar, entry);
this.oldUserObject = oldUserObject;
}
/**
* Constructs a new event used for signalling that an entry was assigned a
* new start end date / time. The entry already carries the new values,
* while the old values can be retrieved from the event object.
*
* @param eventType the event type
* @param calendar the calendar where the event occured
* @param entry the affected entry
* @param oldInterval the previous time interval
*/
public CalendarEvent(EventType eventType, Calendar calendar, Entry> entry, Interval oldInterval) {
this(eventType, calendar, entry);
this.oldInterval = requireNonNull(oldInterval);
}
/**
* Constructs a new event used for signalling that an entry was assigned a
* new text (normally the title). The entry already carries a reference to
* new text and the event object will know the old one.
*
* @param eventType the event type
* @param calendar the calendar where the event occured
* @param entry the affected entry
* @param oldText the previous value of the text
*/
public CalendarEvent(EventType