[california/wip/725787-remove-recurring] Some headway here.
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [california/wip/725787-remove-recurring] Some headway here.
- Date: Fri, 27 Jun 2014 23:28:28 +0000 (UTC)
commit 6e2f49ca103131f13079eaf1ac9dcd7b45b12d72
Author: Jim Nelson <jim yorba org>
Date: Fri Jun 27 16:27:54 2014 -0700
Some headway here.
src/Makefile.am | 1 +
.../backing-calendar-source-subscription.vala | 245 ++++++++++++++++----
.../backing-eds-calendar-source-subscription.vala | 32 +++-
src/collection/collection-iterable.vala | 81 ++++++--
src/component/component-date-time.vala | 6 +
src/component/component-event.vala | 14 +-
src/component/component-instance.vala | 21 ++-
src/component/component-rid.vala | 54 +++++
src/host/host-show-event.vala | 52 ++++-
9 files changed, 436 insertions(+), 70 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index f4c5fd0..f2b9f63 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -86,6 +86,7 @@ california_VALASOURCES = \
component/component-icalendar.vala \
component/component-instance.vala \
component/component-recurrence-rule.vala \
+ component/component-rid.vala \
component/component-uid.vala \
component/component-vtype.vala \
\
diff --git a/src/backing/backing-calendar-source-subscription.vala
b/src/backing/backing-calendar-source-subscription.vala
index 71216fd..3d40dca 100644
--- a/src/backing/backing-calendar-source-subscription.vala
+++ b/src/backing/backing-calendar-source-subscription.vala
@@ -41,27 +41,79 @@ public abstract class CalendarSourceSubscription : BaseObject {
public bool active { get; protected set; default = false; }
/**
+ * Fired as existing master { link Component.Instance}s are discovered when starting a
+ * subscription.
+ *
+ * Only master Instances are reported through this signal. If the master describes recurrences,
+ * those generated recurring Instances will be reported via { link instance_discovered}. If
+ * the master does not describe a recurrence, it will be reported with this signal ''and''
+ * instance_discovered.
+ *
+ * This is fired while { link start} is working, either in the foreground or in the background.
+ * It won't fire until start() is invoked.
+ */
+ public signal void master_discovered(Component.Instance master);
+
+ /**
* Fired as existing { link Component.Instance}s are discovered when starting a subscription.
*
+ * See { link master_discovered} for an explanation of when master Instances and recurring
+ * instances are reported via this signal.
+ *
* This is fired while { link start} is working, either in the foreground or in the background.
* It won't fire until start() is invoked.
*/
public signal void instance_discovered(Component.Instance instance);
/**
+ * Indicates that a master { link Instance} within the { link window} has been added to the
+ * calendar.
+ *
+ * Only master Instances are reported through this signal. If the master describes recurrences,
+ * those recurring Instances will be reported via { link instance_added}. If the master
+ * does not describe a recurrence, it will be reported with this signal ''and''
+ * instance_added.
+ *
+ * The signal is fired for both local additions (added through this interface) and remote
+ * additions.
+ *
+ * This signal won't fire until { link start} is called.
+ */
+ public signal void master_added(Component.Instance instance);
+
+ /**
* Indicates that an { link Instance} within the { link window} has been added to the calendar.
*
+ * See { link master_added} for an explanation of when master Instances and recurring
+ * instances are reported via this signal.
+ *
* The signal is fired for both local additions (added through this interface) and remote
* additions.
*
* This signal won't fire until { link start} is called.
+ *
+ * @see master_added
*/
public signal void instance_added(Component.Instance instance);
/**
+ * Indicates than a master { link Instance} within the { link window} has been removed from
+ * the calendar.
+ *
+ * Like { link master_added} and { link master_discovered}, removal of the master Instance will
+ * always be reported here. If it describes a recurrence, its generated Instances will be
+ * reported removed by { link instance_removed}. Otherwise, the master will ''also'' be
+ * reported removed by instance_removed
+ */
+ public signal void master_removed(Component.Instance instance);
+
+ /**
* Indicates that an { link Instance} within the { link date_window} has been removed from the
* calendar.
*
+ * See { link master_removed} for an explanation of when master Instances and generated
+ * instances are reported via this signal.
+ *
* The signal is fired for both local removals (added through this interface) and remote
* removals.
*
@@ -75,8 +127,10 @@ public abstract class CalendarSourceSubscription : BaseObject {
* This is fired after the alterations have been made. Since the { link Component.Instance}s
* are mutable, it's possible to monitor their properties for changes and be notified that way.
*
- * The signal is fired for both local additions (added through this interface) and remote
- * additions.
+ * This signal is fired for both master and generated Instances.
+ *
+ * The signal is fired for both local alterations (altered through this interface) and remote
+ * alterations.
*
* This signal won't fire until { link start} is called.
*/
@@ -97,6 +151,23 @@ public abstract class CalendarSourceSubscription : BaseObject {
* best course is to call { link Source.set_unavailable} and override
* { link notify_events_dropped} to perform internal bookkeeping.
*/
+ public signal void master_dropped(Component.Instance master);
+
+ /**
+ * Indicates than the { link Instance} within the { link date_window} has been dropped due to
+ * the { link Source} going unavailable.
+ *
+ * Generally all the subscription's instances will be reported one after another, but this
+ * shouldn't be relied upon.
+ *
+ * Since the Source is now unavailable, this indicates that the Subscription will not be
+ * very useful going forward.
+ *
+ * This issue is handled by this base class. Subclasses should only call the notify method
+ * if they have another method of determining the Source is unavailable. Even then, the
+ * best course is to call { link Source.set_unavailable} and override
+ * { link notify_events_dropped} to perform internal bookkeeping.
+ */
public signal void instance_dropped(Component.Instance instance);
/**
@@ -111,6 +182,8 @@ public abstract class CalendarSourceSubscription : BaseObject {
*/
public signal void start_failed(Error err);
+ private Gee.HashMap<Component.UID, Component.Instance> masters = new Gee.HashMap<
+ Component.UID, Component.Instance>();
// Although Component.Instance has no simple notion of one UID for multiple instances, its
// subclasses (i.e. Event) do
private Gee.HashMultiMap<Component.UID, Component.Instance> instances = new Gee.HashMultiMap<
@@ -124,7 +197,26 @@ public abstract class CalendarSourceSubscription : BaseObject {
}
/**
- * Add a @link Component.Instance} discovered while starting the subscription to the
+ * Add a master { link Component.Instance} discovered while starting the subscription to the
+ * internal collection of instances and notify subscribers.
+ *
+ * As with the other notify_*() methods, subclasses should invoke this method to fire the
+ * signal rather than do it directly. This gives { link CalenderSourceSubscription} the
+ * opportunity to update its internal state prior to firing the signal.
+ *
+ * It can also be overridden by a subclass to take action before or after the signal is fired.
+ *
+ * @see master_discovered
+ */
+ protected virtual void notify_master_discovered(Component.Instance master) {
+ if (add_master(master))
+ master_discovered(master);
+ else
+ debug("Cannot add discovered master %s to %s: already known", master.to_string(), to_string());
+ }
+
+ /**
+ * Add a { link Component.Instance} discovered while starting the subscription to the
* internal collection of instances and notify subscribers.
*
* As with the other notify_*() methods, subclasses should invoke this method to fire the
@@ -143,6 +235,19 @@ public abstract class CalendarSourceSubscription : BaseObject {
}
/**
+ * Add a new master { link Component.Instance} to the subscription and notify subscribers.
+ *
+ * @see notify_master_discovered
+ * @see master_added
+ */
+ protected virtual void notify_master_added(Component.Instance master) {
+ if (add_master(master))
+ master_added(master);
+ else
+ debug("Cannot add master %s to %s: already known", master.to_string(), to_string());
+ }
+
+ /**
* Add a new { link Component.Instance} to the subscription and notify subscribers.
*
* @see notify_instance_discovered
@@ -152,18 +257,36 @@ public abstract class CalendarSourceSubscription : BaseObject {
if (add_instance(instance))
instance_added(instance);
else
- debug("Cannot add component %s to %s: already known", instance.to_string(), to_string());
+ debug("Cannot add instance %s to %s: already known", instance.to_string(), to_string());
+ }
+
+ /**
+ * Remove a master { link Component.Instance} from the subscription and notify subscribers.
+ *
+ * It is up to the backing to use { link notify_instance_removed} to remove generated instances.
+ * This class does not automatically remove all generated instances when the master is removed.
+ *
+ * @see notify_master_discovered
+ * @see master_removed
+ */
+ protected virtual void notify_master_removed(Component.UID uid) {
+ if (remove_master(uid))
+ master_removed(uid);
+ else
+ debug("Cannot remove UID %s from %s: not known", uid.to_string(), to_string());
}
/**
* Remove an { link Component.Instance} from the subscription and notify subscribers.
*
+ * If rid is non-null, only that recurring (generated) instance is removed.
+ *
* @see notify_instance_discovered
* @see instance_removed
*/
- protected virtual void notify_instance_removed(Component.UID uid) {
+ protected virtual void notify_instance_removed(Component.UID uid, Component.DateTime? rid) {
Gee.Collection<Component.Instance> removed_instances;
- if (remove_instance(uid, out removed_instances)) {
+ if (remove_instance(uid, rid, out removed_instances)) {
foreach (Component.Instance instance in removed_instances)
instance_removed(instance);
} else {
@@ -178,19 +301,30 @@ public abstract class CalendarSourceSubscription : BaseObject {
* @see instance_altered
*/
protected virtual void notify_instance_altered(Component.Instance instance) {
- if (instances.contains(instance.uid))
+ if (masters.contains(instance.uid) || instances.contains(instance.uid))
instance_altered(instance);
else
debug("Cannot notify altered component %s in %s: not known", instance.to_string(), to_string());
}
/**
+ * Notify that the master { link Component.Instance}s have been dropped due to the
+ * { link Source} going unavailable.
+ */
+ protected virtual void notify_master_dropped(Component.Instance master) {
+ if (remove_master(master.uid))
+ master_dropped(master);
+ else
+ debug("Cannot notify dropped master %s in %s: not known", master.to_string(), to_string());
+ }
+
+ /**
* Notify that the { link Component.Instance}s have been dropped due to the { link Source} going
* unavailable.
*/
protected virtual void notify_instance_dropped(Component.Instance instance) {
Gee.Collection<Component.Instance> removed_instances;
- if (remove_instance(instance.uid, out removed_instances)) {
+ if (remove_instance(instance.uid, instance.rid, out removed_instances)) {
foreach (Component.Instance removed_instance in removed_instances)
instance_dropped(removed_instance);
} else {
@@ -198,6 +332,14 @@ public abstract class CalendarSourceSubscription : BaseObject {
}
}
+ private bool add_master(Component.Instance master) {
+ bool already_exists = masters.has_key(master.uid);
+ if (!already_exists)
+ masters.set(master.uid, master);
+
+ return !already_exists;
+ }
+
private bool add_instance(Component.Instance instance) {
bool already_exists = instances.get(instance.uid).contains(instance);
if (!already_exists)
@@ -206,16 +348,30 @@ public abstract class CalendarSourceSubscription : BaseObject {
return !already_exists;
}
- private bool remove_instance(Component.UID uid, out Gee.Collection<Component.Instance>
removed_instances) {
- bool removed = instances.contains(uid);
- if (removed) {
+ private bool remove_master(Component.UID uid) {
+ return masters.unset(uid);
+ }
+
+ private bool remove_instance(Component.UID uid, Component.RID? rid,
+ out Gee.Collection<Component.Instance> removed_instances) {
+ if (!instances.contains(uid)) {
+ removed_instances = new Gee.ArrayList<Component.Instance>();
+
+ return false;
+ }
+
+ if (rid == null) {
removed_instances = instances.get(uid);
instances.remove_all(uid);
} else {
- removed_instances = new Gee.ArrayList<Component.Instance>();
+ // use array so alteration if instances is possible in the iterate() call
+ removed_instances = traverse_safely<Component.Instance>(instances.get(uid))
+ .filter(instance => instance.rid != null && instance.rid.equal_to(rid))
+ .iterate(instance => instances.remove(instance.uid, instance))
+ .to_array_list();
}
- return removed;
+ return true;
}
/**
@@ -253,51 +409,58 @@ public abstract class CalendarSourceSubscription : BaseObject {
if (calendar.is_available)
return;
- // Use to_array() so no iteration troubles when notify_instance_dropped removes it from
- // the multimap
- debug("Dropping %d instances to %s: unavailable", instances.size, calendar.to_string());
- foreach (Component.Instance instance in instances.get_values().to_array())
- notify_instance_dropped(instance);
+ // use safe iteration because the notify_ methods will remove from the collections, which
+ // will cause an assertion with the normal traverse() method.
+
+ debug("Dropping %d master instances in %s: unavailable", masters.size, calendar.to_string());
+ traverse_safely<Component.Instance>(masters.values)
+ .iterate(master => notify_master_dropped(master));
+
+ debug("Dropping %d generated instances to %s: unavailable", instances.size, calendar.to_string());
+ traverse_safely<Component.Instance>(instances.get_values())
+ .iterate(instance => notify_instance_dropped(instance));
}
/**
* Returns true if the { link Component.UID} has been seen in this
* { link CalendarSourceSubscription}.
*/
- public bool has_uid(Component.UID uid) {
- return instances.contains(uid);
+ public bool has_master(Component.UID uid) {
+ return masters.has_key(uid);
}
/**
- * Returns all { link Component.Instance}s for the { link Component.UID}.
- *
- * @returns null if the UID has not been seen.
+ * Returns the master { link Component.Instance} for the { link Component.UID}, if seen.
*/
- public Gee.Collection<Component.Instance>? for_uid(Component.UID uid) {
- return instances.contains(uid) ? instances.get(uid) : null;
+ public Component.Instance? master_for_uid(Component.UID uid) {
+ return masters.has_key(uid) ? masters.get(uid) : null;
}
/**
- * 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.
+ * Returns true if the { link CalendarSourceSubscription} has seen an
+ * { link Component.Instance} (generated or otherwise) with the { link Component.UID} and,
+ * optionally, { link Component.RID} RECURRENCE-ID.
*
- * A blank Instance with partial fields filled out can be supplied.
+ * Without an rid, will return true for ''any'' generated Instance.
*/
- 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;
+ public bool has_instance(Component.UID uid, Component.RID? rid) {
+ if (!instances.contains(uid))
+ return false;
- // 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;
- }
+ if (rid == null)
+ return true;
- return null;
+ return traverse<Component.Instance>(instances.get(uid))
+ .any(instance => instance.rid != null && instance.rid.equal_to(rid));
+ }
+
+ /**
+ * Returns all { link Component.Instance}s for the { link Component.UID}.
+ *
+ * @returns null if the UID has not been seen.
+ */
+ public Gee.Collection<Component.Instance>? instances_for_uid(Component.UID uid) {
+ return instances.contains(uid) ? instances.get(uid) : null;
}
public override string to_string() {
diff --git a/src/backing/eds/backing-eds-calendar-source-subscription.vala
b/src/backing/eds/backing-eds-calendar-source-subscription.vala
index 0da9af6..86524da 100644
--- a/src/backing/eds/backing-eds-calendar-source-subscription.vala
+++ b/src/backing/eds/backing-eds-calendar-source-subscription.vala
@@ -112,6 +112,29 @@ internal class EdsCalendarSourceSubscription : CalendarSourceSubscription {
private void on_objects_added(SList<weak iCal.icalcomponent> objects) {
foreach (weak iCal.icalcomponent ical_component in objects) {
+ // convert the added object into an Event component and report as a master
+ Component.Event? added_master;
+ try {
+ added_master = Component.Instance.convert(calendar, ical_component) as Component.Event;
+ if (added_master == null)
+ continue;
+ } catch (Error err) {
+ debug("Unable to process master event: %s", err.message);
+
+ continue;
+ }
+
+ notify_master_added(added_master);
+
+ // if not recurring, report as an instance and stop there
+ if (!added_master.is_recurring_master) {
+ debug("Not generating instances for %s: not recurring", added_master.to_string());
+
+ notify_instance_added(added_master);
+
+ continue;
+ }
+
view.client.generate_instances_for_object(
ical_component,
window.start_exact_time.to_time_t(),
@@ -177,8 +200,13 @@ internal class EdsCalendarSourceSubscription : CalendarSourceSubscription {
}
private void on_objects_removed(SList<weak E.CalComponentId?> ids) {
- foreach (weak E.CalComponentId id in ids)
- notify_instance_removed(new Component.UID(id.uid));
+ foreach (weak E.CalComponentId id in ids) {
+ Component.RID? rid = null;
+ if (id.rid != null)
+ rid = new Component.RID(id.rid);
+
+ notify_instance_removed(new Component.UID(id.uid), rid);
+ }
}
}
diff --git a/src/collection/collection-iterable.vala b/src/collection/collection-iterable.vala
index b551cd2..395247c 100644
--- a/src/collection/collection-iterable.vala
+++ b/src/collection/collection-iterable.vala
@@ -8,14 +8,38 @@ namespace California {
/**
* Take a Gee object and return a California.Iterable for convenience.
+ *
+ * An empty Gee.Iterable is created and used if null is passed in.
*/
-public California.Iterable<G> traverse<G>(Gee.Iterable<G> i) {
- return new California.Iterable<G>(i.iterator());
+public California.Iterable<G> traverse<G>(Gee.Iterable<G>? gee_iterable) {
+ Gee.Iterable<G>? iterable = gee_iterable ?? new Gee.ArrayList<G>();
+
+ return new California.Iterable<G>(iterable.iterator());
}
/**
- * Take some non-null items (all must be of type G) and return a
- * California.Iterable for convenience.
+ * Like { link traverse}, but make a copy of the Gee.Iterable to allow for safe iteration over
+ * it.
+ *
+ * "Safe iteration" means later operations that remove elements while iterating do not cause an
+ * assertion.
+ *
+ * An empty Gee.Iterable is created and used if null is passed in.
+ */
+public California.Iterable<G> traverse_safely<G>(Gee.Iterable<G>? iterable) {
+ Gee.ArrayList<G> list = new Gee.ArrayList<G>();
+
+ if (iterable != null) {
+ foreach (G element in iterable)
+ list.add(element);
+ }
+
+ return California.traverse<G>(list);
+}
+
+/**
+ * Take some non-null items (all must be of type G) and return a California.Iterable for
+ * convenience.
*/
public California.Iterable<G> iterate<G>(G g, ...) {
va_list args = va_list();
@@ -30,27 +54,35 @@ public California.Iterable<G> iterate<G>(G g, ...) {
}
/**
- * Take a non-null array of non-null items (all of type G) and return a California.Iterable
- * for convenience.
+ * Take an array of non-null items (all of type G) and return a California.Iterable for convenience.
+ *
+ * An empty Gee.Iterable is created and used if null is passed in.
*/
-public California.Iterable<G> from_array<G>(G[] ar) {
+public California.Iterable<G> from_array<G>(G[]? ar) {
Gee.ArrayList<G> list = new Gee.ArrayList<G>();
- foreach (G item in ar)
- list.add(item);
+
+ if (ar != null) {
+ foreach (G item in ar)
+ list.add(item);
+ }
return California.traverse<G>(list);
}
/**
* Returns an { link Iterable} of Unicode characters for each in the supplied string.
+ *
+ * An empty Gee.Iterable is created and used if null is passed in.
*/
-public Iterable<unichar> from_string(string str) {
+public Iterable<unichar> from_string(string? str) {
Gee.ArrayList<unichar> list = new Gee.ArrayList<unichar>();
- int index = 0;
- unichar ch;
- while (str.get_next_char(ref index, out ch))
- list.add(ch);
+ if (!String.is_empty(str)) {
+ int index = 0;
+ unichar ch;
+ while (str.get_next_char(ref index, out ch))
+ list.add(ch);
+ }
return California.traverse<unichar>(list);
}
@@ -72,6 +104,11 @@ public class Iterable<G> : Object {
public delegate string? ToString<G>(G element);
/**
+ * For simple iteration of the { link Iterable}.
+ */
+ public delegate void Iterate<G>(G element);
+
+ /**
* A private class that lets us take a California.Iterable and convert it back
* into a Gee.Iterable.
*/
@@ -106,6 +143,22 @@ public class Iterable<G> : Object {
return i;
}
+ /**
+ * Be called for each element in the { link Iterable}.
+ *
+ * No transformation of the Iterable is made. The returned Iterable is for the same set of
+ * elements as had been iterated over.
+ */
+ public Iterable<G> iterate(Iterate<G> iteratee) {
+ Gee.ArrayList<G> list = new Gee.ArrayList<G>();
+ foreach (G g in this) {
+ iteratee(g);
+ list.add(g);
+ }
+
+ return new Iterable<G>(list.iterator());
+ }
+
public Iterable<A> map<A>(Gee.MapFunc<A, G> f) {
return new Iterable<A>(i.map<A>(f));
}
diff --git a/src/component/component-date-time.vala b/src/component/component-date-time.vala
index d398f59..685949c 100644
--- a/src/component/component-date-time.vala
+++ b/src/component/component-date-time.vala
@@ -45,6 +45,11 @@ public class DateTime : BaseObject, Gee.Hashable<DateTime>, Gee.Comparable<DateT
public bool is_date { get { return iCal.icaltime_is_date(dt) != 0; } }
/**
+ * Returns the original iCalendar string representing the DATE/DATE-TIME property value.
+ */
+ public string value_as_ical_string { get; private set; }
+
+ /**
* The DATE-TIME for the iCal component and property kind.
*/
public iCal.icaltimetype dt;
@@ -114,6 +119,7 @@ public class DateTime : BaseObject, Gee.Hashable<DateTime>, Gee.Comparable<DateT
}
kind = ical_prop_kind;
+ value_as_ical_string = prop.get_value_as_string();
}
/**
diff --git a/src/component/component-event.vala b/src/component/component-event.vala
index 9a14b3d..5d67b7a 100644
--- a/src/component/component-event.vala
+++ b/src/component/component-event.vala
@@ -96,6 +96,11 @@ public class Event : Instance, Gee.Comparable<Event> {
public RecurrenceRule? rrule { get; private set; default = null; }
/**
+ * @inheritDoc
+ */
+ public override bool is_recurring_master { get { return rid == null && rrule != null; } }
+
+ /**
* Create an { link Event} { link Component} from an EDS CalComponent object.
*
* Throws a BackingError if the E.CalComponent's VTYPE is not VEVENT.
@@ -439,7 +444,7 @@ public class Event : Instance, Gee.Comparable<Event> {
return compare;
// if recurring, go by sequence number, as the UID and RID are the same for all instances
- if (is_recurring) {
+ if (is_recurring_generated) {
compare = sequence - other.sequence;
if (compare != 0)
return compare;
@@ -457,10 +462,13 @@ public class Event : Instance, Gee.Comparable<Event> {
if (this == other_event)
return true;
- if (is_recurring != other_event.is_recurring)
+ if (is_recurring_master != other_event.is_recurring_master)
+ return false;
+
+ if (is_recurring_generated != other_event.is_recurring_generated)
return false;
- if (is_recurring && !rid.equal_to(other_event.rid))
+ if (is_recurring_generated && !rid.equal_to(other_event.rid))
return false;
if (sequence != other_event.sequence)
diff --git a/src/component/component-instance.vala b/src/component/component-instance.vala
index 56c86ab..ef9f534 100644
--- a/src/component/component-instance.vala
+++ b/src/component/component-instance.vala
@@ -13,6 +13,13 @@ namespace California.Component {
* components which allocate a specific amount of time within a calendar. (Free/Busy does allow
* for time to be published/reserved, but this implementation doesn't deal with that component.)
*
+ * In addition, an Instance may be a ''recurring master'', which means it is the "original" iCal
+ * component describing a recurring event. Its { link Backing} will generated other Instances for
+ * each occurrence of the recurring event. These ''recurring generated'' Instances can be used
+ * like any other Instance, but since the first generated Instance will match the time and date of
+ * the master Instance, care needs to be taken. See { link is_recurring_master} and
+ * { link is_recurring_generated} for more information.
+ *
* Mutability is achieved two separate ways. One is to call { link full_update} supplying a new
* iCal component to update an existing one (verified by UID). This will update all fields.
*
@@ -65,14 +72,19 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
*
* See [[https://tools.ietf.org/html/rfc5545#section-3.8.4.4]]
*/
- public Component.DateTime? rid { get; set; default = null; }
+ public Component.RID? rid { get; set; default = null; }
+
+ /**
+ * Returns true if the { link Instance} is a master describing recurrences.
+ */
+ public abstract bool is_recurring_master { get; }
/**
- * Returns true if the { link Recurrable} is in fact a recurring instance.
+ * Returns true if the { link Instance} is generated from a master with a recurring rule.
*
* @see rid
*/
- public bool is_recurring { get { return rid != null; } }
+ public bool is_recurring_generated { get { return rid != null; } }
/**
* The SEQUENCE of a VEVENT, VTODO, or VJOURNAL.
@@ -240,7 +252,8 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
}
try {
- rid = new DateTime(ical_component, iCal.icalproperty_kind.RECURRENCEID_PROPERTY);
+ rid = new Component.RID.from_date_time(
+ new DateTime(ical_component, iCal.icalproperty_kind.RECURRENCEID_PROPERTY));
} catch (ComponentError comperr) {
// ignore if unavailable
if (!(comperr is ComponentError.UNAVAILABLE))
diff --git a/src/component/component-rid.vala b/src/component/component-rid.vala
new file mode 100644
index 0000000..fdc12fc
--- /dev/null
+++ b/src/component/component-rid.vala
@@ -0,0 +1,54 @@
+/* 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.Component {
+
+/**
+ * An immutable representation of an iCalendar RECURRENCE-ID.
+ *
+ * An { link Component.Instance}'s RECURRENCE-ID, SEQUENCE, and UID can be used to specify a
+ * particular instance of a recurring event.
+ *
+ * Although RECURRENCE-ID is technically a DATE or DATE-TIME value (optionally with a RANGE) which
+ * is better represented as a { link Component.DateTime}, in practice its utility is as a simple
+ * string value, as the particulars of its DATE or DATE-TIME are not of interest when scheduling.
+ * Also, some { link Backings} (like EDS) will some times supply a RECURRENCE-ID as a string instead
+ * of a date-time structure, and there's little need to go through the rigamarole of translating
+ * it into a structure just to get hashing and equality comparison.
+ *
+ * See [[https://tools.ietf.org/html/rfc5545#section-3.8.4.4]].
+ */
+
+public class RID : BaseObject, Gee.Hashable<RID>, Gee.Comparable<RID> {
+ public string value { get; private set; }
+
+ public RID(string value) {
+ this.value = value;
+ }
+
+ public RID.from_date_time(DateTime recurrence_id) {
+ value = recurrence_id.value_as_ical_string;
+ }
+
+ public uint hash() {
+ return value.hash();
+ }
+
+ public bool equal_to(RID other) {
+ return compare_to(other) == 0;
+ }
+
+ public int compare_to(RID other) {
+ return (this != other) ? strcmp(value, other.value) : 0;
+ }
+
+ public override string to_string() {
+ return value;
+ }
+}
+
+}
+
diff --git a/src/host/host-show-event.vala b/src/host/host-show-event.vala
index 9f0cf08..5cdc044 100644
--- a/src/host/host-show-event.vala
+++ b/src/host/host-show-event.vala
@@ -46,6 +46,7 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
private Gtk.Button close_button;
private new Component.Event event;
+ private Gtk.Menu? remove_recurring_menu = null;
public ShowEvent() {
Calendar.System.instance.is_24hr_changed.connect(build_display);
@@ -80,15 +81,35 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
// description
set_label(null, description_text, Markup.linkify(escape(event.description), linkify_delegate));
+ // If recurring (and so this is a generated instance of the VEVENT, not the VEVENT itself),
+ // use a popup menu to ask how to remove this event
+ if (event.is_recurring_generated) {
+ remove_recurring_menu = new Gtk.Menu();
+
+ Gtk.MenuItem remove_all = new Gtk.MenuItem.with_mnemonic(_("Remove _All Events"));
+ remove_all.activate.connect(on_remove_recurring_all);
+ remove_recurring_menu.append(remove_all);
+
+ Gtk.MenuItem remove_this = new Gtk.MenuItem.with_mnemonic(_("Remove Only _This Event"));
+ remove_this.activate.connect(on_remove_recurring_this);
+ remove_recurring_menu.append(remove_this);
+
+ Gtk.MenuItem remove_following = new Gtk.MenuItem.with_mnemonic(
+ _("Remove This and All _Following Events"));
+ remove_following.activate.connect(on_remove_recurring_this_and_following);
+ remove_recurring_menu.append(remove_following);
+ }
+
// don't current support updating or removing recurring events properly; see
// https://bugzilla.gnome.org/show_bug.cgi?id=725786
- // https://bugzilla.gnome.org/show_bug.cgi?id=725787
bool read_only = event.calendar_source != null && event.calendar_source.read_only;
- bool visible = !event.is_recurring && !read_only;
- update_button.visible = visible;
- update_button.no_show_all = !visible;
- remove_button.visible = visible;
- remove_button.no_show_all = !visible;
+
+ bool updatable = !event.is_recurring_generated && !read_only;
+ update_button.visible = updatable;
+ update_button.no_show_all = updatable;
+
+ remove_button.visible = !read_only;
+ remove_button.no_show_all = !read_only;
}
private string? escape(string? plain) {
@@ -130,6 +151,15 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
[GtkCallback]
private void on_remove_button_clicked() {
+ if (event.is_recurring_generated) {
+ assert(remove_recurring_menu != null);
+
+ remove_recurring_menu.popup(null, null, null, 0, Gtk.get_current_event_time());
+ remove_recurring_menu.show_all();
+
+ return;
+ }
+
remove_event_async.begin();
}
@@ -160,6 +190,16 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
else
notify_failure(_("Unable to remove event: %s").printf(remove_err.message));
}
+
+ private void on_remove_recurring_all() {
+ remove_event_async.begin();
+ }
+
+ private void on_remove_recurring_this() {
+ }
+
+ private void on_remove_recurring_this_and_following() {
+ }
}
}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]