[california/wip/725788-recurring: 1/2] Display recurring events



commit b3011af8fbd1b7848c93479258be5c7c8e1a4c9a
Author: Jim Nelson <jim yorba org>
Date:   Fri Mar 7 18:10:06 2014 -0800

    Display recurring events
    
    Component tracking now recognizes that Instances with the same UID
    may exist, it's other properties which determine equality.

 src/Makefile.am                                    |    3 +
 .../backing-calendar-source-subscription.vala      |  152 +++++++++++---------
 .../backing-eds-calendar-source-subscription.vala  |    8 +-
 src/backing/eds/backing-eds-calendar-source.vala   |    2 +-
 src/component/component-date-time.vala             |   46 ++++++-
 src/component/component-event.vala                 |   68 +++++++++-
 src/component/component-instance.vala              |   93 ++++++++++--
 src/component/component-recurrable.vala            |   67 +++++++++
 src/component/component-sequenceable.vala          |   48 ++++++
 src/util/util-memory.vala                          |   27 ++++
 src/view/month/month-controllable.vala             |   20 ++-
 11 files changed, 437 insertions(+), 97 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index e8942d2..fc98016 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -57,6 +57,8 @@ california_VALASOURCES = \
        component/component-error.vala \
        component/component-event.vala \
        component/component-instance.vala \
+       component/component-recurrable.vala \
+       component/component-sequenceable.vala \
        component/component-uid.vala \
        component/component-vtype.vala \
        \
@@ -67,6 +69,7 @@ california_VALASOURCES = \
        host/host-popup.vala \
        host/host-show-event.vala \
        \
+       util/util-memory.vala \
        util/util-string.vala \
        \
        view/view.vala \
diff --git a/src/backing/backing-calendar-source-subscription.vala 
b/src/backing/backing-calendar-source-subscription.vala
index cdbac28..90245d5 100644
--- a/src/backing/backing-calendar-source-subscription.vala
+++ b/src/backing/backing-calendar-source-subscription.vala
@@ -41,35 +41,36 @@ public abstract class CalendarSourceSubscription : BaseObject {
     public bool active { get; protected set; default = false; }
     
     /**
-     * Fired as existing { link Component.Event}s are discovered when starting a subscription.
+     * Fired as existing { link Component.Instance}s are discovered when starting a subscription.
      *
      * 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 event_discovered(Component.Event event);
+    public signal void instance_discovered(Component.Instance instance);
     
     /**
-     * Indicates that an event within the { link window} has been added to the calendar.
+     * Indicates that an { link Instance} within the { link window} has been added to the calendar.
      *
      * 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 event_added(Component.Event event);
+    public signal void instance_added(Component.Instance instance);
     
     /**
-     * Indicates that an event within the { link date_window} has been removed from the calendar.
+     * Indicates that an { link Instance} within the { link date_window} has been removed from the
+     * calendar.
      *
      * The signal is fired for both local removals (added through this interface) and remote
      * removals.
      *
      * This signal won't fire until { link start} is called.
      */
-    public signal void event_removed(Component.Event event);
+    public signal void instance_removed(Component.Instance instance);
     
     /**
-     * Indicates that an event within the { link date_window} has been altered.
+     * Indicates that an { link Instance} within the { link date_window} has been altered.
      *
      * 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.
@@ -79,13 +80,13 @@ public abstract class CalendarSourceSubscription : BaseObject {
      *
      * This signal won't fire until { link start} is called.
      */
-    public signal void event_altered(Component.Event event);
+    public signal void instance_altered(Component.Instance instance);
     
     /**
-     * Indicates than the event within the { link date_window} has been dropped due to the
-     * { link Source} going unavailable.
+     * 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 events will be reported one after another, but this
+     * 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
@@ -96,7 +97,7 @@ 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 event_dropped(Component.Event event);
+    public signal void instance_dropped(Component.Instance instance);
     
     /**
      * Fired if { link start} failed.
@@ -110,8 +111,10 @@ public abstract class CalendarSourceSubscription : BaseObject {
      */
     public signal void start_failed(Error err);
     
-    private Gee.HashMap<Component.UID, Component.Event> events = new Gee.HashMap<
-        Component.UID, Component.Event>();
+    // 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<
+        Component.UID, Component.Instance>();
     
     protected CalendarSourceSubscription(CalendarSource calendar, Calendar.ExactTimeSpan window) {
         this.calendar = calendar;
@@ -121,7 +124,8 @@ public abstract class CalendarSourceSubscription : BaseObject {
     }
     
     /**
-     * Add a pre-existing { link Component.Event} to the subscription and notify subscribers.
+     * 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
      * signal rather than do it directly.  This gives { link CalenderSourceSubscription} the
@@ -129,68 +133,89 @@ public abstract class CalendarSourceSubscription : BaseObject {
      *
      * It can also be overridden by a subclass to take action before or after the signal is fired.
      *
-     * @see event_discovered
+     * @see instance_discovered
      */
-    protected virtual void notify_event_discovered(Component.Event event) {
-        if (!events.has_key(event.uid)) {
-            events.set(event.uid, event);
-            event_discovered(event);
-        } else {
-            debug("Cannot add discovered event %s to %s: already known", event.to_string(), to_string());
-        }
+    protected virtual void notify_instance_discovered(Component.Instance instance) {
+        if (add_instance(instance))
+            instance_discovered(instance);
+        else
+            debug("Cannot add discovered component %s to %s: already known", instance.to_string(), 
to_string());
     }
     
     /**
-     * Add a new { link Component.Event} to the subscription and notify subscribers.
+     * Add a new { link Component.Instance} to the subscription and notify subscribers.
      *
-     * @see notify_event_discovered
-     * @see event_added
+     * @see notify_instance_discovered
+     * @see instance_added
      */
-    protected virtual void notify_event_added(Component.Event event) {
-        if (!events.has_key(event.uid)) {
-            events.set(event.uid, event);
-            event_added(event);
-        } else {
-            debug("Cannot add event %s to %s: already known", event.to_string(), to_string());
-        }
+    protected virtual void notify_instance_added(Component.Instance instance) {
+        if (add_instance(instance))
+            instance_added(instance);
+        else
+            debug("Cannot add component %s to %s: already known", instance.to_string(), to_string());
     }
     
     /**
-     * Remove an { link Component.Event} from the subscription and notify subscribers.
+     * Remove an { link Component.Instance} from the subscription and notify subscribers.
      *
-     * @see notify_event_discovered
-     * @see event_removed
+     * @see notify_instance_discovered
+     * @see instance_removed
      */
-    protected virtual void notify_event_removed(Component.UID uid) {
-        Component.Event? event;
-        if (events.unset(uid, out event))
-            event_removed(event);
-        else
+    protected virtual void notify_instance_removed(Component.UID uid) {
+        Gee.Collection<Component.Instance> removed_instances;
+        if (remove_instance(uid, out removed_instances)) {
+            foreach (Component.Instance instance in removed_instances)
+                instance_removed(instance);
+        } else {
             debug("Cannot remove UID %s from %s: not known", uid.to_string(), to_string());
+        }
     }
     
     /**
-     * Update an altered { link Component.Event} and notify subscribers.
+     * Update an altered { link Component.Instance} and notify subscribers.
      *
-     * @see notify_event_discovered
-     * @see event_altered
+     * @see notify_instance_discovered
+     * @see instance_altered
      */
-    protected virtual void notify_event_altered(Component.Event event) {
-        if (events.has_key(event.uid))
-            event_altered(event);
+    protected virtual void notify_instance_altered(Component.Instance instance) {
+        if (instances.contains(instance.uid))
+            instance_altered(instance);
         else
-            debug("Cannot notify altered event %s in %s: not known", event.to_string(), to_string());
+            debug("Cannot notify altered component %s in %s: not known", instance.to_string(), to_string());
     }
     
     /**
-     * Notify that the { link Component.Event}s have been dropped due to the { link Source} going
+     * Notify that the { link Component.Instance}s have been dropped due to the { link Source} going
      * unavailable.
      */
-    protected virtual void notify_event_dropped(Component.Event event) {
-        if (this.events.unset(event.uid))
-            event_dropped(event);
-        else
-            debug("Cannot notify dropped event %s in %s: not known", event.to_string(), to_string());
+    protected virtual void notify_instance_dropped(Component.Instance instance) {
+        Gee.Collection<Component.Instance> removed_instances;
+        if (remove_instance(instance.uid, out removed_instances)) {
+            foreach (Component.Instance removed_instance in removed_instances)
+                instance_dropped(removed_instance);
+        } else {
+            debug("Cannot notify dropped component %s in %s: not known", instance.to_string(), to_string());
+        }
+    }
+    
+    private bool add_instance(Component.Instance instance) {
+        bool already_exists = instances.get(instance.uid).contains(instance);
+        if (!already_exists)
+            instances.set(instance.uid, instance);
+        
+        return !already_exists;
+    }
+    
+    private bool remove_instance(Component.UID uid, out Gee.Collection<Component.Instance> 
removed_instances) {
+        bool removed = instances.contains(uid);
+        if (removed) {
+            removed_instances = instances.get(uid);
+            instances.remove_all(uid);
+        } else {
+            removed_instances = new Gee.ArrayList<Component.Instance>();
+        }
+        
+        return removed;
     }
     
     /**
@@ -228,24 +253,19 @@ public abstract class CalendarSourceSubscription : BaseObject {
         if (calendar.is_available)
             return;
         
-        foreach (Component.Event event in events.values.to_array())
-            notify_event_dropped(event);
+        // Use to_array() so no iteration troubles when notify_instance_dropped removes it from
+        // the multimap
+        foreach (Component.Instance instance in instances.get_values().to_array())
+            notify_instance_dropped(instance);
     }
     
     /**
-     * Returns an { link Component.Instance} for the { link Component.UID}.
+     * Returns all { link Component.Instance}s for the { link Component.UID}.
      *
      * @returns null if the UID has not been seen.
      */
-    public Component.Instance? for_uid(Component.UID uid) {
-        return events.get(uid);
-    }
-    
-    /**
-     * Returns a read-only Map of all known { link Component.Event}s.
-     */
-    public Gee.Map<Component.UID, Component.Event> get_events() {
-        return events.read_only_view;
+    public Gee.Collection<Component.Instance>? 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 050b3e8..e4faf20 100644
--- a/src/backing/eds/backing-eds-calendar-source-subscription.vala
+++ b/src/backing/eds/backing-eds-calendar-source-subscription.vala
@@ -97,7 +97,7 @@ internal class EdsCalendarSourceSubscription : CalendarSourceSubscription {
             Component.Event? event = Component.Instance.convert(calendar, eds_component.get_icalcomponent())
                 as Component.Event;
             if (event != null)
-                notify_event_discovered(event);
+                notify_instance_discovered(event);
         } catch (Error err) {
             debug("Unable to generate discovered event for %s: %s", to_string(), err.message);
         }
@@ -115,7 +115,7 @@ internal class EdsCalendarSourceSubscription : CalendarSourceSubscription {
             try {
                 Component.Event? event = Component.Instance.convert(calendar, ical_component) as 
Component.Event;
                 if (event != null)
-                    notify_event_added(event);
+                    notify_instance_added(event);
             } catch (Error err) {
                 debug("Unable to generate added event for %s: %s", to_string(), err.message);
             }
@@ -136,13 +136,13 @@ internal class EdsCalendarSourceSubscription : CalendarSourceSubscription {
                 debug("Unable to update event %s: %s", event.to_string(), err.message);
             }
             
-            notify_event_altered(event);
+            notify_instance_altered(event);
         }
     }
     
     private void on_objects_removed(SList<weak E.CalComponentId?> ids) {
         foreach (weak E.CalComponentId id in ids)
-            notify_event_removed(new Component.UID(id.uid));
+            notify_instance_removed(new Component.UID(id.uid));
     }
 }
 
diff --git a/src/backing/eds/backing-eds-calendar-source.vala 
b/src/backing/eds/backing-eds-calendar-source.vala
index fafb524..d4e6ecc 100644
--- a/src/backing/eds/backing-eds-calendar-source.vala
+++ b/src/backing/eds/backing-eds-calendar-source.vala
@@ -62,7 +62,7 @@ internal class EdsCalendarSource : CalendarSource {
         string? uid;
         client.create_object_sync(instance.ical_component, out uid, cancellable);
         
-        return (uid != null && uid[0] != '\0') ? new Component.UID(uid) : null;
+        return !String.is_empty(uid) ? new Component.UID(uid) : null;
     }
     
     public override async void update_component_async(Component.Instance instance,
diff --git a/src/component/component-date-time.vala b/src/component/component-date-time.vala
index 4a23b96..56fcb63 100644
--- a/src/component/component-date-time.vala
+++ b/src/component/component-date-time.vala
@@ -6,7 +6,15 @@
 
 namespace California.Component {
 
-public class DateTime : BaseObject {
+/**
+ * An immutable representation of iCal's DATE and DATE-TIME property, which are often used
+ * interchangeably.
+ *
+ * See [[https://tools.ietf.org/html/rfc5545#section-3.3.4]] and
+ * [[https://tools.ietf.org/html/rfc5545#section-3.3.5]]
+ */
+
+public class DateTime : BaseObject, Gee.Hashable<DateTime>, Gee.Comparable<DateTime> {
     /**
      * The TZID for the iCal component and property kind.
      *
@@ -42,10 +50,21 @@ public class DateTime : BaseObject {
     public iCal.icaltimetype dt;
     
     /**
+     * The iCal property type ("kind") for { link dt}.
+     */
+    public iCal.icalproperty_kind kind;
+    
+    /**
      * Creates a new { link DateTime} for the iCal component of the property kind.
      *
-     * Note that DTSTART_PROPERTY, DTEND_PROPERTY, and DTSTAMP_PROPERTY are the only properties
-     * currently supported.
+     * Note that the only properties currently supported are:
+     * * DTSTART_PROPERTY
+     * * DTEND_PROPERTY
+     * * DTSTAMP_PROPERTY
+     * * RECURRENCEID_PROPERTY
+     *
+     * @throws ComponentError.UNAVAILABLE if not found
+     * @throws ComponentError.INVALID if not a valid DATE or DATE-TIME
      */
     public DateTime(iCal.icalcomponent ical_component, iCal.icalproperty_kind ical_prop_kind)
         throws ComponentError {
@@ -66,6 +85,10 @@ public class DateTime : BaseObject {
                 dt = prop.get_dtend();
             break;
             
+            case iCal.icalproperty_kind.RECURRENCEID_PROPERTY:
+                dt = prop.get_recurrenceid();
+            break;
+            
             default:
                 assert_not_reached();
         }
@@ -79,6 +102,8 @@ public class DateTime : BaseObject {
         unowned iCal.icalparameter? param = prop.get_first_parameter(iCal.icalparameter_kind.TZID_PARAMETER);
         if (param != null)
             zone = new Calendar.OlsonZone(param.get_tzid());
+        
+        kind = ical_prop_kind;
     }
     
     /**
@@ -186,6 +211,21 @@ public class DateTime : BaseObject {
         exact_time_span = null;
     }
     
+    public int compare_to(Component.DateTime other) {
+        return (this != other) ? iCal.icaltime_compare(dt, other.dt) : 0;
+    }
+    
+    public bool equal_to(Component.DateTime other) {
+        return (this != other) ? iCal.icaltime_compare(dt, other.dt) == 0 : true;
+    }
+    
+    public uint hash() {
+        // iCal doesn't supply a hashing function, so here goes
+        iCal.icaltimetype utc = iCal.icaltime_convert_to_zone(dt, iCal.icaltimezone.get_utc_timezone());
+        
+        return Memory.hash(&utc, sizeof(iCal.icaltimetype));
+    }
+    
     public override string to_string() {
         try {
             if (is_date)
diff --git a/src/component/component-event.vala b/src/component/component-event.vala
index 7517437..334227c 100644
--- a/src/component/component-event.vala
+++ b/src/component/component-event.vala
@@ -12,7 +12,7 @@ namespace California.Component {
  * See [[https://tools.ietf.org/html/rfc5545#section-3.6.1]]
  */
 
-public class Event : Instance, Gee.Comparable<Event> {
+public class Event : Instance, Sequenceable, Recurrable, Gee.Comparable<Event> {
     public const string PROP_SUMMARY = "summary";
     public const string PROP_DESCRIPTION = "description";
     public const string PROP_EXACT_TIME_SPAN = "exact-time-span";
@@ -55,6 +55,16 @@ public class Event : Instance, Gee.Comparable<Event> {
     public bool is_all_day { get; private set; }
     
     /**
+     * For { link Recurrable}.
+     */
+    public Component.DateTime? rid { get; set; default = null; }
+    
+    /**
+     * For { link Sequenceable}.
+     */
+    public int sequence { get; set; default = 0; }
+    
+    /**
      * Create an { link Event} { link Component} from an EDS CalComponent object.
      *
      * Throws a BackingError if the E.CalComponent's VTYPE is not VEVENT.
@@ -241,12 +251,66 @@ public class Event : Instance, Gee.Comparable<Event> {
         if (compare != 0)
             return compare;
         
+        // if recurring, go by sequence number, as the UID and RID are the same for all instances
+        if (is_recurring) {
+            compare = sequence - other.sequence;
+            if (compare != 0)
+                return compare;
+        }
+        
         // stabilize with UIDs
         return uid.compare_to(other.uid);
     }
     
+    public override bool equal_to(Component.Instance other) {
+        Component.Event? other_event = other as Component.Event;
+        if (other_event == null)
+            return false;
+        
+        if (this == other_event)
+            return true;
+        
+        if (is_recurring != other_event.is_recurring)
+            return false;
+        
+        if (is_recurring && !rid.equal_to(other_event.rid))
+            return false;
+        
+        if (sequence != other_event.sequence)
+            return false;
+        
+        if (exact_time_span != null && other_event.exact_time_span != null
+            && !exact_time_span.equal_to(other_event.exact_time_span)) {
+            return false;
+        }
+        
+        if (date_span != null && other_event.date_span != null
+            && !date_span.equal_to(other_event.date_span)) {
+            return false;
+        }
+        
+        if (exact_time_span != null
+            && !new Calendar.DateSpan.from_exact_time_span(exact_time_span).equal_to(other_event.date_span)) 
{
+            return false;
+        }
+        
+        if (!date_span.equal_to(new Calendar.DateSpan.from_exact_time_span(other_event.exact_time_span))) {
+            return false;
+        }
+        
+        return base.equal_to(other);
+    }
+    
+    public override uint hash() {
+        return uid.hash() ^ ((rid != null) ? rid.hash() : 0) ^ sequence;
+    }
+    
     public override string to_string() {
-        return "Event \"%s\" (%s)".printf(summary,
+        return "Event %s/rid=%s/%d \"%s\" (%s)".printf(
+            uid.to_string(),
+            (rid != null) ? rid.to_string() : "(no-recurring)",
+            sequence,
+            summary,
             exact_time_span != null ? exact_time_span.to_string() : date_span.to_string());
     }
 }
diff --git a/src/component/component-instance.vala b/src/component/component-instance.vala
index 307af70..6226fce 100644
--- a/src/component/component-instance.vala
+++ b/src/component/component-instance.vala
@@ -9,19 +9,16 @@ namespace California.Component {
 /**
  * A mutable iCalendar component that has a definitive instance within a calendar.
  *
- * By "instance", this means { link Event}s, To-Do's, Journals, and Free/Busy components.  In other
- * words, components which allocate a specific item within a calendar.  Some of thse
- * components may be recurring, in which case any particular instance is merely a generated
- * representation of that recurrance.
+ * By "instance", this means { link Event}s, To-Do's, and Journals, and Free/Busy components.  In
+ *  other words, components which allocate a specific amount of time within a calendar.
  *
  * 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 and RID).  This will update all
- * fields.
+ * iCal component to update an existing one (verified by UID).  This will update all fields.
  *
  * The second is to update the mutable properties themselves, which will then update the underlying
  * iCal component.
  *
- * Alarms are contained within Instance components.  Timezones are handled separately.
+ * Alarms will be contained within Instance components.  Timezones are handled separately.
  *
  * Instance also offers a number of methods to convert iCal structures into internal objects.
  */
@@ -110,6 +107,9 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
         uid = new UID(_ical_component.get_uid());
         
         full_update(_ical_component);
+        
+        // watch for property changes and update ical_component when happens
+        notify.connect(on_notify);
     }
     
     /**
@@ -121,6 +121,8 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
         _ical_component = new iCal.icalcomponent(kind);
         uid = Component.UID.generate();
         _ical_component.set_uid(uid.value);
+        
+        notify.connect(on_notify);
     }
     
     /**
@@ -187,10 +189,49 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
         if (!dt_stamp.is_date)
             dtstamp = dt_stamp.to_exact_time();
         
+        //
+        // Interface hooks
+        //
+        
+        Recurrable? recurrable = this as Recurrable;
+        if (recurrable != null)
+            recurrable.update_from_component(ical_component);
+        
+        Sequenceable? sequenceable = this as Sequenceable;
+        if (sequenceable != null)
+            sequenceable.update_from_component(ical_component);
+        
+        // save own copy of component; no ownership transferrance w/ current bindings
         if (_ical_component != ical_component)
             _ical_component = ical_component.clone();
     }
     
+    private void on_notify(ParamSpec pspec) {
+        // don't worry if in full update, that call is supposed to update properties
+        if (in_full_update)
+            return;
+        
+        bool altered = false;
+        
+        //
+        // Interface hooks
+        //
+        
+        Recurrable? recurrable = this as Recurrable;
+        if (recurrable != null)
+            altered = recurrable.on_notify(pspec) || altered;
+        
+        Sequenceable? sequenceable = this as Sequenceable;
+        if (sequenceable != null)
+            altered = sequenceable.on_notify(pspec) || altered;
+        
+        // Instance currently has no properties of its own which may be updated except dtstamp,
+        // which is a special case
+        
+        if (altered)
+            notify_altered(false);
+    }
+    
     /**
      * Returns an appropriate { link Component} instance for the iCalendar component.
      *
@@ -213,7 +254,7 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     /**
      * Convenience method to convert a { link Calendar.Date} to an iCal DATE.
      */
-    protected static void date_to_ical(Calendar.Date date, iCal.icaltimetype *ical_dt) {
+    internal static void date_to_ical(Calendar.Date date, iCal.icaltimetype *ical_dt) {
         ical_dt->year = date.year.value;
         ical_dt->month = date.month.value;
         ical_dt->day = date.day_of_month.value;
@@ -233,7 +274,7 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
      * of the span.  See the iCal specification for information on how each component should
      * treat the situation.
      */
-    protected static void date_span_to_ical(Calendar.DateSpan date_span, bool dtend_inclusive,
+    internal static void date_span_to_ical(Calendar.DateSpan date_span, bool dtend_inclusive,
         iCal.icaltimetype *ical_dtstart, iCal.icaltimetype *ical_dtend) {
         date_to_ical(date_span.start_date, ical_dtstart);
         date_to_ical(date_span.end_date.adjust(dtend_inclusive ? 0 : 1, Calendar.DateUnit.DAY),
@@ -243,7 +284,7 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     /**
      * Convenience method to convert a { link Calendar.ExactTime} to an iCal DATE-TIME.
      */
-    protected static void exact_time_to_ical(Calendar.ExactTime exact_time, iCal.icaltimetype *ical_dt) {
+    internal static void exact_time_to_ical(Calendar.ExactTime exact_time, iCal.icaltimetype *ical_dt) {
         ical_dt->year = exact_time.year.value;
         ical_dt->month = exact_time.month.value;
         ical_dt->day = exact_time.day_of_month.value;
@@ -261,21 +302,43 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     /**
      * Convenience method to convert a { link Calendar.ExactTimeSpan} to a pair of iCal DATE-TIMEs.
      */
-    protected static void exact_time_span_to_ical(Calendar.ExactTimeSpan exact_time_span,
+    internal static void exact_time_span_to_ical(Calendar.ExactTimeSpan exact_time_span,
         iCal.icaltimetype *ical_dtstart, iCal.icaltimetype *ical_dtend) {
         exact_time_to_ical(exact_time_span.start_exact_time, ical_dtstart);
         exact_time_to_ical(exact_time_span.end_exact_time, ical_dtend);
     }
     
     /**
-     * Equality is defined as { link Component.Instance}s having the same UID (and, when available,
-     * RID), nothing more.
+     * Convenience method to remove all instances of a property from { link ical_component}.
+     *
+     * @returns The number of properties found with the specified kind.
+     */
+    internal int remove_all_properties(iCal.icalproperty_kind kind) {
+        int count = 0;
+        unowned iCal.icalproperty? prop;
+        while ((prop = ical_component.get_first_property(kind)) != null) {
+            ical_component.remove_property(prop);
+            count++;
+        }
+        
+        return count;
+    }
+    
+    /**
+     * Equality is defined as { link Component.Instance}s having the same UID.
+     *
+     * Subclasses should override this and { link hash} if more definite equality is necessary.
      */
-    public bool equal_to(Instance other) {
+    public virtual bool equal_to(Instance other) {
         return (this != other) ? uid.equal_to(other.uid) : true;
     }
     
-    public uint hash() {
+    /**
+     * Hash is calculated using the { link Instance} { link UID}.
+     *
+     * Subclasses should override if they override { link equal_to}.
+     */
+    public virtual uint hash() {
         return uid.hash();
     }
 }
diff --git a/src/component/component-recurrable.vala b/src/component/component-recurrable.vala
new file mode 100644
index 0000000..a8ad3e5
--- /dev/null
+++ b/src/component/component-recurrable.vala
@@ -0,0 +1,67 @@
+/* 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 {
+
+/**
+ * Interface for iCal components which potentially can be configured as recurring within a
+ * calendar.
+ *
+ * iCal defines the components which support recurrances as VEVENT, VTODO, and VJOURNAL.
+ */
+
+public interface Recurrable : Component.Instance, Component.Sequenceable {
+    public const string PROP_RID = "rid";
+    
+    private const iCal.icalproperty_kind KIND_RID = iCal.icalproperty_kind.RECURRENCEID_PROPERTY;
+    
+    /**
+     * The RECURRENCE-ID of a recurring component.
+     *
+     * See [[https://tools.ietf.org/html/rfc5545#section-3.8.4.4]]
+     */
+    public abstract Component.DateTime? rid { get; set; default = null; }
+    
+    /**
+     * Returns true if the { link Recurrable} is in fact a recurring instance.
+     */
+    public bool is_recurring { get { return rid != null; } }
+    
+    // Called from within Component.Instance
+    internal void update_from_component(iCal.icalcomponent ical_component) throws Error {
+        try {
+            rid = new DateTime(ical_component, KIND_RID);
+        } catch (ComponentError comperr) {
+            // ignore if unavailable
+            if (!(comperr is ComponentError.UNAVAILABLE))
+                throw comperr;
+            
+            rid = null;
+        }
+    }
+    
+    // Called from within Component.Instance
+    internal bool on_notify(ParamSpec pspec) {
+        bool altered = true;
+        switch (pspec.name) {
+            case PROP_RID:
+                if (rid == null)
+                    remove_all_properties(KIND_RID);
+                else
+                    ical_component.set_recurrenceid(rid.dt);
+            break;
+            
+            default:
+                altered = false;
+            break;
+        }
+        
+        return altered;
+    }
+}
+
+}
+
diff --git a/src/component/component-sequenceable.vala b/src/component/component-sequenceable.vala
new file mode 100644
index 0000000..c663d57
--- /dev/null
+++ b/src/component/component-sequenceable.vala
@@ -0,0 +1,48 @@
+/* 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 {
+
+/**
+ * Interface for iCal components which track their revisions via a counter.
+ *
+ * iCal defines the components which support sequencing as VEVENT, VTODO, and VJOURNAL.
+ */
+
+public interface Sequenceable : Component.Instance {
+    public const string PROP_SEQUENCE = "sequence";
+    
+    /**
+     * The SEQUENCE of a VEVENT, VTODO, or VJOURNAL.
+     *
+     * See [[https://tools.ietf.org/html/rfc5545#section-3.8.7.4]]
+     */
+    public abstract int sequence { get; set; default = 0; }
+    
+    // Called from within Component.Instance
+    internal void update_from_component(iCal.icalcomponent ical_component) throws Error {
+        sequence = ical_component.get_sequence();
+    }
+    
+    // Called from within Component.Instance
+    internal bool on_notify(ParamSpec pspec) {
+        bool altered = true;
+        switch (pspec.name) {
+            case PROP_SEQUENCE:
+                ical_component.set_sequence(sequence);
+            break;
+            
+            default:
+                altered = false;
+            break;
+        }
+        
+        return altered;
+    }
+}
+
+}
+
diff --git a/src/util/util-memory.vala b/src/util/util-memory.vala
new file mode 100644
index 0000000..93981f6
--- /dev/null
+++ b/src/util/util-memory.vala
@@ -0,0 +1,27 @@
+/* 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.Memory {
+
+/**
+ * A rotating-XOR hash that can be used to hash memory buffers of any size.
+ */
+public uint hash(void *ptr, size_t bytes) {
+    if (bytes == 0)
+        return 0;
+    
+    uint8 *u8 = (uint8 *) ptr;
+    
+    // initialize hash to first byte value and then rotate-XOR from there
+    uint hash = *u8;
+    for (int ctr = 1; ctr < bytes; ctr++)
+        hash = (hash << 4) ^ (hash >> 28) ^ (*u8++);
+    
+    return hash;
+}
+
+}
+
diff --git a/src/view/month/month-controllable.vala b/src/view/month/month-controllable.vala
index 5c7ed5a..b22b2d0 100644
--- a/src/view/month/month-controllable.vala
+++ b/src/view/month/month-controllable.vala
@@ -266,10 +266,10 @@ public class Controllable : Gtk.Grid, View.Controllable {
             Backing.CalendarSourceSubscription subscription = calendar.subscribe_async.end(result);
             subscriptions.add(subscription);
             
-            subscription.event_discovered.connect(on_event_added);
-            subscription.event_added.connect(on_event_added);
-            subscription.event_removed.connect(on_event_removed);
-            subscription.event_dropped.connect(on_event_removed);
+            subscription.instance_discovered.connect(on_instance_added);
+            subscription.instance_added.connect(on_instance_added);
+            subscription.instance_removed.connect(on_instance_removed);
+            subscription.instance_dropped.connect(on_instance_removed);
             
             // this will start signals firing for event changes
             subscription.start();
@@ -278,7 +278,11 @@ public class Controllable : Gtk.Grid, View.Controllable {
         }
     }
     
-    private void on_event_added(Component.Event event) {
+    private void on_instance_added(Component.Instance instance) {
+        Component.Event? event = instance as Component.Event;
+        if (event == null)
+            return;
+        
         // add event to every date it represents
         foreach (Calendar.Date date in event.get_event_date_span()) {
             Cell? cell = date_to_cell.get(date);
@@ -287,7 +291,11 @@ public class Controllable : Gtk.Grid, View.Controllable {
         }
     }
     
-    private void on_event_removed(Component.Event event) {
+    private void on_instance_removed(Component.Instance instance) {
+        Component.Event? event = instance as Component.Event;
+        if (event == null)
+            return;
+        
         foreach (Calendar.Date date in event.get_event_date_span()) {
             Cell? cell = date_to_cell.get(date);
             if (cell != null)


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]