[california/wip/725786-edit-recurring] Final touches
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [california/wip/725786-edit-recurring] Final touches
- Date: Thu, 17 Jul 2014 01:25:06 +0000 (UTC)
commit a266b063c167033cbebfc298cce838560a9b0e08
Author: Jim Nelson <jim yorba org>
Date: Wed Jul 16 18:24:47 2014 -0700
Final touches
src/Makefile.am | 1 +
.../backing-calendar-source-subscription.vala | 33 ------
src/backing/backing-calendar-source.vala | 6 +-
.../backing-eds-calendar-source-subscription.vala | 54 +++++++---
src/backing/eds/backing-eds-calendar-source.vala | 7 +-
src/collection/collection-iterable.vala | 13 +++
src/collection/collection.vala | 14 +++
src/component/component-date-time.vala | 57 ++++++-----
src/component/component-instance.vala | 93 +++++++++++++++++
src/component/component-recurrence-rule.vala | 25 +++++-
src/host/host-create-update-event.vala | 31 +++++-
src/host/host-create-update-recurring.vala | 105 +++++++++++++++-----
src/rc/create-update-recurring.ui | 22 ++++
src/tests/tests-iterable.vala | 45 +++++++++
src/tests/tests.vala | 1 +
15 files changed, 399 insertions(+), 108 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index 14a1332..3976255 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -108,6 +108,7 @@ california_VALASOURCES = \
tests/tests-calendar-month-of-year.vala \
tests/tests-calendar-month-span.vala \
tests/tests-calendar-wall-time.vala \
+ tests/tests-iterable.vala \
tests/tests-quick-add.vala \
tests/tests-quick-add-recurring.vala \
tests/tests-string.vala \
diff --git a/src/backing/backing-calendar-source-subscription.vala
b/src/backing/backing-calendar-source-subscription.vala
index bf4fbc4..0ffd6f7 100644
--- a/src/backing/backing-calendar-source-subscription.vala
+++ b/src/backing/backing-calendar-source-subscription.vala
@@ -337,39 +337,6 @@ public abstract class CalendarSourceSubscription : BaseObject {
return instances.contains(uid) ? instances.get(uid) : null;
}
- /**
- * Returns the master { link Component.Instance} for the { link Component.UID}.
- *
- * @returns null if the UID has not been sene.
- */
- public Component.Instance? master_for_uid(Component.UID uid) {
- return traverse<Component.Instance>(instances.get(uid))
- .first_matching(instance => instance.is_master_instance);
- }
-
- /**
- * Returns the seen { link Component.Instance} matching the supplied (possibly partially
- * filled-out) Instance.
- *
- * This is for duplicate detection, especially if the { link Backing} is receiving raw iCal
- * source and needs to verify if it's been parsed and introduced into the system.
- *
- * A blank Instance with partial fields filled out can be supplied.
- */
- public Component.Instance? has_instance(Component.Instance instance) {
- Gee.Collection<Component.Instance>? seen_instances = for_uid(instance.uid);
- if (seen_instances == null || seen_instances.size == 0)
- return null;
-
- // for every instance matching its UID, look for the original
- foreach (Component.Instance seen_instance in seen_instances) {
- if (seen_instance.equal_to(instance))
- return seen_instance;
- }
-
- return null;
- }
-
public override string to_string() {
return "%s::%s".printf(calendar.to_string(), window.to_string());
}
diff --git a/src/backing/backing-calendar-source.vala b/src/backing/backing-calendar-source.vala
index 7b2fac0..2e85e24 100644
--- a/src/backing/backing-calendar-source.vala
+++ b/src/backing/backing-calendar-source.vala
@@ -19,7 +19,7 @@ namespace California.Backing {
public abstract class CalendarSource : Source {
/**
- * The affected range of a modification or removal operation.
+ * The affected range of a removal operation.
*
* Note that zero (0) does ''not'' mean "none", it means { link AffectedInstances.THIS}. The
* additional enums merely expand the scope of the default, which is the supplied instance.
@@ -64,6 +64,10 @@ public abstract class CalendarSource : Source {
/**
* Updates an existing { link Component} instance on the backing { link CalendarSource}.
*
+ * To update all { link Instance}s of a recurring { link Instance}, submit the
+ * { link Component.Instance.master} with modifications rather than one of its generated
+ * instances. Submit a generated instance to update only that one.
+ *
* Outstanding { link CalendarSourceSubscriptions} will eventually report the changes when
* ready.
*/
diff --git a/src/backing/eds/backing-eds-calendar-source-subscription.vala
b/src/backing/eds/backing-eds-calendar-source-subscription.vala
index 30a314f..ab1d582 100644
--- a/src/backing/eds/backing-eds-calendar-source-subscription.vala
+++ b/src/backing/eds/backing-eds-calendar-source-subscription.vala
@@ -188,32 +188,56 @@ internal class EdsCalendarSourceSubscription : CalendarSourceSubscription {
if (!has_uid(uid))
continue;
- // find original for this one
+ Component.DateTime? rid = null;
+ try {
+ rid = new Component.DateTime(ical_component, iCal.icalproperty_kind.RECURRENCEID_PROPERTY);
+ } catch (ComponentError comperr) {
+ if (!(comperr is ComponentError.UNAVAILABLE)) {
+ debug("Unable to get RID of modified component: %s", comperr.message);
+
+ continue;
+ }
+ }
+
+ // get all instances known for this UID to find original to alter
Gee.Collection<Component.Instance>? instances = for_uid(uid);
- if (instances == null || instances.size == 0)
- continue;
- foreach (Component.Instance instance in instances) {
- Component.Event? known_event = instance as Component.Event;
- if (known_event == null)
+ // if no RID, then only one should be returned
+ Component.Instance? instance = null;
+ if (rid == null) {
+ instance = traverse<Component.Instance>(instances).one();
+ if (instance == null) {
+ debug("%d instances found for modified instance, expected 1",
Collection.size(instances));
+
continue;
-
- try {
- known_event.full_update(ical_component, null);
- } catch (Error err) {
- debug("Unable to update event %s: %s", known_event.to_string(), err.message);
+ }
+ } else {
+ // if RID != null, then find the matching instance
+ instance = traverse<Component.Instance>(instances)
+ .first_matching(inst => inst.rid != null && inst.rid.equal_to(rid));
+ if (instance == null) {
+ debug("Cannot find instance with UID %s RID %s, skipping", uid.to_string(),
rid.to_string());
continue;
}
+ }
+
+ Component.Event? modified_event = instance as Component.Event;
+ if (modified_event == null)
+ continue;
+
+ try {
+ modified_event.full_update(ical_component, null);
+ } catch (Error err) {
+ debug("Unable to update event %s: %s", modified_event.to_string(), err.message);
- notify_instance_altered(known_event);
+ continue;
}
- if (instances.size > 1)
- debug("Warning: updated %d modified events, expecting only 1", instances.size);
+ notify_instance_altered(modified_event);
}
- // add any recurring events
+ // remove and re-add any recurring events
on_objects_added(add_list);
}
diff --git a/src/backing/eds/backing-eds-calendar-source.vala
b/src/backing/eds/backing-eds-calendar-source.vala
index 92a7b1c..d6945d2 100644
--- a/src/backing/eds/backing-eds-calendar-source.vala
+++ b/src/backing/eds/backing-eds-calendar-source.vala
@@ -152,7 +152,10 @@ internal class EdsCalendarSource : CalendarSource {
Cancellable? cancellable = null) throws Error {
check_open();
- yield client.modify_object(instance.ical_component, E.CalObjModType.THIS, cancellable);
+ E.CalObjModType modtype =
+ instance.can_generate_instances ? E.CalObjModType.ALL : E.CalObjModType.THIS;
+
+ yield client.modify_object(instance.ical_component, modtype, cancellable);
}
public override async void remove_all_instances_async(Component.UID uid,
@@ -171,7 +174,7 @@ internal class EdsCalendarSource : CalendarSource {
// include an EXDATE in the original iCal source ... I don't quite understand the benefit of
// this, as this suggests (a) other calendar clients won't learn of the removal and (b) the
// instance will be re-generated the next time the user runs an EDS calendar client. In
- // either case, ONLY maps to our desired effect by adding an EXDATE to the iCal source.
+ // either case, THIS maps to our desired effect by adding an EXDATE to the iCal source.
switch (affected) {
case CalendarSource.AffectedInstances.THIS:
yield client.remove_object(uid.value, rid.value, E.CalObjModType.THIS, cancellable);
diff --git a/src/collection/collection-iterable.vala b/src/collection/collection-iterable.vala
index 395247c..5648456 100644
--- a/src/collection/collection-iterable.vala
+++ b/src/collection/collection-iterable.vala
@@ -191,6 +191,19 @@ public class Iterable<G> : Object {
.map<A>(g => { return (A) g; }));
}
+ /**
+ * Returns the first element in the { link Iterable} if and only if it is the only one,
+ * otherwise returns null.
+ */
+ public G? one() {
+ if (!i.next())
+ return null;
+
+ G element = i get();
+
+ return !i.next() ? element : null;
+ }
+
public G? first() {
return (i.next() ? i get() : null);
}
diff --git a/src/collection/collection.vala b/src/collection/collection.vala
index ba28f21..6557566 100644
--- a/src/collection/collection.vala
+++ b/src/collection/collection.vala
@@ -18,5 +18,19 @@ public void terminate() {
return;
}
+/**
+ * Returns true if the Collection is null or empty (zero elements).
+ */
+public inline bool is_empty(Gee.Collection? c) {
+ return c == null || c.size == 0;
+}
+
+/**
+ * Returns the size of the Collection, zero if null.
+ */
+public inline int size(Gee.Collection? c) {
+ return !is_empty(c) ? c.size : 0;
+}
+
}
diff --git a/src/component/component-date-time.vala b/src/component/component-date-time.vala
index 85825c5..1ad627c 100644
--- a/src/component/component-date-time.vala
+++ b/src/component/component-date-time.vala
@@ -62,13 +62,7 @@ public class DateTime : BaseObject, Gee.Hashable<DateTime>, Gee.Comparable<DateT
public iCal.icalproperty_kind kind;
/**
- * Creates a new { link DateTime} for the iCal component of the property kind.
- *
- * Note that the only properties currently supported are:
- * * DTSTART_PROPERTY
- * * DTEND_PROPERTY
- * * DTSTAMP_PROPERTY
- * * RECURRENCEID_PROPERTY
+ * Creates a new { link DateTime} for the first iCal property of the kind.
*
* @throws ComponentError.UNAVAILABLE if not found
* @throws ComponentError.INVALID if not a valid DATE or DATE-TIME
@@ -79,32 +73,33 @@ public class DateTime : BaseObject, Gee.Hashable<DateTime>, Gee.Comparable<DateT
if (prop == null)
throw new ComponentError.UNAVAILABLE("No property of kind %s", ical_prop_kind.to_string());
- switch (ical_prop_kind) {
- case iCal.icalproperty_kind.DTSTAMP_PROPERTY:
- dt = prop.get_dtstamp();
- break;
-
- case iCal.icalproperty_kind.DTSTART_PROPERTY:
- dt = prop.get_dtstart();
- break;
-
- case iCal.icalproperty_kind.DTEND_PROPERTY:
- dt = prop.get_dtend();
+ init_from_property(prop);
+ }
+
+ public DateTime.from_property(iCal.icalproperty prop) throws ComponentError {
+ init_from_property(prop);
+ }
+
+ private void init_from_property(iCal.icalproperty prop) throws ComponentError {
+ unowned iCal.icalvalue prop_value = prop.get_value();
+ switch (prop_value.isa()) {
+ case iCal.icalvalue_kind.DATE_VALUE:
+ dt = prop_value.get_date();
break;
- case iCal.icalproperty_kind.RECURRENCEID_PROPERTY:
- dt = prop.get_recurrenceid();
+ case iCal.icalvalue_kind.DATETIME_VALUE:
+ dt = prop_value.get_datetime();
break;
default:
- assert_not_reached();
+ throw new ComponentError.INVALID("Not a DATE/DATE-TIME value: %s",
prop_value.isa().to_string());
}
if (iCal.icaltime_is_null_time(dt) != 0)
- throw new ComponentError.INVALID("DATE-TIME for %s is null time", ical_prop_kind.to_string());
+ throw new ComponentError.INVALID("DATE-TIME for %s is null time", prop.isa().to_string());
if (iCal.icaltime_is_valid_time(dt) == 0)
- throw new ComponentError.INVALID("DATE-TIME for %s is invalid", ical_prop_kind.to_string());
+ throw new ComponentError.INVALID("DATE-TIME for %s is invalid", prop.isa().to_string());
unowned iCal.icalparameter? param = prop.get_first_parameter(iCal.icalparameter_kind.TZID_PARAMETER);
if (param != null) {
@@ -120,7 +115,7 @@ public class DateTime : BaseObject, Gee.Hashable<DateTime>, Gee.Comparable<DateT
zone = new Calendar.OlsonZone(dt.zone->get_location());
}
- kind = ical_prop_kind;
+ kind = prop.isa();
value = prop.get_value_as_string();
}
@@ -216,6 +211,20 @@ public class DateTime : BaseObject, Gee.Hashable<DateTime>, Gee.Comparable<DateT
}
/**
+ * Returns an iCal value for the { link DateTime}.
+ */
+ internal iCal.icalvalue to_ical_value() {
+ iCal.icalvalue prop_value = new iCal.icalvalue(
+ is_date ? iCal.icalvalue_kind.DATE_VALUE : iCal.icalvalue_kind.DATE_VALUE);
+ if (is_date)
+ prop_value.set_date(dt);
+ else
+ prop_value.set_datetime(dt);
+
+ return prop_value;
+ }
+
+ /**
* Convert two { link DateTime}s into a { link Calendar.DateSpan} or a
* { link Calendar.ExactTimeSpan} depending on what they represent.
*
diff --git a/src/component/component-instance.vala b/src/component/component-instance.vala
index 002dd08..aff3575 100644
--- a/src/component/component-instance.vala
+++ b/src/component/component-instance.vala
@@ -35,6 +35,8 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
public const string PROP_ICAL_COMPONENT = "ical-component";
public const string PROP_RRULE = "rrule";
public const string PROP_RID = "rid";
+ public const string PROP_EXDATES = "exdates";
+ public const string PROP_RDATES = "rdates";
public const string PROP_SEQUENCE = "sequence";
public const string PROP_MASTER = "master";
@@ -83,6 +85,40 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
public Component.DateTime? rid { get; set; default = null; }
/**
+ * All EXDATEs (DATE-TIME exceptions for recurring instances) in the { link Instance}.
+ *
+ * Returns a read-only set of { link DateTime}s. Use { link set_exdates} to change.
+ *
+ * See [[https://tools.ietf.org/html/rfc5545#section-3.8.5.1]]
+ */
+ private Gee.SortedSet<DateTime>? _exdates = null;
+ public Gee.SortedSet<DateTime>? exdates {
+ owned get {
+ return Collection.is_empty(_exdates) ? null : _exdates.read_only_view;
+ }
+
+ set {
+ _exdates = value;
+ }
+ }
+
+ /**
+ * All RDATEs (DATE-TIMEs manually set for recurring instances) in the { link Instance}.
+ *
+ * See [[https://tools.ietf.org/html/rfc5545#section-3.8.5.2]]
+ */
+ private Gee.SortedSet<DateTime>? _rdates = null;
+ public Gee.SortedSet<DateTime>? rdates {
+ owned get {
+ return Collection.is_empty(_rdates) ? null : _rdates.read_only_view;
+ }
+
+ set {
+ _rdates = value;
+ }
+ }
+
+ /**
* Returns true if the { link Instance} is a master instance.
*
* A master instance is one that has not been generated from another Instance's recurring
@@ -312,6 +348,9 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
debug("Unable to parse RRULE for %s: %s", to_string(), comperr.message);
}
+ exdates = get_multiple_date_times(iCal.icalproperty_kind.EXDATE_PROPERTY);
+ rdates = get_multiple_date_times(iCal.icalproperty_kind.RDATE_PROPERTY);
+
// save own copy of component; no ownership transferrance w/ current bindings
if (_ical_component != ical_component)
_ical_component = ical_component.clone();
@@ -344,6 +383,20 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
rrule.add_to_ical(ical_component);
break;
+ case PROP_EXDATES:
+ if (Collection.is_empty(exdates))
+ remove_all_properties(iCal.icalproperty_kind.EXDATE_PROPERTY);
+ else
+ set_multiple_date_times(iCal.icalproperty_kind.EXDATE_PROPERTY, exdates);
+ break;
+
+ case PROP_RDATES:
+ if (Collection.is_empty(rdates))
+ remove_all_properties(iCal.icalproperty_kind.RDATE_PROPERTY);
+ else
+ set_multiple_date_times(iCal.icalproperty_kind.RDATE_PROPERTY, rdates);
+ break;
+
default:
altered = false;
break;
@@ -416,6 +469,46 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
}
/**
+ * Convenience method to convert a collection of DATE/DATE-TIME properties into a SortedSet of
+ * { link DateTime}s.
+ *
+ * @see set_multiple_date_times
+ */
+ protected Gee.SortedSet<DateTime>? get_multiple_date_times(iCal.icalproperty_kind kind) {
+ Gee.SortedSet<DateTime> date_times = new Gee.TreeSet<DateTime>();
+
+ unowned iCal.icalproperty? prop = ical_component.get_first_property(kind);
+ while (prop != null) {
+ try {
+ date_times.add(new DateTime.from_property(prop));
+ } catch (ComponentError comperr) {
+ debug("Unable to parse DATE/DATE-TIME for %s: %s", kind.to_string(), comperr.message);
+ }
+
+ prop = ical_component.get_next_property(kind);
+ }
+
+ return date_times.size > 0 ? date_times : null;
+ }
+
+ /**
+ * Convenience method to set (replace) a collection of DATE/DATE-TIME properties from a
+ * Collection of { link DateTime}s.
+ *
+ * @see get_multiple_date_times
+ */
+ protected void set_multiple_date_times(iCal.icalproperty_kind kind, Gee.Collection<DateTime> date_times)
{
+ remove_all_properties(kind);
+
+ foreach (DateTime date_time in date_times) {
+ iCal.icalproperty prop = new iCal.icalproperty(kind);
+ prop.set_value(date_time.to_ical_value());
+
+ ical_component.add_property(prop);
+ }
+ }
+
+ /**
* Convenience method to convert a { link Calendar.DateSpan} to a pair of iCal DATEs.
*
* dtend_inclusive indicates whether the dt_end should be treated as inclusive or exclusive
diff --git a/src/component/component-recurrence-rule.vala b/src/component/component-recurrence-rule.vala
index 845e94e..9bd45e3 100644
--- a/src/component/component-recurrence-rule.vala
+++ b/src/component/component-recurrence-rule.vala
@@ -24,7 +24,7 @@ public class RecurrenceRule : BaseObject {
* Enumeration of various BY rules (BYSECOND, BYMINUTE, etc.)
*/
public enum ByRule {
- SECOND,
+ SECOND = 0,
MINUTE,
HOUR,
DAY,
@@ -32,7 +32,11 @@ public class RecurrenceRule : BaseObject {
YEAR_DAY,
WEEK_NUM,
MONTH,
- SET_POS
+ SET_POS,
+ /**
+ * The number of { link ByRule}s, this is not a valid value.
+ */
+ COUNT;
}
/**
@@ -338,6 +342,8 @@ public class RecurrenceRule : BaseObject {
dow = Calendar.DayOfWeek.for(dow_value, Calendar.FirstOfWeek.SUNDAY);
} catch (CalendarError calerr) {
debug("Unable to decode day of week value %d: %s", dow_value, calerr.message);
+
+ return false;
}
}
@@ -487,6 +493,21 @@ public class RecurrenceRule : BaseObject {
}
/**
+ * Returns a Gee.Set of { link ByRule}s that are active, i.e. have defined rules.
+ */
+ public Gee.Set<ByRule> get_active_by_rules() {
+ Gee.Set<ByRule> active = new Gee.HashSet<ByRule>();
+ for (int ctr = 0; ctr < ByRule.COUNT; ctr++) {
+ ByRule by_rule = (ByRule) ctr;
+
+ if (get_by_set(by_rule).size > 0)
+ active.add(by_rule);
+ }
+
+ return active;
+ }
+
+ /**
* Converts a { link RecurrenceRule} into an iCalendar RRULE property and adds it to the
* iCal component.
*
diff --git a/src/host/host-create-update-event.vala b/src/host/host-create-update-event.vala
index 69d6731..8f0b45f 100644
--- a/src/host/host-create-update-event.vala
+++ b/src/host/host-create-update-event.vala
@@ -306,14 +306,37 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
// TODO: Now that a clone is being used for editing, can directly bind controls properties to
// Event's properties and update that way ... doesn't quite work when updating the master event,
// however
- private void update_component(Component.Event target, bool update_dtstart) {
+ private void update_component(Component.Event target, bool replace_dtstart) {
target.calendar_source = calendar_model.active;
target.summary = summary_entry.text;
target.location = location_entry.text;
target.description = description_textview.buffer.text;
- if (!update_dtstart)
+ // if updating the master, don't replace the dtstart/dtend, but do want to adjust it from
+ // DATE to DATE-TIME or vice-versa
+ if (!replace_dtstart) {
+ if (target.is_all_day != all_day_toggle.active) {
+ if (all_day_toggle.active) {
+ target.set_event_date_span(target.get_event_date_span(null));
+ } else {
+ // use existing timezone unless not specified in original event
+ Calendar.DateSpan target_date_span = target.get_event_date_span(null);
+ Calendar.Timezone tz = (target.exact_time_span != null)
+ ? target.exact_time_span.start_exact_time.tz
+ : Calendar.Timezone.local;
+ target.set_event_exact_time_span(
+ new Calendar.ExactTimeSpan(
+ new Calendar.ExactTime(tz, target_date_span.start_date,
+ time_map.get(dtstart_time_combo.get_active_text())),
+ new Calendar.ExactTime(tz, target_date_span.end_date,
+ time_map.get(dtend_time_combo.get_active_text()))
+ )
+ );
+ }
+ }
+
return;
+ }
if (all_day_toggle.active) {
target.set_event_date_span(selected_date_span);
@@ -333,8 +356,8 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
}
}
- private void create_update_event(Component.Event target, bool update_dtstart) {
- update_component(target, update_dtstart);
+ private void create_update_event(Component.Event target, bool replace_dtstart) {
+ update_component(target, replace_dtstart);
if (is_update)
update_event_async.begin(target, null);
diff --git a/src/host/host-create-update-recurring.vala b/src/host/host-create-update-recurring.vala
index 30dc901..ce6c4eb 100644
--- a/src/host/host-create-update-recurring.vala
+++ b/src/host/host-create-update-recurring.vala
@@ -95,6 +95,9 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
private Gtk.RadioButton ends_on_radiobutton;
[GtkChild]
+ private Gtk.Label warning_label;
+
+ [GtkChild]
private Gtk.Button end_date_button;
[GtkChild]
@@ -221,20 +224,9 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
event = (Component.Event) message;
master = event.is_master_instance ? event : (Component.Event) event.master;
- // need to use the master component in order to update the master RRULE
- if (!can_update_recurring(event)) {
- jump_back();
-
- return;
- }
-
update_controls();
}
- public static bool can_update_recurring(Component.Event event) {
- return event.is_master_instance || (event.master is Component.Event);
- }
-
private void update_controls() {
make_recurring_checkbutton.active = (master.rrule != null);
@@ -257,45 +249,37 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
repeats_combobox.active = Repeats.DAILY;
every_entry.text = "1";
never_radiobutton.active = true;
+ warning_label.visible = false;
return;
}
// "Repeats" combobox
switch (master.rrule.freq) {
- case iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE:
- repeats_combobox.active = Repeats.DAILY;
- break;
-
- // TODO: Don't allow for editing weekly rules with anything but BYDAY or BYDAY where
- // the position value is non-zero
case iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE:
repeats_combobox.active = Repeats.WEEKLY;
break;
- // TODO: Don't support MONTHLY RRULEs with multiple ByRules or ByRules we can't
- // represent ... basically, non-simple repeating rules
- // TODO: BYDAY should be the exact month-day of week for the DTSTART, MONTH_DAY should
- // be the month-day of the month for the DTSTART
case iCal.icalrecurrencetype_frequency.MONTHLY_RECURRENCE:
bool by_day = master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.DAY).size > 0;
bool by_monthday = master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.MONTH_DAY).size
0;
- if (by_day && !by_monthday)
- repeats_combobox.active = Repeats.DAY_OF_THE_WEEK;
- else if (!by_day && by_monthday)
+ // fall back on month day of the week
+ if (!by_day && by_monthday)
repeats_combobox.active = Repeats.DAY_OF_THE_MONTH;
else
- assert_not_reached();
+ repeats_combobox.active = Repeats.DAY_OF_THE_WEEK;
break;
case iCal.icalrecurrencetype_frequency.YEARLY_RECURRENCE:
repeats_combobox.active = Repeats.YEARLY;
break;
- // TODO: Don't support sub-day RRULEs
+ // Fall back on Daily for default, warning label is shown if anything not supported
+ case iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE:
default:
- assert_not_reached();
+ repeats_combobox.active = Repeats.DAILY;
+ break;
}
// "Every" entry
@@ -328,6 +312,69 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
ends_on_radiobutton.active = true;
end_date = master.rrule.get_recurrence_end_date();
}
+
+ // look for RRULEs that our editor cannot deal with
+ string? supported = is_supported_rrule();
+ if (supported != null)
+ debug("Unsupported RRULE: %s", supported);
+
+ warning_label.visible = supported != null;
+ }
+
+ // Returns a logging string for why not reported, null if supported
+ private string? is_supported_rrule() {
+ // only some frequencies support, and in some of those, certain requirements
+ switch (master.rrule.freq) {
+ case iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE:
+ case iCal.icalrecurrencetype_frequency.YEARLY_RECURRENCE:
+ // do nothing, continue
+ break;
+
+ case iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE:
+ // can only hold BYDAY rules and all BYDAY rules must be zero
+ Gee.Set<Component.RecurrenceRule.ByRule> active = master.rrule.get_active_by_rules();
+ if (!active.contains(Component.RecurrenceRule.ByRule.DAY))
+ return "weekly-not-byday";
+
+ if (active.size > 1)
+ return "weekly-multiple-byrules";
+
+ foreach (int day in master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.DAY)) {
+ int position;
+ if (!Component.RecurrenceRule.decode_day(day, null, out position))
+ return "weekly-undecodeable-byday";
+
+ if (position != 0)
+ return "weekly-nonzero-byday-position";
+ }
+ break;
+
+ // Must be a "simple" monthly recurrence
+ case iCal.icalrecurrencetype_frequency.MONTHLY_RECURRENCE:
+ bool by_day = master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.DAY).size > 0;
+ bool by_monthday = master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.MONTH_DAY).size
0;
+
+ // can support one and only one
+ if (by_day == by_monthday)
+ return "monthly-byday-and-bymonthday";
+
+ if (master.rrule.get_active_by_rules().size > 1)
+ return "monthly-multiple-byrules";
+ break;
+
+ default:
+ return "unsupported-frequency";
+ }
+
+ // do not support editing w/ EXDATEs
+ if (!Collection.is_empty(master.exdates))
+ return "exdates";
+
+ // do not support editing w/ RDATEs
+ if (!Collection.is_empty(master.rdates))
+ return "rdates";
+
+ return null;
}
[GtkCallback]
@@ -526,6 +573,10 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
}
}
+ // remove EXDATEs and RDATEs, those are not currently supported
+ master.exdates = null;
+ master.rdates = null;
+
master.make_recurring(rrule);
}
}
diff --git a/src/rc/create-update-recurring.ui b/src/rc/create-update-recurring.ui
index 302c5ec..205cef3 100644
--- a/src/rc/create-update-recurring.ui
+++ b/src/rc/create-update-recurring.ui
@@ -520,6 +520,28 @@
</object>
<packing>
<property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="warning_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="no_show_all">True</property>
+ <property name="margin_left">16</property>
+ <property name="margin_top">8</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">WARNING: California cannot edit this event's recurring
criteria.
+• Press Cancel to keep the current criteria.
+• Press OK to overwrite the existing criteria with your changes.</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
<property name="top_attach">2</property>
<property name="width">2</property>
<property name="height">1</property>
diff --git a/src/tests/tests-iterable.vala b/src/tests/tests-iterable.vala
new file mode 100644
index 0000000..905c721
--- /dev/null
+++ b/src/tests/tests-iterable.vala
@@ -0,0 +1,45 @@
+/* Copyright 2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+namespace California.Tests {
+
+private class Iterable : UnitTest.Harness {
+ public Iterable() {
+ add_case("one-zero", one_zero);
+ add_case("one-one", one_one);
+ add_case("one_many", one_many);
+ }
+
+ protected override void setup() throws Error {
+ Collection.init();
+ }
+
+ protected override void teardown() {
+ Collection.terminate();
+ }
+
+ private bool one_zero() throws Error {
+ return traverse<int?>(new Gee.ArrayList<int?>()).one() == null;
+ }
+
+ private bool one_one() throws Error {
+ Gee.ArrayList<int?> list = new Gee.ArrayList<int?>();
+ list.add(1);
+
+ return traverse<int?>(list).one() == 1;
+ }
+
+ private bool one_many() throws Error {
+ Gee.ArrayList<int?> list = new Gee.ArrayList<int?>();
+ list.add(1);
+ list.add(2);
+
+ return traverse<int?>(list).one() == null;
+ }
+}
+
+}
+
diff --git a/src/tests/tests.vala b/src/tests/tests.vala
index b8dd74c..24c171e 100644
--- a/src/tests/tests.vala
+++ b/src/tests/tests.vala
@@ -12,6 +12,7 @@ public int run(string[] args) {
LogLevelFlags.LEVEL_WARNING | LogLevelFlags.LEVEL_ERROR | LogLevelFlags.LEVEL_CRITICAL);
UnitTest.Harness.register(new String());
+ UnitTest.Harness.register(new Iterable());
UnitTest.Harness.register(new CalendarDate());
UnitTest.Harness.register(new CalendarMonthSpan());
UnitTest.Harness.register(new CalendarMonthOfYear());
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]