diff --git a/CalendarFXApp/src/main/java/com/calendarfx/app/CalendarApp.java b/CalendarFXApp/src/main/java/com/calendarfx/app/CalendarApp.java index 657a0318..5150c412 100644 --- a/CalendarFXApp/src/main/java/com/calendarfx/app/CalendarApp.java +++ b/CalendarFXApp/src/main/java/com/calendarfx/app/CalendarApp.java @@ -95,6 +95,7 @@ public void run() { updateTimeThread.start(); Scene scene = new Scene(stackPane); + scene.focusOwnerProperty().addListener(it -> System.out.println("focus owner: " + scene.getFocusOwner())); CSSFX.start(scene); primaryStage.setTitle("Calendar"); diff --git a/CalendarFXGoogle/src/main/java/com/calendarfx/google/model/GoogleEntry.java b/CalendarFXGoogle/src/main/java/com/calendarfx/google/model/GoogleEntry.java index d5d954d4..68bb1a4a 100644 --- a/CalendarFXGoogle/src/main/java/com/calendarfx/google/model/GoogleEntry.java +++ b/CalendarFXGoogle/src/main/java/com/calendarfx/google/model/GoogleEntry.java @@ -19,7 +19,6 @@ import com.calendarfx.model.Entry; import com.google.api.services.calendar.model.Event; import com.google.api.services.calendar.model.EventAttendee; -import impl.com.calendarfx.view.util.Util; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.beans.property.BooleanProperty; @@ -30,6 +29,8 @@ import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; +import java.util.Objects; + /** * Custom entry representing a google event. This contains all the required * information for a single event of google calendar. @@ -83,7 +84,7 @@ public final void setStatus(Status status) { public void set(boolean newValue) { boolean oldValue = get(); - if (!Util.equals(oldValue, newValue)) { + if (!Objects.equals(oldValue, newValue)) { super.set(newValue); if (getCalendar() instanceof GoogleCalendar) { @@ -119,7 +120,7 @@ public final void setAttendeesCanModify(boolean attendeesCanModify) { public void set(boolean newValue) { boolean oldValue = get(); - if (!Util.equals(oldValue, newValue)) { + if (!Objects.equals(oldValue, newValue)) { super.set(newValue); if (getCalendar() instanceof GoogleCalendar) { @@ -151,7 +152,7 @@ public final void setAttendeesCanInviteOthers( public void set(boolean newValue) { boolean oldValue = get(); - if (!Util.equals(oldValue, newValue)) { + if (!Objects.equals(oldValue, newValue)) { super.set(newValue); if (getCalendar() instanceof GoogleCalendar) { diff --git a/CalendarFXView/src/main/java/com/calendarfx/model/Entry.java b/CalendarFXView/src/main/java/com/calendarfx/model/Entry.java index d27d1ead..6597520a 100644 --- a/CalendarFXView/src/main/java/com/calendarfx/model/Entry.java +++ b/CalendarFXView/src/main/java/com/calendarfx/model/Entry.java @@ -227,7 +227,7 @@ public void set(Interval newInterval) { Interval oldInterval = getValue(); - if (!Util.equals(newInterval, oldInterval)) { + if (!Objects.equals(newInterval, oldInterval)) { Calendar calendar = getCalendar(); @@ -672,7 +672,7 @@ public final StringProperty recurrenceRuleProperty() { public void set(String newRecurrence) { String oldRecurrence = get(); - if (!Util.equals(oldRecurrence, newRecurrence)) { + if (!Objects.equals(oldRecurrence, newRecurrence)) { Calendar calendar = getCalendar(); @@ -839,7 +839,7 @@ public final String getId() { public void set(Calendar newCalendar) { Calendar oldCalendar = get(); - if (!Util.equals(oldCalendar, newCalendar)) { + if (!Objects.equals(oldCalendar, newCalendar)) { if (oldCalendar != null) { if (!isRecurrence()) { @@ -1004,7 +1004,7 @@ public final ZoneId getZoneId() { public void set(String newTitle) { String oldTitle = get(); - if (!Util.equals(oldTitle, newTitle)) { + if (!Objects.equals(oldTitle, newTitle)) { super.set(newTitle); Calendar calendar = getCalendar(); @@ -1062,7 +1062,7 @@ public final StringProperty locationProperty() { public void set(String newLocation) { String oldLocation = get(); - if (!Util.equals(oldLocation, newLocation)) { + if (!Objects.equals(oldLocation, newLocation)) { super.set(newLocation); @@ -1215,7 +1215,7 @@ public final LocalTime getEndTime() { public void set(boolean newFullDay) { boolean oldFullDay = get(); - if (!Util.equals(oldFullDay, newFullDay)) { + if (!Objects.equals(oldFullDay, newFullDay)) { super.set(newFullDay); @@ -1508,7 +1508,7 @@ public final BooleanProperty hiddenProperty() { } /** - * An entry can be made explicityl hidden. + * An entry can be made explicitly hidden. * * @param hidden true if the entry should not be visible in the calendar */ diff --git a/CalendarFXView/src/main/java/com/calendarfx/util/Util.java b/CalendarFXView/src/main/java/com/calendarfx/util/Util.java deleted file mode 100644 index 6be47fa1..00000000 --- a/CalendarFXView/src/main/java/com/calendarfx/util/Util.java +++ /dev/null @@ -1,363 +0,0 @@ -/* - * 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.util; - -import com.calendarfx.view.DateControl; -import com.calendarfx.view.Messages; -import javafx.beans.InvalidationListener; -import javafx.beans.Observable; -import javafx.beans.WeakListener; -import javafx.beans.property.Property; -import javafx.geometry.Orientation; -import javafx.scene.Node; -import javafx.scene.Parent; -import javafx.scene.control.ScrollBar; -import net.fortuna.ical4j.model.Recur; -import net.fortuna.ical4j.model.WeekDay; -import net.fortuna.ical4j.transform.recurrence.Frequency; - -import java.lang.ref.WeakReference; -import java.text.MessageFormat; -import java.time.DayOfWeek; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.time.format.FormatStyle; -import java.util.List; -import java.util.Objects; - -import static java.time.temporal.ChronoField.DAY_OF_WEEK; - -/** - * Utility methods. - */ -public class Util { - - /** - * An interface used for converting an object of one type to an object - * of another type. - * - * @param the first (left) type - * @param the second (right) type - */ - public interface Converter { - - L toLeft(R right); - - R toRight(L left); - } - - /** - * Converts the given recurrence rule (according to RFC 2445) into a human - * readable text, e.g. "RRULE:FREQ=DAILY;" becomes "Every day". - * - * @param rrule the rule - * @param startDate the start date for the rule - * @return a nice text describing the rule - */ - public static String convertRFC2445ToText(String rrule, - LocalDate startDate) { - - try { - Recur rule = new Recur<>(rrule.replaceFirst("^RRULE:", "")); - StringBuilder sb = new StringBuilder(); - - String granularity; - String granularities; - - switch (rule.getFrequency()) { - case DAILY: - granularity = Messages.getString("Util.DAY"); - granularities = Messages.getString("Util.DAYS"); - break; - case MONTHLY: - granularity = Messages.getString("Util.MONTH"); - granularities = Messages.getString("Util.MONTHS"); - break; - case WEEKLY: - granularity = Messages.getString("Util.WEEK"); - granularities = Messages.getString("Util.WEEKS"); - break; - case YEARLY: - granularity = Messages.getString("Util.YEAR"); - granularities = Messages.getString("Util.YEARS"); - break; - case HOURLY: - granularity = Messages.getString("Util.HOUR"); - granularities = Messages.getString("Util.HOURS"); - break; - case MINUTELY: - granularity = Messages.getString("Util.MINUTE"); - granularities = Messages.getString("Util.MINUTES"); - break; - case SECONDLY: - granularity = Messages.getString("Util.SECOND"); - granularities = Messages.getString("Util.SECONDS"); - break; - default: - granularity = ""; - granularities = ""; - } - - int interval = rule.getInterval(); - if (interval > 1) { - sb.append(MessageFormat.format(Messages.getString("Util.EVERY_PLURAL"), rule.getInterval(), granularities)); - } else { - sb.append(MessageFormat.format(Messages.getString("Util.EVERY_SINGULAR"), granularity)); - } - - /* - * Weekdays - */ - - if (rule.getFrequency().equals(Frequency.WEEKLY)) { - List byDay = rule.getDayList(); - if (!byDay.isEmpty()) { - sb.append(Messages.getString("Util.ON_WEEKDAY")); - for (int i = 0; i < byDay.size(); i++) { - WeekDay num = byDay.get(i); - sb.append(makeHuman(num.getDay())); - if (i < byDay.size() - 1) { - sb.append(", "); - } - } - } - } - - if (rule.getFrequency().equals(Frequency.MONTHLY)) { - - if (!rule.getMonthDayList().isEmpty()) { - - int day = rule.getMonthDayList().get(0); - sb.append(Messages.getString("Util.ON_MONTH_DAY")); - sb.append(day); - - } else if (!rule.getDayList().isEmpty()) { - - /* - * We only support one day. - */ - WeekDay num = rule.getDayList().get(0); - sb.append(MessageFormat.format(Messages.getString("Util.ON_MONTH_WEEKDAY"), makeHuman(num.getOffset()), makeHuman(num.getDay()))); - } - } - - if (rule.getFrequency().equals(Frequency.YEARLY)) { - sb.append(MessageFormat.format(Messages.getString("Util.ON_DATE"), DateTimeFormatter.ofPattern(Messages.getString("Util.MONTH_AND_DAY_FORMAT")).format(startDate))); - } - - int count = rule.getCount(); - if (count > 0) { - if (count == 1) { - return Messages.getString("Util.ONCE"); - } else { - sb.append(MessageFormat.format(Messages.getString("Util.TIMES"), count)); - } - } else { - LocalDate until = rule.getUntil(); - if (until != null) { - sb.append(MessageFormat.format(Messages.getString("Util.UNTIL_DATE"), DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).format(until))); - } - } - - return sb.toString(); - } catch (IllegalArgumentException | DateTimeParseException e) { - e.printStackTrace(); - return Messages.getString("Util.INVALID_RULE"); - } - } - - private static String makeHuman(WeekDay.Day wday) { - switch (wday) { - case FR: - return Messages.getString("Util.FRIDAY"); - case MO: - return Messages.getString("Util.MONDAY"); - case SA: - return Messages.getString("Util.SATURDAY"); - case SU: - return Messages.getString("Util.SUNDAY"); - case TH: - return Messages.getString("Util.THURSDAY"); - case TU: - return Messages.getString("Util.TUESDAY"); - case WE: - return Messages.getString("Util.WEDNESDAY"); - default: - throw new IllegalArgumentException("unknown weekday: " + wday); - } - } - - private static String makeHuman(int num) { - switch (num) { - case 1: - return Messages.getString("Util.FIRST"); - case 2: - return Messages.getString("Util.SECOND"); - case 3: - return Messages.getString("Util.THIRD"); - case 4: - return Messages.getString("Util.FOURTH"); - case 5: - return Messages.getString("Util.FIFTH"); - default: - return Integer.toString(num); - } - } - - /** - * Searches for a {@link ScrollBar} of the given orientation (vertical, horizontal) - * somewhere in the containment hierarchy of the given parent node. - * - * @param parent the parent node - * @param orientation the orientation (horizontal, vertical) - * @return a scrollbar or null if none can be found - */ - public static ScrollBar findScrollBar(Parent parent, Orientation orientation) { - for (Node node : parent.getChildrenUnmodifiable()) { - if (node instanceof ScrollBar) { - ScrollBar b = (ScrollBar) node; - if (b.getOrientation().equals(orientation)) { - return b; - } - } - - if (node instanceof Parent) { - ScrollBar b = findScrollBar((Parent) node, orientation); - if (b != null) { - return b; - } - } - } - - return null; - } - - /** - * Adjusts the given date to a new date that marks the beginning of the week where the - * given date is located. If "Monday" is the first day of the week and the given date - * is a "Wednesday" then this method will return a date that is two days earlier than the - * given date. - * - * @param date the date to adjust - * @param firstDayOfWeek the day of week that is considered the start of the week ("Monday" in Germany, "Sunday" in the US) - * @return the date of the first day of the week - * @see #adjustToLastDayOfWeek(LocalDate, DayOfWeek) - * @see DateControl#getFirstDayOfWeek() - */ - public static LocalDate adjustToFirstDayOfWeek(LocalDate date, DayOfWeek firstDayOfWeek) { - LocalDate newDate = date.with(DAY_OF_WEEK, firstDayOfWeek.getValue()); - if (newDate.isAfter(date)) { - newDate = newDate.minusWeeks(1); - } - - return newDate; - } - - /** - * Adjusts the given date to a new date that marks the end of the week where the - * given date is located. If "Monday" is the first day of the week and the given date - * is a "Wednesday" then this method will return a date that is four days later than the - * given date. This method calculates the first day of the week and then adds six days - * to it. - * - * @param date the date to adjust - * @param firstDayOfWeek the day of week that is considered the start of the week ("Monday" in Germany, "Sunday" in the US) - * @return the date of the first day of the week - * @see #adjustToFirstDayOfWeek(LocalDate, DayOfWeek) - * @see DateControl#getFirstDayOfWeek() - */ - public static LocalDate adjustToLastDayOfWeek(LocalDate date, DayOfWeek firstDayOfWeek) { - LocalDate startOfWeek = adjustToFirstDayOfWeek(date, firstDayOfWeek); - return startOfWeek.plusDays(6); - } - - /** - * Creates a bidirectional binding between the two given properties of different types via the - * help of a {@link Converter}. - * - * @param leftProperty the left property - * @param rightProperty the right property - * @param converter the converter - * @param the type of the left property - * @param the type of the right property - */ - public static void bindBidirectional(Property leftProperty, Property rightProperty, Converter converter) { - BidirectionalConversionBinding binding = new BidirectionalConversionBinding<>(leftProperty, rightProperty, converter); - leftProperty.addListener(binding); - rightProperty.addListener(binding); - leftProperty.setValue(converter.toLeft(rightProperty.getValue())); - } - - private static class BidirectionalConversionBinding implements InvalidationListener, WeakListener { - - private final WeakReference> leftReference; - private final WeakReference> rightReference; - private final Converter converter; - private boolean updating; - - private BidirectionalConversionBinding(Property leftProperty, Property rightProperty, Converter converter) { - this.leftReference = new WeakReference<>(Objects.requireNonNull(leftProperty)); - this.rightReference = new WeakReference<>(Objects.requireNonNull(rightProperty)); - this.converter = Objects.requireNonNull(converter); - } - - public Property getLeftProperty() { - return leftReference.get(); - } - - public Property getRightProperty() { - return rightReference.get(); - } - - @Override - public boolean wasGarbageCollected() { - return getLeftProperty() == null || getRightProperty() == null; - } - - @Override - public void invalidated(Observable observable) { - if (updating) { - return; - } - - final Property leftProperty = getLeftProperty(); - final Property rightProperty = getRightProperty(); - - if (wasGarbageCollected()) { - if (leftProperty != null) { - leftProperty.removeListener(this); - } - if (rightProperty != null) { - rightProperty.removeListener(this); - } - } else { - try { - updating = true; - - if (observable == leftProperty) { - rightProperty.setValue(converter.toRight(leftProperty.getValue())); - } else { - leftProperty.setValue(converter.toLeft(rightProperty.getValue())); - } - } finally { - updating = false; - } - } - } - } -} diff --git a/CalendarFXView/src/main/java/com/calendarfx/view/AllDayView.java b/CalendarFXView/src/main/java/com/calendarfx/view/AllDayView.java index 21dbe4c2..583b63c3 100644 --- a/CalendarFXView/src/main/java/com/calendarfx/view/AllDayView.java +++ b/CalendarFXView/src/main/java/com/calendarfx/view/AllDayView.java @@ -17,8 +17,8 @@ package com.calendarfx.view; import com.calendarfx.model.Entry; -import com.calendarfx.util.Util; import impl.com.calendarfx.view.AllDayViewSkin; +import impl.com.calendarfx.view.util.Util; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.IntegerProperty; @@ -78,7 +78,7 @@ public AllDayView(int numberOfDays) { getStyleClass().add(ALL_DAY_VIEW); setNumberOfDays(numberOfDays); - new CreateDeleteHandler(this); + new CreateAndDeleteHandler(this); } /** diff --git a/CalendarFXView/src/main/java/com/calendarfx/view/ContextMenuProvider.java b/CalendarFXView/src/main/java/com/calendarfx/view/ContextMenuProvider.java index a70616fb..c690a8cd 100644 --- a/CalendarFXView/src/main/java/com/calendarfx/view/ContextMenuProvider.java +++ b/CalendarFXView/src/main/java/com/calendarfx/view/ContextMenuProvider.java @@ -46,8 +46,7 @@ * * @see DateControl#setContextMenuCallback(Callback) */ -public class ContextMenuProvider - implements Callback { +public class ContextMenuProvider implements Callback { @Override public ContextMenu call(ContextMenuParameter param) { diff --git a/CalendarFXView/src/main/java/com/calendarfx/view/CreateDeleteHandler.java b/CalendarFXView/src/main/java/com/calendarfx/view/CreateAndDeleteHandler.java similarity index 64% rename from CalendarFXView/src/main/java/com/calendarfx/view/CreateDeleteHandler.java rename to CalendarFXView/src/main/java/com/calendarfx/view/CreateAndDeleteHandler.java index 84d83b12..04d696f8 100644 --- a/CalendarFXView/src/main/java/com/calendarfx/view/CreateDeleteHandler.java +++ b/CalendarFXView/src/main/java/com/calendarfx/view/CreateAndDeleteHandler.java @@ -21,28 +21,23 @@ import com.calendarfx.util.LoggingDomain; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; -import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import java.time.ZonedDateTime; import java.util.Optional; -import static java.util.Objects.requireNonNull; +class CreateAndDeleteHandler extends DeleteHandler { -class CreateDeleteHandler { - - private final DateControl dateControl; - - public CreateDeleteHandler(DateControl control) { - this.dateControl = requireNonNull(control); + public CreateAndDeleteHandler(DateControl control) { + super(control); dateControl.addEventHandler(MouseEvent.MOUSE_CLICKED, this::createEntry); - dateControl.addEventHandler(KeyEvent.KEY_PRESSED, this::deleteEntries); } private void createEntry(MouseEvent evt) { - if (!(dateControl instanceof DayView) && evt.getButton().equals(MouseButton.PRIMARY) && evt.getClickCount() == dateControl.getCreateEntryClickCount()) { + System.out.println("entry click count: " + dateControl.getCreateEntryClickCount()); + if (evt.getButton().equals(MouseButton.PRIMARY) && evt.getClickCount() == dateControl.getCreateEntryClickCount()) { if (!evt.isStillSincePress()) { return; @@ -86,33 +81,4 @@ private void createEntry(MouseEvent evt) { evt.consume(); } } - - private void deleteEntries(KeyEvent evt) { - switch (evt.getCode()) { - case DELETE: - case BACK_SPACE: - for (Entry entry : dateControl.getSelections()) { - if (!dateControl.getEntryEditPolicy().call(new DateControl.EntryEditParameter(dateControl, entry, DateControl.EditOperation.DELETE))) { - continue; - } - if (entry.isRecurrence()) { - entry = entry.getRecurrenceSourceEntry(); - } - if (!dateControl.getEntryEditPolicy().call(new DateControl.EntryEditParameter(dateControl, entry, DateControl.EditOperation.DELETE))) { - continue; - } - - Calendar calendar = entry.getCalendar(); - if (calendar != null && !calendar.isReadOnly()) { - entry.removeFromCalendar(); - } - } - dateControl.clearSelection(); - break; - case F5: - dateControl.refreshData(); - default: - break; - } - } } \ No newline at end of file diff --git a/CalendarFXView/src/main/java/com/calendarfx/view/DateControl.java b/CalendarFXView/src/main/java/com/calendarfx/view/DateControl.java index 26976906..02984bcc 100644 --- a/CalendarFXView/src/main/java/com/calendarfx/view/DateControl.java +++ b/CalendarFXView/src/main/java/com/calendarfx/view/DateControl.java @@ -61,6 +61,7 @@ import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import javafx.scene.shape.Rectangle; +import javafx.stage.Modality; import javafx.util.Callback; import org.controlsfx.control.PopOver; import org.controlsfx.control.PopOver.ArrowLocation; @@ -76,7 +77,6 @@ import java.time.temporal.ChronoUnit; import java.time.temporal.WeekFields; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Objects; @@ -107,7 +107,7 @@ * properties will be bound to each other. This not only includes date and time * zone properties but also all the factory and detail callbacks. This allows an * application to create a complex calendar control and to configure only that - * control without worrying about the date controls that are nested inside of + * control without worrying about the date controls that are nested inside * it. The children will all "inherit" their settings from the parent control. * *

Current Date, Today, First Day of Week

The {@link #dateProperty()} @@ -168,8 +168,6 @@ public abstract class DateControl extends CalendarFXControl { * with default implementations. */ protected DateControl() { - setOnMouseClicked(evt -> requestFocus()); - setUsagePolicy(count -> { if (count < 0) { throw new IllegalArgumentException("usage count can not be smaller than zero, but was " + count); @@ -217,14 +215,12 @@ protected DateControl() { */ setDefaultCalendarProvider(control -> { List sources = getCalendarSources(); - if (sources != null) { - for (CalendarSource s : sources) { - List calendars = s.getCalendars(); - if (calendars != null && !calendars.isEmpty()) { - for (Calendar c : calendars) { - if (!c.isReadOnly() && isCalendarVisible(c)) { - return c; - } + for (CalendarSource s : sources) { + List calendars = s.getCalendars(); + if (calendars != null && !calendars.isEmpty()) { + for (Calendar c : calendars) { + if (!c.isReadOnly() && isCalendarVisible(c)) { + return c; } } } @@ -264,11 +260,11 @@ protected DateControl() { if (evt instanceof MouseEvent) { MouseEvent mouseEvent = (MouseEvent) evt; if (mouseEvent.getClickCount() == 2) { - showEntryDetails(param.getEntry(), param.getOwner(), param.getScreenY()); + showEntryDetails(param.getEntry(), param.getNode(), param.getOwner(), param.getScreenY()); return true; } } else { - showEntryDetails(param.getEntry(), param.getOwner(), param.getScreenY()); + showEntryDetails(param.getEntry(), param.getNode(), param.getOwner(), param.getScreenY()); return true; } @@ -307,7 +303,7 @@ protected DateControl() { Callback detailsCallback = getEntryDetailsCallback(); if (detailsCallback != null) { ContextMenuEvent ctxEvent = param.getContextMenuEvent(); - EntryDetailsParameter entryDetailsParam = new EntryDetailsParameter(ctxEvent, DateControl.this, entryView.getEntry(), entryView, ctxEvent.getScreenX(), ctxEvent.getScreenY()); + EntryDetailsParameter entryDetailsParam = new EntryDetailsParameter(ctxEvent, DateControl.this, entryView.getEntry(), entryView, getScene().getRoot(), ctxEvent.getScreenX(), ctxEvent.getScreenY()); detailsCallback.call(entryDetailsParam); } }); @@ -356,7 +352,10 @@ protected DateControl() { Calendar calendar = entry.getCalendar(); if (!calendar.isReadOnly()) { if (entry.isRecurrence()) { - entry.getRecurrenceSourceEntry().removeFromCalendar(); + Entry recurrenceSourceEntry = entry.getRecurrenceSourceEntry(); + if (recurrenceSourceEntry != null) { + recurrenceSourceEntry.removeFromCalendar(); + } } else { entry.removeFromCalendar(); } @@ -386,7 +385,6 @@ protected DateControl() { } if (!usesOwnContextMenu) { - evt.consume(); Callback callback = getContextMenuCallback(); if (callback != null) { Callback calendarProvider = getDefaultCalendarProvider(); @@ -399,9 +397,10 @@ protected DateControl() { ContextMenuParameter param = new ContextMenuParameter(evt, DateControl.this, calendar, time); ContextMenu menu = callback.call(param); if (menu != null) { - setContextMenu(menu); - menu.show(DateControl.this, evt.getScreenX(), evt.getScreenY()); + menu.show(getScene().getWindow(), evt.getScreenX(), evt.getScreenY()); } + + evt.consume(); } } }); @@ -542,14 +541,14 @@ public final void setDraggedEntry(DraggedEntry entry) { * note that the time passed to the factory will be adjusted based on the * current virtual grid settings (see {@link #virtualGridProperty()}). * - * @param time the time where the entry will be created (the entry start + * @param time the time point where the entry will be created (the entry start * time) * @return the new calendar entry or null if no entry could be created * @see #setEntryFactory(Callback) * @see #setVirtualGrid(VirtualGrid) */ public final Entry createEntryAt(ZonedDateTime time) { - return createEntryAt(time, null); + return createEntryAt(time, null, false); } /** @@ -570,6 +569,28 @@ public final Entry createEntryAt(ZonedDateTime time) { * @see #setVirtualGrid(VirtualGrid) */ public final Entry createEntryAt(ZonedDateTime time, Calendar calendar) { + return createEntryAt(time, calendar, false); + } + + /** + * Creates a new entry at the given time. The method delegates the actual + * instance creation to the entry factory (see + * {@link #entryFactoryProperty()}). The factory receives a parameter object + * that contains the calendar where the entry can be added, however the + * factory can choose to add the entry to any calendar it likes. Please note + * that the time passed to the factory will be adjusted based on the current + * virtual grid settings (see {@link #virtualGridProperty()}). + * + * @param time the time point where the entry will be created (the entry start + * time) + * @param calendar the calendar to which the new entry will be added (if null the + * default calendar provider will be invoked) + * @param initiallyHidden entry will be invisible until the application calls {@link Entry#setHidden(boolean)}. + * @return the new calendar entry or null if no entry could be created + * @see #setEntryFactory(Callback) + * @see #setVirtualGrid(VirtualGrid) + */ + public final Entry createEntryAt(ZonedDateTime time, Calendar calendar, boolean initiallyHidden) { requireNonNull(time); VirtualGrid grid = getVirtualGrid(); if (grid != null) { @@ -597,6 +618,7 @@ public final Entry createEntryAt(ZonedDateTime time, Calendar calendar) { * entry would not be shown to the user. */ setCalendarVisibility(calendar, true); + CreateEntryParameter param = new CreateEntryParameter(this, calendar, time); Callback> factory = getEntryFactory(); Entry entry = factory.call(param); @@ -607,6 +629,7 @@ public final Entry createEntryAt(ZonedDateTime time, Calendar calendar) { * at the given location. */ if (entry != null) { + entry.setHidden(initiallyHidden); entry.setCalendar(calendar); } @@ -615,6 +638,8 @@ public final Entry createEntryAt(ZonedDateTime time, Calendar calendar) { } else { Alert alert = new Alert(AlertType.WARNING); + alert.initOwner(this.getScene().getWindow()); + alert.initModality(Modality.WINDOW_MODAL); alert.setTitle(Messages.getString("DateControl.TITLE_CALENDAR_PROBLEM")); alert.setHeaderText(Messages.getString("DateControl.HEADER_TEXT_UNABLE_TO_CREATE_NEW_ENTRY")); String newLine = System.getProperty("line.separator"); @@ -668,7 +693,7 @@ public final void editEntry(Entry entry) { * becomes visible and brings up the detail editor / UI for the entry * (default is a popover). * - * @param entry the entry to show + * @param entry the entry to show * @param changeDate change the date of the control to the entry's start date */ public final void editEntry(Entry entry, boolean changeDate) { @@ -717,7 +742,7 @@ private void doEditEntry(Entry entry) { Point2D location = entryView.localToScreen(0, 0); Callback callback = getEntryDetailsCallback(); - EntryDetailsParameter param = new EntryDetailsParameter(null, this, entry, entryView, location.getX(), location.getY()); + EntryDetailsParameter param = new EntryDetailsParameter(null, this, entry, entryView, getScene().getRoot(), location.getX(), location.getY()); callback.call(param); } }); @@ -733,7 +758,7 @@ private void doBounceEntry(Entry entry) { private PopOver entryPopOver; - private void showEntryDetails(Entry entry, Node owner, double screenY) { + private void showEntryDetails(Entry entry, Node node, Node owner, double screenY) { Callback contentCallback = getEntryDetailsPopOverContentCallback(); if (contentCallback == null) { throw new IllegalStateException("No content callback found for entry popover"); @@ -741,7 +766,7 @@ private void showEntryDetails(Entry entry, Node owner, double screenY) { if (entryPopOver == null || entryPopOver.isDetached()) { entryPopOver = new PopOver(); - entryPopOver.setAnimated(false); // important, otherwise too many side-effects + entryPopOver.setAnimated(false); // important, otherwise too many side effects } EntryDetailsPopOverContentParameter param = new EntryDetailsPopOverContentParameter(entryPopOver, this, owner, entry); @@ -753,16 +778,16 @@ private void showEntryDetails(Entry entry, Node owner, double screenY) { entryPopOver.setContentNode(content); - ArrowLocation location = ViewHelper.findPopOverArrowLocation(owner); + ArrowLocation location = ViewHelper.findPopOverArrowLocation(node); if (location == null) { location = ArrowLocation.TOP_LEFT; } entryPopOver.setArrowLocation(location); - Point2D position = ViewHelper.findPopOverArrowPosition(owner, screenY, entryPopOver.getArrowSize(), location); + Point2D position = ViewHelper.findPopOverArrowPosition(node, screenY, entryPopOver.getArrowSize(), location); - entryPopOver.show(owner, position.getX(), position.getY()); + entryPopOver.show(node, position.getX(), position.getY()); } /** @@ -799,7 +824,7 @@ public DateControl getDateControl() { /** * The parameter object passed to the entry factory. It contains the most * important parameters for creating a new entry: the requesting date - * control, the time where the user performed a double click and the default + * control, the time point where the user performed a double click and the default * calendar. * * @see DateControl#entryFactoryProperty() @@ -873,8 +898,6 @@ public String toString() { *

* The code below shows the default entry factory that is set on every date * control. - * - * *

      * setEntryFactory(param -> {
      * 	DateControl control = param.getControl();
@@ -969,7 +992,7 @@ public DateControl getDateControl() {
      * account.
      *
      * 

Code Example

The code below shows the default implementation of - * this factory. Applications can choose to bring up a full featured user + * this factory. Applications can choose to bring up a full-featured user * interface / dialog to specify the exact location of the source (either * locally or over a network). A local calendar source might read its data * from an XML file while a remote source could load data from a web @@ -1046,7 +1069,7 @@ public EntryViewBase getEntryView() { } /** - * Convenience method to easily lookup the entry for which the view was + * Convenience method to easily look up the entry for which the view was * created. * * @return the calendar entry @@ -1056,7 +1079,7 @@ public Entry getEntry() { } /** - * Convenience method to easily lookup the calendar of the entry for + * Convenience method to easily look up the calendar of the entry for * which the view was created. * * @return the calendar @@ -1172,7 +1195,7 @@ public String toString() { /** * A property that stores a callback used for editing entries. If an edit operation will be executed - * on an entry then the callback will be invoked to determine if the operation is allowed. By default + * on an entry then the callback will be invoked to determine if the operation is allowed. By default, * all operations listed inside {@link EditOperation} are allowed. * * @return the property @@ -1211,8 +1234,6 @@ public final void setEntryEditPolicy(Callback polic * *

Code Example

The code below shows the default implementation of * this callback. - * - * *
      * setEntryContextMenuCallback(param -> {
      * 	EntryViewBase<?> entryView = param.getEntryView();
@@ -1290,7 +1311,7 @@ public static final class ContextMenuParameter extends ContextMenuParameterBase
          * @param dateControl the date control where the event occurred
          * @param calendar    the (default) calendar where newly created entries should
          *                    be added (can be null if no editable calendar was found)
-         * @param time        the time where the mouse click occurred
+         * @param time        the time point where the mouse click occurred
          */
         public ContextMenuParameter(ContextMenuEvent evt, DateControl dateControl, Calendar calendar, ZonedDateTime time) {
             super(evt, dateControl);
@@ -1310,7 +1331,7 @@ public Calendar getCalendar() {
         }
 
         /**
-         * The time where the mouse click occurred.
+         * The time point where the mouse click occurred.
          *
          * @return the time shown at the mouse click location
          */
@@ -1336,8 +1357,6 @@ public String toString() {
      * 

Code Example

*

* The code below shows a part of the default implementation: - * - * *

      * setContextMenuCallback(param -> {
      * 	ContextMenu menu = new ContextMenu();
@@ -1382,7 +1401,7 @@ public final void setContextMenuCallback(Callback
      * setDefaultCalendarProvider(control -> {
      * 	List<CalendarSource> sources = getCalendarSources();
@@ -1459,6 +1476,7 @@ private abstract static class DetailsParameter {
         private final Node owner;
         private final double screenX;
         private final double screenY;
+        private final Node node;
 
         /**
          * Constructs a new parameter object.
@@ -1472,9 +1490,10 @@ private abstract static class DetailsParameter {
          * @param screenX    the screen location where the event occurred
          * @param screenY    the screen location where the event occurred
          */
-        public DetailsParameter(InputEvent inputEvent, DateControl control, Node owner, double screenX, double screenY) {
+        public DetailsParameter(InputEvent inputEvent, DateControl control, Node node, Node owner, double screenX, double screenY) {
             this.inputEvent = inputEvent;
             this.dateControl = requireNonNull(control);
+            this.node = requireNonNull(node);
             this.owner = requireNonNull(owner);
             this.screenX = screenX;
             this.screenY = screenY;
@@ -1484,14 +1503,25 @@ public DetailsParameter(InputEvent inputEvent, DateControl control, Node owner,
          * Returns the node that should be used as the owner of a dialog /
          * popover. We should not use the entry view as the owner of a dialog /
          * popover because views come and go. We need something that lives
-         * longer.
+         * longer. A good candidate will be the root node of the scene.
          *
-         * @return an owner node for the details dialog / popover
+         * @return an owner node for the detail dialog / popover
          */
         public Node getOwner() {
             return owner;
         }
 
+        /**
+         * Returns the node that will be used for calculating the position
+         * of the detail dialog / popover. Popovers will point an arrow at the
+         * given node.
+         *
+         * @return the annotated node
+         */
+        public Node getNode() {
+            return node;
+        }
+
         /**
          * The screen X location where the event occurred.
          *
@@ -1547,13 +1577,14 @@ public final static class EntryDetailsParameter extends DetailsParameter {
          *                   selection)
          * @param control    the control where the event occurred
          * @param entry      the entry for which details are requested
+         * @param node       the node to which the popover will be placed relative too (when using popovers)
          * @param owner      a node that can be used as an owner for the dialog or
          *                   popover
          * @param screenX    the screen location where the event occurred
          * @param screenY    the screen location where the event occurred
          */
-        public EntryDetailsParameter(InputEvent inputEvent, DateControl control, Entry entry, Node owner, double screenX, double screenY) {
-            super(inputEvent, control, owner, screenX, screenY);
+        public EntryDetailsParameter(InputEvent inputEvent, DateControl control, Entry entry, Node node, Node owner, double screenX, double screenY) {
+            super(inputEvent, control, node, owner, screenX, screenY);
             this.entry = entry;
         }
 
@@ -1584,13 +1615,13 @@ public final static class DateDetailsParameter extends DetailsParameter {
          *                   selection)
          * @param control    the control where the event occurred
          * @param date       the date for which details are required
-         * @param owner      a node that can be used as an owner for the dialog or
-         *                   popover
+         * @param node       the annotated node (popover will point at it with an arrow)
+         * @param owner      a node that can be used as an owner for the dialog or popover
          * @param screenX    the screen location where the event occurred
          * @param screenY    the screen location where the event occurred
          */
-        public DateDetailsParameter(InputEvent inputEvent, DateControl control, Node owner, LocalDate date, double screenX, double screenY) {
-            super(inputEvent, control, owner, screenX, screenY);
+        public DateDetailsParameter(InputEvent inputEvent, DateControl control, Node node, Node owner, LocalDate date, double screenX, double screenY) {
+            super(inputEvent, control, node, owner, screenX, screenY);
             this.localDate = requireNonNull(date);
         }
 
@@ -1732,7 +1763,7 @@ public final static class EntryDetailsPopOverContentParameter {
         /**
          * Constructs a new parameter object.
          *
-         * @param popOver the pop over for which details will be created
+         * @param popOver the popover for which details will be created
          * @param control the control where the event occurred
          * @param node    the node where the event occurred
          * @param entry   the entry for which details will be shown
@@ -1849,9 +1880,8 @@ public final LocalDate getToday() {
 
     /**
      * A flag used to indicate that the view will mark the area that represents
-     * the value of {@link #todayProperty()}. By default this area will be
+     * the value of {@link #todayProperty()}. By default, this area will be
      * filled with a different color (red) than the rest (white).
-     *
      * All Day View Today
      *
      * @return true if today will be shown differently
@@ -2125,7 +2155,7 @@ public final WeekFields getWeekFields() {
     }
 
     /**
-     * A convenience method to lookup the first day of the week ("Monday" in
+     * A convenience method to look up the first day of the week ("Monday" in
      * Germany, "Sunday" in the US). This method delegates to
      * {@link WeekFields#getFirstDayOfWeek()}.
      *
@@ -2143,7 +2173,7 @@ public final DayOfWeek getFirstDayOfWeek() {
      * currently attached to this date control. This is a convenience list that
      * "flattens" the two level structure of sources and their calendars. It is
      * a read-only list because calendars can not be added directly to a date
-     * control. Instead they are added to calendar sources and those sources are
+     * control. Instead, they are added to calendar sources and those sources are
      * then added to the control.
      *
      * @return the list of all calendars shown by this control
@@ -2341,7 +2371,7 @@ public enum Layout {
     private final ObjectProperty layout = new SimpleObjectProperty<>(this, "layout", Layout.STANDARD);
 
     /**
-     * Stores the strategy used by the view to layout the entries of several
+     * Stores the strategy used by the view to lay out the entries of several
      * calendars at once. The standard layout ignores the source calendar of an
      * entry and finds the next available place in the UI that satisfies the
      * time bounds of the entry. The {@link Layout#SWIMLANE} strategy allocates
@@ -2471,9 +2501,7 @@ public final WeakList getBoundDateControls() {
      */
     public final void unbindAll() {
         WeakList controls = getBoundDateControls();
-        Iterator iterator = controls.iterator();
-        while (iterator.hasNext()) {
-            DateControl next = iterator.next();
+        for (DateControl next : controls) {
             unbind(next);
         }
     }
@@ -2483,7 +2511,7 @@ public final void unbindAll() {
 
     /**
      * A property used to control whether the control allows the user to click on it or an element
-     * inside of it in order to "jump" to another screen with more detail. Example: in the {@link CalendarView}
+     * inside it in order to "jump" to another screen with more detail. Example: in the {@link CalendarView}
      * the user can click on the "day of month" label of a cell inside the {@link MonthSheetView} in
      * order to switch to the {@link DayPage} where the user will see all entries scheduled for that day.
      *
@@ -3272,7 +3300,7 @@ public String getCategory() {
 
         items.add(new Item() {
             @Override
-            public Optional> getObservableValue() {
+            public Optional> getObservableValue() {
                 return Optional.empty();
             }
 
@@ -3313,7 +3341,7 @@ public String getCategory() {
 
         items.add(new Item() {
             @Override
-            public Optional> getObservableValue() {
+            public Optional> getObservableValue() {
                 return Optional.empty();
             }
 
diff --git a/CalendarFXView/src/main/java/com/calendarfx/view/DayView.java b/CalendarFXView/src/main/java/com/calendarfx/view/DayView.java
index fb057519..0136330e 100644
--- a/CalendarFXView/src/main/java/com/calendarfx/view/DayView.java
+++ b/CalendarFXView/src/main/java/com/calendarfx/view/DayView.java
@@ -70,7 +70,7 @@ public DayView() {
 
 		setEntryViewFactory(DayEntryView::new);
 
-		new CreateDeleteHandler(this);
+		new DeleteHandler(this);
 	}
 
 	@Override
diff --git a/CalendarFXView/src/main/java/com/calendarfx/view/DayViewBase.java b/CalendarFXView/src/main/java/com/calendarfx/view/DayViewBase.java
index e422664b..d065266b 100644
--- a/CalendarFXView/src/main/java/com/calendarfx/view/DayViewBase.java
+++ b/CalendarFXView/src/main/java/com/calendarfx/view/DayViewBase.java
@@ -1062,6 +1062,26 @@ public final void setLassoEnd(Instant lassoEnd) {
         this.lassoEnd.set(lassoEnd);
     }
 
+    private final BooleanProperty enableStartAndEndTimesFlip = new SimpleBooleanProperty(this, "enableStartAndEndTimesFlip", true);
+
+    public final boolean isEnableStartAndEndTimesFlip() {
+        return enableStartAndEndTimesFlip.get();
+    }
+
+    /**
+     * Determines whether the user can flip the start and the end time of an entry by
+     * dragging either one to the other side of the other one.
+     *
+     * @return whether start and end times can be flipped
+     */
+    public final BooleanProperty enableStartAndEndTimesFlipProperty() {
+        return enableStartAndEndTimesFlip;
+    }
+
+    public final void setEnableStartAndEndTimesFlip(boolean enableStartAndEndTimesFlip) {
+        this.enableStartAndEndTimesFlip.set(enableStartAndEndTimesFlip);
+    }
+
     /**
      * Invokes {@link DateControl#bind(DateControl, boolean)} and adds some more
      * bindings between this control and the given control.
diff --git a/CalendarFXView/src/main/java/com/calendarfx/view/DeleteHandler.java b/CalendarFXView/src/main/java/com/calendarfx/view/DeleteHandler.java
new file mode 100644
index 00000000..925855e0
--- /dev/null
+++ b/CalendarFXView/src/main/java/com/calendarfx/view/DeleteHandler.java
@@ -0,0 +1,62 @@
+/*
+ *  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.Entry;
+import javafx.scene.input.KeyEvent;
+
+import static java.util.Objects.requireNonNull;
+
+class DeleteHandler {
+
+    protected final DateControl dateControl;
+
+    public DeleteHandler(DateControl control) {
+        this.dateControl = requireNonNull(control);
+        dateControl.addEventHandler(KeyEvent.KEY_PRESSED, this::deleteEntries);
+    }
+
+    private void deleteEntries(KeyEvent evt) {
+        switch (evt.getCode()) {
+            case DELETE:
+            case BACK_SPACE:
+                for (Entry entry : dateControl.getSelections()) {
+                    if (!dateControl.getEntryEditPolicy().call(new DateControl.EntryEditParameter(dateControl, entry, DateControl.EditOperation.DELETE))) {
+                        continue;
+                    }
+                    if (entry.isRecurrence()) {
+                        entry = entry.getRecurrenceSourceEntry();
+                    }
+                    if (!dateControl.getEntryEditPolicy().call(new DateControl.EntryEditParameter(dateControl, entry, DateControl.EditOperation.DELETE))) {
+                        continue;
+                    }
+
+                    Calendar calendar = entry.getCalendar();
+                    if (calendar != null && !calendar.isReadOnly()) {
+                        entry.removeFromCalendar();
+                    }
+                }
+                dateControl.clearSelection();
+                break;
+            case F5:
+                dateControl.refreshData();
+            default:
+                break;
+        }
+    }
+}
\ No newline at end of file
diff --git a/CalendarFXView/src/main/java/com/calendarfx/view/EntryViewBase.java b/CalendarFXView/src/main/java/com/calendarfx/view/EntryViewBase.java
index 5c5c77b2..891e84b7 100644
--- a/CalendarFXView/src/main/java/com/calendarfx/view/EntryViewBase.java
+++ b/CalendarFXView/src/main/java/com/calendarfx/view/EntryViewBase.java
@@ -116,6 +116,10 @@ public abstract class EntryViewBase extends CalendarFXCon
 
     private final WeakListChangeListener weakStyleListener = new WeakListChangeListener<>(styleListener);
 
+    private final InvalidationListener bindVisibilityListener = it -> bindVisibility();
+
+    private final WeakInvalidationListener weakBindVisibilityListener = new WeakInvalidationListener(bindVisibilityListener);
+
     /**
      * Constructs a new view for the given entry.
      *
@@ -135,6 +139,7 @@ protected EntryViewBase(Entry entry) {
             if (evt.getButton().equals(PRIMARY) && evt.isStillSincePress() && evt.getClickCount() == getDetailsClickCount()) {
                 showDetails(evt, evt.getScreenX(), evt.getScreenY());
             }
+            evt.consume();
         });
 
         addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, evt -> {
@@ -149,12 +154,11 @@ protected EntryViewBase(Entry entry) {
                     EntryContextMenuParameter param = new EntryContextMenuParameter(evt, dateControl, EntryViewBase.this);
                     ContextMenu menu = callback.call(param);
                     if (menu != null) {
-                        setContextMenu(menu);
-                        menu.show(this, evt.getScreenX(), evt.getScreenY());
+                        menu.show(getScene().getWindow(), evt.getScreenX(), evt.getScreenY());
+                        evt.consume();
                     }
                 }
             }
-            evt.consume();
         });
 
         @SuppressWarnings("unchecked")
@@ -228,9 +232,12 @@ protected EntryViewBase(Entry entry) {
 
         addEventHandler(MouseEvent.MOUSE_PRESSED, this::performSelection);
 
-        bindEntry(entry);
+        bindEntry();
+        bindVisibility();
 
-        layerProperty().addListener(weakLayerListener);
+        layerProperty().addListener(weakBindVisibilityListener);
+
+        visibleProperty().addListener(it -> System.out.println("Entry: " + entry.getTitle() + ", visible = " + isVisible()));
     }
 
     private final IntegerProperty detailsClickCount = new SimpleIntegerProperty(this, "detailsClickCount", 2);
@@ -240,12 +247,11 @@ public final int getDetailsClickCount() {
     }
 
     /**
-     * Determins the click count that is required to trigger the
+     * Determines the click count that is required to trigger the
      * "show details" action.
      *
-     * @see DateControl#entryDetailsCallbackProperty()
-     *
      * @return the "show details" click count
+     * @see DateControl#entryDetailsCallbackProperty()
      */
     public final IntegerProperty detailsClickCountProperty() {
         return detailsClickCount;
@@ -264,15 +270,7 @@ public final Entry getEntry() {
         return entry;
     }
 
-    private final InvalidationListener calendarListener = it -> bindVisibility();
-
-    private final WeakInvalidationListener weakCalendarListener = new WeakInvalidationListener(calendarListener);
-
-    private final InvalidationListener layerListener = it -> bindVisibility();
-
-    private final WeakInvalidationListener weakLayerListener = new WeakInvalidationListener(layerListener);
-
-    private void bindEntry(Entry entry) {
+    private void bindEntry() {
         setStartDate(entry.getStartDate());
         setEndDate(entry.getEndDate());
         setStartTime(entry.getStartTime());
@@ -286,37 +284,44 @@ private void bindEntry(Entry entry) {
             getProperties().put("selected", true);
         }
 
-        entry.calendarProperty().addListener(weakCalendarListener);
+        entry.hiddenProperty().addListener(weakBindVisibilityListener);
+        entry.calendarProperty().addListener(weakBindVisibilityListener);
     }
 
     private void bindVisibility() {
         Entry entry = getEntry();
+
         T dateControl = getDateControl();
 
         if (entry != null && dateControl != null) {
             Calendar calendar = entry.getCalendar();
 
-            if (calendar != null) {
-                BooleanBinding binding = Bindings.and(dateControl.getCalendarVisibilityProperty(calendar), Bindings.not(hiddenProperty()));
+            // the entry view can be hidden
+            BooleanBinding binding = Bindings.createBooleanBinding(() -> !isHidden(), hiddenProperty());
 
-                binding = binding.and(entry.hiddenProperty().not());
+            if (calendar != null) {
+                // the calendar can be hidden
+                binding = binding.and(dateControl.getCalendarVisibilityProperty(calendar));
+            }
 
-                if (getLayer() != null) {
-                    binding = binding.and(Bindings.createBooleanBinding(this::isAssignedLayerVisible, dateControl.visibleLayersProperty()));
-                }
+            // the entry itself can also be hidden
+            binding = binding.and(entry.hiddenProperty().not());
 
-                if (dateControl instanceof DayViewBase) {
-                    /*
-                     * Day views support editing of an availability calendar. During editing the
-                     * entries might be shown, hidden, or become somewhat transparent.
-                     */
-                    DayViewBase dayView = (DayViewBase) dateControl;
+            if (getLayer() != null) {
+                binding = binding.and(Bindings.createBooleanBinding(this::isAssignedLayerVisible, dateControl.visibleLayersProperty()));
+            }
 
-                    binding = binding.and(dayView.editAvailabilityProperty().not().or(dayView.entryViewAvailabilityEditingBehaviourProperty().isEqualTo(AvailabilityEditingEntryBehaviour.HIDE).not()));
-                }
+            if (dateControl instanceof DayViewBase) {
+                /*
+                 * Day views support editing of an availability calendar. During editing the
+                 * entries might be shown, hidden, or become somewhat transparent.
+                 */
+                DayViewBase dayView = (DayViewBase) dateControl;
 
-                visibleProperty().bind(binding);
+                binding = binding.and(dayView.editAvailabilityProperty().not().or(dayView.entryViewAvailabilityEditingBehaviourProperty().isEqualTo(AvailabilityEditingEntryBehaviour.HIDE).not()));
             }
+
+            visibleProperty().bind(binding);
         }
     }
 
@@ -478,7 +483,7 @@ private void showDetails(InputEvent evt, double x, double y) {
          */
         if (control != null && getParent() != null) {
             Callback callback = control.getEntryDetailsCallback();
-            EntryDetailsParameter param = new EntryDetailsParameter(evt, control, getEntry(), this, x, y);
+            EntryDetailsParameter param = new EntryDetailsParameter(evt, control, getEntry(), this, getScene().getRoot(), x, y);
             callback.call(param);
         }
     }
@@ -966,7 +971,8 @@ public final void setLayer(Layer layer) {
      * Width percentage is only used for width computation when {@link #prefWidthProperty()}
      * of view entry has no defined value and when {@link #alignmentStrategyProperty()}
      * is not {@link AlignmentStrategy#FILL}.
-     *

+ *

+ * * @return the entry percentage width */ public final DoubleProperty widthPercentageProperty() { @@ -1053,9 +1059,9 @@ public final void setHeightLayoutStrategy(HeightLayoutStrategy heightLayoutStrat * require the entry to simply use the preferred width of the view and align the * entry's view on the left, the center, or the middle. *

- * If the time intervals of two entries are overlapping then the entries might - * be placed in two columns. The alignment strategy would then determine the layout - * of the entry within its column. + * If the time intervals of two entries are overlapping then the entries might + * be placed in two columns. The alignment strategy would then determine the layout + * of the entry within its column. *

* * @see #setAlignmentStrategy(AlignmentStrategy) @@ -1238,6 +1244,8 @@ private void performSelection(MouseEvent evt) { } getProperties().remove(disableFocusHandlingKey); + + evt.consume(); } } diff --git a/CalendarFXView/src/main/java/com/calendarfx/view/MonthSheetView.java b/CalendarFXView/src/main/java/com/calendarfx/view/MonthSheetView.java index 9bca9ba8..9dbde35d 100644 --- a/CalendarFXView/src/main/java/com/calendarfx/view/MonthSheetView.java +++ b/CalendarFXView/src/main/java/com/calendarfx/view/MonthSheetView.java @@ -138,15 +138,10 @@ private ContextMenu createContextMenu() { ContextMenu contextMenu = new ContextMenu(); MenuItem newEntry = new MenuItem(Messages.getString("MonthSheetView.ADD_NEW_EVENT")); 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); - + createEntryAt(ZonedDateTime.of(date, LocalTime.of(12, 0), getZoneId())); }); + contextMenu.getItems().add(newEntry); contextMenu.getItems().add(new SeparatorMenuItem()); diff --git a/CalendarFXView/src/main/java/com/calendarfx/view/MonthView.java b/CalendarFXView/src/main/java/com/calendarfx/view/MonthView.java index a70e143a..d2fe31ea 100644 --- a/CalendarFXView/src/main/java/com/calendarfx/view/MonthView.java +++ b/CalendarFXView/src/main/java/com/calendarfx/view/MonthView.java @@ -57,7 +57,7 @@ public MonthView() { setEntryViewFactory(MonthEntryView::new); - new CreateDeleteHandler(this); + new CreateAndDeleteHandler(this); getSelectedDates().addListener((Observable it) -> { if (getSelectedDates().size() == 1) { diff --git a/CalendarFXView/src/main/java/com/calendarfx/view/RecurrenceView.java b/CalendarFXView/src/main/java/com/calendarfx/view/RecurrenceView.java index 2ddf1317..06b80b21 100644 --- a/CalendarFXView/src/main/java/com/calendarfx/view/RecurrenceView.java +++ b/CalendarFXView/src/main/java/com/calendarfx/view/RecurrenceView.java @@ -17,7 +17,7 @@ package com.calendarfx.view; import com.calendarfx.model.Entry; -import com.calendarfx.util.Util; + import impl.com.calendarfx.view.RecurrenceViewSkin; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; diff --git a/CalendarFXView/src/main/java/com/calendarfx/view/WeekDayHeaderView.java b/CalendarFXView/src/main/java/com/calendarfx/view/WeekDayHeaderView.java index 635ef1e7..ef24ee75 100644 --- a/CalendarFXView/src/main/java/com/calendarfx/view/WeekDayHeaderView.java +++ b/CalendarFXView/src/main/java/com/calendarfx/view/WeekDayHeaderView.java @@ -16,8 +16,8 @@ package com.calendarfx.view; -import com.calendarfx.util.Util; import impl.com.calendarfx.view.WeekDayHeaderViewSkin; +import impl.com.calendarfx.view.util.Util; import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; diff --git a/CalendarFXView/src/main/java/com/calendarfx/view/popover/EntryDetailsView.java b/CalendarFXView/src/main/java/com/calendarfx/view/popover/EntryDetailsView.java index 8fd86551..6f2ee7b1 100644 --- a/CalendarFXView/src/main/java/com/calendarfx/view/popover/EntryDetailsView.java +++ b/CalendarFXView/src/main/java/com/calendarfx/view/popover/EntryDetailsView.java @@ -17,12 +17,12 @@ package com.calendarfx.view.popover; import com.calendarfx.model.Entry; -import com.calendarfx.util.Util; import com.calendarfx.view.DateControl; import com.calendarfx.view.Messages; import com.calendarfx.view.RecurrenceView; import com.calendarfx.view.TimeField; import impl.com.calendarfx.view.ZoneIdStringConverter; +import impl.com.calendarfx.view.util.Util; import javafx.beans.InvalidationListener; import javafx.beans.WeakInvalidationListener; import javafx.beans.binding.Bindings; diff --git a/CalendarFXView/src/main/java/com/calendarfx/view/popover/EntryPopOverContentPane.java b/CalendarFXView/src/main/java/com/calendarfx/view/popover/EntryPopOverContentPane.java index 164b0baf..0a2dedff 100644 --- a/CalendarFXView/src/main/java/com/calendarfx/view/popover/EntryPopOverContentPane.java +++ b/CalendarFXView/src/main/java/com/calendarfx/view/popover/EntryPopOverContentPane.java @@ -19,6 +19,7 @@ import com.calendarfx.model.Entry; import com.calendarfx.view.CalendarView; import com.calendarfx.view.DateControl; +import com.calendarfx.view.DayViewBase; import com.calendarfx.view.Messages; import javafx.beans.InvalidationListener; import javafx.beans.WeakInvalidationListener; @@ -42,7 +43,7 @@ public class EntryPopOverContentPane extends PopOverContentPane { private final WeakInvalidationListener weakHideListener = new WeakInvalidationListener(hideListener); private final InvalidationListener fullDayListener = obs -> { - if (getEntry().isFullDay() && !getPopOver().isDetached()) { + if (getEntry().isFullDay() && !getPopOver().isDetached() && getDateControl() instanceof DayViewBase) { getPopOver().setDetached(true); } }; diff --git a/CalendarFXView/src/main/java/com/calendarfx/view/print/PrintView.java b/CalendarFXView/src/main/java/com/calendarfx/view/print/PrintView.java index 5e6e89b1..5f35b14b 100644 --- a/CalendarFXView/src/main/java/com/calendarfx/view/print/PrintView.java +++ b/CalendarFXView/src/main/java/com/calendarfx/view/print/PrintView.java @@ -18,12 +18,12 @@ import com.calendarfx.model.CalendarSource; import com.calendarfx.util.LoggingDomain; -import com.calendarfx.util.Util; import com.calendarfx.view.CalendarView; import com.calendarfx.view.DateControl; import com.calendarfx.view.Messages; import com.calendarfx.view.SourceView; import impl.com.calendarfx.view.print.PrintViewSkin; +import impl.com.calendarfx.view.util.Util; import javafx.beans.InvalidationListener; import javafx.beans.binding.Bindings; import javafx.beans.property.ObjectProperty; @@ -32,7 +32,13 @@ import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.event.EventHandler; -import javafx.print.*; +import javafx.print.JobSettings; +import javafx.print.PageLayout; +import javafx.print.PageOrientation; +import javafx.print.Paper; +import javafx.print.PrintColor; +import javafx.print.Printer; +import javafx.print.PrinterJob; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; diff --git a/CalendarFXView/src/main/java/com/calendarfx/view/print/TimeRangeView.java b/CalendarFXView/src/main/java/com/calendarfx/view/print/TimeRangeView.java index a0907b16..6c965928 100644 --- a/CalendarFXView/src/main/java/com/calendarfx/view/print/TimeRangeView.java +++ b/CalendarFXView/src/main/java/com/calendarfx/view/print/TimeRangeView.java @@ -16,10 +16,11 @@ package com.calendarfx.view.print; -import com.calendarfx.util.Util; + import com.calendarfx.view.CalendarView; import com.calendarfx.view.print.TimeRangeField.TimeRangeFieldValue; import impl.com.calendarfx.view.print.TimeRangeViewSkin; +import impl.com.calendarfx.view.util.Util; import javafx.beans.InvalidationListener; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyIntegerProperty; diff --git a/CalendarFXView/src/main/java/impl/com/calendarfx/view/AgendaViewSkin.java b/CalendarFXView/src/main/java/impl/com/calendarfx/view/AgendaViewSkin.java index e2f7e6a4..8379b279 100644 --- a/CalendarFXView/src/main/java/impl/com/calendarfx/view/AgendaViewSkin.java +++ b/CalendarFXView/src/main/java/impl/com/calendarfx/view/AgendaViewSkin.java @@ -154,13 +154,18 @@ private void updateList(String reason) { Map>> dataMap = new HashMap<>(); dataLoader.loadEntries(dataMap); + List listEntries = new ArrayList<>(); for (LocalDate date : dataMap.keySet()) { AgendaEntry listViewEntry = new AgendaEntry(date); for (Entry entry : dataMap.get(date)) { - listViewEntry.getEntries().add(entry); + if (!entry.isHidden()) { + listViewEntry.getEntries().add(entry); + } + } + if (!listViewEntry.getEntries().isEmpty()) { + listEntries.add(listViewEntry); } - listEntries.add(listViewEntry); } Collections.sort(listEntries); diff --git a/CalendarFXView/src/main/java/impl/com/calendarfx/view/AllDayViewSkin.java b/CalendarFXView/src/main/java/impl/com/calendarfx/view/AllDayViewSkin.java index e39c0189..77643929 100644 --- a/CalendarFXView/src/main/java/impl/com/calendarfx/view/AllDayViewSkin.java +++ b/CalendarFXView/src/main/java/impl/com/calendarfx/view/AllDayViewSkin.java @@ -21,13 +21,13 @@ import com.calendarfx.model.CalendarSource; import com.calendarfx.model.Entry; import com.calendarfx.util.LoggingDomain; -import com.calendarfx.util.Util; import com.calendarfx.view.AllDayEntryView; import com.calendarfx.view.AllDayView; import com.calendarfx.view.DraggedEntry; import com.calendarfx.view.EntryViewBase; import impl.com.calendarfx.view.util.Placement; import impl.com.calendarfx.view.util.TimeBoundsResolver; +import impl.com.calendarfx.view.util.Util; import javafx.beans.InvalidationListener; import javafx.geometry.Insets; import javafx.scene.Node; diff --git a/CalendarFXView/src/main/java/impl/com/calendarfx/view/DayViewEditController.java b/CalendarFXView/src/main/java/impl/com/calendarfx/view/DayViewEditController.java index c6e32ae3..e68e647c 100644 --- a/CalendarFXView/src/main/java/impl/com/calendarfx/view/DayViewEditController.java +++ b/CalendarFXView/src/main/java/impl/com/calendarfx/view/DayViewEditController.java @@ -19,8 +19,8 @@ import com.calendarfx.model.Calendar; import com.calendarfx.model.Entry; import com.calendarfx.util.LoggingDomain; -import com.calendarfx.view.DateControl; import com.calendarfx.view.DateControl.EditOperation; +import com.calendarfx.view.DateControl.EntryEditParameter; import com.calendarfx.view.DayEntryView; import com.calendarfx.view.DayView; import com.calendarfx.view.DayViewBase; @@ -37,6 +37,7 @@ import javafx.scene.Cursor; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; +import javafx.util.Callback; import java.time.DayOfWeek; import java.time.Duration; @@ -55,6 +56,7 @@ public class DayViewEditController { private static final Logger LOGGER = LoggingDomain.EDITING; private boolean dragging; + private boolean creating; private final DayViewBase dayViewBase; private DayEntryView dayEntryView; private Entry entry; @@ -73,18 +75,18 @@ public DayViewEditController(DayViewBase dayView) { // mouse released is very important for us. register with the scene, so we get that in any case. if (dayView.getScene() != null) { dayView.addEventFilter(MouseEvent.MOUSE_RELEASED, mouseReleasedHandler); - dayView.addEventFilter(MouseEvent.MOUSE_EXITED, mouseReleasedHandler); +// dayView.addEventFilter(MouseEvent.MOUSE_EXITED, mouseReleasedHandler); } // also register with the scene property. Mostly to remove our event filter if the component gets destroyed. dayView.sceneProperty().addListener(((observable, oldValue, newValue) -> { if (oldValue != null) { oldValue.removeEventFilter(MouseEvent.MOUSE_RELEASED, mouseReleasedHandler); - oldValue.removeEventFilter(MouseEvent.MOUSE_EXITED, mouseReleasedHandler); +// oldValue.removeEventFilter(MouseEvent.MOUSE_EXITED, mouseReleasedHandler); } if (newValue != null) { newValue.addEventFilter(MouseEvent.MOUSE_RELEASED, mouseReleasedHandler); - newValue.addEventFilter(MouseEvent.MOUSE_EXITED, mouseReleasedHandler); +// newValue.addEventFilter(MouseEvent.MOUSE_EXITED, mouseReleasedHandler); } })); dayView.addEventFilter(MouseEvent.MOUSE_MOVED, this::mouseMoved); @@ -123,17 +125,17 @@ private void initDragModeAndHandle(MouseEvent evt) { LOGGER.finer("y-coordinate inside entry view: " + y); if (y > dayEntryView.getHeight() - 5) { - if (dayEntryView.getHeightLayoutStrategy().equals(HeightLayoutStrategy.USE_START_AND_END_TIME) && dayViewBase.getEntryEditPolicy().call(new DateControl.EntryEditParameter(dayViewBase, entry, EditOperation.CHANGE_END))) { + if (dayEntryView.getHeightLayoutStrategy().equals(HeightLayoutStrategy.USE_START_AND_END_TIME) && dayViewBase.getEntryEditPolicy().call(new EntryEditParameter(dayViewBase, entry, EditOperation.CHANGE_END))) { dragMode = DraggedEntry.DragMode.END_TIME; handle = Handle.BOTTOM; } } else if (y < 5) { - if (dayEntryView.getHeightLayoutStrategy().equals(HeightLayoutStrategy.USE_START_AND_END_TIME) && dayViewBase.getEntryEditPolicy().call(new DateControl.EntryEditParameter(dayViewBase, entry, EditOperation.CHANGE_START))) { + if (dayEntryView.getHeightLayoutStrategy().equals(HeightLayoutStrategy.USE_START_AND_END_TIME) && dayViewBase.getEntryEditPolicy().call(new EntryEditParameter(dayViewBase, entry, EditOperation.CHANGE_START))) { dragMode = DraggedEntry.DragMode.START_TIME; handle = Handle.TOP; } } else { - if (dayViewBase.getEntryEditPolicy().call(new DateControl.EntryEditParameter(dayViewBase, entry, EditOperation.MOVE))) { + if (dayViewBase.getEntryEditPolicy().call(new EntryEditParameter(dayViewBase, entry, EditOperation.MOVE))) { dragMode = DraggedEntry.DragMode.START_AND_END_TIME; handle = Handle.CENTER; } @@ -195,7 +197,12 @@ private void mousePressed(MouseEvent evt) { LOGGER.finer("mouse event y-coordinate:" + evt.getY()); LOGGER.finer("time: " + dayViewBase.getZonedDateTimeAt(evt.getX(), evt.getY(), dayViewBase.getZoneId())); + boolean initiallyHideDraggedEntry = false; + creating = false; + if (!dayViewBase.isScrollingEnabled() && evt.getTarget() instanceof DayView) { + creating = true; + Optional calendar = dayViewBase.getCalendarAt(evt.getX(), evt.getY()); Instant instantAt = dayViewBase.getInstantAt(evt); @@ -205,7 +212,7 @@ private void mousePressed(MouseEvent evt) { } ZonedDateTime time = ZonedDateTime.ofInstant(instantAt, dayViewBase.getZoneId()); - entry = dayViewBase.createEntryAt(time, calendar.orElse(null)); + entry = dayViewBase.createEntryAt(time, calendar.orElse(null), true); if (virtualGrid != null) { entry.setInterval(entry.getInterval().withEndTime(entry.getInterval().getStartTime().plus(virtualGrid.getAmount(), virtualGrid.getUnit()))); @@ -228,6 +235,9 @@ private void mousePressed(MouseEvent evt) { dragging = true; showEntryDetails = true; } + + initiallyHideDraggedEntry = true; + } else if (evt.getTarget() instanceof EntryViewBase) { initDragModeAndHandle(evt); } else { @@ -237,9 +247,11 @@ private void mousePressed(MouseEvent evt) { LOGGER.finer("drag mode: " + dragMode); LOGGER.finer("handle: " + handle); + Callback entryEditPolicy = dayViewBase.getEntryEditPolicy(); + switch (dragMode) { case START_AND_END_TIME: - if (dayViewBase.getEntryEditPolicy().call(new DateControl.EntryEditParameter(dayViewBase, entry, EditOperation.MOVE))) { + if (entryEditPolicy.call(new EntryEditParameter(dayViewBase, entry, EditOperation.MOVE))) { dragging = true; if (dayEntryView != null) { dayEntryView.getProperties().put("dragged", true); @@ -257,7 +269,7 @@ private void mousePressed(MouseEvent evt) { } break; case END_TIME: - if (dayViewBase.getEntryEditPolicy().call(new DateControl.EntryEditParameter(dayViewBase, entry, EditOperation.CHANGE_END))) { + if (entryEditPolicy.call(new EntryEditParameter(dayViewBase, entry, EditOperation.CHANGE_END))) { dragging = true; if (dayEntryView != null) { dayEntryView.getProperties().put("dragged-end", true); @@ -265,7 +277,7 @@ private void mousePressed(MouseEvent evt) { } break; case START_TIME: - if (dayViewBase.getEntryEditPolicy().call(new DateControl.EntryEditParameter(dayViewBase, entry, EditOperation.CHANGE_START))) { + if (entryEditPolicy.call(new EntryEditParameter(dayViewBase, entry, EditOperation.CHANGE_START))) { dragging = true; if (dayEntryView != null) { dayEntryView.getProperties().put("dragged-start", true); @@ -283,6 +295,7 @@ private void mousePressed(MouseEvent evt) { if (dayViewBase != null) { DraggedEntry draggedEntry = new DraggedEntry(entry, dragMode); draggedEntry.setOffsetDuration(offsetDuration); + draggedEntry.setHidden(initiallyHideDraggedEntry); dayViewBase.setDraggedEntry(draggedEntry); } } @@ -324,15 +337,23 @@ private void mouseReleased(MouseEvent evt) { DraggedEntry draggedEntry = dayViewBase.getDraggedEntry(); if (draggedEntry != null) { - entry.setInterval(draggedEntry.getInterval()); - dayViewBase.setDraggedEntry(null); - if (dayViewBase.isShowDetailsUponEntryCreation() && showEntryDetails) { - dayViewBase.fireEvent(new RequestEvent(dayViewBase, dayViewBase, entry)); + if (!draggedEntry.isHidden()) { + entry.setInterval(draggedEntry.getInterval()); + dayViewBase.setDraggedEntry(null); + if (dayViewBase.isShowDetailsUponEntryCreation() && showEntryDetails) { + dayViewBase.fireEvent(new RequestEvent(dayViewBase, dayViewBase, entry)); + } + } else { + entry.removeFromCalendar(); } } } private void mouseDragged(MouseEvent evt) { + if (evt.isStillSincePress()) { + return; + } + if (!dayViewBase.isScrollingEnabled()) { setLassoEnd(snapToGrid(dayViewBase.getInstantAt(evt), dayViewBase.getAvailabilityGrid(), true)); } @@ -346,6 +367,17 @@ private void mouseDragged(MouseEvent evt) { return; } + /* + * We might run in the sampler application. Then the entry view will not + * be inside a date control. + */ + DraggedEntry draggedEntry = dayViewBase.getDraggedEntry(); + + if (draggedEntry != null) { + draggedEntry.getOriginalEntry().setHidden(false); + draggedEntry.setHidden(false); + } + switch (dragMode) { case START_TIME: switch (handle) { @@ -544,7 +576,8 @@ public final void setLassoEnd(Instant lassoEnd) { this.lassoEnd.set(lassoEnd); } - private final ObjectProperty> onLassoFinished = new SimpleObjectProperty<>(this, "onLassoFinished", (start, end) ->{}); + private final ObjectProperty> onLassoFinished = new SimpleObjectProperty<>(this, "onLassoFinished", (start, end) -> { + }); public final BiConsumer getOnLassoFinished() { return onLassoFinished.get(); diff --git a/CalendarFXView/src/main/java/impl/com/calendarfx/view/DayViewSkin.java b/CalendarFXView/src/main/java/impl/com/calendarfx/view/DayViewSkin.java index 49333d67..a96c7d13 100644 --- a/CalendarFXView/src/main/java/impl/com/calendarfx/view/DayViewSkin.java +++ b/CalendarFXView/src/main/java/impl/com/calendarfx/view/DayViewSkin.java @@ -38,6 +38,7 @@ import com.calendarfx.view.VirtualGrid; import impl.com.calendarfx.view.util.Placement; import impl.com.calendarfx.view.util.TimeBoundsResolver; +import impl.com.calendarfx.view.util.Util; import impl.com.calendarfx.view.util.VisualBoundsResolver; import javafx.animation.FadeTransition; import javafx.application.Platform; @@ -550,11 +551,13 @@ protected void layoutChildrenStatic(double contentX, double contentY, double con line.setEndY(yy); } - // the dragged entry view - if (draggedEntryView != null) { - boolean showing = isRelevant(draggedEntryView.getEntry()); - draggedEntryView.setVisible(showing); - } +// // the dragged entry view +// if (draggedEntryView != null) { +// Entry draggedEntry = draggedEntryView.getEntry(); +// if (!draggedEntry.isHidden()) { +// //draggedEntry.setHidden(!isRelevant(draggedEntry)); +// } +// } layoutEntries(contentX, contentY, contentWidth, contentHeight); layoutCurrentTime(contentX, contentY, contentWidth); @@ -878,7 +881,7 @@ protected void entryIntervalChanged(CalendarEvent evt) { } private boolean removeEntryView(Entry entry) { - boolean removed = entryViewGroup.getChildren().removeIf(node -> { + boolean removed = Util.removeChildren(entryViewGroup, node -> { DayEntryView view = (DayEntryView) node; Entry removedEntry = entry; diff --git a/CalendarFXView/src/main/java/impl/com/calendarfx/view/MonthSheetViewSkin.java b/CalendarFXView/src/main/java/impl/com/calendarfx/view/MonthSheetViewSkin.java index 5d999804..b41b7f76 100644 --- a/CalendarFXView/src/main/java/impl/com/calendarfx/view/MonthSheetViewSkin.java +++ b/CalendarFXView/src/main/java/impl/com/calendarfx/view/MonthSheetViewSkin.java @@ -21,7 +21,7 @@ import com.calendarfx.model.CalendarSource; import com.calendarfx.model.Entry; import com.calendarfx.util.LoggingDomain; -import com.calendarfx.view.DateControl; +import com.calendarfx.view.DateControl.DateDetailsParameter; import com.calendarfx.view.DateSelectionModel; import com.calendarfx.view.MonthSheetView; import com.calendarfx.view.MonthSheetView.DateCell; @@ -409,9 +409,10 @@ public int hashCode() { private void showDateDetails(LocalDate date) { DateCell cell = cellMap.get(date); Bounds bounds = cell.localToScreen(cell.getLayoutBounds()); - Callback callback = getSkinnable().getDateDetailsCallback(); - DateControl.DateDetailsParameter param = new DateControl.DateDetailsParameter(null, getSkinnable(), cell, date, bounds.getMinX(), bounds.getMinY()); - callback.call(param); + Callback callback = getSkinnable().getDateDetailsCallback(); + if (callback != null) { + callback.call(new DateDetailsParameter(null, getSkinnable(), cell, cell.getScene().getRoot(), date, bounds.getMinX(), bounds.getMinY())); + } } private final WeakEventHandler weakCellClickedHandler = new WeakEventHandler<>(cellClickedHandler); diff --git a/CalendarFXView/src/main/java/impl/com/calendarfx/view/MonthViewSkin.java b/CalendarFXView/src/main/java/impl/com/calendarfx/view/MonthViewSkin.java index 44493040..e28640a7 100644 --- a/CalendarFXView/src/main/java/impl/com/calendarfx/view/MonthViewSkin.java +++ b/CalendarFXView/src/main/java/impl/com/calendarfx/view/MonthViewSkin.java @@ -21,12 +21,13 @@ import com.calendarfx.model.CalendarSource; import com.calendarfx.model.Entry; import com.calendarfx.util.LoggingDomain; -import com.calendarfx.util.Util; import com.calendarfx.view.EntryViewBase.Position; import com.calendarfx.view.Messages; import com.calendarfx.view.MonthEntryView; import com.calendarfx.view.MonthView; import com.calendarfx.view.RequestEvent; +import impl.com.calendarfx.view.util.Util; +import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.collections.FXCollections; @@ -624,7 +625,8 @@ class MonthDayEntriesPane extends Pane { this.week = week; this.day = day; - entries.addListener((Observable evt) -> update()); + // since JavaFX 19 this needs to be run later + entries.addListener((Observable evt) -> Platform.runLater(() -> update())); setMinSize(0, 0); setPrefSize(0, 0); @@ -654,7 +656,7 @@ public final ObservableList> getEntries() { } private void update() { - getChildren().removeIf(node -> node instanceof MonthEntryView); + Util.removeChildren(this, node -> node instanceof MonthEntryView); if (!entries.isEmpty()) { diff --git a/CalendarFXView/src/main/java/impl/com/calendarfx/view/RecurrenceViewSkin.java b/CalendarFXView/src/main/java/impl/com/calendarfx/view/RecurrenceViewSkin.java index 8419b9a9..c4bde10a 100644 --- a/CalendarFXView/src/main/java/impl/com/calendarfx/view/RecurrenceViewSkin.java +++ b/CalendarFXView/src/main/java/impl/com/calendarfx/view/RecurrenceViewSkin.java @@ -17,9 +17,9 @@ package impl.com.calendarfx.view; import com.calendarfx.util.LoggingDomain; -import com.calendarfx.util.Util; import com.calendarfx.view.Messages; import com.calendarfx.view.RecurrenceView; +import impl.com.calendarfx.view.util.Util; import javafx.beans.InvalidationListener; import javafx.beans.binding.Bindings; import javafx.geometry.HPos; @@ -370,8 +370,7 @@ private void updateView() { weekDaySaturdayButton.setSelected(isSelected(Day.SA, days)); weekDaySundayButton.setSelected(isSelected(Day.SU, days)); - summary.setText(Util.convertRFC2445ToText(rule, - getSkinnable().getStartDate())); + summary.setText(Util.convertRFC2445ToText(rule, getSkinnable().getStartDate())); } catch (IllegalArgumentException | DateTimeParseException e) { e.printStackTrace(); } diff --git a/CalendarFXView/src/main/java/impl/com/calendarfx/view/YearMonthViewSkin.java b/CalendarFXView/src/main/java/impl/com/calendarfx/view/YearMonthViewSkin.java index fb98ed20..2c7bddb2 100644 --- a/CalendarFXView/src/main/java/impl/com/calendarfx/view/YearMonthViewSkin.java +++ b/CalendarFXView/src/main/java/impl/com/calendarfx/view/YearMonthViewSkin.java @@ -21,12 +21,12 @@ import com.calendarfx.model.CalendarSource; import com.calendarfx.model.Entry; import com.calendarfx.util.LoggingDomain; -import com.calendarfx.util.Util; import com.calendarfx.view.DateControl; import com.calendarfx.view.DateControl.DateDetailsParameter; import com.calendarfx.view.Messages; import com.calendarfx.view.RequestEvent; import com.calendarfx.view.YearMonthView; +import impl.com.calendarfx.view.util.Util; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.geometry.HPos; @@ -507,11 +507,10 @@ private void handleSingleClick(MouseEvent evt, Node node, LocalDate date) { case NONE: break; case SHOW_DETAILS: - Callback callback = view - .getDateDetailsCallback(); - DateDetailsParameter param = new DateDetailsParameter(evt, view, - node, date, evt.getScreenX(), evt.getScreenY()); - callback.call(param); + Callback callback = view.getDateDetailsCallback(); + if (callback != null) { + callback.call(new DateDetailsParameter(evt, view, node, node.getScene().getRoot(), date, evt.getScreenX(), evt.getScreenY())); + } break; case PERFORM_SELECTION: boolean multiSelect = evt.isShiftDown() || evt.isShortcutDown(); diff --git a/CalendarFXView/src/main/java/impl/com/calendarfx/view/resources/ResourcesViewContainerSkin.java b/CalendarFXView/src/main/java/impl/com/calendarfx/view/resources/ResourcesViewContainerSkin.java index 00e284d5..b584df05 100644 --- a/CalendarFXView/src/main/java/impl/com/calendarfx/view/resources/ResourcesViewContainerSkin.java +++ b/CalendarFXView/src/main/java/impl/com/calendarfx/view/resources/ResourcesViewContainerSkin.java @@ -185,7 +185,7 @@ private void updateViewResourcesOverDates() { weekView.installDefaultLassoFinishedBehaviour(); - weekView.numberOfDaysProperty().bind(resourcesView.numberOfDaysProperty()); + weekView.numberOfDaysProperty().bindBidirectional(resourcesView.numberOfDaysProperty()); CalendarSource calendarSource = createCalendarSource(resource); weekView.getCalendarSources().setAll(calendarSource); diff --git a/CalendarFXView/src/main/java/impl/com/calendarfx/view/util/Util.java b/CalendarFXView/src/main/java/impl/com/calendarfx/view/util/Util.java index cb9f8301..cda1b239 100644 --- a/CalendarFXView/src/main/java/impl/com/calendarfx/view/util/Util.java +++ b/CalendarFXView/src/main/java/impl/com/calendarfx/view/util/Util.java @@ -16,18 +16,43 @@ package impl.com.calendarfx.view.util; +import com.calendarfx.view.DateControl; +import com.calendarfx.view.Messages; import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakListener; +import javafx.beans.property.Property; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.geometry.Orientation; +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.scene.Parent; import javafx.scene.control.MultipleSelectionModel; - +import javafx.scene.control.ScrollBar; +import javafx.scene.layout.Pane; +import net.fortuna.ical4j.model.Recur; +import net.fortuna.ical4j.model.WeekDay; +import net.fortuna.ical4j.transform.recurrence.Frequency; + +import java.lang.ref.WeakReference; +import java.text.MessageFormat; import java.time.DayOfWeek; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.format.FormatStyle; import java.time.temporal.ChronoField; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.stream.Collectors; import static java.time.temporal.ChronoField.DAY_OF_WEEK; import static java.time.temporal.ChronoField.DAY_OF_YEAR; @@ -40,37 +65,46 @@ import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; import static java.time.temporal.ChronoUnit.DAYS; -@SuppressWarnings("javadoc") +/** + * A collection of useful static methods for easy re-use in various + * places of the framework. + */ public final class Util { - public static boolean intersect(LocalDate aStart, LocalDate aEnd, - LocalDate bStart, LocalDate bEnd) { + public static boolean removeChildren(Pane parent, Predicate predicate) { + List list = new ArrayList<>(parent.getChildrenUnmodifiable().stream().filter(predicate.negate()).collect(Collectors.toList())); + boolean childrenWereRemoved = list.removeIf(predicate); + parent.getChildren().setAll(list); + return childrenWereRemoved; + } + + public static boolean removeChildren(Group group, Predicate predicate) { + List list = new ArrayList<>(group.getChildrenUnmodifiable()); + boolean childrenWereRemoved = list.removeIf(predicate); + group.getChildren().setAll(list); + return childrenWereRemoved; + } + public static boolean intersect(LocalDate aStart, LocalDate aEnd, LocalDate bStart, LocalDate bEnd) { // Same start time or same end time? if (aStart.equals(bStart) || aEnd.equals(bEnd)) { return true; } return aStart.isBefore(bEnd) && aEnd.isAfter(bStart); - } - public static boolean intersect(LocalTime aStart, LocalTime aEnd, - LocalTime bStart, LocalTime bEnd) { - + public static boolean intersect(LocalTime aStart, LocalTime aEnd, LocalTime bStart, LocalTime bEnd) { // Same start time or same end time? if (aStart.equals(bStart) || aEnd.equals(bEnd)) { return true; } return aStart.isBefore(bEnd) && aEnd.isAfter(bStart); - } - public static boolean intersect(ZonedDateTime aStart, ZonedDateTime aEnd, - ZonedDateTime bStart, ZonedDateTime bEnd) { - + public static boolean intersect(ZonedDateTime aStart, ZonedDateTime aEnd, ZonedDateTime bStart, ZonedDateTime bEnd) { // Same start time or same end time? if (aStart.equals(bStart) || aEnd.equals(bEnd)) { return true; @@ -80,8 +114,7 @@ public static boolean intersect(ZonedDateTime aStart, ZonedDateTime aEnd, } - public static LocalDateTime truncate(LocalDateTime time, ChronoUnit unit, - int stepRate, DayOfWeek firstDayOfWeek) { + public static LocalDateTime truncate(LocalDateTime time, ChronoUnit unit, int stepRate, DayOfWeek firstDayOfWeek) { switch (unit) { case DAYS: return adjustField(time, DAY_OF_YEAR, stepRate).truncatedTo(unit); @@ -90,55 +123,37 @@ public static LocalDateTime truncate(LocalDateTime time, ChronoUnit unit, case HOURS: return adjustField(time, HOUR_OF_DAY, stepRate).truncatedTo(unit); case MINUTES: - return adjustField(time, MINUTE_OF_HOUR, stepRate) - .truncatedTo(unit); + return adjustField(time, MINUTE_OF_HOUR, stepRate).truncatedTo(unit); case SECONDS: - return adjustField(time, SECOND_OF_MINUTE, stepRate).truncatedTo( - unit); + return adjustField(time, SECOND_OF_MINUTE, stepRate).truncatedTo(unit); case MILLIS: - return adjustField(time, MILLI_OF_SECOND, stepRate).truncatedTo( - unit); + return adjustField(time, MILLI_OF_SECOND, stepRate).truncatedTo(unit); case MICROS: - return adjustField(time, MICRO_OF_SECOND, stepRate).truncatedTo( - unit); + return adjustField(time, MICRO_OF_SECOND, stepRate).truncatedTo(unit); case NANOS: - return adjustField(time, NANO_OF_SECOND, stepRate) - .truncatedTo(unit); + return adjustField(time, NANO_OF_SECOND, stepRate).truncatedTo(unit); case MONTHS: - return time - .with(MONTH_OF_YEAR, - Math.max( - 1, - time.get(MONTH_OF_YEAR) - - time.get(MONTH_OF_YEAR) - % stepRate)).withDayOfMonth(1) - .truncatedTo(DAYS); + return time.with(MONTH_OF_YEAR, Math.max(1, time.get(MONTH_OF_YEAR) - time.get(MONTH_OF_YEAR) % stepRate)).withDayOfMonth(1).truncatedTo(DAYS); case YEARS: - return adjustField(time, ChronoField.YEAR, stepRate).withDayOfYear( - 1).truncatedTo(DAYS); + return adjustField(time, ChronoField.YEAR, stepRate).withDayOfYear(1).truncatedTo(DAYS); case WEEKS: - return time.with(DAY_OF_WEEK, firstDayOfWeek.getValue()).truncatedTo( - DAYS); + return time.with(DAY_OF_WEEK, firstDayOfWeek.getValue()).truncatedTo(DAYS); case DECADES: int decade = time.getYear() / 10 * 10; - return time.with(ChronoField.YEAR, decade).withDayOfYear(1) - .truncatedTo(DAYS); + return time.with(ChronoField.YEAR, decade).withDayOfYear(1).truncatedTo(DAYS); case CENTURIES: int century = time.getYear() / 100 * 100; - return time.with(ChronoField.YEAR, century).withDayOfYear(1) - .truncatedTo(DAYS); + return time.with(ChronoField.YEAR, century).withDayOfYear(1).truncatedTo(DAYS); case MILLENNIA: int millenium = time.getYear() / 1000 * 1000; - return time.with(ChronoField.YEAR, millenium).withDayOfYear(1) - .truncatedTo(DAYS); + return time.with(ChronoField.YEAR, millenium).withDayOfYear(1).truncatedTo(DAYS); default: } return time; } - public static ZonedDateTime truncate(ZonedDateTime time, ChronoUnit unit, - int stepRate, DayOfWeek firstDayOfWeek) { + public static ZonedDateTime truncate(ZonedDateTime time, ChronoUnit unit, int stepRate, DayOfWeek firstDayOfWeek) { switch (unit) { case DAYS: return adjustField(time, DAY_OF_YEAR, stepRate).truncatedTo(unit); @@ -147,92 +162,56 @@ public static ZonedDateTime truncate(ZonedDateTime time, ChronoUnit unit, case HOURS: return adjustField(time, HOUR_OF_DAY, stepRate).truncatedTo(unit); case MINUTES: - return adjustField(time, MINUTE_OF_HOUR, stepRate) - .truncatedTo(unit); + return adjustField(time, MINUTE_OF_HOUR, stepRate).truncatedTo(unit); case SECONDS: - return adjustField(time, SECOND_OF_MINUTE, stepRate).truncatedTo( - unit); + return adjustField(time, SECOND_OF_MINUTE, stepRate).truncatedTo(unit); case MILLIS: - return adjustField(time, MILLI_OF_SECOND, stepRate).truncatedTo( - unit); + return adjustField(time, MILLI_OF_SECOND, stepRate).truncatedTo(unit); case MICROS: - return adjustField(time, MICRO_OF_SECOND, stepRate).truncatedTo( - unit); + return adjustField(time, MICRO_OF_SECOND, stepRate).truncatedTo(unit); case NANOS: - return adjustField(time, NANO_OF_SECOND, stepRate) - .truncatedTo(unit); + return adjustField(time, NANO_OF_SECOND, stepRate).truncatedTo(unit); case MONTHS: - return time - .with(MONTH_OF_YEAR, - Math.max( - 1, - time.get(MONTH_OF_YEAR) - - time.get(MONTH_OF_YEAR) - % stepRate)).withDayOfMonth(1) - .truncatedTo(DAYS); + return time.with(MONTH_OF_YEAR, Math.max(1, time.get(MONTH_OF_YEAR) - time.get(MONTH_OF_YEAR) % stepRate)).withDayOfMonth(1).truncatedTo(DAYS); case YEARS: - return adjustField(time, ChronoField.YEAR, stepRate).withDayOfYear( - 1).truncatedTo(DAYS); + return adjustField(time, ChronoField.YEAR, stepRate).withDayOfYear(1).truncatedTo(DAYS); case WEEKS: - return time.with(DAY_OF_WEEK, firstDayOfWeek.getValue()).truncatedTo( - DAYS); + return time.with(DAY_OF_WEEK, firstDayOfWeek.getValue()).truncatedTo(DAYS); case DECADES: int decade = time.getYear() / 10 * 10; - return time.with(ChronoField.YEAR, decade).withDayOfYear(1) - .truncatedTo(DAYS); + return time.with(ChronoField.YEAR, decade).withDayOfYear(1).truncatedTo(DAYS); case CENTURIES: int century = time.getYear() / 100 * 100; - return time.with(ChronoField.YEAR, century).withDayOfYear(1) - .truncatedTo(DAYS); + return time.with(ChronoField.YEAR, century).withDayOfYear(1).truncatedTo(DAYS); case MILLENNIA: int millenium = time.getYear() / 1000 * 1000; - return time.with(ChronoField.YEAR, millenium).withDayOfYear(1) - .truncatedTo(DAYS); + return time.with(ChronoField.YEAR, millenium).withDayOfYear(1).truncatedTo(DAYS); default: } return time; } - public static LocalTime truncate(LocalTime time, ChronoUnit unit, - int stepRate) { + public static LocalTime truncate(LocalTime time, ChronoUnit unit, int stepRate) { switch (unit) { case HOURS: return adjustField(time, HOUR_OF_DAY, stepRate).truncatedTo(unit); case MINUTES: - return adjustField(time, MINUTE_OF_HOUR, stepRate) - .truncatedTo(unit); + return adjustField(time, MINUTE_OF_HOUR, stepRate).truncatedTo(unit); case SECONDS: - return adjustField(time, SECOND_OF_MINUTE, stepRate).truncatedTo( - unit); + return adjustField(time, SECOND_OF_MINUTE, stepRate).truncatedTo(unit); case MILLIS: - return adjustField(time, MILLI_OF_SECOND, stepRate).truncatedTo( - unit); + return adjustField(time, MILLI_OF_SECOND, stepRate).truncatedTo(unit); case MICROS: - return adjustField(time, MICRO_OF_SECOND, stepRate).truncatedTo( - unit); + return adjustField(time, MICRO_OF_SECOND, stepRate).truncatedTo(unit); case NANOS: - return adjustField(time, NANO_OF_SECOND, stepRate) - .truncatedTo(unit); + return adjustField(time, NANO_OF_SECOND, stepRate).truncatedTo(unit); default: } return time; } - public static boolean equals(Object first, Object second) { - if (first == null) { - return second == null; - } - - if (second == null) { - // because we already know that first is not null (see above) - return false; - } - - return first.equals(second); - } - public static void runInFXThread(Runnable runnable) { if (Platform.isFxApplicationThread()) { runnable.run(); @@ -241,10 +220,6 @@ public static void runInFXThread(Runnable runnable) { } } - public static MultipleSelectionModel createEmptySelectionModel() { - return new EmptySelectionModel<>(); - } - private static ZonedDateTime adjustField(ZonedDateTime time, ChronoField field, int stepRate) { return time.with(field, time.get(field) - time.get(field) % stepRate); } @@ -257,6 +232,10 @@ private static LocalTime adjustField(LocalTime time, ChronoField field, int step return time.with(field, time.get(field) - time.get(field) % stepRate); } + public static MultipleSelectionModel createEmptySelectionModel() { + return new EmptySelectionModel<>(); + } + private static class EmptySelectionModel extends MultipleSelectionModel { @Override public void selectPrevious() { @@ -327,4 +306,315 @@ public ObservableList getSelectedIndices() { } } + /** + * An interface used for converting an object of one type to an object + * of another type. + * + * @param the first (left) type + * @param the second (right) type + */ + public interface Converter { + + L toLeft(R right); + + R toRight(L left); + } + + /** + * Converts the given recurrence rule (according to RFC 2445) into a human-readable text, + * e.g. "RRULE:FREQ=DAILY;" becomes "Every day". + * + * @param rrule the rule + * @param startDate the start date for the rule + * @return a nice text describing the rule + */ + public static String convertRFC2445ToText(String rrule, LocalDate startDate) { + try { + Recur rule = new Recur<>(rrule.replaceFirst("^RRULE:", "")); + StringBuilder sb = new StringBuilder(); + + String granularity; + String granularities; + + switch (rule.getFrequency()) { + case DAILY: + granularity = Messages.getString("Util.DAY"); + granularities = Messages.getString("Util.DAYS"); + break; + case MONTHLY: + granularity = Messages.getString("Util.MONTH"); + granularities = Messages.getString("Util.MONTHS"); + break; + case WEEKLY: + granularity = Messages.getString("Util.WEEK"); + granularities = Messages.getString("Util.WEEKS"); + break; + case YEARLY: + granularity = Messages.getString("Util.YEAR"); + granularities = Messages.getString("Util.YEARS"); + break; + case HOURLY: + granularity = Messages.getString("Util.HOUR"); + granularities = Messages.getString("Util.HOURS"); + break; + case MINUTELY: + granularity = Messages.getString("Util.MINUTE"); + granularities = Messages.getString("Util.MINUTES"); + break; + case SECONDLY: + granularity = Messages.getString("Util.SECOND"); + granularities = Messages.getString("Util.SECONDS"); + break; + default: + granularity = ""; + granularities = ""; + } + + int interval = rule.getInterval(); + if (interval > 1) { + sb.append(MessageFormat.format(Messages.getString("Util.EVERY_PLURAL"), rule.getInterval(), granularities)); + } else { + sb.append(MessageFormat.format(Messages.getString("Util.EVERY_SINGULAR"), granularity)); + } + + /* + * Weekdays + */ + + if (rule.getFrequency().equals(Frequency.WEEKLY)) { + List byDay = rule.getDayList(); + if (!byDay.isEmpty()) { + sb.append(Messages.getString("Util.ON_WEEKDAY")); + for (int i = 0; i < byDay.size(); i++) { + WeekDay num = byDay.get(i); + sb.append(makeHuman(num.getDay())); + if (i < byDay.size() - 1) { + sb.append(", "); + } + } + } + } + + if (rule.getFrequency().equals(Frequency.MONTHLY)) { + + if (!rule.getMonthDayList().isEmpty()) { + + int day = rule.getMonthDayList().get(0); + sb.append(Messages.getString("Util.ON_MONTH_DAY")); + sb.append(day); + + } else if (!rule.getDayList().isEmpty()) { + + /* + * We only support one day. + */ + WeekDay num = rule.getDayList().get(0); + sb.append(MessageFormat.format(Messages.getString("Util.ON_MONTH_WEEKDAY"), makeHuman(num.getOffset()), makeHuman(num.getDay()))); + } + } + + if (rule.getFrequency().equals(Frequency.YEARLY)) { + sb.append(MessageFormat.format(Messages.getString("Util.ON_DATE"), DateTimeFormatter.ofPattern(Messages.getString("Util.MONTH_AND_DAY_FORMAT")).format(startDate))); + } + + int count = rule.getCount(); + if (count > 0) { + if (count == 1) { + return Messages.getString("Util.ONCE"); + } else { + sb.append(MessageFormat.format(Messages.getString("Util.TIMES"), count)); + } + } else { + LocalDate until = rule.getUntil(); + if (until != null) { + sb.append(MessageFormat.format(Messages.getString("Util.UNTIL_DATE"), DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).format(until))); + } + } + + return sb.toString(); + } catch (IllegalArgumentException | DateTimeParseException e) { + e.printStackTrace(); + return Messages.getString("Util.INVALID_RULE"); + } + } + + private static String makeHuman(WeekDay.Day wday) { + switch (wday) { + case FR: + return Messages.getString("Util.FRIDAY"); + case MO: + return Messages.getString("Util.MONDAY"); + case SA: + return Messages.getString("Util.SATURDAY"); + case SU: + return Messages.getString("Util.SUNDAY"); + case TH: + return Messages.getString("Util.THURSDAY"); + case TU: + return Messages.getString("Util.TUESDAY"); + case WE: + return Messages.getString("Util.WEDNESDAY"); + default: + throw new IllegalArgumentException("unknown weekday: " + wday); + } + } + + private static String makeHuman(int num) { + switch (num) { + case 1: + return Messages.getString("Util.FIRST"); + case 2: + return Messages.getString("Util.SECOND"); + case 3: + return Messages.getString("Util.THIRD"); + case 4: + return Messages.getString("Util.FOURTH"); + case 5: + return Messages.getString("Util.FIFTH"); + default: + return Integer.toString(num); + } + } + + /** + * Searches for a {@link ScrollBar} of the given orientation (vertical, horizontal) + * somewhere in the containment hierarchy of the given parent node. + * + * @param parent the parent node + * @param orientation the orientation (horizontal, vertical) + * @return a scrollbar or null if none can be found + */ + public static ScrollBar findScrollBar(Parent parent, Orientation orientation) { + for (Node node : parent.getChildrenUnmodifiable()) { + if (node instanceof ScrollBar) { + ScrollBar b = (ScrollBar) node; + if (b.getOrientation().equals(orientation)) { + return b; + } + } + + if (node instanceof Parent) { + ScrollBar b = findScrollBar((Parent) node, orientation); + if (b != null) { + return b; + } + } + } + + return null; + } + + /** + * Adjusts the given date to a new date that marks the beginning of the week where the + * given date is located. If "Monday" is the first day of the week and the given date + * is a "Wednesday" then this method will return a date that is two days earlier than the + * given date. + * + * @param date the date to adjust + * @param firstDayOfWeek the day of week that is considered the start of the week ("Monday" in Germany, "Sunday" in the US) + * @return the date of the first day of the week + * @see #adjustToLastDayOfWeek(LocalDate, DayOfWeek) + * @see DateControl#getFirstDayOfWeek() + */ + public static LocalDate adjustToFirstDayOfWeek(LocalDate date, DayOfWeek firstDayOfWeek) { + LocalDate newDate = date.with(DAY_OF_WEEK, firstDayOfWeek.getValue()); + if (newDate.isAfter(date)) { + newDate = newDate.minusWeeks(1); + } + + return newDate; + } + + /** + * Adjusts the given date to a new date that marks the end of the week where the + * given date is located. If "Monday" is the first day of the week and the given date + * is a "Wednesday" then this method will return a date that is four days later than the + * given date. This method calculates the first day of the week and then adds six days + * to it. + * + * @param date the date to adjust + * @param firstDayOfWeek the day of week that is considered the start of the week ("Monday" in Germany, "Sunday" in the US) + * @return the date of the first day of the week + * @see #adjustToFirstDayOfWeek(LocalDate, DayOfWeek) + * @see DateControl#getFirstDayOfWeek() + */ + public static LocalDate adjustToLastDayOfWeek(LocalDate date, DayOfWeek firstDayOfWeek) { + LocalDate startOfWeek = adjustToFirstDayOfWeek(date, firstDayOfWeek); + return startOfWeek.plusDays(6); + } + + /** + * Creates a bidirectional binding between the two given properties of different types via the + * help of a {@link Converter}. + * + * @param leftProperty the left property + * @param rightProperty the right property + * @param converter the converter + * @param the type of the left property + * @param the type of the right property + */ + public static void bindBidirectional(Property leftProperty, Property rightProperty, Converter converter) { + BidirectionalConversionBinding binding = new BidirectionalConversionBinding<>(leftProperty, rightProperty, converter); + leftProperty.addListener(binding); + rightProperty.addListener(binding); + leftProperty.setValue(converter.toLeft(rightProperty.getValue())); + } + + private static class BidirectionalConversionBinding implements InvalidationListener, WeakListener { + + private final WeakReference> leftReference; + private final WeakReference> rightReference; + private final Converter converter; + private boolean updating; + + private BidirectionalConversionBinding(Property leftProperty, Property rightProperty, Converter converter) { + this.leftReference = new WeakReference<>(Objects.requireNonNull(leftProperty)); + this.rightReference = new WeakReference<>(Objects.requireNonNull(rightProperty)); + this.converter = Objects.requireNonNull(converter); + } + + public Property getLeftProperty() { + return leftReference.get(); + } + + public Property getRightProperty() { + return rightReference.get(); + } + + @Override + public boolean wasGarbageCollected() { + return getLeftProperty() == null || getRightProperty() == null; + } + + @Override + public void invalidated(Observable observable) { + if (updating) { + return; + } + + final Property leftProperty = getLeftProperty(); + final Property rightProperty = getRightProperty(); + + if (wasGarbageCollected()) { + if (leftProperty != null) { + leftProperty.removeListener(this); + } + if (rightProperty != null) { + rightProperty.removeListener(this); + } + } else { + try { + updating = true; + + if (observable == leftProperty) { + rightProperty.setValue(converter.toRight(leftProperty.getValue())); + } else { + leftProperty.setValue(converter.toLeft(rightProperty.getValue())); + } + } finally { + updating = false; + } + } + } + } }