[california/wip/725787-remove-recurring] Some headway here.



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]