[california] Display recurring events: Closes bgo#725788



commit db84572eed0a5a3ec2141eb8ecd4cb47975357e2
Author: Jim Nelson <jim yorba org>
Date:   Fri Mar 7 19:46:55 2014 -0800

    Display recurring events: Closes bgo#725788
    
    Recurring events are now handled properly internally, although
    creating/updating/removing will have to wait.  Thus, an instance
    of a recurring event can only be examined now, cannot be updated.
    
    This also fixes a bug where the month view only displayed events
    within the month but not on days outside of the month that were
    in view.

 src/Makefile.am                                    |    1 +
 .../backing-calendar-source-subscription.vala      |  152 +++++++++++---------
 .../backing-eds-calendar-source-subscription.vala  |    8 +-
 src/backing/eds/backing-eds-calendar-source.vala   |    2 +-
 src/calendar/calendar-span.vala                    |    7 +
 src/component/component-date-time.vala             |   46 ++++++-
 src/component/component-event.vala                 |   56 +++++++-
 src/component/component-instance.vala              |  110 +++++++++++++--
 src/host/host-show-event.vala                      |   15 ++
 src/util/util-memory.vala                          |   27 ++++
 src/view/month/month-controllable.vala             |   43 ++++--
 11 files changed, 369 insertions(+), 98 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index e8942d2..fd2c362 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -67,6 +67,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/calendar/calendar-span.vala b/src/calendar/calendar-span.vala
index 187bd88..416ecad 100644
--- a/src/calendar/calendar-span.vala
+++ b/src/calendar/calendar-span.vala
@@ -66,6 +66,13 @@ public interface Span<G> : BaseObject, Collection.SimpleIterable<G> {
     }
     
     /**
+     * Converts the { link Span} into a { link DateSpan}.
+     */
+    public DateSpan to_date_span() {
+        return new DateSpan(start_date, end_date);
+    }
+    
+    /**
      * true if the { link Span} contains the specified { link Date}.
      *
      * This is named to conform to Vala's rule for automatic syntax support.  This allows for the
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..3aea5fd 100644
--- a/src/component/component-event.vala
+++ b/src/component/component-event.vala
@@ -241,12 +241,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..f9769d7 100644
--- a/src/component/component-instance.vala
+++ b/src/component/component-instance.vala
@@ -9,19 +9,17 @@ 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 Journal components.  In other words,
+ * 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.)
  *
  * 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.
  */
@@ -31,6 +29,8 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     public const string PROP_DTSTAMP = "dtstamp";
     public const string PROP_UID = "uid";
     public const string PROP_ICAL_COMPONENT = "ical-component";
+    public const string PROP_RID = "rid";
+    public const string PROP_SEQUENCE = "sequence";
     
     protected const string PROP_IN_FULL_UPDATE = "in-full-update";
     
@@ -61,6 +61,27 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     public UID uid { get; private set; }
     
     /**
+     * The RECURRENCE-ID of a recurring component.
+     *
+     * See [[https://tools.ietf.org/html/rfc5545#section-3.8.4.4]]
+     */
+    public Component.DateTime? rid { get; set; default = null; }
+    
+    /**
+     * Returns true if the { link Recurrable} is in fact a recurring instance.
+     *
+     * @see rid
+     */
+    public bool is_recurring { get { return rid != null; } }
+    
+    /**
+     * The SEQUENCE of a VEVENT, VTODO, or VJOURNAL.
+     *
+     * See [[https://tools.ietf.org/html/rfc5545#section-3.8.7.4]]
+     */
+    public int sequence { get; set; default = 0; }
+    
+    /**
      * The iCal component being represented by this { link Instance}.
      */
     private iCal.icalcomponent _ical_component;
@@ -110,6 +131,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 +145,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 +213,50 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
         if (!dt_stamp.is_date)
             dtstamp = dt_stamp.to_exact_time();
         
+        try {
+            rid = new DateTime(ical_component, iCal.icalproperty_kind.RECURRENCEID_PROPERTY);
+        } catch (ComponentError comperr) {
+            // ignore if unavailable
+            if (!(comperr is ComponentError.UNAVAILABLE))
+                throw comperr;
+            
+            rid = null;
+        }
+        
+        sequence = ical_component.get_sequence();
+        
+        // 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 = true;
+        switch (pspec.name) {
+            case PROP_RID:
+                if (rid == null)
+                    remove_all_properties(iCal.icalproperty_kind.RECURRENCEID_PROPERTY);
+                else
+                    ical_component.set_recurrenceid(rid.dt);
+            break;
+            
+            case PROP_SEQUENCE:
+                ical_component.set_sequence(sequence);
+            break;
+            
+            default:
+                altered = false;
+            break;
+        }
+        
+        if (altered)
+            notify_altered(false);
+    }
+    
     /**
      * Returns an appropriate { link Component} instance for the iCalendar component.
      *
@@ -268,14 +334,36 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     }
     
     /**
-     * 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.
      */
-    public bool equal_to(Instance other) {
+    protected 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 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/host/host-show-event.vala b/src/host/host-show-event.vala
index afb5367..b55c0ac 100644
--- a/src/host/host-show-event.vala
+++ b/src/host/host-show-event.vala
@@ -11,6 +11,12 @@ public class ShowEvent : Gtk.Grid {
     [GtkChild]
     private Gtk.Label text_label;
     
+    [GtkChild]
+    private Gtk.Button update_button;
+    
+    [GtkChild]
+    private Gtk.Button remove_button;
+    
     private new Component.Event event;
     
     public signal void remove_event(Component.Event event);
@@ -83,6 +89,15 @@ public class ShowEvent : Gtk.Grid {
         add_lf_lf(builder).append_printf("<small>%s</small>", Markup.escape_text(span));
         
         text_label.label = builder.str;
+        
+        // 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 visible = !event.is_recurring;
+        update_button.visible = visible;
+        update_button.no_show_all = !visible;
+        remove_button.visible = visible;
+        remove_button.no_show_all = !visible;
     }
     
     // Adds two linefeeds if there's existing text
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..a1a0e74 100644
--- a/src/view/month/month-controllable.vala
+++ b/src/view/month/month-controllable.vala
@@ -37,7 +37,7 @@ public class Controllable : Gtk.Grid, View.Controllable {
     /**
      * @inheritDoc
      */
-    public Calendar.FirstOfWeek first_of_week { get; set; }
+    public Calendar.FirstOfWeek first_of_week { get; set; default = Calendar.FirstOfWeek.SUNDAY; }
     
     /**
      * Show days outside the current month.
@@ -45,6 +45,11 @@ public class Controllable : Gtk.Grid, View.Controllable {
     public bool show_outside_month { get; set; default = true; }
     
     /**
+     * The span of dates being displayed.
+     */
+    public Calendar.DateSpan window { get; private set; }
+    
+    /**
      * @inheritDoc
      */
     public string current_label { get; protected set; }
@@ -109,7 +114,6 @@ public class Controllable : Gtk.Grid, View.Controllable {
         
         // update now that signal handlers are in place
         month_of_year = Calendar.System.today.month_of_year();
-        first_of_week = Calendar.FirstOfWeek.SUNDAY;
     }
     
     /**
@@ -215,6 +219,9 @@ public class Controllable : Gtk.Grid, View.Controllable {
         int row = 0;
         foreach (Calendar.Week week in span)
             update_week(row++, week);
+        
+        // update the window being displayed
+        window = span.to_date_span();
     }
     
     private void update_first_of_week() {
@@ -227,6 +234,7 @@ public class Controllable : Gtk.Grid, View.Controllable {
         
         // requires updating all the cells as well, since all dates have to be shifted
         update_cells();
+        update_subscription();
     }
     
     private void on_month_of_year_changed() {
@@ -244,9 +252,12 @@ public class Controllable : Gtk.Grid, View.Controllable {
         }
         
         update_cells();
-        
-        // generate new ExactTimeSpan window for all calendar subscriptions
-        Calendar.ExactTimeSpan window = new Calendar.ExactTimeSpan.from_date_span(month_of_year,
+        update_subscription();
+    }
+    
+    private void update_subscription() {
+        // convert DateSpan window into an ExactTimeSpan, which is what the subscription wants
+        Calendar.ExactTimeSpan time_window = new Calendar.ExactTimeSpan.from_date_span(window,
             Calendar.Timezone.local);
         
         // clear current subscriptions and generate new subscriptions for new window
@@ -254,7 +265,7 @@ public class Controllable : Gtk.Grid, View.Controllable {
         foreach (Backing.Store store in Backing.Manager.instance.get_stores()) {
             foreach (Backing.Source source in store.get_sources_of_type<Backing.CalendarSource>()) {
                 Backing.CalendarSource calendar = (Backing.CalendarSource) source;
-                calendar.subscribe_async.begin(window, null, on_subscribed);
+                calendar.subscribe_async.begin(time_window, null, on_subscribed);
             }
         }
     }
@@ -266,10 +277,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 +289,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 +302,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]