package jfxtras.internal.scene.control.skin.agenda.icalendar.base24hour.popup;
import java.io.IOException;
import java.net.URL;
import java.time.DateTimeException;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.beans.InvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar.ButtonData;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.util.Callback;
import javafx.util.StringConverter;
import jfxtras.icalendarfx.components.VDisplayable;
import jfxtras.icalendarfx.properties.component.recurrence.ExceptionDates;
import jfxtras.icalendarfx.properties.component.recurrence.RecurrenceRule;
import jfxtras.icalendarfx.properties.component.recurrence.rrule.FrequencyType;
import jfxtras.icalendarfx.properties.component.recurrence.rrule.Interval;
import jfxtras.icalendarfx.properties.component.recurrence.rrule.RecurrenceRuleValue;
import jfxtras.icalendarfx.properties.component.recurrence.rrule.Until;
import jfxtras.icalendarfx.properties.component.recurrence.rrule.byxxx.ByDay;
import jfxtras.icalendarfx.properties.component.recurrence.rrule.byxxx.ByRule;
import jfxtras.icalendarfx.properties.component.recurrence.rrule.byxxx.ByDay.ByDayPair;
import jfxtras.icalendarfx.utilities.DateTimeUtilities;
import jfxtras.icalendarfx.utilities.DateTimeUtilities.DateTimeType;
import jfxtras.internal.scene.control.skin.agenda.icalendar.base24hour.Settings;
/**
* VBox containing controls to edit the {@link RecurrenceRule} in a {@link VDisplayable}.
* <p>
* Note: Only supports one Exception Date property (the iCalendar standard allows multiple Exception
* Date properties)
*
* @author David Bal
*
* @param <T> subclass of {@link VDisplayable}
*/
public abstract class EditRecurrenceRuleVBox<T extends VDisplayable<T>> extends VBox
{
final public static int EXCEPTION_CHOICE_LIMIT = 50;
final public static int INITIAL_COUNT = 10;
final public static Period DEFAULT_UNTIL_PERIOD = Period.ofMonths(1); // amount of time beyond start default for UNTIL (ends on)
T vComponent;
private RecurrenceRuleValue rrule;
private RecurrenceRuleValue oldRRule; // previous rrule from toggling repeatableCheckBox
private ObjectProperty<RecurrenceRuleValue> recurrenceRuleProperty;
private ObjectProperty<Temporal> dateTimeStartRecurrenceNew;
protected ObjectProperty<Temporal> dateTimeStartProperty;
@FXML private ResourceBundle resources; // ResourceBundle that was given to the FXMLLoader
@FXML private CheckBox repeatableCheckBox;
@FXML private GridPane repeatableGridPane;
@FXML ComboBox<FrequencyType> frequencyComboBox;
@FXML Spinner<Integer> intervalSpinner;
@FXML private Label frequencyLabel;
@FXML private Label eventLabel;
@FXML private Label weeklyLabel;
// TODO - CHANGE ORDER OF DAYS OF WEEK FOR OTHER LOCALE
@FXML private HBox weeklyHBox;
@FXML private CheckBox sundayCheckBox;
@FXML private CheckBox mondayCheckBox;
@FXML private CheckBox tuesdayCheckBox;
@FXML private CheckBox wednesdayCheckBox;
@FXML private CheckBox thursdayCheckBox;
@FXML private CheckBox fridayCheckBox;
@FXML private CheckBox saturdayCheckBox;
private final Map<BooleanProperty, DayOfWeek> checkBoxDayOfWeekMap = new HashMap<>();
final ObservableList<DayOfWeek> dayOfWeekList = FXCollections.observableArrayList();
private Map<DayOfWeek, BooleanProperty> dayOfWeekCheckBoxMap;
@FXML private VBox monthlyVBox;
@FXML private Label monthlyLabel;
private ToggleGroup monthlyGroup;
@FXML private RadioButton dayOfMonthRadioButton;
@FXML private RadioButton dayOfWeekRadioButton;
@FXML DatePicker startDatePicker;
@FXML private RadioButton endNeverRadioButton;
@FXML private RadioButton endAfterRadioButton;
@FXML private RadioButton untilRadioButton;
@FXML private Spinner<Integer> endAfterEventsSpinner;
@FXML private DatePicker untilDatePicker;
private ToggleGroup endGroup;
@FXML private Label repeatSummaryLabel;
@FXML private ComboBox<Temporal> exceptionComboBox;
@FXML private Button addExceptionButton;
@FXML private ListView<Temporal> exceptionsListView; // EXDATE date/times to be skipped (deleted events)
@FXML private Button removeExceptionButton;
private List<Temporal> exceptionMasterList = new ArrayList<>();
// CONSTRUCTOR
public EditRecurrenceRuleVBox()
{
super();
loadFxml(EditDescriptiveVBox.class.getResource("RecurrenceRule.fxml"), this);
}
private DateTimeFormatter getFormatter(Temporal t)
{
return t.isSupported(ChronoUnit.NANOS) ? Settings.DATE_FORMAT_AGENDA_EXCEPTION : Settings.DATE_FORMAT_AGENDA_EXCEPTION_DATEONLY;
}
// DAY OF WEEK CHECKBOX LISTENER
private final ChangeListener<? super Boolean> dayOfWeekCheckBoxListener = (obs, oldSel, newSel) ->
{
DayOfWeek dayOfWeek = checkBoxDayOfWeekMap.get(obs);
ByDay rule = (ByDay) vComponent
.getRecurrenceRule()
.getValue()
.lookupByRule(ByDay.class);
if (newSel)
{
if (! dayOfWeekList.contains(dayOfWeek))
{
rule.addDayOfWeek(dayOfWeek);
dayOfWeekList.add(dayOfWeek);
}
} else
{
rule.removeDayOfWeek(dayOfWeek);
dayOfWeekList.remove(dayOfWeek);
}
};
/* Listener for dayOfWeekRadioButton when frequency if monthly
* Note: Only attached to dayOfWeekRadioButton and not to dayOfMonthRadioButton. The dayOfWeekRadioButton
* toggles when dayOfMonthRadioButton changes. Attaching to both would cause double firing.
*/
private ChangeListener<? super Boolean> dayOfWeekButtonListener = (observable, oldSelection, newSelection) ->
{
if (newSelection)
{
int ordinalWeekNumber = DateTimeUtilities.weekOrdinalInMonth(dateTimeStartRecurrenceNew.get());
DayOfWeek dayOfWeek = DayOfWeek.from(dateTimeStartRecurrenceNew.get());
ByRule<?> byDayRuleMonthly = new ByDay(new ByDayPair(dayOfWeek, ordinalWeekNumber));
rrule.addChild(byDayRuleMonthly);
} else
{ // remove rule to reset to default behavior of repeat by day of month
ByRule<?> r = rrule.lookupByRule(ByDay.class);
rrule.removeChild(r);
}
refreshSummary();
refreshExceptionDates();
};
//MAKE EXCEPTION DATES LISTENER
final private InvalidationListener makeExceptionDatesAndSummaryListener = (obs) ->
{
refreshSummary();
refreshExceptionDates();
};
private void refreshSummary()
{
String summaryString = makeSummary(rrule, vComponent.getDateTimeStart().getValue());
repeatSummaryLabel.setText(summaryString);
}
private final ChangeListener<? super Boolean> neverListener = (obs, oldValue, newValue) ->
{
if (newValue)
{
// rrule.setUntil((Until) null);
// rrule.setCount((Count) null);
refreshSummary();
refreshExceptionDates();
}
};
/* When recurrence start date changes switch day of week to new value */
private final ChangeListener<? super Temporal> weeklyRecurrenceListener = (obs, oldValue, newValue) ->
{
DayOfWeek oldDayOfWeek = DayOfWeek.from(oldValue);
DayOfWeek newDayOfWeek = DayOfWeek.from(newValue);
if (! dayOfWeekCheckBoxMap.get(newDayOfWeek).get())
{
dayOfWeekCheckBoxMap.get(oldDayOfWeek).set(false);
dayOfWeekCheckBoxMap.get(newDayOfWeek).set(true);
}
};
// FREQUENCY CHANGE LISTENER
private final ChangeListener<? super FrequencyType> frequencyListener = (obs, oldValue, newValue) ->
{
rrule.setFrequency(newValue);
// Change Frequency if different. Copy Interval, null ExDate
if (rrule.getFrequency().getValue() != newValue)
{
exceptionsListView.getItems().clear();
vComponent.setExceptionDates(null);
}
if (oldValue == FrequencyType.WEEKLY)
{
dateTimeStartRecurrenceNew.removeListener(weeklyRecurrenceListener);
}
if (rrule.getByRules() != null) // TODO - IS THIS NECESSARY? (MAYBE FOR CHANGE FROM MONTHLY?)
{
ByRule<?> r = rrule.lookupByRule(ByDay.class);
if (r != null) rrule.removeChild(r);
}
// Setup monthlyVBox and weeklyHBox setting visibility
switch (newValue)
{
case DAILY:
case YEARLY:
break;
case MONTHLY:
dayOfMonthRadioButton.setSelected(true);
dayOfWeekRadioButton.setSelected(false);
break;
case WEEKLY:
dateTimeStartRecurrenceNew.addListener(weeklyRecurrenceListener);
if (dayOfWeekList.isEmpty())
{
DayOfWeek dayOfWeek = LocalDate.from(dateTimeStartRecurrenceNew.get()).getDayOfWeek();
rrule.addChild(new ByDay(dayOfWeek)); // add days already clicked
dayOfWeekCheckBoxMap.get(dayOfWeek).set(true);
} else
{
rrule.addChild(new ByDay(dayOfWeekList)); // add days already clicked
}
break;
case SECONDLY:
case MINUTELY:
case HOURLY:
throw new IllegalArgumentException("Frequency " + newValue + " not implemented");
default:
break;
}
// visibility of monthlyVBox & weeklyHBox
setFrequencyVisibility(newValue);
if (intervalSpinner.getValueFactory() != null)
{
setIntervalText(intervalSpinner.getValue());
}
// Make summary and exceptions
refreshSummary();
refreshExceptionDates();
};
private void setIntervalText(int value)
{
final String frequencyLabelText;
if (value == 1)
{
frequencyLabelText = Settings.REPEAT_FREQUENCIES_SINGULAR.get(frequencyComboBox.getValue());
} else
{
frequencyLabelText = Settings.REPEAT_FREQUENCIES_PLURAL.get(frequencyComboBox.getValue());
}
frequencyLabel.setText(frequencyLabelText);
}
private void setFrequencyVisibility(FrequencyType f)
{
// Setup monthlyVBox and weeklyHBox setting visibility
switch (f)
{
case DAILY:
case YEARLY:
{
monthlyVBox.setVisible(false);
monthlyLabel.setVisible(false);
weeklyHBox.setVisible(false);
weeklyLabel.setVisible(false);
break;
}
case MONTHLY:
monthlyVBox.setVisible(true);
monthlyLabel.setVisible(true);
weeklyHBox.setVisible(false);
weeklyLabel.setVisible(false);
break;
case WEEKLY:
{
monthlyVBox.setVisible(false);
monthlyLabel.setVisible(false);
weeklyHBox.setVisible(true);
weeklyLabel.setVisible(true);
break;
}
case SECONDLY:
case MINUTELY:
case HOURLY:
throw new IllegalArgumentException("Frequency " + f + " not implemented");
default:
break;
}
}
private final ChangeListener<? super Integer> intervalSpinnerListener = (observable, oldValue, newValue) ->
{
if (newValue == 1)
{
rrule.setInterval((Interval) null); // rely on default value of 1
} else
{
rrule.setInterval(newValue);
}
setIntervalText(newValue);
refreshSummary();
refreshExceptionDates();
};
private final ChangeListener<? super LocalDate> untilListener = (observable, oldSelection, newSelection) ->
{
if (newSelection != null)
{
LocalDate dtstartZDate = LocalDate.from(DateTimeType.DATE_WITH_UTC_TIME.from(vComponent.getDateTimeStart().getValue()));
if (newSelection.isBefore(dtstartZDate))
{
tooEarlyDateAlert(vComponent.getDateTimeStart().getValue());
untilDatePicker.setValue(oldSelection);
} else
{
Temporal until = findUntil(newSelection);
LocalDate newSelectionZDate = LocalDate.from(DateTimeType.DATE_WITH_UTC_TIME.from(vComponent.getDateTimeStart().getValue().with(newSelection)));
if (LocalDate.from(until).equals(newSelectionZDate))
{
rrule.setUntil(until);
refreshSummary();
refreshExceptionDates();
} else
{
notOccurrenceDateAlert(newSelection);
untilDatePicker.setValue(oldSelection);
}
}
}
};
private final ChangeListener<? super Boolean> untilRadioButtonListener = (observable, oldSelection, newSelection) ->
{
if (newSelection)
{
if (rrule.getUntil() == null)
{ // new selection - use date/time one month in the future as default
boolean isRecurrenceFarEnoughInFuture = DateTimeUtilities
.isAfter(dateTimeStartRecurrenceNew.get(), vComponent.getDateTimeStart().getValue().plus(DEFAULT_UNTIL_PERIOD));
LocalDate defaultEndOnDateTime = (isRecurrenceFarEnoughInFuture)
? LocalDate.from(dateTimeStartRecurrenceNew.get())
: LocalDate.from(vComponent.getDateTimeStart().getValue().plus(DEFAULT_UNTIL_PERIOD));
Temporal until = findUntil(defaultEndOnDateTime); // adjust to actual recurrence
rrule.setUntil(until);
}
untilDatePicker.setValue(LocalDate.from(rrule.getUntil().getValue()));
untilDatePicker.setDisable(false);
untilDatePicker.show();
refreshExceptionDates();
} else
{
untilDatePicker.setValue(null);
rrule.setUntil((Until) null);
untilDatePicker.setDisable(true);
}
};
/**
* Finds closest recurrence, at least one recurrence past DTSTART, from initialUntilDate
*
* @param initialUntilDate - selected date from untilDatePicker
* @return - best match for until
*/
private Temporal findUntil(LocalDate initialUntilDate)
{
Temporal timeAdjustedSelection = vComponent.getDateTimeStart().getValue().with(initialUntilDate);
// find last recurrence that is not after initialUntilDate
RecurrenceRuleValue rruleCopy = new RecurrenceRuleValue(rrule);
rruleCopy.setUntil((Until) null);
Iterator<Temporal> i = rruleCopy.streamRecurrences(vComponent.getDateTimeStart().getValue()).iterator();
Temporal until = i.next();
while (i.hasNext())
{
Temporal temporal = i.next();
if (DateTimeUtilities.isAfter(temporal, timeAdjustedSelection)) break;
until = temporal;
}
return (until instanceof LocalDate) ? until : DateTimeType.DATE_WITH_UTC_TIME.from(until); // ensure type is DATE_WITH_UTC_TIME
}
// listen for changes to start date/time (type may change requiring new exception date choices)
private final ChangeListener<? super Temporal> dateTimeStartToExceptionChangeListener = (obs, oldValue, newValue) ->
{
System.out.println("exception listener");
exceptionMasterList.clear();
refreshExceptionDates();
// update existing exceptions
if (! exceptionsListView.getItems().isEmpty())
{
List<Temporal> newItems = null;
if (newValue.isSupported(ChronoUnit.SECONDS))
{
LocalTime time = LocalDateTime.from(newValue).toLocalTime();
newItems = exceptionsListView.getItems()
.stream()
.map(d -> LocalDate.from(d).atTime(time))
.collect(Collectors.toList());
} else if (newValue.isSupported(ChronoUnit.DAYS))
{
newItems = exceptionsListView.getItems()
.stream()
.map(d -> LocalDate.from(d))
.collect(Collectors.toList());
} else
{
throw new DateTimeException("Unsupported Temporal class:" + newValue.getClass());
}
exceptionsListView.setItems(FXCollections.observableArrayList(newItems));
}
};
/** Synch startDatePicker with DTSTART component. In subclass DTEND or DUE are synched too */
void synchStartDatePickerAndComponent(LocalDate oldValue, LocalDate newValue)
{
final boolean isValid;
if (rrule.getFrequency().getValue() == FrequencyType.WEEKLY)
{ // ensure picked date is valid for weekly frequency
DayOfWeek dayOfWeek = DayOfWeek.from(newValue);
ByDay byDay = (ByDay) rrule.lookupByRule(ByDay.class);
isValid = byDay.dayOfWeekWithoutOrdinalList().contains(dayOfWeek);
} else
{
isValid = true;
}
if (isValid)
{
// TODO
// If recurrence before or equal to DTSTART adjust to match DTSTART
// if recurrence is after DTSTART don't adjust it
Period shift = Period.between(oldValue, newValue);
Temporal newStart = vComponent.getDateTimeStart().getValue().plus(shift);
vComponent.setDateTimeStart(newStart);
} else
{
notOccurrenceDateAlert(newValue);
}
}
// INITIALIZATION - runs when FXML is initialized
@FXML public void initialize()
{
// REPEATABLE CHECKBOX
repeatableCheckBox.selectedProperty().addListener((observable, oldSelection, newSelection) ->
{
if (newSelection)
{
// removeListeners();
if (rrule == null)
{
if (oldRRule != null)
{
rrule = oldRRule;
vComponent.setRecurrenceRule(rrule);
} else
{
// setup new default RRule
rrule = new RecurrenceRuleValue()
.withFrequency(FrequencyType.WEEKLY)
.withByRules(new ByDay(DayOfWeek.from(dateTimeStartRecurrenceNew.get())));
vComponent.setRecurrenceRule(rrule);
setInitialValues(vComponent);
}
}
repeatableGridPane.setDisable(false);
startDatePicker.setDisable(false);
} else
{
oldRRule = rrule;
rrule = null;
vComponent.setRecurrenceRule(rrule);
repeatableGridPane.setDisable(true);
startDatePicker.setDisable(true);
}
});
// DAY OF WEEK CHECK BOX LISTENERS (FOR WEEKLY)
checkBoxDayOfWeekMap.put(sundayCheckBox.selectedProperty(), DayOfWeek.SUNDAY);
checkBoxDayOfWeekMap.put(mondayCheckBox.selectedProperty(), DayOfWeek.MONDAY);
checkBoxDayOfWeekMap.put(tuesdayCheckBox.selectedProperty(), DayOfWeek.TUESDAY);
checkBoxDayOfWeekMap.put(wednesdayCheckBox.selectedProperty(), DayOfWeek.WEDNESDAY);
checkBoxDayOfWeekMap.put(thursdayCheckBox.selectedProperty(), DayOfWeek.THURSDAY);
checkBoxDayOfWeekMap.put(fridayCheckBox.selectedProperty(), DayOfWeek.FRIDAY);
checkBoxDayOfWeekMap.put(saturdayCheckBox.selectedProperty(), DayOfWeek.SATURDAY);
dayOfWeekCheckBoxMap = checkBoxDayOfWeekMap.entrySet().stream().collect(Collectors.toMap(e -> e.getValue(), e -> e.getKey()));
checkBoxDayOfWeekMap.entrySet().stream().forEach(entry -> entry.getKey().addListener(dayOfWeekCheckBoxListener));
// Setup frequencyComboBox items
FrequencyType[] supportedFrequencyProperties = new FrequencyType[] { FrequencyType.DAILY,
FrequencyType.WEEKLY,
FrequencyType.MONTHLY,
FrequencyType.YEARLY };
frequencyComboBox.setItems(FXCollections.observableArrayList(supportedFrequencyProperties));
frequencyComboBox.setConverter(new StringConverter<FrequencyType>()
{
@Override
public String toString(FrequencyType frequencyType)
{
return Settings.REPEAT_FREQUENCIES.get(frequencyType);
}
@Override public FrequencyType fromString(String string) {
throw new RuntimeException("not required for non editable ComboBox");
}
});
// INTERVAL SPINNER
// Make frequencySpinner and only accept numbers (needs below two listeners)
intervalSpinner.setEditable(true);
intervalSpinner.getEditor().addEventHandler(KeyEvent.KEY_PRESSED, (event) ->
{
if (event.getCode() == KeyCode.ENTER)
{
String s = intervalSpinner.getEditor().textProperty().get();
boolean isNumber = s.matches("[0-9]+");
if (! isNumber)
{
String lastValue = intervalSpinner.getValue().toString();
intervalSpinner.getEditor().textProperty().set(lastValue);
notNumberAlert();
}
}
});
intervalSpinner.focusedProperty().addListener((obs, wasFocused, isNowFocused) ->
{
if (! isNowFocused)
{
int value;
String s = intervalSpinner.getEditor().textProperty().get();
boolean isNumber = s.matches("[0-9]+");
if (isNumber)
{
value = Integer.parseInt(s);
if (value > 1)
{
rrule.setInterval(value);
refreshSummary();
refreshExceptionDates();
}
} else {
String lastValue = intervalSpinner.getValue().toString();
intervalSpinner.getEditor().textProperty().set(lastValue);
notNumberAlert();
}
}
});
startDatePicker.valueProperty().addListener((obs, oldValue, newValue) ->
{
if (oldValue != null)
{
synchStartDatePickerAndComponent(oldValue, newValue);
}
});
startDatePicker.focusedProperty().addListener((obs, wasFocused, isNowFocused) ->
{
if (! isNowFocused)
{
try {
String s = startDatePicker.getEditor().getText();
LocalDate d = startDatePicker.getConverter().fromString(s);
startDatePicker.setValue(d);
} catch (DateTimeParseException e)
{
// display alert and return original date if can't parse
String exampleDate = startDatePicker.getConverter().toString(LocalDate.now());
notDateAlert(exampleDate);
LocalDate d = startDatePicker.getValue();
String s = startDatePicker.getConverter().toString(d);
startDatePicker.getEditor().setText(s);
}
}
});
// END AFTER LISTENERS
endAfterEventsSpinner.valueProperty().addListener((observable, oldSelection, newSelection) ->
{
if (endAfterRadioButton.isSelected())
{
rrule.setCount(newSelection);
refreshSummary();
refreshExceptionDates();
}
if (newSelection == 1) {
eventLabel.setText(resources.getString("event"));
} else {
eventLabel.setText(resources.getString("events"));
}
});
endAfterRadioButton.selectedProperty().addListener((observable, oldSelection, newSelection) ->
{
if (newSelection)
{
endAfterEventsSpinner.setDisable(false);
eventLabel.setDisable(false);
rrule.setCount(endAfterEventsSpinner.getValue());
refreshSummary();
refreshExceptionDates();
} else
{
rrule.setCount(null);
endAfterEventsSpinner.setValueFactory(null);
endAfterEventsSpinner.setDisable(true);
eventLabel.setDisable(true);
}
});
// Make endAfterEventsSpinner and only accept numbers in text field (needs below two listeners)
endAfterEventsSpinner.setEditable(true);
endAfterEventsSpinner.getEditor().addEventHandler(KeyEvent.KEY_PRESSED, (event) ->
{
if (event.getCode() == KeyCode.ENTER) {
String s = endAfterEventsSpinner.getEditor().textProperty().get();
boolean isNumber = s.matches("[0-9]+");
if (! isNumber) {
String lastValue = endAfterEventsSpinner.getValue().toString();
endAfterEventsSpinner.getEditor().textProperty().set(lastValue);
notNumberAlert();
}
}
});
endAfterEventsSpinner.focusedProperty().addListener((obs, wasFocused, isNowFocused) ->
{
if (! isNowFocused)
{
int value;
String s = endAfterEventsSpinner.getEditor().getText();
boolean isNumber = s.matches("[0-9]+");
if (isNumber) {
value = Integer.parseInt(s);
rrule.setCount(value);
} else {
String lastValue = endAfterEventsSpinner.getValue().toString();
endAfterEventsSpinner.getEditor().textProperty().set(lastValue);
notNumberAlert();
}
}
});
untilDatePicker.valueProperty().addListener(untilListener);
untilRadioButton.selectedProperty().addListener(untilRadioButtonListener);
// Day of week tooltips
sundayCheckBox.setTooltip(new Tooltip(resources.getString("sunday")));
mondayCheckBox.setTooltip(new Tooltip(resources.getString("monday")));
tuesdayCheckBox.setTooltip(new Tooltip(resources.getString("tuesday")));
wednesdayCheckBox.setTooltip(new Tooltip(resources.getString("wednesday")));
thursdayCheckBox.setTooltip(new Tooltip(resources.getString("thursday")));
fridayCheckBox.setTooltip(new Tooltip(resources.getString("friday")));
saturdayCheckBox.setTooltip(new Tooltip(resources.getString("saturday")));
// Monthly ToggleGroup
monthlyGroup = new ToggleGroup();
dayOfMonthRadioButton.setToggleGroup(monthlyGroup);
dayOfWeekRadioButton.setToggleGroup(monthlyGroup);
// End criteria ToggleGroup
endGroup = new ToggleGroup();
endNeverRadioButton.setToggleGroup(endGroup);
endAfterRadioButton.setToggleGroup(endGroup);
untilRadioButton.setToggleGroup(endGroup);
ListChangeListener<? super Temporal> exceptionsListChangeListener = (change) ->
{
while (change.next())
{
if (change.wasAdded())
{
List<? extends Temporal> added1 = change.getAddedSubList();
final List<ExceptionDates> exceptionDates = (vComponent.getExceptionDates() == null) ? Collections.emptyList() : vComponent.getExceptionDates();
boolean isEmpty = exceptionDates.isEmpty();
DateTimeType startType = DateTimeType.of(vComponent.getDateTimeStart().getValue());
DateTimeType newType = DateTimeType.of(added1.get(0));
boolean isTemporalTypeChanged = startType != newType;
if (isEmpty || isTemporalTypeChanged)
{
List<? extends Temporal> list = change.getList();
Temporal[] allExceptions = list.toArray(new Temporal[list.size()]);
ExceptionDates ed = new ExceptionDates(allExceptions);
vComponent.setExceptionDates(new ArrayList<>(Arrays.asList(ed)));
} else
{
// NOTE: Only works for one EXDATE property
vComponent.getExceptionDates().get(0).getValue().addAll(added1);
}
} else if (change.wasRemoved())
{
List<? extends Temporal> removed = change.getRemoved();
vComponent.getExceptionDates().get(0).getValue().removeAll(removed);
}
}
};
exceptionsListView.getItems().addListener(exceptionsListChangeListener);
}
/**
* Provide necessary data to setup
*
* @param vComponent - component to be edited
* @param dateTimeStartRecurrenceNew - reference to start date or date/time from {@link EditDecriptiveVBox}
*/
public void setupData(
T vComponent
, ObjectProperty<Temporal> dateTimeStartRecurrenceNew)
{
dateTimeStartProperty = new SimpleObjectProperty<Temporal>(vComponent.getDateTimeStart().getValue());
rrule = (vComponent.getRecurrenceRule() != null) ? vComponent.getRecurrenceRule().getValue() : null;
recurrenceRuleProperty = new SimpleObjectProperty<>(rrule);
this.vComponent = vComponent;
this.dateTimeStartRecurrenceNew = dateTimeStartRecurrenceNew;
if (! isSupported(vComponent))
{
throw new RuntimeException("Unsupported VComponent");
}
// Listen to DTSTART type changes (by selecting wholeDay in DescriptiveVBox)
dateTimeStartRecurrenceNew.addListener((obs, oldValue, newValue) ->
{
// Change EXCEPTIONS type
DateTimeType newType = DateTimeType.of(newValue);
Temporal[] convertedExceptions = exceptionsListView.getItems()
.stream()
.map(e ->
{
Temporal converted = newType.from(e);
if (converted.isSupported(ChronoUnit.NANOS))
{
converted = converted.with(LocalTime.from(newValue));
}
return converted;
})
.toArray(size -> new Temporal[size]);
exceptionsListView.getItems().clear();
exceptionsListView.getItems().addAll(convertedExceptions);
// Change UNTIL type
if (untilRadioButton.isSelected())
{
Temporal untilOld = vComponent.getRecurrenceRule().getValue().getUntil().getValue();
final Temporal untilNew;
if (newValue instanceof ZonedDateTime)
{
LocalTime localTime = ((ZonedDateTime) newValue).withZoneSameInstant(ZoneId.of("Z")).toLocalTime();
untilNew = LocalDate.from(untilOld).atTime(localTime).atZone(ZoneId.of("Z"));
} else if (newValue instanceof LocalDateTime)
{
LocalTime localTime = LocalTime.from(newValue);
untilNew = LocalDate.from(untilOld).atTime(localTime).atZone(ZoneId.of("Z"));
// untilNew = ((LocalDateTime) newValue.with((TemporalAdjuster) untilOld)).atZone(ZoneId.of("Z"));
} else
{
untilNew = LocalDate.from(vComponent.getRecurrenceRule().getValue().getUntil().getValue());
}
rrule.setUntil(untilNew);
}
exceptionMasterList.clear();
refreshExceptionDates();
});
// Add or remove functionality and listeners when RRULE changes
recurrenceRuleProperty.addListener((obs, oldValue, newValue) ->
{
if (newValue != null)
{
addListeners();
dateTimeStartProperty.addListener(dateTimeStartToExceptionChangeListener);
// vComponent.getDateTimeStart().valueProperty().addListener(dateTimeStartToExceptionChangeListener);
} else
{
removeListeners();
dateTimeStartProperty.removeListener(dateTimeStartToExceptionChangeListener);
// vComponent.getDateTimeStart().valueProperty().removeListener(dateTimeStartToExceptionChangeListener);
}
});
/* EXCEPTIONS
* Note: exceptionComboBox string converter must be setup after the controller's initialization
* because the resource bundle isn't instantiated earlier.
*
* Note2:
* When exceptions change you can only on change ALL or CANCEL - a change is an addition or removal, not merely a TYPE change such as from toggling wholeDayCheckBoy
* TODO - NEED TO MAKE SURE TEMPORAL CLASS MATCHES WHOLE DAY CHECKBOX
*
*/
// exceptionFirstTemporal = vComponent.getDateTimeStart().getValue();
exceptionComboBox.setConverter(new StringConverter<Temporal>()
{ // setup string converter
@Override public String toString(Temporal temporal)
{
DateTimeFormatter myFormatter = getFormatter(temporal);
return myFormatter.format(temporal);
}
@Override public Temporal fromString(String string) { throw new RuntimeException("not required for non editable ComboBox"); }
});
exceptionComboBox.valueProperty().addListener(obs -> addExceptionButton.setDisable(false)); // turn on add button when exception date is selected in combobox
exceptionsListView.getSelectionModel().selectedItemProperty().addListener(obs ->
{
removeExceptionButton.setDisable(false); // turn on add button when exception date is selected in combobox
});
// Format Temporal in exceptionsListView to LocalDate or LocalDateTime
final Callback<ListView<Temporal>, ListCell<Temporal>> temporalCellFactory = new Callback<ListView<Temporal>, ListCell<Temporal>>()
{
@Override
public ListCell<Temporal> call(ListView<Temporal> list)
{
return new ListCell<Temporal>()
{
@Override public void updateItem(Temporal temporal, boolean empty)
{
super.updateItem(temporal, empty);
if (temporal == null || empty)
{
setText(null);
setStyle("");
} else
{
// Format date.
DateTimeFormatter myFormatter = getFormatter(temporal);
setText(myFormatter.format(temporal));
}
}
};
}
};
exceptionsListView.setCellFactory(temporalCellFactory);
/*
* Make infinite scroll bars for exception dates
*
* When node is attached to a scene an on-shown event handler is attached to the window
* to loop through all the vertical scroll bars to add value property listeners to them
* that adds data when scroll is near the extremes.
*
* The scroll bars don't exist before the node is attached to the scene which is why
* the sceneProperty listener and the onShown event handler are required.
*/
sceneProperty().addListener((obs, oldScene, newScene) ->
{
if (newScene != null)
{
newScene.getWindow().setOnShown(event ->
{
for (Node node: exceptionComboBox.lookupAll(".scroll-bar"))
{
if (node instanceof ScrollBar) {
final ScrollBar bar = (ScrollBar) node;
if (bar.getOrientation() == Orientation.VERTICAL)
{
bar.valueProperty().addListener((ChangeListener<Number>) (value, oldValue, newValue) ->
{
if (((double) newValue > 0.9) && ((double) oldValue < 0.9))
{
// add data to bottom
int elements = exceptionComboBox.getItems().size();
if (elements == EXCEPTION_CHOICE_LIMIT)
{
// exceptionFirstTemporal = exceptionComboBox.getItems().get(elements/3);
makeExceptionDates();
bar.setValue(.5);
}
} else if (((double) newValue < 0.1) && ((double) oldValue > 0.1))
{
// add data to top
int elements = exceptionComboBox.getItems().size();
Temporal firstElement = exceptionComboBox.getItems().get(0);
int indexInMasterList = exceptionMasterList.indexOf(firstElement);
int newIndex = Math.max((indexInMasterList - elements/3), 0);
if (newIndex < indexInMasterList)
{
// exceptionFirstTemporal = exceptionMasterList.get(newIndex);
makeExceptionDates();
bar.setValue(.5);
}
}
});
}
}
}
});
}
});
// SETUP CONTROLLER'S INITIAL DATA FROM RRULE
boolean isInitiallyRepeatable = (rrule != null);
if (isInitiallyRepeatable)
{
setInitialValues(vComponent);
}
repeatableCheckBox.setSelected(isInitiallyRepeatable);
// DAY OF WEEK RAIO BUTTON LISTENER (FOR MONTHLY)
dayOfWeekRadioButton.selectedProperty().addListener(dayOfWeekButtonListener);
// LISTENERS TO BE ADDED AFTER INITIALIZATION
addListeners(); // Listeners to update exception dates
frequencyComboBox.valueProperty().addListener(frequencyListener);
}
private void addListeners()
{
endNeverRadioButton.selectedProperty().addListener(neverListener);
intervalSpinner.valueProperty().addListener(intervalSpinnerListener);
dayOfWeekList.addListener(makeExceptionDatesAndSummaryListener);
}
private void removeListeners()
{
endNeverRadioButton.selectedProperty().removeListener(neverListener);
intervalSpinner.valueProperty().removeListener(intervalSpinnerListener);
dayOfWeekList.removeListener(makeExceptionDatesAndSummaryListener);
}
/* Set controls to values in rRule */
private void setInitialValues(VDisplayable<?> vComponent)
{
// setup FREQUENCY
FrequencyType frequencyType = rrule.getFrequency().getValue();
frequencyComboBox.setValue(frequencyType); // will trigger frequencyListener
switch(frequencyType)
{
case MONTHLY:
ByDay rule = (ByDay) rrule.lookupByRule(ByDay.class);
if (rule == null)
{
dayOfMonthRadioButton.setSelected(true);
} else
{
dayOfWeekRadioButton.setSelected(true);
}
break;
case WEEKLY:
setDayOfWeek(rrule);
dateTimeStartRecurrenceNew.addListener(weeklyRecurrenceListener);
break;
default:
break;
}
// frequencyComboBox.valueProperty().addListener((obs, oldValue, newValue) -> );
// rrule.getFrequency().valueProperty().bind(frequencyComboBox.valueProperty());
setFrequencyVisibility(frequencyType);
// setup INTERVAL
final int initialInterval;
if (rrule.getInterval() == null || rrule.getInterval().getValue().equals(1))
{
initialInterval = Interval.DEFAULT_INTERVAL;
} else
{
initialInterval = rrule.getInterval().getValue();
}
intervalSpinner.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 100, initialInterval));
setIntervalText(initialInterval);
// ExDates
if (vComponent.getExceptionDates() != null)
{
List<Temporal> collect = vComponent
.getExceptionDates()
.stream()
.flatMap(e -> e.getValue().stream())
.collect(Collectors.toList());
exceptionsListView.getItems().addAll(collect);
}
int initialCount = (rrule.getCount() != null) ? rrule.getCount().getValue() : INITIAL_COUNT;
endAfterEventsSpinner.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 1000, initialCount));
if (rrule.getCount() != null)
{
endAfterRadioButton.setSelected(true);
} else if (rrule.getUntil() != null)
{
untilRadioButton.setSelected(true);
} else
{
endNeverRadioButton.setSelected(true);
}
startDatePicker.setValue(LocalDate.from(vComponent.getDateTimeStart().getValue()));
refreshSummary();
refreshExceptionDates(); // Should this be here? - TODO - CHECK # OF CALLS
}
/** Set day of week properties if FREQ=WEEKLY and has BYDAY rule
* This method is called only during setup */
private void setDayOfWeek(RecurrenceRuleValue rRule)
{
// Set day of week properties
if (rRule.getFrequency().getValue() == FrequencyType.WEEKLY)
{
ByRule<?> rule = rRule.lookupByRule(ByDay.class);
((ByDay) rule).dayOfWeekWithoutOrdinalList()
.stream()
.forEach(d ->
{
switch(d)
{
case FRIDAY:
fridayCheckBox.setSelected(true);
break;
case MONDAY:
mondayCheckBox.setSelected(true);
break;
case SATURDAY:
saturdayCheckBox.setSelected(true);
break;
case SUNDAY:
sundayCheckBox.setSelected(true);
break;
case THURSDAY:
thursdayCheckBox.setSelected(true);
break;
case TUESDAY:
tuesdayCheckBox.setSelected(true);
break;
case WEDNESDAY:
wednesdayCheckBox.setSelected(true);
break;
default:
break;
}
});
}
}
/** Make list of start date/times for exceptionComboBox */
private void refreshExceptionDates()
{
// exceptionFirstTemporal = vComponent.getDateTimeStart().getValue();
if (vComponent.getRecurrenceRule() != null)
{
makeExceptionDates();
}
}
/** Make list of start date/times for exceptionComboBox */
private void makeExceptionDates()
{
final Temporal newDateTimeStart;
Temporal dtstart = vComponent.getDateTimeStart().getValue();
if (dateTimeStartRecurrenceNew.get() instanceof LocalDate)
{
newDateTimeStart = LocalDate.from(dtstart);
} else if ((dateTimeStartRecurrenceNew.get() instanceof LocalDateTime) || (dateTimeStartRecurrenceNew.get() instanceof ZonedDateTime))
{
LocalDate dtstartLocalDate = LocalDate.from(dtstart);
newDateTimeStart = dateTimeStartRecurrenceNew.get().with(dtstartLocalDate);
} else
{
throw new DateTimeException("Unsupported Temporal type:" + dateTimeStartRecurrenceNew.get().getClass());
}
exceptionComboBox.getItems().clear();
boolean isRecurrenceRuleValid = vComponent.getRecurrenceRule().getValue().isValid();
if (isRecurrenceRuleValid)
{
// FILTER OUT EXCEPTIONS
final Stream<Temporal> stream1;
if (vComponent.getExceptionDates() != null)
{
List<Temporal> allExceptions = vComponent.getExceptionDates()
.stream()
.flatMap(e -> e.getValue().stream())
.collect(Collectors.toList());
stream1 = vComponent.getRecurrenceRule().getValue()
.streamRecurrences(newDateTimeStart)
.filter(v -> ! allExceptions.contains(v));
} else
{
stream1 = vComponent.getRecurrenceRule().getValue()
.streamRecurrences(newDateTimeStart);
}
// Convert to ZonedDateTime time, if needed
final Stream<Temporal> stream2;
if (DateTimeType.of(newDateTimeStart) == DateTimeType.DATE_WITH_LOCAL_TIME_AND_TIME_ZONE)
{
stream2 = stream1.map(t -> ((ZonedDateTime) t).withZoneSameInstant(ZoneId.systemDefault()));
} else
{
stream2 = stream1;
}
Temporal lastDateInMasterList = (exceptionMasterList.isEmpty()) ? newDateTimeStart.with(LocalDate.MIN) : exceptionMasterList.get(exceptionMasterList.size()-1);
List<Temporal> exceptionDates = stream2
.limit(EXCEPTION_CHOICE_LIMIT)
.peek(t ->
{
if (DateTimeUtilities.isAfter(t, lastDateInMasterList))
{
exceptionMasterList.add(t);
}
})
.collect(Collectors.toList());
exceptionComboBox.getItems().addAll(exceptionDates);
} else
{
// Allow invalid as a temporary condition, checked after save
// throw new RuntimeException("not valid RRULE:" + System.lineSeparator() + vComponent.getRecurrenceRule().errors());
}
}
@FXML private void handleAddException()
{
Temporal d = exceptionComboBox.getValue();
exceptionsListView.getItems().add(d);
// final ObservableList<ExceptionDates> exceptionDates;
// if (vComponent.getExceptionDates() == null)
// {
// exceptionDates = FXCollections.observableArrayList();
// vComponent.setExceptionDates(exceptionDates);
// } else
// {
// exceptionDates = vComponent.getExceptionDates();
// }
//
// if (exceptionDates.isEmpty())
// {
// exceptionDates.add(new ExceptionDates(d));
// } else
// {
// vComponent.getExceptionDates().get(0).getValue().add(d);
// }
refreshExceptionDates();
Collections.sort(exceptionsListView.getItems(),DateTimeUtilities.TEMPORAL_COMPARATOR); // Maintain sorted list
if (exceptionComboBox.getValue() == null) addExceptionButton.setDisable(true);
}
@FXML private void handleRemoveException()
{
Temporal d = exceptionsListView.getSelectionModel().getSelectedItem();
vComponent.getExceptionDates().get(0).getValue().remove(d);
refreshExceptionDates();
exceptionsListView.getItems().remove(d);
if (exceptionsListView.getSelectionModel().getSelectedItem() == null)
{
removeExceptionButton.setDisable(true);
}
if (exceptionsListView.getItems().isEmpty())
{
vComponent.setExceptionDates(null);
}
}
// Displays an alert notifying user number input is not valid
private static void notNumberAlert()
{
Alert alert = new Alert(AlertType.ERROR);
alert.setTitle("Invalid Number");
alert.setHeaderText("Please enter valid numbers.");
alert.setContentText("Must be greater than or equal to 1");
ButtonType buttonTypeOk = new ButtonType("OK", ButtonData.CANCEL_CLOSE);
alert.getButtonTypes().setAll(buttonTypeOk);
alert.showAndWait();
}
// Displays an alert notifying UNTIL date is too early
private static void tooEarlyDateAlert(Temporal t)
{
Alert alert = new Alert(AlertType.ERROR);
alert.setTitle("Invalid Date Selection");
alert.setHeaderText("Event can't end before it begins.");
alert.setContentText("Must be after " + Settings.DATE_FORMAT_AGENDA_DATEONLY.format(t));
ButtonType buttonTypeOk = new ButtonType("OK", ButtonData.CANCEL_CLOSE);
alert.getButtonTypes().setAll(buttonTypeOk);
alert.showAndWait();
}
// // Displays an alert notifying UNTIL date is not an occurrence and changed to
// private static void notOccurrenceDateAlert(Temporal temporal)
// {
// Alert alert = new Alert(AlertType.ERROR);
// alert.setTitle("Invalid Date Selection");
// alert.setHeaderText("Not an occurrence date");
// alert.setContentText("Date has been changed to " + Settings.DATE_FORMAT_AGENDA_DATEONLY.format(temporal));
// ButtonType buttonTypeOk = new ButtonType("OK", ButtonData.CANCEL_CLOSE);
// alert.getButtonTypes().setAll(buttonTypeOk);
// alert.showAndWait();
// }
// Displays an alert notifying UNTIL date is not an occurrence and changed to
private static void notOccurrenceDateAlert(Temporal temporal)
{
Alert alert = new Alert(AlertType.ERROR);
alert.setTitle("Invalid Date Selection");
alert.setHeaderText(Settings.DATE_FORMAT_AGENDA_DATEONLY.format(temporal) + " is not an occurrence date");
alert.setContentText("Please select a date following repeat rules");
ButtonType buttonTypeOk = new ButtonType("OK", ButtonData.CANCEL_CLOSE);
alert.getButtonTypes().setAll(buttonTypeOk);
alert.showAndWait();
}
private static void notDateAlert(String exampleDate)
{
Alert alert = new Alert(AlertType.ERROR);
alert.setTitle("Invalid Date");
alert.setHeaderText("Please enter valid date.");
alert.setContentText("Example date format:" + exampleDate);
ButtonType buttonTypeOk = new ButtonType("OK", ButtonData.CANCEL_CLOSE);
alert.getButtonTypes().setAll(buttonTypeOk);
alert.showAndWait();
}
/**
* Produce easy to read summary of repeat rule
* Is limited to producing strings for following repeat rules:
* Any individual Frequency (FREQ)
* COUNT and UNTIL properties
* MONTHLY and WEEKLY with ByDay Byxxx rule
*
* For example:
* RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=11;BYDAY=MO,WE,FR produces:
* "Every 2 weeks on Monday, Wednesday, Friday, 11 times"
*
* @param startTemporal LocalDate or LocalDateTime of start date/time (DTSTART)
* @return Easy to read summary of repeat rule
*/
public static String makeSummary(RecurrenceRuleValue rrule, Temporal startTemporal)
{
// System.out.println("new summary:" + rrule);
if (! rrule.isValid())
{
return (Settings.resources == null) ? "Never" : Settings.resources.getString("rrule.summary.never");
}
StringBuilder builder = new StringBuilder();
if ((rrule.getCount() != null) && (rrule.getCount().getValue() == 1))
{
return (Settings.resources == null) ? "Once" : Settings.resources.getString("rrule.summary.once");
}
final String frequencyText;
if ((rrule.getInterval() == null) || (rrule.getInterval().getValue() == 1))
{
frequencyText = Settings.REPEAT_FREQUENCIES.get(rrule.getFrequency().getValue());
} else if (rrule.getInterval().getValue() > 1)
{
String every = (Settings.resources == null) ? "Every" : Settings.resources.getString("rrule.summary.every");
builder.append(every + " ");
builder.append(rrule.getInterval().getValue() + " ");
frequencyText = Settings.REPEAT_FREQUENCIES_PLURAL.get(rrule.getFrequency().getValue());
} else
{
throw new RuntimeException("Interval can't be less than 1");
}
builder.append(frequencyText);
// NOTE: only ByRule allowed for this control is ByDay - others are not supported by this control
ByDay byDay = (ByDay) rrule.lookupByRule(ByDay.class);
switch (rrule.getFrequency().getValue())
{
case DAILY: // add nothing else
break;
case MONTHLY:
int dayOfMonth = LocalDate.from(startTemporal).getDayOfMonth();
String onDay = (Settings.resources == null) ? "on day" : Settings.resources.getString("rrule.summary.on.day");
if (byDay == null)
{
builder.append(" " + onDay + " " + dayOfMonth);
} else
{
String onThe = (Settings.resources == null) ? "on the" : Settings.resources.getString("rrule.summary.on.the");
builder.append(" " + onThe + " " + byDaySummary(byDay));
}
break;
case WEEKLY:
{
String on = (Settings.resources == null) ? "on" : Settings.resources.getString("rrule.summary.on");
if (byDay == null)
{
DayOfWeek dayOfWeek = LocalDate.from(startTemporal).getDayOfWeek();
String dayOfWeekString = Settings.DAYS_OF_WEEK_MAP.get(dayOfWeek);
builder.append(" " + on + " " + dayOfWeekString);
} else
{
builder.append(" " + on + " " + byDaySummary(byDay));
}
break;
}
case YEARLY:
{
String on = (Settings.resources == null) ? "on" : Settings.resources.getString("rrule.summary.on");
String monthAndDay = Settings.DATE_FORMAT_AGENDA_MONTHDAY.format(startTemporal);
builder.append(" " + on + " " + monthAndDay);
break;
}
case HOURLY:
case MINUTELY:
case SECONDLY:
throw new IllegalArgumentException("Not supported:" + rrule.getFrequency().getValue());
default:
break;
}
if (rrule.getCount() != null)
{
String times = (Settings.resources == null) ? "times" : Settings.resources.getString("rrule.summary.times");
builder.append(", " + rrule.getCount().getValue() + " " + times);
} else if (rrule.getUntil() != null)
{
String until = (Settings.resources == null) ? "until" : Settings.resources.getString("rrule.summary.until");
String date = Settings.DATE_FORMAT_AGENDA_DATEONLY.format(rrule.getUntil().getValue());
builder.append(", " + until + " " + date);
}
return builder.toString();
}
private boolean isSupported(VDisplayable<?> vComponent)
{
if (rrule == null)
{
return true;
}
ByDay byDay = (ByDay) rrule.lookupByRule(ByDay.class);
int byRulesSize = (rrule.getByRules() == null) ? 0 : rrule.getByRules().size();
int unsupportedRules = (byDay == null) ? byRulesSize : byRulesSize-1;
if (unsupportedRules > 0)
{
String unsupportedByRules = rrule.getByRules()
.stream()
// .map(e -> e.getValue())
.filter(b -> b instanceof ByDay)
// .filter(b -> b.elementType() != RRuleElementType.BY_DAY)
// .map(b -> b.elementType().toString())
.map(b -> b.name())
.collect(Collectors.joining(","));
System.out.println("RRULE contains unsupported ByRule" + ((unsupportedRules > 1) ? "s:" : ":") + unsupportedByRules);
return false;
}
return true;
}
/**
* Produces an easy to ready summary for ByDay rule with only one ByDayPair.
* Returns null for more than one ByDayPair.
* Example: third Monday
*
* @return easy to read summary of rule
*/
private static String byDaySummary(ByDay byDay)
{
StringBuilder builder = new StringBuilder();
for (ByDayPair b : byDay.getValue())
{
int ordinal = b.getOrdinal();
DayOfWeek dayOfWeek = b.getDayOfWeek();
String ordinalString = (ordinal > 0) ? Settings.ORDINALS.get(ordinal) + " " : "";
String dayOfWeekString = Settings.DAYS_OF_WEEK_MAP.get(dayOfWeek);
if (builder.length() > 0) builder.append(", ");
builder.append(ordinalString + dayOfWeekString);
}
return builder.toString();
}
protected static void loadFxml(URL fxmlFile, Object rootController)
{
FXMLLoader loader = new FXMLLoader(fxmlFile);
loader.setController(rootController);
loader.setRoot(rootController);
loader.setResources(Settings.resources);
try {
loader.load();
}
catch(IOException e) {
e.printStackTrace();
}
}
}