[california/wip/734698-agenda] Deal with events/calendars removing, changing



commit 27fb97cd66c89bd9bc760732ab3267befddecb80
Author: Jim Nelson <jim yorba org>
Date:   Thu Dec 4 19:22:17 2014 -0800

    Deal with events/calendars removing, changing

 src/collection/collection-iterable.vala |   24 +++++++++
 src/component/component-instance.vala   |    5 ++-
 src/rc/view-agenda-date-row.ui          |    2 +-
 src/rc/view-agenda-event-row.ui         |   39 +++++++++++++--
 src/toolkit/toolkit-listbox-model.vala  |   10 ++++
 src/view/agenda/agenda-controller.vala  |   53 ++++++++++++++++-----
 src/view/agenda/agenda-date-row.vala    |   46 +++++++++++++++++-
 src/view/agenda/agenda-event-row.vala   |   79 ++++++++++++++++++++++++++----
 8 files changed, 226 insertions(+), 32 deletions(-)
---
diff --git a/src/collection/collection-iterable.vala b/src/collection/collection-iterable.vala
index cd1afbd..5c26149 100644
--- a/src/collection/collection-iterable.vala
+++ b/src/collection/collection-iterable.vala
@@ -307,6 +307,30 @@ public class Iterable<G> : Object {
     }
     
     /**
+     * Returns true if the { link Iterable} is empty.
+     *
+     * This is more efficient than checking if { link} count is zero when the Iterable is holding
+     * items.
+     *
+     * @see is_nonempty
+     */
+    public bool is_empty() {
+        return !iterator().has_next();
+    }
+    
+    /**
+     * Returns true if the { link Iterable} is non-empty.
+     *
+     * This is more efficient than checking if { link} count is non-zero when the Iterable is
+     * holding items.
+     *
+     * @see is_empty
+     */
+    public bool is_nonempty() {
+        return iterator().has_next();
+    }
+    
+    /**
      * The resulting Gee.Iterable comes with the same caveat that you may only
      * iterate over it once.
      */
diff --git a/src/component/component-instance.vala b/src/component/component-instance.vala
index b65602b..258b6a7 100644
--- a/src/component/component-instance.vala
+++ b/src/component/component-instance.vala
@@ -29,7 +29,6 @@ namespace California.Component {
  */
 
 public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
-    public const string PROP_CALENDAR_SOURCE = "calendar-source";
     public const string PROP_DTSTAMP = "dtstamp";
     public const string PROP_UID = "uid";
     public const string PROP_ICAL_COMPONENT = "ical-component";
@@ -47,6 +46,10 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     /**
      * The { link Backing.CalendarSource} this { link Instance} originated from.
      *
+     * This field is immutable for the lifetime of the { link Instance}.  If an Instance is moved
+     * to another calendar, this instance will be destroyed and a new one reported from the
+     * appropriate { link Backing.CalendarSourceSubscription}.
+     *
      * This will initialize as null if created as a { link blank} Instance.
      */
     public Backing.CalendarSource? calendar_source { get; private set; default = null; }
diff --git a/src/rc/view-agenda-date-row.ui b/src/rc/view-agenda-date-row.ui
index 88fee77..ca1709b 100644
--- a/src/rc/view-agenda-date-row.ui
+++ b/src/rc/view-agenda-date-row.ui
@@ -9,7 +9,7 @@
       <object class="GtkLabel" id="date_label">
         <property name="visible">True</property>
         <property name="can_focus">False</property>
-        <property name="margin_right">8</property>
+        <property name="margin_right">16</property>
         <property name="margin_top">3</property>
         <property name="xalign">1</property>
         <property name="yalign">0</property>
diff --git a/src/rc/view-agenda-event-row.ui b/src/rc/view-agenda-event-row.ui
index a80fc07..e8b0d0c 100644
--- a/src/rc/view-agenda-event-row.ui
+++ b/src/rc/view-agenda-event-row.ui
@@ -14,9 +14,8 @@
           <object class="GtkLabel" id="time_label">
             <property name="visible">True</property>
             <property name="can_focus">False</property>
-            <property name="margin_right">8</property>
+            <property name="margin_right">16</property>
             <property name="xalign">0</property>
-            <property name="yalign">0</property>
             <property name="label">(time)</property>
             <property name="use_markup">True</property>
           </object>
@@ -37,18 +36,48 @@
           <object class="GtkLabel" id="summary_label">
             <property name="visible">True</property>
             <property name="can_focus">False</property>
-            <property name="xalign">0</property>
-            <property name="yalign">0</property>
             <property name="label">(summary)</property>
             <property name="use_markup">True</property>
           </object>
         </child>
       </object>
       <packing>
-        <property name="expand">True</property>
+        <property name="expand">False</property>
         <property name="fill">True</property>
         <property name="position">1</property>
       </packing>
     </child>
+    <child>
+      <object class="GtkImage" id="guests_icon">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="no_show_all">True</property>
+        <property name="tooltip_text" translatable="yes">Event has guests</property>
+        <property name="xpad">4</property>
+        <property name="pixel_size">12</property>
+        <property name="icon_name">system-users-symbolic</property>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkImage" id="recurring_icon">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="no_show_all">True</property>
+        <property name="tooltip_text" translatable="yes">Event is recurring</property>
+        <property name="xpad">4</property>
+        <property name="pixel_size">12</property>
+        <property name="icon_name">rotation-allowed-symbolic</property>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">3</property>
+      </packing>
+    </child>
   </template>
 </interface>
diff --git a/src/toolkit/toolkit-listbox-model.vala b/src/toolkit/toolkit-listbox-model.vala
index 4fb36ae..8595bd6 100644
--- a/src/toolkit/toolkit-listbox-model.vala
+++ b/src/toolkit/toolkit-listbox-model.vala
@@ -255,6 +255,16 @@ public class ListBoxModel<G> : BaseObject {
         }
         
         row.changed();
+        
+        // reset size as filter could have changed contents of list
+        int count = 0;
+        foreach (Gtk.Widget widget in listbox.get_children()) {
+            Gtk.ListBoxRow child = (Gtk.ListBoxRow) widget;
+            if (model_filter == null || model_filter(child.get_data<G>(KEY)))
+                count++;
+        }
+        
+        size = count;
     }
     
     /**
diff --git a/src/view/agenda/agenda-controller.vala b/src/view/agenda/agenda-controller.vala
index 94166c0..c1de5a5 100644
--- a/src/view/agenda/agenda-controller.vala
+++ b/src/view/agenda/agenda-controller.vala
@@ -66,7 +66,7 @@ public class Controller : BaseObject, View.Controllable {
         
         container = new Container(this, listbox);
         current_span = new Calendar.DateSpan(Calendar.System.today,
-            Calendar.System.today.adjust_by(1, Calendar.DateUnit.MONTH));
+            Calendar.System.today.adjust_by(2, Calendar.DateUnit.MONTH));
         listbox_model = new Toolkit.ListBoxModel<Calendar.Date>(listbox, model_presentation);
         
         // Don't prelight the DateRows, as they can't be selected or activated
@@ -109,25 +109,34 @@ public class Controller : BaseObject, View.Controllable {
      * @inheritDoc
      */
     public void unselect_all() {
+        // no notion of selection in Agenda view
     }
     
     /**
      * @inheritDoc
      */
     public Gtk.Widget? get_widget_for_date(Calendar.Date date) {
-        return null;
+        return listbox_model.get_widget_for_item(date);
     }
     
     private Gtk.Widget model_presentation(Calendar.Date date) {
-        return new DateRow(this, date);
+        DateRow date_row = new DateRow(this, date);
+        date_row.empty.connect(on_date_row_empty);
+        
+        return date_row;
+    }
+    
+    private void on_date_row_empty(DateRow date_row) {
+        listbox_model.remove(date_row.date);
     }
     
     private void update_subscriptions() {
         subscriptions = new Backing.CalendarSubscriptionManager(
             current_span.to_exact_time_span(Calendar.Timezone.local));
         
-        subscriptions.instance_added.connect(on_instance_added);
-        subscriptions.instance_altered.connect(on_instance_altered);
+        subscriptions.calendar_added.connect(on_calendar_added);
+        subscriptions.calendar_removed.connect(on_calendar_removed);
+        subscriptions.instance_added.connect(on_instance_added_or_altered);
         subscriptions.instance_removed.connect(on_instance_removed);
         
         subscriptions.start_async.begin();
@@ -141,12 +150,30 @@ public class Controller : BaseObject, View.Controllable {
         is_viewing_today = Calendar.System.today in current_span;
     }
     
-    private void on_instance_added(Component.Instance instance) {
+    private void on_calendar_added(Backing.CalendarSource calendar) {
+        calendar.notify[Backing.Source.PROP_VISIBLE].connect(on_calendar_visibility_changed);
+    }
+    
+    private void on_calendar_removed(Backing.CalendarSource calendar) {
+        calendar.notify[Backing.Source.PROP_VISIBLE].disconnect(on_calendar_visibility_changed);
+    }
+    
+    private void on_calendar_visibility_changed(Object o, ParamSpec pspec) {
+        Backing.CalendarSource calendar = (Backing.CalendarSource) o;
+        
+        foreach (Calendar.Date date in listbox_model.all()) {
+            DateRow date_row = (DateRow) listbox_model.get_widget_for_item(date);
+            date_row.notify_calendar_visibility_changed(calendar);
+        }
+    }
+    
+    private void on_instance_added_or_altered(Component.Instance instance) {
         Component.Event? event = instance as Component.Event;
         if (event == null)
             return;
         
         foreach (Calendar.Date date in event.get_event_date_span(Calendar.Timezone.local)) {
+            // Add dates on-demand; not all dates are listed in Agenda view
             if (!listbox_model.contains(date))
                 listbox_model.add(date);
             
@@ -155,16 +182,18 @@ public class Controller : BaseObject, View.Controllable {
         }
     }
     
-    private void on_instance_altered(Component.Instance instance) {
-        Component.Event? event = instance as Component.Event;
-        if (event == null)
-            return;
-    }
-    
     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(Calendar.Timezone.local)) {
+            if (!listbox_model.contains(date))
+                continue;
+            
+            DateRow date_row = (DateRow) listbox_model.get_widget_for_item(date);
+            date_row.remove_event(event);
+        }
     }
     
     public override string to_string() {
diff --git a/src/view/agenda/agenda-date-row.vala b/src/view/agenda/agenda-date-row.vala
index 7d8ad5a..cdc73b4 100644
--- a/src/view/agenda/agenda-date-row.vala
+++ b/src/view/agenda/agenda-date-row.vala
@@ -15,6 +15,8 @@ private class DateRow : Gtk.Box {
     
     public Calendar.Date date { get; private set; }
     
+    public int size { get { return listbox_model.size; } }
+    
     [GtkChild]
     private Gtk.Label date_label;
     
@@ -24,11 +26,15 @@ private class DateRow : Gtk.Box {
     private unowned Controller owner;
     private Toolkit.ListBoxModel<Component.Event> listbox_model;
     
+    public signal void empty();
+    
     public DateRow(Controller owner, Calendar.Date date) {
         this.owner = owner;
         this.date = date;
         
-        listbox_model = new Toolkit.ListBoxModel<Component.Event>(event_listbox, model_presentation);
+        listbox_model = new Toolkit.ListBoxModel<Component.Event>(event_listbox, model_presentation,
+            model_filter);
+        listbox_model.notify[Toolkit.ListBoxModel.PROP_SIZE].connect(on_listbox_model_size_changed);
         
         // Don't prelight the DateRows, as they can't be selected or activated
         listbox_model.row_added.connect((row, item) => {
@@ -49,13 +55,49 @@ private class DateRow : Gtk.Box {
         date_label_size_group = null;
     }
     
+    private void on_listbox_model_size_changed() {
+        if (listbox_model.size == 0)
+            empty();
+    }
+    
     public void add_event(Component.Event event) {
-        listbox_model.add(event);
+        if (!listbox_model.add(event))
+            return;
+        
+        // watch for date changes, which affect if the event is represented here
+        event.notify[Component.Event.PROP_DATE_SPAN].connect(on_date_changed);
+        event.notify[Component.Event.PROP_EXACT_TIME_SPAN].connect(on_date_changed);
+    }
+    
+    public void remove_event(Component.Event event) {
+        if (!listbox_model.remove(event))
+            return;
+        
+        event.notify[Component.Event.PROP_DATE_SPAN].disconnect(on_date_changed);
+        event.notify[Component.Event.PROP_EXACT_TIME_SPAN].disconnect(on_date_changed);
+    }
+    
+    private void on_date_changed(Object o, ParamSpec pspec) {
+        Component.Event event = (Component.Event) o;
+        
+        if (!(date in event.get_event_date_span(Calendar.Timezone.local)))
+            remove_event(event);
+    }
+    
+    public void notify_calendar_visibility_changed(Backing.CalendarSource calendar_source) {
+        foreach (Component.Event event in listbox_model.all()) {
+            if (event.calendar_source == calendar_source)
+                listbox_model.mutated(event);
+        }
     }
     
     private Gtk.Widget model_presentation(Component.Event event) {
         return new EventRow(owner, event);
     }
+    
+    private bool model_filter(Component.Event event) {
+        return event.calendar_source.visible;
+    }
 }
 
 }
diff --git a/src/view/agenda/agenda-event-row.vala b/src/view/agenda/agenda-event-row.vala
index 2dc33ca..6580510 100644
--- a/src/view/agenda/agenda-event-row.vala
+++ b/src/view/agenda/agenda-event-row.vala
@@ -10,8 +10,6 @@ namespace California.View.Agenda {
 private class EventRow : Gtk.Box {
     private const Calendar.WallTime.PrettyFlag TIME_PRETTY_FLAGS = Calendar.WallTime.PrettyFlag.NONE;
     
-    private const string COLOR_PRINTF = "<span color=\"%s\">%s</span>";
-    
     private static Gtk.SizeGroup time_label_size_group;
     
     public new Component.Event event { get; private set; }
@@ -28,6 +26,12 @@ private class EventRow : Gtk.Box {
     [GtkChild]
     private Gtk.Label summary_label;
     
+    [GtkChild]
+    private Gtk.Image guests_icon;
+    
+    [GtkChild]
+    private Gtk.Image recurring_icon;
+    
     private Controller owner;
     private Toolkit.ButtonConnector button_connector = new Toolkit.ButtonConnector();
     private Toolkit.MotionConnector motion_connector = new Toolkit.MotionConnector();
@@ -46,13 +50,48 @@ private class EventRow : Gtk.Box {
         motion_connector.connect_to(summary_eventbox);
         
         button_connector.clicked.connect(on_event_clicked);
+        button_connector.double_clicked.connect(on_event_double_clicked);
         motion_connector.entered.connect(on_event_entered_exited);
         motion_connector.exited.connect(on_event_entered_exited);
         motion_connector.motion.connect(on_event_motion);
         
+        // watch for changes to the event
+        event.notify[Component.Event.PROP_SUMMARY].connect(update_ui);
+        event.notify[Component.Event.PROP_DATE_SPAN].connect(update_ui);
+        event.notify[Component.Event.PROP_EXACT_TIME_SPAN].connect(update_ui);
+        event.notify[Component.Event.PROP_LOCATION].connect(update_ui);
+        event.notify[Component.Instance.PROP_ATTENDEES].connect(update_ui);
+        event.notify[Component.Instance.PROP_ORGANIZERS].connect(update_ui);
+        event.notify[Component.Instance.PROP_RRULE].connect(update_ui);
+        
+        // watch for changes to the calendar (which is immutable for the lifetime of the Event
+        // instances)
+        event.calendar_source.notify[Backing.Source.PROP_COLOR].connect(update_ui);
+        
+        // .. date formatting changes
+        Calendar.System.instance.is_24hr_changed.connect(update_ui);
+        Calendar.System.instance.zone_changed.connect(update_ui);
+        Calendar.System.instance.timezone_changed.connect(update_ui);
+        
         update_ui();
     }
     
+    ~EventRow() {
+        event.notify[Component.Event.PROP_SUMMARY].disconnect(update_ui);
+        event.notify[Component.Event.PROP_DATE_SPAN].disconnect(update_ui);
+        event.notify[Component.Event.PROP_EXACT_TIME_SPAN].disconnect(update_ui);
+        event.notify[Component.Event.PROP_LOCATION].disconnect(update_ui);
+        event.notify[Component.Instance.PROP_ATTENDEES].disconnect(update_ui);
+        event.notify[Component.Instance.PROP_ORGANIZERS].disconnect(update_ui);
+        event.notify[Component.Instance.PROP_RRULE].disconnect(update_ui);
+        
+        event.calendar_source.notify[Backing.Source.PROP_COLOR].disconnect(update_ui);
+        
+        Calendar.System.instance.is_24hr_changed.disconnect(update_ui);
+        Calendar.System.instance.zone_changed.disconnect(update_ui);
+        Calendar.System.instance.timezone_changed.disconnect(update_ui);
+    }
+    
     internal static void init() {
         time_label_size_group = new Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL);
     }
@@ -62,26 +101,44 @@ private class EventRow : Gtk.Box {
     }
     
     private void update_ui() {
-        summary_label.label = COLOR_PRINTF.printf(event.calendar_source.color, event.summary);
-        
-        string label;
         if (event.is_all_day) {
-            label = _("All day");
+            time_label.label = _("All day");
         } else {
+            Calendar.ExactTimeSpan time_span = event.exact_time_span.to_timezone(Calendar.Timezone.local);
+            
             // hex value is an endash
-            label = "%s &#x2013; %s".printf(
-                event.exact_time_span.start_exact_time.to_wall_time().to_pretty_string(TIME_PRETTY_FLAGS),
-                event.exact_time_span.end_exact_time.to_wall_time().to_pretty_string(TIME_PRETTY_FLAGS)
+            time_label.label = "%s &#x2013; %s".printf(
+                time_span.start_exact_time.to_wall_time().to_pretty_string(TIME_PRETTY_FLAGS),
+                time_span.end_exact_time.to_wall_time().to_pretty_string(TIME_PRETTY_FLAGS)
             );
         }
         
-        time_label.label = COLOR_PRINTF.printf(event.calendar_source.color, label);
+        if (!String.is_empty(event.location)) {
+            // hex value is an endash
+            summary_label.label = "<span color=\"%s\">%s</span> &#x2013; %s".printf(
+                event.calendar_source.color, event.summary, event.location);
+        } else {
+            summary_label.label = "<span color=\"%s\">%s</span>".printf(
+                event.calendar_source.color, event.summary);
+        }
+        
+        // only show guests icon if attendees include someone not an organizer
+        guests_icon.visible = traverse<Component.Person>(event.attendees)
+            .filter(person => !event.organizers.contains(person))
+            .is_nonempty();
+        recurring_icon.visible = event.rrule != null;
     }
     
     private bool on_event_clicked(Toolkit.ButtonEvent details) {
         owner.request_display_event(event, details.widget, details.press_point);
         
-        return Toolkit.PROPAGATE;
+        return Toolkit.STOP;
+    }
+    
+    private bool on_event_double_clicked(Toolkit.ButtonEvent details) {
+        owner.request_edit_event(event, details.widget, details.press_point);
+        
+        return Toolkit.STOP;
     }
     
     private void on_event_entered_exited(Toolkit.MotionEvent details) {


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