[california] Transitions when navigating month view: Closes bug #728838



commit 73aed592a4dbeaa9da3595f27fd4d9082db51891
Author: Jim Nelson <jim yorba org>
Date:   Fri Apr 25 18:39:16 2014 -0700

    Transitions when navigating month view: Closes bug #728838
    
    This introductions sliding transitions when navigating the month view
    as well as some work to speed up startup time.  Readahead caching is
    done on neighboring months so their events are loaded before sliding
    into view.

 src/Makefile.am                                    |    6 +-
 .../backing-calendar-subscription-manager.vala     |   34 ++-
 src/calendar/calendar-month-of-year.vala           |   36 +++
 src/calendar/calendar-month-span.vala              |  143 +++++++++++
 src/calendar/calendar-week-span.vala               |    2 +-
 src/host/host-main-window.vala                     |    4 +-
 src/tests/tests-calendar-month-of-year.vala        |   47 ++++
 src/tests/tests-calendar-month-span.vala           |   81 ++++++
 src/tests/tests.vala                               |    2 +
 src/view/month/month-cell.vala                     |   10 +-
 src/view/month/month-controller.vala               |  269 ++++++++++++++++++++
 .../{month-controllable.vala => month-grid.vala}   |  245 ++++++++-----------
 src/view/view-controllable.vala                    |    6 +
 13 files changed, 718 insertions(+), 167 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index 1e8ebda..8ca1e2a 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -59,6 +59,7 @@ california_VALASOURCES = \
        calendar/calendar-first-of-week.vala \
        calendar/calendar-month.vala \
        calendar/calendar-month-of-year.vala \
+       calendar/calendar-month-span.vala \
        calendar/calendar-olson-zone.vala \
        calendar/calendar-span.vala \
        calendar/calendar-system.vala \
@@ -100,6 +101,8 @@ california_VALASOURCES = \
        \
        tests/tests.vala \
        tests/tests-calendar-date.vala \
+       tests/tests-calendar-month-of-year.vala \
+       tests/tests-calendar-month-span.vala \
        tests/tests-quick-add.vala \
        \
        toolkit/toolkit.vala \
@@ -124,7 +127,8 @@ california_VALASOURCES = \
        \
        view/month/month.vala \
        view/month/month-cell.vala \
-       view/month/month-controllable.vala \
+       view/month/month-controller.vala \
+       view/month/month-grid.vala \
        \
        $(NULL)
 
diff --git a/src/backing/backing-calendar-subscription-manager.vala 
b/src/backing/backing-calendar-subscription-manager.vala
index c4851ef..d686310 100644
--- a/src/backing/backing-calendar-subscription-manager.vala
+++ b/src/backing/backing-calendar-subscription-manager.vala
@@ -26,6 +26,11 @@ public class CalendarSubscriptionManager : BaseObject {
     public Calendar.ExactTimeSpan window { get; private set; }
     
     /**
+     * Set to true when { link start_async} begins.
+     */
+    public bool is_started { get; private set; default = false; }
+    
+    /**
      * Indicates a { link CalendarSource} was added to the manager, either listed when first
      * created or detected at runtime afterwards.
      */
@@ -62,7 +67,7 @@ public class CalendarSubscriptionManager : BaseObject {
      *
      * The { link window} cannot be modified once created.
      *
-     * Events will not be signalled until { link start} is called.
+     * Events will not be signalled until { link start_async} is called.
      */
     public CalendarSubscriptionManager(Calendar.ExactTimeSpan window) {
         this.window = window;
@@ -85,38 +90,39 @@ public class CalendarSubscriptionManager : BaseObject {
      * There is no "stop" method.  Destroying the object will cancel all subscriptions, although
      * signals will not be fired at that time.
      */
-    public void start() {
+    public async void start_async() {
+        // to prevent reentrancy
+        if (is_started)
+            return;
+        
+        is_started = true;
+        
         foreach (Backing.Store store in Backing.Manager.instance.get_stores()) {
             // watch each store for future added sources
             store.source_added.connect(on_source_added);
             store.source_removed.connect(on_source_removed);
             
             foreach (Backing.Source source in store.get_sources_of_type<Backing.CalendarSource>())
-                add_calendar((Backing.CalendarSource) source);
+                yield add_calendar_async((Backing.CalendarSource) source, cancellable);
         }
     }
     
     private void on_source_added(Backing.Source source) {
         Backing.CalendarSource? calendar = source as Backing.CalendarSource;
         if (calendar != null)
-            add_calendar(calendar);
+            add_calendar_async.begin(calendar, cancellable);
     }
     
-    private void add_calendar(Backing.CalendarSource calendar) {
+    private async void add_calendar_async(Backing.CalendarSource calendar, Cancellable? cancellable) {
         // report calendar as added to subscription
         calendar_added(calendar);
         
         // start generating instances on this calendar
-        calendar.subscribe_async.begin(window, cancellable, on_subscribed);
-    }
-    
-    // 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
-    private void on_subscribed(Object? source, AsyncResult result) {
-        Backing.CalendarSource calendar = (Backing.CalendarSource) source;
-        
         try {
-            Backing.CalendarSourceSubscription subscription = calendar.subscribe_async.end(result);
+            // 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);
             
             // okay to use "this" ref
             subscriptions.add(subscription);
diff --git a/src/calendar/calendar-month-of-year.vala b/src/calendar/calendar-month-of-year.vala
index 7eaa491..7425e3b 100644
--- a/src/calendar/calendar-month-of-year.vala
+++ b/src/calendar/calendar-month-of-year.vala
@@ -84,6 +84,42 @@ public class MonthOfYear : DateSpan {
         return start_date.adjust(quantity, DateUnit.MONTH).month_of_year();
     }
     
+    /**
+     * Returns the number of months between the two { link MonthOfYear}s.
+     *
+     * If the supplied MonthOfYear is earlier than this one, a negative value is returned.
+     */
+    public int difference(MonthOfYear other) {
+        int compare = compare_to(other);
+        if (compare == 0)
+            return 0;
+        
+        // TODO: Iterating sucks, but it will have to suffice for now.
+        int count = 0;
+        MonthOfYear current = this;
+        for (;;) {
+            current = (compare > 0) ? current.previous() : current.next();
+            count += (compare > 0) ? -1 : 1;
+            
+            if (current.equal_to(other))
+                return count;
+        }
+    }
+    
+    /**
+     * Returns the chronological next { link MonthOfYear}.
+     */
+    public MonthOfYear next() {
+        return adjust(1);
+    }
+    
+    /**
+     * Returns the chronological prior { link MonthOfYear}.
+     */
+    public MonthOfYear previous() {
+        return adjust(-1);
+    }
+    
     public override string to_string() {
         return "%s %s".printf(month.to_string(), year.to_string());
     }
diff --git a/src/calendar/calendar-month-span.vala b/src/calendar/calendar-month-span.vala
new file mode 100644
index 0000000..506dbc8
--- /dev/null
+++ b/src/calendar/calendar-month-span.vala
@@ -0,0 +1,143 @@
+/* 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.Calendar {
+
+/**
+ * An immutable representation of a span of { link MonthOfYear}s.
+ *
+ * This class provides methods that can turn a { link DateSpan} an iteration of MonthOfYear objects.
+ * Partial months are included; the caller needs to do their own clamping if they want to avoid
+ * days outside of the DateSpan.
+ */
+
+public class MonthSpan : BaseObject, Collection.SimpleIterable<MonthOfYear>, Span<MonthOfYear>,
+    Gee.Comparable<MonthSpan>, Gee.Hashable<MonthSpan> {
+    private class MonthSpanIterator : BaseObject, Collection.SimpleIterator<MonthOfYear> {
+        public MonthOfYear first;
+        public MonthOfYear last;
+        public MonthOfYear? current = null;
+        
+        public MonthSpanIterator(MonthSpan owner) {
+            first = owner.start();
+            last = owner.end();
+        }
+        
+        public new MonthOfYear get() {
+            return current;
+        }
+        
+        public bool next() {
+            if (current == null)
+                current = first;
+            else if (current.start_date.compare_to(last.start_date) < 0)
+                current = current.adjust(1);
+            else
+                return false;
+            
+            return true;
+        }
+        
+        public override string to_string() {
+            return "MonthSpanIterator %s::%s".printf(first.to_string(), last.to_string());
+        }
+    }
+    
+    /**
+     * The { link DateSpan} of the { link MonthOfYear}s.
+     */
+    public DateSpan dates { get; private set; }
+    
+    /**
+     * inheritDoc
+     */
+    public Date start_date { owned get { return dates.start_date; } }
+    
+    /**
+     * inheritDoc
+     */
+    public Date end_date { owned get { return dates.end_date; } }
+    
+    /**
+     * Create a span of { link MonthOfYear}s corresponding to the { link DateSpan}.
+     */
+    public MonthSpan(DateSpan dates) {
+        this.dates = dates;
+    }
+    
+    /**
+     * Create a span of { link MonthOfYear}s corresponding to the start and end months.
+     */
+    public MonthSpan.from_months(MonthOfYear start, MonthOfYear end) {
+        dates = new DateSpan(start.start_date, end.end_date);
+    }
+    
+    /**
+     * Create an arbitrary span of { link MonthOfYear}s starting from the specified starting month.
+     */
+    public MonthSpan.count(MonthOfYear start, int count) {
+        dates = new DateSpan(start.start_date, start.adjust(count).end_date);
+    }
+    
+    /**
+     * inheritDoc
+     */
+    public MonthOfYear start() {
+        return dates.start_date.month_of_year();
+    }
+    
+    /**
+     * inheritDoc
+     */
+    public MonthOfYear end() {
+        return dates.end_date.month_of_year();
+    }
+    
+    /**
+     * @inheritDoc
+     */
+    public bool contains(Date date) {
+        return dates.contains(date);
+    }
+    
+    /**
+     * @inheritDoc
+     */
+    public bool has(MonthOfYear month) {
+        return (start().compare_to(month) <= 0) && (end().compare_to(month) >= 0);
+    }
+    
+    /**
+     * Returns an Iterator for each { link MonthOfYear} (full and partial) in the { link MonthSpan}.
+     */
+    public Collection.SimpleIterator<MonthOfYear> iterator() {
+        return new MonthSpanIterator(this);
+    }
+    
+    /**
+     * Compares two { link MonthSpan}s by their { link start_date}.
+     */
+    public int compare_to(MonthSpan other) {
+        return start_date.compare_to(other.start_date);
+    }
+    
+    public bool equal_to(MonthSpan other) {
+        if (this == other)
+            return true;
+        
+        return start_date.equal_to(other.start_date) && end_date.equal_to(other.end_date);
+    }
+    
+    public uint hash() {
+        return start_date.hash() ^ end_date.hash();
+    }
+    
+    public override string to_string() {
+        return "months of %s".printf(dates.to_string());
+    }
+}
+
+}
diff --git a/src/calendar/calendar-week-span.vala b/src/calendar/calendar-week-span.vala
index 91093bc..16a54b8 100644
--- a/src/calendar/calendar-week-span.vala
+++ b/src/calendar/calendar-week-span.vala
@@ -47,7 +47,7 @@ public class WeekSpan : BaseObject, Collection.SimpleIterable<Week>, Span<Week>,
     }
     
     /**
-     * The { link DateSpan} of thw { link Week}s.
+     * The { link DateSpan} of the { link Week}s.
      */
     public DateSpan dates { get; private set; }
     
diff --git a/src/host/host-main-window.vala b/src/host/host-main-window.vala
index 1d2922e..aa8459c 100644
--- a/src/host/host-main-window.vala
+++ b/src/host/host-main-window.vala
@@ -36,7 +36,7 @@ public class MainWindow : Gtk.ApplicationWindow {
     public Calendar.FirstOfWeek first_of_week { get; set; }
     
     private View.Controllable current_view;
-    private View.Month.Controllable month_view = new View.Month.Controllable();
+    private View.Month.Controller month_view = new View.Month.Controller();
     private Gtk.Button quick_add_button;
     
     public MainWindow(Application app) {
@@ -109,7 +109,7 @@ public class MainWindow : Gtk.ApplicationWindow {
 #if ENABLE_UNITY
         layout.pack_start(headerbar, false, true, 0);
 #endif
-        layout.pack_end(month_view, true, true, 0);
+        layout.pack_end(month_view.get_container(), true, true, 0);
         
         // current host bindings and signals
         current_view.request_create_timed_event.connect(on_request_create_timed_event);
diff --git a/src/tests/tests-calendar-month-of-year.vala b/src/tests/tests-calendar-month-of-year.vala
new file mode 100644
index 0000000..48d036c
--- /dev/null
+++ b/src/tests/tests-calendar-month-of-year.vala
@@ -0,0 +1,47 @@
+/* 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.Tests {
+
+private class CalendarMonthOfYear : UnitTest.Harness {
+    public CalendarMonthOfYear() {
+        add_case("difference-same", difference_same);
+        add_case("difference-negative", difference_negative);
+        add_case("difference-positive", difference_positive);
+    }
+    
+    protected override void setup() throws Error {
+        Calendar.init();
+    }
+    
+    protected override void teardown() {
+        Calendar.terminate();
+    }
+    
+    private bool difference_same() throws Error {
+        Calendar.MonthOfYear jan = new Calendar.MonthOfYear(Calendar.Month.JAN, new Calendar.Year(2014));
+        Calendar.MonthOfYear jan2 = new Calendar.MonthOfYear(Calendar.Month.JAN, new Calendar.Year(2014));
+        
+        return jan.difference(jan2) == 0;
+    }
+    
+    private bool difference_negative() throws Error {
+        Calendar.MonthOfYear jan = new Calendar.MonthOfYear(Calendar.Month.JAN, new Calendar.Year(2014));
+        Calendar.MonthOfYear dec = new Calendar.MonthOfYear(Calendar.Month.DEC, new Calendar.Year(2013));
+        
+        return jan.difference(dec) == -1;
+    }
+    
+    private bool difference_positive() throws Error {
+        Calendar.MonthOfYear jan = new Calendar.MonthOfYear(Calendar.Month.JAN, new Calendar.Year(2014));
+        Calendar.MonthOfYear feb = new Calendar.MonthOfYear(Calendar.Month.FEB, new Calendar.Year(2014));
+        
+        return jan.difference(feb) == 1;
+    }
+}
+
+}
+
diff --git a/src/tests/tests-calendar-month-span.vala b/src/tests/tests-calendar-month-span.vala
new file mode 100644
index 0000000..5f200d3
--- /dev/null
+++ b/src/tests/tests-calendar-month-span.vala
@@ -0,0 +1,81 @@
+/* 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.Tests {
+
+private class CalendarMonthSpan : UnitTest.Harness {
+    public CalendarMonthSpan() {
+        add_case("todays-month", todays_month);
+        add_case("contains-date", contains_date);
+        add_case("has-month", has_month);
+        add_case("iterator", iterator);
+    }
+    
+    protected override void setup() throws Error {
+        Calendar.init();
+    }
+    
+    protected override void teardown() {
+        Calendar.terminate();
+    }
+    
+    private Calendar.Date from_today(int days) {
+        return Calendar.System.today.adjust(days, Calendar.DateUnit.DAY);
+    }
+    
+    private bool todays_month() throws Error {
+        Calendar.MonthSpan span = new Calendar.MonthSpan(new Calendar.DateSpan(
+            from_today(0), from_today(0)));
+        
+        return span.start().equal_to(Calendar.System.today.month_of_year())
+            && span.end().equal_to(Calendar.System.today.month_of_year());
+    }
+    
+    private bool contains_date() throws Error {
+        Calendar.Date first = new Calendar.Date(Calendar.DayOfMonth.for_checked(1), Calendar.Month.JAN, new 
Calendar.Year(2014));
+        Calendar.Date last = new Calendar.Date(Calendar.DayOfMonth.for_checked(30), Calendar.Month.JAN, new 
Calendar.Year(2014));
+        Calendar.MonthSpan span = new Calendar.MonthSpan(new Calendar.DateSpan(first, last));
+        
+        return span.contains(first.adjust(15, Calendar.DateUnit.DAY));
+    }
+    
+    private bool has_month() throws Error {
+        Calendar.Date first = new Calendar.Date(Calendar.DayOfMonth.for_checked(1), Calendar.Month.JAN, new 
Calendar.Year(2014));
+        Calendar.Date last = new Calendar.Date(Calendar.DayOfMonth.for_checked(30), Calendar.Month.MAR, new 
Calendar.Year(2014));
+        Calendar.MonthSpan span = new Calendar.MonthSpan(new Calendar.DateSpan(first, last));
+        
+        return span.has(new Calendar.MonthOfYear(Calendar.Month.FEB, new Calendar.Year(2014)));
+    }
+    
+    private bool iterator() throws Error {
+        Calendar.Date first = new Calendar.Date(Calendar.DayOfMonth.for_checked(1), Calendar.Month.JAN, new 
Calendar.Year(2014));
+        Calendar.Date last = new Calendar.Date(Calendar.DayOfMonth.for_checked(30), Calendar.Month.JUN, new 
Calendar.Year(2014));
+        Calendar.MonthSpan span = new Calendar.MonthSpan(new Calendar.DateSpan(first, last));
+        
+        Calendar.Month[] months = {
+            Calendar.Month.JAN,
+            Calendar.Month.FEB,
+            Calendar.Month.MAR,
+            Calendar.Month.APR,
+            Calendar.Month.MAY,
+            Calendar.Month.JUN,
+        };
+        
+        int ctr = 0;
+        foreach (Calendar.MonthOfYear moy in span) {
+            if (moy.month != months[ctr++])
+                return false;
+            
+            if (moy.year.value != 2014)
+                return false;
+        }
+        
+        return ctr == 6;
+    }
+}
+
+}
+
diff --git a/src/tests/tests.vala b/src/tests/tests.vala
index 0684618..20b6638 100644
--- a/src/tests/tests.vala
+++ b/src/tests/tests.vala
@@ -9,6 +9,8 @@ namespace California.Tests {
 public int run(string[] args) {
     UnitTest.Harness.register(new QuickAdd());
     UnitTest.Harness.register(new CalendarDate());
+    UnitTest.Harness.register(new CalendarMonthSpan());
+    UnitTest.Harness.register(new CalendarMonthOfYear());
     
     return UnitTest.Harness.exec_all();
 }
diff --git a/src/view/month/month-cell.vala b/src/view/month/month-cell.vala
index 2ee1959..1381fa6 100644
--- a/src/view/month/month-cell.vala
+++ b/src/view/month/month-cell.vala
@@ -10,7 +10,7 @@ namespace California.View.Month {
  * A single cell within a { link MonthGrid}.
  */
 
-public class Cell : Gtk.EventBox {
+private class Cell : Gtk.EventBox {
     private const int TOP_LINE_FONT_SIZE_PT = 11;
     private const int LINE_FONT_SIZE_PT = 8;
     
@@ -35,7 +35,7 @@ public class Cell : Gtk.EventBox {
         POINTED
     }
     
-    public weak Controllable owner { get; private set; }
+    public weak Grid owner { get; private set; }
     public int row { get; private set; }
     public int col { get; private set; }
     
@@ -88,7 +88,7 @@ public class Cell : Gtk.EventBox {
     
     private Gtk.DrawingArea canvas = new Gtk.DrawingArea();
     
-    public Cell(Controllable owner, int row, int col) {
+    public Cell(Grid owner, int row, int col) {
         this.owner = owner;
         this.row = row;
         this.col = col;
@@ -395,13 +395,13 @@ public class Cell : Gtk.EventBox {
         }
         
         // only draw bottom line if not on the bottom row
-        if (row < Controllable.ROWS - 1) {
+        if (row < Grid.ROWS - 1) {
             ctx.move_to(0, height);
             ctx.line_to(width, height);
         }
         
         // only draw right line if not on the right-most column
-        if (col < Controllable.COLS - 1) {
+        if (col < Grid.COLS - 1) {
             ctx.move_to(width, 0);
             ctx.line_to(width, height);
         }
diff --git a/src/view/month/month-controller.vala b/src/view/month/month-controller.vala
new file mode 100644
index 0000000..fb3dec3
--- /dev/null
+++ b/src/view/month/month-controller.vala
@@ -0,0 +1,269 @@
+/* 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.Month {
+
+/**
+ * The { link View.Controllable} for a Month View of the user's calendars.
+ *
+ * The Controller holds a GtkStack of { link Grid}s which it "flips" back and forth through as
+ * the user navigates the calendar.
+ */
+
+public class Controller : BaseObject, View.Controllable {
+    public const string PROP_MONTH_OF_YEAR = "month-of-year";
+    public const string PROP_SHOW_OUTSIDE_MONTH = "show-outside-month";
+    
+    // Slower than default to make more apparent to user what's occurring
+    private const int TRANSITION_DURATION_MSEC = 500;
+    
+    // number of Grids to keep in GtkStack and cache (in terms of months) ... this should be an
+    // even number, as it is halved to determine neighboring months depths
+    private const int CACHE_NEIGHBORS_COUNT = 4;
+    
+    /**
+     * The month and year being displayed.
+     *
+     * Defaults to the current month and year.
+     */
+    public Calendar.MonthOfYear month_of_year { get; private set; }
+    
+    /**
+     * @inheritDoc
+     */
+    public Calendar.FirstOfWeek first_of_week { get; set; }
+    
+    /**
+     * Show days outside the current month.
+     */
+    public bool show_outside_month { get; set; default = true; }
+    
+    /**
+     * @inheritDoc
+     */
+    public string current_label { get; protected set; }
+    
+    /**
+     * @inheritDoc
+     */
+    public bool is_viewing_today { get; protected set; }
+    
+    /**
+     * @inheritDoc
+     */
+    public Calendar.Date default_date { get; protected set; }
+    
+    private Gtk.Grid master_grid = new Gtk.Grid();
+    private Gtk.Stack stack = new Gtk.Stack();
+    private Gee.HashMap<Calendar.MonthOfYear, Grid> month_grids = new Gee.HashMap<Calendar.MonthOfYear, 
Grid>();
+    
+    public Controller() {
+        master_grid.column_homogeneous = true;
+        master_grid.column_spacing = 0;
+        master_grid.row_homogeneous = false;
+        master_grid.row_spacing = 0;
+        master_grid.expand = true;
+        
+        stack.transition_duration = TRANSITION_DURATION_MSEC;
+        
+        // insert labels for days of the week across top of master grid
+        for (int col = 0; col < Grid.COLS; col++) {
+            Gtk.Label dow_label = new Gtk.Label(null);
+            dow_label.margin_top = 2;
+            dow_label.margin_bottom = 2;
+            
+            // update label if first-of-week changes
+            int dow_col = col + Calendar.DayOfWeek.MIN;
+            notify[PROP_FIRST_OF_WEEK].connect(() => {
+                Calendar.DayOfWeek dow = Calendar.DayOfWeek.for_checked(dow_col, first_of_week);
+                dow_label.label = dow.abbrev_name;
+            });
+            
+            master_grid.attach(dow_label, col, 0, 1, 1);
+        }
+        
+        // the stack is what flips between the month grids (it's inserted empty here, changes to
+        // first_of_week are what fill the stack with Grids and select which to display)
+        master_grid.attach(stack, 0, 1, Grid.COLS, 1);
+        
+        notify[PROP_MONTH_OF_YEAR].connect(on_month_of_year_changed);
+        Calendar.System.instance.today_changed.connect(on_today_changed);
+        
+        // update now that signal handlers are in place ... do first_of_week first since more heavy
+        // processing is done when month_of_year changes
+        first_of_week = Calendar.FirstOfWeek.SUNDAY;
+        month_of_year = Calendar.System.today.month_of_year();
+    }
+    
+    ~Controller() {
+        Calendar.System.instance.today_changed.disconnect(on_today_changed);
+    }
+    
+    // Creates a new Grid for the MonthOfYear, storing locally and adding to the GtkStack.  Will
+    // reuse existing Grids whenever possible.
+    private void ensure_month_grid_exists(Calendar.MonthOfYear month_of_year) {
+        if (month_grids.has_key(month_of_year))
+            return;
+        
+        Grid month_grid = new Grid(this, month_of_year);
+        month_grid.show_all();
+        
+        // add to local store and to the GtkStack itself
+        month_grids.set(month_of_year, month_grid);
+        stack.add_named(month_grid, month_grid.id);
+    }
+    
+    // Performs Grid caching by ensuring that Grids are available for the current, next, and
+    // previous month and that Grids outside that range are dropped.  The current chronological
+    // month is never discarded.
+    private void update_month_grid_cache() {
+        Calendar.MonthSpan cache_span = new Calendar.MonthSpan.from_months(
+            month_of_year.adjust(0 - (CACHE_NEIGHBORS_COUNT / 2)),
+            month_of_year.adjust(CACHE_NEIGHBORS_COUNT / 2));
+        
+        // trim cache
+        Gee.MapIterator<Calendar.MonthOfYear, Grid> iter = month_grids.map_iterator();
+        while (iter.next()) {
+            Calendar.MonthOfYear grid_moy = iter.get_key();
+            
+            // always keep current month
+            if (grid_moy.equal_to(Calendar.System.today.month_of_year()))
+                continue;
+            
+            // keep if grid is in cache span
+            if (cache_span.has(grid_moy))
+                continue;
+            
+            // drop, remove from GtkStack and local storage
+            stack.remove(iter.get_value());
+            iter.unset();
+        }
+        
+        // ensure all-months in span are available
+        foreach (Calendar.MonthOfYear moy in cache_span)
+            ensure_month_grid_exists(moy);
+    }
+    
+    private unowned Grid? get_current_month_grid() {
+        return (Grid?) stack.get_visible_child();
+    }
+    
+    /**
+     * @inheritDoc
+     */
+    public void next() {
+        month_of_year = month_of_year.next();
+    }
+    
+    /**
+     * @inheritDoc
+     */
+    public void prev() {
+        month_of_year = month_of_year.previous();
+    }
+    
+    /**
+     * @inheritDoc
+     */
+    public Gtk.Widget today() {
+        // since changing the date is expensive in terms of adding/removing subscriptions, only
+        // update the property if it's actually different
+        Calendar.MonthOfYear now = Calendar.System.today.month_of_year();
+        if (!now.equal_to(month_of_year))
+            month_of_year = now;
+        
+        // current should be set by the month_of_year being set
+        Grid? current_grid = get_current_month_grid();
+        assert(current_grid != null);
+        
+        // this grid better have a cell with this date in it
+        Cell? cell = current_grid.get_cell_for_date(Calendar.System.today);
+        assert(cell != null);
+        
+        return cell;
+    }
+    
+    /**
+     * @inheritDoc
+     */
+    public void unselect_all() {
+        Grid? current_grid = get_current_month_grid();
+        if (current_grid != null)
+            current_grid.unselect_all();
+    }
+    
+    /**
+     * @inheritDoc
+     */
+    public Gtk.Widget get_container() {
+        return master_grid;
+    }
+    
+    private void update_is_viewing_today() {
+        is_viewing_today = month_of_year.equal_to(Calendar.System.today.month_of_year());
+    }
+    
+    private void on_today_changed() {
+        // don't update view but indicate if it's still in view
+        update_is_viewing_today();
+    }
+    
+    private void on_month_of_year_changed() {
+        current_label = month_of_year.full_name;
+        update_is_viewing_today();
+        
+        // default date is first of month unless displaying current month, in which case it's
+        // current date
+        try {
+            default_date = is_viewing_today ? Calendar.System.today
+                : month_of_year.date_for(month_of_year.first_day_of_month());
+        } catch (CalendarError calerr) {
+            // this should always work
+            error("Unable to set default date for %s: %s", month_of_year.to_string(), calerr.message);
+        }
+        
+        // set up transition to give appearance of moving chronologically through the pages of
+        // a calendar
+        Grid? current_grid = get_current_month_grid();
+        if (current_grid != null) {
+            Calendar.MonthOfYear current_moy = current_grid.month_of_year;
+            int compare = month_of_year.compare_to(current_moy);
+            if (compare < 0)
+                stack.transition_type = Gtk.StackTransitionType.SLIDE_RIGHT;
+            else if (compare > 0)
+                stack.transition_type = Gtk.StackTransitionType.SLIDE_LEFT;
+            else
+                return;
+        }
+        
+        // because grid cache is populated/trimmed after sliding month into view, ensure the
+        // desired month already exists
+        ensure_month_grid_exists(month_of_year);
+        
+        // make visible using proper transition type
+        stack.set_visible_child(month_grids.get(month_of_year));
+        
+        // now update the cache to store current month and neighbors ... do this after doing above
+        // comparison because this update affects the GtkStack, which may revert to another page
+        // when the cache is trimmed, making the notion of "current" indeterminate; the most
+        // visible symptom of this is navigating far from today's month then clicking the Today
+        // button and no transition occurs because, when the cache is trimmed, today's month is
+        // the current child ... to avoid dropping the Widget before the transition completes,
+        // wait before doing this; 3.12's "transition-running" property would be useful here
+        Idle.add(() => {
+            update_month_grid_cache();
+            
+            return false;
+        }, Priority.LOW);
+    }
+    
+    public override string to_string() {
+        return "Month.Controller for %s".printf(month_of_year.to_string());
+    }
+}
+
+}
+
diff --git a/src/view/month/month-controllable.vala b/src/view/month/month-grid.vala
similarity index 74%
rename from src/view/month/month-controllable.vala
rename to src/view/month/month-grid.vala
index cf5b34f..bb21d85 100644
--- a/src/view/month/month-controllable.vala
+++ b/src/view/month/month-grid.vala
@@ -7,42 +7,38 @@
 namespace California.View.Month {
 
 /**
- * A Gtk.Grid widget that displays a month's worth of days as cells.
- *
- * @see Cell
+ * A Gtk.Grid of { link Cell}s, each representing a particular { link Calendar.Date}.
  */
 
-public class Controllable : Gtk.Grid, View.Controllable {
+private class Grid : Gtk.Grid {
+    public const string PROP_MONTH_OF_YEAR = "month-of-year";
+    public const string PROP_WINDOW = "window";
+    public const string PROP_FIRST_OF_WEEK = "first-of-week";
+    
     // days of the week
     public const int COLS = Calendar.DayOfWeek.COUNT;
     // calendar weeks to be displayed at any one time
     public const int ROWS = 6;
     
-    // day of week labels are stored in the -1 row
-    private const int DOW_ROW = -1;
-    
-    public const string PROP_MONTH_OF_YEAR = "month-of-year";
-    public const string PROP_SHOW_OUTSIDE_MONTH = "show-outside-month";
-    
     // Delegate for walking only Cells in the Grid.  Return true to keep iterating.
     private delegate bool CellCallback(Cell cell);
     
     /**
-     * The month and year being displayed.
-     *
-     * Defaults to the current month and year.
+     * { link Month.Controller} that created and holds this { link Grid}.
      */
-    public Calendar.MonthOfYear month_of_year { get; private set; }
+    public weak Controller owner { get; private set; }
     
     /**
-     * @inheritDoc
+     * { link MonthOfYear} this { link Grid} represents.
+     *
+     * This is immutable; Grids are not designed to be re-used for other months.
      */
-    public Calendar.FirstOfWeek first_of_week { get; set; }
+    public Calendar.MonthOfYear month_of_year { get; private set; }
     
     /**
-     * Show days outside the current month.
+     * The first day of the week, as defined by this { link Grid}'s { link Controller}.
      */
-    public bool show_outside_month { get; set; default = true; }
+    public Calendar.FirstOfWeek first_of_week { get; private set; }
     
     /**
      * The span of dates being displayed.
@@ -50,29 +46,25 @@ public class Controllable : Gtk.Grid, View.Controllable {
     public Calendar.DateSpan window { get; private set; }
     
     /**
-     * @inheritDoc
-     */
-    public string current_label { get; protected set; }
-    
-    /**
-     * @inheritDoc
-     */
-    public bool is_viewing_today { get; protected set; }
-    
-    /**
-     * @inheritDoc
+     * The name (id) of the { link Grid}.
+     *
+     * This is used when the Grid is added to Gtk.Stack.
      */
-    public Calendar.Date default_date { get; protected set; }
+    public string id { get { return month_of_year.full_name; } }
     
     private Gee.HashMap<Calendar.Date, Cell> date_to_cell = new Gee.HashMap<Calendar.Date, Cell>();
     private Backing.CalendarSubscriptionManager? subscriptions = null;
     private Gdk.EventType button_press_type = Gdk.EventType.NOTHING;
     private Gdk.Point button_press_point = Gdk.Point();
     
-    public Controllable() {
+    public Grid(Controller owner, Calendar.MonthOfYear month_of_year) {
+        this.owner = owner;
+        this.month_of_year = month_of_year;
+        first_of_week = owner.first_of_week;
+        
         column_homogeneous = true;
         column_spacing = 0;
-        row_homogeneous = false;
+        row_homogeneous = true;
         row_spacing = 0;
         
         // prep the grid with a fixed number of rows and columns
@@ -82,20 +74,9 @@ public class Controllable : Gtk.Grid, View.Controllable {
         for (int col = 0; col < COLS; col++)
             insert_column(0);
         
-        // pre-add grid elements for days of the week along the top row (using -1 as the row so the
-        // remainder of grid is "naturally" zero-based rows)
-        for (int col = 0; col < COLS; col++) {
-            Gtk.Label dow_cell = new Gtk.Label(null);
-            dow_cell.margin_top = 2;
-            dow_cell.margin_bottom = 2;
-            
-            attach(dow_cell, col, DOW_ROW, 1, 1);
-        }
-        
         // pre-add grid elements for every cell, which are updated when the MonthYear changes
         for (int row = 0; row < ROWS; row++) {
             for (int col = 0; col < COLS; col++) {
-                // mouse events are enabled in Cell's constructor, not here
                 Cell cell = new Cell(this, row, col);
                 cell.expand = true;
                 cell.events |= Gdk.EventMask.BUTTON_PRESS_MASK & Gdk.EventMask.BUTTON1_MOTION_MASK;
@@ -107,58 +88,20 @@ public class Controllable : Gtk.Grid, View.Controllable {
             }
         }
         
-        notify[PROP_MONTH_OF_YEAR].connect(on_month_of_year_changed);
-        notify[PROP_FIRST_OF_WEEK].connect(update_first_of_week);
-        notify[PROP_SHOW_OUTSIDE_MONTH].connect(update_cells);
-        Calendar.System.instance.today_changed.connect(on_today_changed);
-        
-        // update now that signal handlers are in place
-        month_of_year = Calendar.System.today.month_of_year();
-        first_of_week = Calendar.FirstOfWeek.SUNDAY;
-    }
-    
-    ~Controllable() {
-        Calendar.System.instance.today_changed.disconnect(on_today_changed);
-    }
-    
-    /**
-     * @inheritDoc
-     */
-    public void next() {
-        month_of_year = month_of_year.adjust(1);
-    }
-    
-    /**
-     * @inheritDoc
-     */
-    public void prev() {
-        month_of_year = month_of_year.adjust(-1);
-    }
-    
-    /**
-     * @inheritDoc
-     */
-    public Gtk.Widget today() {
-        // since changing the date is expensive in terms of adding/removing subscriptions, only
-        // update the property if it's actually different
-        Calendar.MonthOfYear now = Calendar.System.today.month_of_year();
-        if (!now.equal_to(month_of_year))
-            month_of_year = now;
-        
-        assert(date_to_cell.has_key(Calendar.System.today));
+        // update all the Cells by assigning them Dates ... this also updates the window, which
+        // is necessary for subscriptions
+        update_cells();
+        update_subscriptions();
         
-        return date_to_cell.get(Calendar.System.today);
+        owner.notify[Controller.PROP_MONTH_OF_YEAR].connect(on_controller_month_of_year_changed);
+        owner.notify[View.Controllable.PROP_FIRST_OF_WEEK].connect(update_first_of_week);
+        owner.notify[Controller.PROP_SHOW_OUTSIDE_MONTH].connect(update_cells);
     }
     
-    /**
-     * @inheritDoc
-     */
-    public void unselect_all() {
-        foreach_cell((cell) => {
-            cell.selected = false;
-            
-            return true;
-        });
+    ~Grid() {
+        owner.notify[Controller.PROP_MONTH_OF_YEAR].disconnect(on_controller_month_of_year_changed);
+        owner.notify[View.Controllable.PROP_FIRST_OF_WEEK].disconnect(update_first_of_week);
+        owner.notify[Controller.PROP_SHOW_OUTSIDE_MONTH].disconnect(update_cells);
     }
     
     private Cell get_cell(int row, int col) {
@@ -168,7 +111,11 @@ public class Controllable : Gtk.Grid, View.Controllable {
         return (Cell) get_child_at(col, row);
     }
     
-    internal Cell? get_cell_for_date(Calendar.Date date) {
+    /**
+     * Returns the { link Cell} for the specified { link Calendar.Date}, if it is contained by this
+     * { link Grid}.
+     */
+    public Cell? get_cell_for_date(Calendar.Date date) {
         return date_to_cell.get(date);
     }
     
@@ -201,14 +148,14 @@ public class Controllable : Gtk.Grid, View.Controllable {
     
     private void update_week(int row, Calendar.Week week) {
         foreach (Calendar.Date date in week) {
-            int col = date.day_of_week.ordinal(first_of_week) - 1;
+            int col = date.day_of_week.ordinal(owner.first_of_week) - 1;
             
             Cell cell = get_cell(row, col);
             
             // if the date is in the month or configured to show days outside the month, set
             // the cell to show that date; otherwise, it'll be cleared
             cell.clear();
-            cell.date = (date in month_of_year) || show_outside_month ? date : null;
+            cell.date = (date in month_of_year) || owner.show_outside_month ? date : null;
             
             // add to map for quick lookups
             date_to_cell.set(date, cell);
@@ -221,7 +168,7 @@ public class Controllable : Gtk.Grid, View.Controllable {
         
         // create a WeekSpan for the first week of the month to the last displayed week (not all
         // months will fill all displayed weeks, but some will)
-        Calendar.WeekSpan span = new Calendar.WeekSpan.count(month_of_year.weeks(first_of_week).start(),
+        Calendar.WeekSpan span = new 
Calendar.WeekSpan.count(month_of_year.weeks(owner.first_of_week).start(),
             ROWS - 1);
         
         // fill in weeks of the displayed month
@@ -233,60 +180,63 @@ public class Controllable : Gtk.Grid, View.Controllable {
         window = span.to_date_span();
     }
     
-    private void update_first_of_week() {
-        // set label text in day of week row
-        int col = 0;
-        foreach (Calendar.DayOfWeek dow in Calendar.DayOfWeek.iterator(first_of_week)) {
-            Gtk.Label dow_cell = (Gtk.Label) get_child_at(col++, DOW_ROW);
-            dow_cell.label = dow.abbrev_name;
-        }
-        
-        // requires updating all the cells as well, since all dates have to be shifted
-        update_cells();
-        update_subscription();
-    }
-    
-    private void update_is_viewing_today() {
-        is_viewing_today = month_of_year.equal_to(Calendar.System.today.month_of_year());
-    }
-    
-    private void on_today_changed() {
-        // don't update view but indicate if it's still in view
-        update_is_viewing_today();
-    }
-    
-    private void on_month_of_year_changed() {
-        current_label = month_of_year.full_name;
-        update_is_viewing_today();
-        
-        // default date is first of month unless displaying current month, in which case it's
-        // current date
-        try {
-            default_date = is_viewing_today ? Calendar.System.today
-                : month_of_year.date_for(month_of_year.first_day_of_month());
-        } catch (CalendarError calerr) {
-            // this should always work
-            error("Unable to set default date for %s: %s", month_of_year.to_string(), calerr.message);
-        }
-        
-        update_cells();
-        update_subscription();
-    }
-    
-    private void update_subscription() {
+    private void update_subscriptions() {
         // 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);
         
+        if (subscriptions != null && subscriptions.window.equal_to(time_window))
+            return;
+        
         // create new subscription manager, subscribe to its signals, and let them drive
-        subscriptions = null;
         subscriptions = new Backing.CalendarSubscriptionManager(time_window);
         subscriptions.calendar_added.connect(on_calendar_added);
         subscriptions.calendar_removed.connect(on_calendar_removed);
         subscriptions.instance_added.connect(on_instance_added);
         subscriptions.instance_removed.connect(on_instance_removed);
         
-        subscriptions.start();
+        // only start if this month is being displayed, otherwise will be started when owner's
+        // month of year changes to this one or a timeout (to prevent only subscribing
+        // when scrolled into view)
+        if (owner.month_of_year.equal_to(month_of_year)) {
+            subscriptions.start_async.begin();
+        } else {
+            // use distance from currently displayed month as a way to space out subscription
+            // starts, which are a little taxing ... assume future months are more likely to be
+            // moved to than past months, hence earlier months get the +1 dinged against them
+            int diff = owner.month_of_year.difference(month_of_year);
+            if (diff < 0)
+                diff = diff.abs() + 1;
+            
+            Timeout.add(300 + (diff * 100), () => {
+                subscriptions.start_async.begin();
+                
+                return false;
+            });
+        }
+    }
+    
+    private void on_controller_month_of_year_changed() {
+        // if this Grid is being displayed, immediately activate subscriptions
+        if (!owner.month_of_year.equal_to(month_of_year))
+            return;
+        
+        if (subscriptions == null)
+            update_subscriptions();
+        else if (!subscriptions.is_started)
+            subscriptions.start_async.begin();
+    }
+    
+    private void update_first_of_week() {
+        // avoid some extra work
+        if (first_of_week == owner.first_of_week)
+            return;
+        
+        first_of_week = owner.first_of_week;
+        
+        // requires updating all the cells as well, since all dates have to be shifted
+        update_cells();
+        update_subscriptions();
     }
     
     private void on_calendar_added(Backing.CalendarSource calendar) {
@@ -334,14 +284,21 @@ public class Controllable : Gtk.Grid, View.Controllable {
         }
     }
     
+    public void unselect_all() {
+        foreach_cell((cell) => {
+            cell.selected = false;
+            
+            return true;
+        });
+    }
+    
     private bool on_cell_button_event(Gtk.Widget widget, Gdk.EventButton event) {
         // only interested in left-clicks
         if (event.button != 1)
             return false;
         
         // NOTE: widget is the *pressed* widget, even for "release" events, no matter where the release
-        // occurs ... this signal handler is fired from Cells, never the GtkLabels across the top
-        // of the grid
+        // occurs
         Cell press_cell = (Cell) widget;
         
         switch (event.type) {
@@ -411,12 +368,12 @@ public class Controllable : Gtk.Grid, View.Controllable {
         if (press_cell == release_cell) {
             Component.Event? event = release_cell.get_event_at(release_point);
             if (event != null) {
-                request_display_event(event, release_cell, release_point);
+                owner.request_display_event(event, release_cell, release_point);
                 stop_propagation = true;
             }
         } else if (press_cell.date != null && release_cell.date != null) {
             // create multi-day event
-            request_create_all_day_event(new Calendar.DateSpan(press_cell.date, release_cell.date),
+            owner.request_create_all_day_event(new Calendar.DateSpan(press_cell.date, release_cell.date),
                 release_cell, release_point);
             stop_propagation = true;
         } else {
@@ -453,7 +410,7 @@ public class Controllable : Gtk.Grid, View.Controllable {
         
         Calendar.ExactTime end = start.adjust_time(1, Calendar.TimeUnit.HOUR);
         
-        request_create_timed_event(new Calendar.ExactTimeSpan(start, end), release_cell, release_point);
+        owner.request_create_timed_event(new Calendar.ExactTimeSpan(start, end), release_cell, 
release_point);
         
         // stop propagation
         return true;
diff --git a/src/view/view-controllable.vala b/src/view/view-controllable.vala
index 0346bf9..db5dc44 100644
--- a/src/view/view-controllable.vala
+++ b/src/view/view-controllable.vala
@@ -65,6 +65,12 @@ public interface Controllable : Object {
         Gdk.Point? for_location);
     
     /**
+     * Returns the Gtk.Widget container that should be used to display the { link Controllable}'s
+     * contents.
+     */
+    public abstract Gtk.Widget get_container();
+    
+    /**
      * Move forward one calendar unit.
      */
     public abstract void next();



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