[california/wip/728838-transitions: 3/3] Cache improvements, final touches



commit b1c3a828c3a581947ef4a7f541dbfafb6ac672fc
Author: Jim Nelson <jim yorba org>
Date:   Fri Apr 25 18:34:24 2014 -0700

    Cache improvements, final touches

 src/Makefile.am                                    |    1 +
 .../backing-calendar-subscription-manager.vala     |   34 +++++---
 src/calendar/calendar-month-of-year.vala           |   22 +++++
 src/tests/tests-calendar-month-of-year.vala        |   47 ++++++++++
 src/tests/tests.vala                               |    1 +
 src/view/month/month-cell.vala                     |    2 +-
 src/view/month/month-controller.vala               |   90 +++++++++++++-------
 src/view/month/month-grid.vala                     |   54 ++++++++++--
 8 files changed, 198 insertions(+), 53 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index 549805d..8ca1e2a 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -101,6 +101,7 @@ 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 \
        \
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 65d297f..7425e3b 100644
--- a/src/calendar/calendar-month-of-year.vala
+++ b/src/calendar/calendar-month-of-year.vala
@@ -85,6 +85,28 @@ public class MonthOfYear : DateSpan {
     }
     
     /**
+     * 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() {
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.vala b/src/tests/tests.vala
index c242023..20b6638 100644
--- a/src/tests/tests.vala
+++ b/src/tests/tests.vala
@@ -10,6 +10,7 @@ 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 637ef47..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;
     
diff --git a/src/view/month/month-controller.vala b/src/view/month/month-controller.vala
index daa4668..fb3dec3 100644
--- a/src/view/month/month-controller.vala
+++ b/src/view/month/month-controller.vala
@@ -20,6 +20,10 @@ public class Controller : BaseObject, View.Controllable {
     // 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.
      *
@@ -88,9 +92,10 @@ public class Controller : BaseObject, View.Controllable {
         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
-        month_of_year = Calendar.System.today.month_of_year();
+        // 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() {
@@ -99,9 +104,9 @@ public class Controller : BaseObject, View.Controllable {
     
     // Creates a new Grid for the MonthOfYear, storing locally and adding to the GtkStack.  Will
     // reuse existing Grids whenever possible.
-    private Grid create_month_grid(Calendar.MonthOfYear month_of_year) {
+    private void ensure_month_grid_exists(Calendar.MonthOfYear month_of_year) {
         if (month_grids.has_key(month_of_year))
-            return month_grids.get(month_of_year);
+            return;
         
         Grid month_grid = new Grid(this, month_of_year);
         month_grid.show_all();
@@ -109,41 +114,41 @@ public class Controller : BaseObject, View.Controllable {
         // 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);
-        
-        return month_grid;
     }
     
     // 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.MonthOfYear next_month = month_of_year.next();
-        Calendar.MonthOfYear prev_month = month_of_year.previous();
-        Calendar.MonthSpan cache_span = new Calendar.MonthSpan.from_months(prev_month, next_month);
+        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));
         
-        // drop anything outside three-month range, other than current chronological month
+        // 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;
             
-            // remove from GtkStack and local storage
+            // drop, remove from GtkStack and local storage
             stack.remove(iter.get_value());
             iter.unset();
         }
         
-        // ensure three-months worth of grids are available
-        create_month_grid(month_of_year);
-        create_month_grid(next_month);
-        create_month_grid(prev_month);
+        // 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();
+    private unowned Grid? get_current_month_grid() {
+        return (Grid?) stack.get_visible_child();
     }
     
     /**
@@ -170,7 +175,12 @@ public class Controller : BaseObject, View.Controllable {
         if (!now.equal_to(month_of_year))
             month_of_year = now;
         
-        Cell? cell = get_current_month_grid().get_cell_for_date(Calendar.System.today);
+        // 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;
@@ -180,7 +190,9 @@ public class Controller : BaseObject, View.Controllable {
      * @inheritDoc
      */
     public void unselect_all() {
-        get_current_month_grid().unselect_all();
+        Grid? current_grid = get_current_month_grid();
+        if (current_grid != null)
+            current_grid.unselect_all();
     }
     
     /**
@@ -213,21 +225,39 @@ public class Controller : BaseObject, View.Controllable {
             error("Unable to set default date for %s: %s", month_of_year.to_string(), calerr.message);
         }
         
-        // update the cache to store current month and neighbors
-        update_month_grid_cache();
-        
         // set up transition to give appearance of moving chronologically through the pages of
         // a calendar
-        Calendar.MonthOfYear current_moy = get_current_month_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;
+        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() {
diff --git a/src/view/month/month-grid.vala b/src/view/month/month-grid.vala
index c76803b..bb21d85 100644
--- a/src/view/month/month-grid.vala
+++ b/src/view/month/month-grid.vala
@@ -10,7 +10,7 @@ namespace California.View.Month {
  * A Gtk.Grid of { link Cell}s, each representing a particular { link Calendar.Date}.
  */
 
-public class Grid : Gtk.Grid {
+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";
@@ -53,7 +53,7 @@ public class Grid : Gtk.Grid {
     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;
+    private Backing.CalendarSubscriptionManager? subscriptions = null;
     private Gdk.EventType button_press_type = Gdk.EventType.NOTHING;
     private Gdk.Point button_press_point = Gdk.Point();
     
@@ -93,11 +93,13 @@ public class Grid : Gtk.Grid {
         update_cells();
         update_subscriptions();
         
+        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);
     }
     
     ~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);
     }
@@ -109,7 +111,11 @@ public class Grid : Gtk.Grid {
         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);
     }
     
@@ -179,6 +185,9 @@ public class Grid : Gtk.Grid {
         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 = new Backing.CalendarSubscriptionManager(time_window);
         subscriptions.calendar_added.connect(on_calendar_added);
@@ -186,10 +195,43 @@ public class Grid : Gtk.Grid {
         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
@@ -405,10 +447,6 @@ public class Grid : Gtk.Grid {
         
         return true;
     }
-    
-    public string to_string() {
-        return "Month.Grid for %s".printf(month_of_year.to_string());
-    }
 }
 
 }


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