[california] Upcoming events / Agenda view: Bug #734698



commit 6635b308a6d295375ef4ceb61828d2af8f67c8b3
Author: Jim Nelson <jim yorba org>
Date:   Mon Dec 8 15:11:53 2014 -0800

    Upcoming events / Agenda view: Bug #734698
    
    Basic Agenda view functionality, with ability to change the starting
    date one day at a time and expand the amount of time being displayed
    one month at a time.

 po/POTFILES.in                                     |    6 +
 po/POTFILES.skip                                   |    3 +
 src/Makefile.am                                    |    9 +
 .../backing-calendar-subscription-manager.vala     |   60 ++++-
 src/calendar/calendar-date.vala                    |   24 ++
 src/calendar/calendar-exact-time-span.vala         |   19 ++
 src/calendar/calendar-span.vala                    |   52 ++++
 src/calendar/calendar.vala                         |    6 +-
 src/california-resources.xml                       |    9 +
 src/collection/collection-iterable.vala            |   24 ++
 src/component/component-event.vala                 |    1 -
 src/component/component-instance.vala              |    5 +-
 src/host/host-main-window.vala                     |    3 +
 src/rc/view-agenda-date-row.ui                     |   40 +++
 src/rc/view-agenda-event-row.ui                    |   84 ++++++
 src/rc/view-agenda-load-more-row.ui                |   42 +++
 src/toolkit/toolkit-listbox-model.vala             |   33 ++
 src/toolkit/toolkit.vala                           |   15 +
 src/view/agenda/agenda-controller.vala             |  309 ++++++++++++++++++++
 src/view/agenda/agenda-date-row.vala               |  115 ++++++++
 src/view/agenda/agenda-event-row.vala              |  160 ++++++++++
 src/view/agenda/agenda-load-more-row.vala          |   48 +++
 src/view/agenda/agenda.vala                        |   35 +++
 src/view/month/month-controller.vala               |    5 -
 src/view/view.vala                                 |    2 +
 25 files changed, 1098 insertions(+), 11 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index d4bdc75..15a5e19 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -26,6 +26,9 @@ src/host/host-quick-create-event.vala
 src/host/host-show-event.vala
 src/manager/manager-calendar-list-item.vala
 src/manager/manager-remove-calendar.vala
+src/view/agenda/agenda-controller.vala
+src/view/agenda/agenda-event-row.vala
+src/view/agenda/agenda-load-more-row.vala
 src/view/month/month-controller.vala
 src/view/week/week-controller.vala
 [type: gettext/glade]src/rc/activator-generic-subscribe.ui
@@ -47,4 +50,7 @@ src/view/week/week-controller.vala
 [type: gettext/glade]src/rc/manager-calendar-list.ui
 [type: gettext/glade]src/rc/manager-calendar-list-item.ui
 [type: gettext/glade]src/rc/manager-remove-calendar.ui
+[type: gettext/glade]src/rc/view-agenda-date-row.ui
+[type: gettext/glade]src/rc/view-agenda-event-row.ui
+[type: gettext/glade]src/rc/view-agenda-load-more-row.ui
 [type: gettext/glade]src/rc/window-menu.interface
diff --git a/po/POTFILES.skip b/po/POTFILES.skip
index 05f7413..8afa4c4 100644
--- a/po/POTFILES.skip
+++ b/po/POTFILES.skip
@@ -24,6 +24,9 @@ src/host/host-quick-create-event.c
 src/host/host-show-event.c
 src/manager/manager-calendar-list-item.c
 src/manager/manager-remove-calendar.c
+src/view/agenda/agenda-controller.c
+src/view/agenda/agenda-event-row.c
+src/view/agenda/agenda-load-more-row.c
 src/view/month/month-controller.c
 src/view/week/week-controller.c
 
diff --git a/src/Makefile.am b/src/Makefile.am
index f91c67c..070b25c 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -170,6 +170,12 @@ california_VALASOURCES = \
        view/view-controllable.vala \
        view/view-palette.vala \
        \
+       view/agenda/agenda.vala \
+       view/agenda/agenda-controller.vala \
+       view/agenda/agenda-date-row.vala \
+       view/agenda/agenda-event-row.vala \
+       view/agenda/agenda-load-more-row.vala \
+       \
        view/common/common.vala \
        view/common/common-events-cell.vala \
        view/common/common-instance-container.vala \
@@ -214,6 +220,9 @@ california_RC = \
        rc/manager-calendar-list.ui \
        rc/manager-calendar-list-item.ui \
        rc/manager-remove-calendar.ui \
+       rc/view-agenda-date-row.ui \
+       rc/view-agenda-event-row.ui \
+       rc/view-agenda-load-more-row.ui \
        rc/window-menu.interface \
        $(NULL)
 
diff --git a/src/backing/backing-calendar-subscription-manager.vala 
b/src/backing/backing-calendar-subscription-manager.vala
index 71da1b4..68fa314 100644
--- a/src/backing/backing-calendar-subscription-manager.vala
+++ b/src/backing/backing-calendar-subscription-manager.vala
@@ -115,6 +115,55 @@ public class CalendarSubscriptionManager : BaseObject {
         }
     }
     
+    /**
+     * Expand the { link CalendarSubscriptionManager}'s { link window} of time.
+     *
+     * expand_window() will increase the contiguous span of time being monitored for changes by the
+     * subscription manager.  There is no provision for managing multiple ''fragments'' of time,
+     * only expanding the window.
+     *
+     * expand_window() should ''not'' be called until { link start_async} has completed.  Results
+     * are unguaranteed if called while start_async() is executing.
+     *
+     * If expanded_time is within the current window, nothing happens.
+     *
+     * TODO: Currently the subscription manager will expand the range by creating a new
+     * { link CalendarSubscription} for the new dates.  This can be inefficient when dealing with
+     * lots of small ranges.  A better solution would be to create wider ranges and filter out
+     * events outside of the specified window.
+     */
+    public async void expand_window_async(Calendar.ExactTime expanded_time) {
+        if (expanded_time in window)
+            return;
+        
+        // and create a new subscription window to cover the new span of time without overlapping
+        // existing subscription(s)
+        Calendar.ExactTimeSpan subscription_window;
+        if (expanded_time.compare_to(window.start_exact_time) < 0) {
+            subscription_window = new Calendar.ExactTimeSpan(
+                expanded_time,
+                window.start_exact_time.adjust_time(-1, Calendar.TimeUnit.SECOND)
+            );
+        } else {
+            assert(expanded_time.compare_to(window.end_exact_time) > 0);
+            
+            subscription_window = new Calendar.ExactTimeSpan(
+                window.end_exact_time.adjust_time(1, Calendar.TimeUnit.SECOND),
+                expanded_time
+            );
+        }
+        
+        // expand the current window ... do this before adding subscriptions so if new calendars
+        // are reported during async calls, they use the full expanded window
+        window = window.expand(expanded_time);
+        
+        // create new subscriptions for the expanded span only
+        foreach (Backing.Store store in Backing.Manager.instance.get_stores()) {
+            foreach (Backing.CalendarSource calendar in store.get_sources_of_type<Backing.CalendarSource>())
+                yield add_subscription_async(calendar, subscription_window, null);
+        }
+    }
+    
     private void on_source_added(Backing.Source source) {
         Backing.CalendarSource? calendar = source as Backing.CalendarSource;
         if (calendar != null)
@@ -125,12 +174,17 @@ public class CalendarSubscriptionManager : BaseObject {
         // report calendar as added to subscription
         calendar_added(calendar);
         
-        // start generating instances on this calendar
+        // add a subscription for the new calendar with existing window
+        yield add_subscription_async(calendar, window, cancellable);
+    }
+    
+    private async void add_subscription_async(Backing.CalendarSource calendar,
+        Calendar.ExactTimeSpan subscription_window, Cancellable? cancellable) {
         try {
             // Since this might be called after the dtor has finished (cancelling the operation), don't
             // touch the "this" ref unless the Error is shown not to be a cancellation
-            Backing.CalendarSourceSubscription subscription = yield calendar.subscribe_async(window,
-                cancellable);
+            Backing.CalendarSourceSubscription subscription = yield calendar.subscribe_async(
+                subscription_window, cancellable);
             
             // okay to use "this" ref
             subscriptions.add(subscription);
diff --git a/src/calendar/calendar-date.vala b/src/calendar/calendar-date.vala
index 1bf41ce..4ba9e43 100644
--- a/src/calendar/calendar-date.vala
+++ b/src/calendar/calendar-date.vala
@@ -62,6 +62,15 @@ public class Date : Unit<Date>, Gee.Comparable<Date>, Gee.Hashable<Date> {
         NO_DAY_OF_WEEK
     }
     
+    /**
+     * The practical earliest representable { link Date} in time.
+     */
+    public static Date earliest { get; private set; }
+    
+    /**
+     * The practical latest representable { link Date} in time.
+     */
+    public static Date latest { get; private set; }
     
     /**
      * @inheritDoc
@@ -145,6 +154,21 @@ public class Date : Unit<Date>, Gee.Comparable<Date>, Gee.Hashable<Date> {
         year = new Year.from_gdate(gdate);
     }
     
+    internal static void init() throws CalendarError {
+        GLib.Date earliest_gdate = GLib.Date();
+        earliest_gdate.set_julian(1);
+        earliest = new Date.from_gdate(earliest_gdate);
+        
+        // GLib.Date.set_julian(uint.MAX) causes strange assertions inside of GLib, so just jimmying
+        // together a date far in the future for now
+        latest = new Date(DayOfMonth.for(31), Month.DEC, new Year(100000));
+    }
+    
+    internal static void terminate() {
+        earliest = null;
+        latest = null;
+    }
+    
     /**
      * Returns the { link Week} the { link Date} falls in.
      */
diff --git a/src/calendar/calendar-exact-time-span.vala b/src/calendar/calendar-exact-time-span.vala
index 4db2ca1..6e4a0b2 100644
--- a/src/calendar/calendar-exact-time-span.vala
+++ b/src/calendar/calendar-exact-time-span.vala
@@ -140,6 +140,25 @@ public class ExactTimeSpan : BaseObject, Gee.Comparable<ExactTimeSpan>, Gee.Hash
     }
     
     /**
+     * Returns an { link ExactTimeSpan} expanded to include the supplied { link ExactTime}.
+     *
+     * If the expanded_time is within this ExactTimeSpan, this object is returned.
+     */
+    public ExactTimeSpan expand(ExactTime expanded_time) {
+        if (contains(expanded_time))
+            return this;
+        
+        // if supplied time before start of span, that becomes the new start time
+        if (expanded_time.compare_to(start_exact_time) < 0)
+            return new ExactTimeSpan(expanded_time, end_exact_time);
+        
+        // prior tests guarantee supplied time is after end of this span
+        assert(expanded_time.compare_to(end_exact_time) > 0);
+        
+        return new ExactTimeSpan(start_exact_time, expanded_time);
+    }
+    
+    /**
      * Returns a prettified string describing the { link Event}'s time span in as concise and
      * economical manner possible.
      *
diff --git a/src/calendar/calendar-span.vala b/src/calendar/calendar-span.vala
index 2f7eaeb..e5f2355 100644
--- a/src/calendar/calendar-span.vala
+++ b/src/calendar/calendar-span.vala
@@ -180,6 +180,58 @@ public abstract class Span : BaseObject {
     }
     
     /**
+     * Returns a { link DateSpan} that covers the time of this { link Span} and the supplied
+     * { link Date}.
+     *
+     * If the Date is within the existing Span, a DateSpan for this Span is returned, i.e. this
+     * is just like calling { link to_date_span}.
+     */
+    public DateSpan expand(Calendar.Date expansion) {
+        Date new_start = (expansion.compare_to(start_date) < 0) ? expansion : start_date;
+        Date new_end = (expansion.compare_to(end_date) > 0) ? expansion : end_date;
+        
+        return new DateSpan(new_start, new_end);
+    }
+    
+    /**
+     * Returns a { link DateSpan} that represents this { link Span} with the { link start_date}
+     * set to the supplied { link Date}.
+     *
+     * If the new start_date is the same or later than the { link end_date}, a one-day Span is
+     * returned that matches the supplied Date.
+     *
+     * If the new start date is outside the range of this Span, a DateSpan for this Span is
+     * returned, i.e. this is just like calling { link to_date_span}.
+     *
+     * @see reduce_from_end
+     */
+    public DateSpan reduce_from_start(Calendar.Date new_start_date) {
+        if (!has_date(new_start_date))
+            return to_date_span();
+        
+        return new DateSpan(new_start_date, end_date);
+    }
+    
+    /**
+     * Returns a { link DateSpan} that represents this { link Span} with the { link end_date}
+     * set to the supplied { link Date}.
+     *
+     * If the new end_date is the same or earlier than the { link start_date}, a one-day Span is
+     * returned that matches the supplied Date.
+     *
+     * If the new end date is outside the range of this Span, a DateSpan for this Span is
+     * returned, i.e. this is just like calling { link to_date_span}.
+     *
+     * @see reduce_from_start
+     */
+    public DateSpan reduce_from_end(Calendar.Date new_end_date) {
+        if (!has_date(new_end_date))
+            return to_date_span();
+        
+        return new DateSpan(start_date, new_end_date);
+    }
+    
+    /**
      * True if the { link Span} contains the specified { link Date}.
      */
     public bool has_date(Date date) {
diff --git a/src/calendar/calendar.vala b/src/calendar/calendar.vala
index 75cc426..09f7a83 100644
--- a/src/calendar/calendar.vala
+++ b/src/calendar/calendar.vala
@@ -233,13 +233,15 @@ public void init() throws Error {
     // This init() throws an IOError, so perform before others to prevent unnecessary unwinding
     System.preinit();
     
-    // internal initialization
     Collection.init();
+    
+    // internal initialization
     OlsonZone.init();
     DayOfWeek.init();
     DayOfMonth.init();
     Month.init();
     WallTime.init();
+    Date.init();
     System.init();
     Timezone.init();
 }
@@ -250,11 +252,13 @@ public void terminate() {
     
     Timezone.terminate();
     System.terminate();
+    Date.terminate();
     WallTime.terminate();
     Month.terminate();
     DayOfMonth.terminate();
     DayOfWeek.terminate();
     OlsonZone.terminate();
+    
     Collection.terminate();
 }
 
diff --git a/src/california-resources.xml b/src/california-resources.xml
index 3f09836..707ebd1 100644
--- a/src/california-resources.xml
+++ b/src/california-resources.xml
@@ -58,6 +58,15 @@
         <file compressed="true">rc/manager-remove-calendar.ui</file>
     </gresource>
     <gresource prefix="/org/yorba/california">
+        <file compressed="false">rc/view-agenda-date-row.ui</file>
+    </gresource>
+    <gresource prefix="/org/yorba/california">
+        <file compressed="false">rc/view-agenda-event-row.ui</file>
+    </gresource>
+    <gresource prefix="/org/yorba/california">
+        <file compressed="true">rc/view-agenda-load-more-row.ui</file>
+    </gresource>
+    <gresource prefix="/org/yorba/california">
         <file compressed="true">rc/window-menu.interface</file>
     </gresource>
 </gresources>
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-event.vala b/src/component/component-event.vala
index 3f0734e..c21ee7c 100644
--- a/src/component/component-event.vala
+++ b/src/component/component-event.vala
@@ -274,7 +274,6 @@ public class Event : Instance, Gee.Comparable<Event> {
      *
      * This will return a DateSpan whether the Event is a DATE or DATE-TIME VEVENT.
      */
-    // TODO: Make date_span/exact_time_span a separate object
     public Calendar.DateSpan get_event_date_span(Calendar.Timezone? tz) {
         if (date_span != null)
             return date_span;
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/host/host-main-window.vala b/src/host/host-main-window.vala
index 61e3df6..17e7a4c 100644
--- a/src/host/host-main-window.vala
+++ b/src/host/host-main-window.vala
@@ -89,6 +89,7 @@ public class MainWindow : Gtk.ApplicationWindow {
     private View.Palette palette;
     private View.Controllable month_view;
     private View.Controllable week_view;
+    private View.Controllable agenda_view;
     private View.Controllable? current_controller = null;
     private Gee.HashSet<Binding> current_bindings = new Gee.HashSet<Binding>();
     private Gtk.Stack view_stack = new Gtk.Stack();
@@ -142,10 +143,12 @@ public class MainWindow : Gtk.ApplicationWindow {
         // ... then create the hosted views
         month_view = new View.Month.Controller(palette);
         week_view = new View.Week.Controller(palette);
+        agenda_view = new View.Agenda.Controller(palette);
         
         // add views to view stack, first added is first shown
         add_controller(month_view);
         add_controller(week_view);
+        add_controller(agenda_view);
         
         // if not on Unity, use headerbar as the titlebar (removes window chrome) and provide close
         // button for users who might have trouble finding it otherwise
diff --git a/src/rc/view-agenda-date-row.ui b/src/rc/view-agenda-date-row.ui
new file mode 100644
index 0000000..ca1709b
--- /dev/null
+++ b/src/rc/view-agenda-date-row.ui
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.18.3 -->
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <template class="CaliforniaViewAgendaDateRow" parent="GtkBox">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkLabel" id="date_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_right">16</property>
+        <property name="margin_top">3</property>
+        <property name="xalign">1</property>
+        <property name="yalign">0</property>
+        <property name="label">(date)</property>
+        <style>
+          <class name="dim-label"/>
+        </style>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkListBox" id="event_listbox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="selection_mode">none</property>
+      </object>
+      <packing>
+        <property name="expand">True</property>
+        <property name="fill">True</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/src/rc/view-agenda-event-row.ui b/src/rc/view-agenda-event-row.ui
new file mode 100644
index 0000000..c4d05ed
--- /dev/null
+++ b/src/rc/view-agenda-event-row.ui
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.18.3 -->
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <template class="CaliforniaViewAgendaEventRow" parent="GtkBox">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkEventBox" id="time_eventbox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="visible_window">False</property>
+        <child>
+          <object class="GtkLabel" id="time_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="margin_right">16</property>
+            <property name="xalign">0</property>
+            <property name="label">(time)</property>
+            <property name="use_markup">True</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkEventBox" id="summary_eventbox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="visible_window">False</property>
+        <child>
+          <object class="GtkLabel" id="summary_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="label">(summary)</property>
+            <property name="use_markup">True</property>
+            <property name="ellipsize">end</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <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/rc/view-agenda-load-more-row.ui b/src/rc/view-agenda-load-more-row.ui
new file mode 100644
index 0000000..b360c96
--- /dev/null
+++ b/src/rc/view-agenda-load-more-row.ui
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.18.3 -->
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <template class="CaliforniaViewAgendaLoadMoreRow" parent="GtkBox">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="halign">center</property>
+    <property name="hexpand">True</property>
+    <property name="spacing">8</property>
+    <child>
+      <object class="GtkLabel" id="showing_until_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="label">Showing events until Month, Day, Year.</property>
+        <attributes>
+          <attribute name="style" value="italic"/>
+        </attributes>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButton" id="load_more_button">
+        <property name="label" translatable="yes" comments="As in, &quot;Load more events&quot;">Load 
_More</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="use_underline">True</property>
+        <signal name="clicked" handler="on_load_more_button_clicked" 
object="CaliforniaViewAgendaLoadMoreRow" swapped="no"/>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/src/toolkit/toolkit-listbox-model.vala b/src/toolkit/toolkit-listbox-model.vala
index 92ef654..10d92a3 100644
--- a/src/toolkit/toolkit-listbox-model.vala
+++ b/src/toolkit/toolkit-listbox-model.vala
@@ -58,6 +58,11 @@ public class ListBoxModel<G> : BaseObject {
     public signal void added(G item);
     
     /**
+     * Fired when a GtkListBoxRow is added to the { link listbox}.
+     */
+    public signal void row_added(Gtk.ListBoxRow row, G item);
+    
+    /**
      * Fired when an item is removed from the { link ListBoxModel}.
      *
      * @see remove
@@ -65,6 +70,11 @@ public class ListBoxModel<G> : BaseObject {
     public signal void removed(G item);
     
     /**
+     * Fired when a GtkListBoxRow is removed from the { link listbox}.
+     */
+    public signal void row_removed(Gtk.ListBoxRow row, G item);
+    
+    /**
      * Fired when the { link listbox} activates an item.
      *
      * Gtk.ListBox can activate an item with a double- or single-click, depending on configuration.
@@ -107,6 +117,7 @@ public class ListBoxModel<G> : BaseObject {
      * Returns true if the model (and therefore the listbox) were altered due to the addition.
      *
      * @see added
+     * @see row_added
      */
     public bool add(G item) {
         if (items.has_key(item))
@@ -115,9 +126,18 @@ public class ListBoxModel<G> : BaseObject {
         // item -> Gtk.ListBoxRow, with MutableWidget support
         Gtk.ListBoxRow row = new Gtk.ListBoxRow();
         Gtk.Widget widget = model_presentation(item);
+        
+        // allow for external callers to make the ListBoxRow visible via their supplied widget's
+        // visibility flag ... this is necessary because setting the presentation widget to invisible
+        // leaves the row's visible and taking up a little space for border and margin and such
+        widget.bind_property("visible", row, "visible",
+            BindingFlags.BIDIRECTIONAL | BindingFlags.SYNC_CREATE);
+        
+        // if widget is mutable, watch for that
         MutableWidget? mutable = widget as MutableWidget;
         if (mutable != null)
             mutable.mutated.connect(() => { row.changed(); });
+        
         row.add(widget);
         
         // mappings
@@ -131,6 +151,7 @@ public class ListBoxModel<G> : BaseObject {
         size = size + 1;
         
         added(item);
+        row_added(row, item);
         
         return true;
     }
@@ -158,6 +179,7 @@ public class ListBoxModel<G> : BaseObject {
      * Returns true if the model (and therefore the listbox) were altered due to the removal.
      *
      * @see removed
+     * @see row_removed
      */
     public bool remove(G item) {
         return internal_remove(item, true);
@@ -192,6 +214,7 @@ public class ListBoxModel<G> : BaseObject {
         size = (size - 1).clamp(0, int.MAX);
         
         removed(item);
+        row_removed(row, item);
         
         return true;
     }
@@ -241,6 +264,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/toolkit/toolkit.vala b/src/toolkit/toolkit.vala
index 94c87ae..7ade342 100644
--- a/src/toolkit/toolkit.vala
+++ b/src/toolkit/toolkit.vala
@@ -150,4 +150,19 @@ public void destroy_later(Gtk.Widget widget) {
     }, Priority.LOW);
 }
 
+/**
+ * Prevent prelight when a mouse hovers over the widget.
+ *
+ * This operates by preventing the Gtk.StateFlag.PRELIGHT state from being entered.  This may have
+ * negative effects for some widgets and should be used with caution.
+ */
+public void prevent_prelight(Gtk.Widget widget) {
+    widget.state_flags_changed.connect(on_state_flags_changed);
+}
+
+private void on_state_flags_changed(Gtk.Widget widget, Gtk.StateFlags old_state_flags) {
+    if ((widget.get_state_flags() & Gtk.StateFlags.PRELIGHT) != 0)
+        widget.unset_state_flags(Gtk.StateFlags.PRELIGHT);
+}
+
 }
diff --git a/src/view/agenda/agenda-controller.vala b/src/view/agenda/agenda-controller.vala
new file mode 100644
index 0000000..3b85944
--- /dev/null
+++ b/src/view/agenda/agenda-controller.vala
@@ -0,0 +1,309 @@
+/* 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.View.Agenda {
+
+public class Controller : BaseObject, View.Controllable {
+    public const string PROP_CURRENT_SPAN = "current-span";
+    
+    public const string VIEW_ID = "agenda";
+    
+    private class Container : Gtk.ScrolledWindow, View.Container {
+        private unowned Controllable _owner;
+        public unowned Controllable owner { get { return _owner; } }
+        
+        public Container(Controller controller, Gtk.Widget child) {
+            _owner = controller;
+            
+            add_with_viewport(child);
+        }
+    }
+    
+    /**
+     * @inheritDoc
+     */
+    public string id { get { return VIEW_ID; } }
+    
+    /**
+     * @inheritDoc
+     */
+    public string title { get { return _("Agenda"); } }
+    
+    /**
+     * @inheritDoc
+     */
+    public string current_label { get; protected set; }
+    
+    /**
+     * @inheritDoc
+     */
+    public bool is_viewing_today { get; protected set; }
+    
+    /**
+     * @inheritDoc
+     */
+    public ChronologyMotion motion { get { return ChronologyMotion.VERTICAL; } }
+    
+    /**
+     * @inheritDoc
+     */
+    public bool in_transition { get; protected set; }
+    
+    /**
+     * { link View.Palette} for the entire view.
+     */
+    public View.Palette palette { get; private set; }
+    
+    /**
+     * Current { link Calendar.DateSpan} being displayed.
+     */
+    public Calendar.DateSpan current_span { get; private set; }
+    
+    private Container container;
+    private Backing.CalendarSubscriptionManager? subscriptions = null;
+    private Gtk.ListBox listbox = new Gtk.ListBox();
+    private Toolkit.ListBoxModel<Calendar.Date> listbox_model;
+    private LoadMoreRow load_more_row;
+    
+    public Controller(View.Palette palette) {
+        this.palette = palette;
+        
+        container = new Container(this, listbox);
+        listbox_model = new Toolkit.ListBoxModel<Calendar.Date>(listbox, model_presentation);
+        
+        // Don't prelight the DateRows, as they can't be selected or activated
+        listbox_model.row_added.connect((row, item) => {
+            Toolkit.prevent_prelight(row);
+        });
+        
+        listbox.selection_mode = Gtk.SelectionMode.NONE;
+        listbox.activate_on_single_click = false;
+        
+        // this will initialize current_span
+        reset_subscriptions(
+            new Calendar.DateSpan(
+                Calendar.System.today,
+                Calendar.System.today.adjust_by(2, Calendar.DateUnit.MONTH)
+            )
+        );
+        
+        // LoadMoreRow is persistent and always sorts to the end of the list (see model_presentation)
+        // (need to add after setting current_span in reset_subscriptions)
+        load_more_row = new LoadMoreRow(this);
+        load_more_row.load_more.connect(on_load_more);
+        listbox_model.add(Calendar.Date.latest);
+    }
+    
+    /**
+     * @inheritDoc
+     */
+    public View.Container get_container() {
+        return container;
+    }
+    
+    /**
+     * @inheritDoc
+     */
+    public void next() {
+        reduce_subscriptions_start(current_span.start_date.adjust_by(1, Calendar.DateUnit.DAY));
+    }
+    
+    /**
+     * @inheritDoc
+     */
+    public void previous() {
+        expand_subscriptions(current_span.start_date.adjust_by(-1, Calendar.DateUnit.DAY));
+    }
+    
+    /**
+     * @inheritDoc
+     */
+    public void today() {
+        reset_subscriptions(new Calendar.DateSpan(Calendar.System.today, current_span.end_date));
+    }
+    
+    /**
+     * @inheritDoc
+     */
+    public void unselect_all() {
+        // no notion of selection in Agenda view
+    }
+    
+    /**
+     * @inheritDoc
+     */
+    public Gtk.Widget? get_widget_for_date(Calendar.Date date) {
+        return listbox_model.get_widget_for_item(date);
+    }
+    
+    private Gtk.Widget model_presentation(Calendar.Date date) {
+        if (date.equal_to(Calendar.Date.latest))
+            return load_more_row;
+        
+        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 on_load_more() {
+        expand_subscriptions(current_span.end_date.adjust_by(1, Calendar.DateUnit.MONTH));
+    }
+    
+    private Iterable<DateRow> traverse_date_rows() {
+        return traverse<Calendar.Date>(listbox_model.all())
+            .map_nonnull<DateRow>(date => listbox_model.get_widget_for_item(date) as DateRow);
+    }
+    
+    // Make existing DateRow widgets visible depending on if they're in the current_span; don't
+    // remove them to allow them to continue to receive event notifications in case the window is
+    // widened again to show them
+    private void show_hide_date_rows() {
+        traverse_date_rows()
+            .iterate(date_row => date_row.visible = date_row.date in current_span);
+    }
+    
+    private void clear_date_rows() {
+        traverse_date_rows()
+            .iterate(date_row => listbox_model.remove(date_row.date));
+    }
+    
+    private void reset_subscriptions(Calendar.DateSpan new_span) {
+        current_span = new_span;
+        
+        clear_date_rows();
+        
+        subscriptions = new Backing.CalendarSubscriptionManager(
+            current_span.to_exact_time_span(Calendar.Timezone.local));
+        
+        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();
+        
+        update_view_details();
+    }
+    
+    private void expand_subscriptions(Calendar.Date expansion) {
+        current_span = current_span.expand(expansion);
+        
+        // make previously invisible widgets (due to window reduction) visible again if in new span
+        show_hide_date_rows();
+        
+        // to avoid adding a lot of little expansions (which is expensive), add them a month at a
+        // time ... first check if subscription expansion even necessary, and if so, on which ends
+        // of the span ... first, convert to DateSpan
+        Calendar.DateSpan sub_span = new Calendar.DateSpan.from_exact_time_span(
+            subscriptions.window.to_timezone(Calendar.Timezone.local));
+        
+        bool expanded = false;
+        
+        // if necessary, walk the subscription start date back one month from requested date
+        if (!(current_span.start_date in sub_span)) {
+            Calendar.Date new_sub_start = sub_span.start_date.adjust_by(-1, Calendar.DateUnit.MONTH);
+            if (current_span.start_date.compare_to(new_sub_start) < 0)
+                new_sub_start = current_span.start_date;
+            
+            subscriptions.expand_window_async.begin(
+                new_sub_start.to_exact_time_span(Calendar.Timezone.local).start_exact_time);
+            expanded = true;
+        }
+        
+        // do the same for the subscription end date
+        if (!(current_span.end_date in sub_span)) {
+            Calendar.Date new_sub_end = sub_span.end_date.adjust_by(1, Calendar.DateUnit.MONTH);
+            if (current_span.end_date.compare_to(new_sub_end) > 0)
+                new_sub_end = current_span.end_date;
+            
+            subscriptions.expand_window_async.begin(
+                new_sub_end.to_exact_time_span(Calendar.Timezone.local).end_exact_time);
+            expanded = true;
+        }
+        
+        if (expanded)
+            debug("Agenda subscription window expanded to %s", subscriptions.window.to_string());
+        
+        update_view_details();
+    }
+    
+    private void reduce_subscriptions_start(Calendar.Date new_start) {
+        current_span = current_span.reduce_from_start(new_start);
+        
+        // make previously invisible widgets (due to window reduction) visible again if in new span
+        show_hide_date_rows();
+        
+        update_view_details();
+    }
+    
+    private void update_view_details() {
+        current_label = current_span.start_date.to_pretty_string(
+            Calendar.Date.PrettyFlag.ABBREV
+            | Calendar.Date.PrettyFlag.INCLUDE_YEAR
+            | Calendar.Date.PrettyFlag.NO_TODAY
+        );
+        is_viewing_today = current_span.start_date.equal_to(Calendar.System.today);
+    }
+    
+    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;
+        
+        traverse_date_rows()
+            .iterate(date_row => 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);
+            
+            DateRow date_row = (DateRow) listbox_model.get_widget_for_item(date);
+            date_row.add_event(event);
+            
+            // possible to be notified of Event outside of current_span; see reduce_subscriptions()
+            date_row.visible = date in current_span;
+        }
+    }
+    
+    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() {
+        return classname;
+    }
+}
+
+}
+
diff --git a/src/view/agenda/agenda-date-row.vala b/src/view/agenda/agenda-date-row.vala
new file mode 100644
index 0000000..18781c3
--- /dev/null
+++ b/src/view/agenda/agenda-date-row.vala
@@ -0,0 +1,115 @@
+/* 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.View.Agenda {
+
+[GtkTemplate (ui = "/org/yorba/california/rc/view-agenda-date-row.ui")]
+private class DateRow : Gtk.Box {
+    private const Calendar.Date.PrettyFlag DATE_PRETTY_FLAGS =
+        Calendar.Date.PrettyFlag.INCLUDE_OTHER_YEAR;
+    
+    private static Gtk.SizeGroup date_label_size_group;
+    
+    public Calendar.Date date { get; private set; }
+    
+    public int size { get { return listbox_model.size; } }
+    
+    [GtkChild]
+    private Gtk.Label date_label;
+    
+    [GtkChild]
+    private Gtk.ListBox event_listbox;
+    
+    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,
+            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) => {
+            Toolkit.prevent_prelight(row);
+        });
+        
+        // all date labels are same width
+        date_label_size_group.add_widget(date_label);
+        
+        // Because some date text labels are relative (i.e. "Today"), refresh when the date changes
+        Calendar.System.instance.today_changed.connect(update_ui);
+        
+        update_ui();
+    }
+    
+    ~DateRow() {
+        Calendar.System.instance.today_changed.disconnect(update_ui);
+    }
+    
+    internal static void init() {
+        date_label_size_group = new Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL);
+    }
+    
+    internal static void terminate() {
+        date_label_size_group = null;
+    }
+    
+    private void update_ui() {
+        date_label.label = date.to_pretty_string(DATE_PRETTY_FLAGS);
+    }
+    
+    private void on_listbox_model_size_changed() {
+        if (listbox_model.size == 0)
+            empty();
+    }
+    
+    public void add_event(Component.Event 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_event_date_changed);
+        event.notify[Component.Event.PROP_EXACT_TIME_SPAN].connect(on_event_date_changed);
+    }
+    
+    public void remove_event(Component.Event event) {
+        if (!listbox_model.remove(event))
+            return;
+        
+        event.notify[Component.Event.PROP_DATE_SPAN].disconnect(on_event_date_changed);
+        event.notify[Component.Event.PROP_EXACT_TIME_SPAN].disconnect(on_event_date_changed);
+    }
+    
+    private void on_event_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
new file mode 100644
index 0000000..2558dae
--- /dev/null
+++ b/src/view/agenda/agenda-event-row.vala
@@ -0,0 +1,160 @@
+/* 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.View.Agenda {
+
+[GtkTemplate (ui = "/org/yorba/california/rc/view-agenda-event-row.ui")]
+private class EventRow : Gtk.Box, Toolkit.MutableWidget {
+    private const Calendar.WallTime.PrettyFlag TIME_PRETTY_FLAGS = Calendar.WallTime.PrettyFlag.NONE;
+    
+    private static Gtk.SizeGroup time_label_size_group;
+    
+    public new Component.Event event { get; private set; }
+    
+    [GtkChild]
+    private Gtk.EventBox time_eventbox;
+    
+    [GtkChild]
+    private Gtk.Label time_label;
+    
+    [GtkChild]
+    private Gtk.EventBox summary_eventbox;
+    
+    [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();
+    
+    public EventRow(Controller owner, Component.Event event) {
+        this.owner = owner;
+        this.event = event;
+        
+        // all time labels are the same width
+        time_label_size_group.add_widget(time_label);
+        
+        // capture motion and mouse clicks for both labels
+        button_connector.connect_to(time_eventbox);
+        button_connector.connect_to(summary_eventbox);
+        motion_connector.connect_to(time_eventbox);
+        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);
+        
+        // .. and assume that all property changes cause sort-order changes (no reliable way to
+        // know exactly when for now)
+        event.altered.connect(() => { mutated(); });
+        
+        // .. 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);
+    }
+    
+    internal static void terminate() {
+        time_label_size_group = null;
+    }
+    
+    private void update_ui() {
+        if (event.is_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
+            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)
+            );
+        }
+        
+        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.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) {
+        // when entering or leaving cell, reset the cursor
+        Toolkit.set_toplevel_cursor(details.widget, null);
+    }
+    
+    private void on_event_motion(Toolkit.MotionEvent details) {
+        // if hovering over an event, show the "hyperlink" cursor
+        Toolkit.set_toplevel_cursor(details.widget, Gdk.CursorType.HAND1);
+    }
+}
+
+}
+
diff --git a/src/view/agenda/agenda-load-more-row.vala b/src/view/agenda/agenda-load-more-row.vala
new file mode 100644
index 0000000..5c1d65a
--- /dev/null
+++ b/src/view/agenda/agenda-load-more-row.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.View.Agenda {
+
+[GtkTemplate (ui = "/org/yorba/california/rc/view-agenda-load-more-row.ui")]
+private class LoadMoreRow : Gtk.Box {
+    private unowned Controller owner;
+    
+    [GtkChild]
+    private Gtk.Label showing_until_label;
+    
+    public signal void load_more();
+    
+    public LoadMoreRow(Controller owner) {
+        this.owner = owner;
+        
+        owner.notify[Controller.PROP_CURRENT_SPAN].connect(update_ui);
+        
+        update_ui();
+    }
+    
+    ~LoadMoreRow() {
+        owner.notify[Controller.PROP_CURRENT_SPAN].disconnect(update_ui);
+    }
+    
+    private void update_ui() {
+        string date_str = owner.current_span.end_date.to_pretty_string(
+            Calendar.Date.PrettyFlag.INCLUDE_YEAR
+            | Calendar.Date.PrettyFlag.NO_DAY_OF_WEEK
+            | Calendar.Date.PrettyFlag.NO_TODAY
+        );
+        
+        // %s is a date, i.e. "Showing events until December 5, 2014"
+        showing_until_label.label = _("Showing events until %s").printf(date_str);
+    }
+    
+    [GtkCallback]
+    private void on_load_more_button_clicked() {
+        load_more();
+    }
+}
+
+}
+
diff --git a/src/view/agenda/agenda.vala b/src/view/agenda/agenda.vala
new file mode 100644
index 0000000..66cfb7a
--- /dev/null
+++ b/src/view/agenda/agenda.vala
@@ -0,0 +1,35 @@
+/* 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.View.Agenda {
+
+private int init_count = 0;
+
+public void init() throws Error {
+    if (!Unit.do_init(ref init_count))
+        return;
+    
+    Toolkit.init();
+    Calendar.init();
+    
+    // internal unit init
+    DateRow.init();
+    EventRow.init();
+}
+
+public void terminate() {
+    if (!Unit.do_terminate(ref init_count))
+        return;
+    
+    EventRow.terminate();
+    DateRow.terminate();
+    
+    Calendar.terminate();
+    Toolkit.terminate();
+}
+
+}
+
diff --git a/src/view/month/month-controller.vala b/src/view/month/month-controller.vala
index f70d30c..5a588a0 100644
--- a/src/view/month/month-controller.vala
+++ b/src/view/month/month-controller.vala
@@ -68,11 +68,6 @@ public class Controller : BaseObject, View.Controllable {
     /**
      * @inheritDoc
      */
-    public Calendar.Date default_date { get; protected set; }
-    
-    /**
-     * @inheritDoc
-     */
     public bool in_transition { get; protected set; }
     
     /**
diff --git a/src/view/view.vala b/src/view/view.vala
index 584bfaa..22ea522 100644
--- a/src/view/view.vala
+++ b/src/view/view.vala
@@ -24,12 +24,14 @@ public void init() throws Error {
     View.Common.init();
     View.Month.init();
     View.Week.init();
+    View.Agenda.init();
 }
 
 public void terminate() {
     if (!Unit.do_terminate(ref init_count))
         return;
     
+    View.Agenda.terminate();
     View.Week.terminate();
     View.Month.terminate();
     View.Common.terminate();


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