[california/wip/725767-week] Hook up Week.Controller to StackModel



commit f301000745fd967aa90896322ebe94e6fb0e714e
Author: Jim Nelson <jim yorba org>
Date:   Fri May 2 16:02:51 2014 -0700

    Hook up Week.Controller to StackModel
    
    Need to do same for Month.Controller.

 src/Makefile.am                                |    1 +
 src/collection/collection-simple-iterable.vala |   14 +
 src/toolkit/toolkit-listbox-model.vala         |    2 +-
 src/toolkit/toolkit-stack-model.vala           |  331 ++++++++++++++++++++++++
 src/view/week/week-controller.vala             |   52 ++++-
 src/view/week/week.vala                        |    2 +
 6 files changed, 396 insertions(+), 6 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index 5442690..9a3aaa8 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -115,6 +115,7 @@ california_VALASOURCES = \
        toolkit/toolkit-listbox-model.vala \
        toolkit/toolkit-mutable-widget.vala \
        toolkit/toolkit-popup.vala \
+       toolkit/toolkit-stack-model.vala \
        \
        util/util-gfx.vala \
        util/util-memory.vala \
diff --git a/src/collection/collection-simple-iterable.vala b/src/collection/collection-simple-iterable.vala
index b880a6a..ad628dc 100644
--- a/src/collection/collection-simple-iterable.vala
+++ b/src/collection/collection-simple-iterable.vala
@@ -15,11 +15,25 @@ namespace California.Collection {
  * @see SimpleIterator
  */
 
+[GenericAccessors]
 public interface SimpleIterable<G> : BaseObject {
     /**
      * Returns a { link SimpleIterator} that can be used with Vala's foreach keyword.
      */
     public abstract SimpleIterator<G> iterator();
+    
+    /**
+     * Returns all the items in the { link SimpleIterable} as a single Gee.List.
+     */
+    public Gee.List<G> as_list(owned Gee.EqualDataFunc<G>? equal_func = null) {
+        Gee.List<G> list = new Gee.ArrayList<G>((owned) equal_func);
+        
+        SimpleIterator<G> iter = iterator();
+        while (iter.next())
+            list.add(iter.get());
+        
+        return list;
+    }
 }
 
 }
diff --git a/src/toolkit/toolkit-listbox-model.vala b/src/toolkit/toolkit-listbox-model.vala
index 7fc29a5..aca1582 100644
--- a/src/toolkit/toolkit-listbox-model.vala
+++ b/src/toolkit/toolkit-listbox-model.vala
@@ -182,7 +182,7 @@ public class ListBoxModel<G> : BaseObject {
             return false;
         
         if (remove_from_listbox)
-            listbox.remove(row);
+            row.destroy();
         
         removed(item);
         
diff --git a/src/toolkit/toolkit-stack-model.vala b/src/toolkit/toolkit-stack-model.vala
new file mode 100644
index 0000000..e3ab17a
--- /dev/null
+++ b/src/toolkit/toolkit-stack-model.vala
@@ -0,0 +1,331 @@
+/* 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.Toolkit {
+
+/**
+ * A caching read-ahead model for Gtk.Stack.
+ *
+ * StackModel allows for items of any type to be stored in sorted order and presented in a Gtk.Stack
+ * via presentation Gtk.Widgets generated by the caller for each item.  Gtk.Stack (and
+ * Gtk.Container) do not have a notion of ordering, so StackModel "fakes" a sense of ordering by
+ * configuring the Gtk.Stack prior to each transition to make it look like one presentation widget
+ * is spatially above/below or left/right to the widget being transitioned to.
+ *
+ * StackModel also caches presentation widgets.  A { link TrimPresentationFromCache} callback can
+ * be supplied to selectively remove widgets from the cache, while a
+ * { link EnsurePresentationInCache} callback can be supplied to enforce locality.
+ *
+ * If caching and read-ahead are used, the Gtk.Stack is probably not well-suited for a
+ * Gtk.StackSwitcher, since items may come and go at almost any time.  It's for this reason that
+ * { link ModelPresentation} returns an id but not a title for the widget.
+ *
+ * @see Deck
+ */
+
+public class StackModel<G> : BaseObject {
+    public const string PROP_STACK = "stack";
+    public const string PROP_VISIBLE_ITEM = "visible-item";
+    
+    /**
+     * Transition type for spatial transitions according to ordering.
+     */
+    public enum OrderedTransitionType {
+        CROSSFADE,
+        SLIDE_LEFT_RIGHT,
+        SLIDE_UP_DOWN;
+        
+        /**
+         * Returns the Gtk.StackTransitionType that matches the { link OrderedTransitionType} for
+         * the direction implied by the comparison result.
+         *
+         * Negative values are to the left or up, positive values are to the right or down.
+         * There is no direction for crossfading.  Zero means equal, returning NONE unless the
+         * ordered type is CROSSFADE.
+         */
+        public Gtk.StackTransitionType to_stack_transition(int compare) {
+            if (compare == 0)
+                return (this == CROSSFADE) ? Gtk.StackTransitionType.CROSSFADE : 
Gtk.StackTransitionType.NONE;
+            
+            switch (this) {
+                case CROSSFADE:
+                    return Gtk.StackTransitionType.CROSSFADE;
+                
+                case SLIDE_LEFT_RIGHT:
+                    return (compare < 0) ? Gtk.StackTransitionType.SLIDE_LEFT : 
Gtk.StackTransitionType.SLIDE_RIGHT;
+                
+                case SLIDE_UP_DOWN:
+                    return (compare < 0) ? Gtk.StackTransitionType.SLIDE_UP : 
Gtk.StackTransitionType.SLIDE_DOWN;
+                
+                default:
+                    assert_not_reached();
+            }
+        }
+    }
+    
+    /**
+     * Callback to convert the item into a child widget for the { link stack}.
+     *
+     * The callback may also return an identifier for the widget, which may be used to reference
+     * it later in the stack.  Note that { link StackModel} doesn't store or track this identifier.
+     */
+    public delegate Gtk.Widget ModelPresentation<G>(G item, out string? id);
+    
+    /**
+     * Callback for determining if the presentation Gtk.Widget for an item should be kept in the
+     * cache.
+     *
+     * Returns true if the widget associated with the item should be removed from the cache.
+     * visible_item indicates which item is currently being presented to the user.
+     */
+    public delegate bool TrimPresentationFromCache<G>(G item, G? visible_item);
+    
+    /**
+     * Callback for maintaining read-ahead presentation Gtk.Widgets in the cache.
+     *
+     * The caller should return a collection of items that should be introduced into the cache,
+     * if not already present.  Presentation widgets will be generated for the items to ensure
+     * they're ready for display.
+     *
+     * This is used as a read-ahead mechanism as well as a way for the caller to enforce cache
+     * locality.  It can be used, for example, to guarantee that certain items are always stored
+     * in the cache, such as a "home" page, as well as the next and previous ''n'' items.
+     *
+     * visible_item indicates which item is currently being presented to the user.
+     */
+    public delegate Gee.Collection<G>? EnsurePresentationInCache<G>(G? visible_item);
+    
+    /**
+     * The Gtk.Stack the { link StackModel} is backing.
+     */
+    public Gtk.Stack stack { get; private set; }
+    
+    /**
+     * The current visible item in the { link stack}.
+     */
+    public G? visible_item { get; private set; default = null; }
+    
+    private OrderedTransitionType ordered_transition_type;
+    private unowned ModelPresentation<G> model_presentation;
+    private unowned TrimPresentationFromCache<G>? trim_from_cache;
+    private unowned EnsurePresentationInCache<G>? ensure_in_cache;
+    private unowned CompareDataFunc<G>? comparator;
+    private Gee.HashMap<G, Gtk.Widget?> items;
+    private bool in_balance_cache = false;
+    private bool stack_destroyed = false;
+    
+    public StackModel(Gtk.Stack stack,
+        OrderedTransitionType ordered_transition_type,
+        ModelPresentation<G> model_presentation,
+        TrimPresentationFromCache<G>? trim_from_cache = null,
+        EnsurePresentationInCache<G>? ensure_in_cache = null,
+        CompareDataFunc<G>? comparator = null,
+        owned Gee.HashDataFunc<G>? hash_func = null,
+        owned Gee.EqualDataFunc<G>? equal_func = null) {
+        
+        this.stack = stack;
+        this.ordered_transition_type = ordered_transition_type;
+        this.model_presentation = model_presentation;
+        this.trim_from_cache = trim_from_cache;
+        this.ensure_in_cache = ensure_in_cache;
+        this.comparator = comparator;
+        
+        items = new Gee.HashMap<G, Gtk.Widget?>((owned) hash_func, (owned) equal_func);
+        
+        stack.remove.connect(on_stack_removed);
+        stack.notify["visible-child"].connect(on_stack_child_visible);
+        stack.destroy.connect(on_stack_destroyed);
+    }
+    
+    ~StackModel() {
+        stack.remove.disconnect(on_stack_removed);
+        stack.notify["visible-child"].disconnect(on_stack_child_visible);
+        stack.destroy.disconnect(on_stack_destroyed);
+    }
+    
+    /**
+     * Add the item to the { link StackModel}.
+     *
+     * This will not necessarily make the item visible (in particular, only if the { link stack}
+     * is already empty).  Use { link show_item} for that.
+     *
+     * Returns true if the item was added, false otherwise (already present).
+     */
+    public bool add(G item) {
+        if (items.has_key(item))
+            return false;
+        
+        items.set(item, null);
+        
+        // don't need to balance the cache; "visible-child" will do that automatically when
+        // show() is called
+        
+        return true;
+    }
+    
+    /**
+     * Removes the item from the { link StackModel}.
+     *
+     * If the item is already visible in the { link stack}, the Gtk.Stack will itself determine
+     * which widget will take its place.  If this is undesirable, call { link show} ''before''
+     * removing the item.
+     *
+     * Returns true if the item was removed, false otherwise (not present).
+     */
+    public bool remove(G item) {
+        Gtk.Widget? presentation;
+        if (!items.unset(item, out presentation))
+            return false;
+        
+        // remove from stack, let "removed" signal handler do the rest
+        if (presentation != null)
+            presentation.destroy();
+        
+        return true;
+    }
+    
+    /**
+     * Show the item using the specified transition.
+     *
+     * If the item was not already present in { link StackModel}, it will be added.
+     *
+     * @see add
+     */
+    public void show(G item) {
+        add(item);
+        
+        Gtk.Widget presentation = ensure_presentation_exists(item);
+        
+        if (visible_item == null) {
+            stack.transition_type = Gtk.StackTransitionType.NONE;
+        } else {
+            stack.transition_type = ordered_transition_type.to_stack_transition(
+                item_comparator(visible_item, item));
+        }
+        
+        stack.set_visible_child(presentation);
+    }
+    
+    private void on_stack_removed(Gtk.Widget child) {
+        // remove from cache, if present
+        bool found = false;
+        Gee.MapIterator<G, Gtk.Widget?> iter = items.map_iterator();
+        while (iter.next()) {
+            if (iter.get_value() == child) {
+                found = true;
+                iter.set_value(null);
+                
+                break;
+            }
+        }
+        
+        // only destroy widget if found (otherwise added externally from StackModel, so not ours
+        // to break)
+        if (found) {
+            child.destroy();
+            balance_cache("on_stack_removed");
+        }
+    }
+    
+    private void on_stack_child_visible() {
+        if (stack.visible_child == null) {
+            visible_item = null;
+            
+            return;
+        }
+        
+        // find item for widget ... obviously for larger stacks a reverse mapping (perhaps with
+        // get/set_data()) would be preferable, but this will do for now
+        Gee.MapIterator<G, Gtk.Widget?> iter = items.map_iterator();
+        while (iter.next()) {
+            if (iter.get_value() == stack.visible_child) {
+                visible_item = iter.get_key();
+                
+                balance_cache("on_stack_child_visible");
+                
+                return;
+            }
+        }
+        
+        // nothing found
+        visible_item = null;
+    }
+    
+    private void on_stack_destroyed() {
+        stack_destroyed = true;
+    }
+    
+    private Gtk.Widget ensure_presentation_exists(G item) {
+        Gtk.Widget? presentation = items.get(item);
+        if (presentation != null)
+            return presentation;
+        
+        // item -> presentation widget and identifier
+        string? id;
+        presentation = model_presentation(item, out id);
+        presentation.show_all();
+        
+        // mappings
+        items.set(item, presentation);
+        
+        // add to stack using identifier
+        if (id != null)
+            stack.add_named(presentation, id);
+        else
+            stack.add(presentation);
+        
+        return presentation;
+    }
+    
+    private void balance_cache(string why) {
+        // don't balance the cache if the stack is destroyed or if already balancing the cache
+        if (stack_destroyed || in_balance_cache)
+            return;
+        
+        debug("%s: balance cache: %s", to_string(), why);
+        in_balance_cache = true;
+        
+        // trim existing widgets from cache
+        if (trim_from_cache != null) {
+            Gee.MapIterator<G, Gtk.Widget?> iter = items.map_iterator();
+            while (iter.next()) {
+                Gtk.Widget? presentation = iter.get_value();
+                if (presentation != null && trim_from_cache(iter.get_key(), visible_item)) {
+                    // set_value before removing from stack to prevent our signal handler from
+                    // unsetting underneath us and causing iterator stamp problems
+                    iter.set_value(null);
+                    presentation.destroy();
+                }
+            }
+        }
+        
+        // read-ahead (add any widgets the user requires)
+        if (ensure_in_cache != null) {
+            Gee.Collection<G>? ensure_items = ensure_in_cache(visible_item);
+            if (ensure_items != null && ensure_items.size > 0) {
+                foreach (G ensure_item in ensure_items)
+                    ensure_presentation_exists(ensure_item);
+            }
+        }
+        
+        debug("%s: cache balanced: %s", to_string(), why);
+        in_balance_cache = false;
+    }
+    
+    private int item_comparator(G a, G b) {
+        if (comparator != null)
+            return comparator(a, b);
+        
+        return Gee.Functions.get_compare_func_for(typeof(G))(a, b);
+    }
+    
+    public override string to_string() {
+        return "StackModel (%d items)".printf(items.size);
+    }
+}
+
+}
+
diff --git a/src/view/week/week-controller.vala b/src/view/week/week-controller.vala
index e61d599..ae1666b 100644
--- a/src/view/week/week-controller.vala
+++ b/src/view/week/week-controller.vala
@@ -13,6 +13,8 @@ namespace California.View.Week {
 public class Controller : BaseObject, View.Controllable {
     public const string PROP_WEEK = "week";
     
+    private const int CACHE_NEIGHBORS_COUNT = 4;
+    
     private class MasterStack : Gtk.Stack, View.Container {
         private Controller _owner;
         public unowned View.Controllable owner { get { return _owner; } }
@@ -48,13 +50,18 @@ public class Controller : BaseObject, View.Controllable {
     public Calendar.FirstOfWeek first_of_week { get; set; }
     
     private MasterStack stack;
+    private Toolkit.StackModel<Calendar.Week> stack_model;
+    private Calendar.WeekSpan cache_span;
     
     public Controller() {
         stack = new MasterStack(this);
         stack.homogeneous = true;
-        stack.transition_type = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT;
         stack.transition_duration = 300;
         
+        stack_model = new Toolkit.StackModel<Calendar.Week>(stack,
+            Toolkit.StackModel.OrderedTransitionType.SLIDE_LEFT_RIGHT, model_presentation,
+            trim_presentation_from_cache, ensure_presentation_in_cache);
+        
         // set this before signal handlers are in place (week and first_of_week are very closely
         // tied in this view)
         first_of_week = Calendar.FirstOfWeek.SUNDAY;
@@ -103,6 +110,38 @@ public class Controller : BaseObject, View.Controllable {
     public void unselect_all() {
     }
     
+    private Gtk.Widget model_presentation(Calendar.Week week, out string? id) {
+        debug("Creating Grid for %s", week.to_string());
+        
+        Grid week_grid = new Grid(week);
+        id = week_grid.id;
+        
+        return week_grid;
+    }
+    
+    private bool trim_presentation_from_cache(Calendar.Week week, Calendar.Week? visible_week) {
+        // always keep today's week in cache
+        if (week.equal_to(Calendar.System.today.week_of(first_of_week)))
+            return false;
+        
+        debug("Trim %s: %s", week.to_string(), (!cache_span.has(week)).to_string());
+        
+        // otherwise only keep weeks that are in the current cache span
+        return !cache_span.has(week);
+    }
+    
+    private Gee.Collection<Calendar.Week>? ensure_presentation_in_cache(Calendar.Week? visible_week) {
+        // return current cache span as a collection
+        Gee.List<Calendar.Week> weeks = cache_span.as_list();
+        
+        // add today's week to the mix
+        weeks.add(Calendar.System.today.week_of(first_of_week));
+        
+        debug("ensuring %d weeks", weeks.size);
+        
+        return weeks;
+    }
+    
     private void on_first_of_week_changed() {
         // update week to reflect this change, but only if necessary
         if (first_of_week != week.first_of_week)
@@ -126,11 +165,14 @@ public class Controller : BaseObject, View.Controllable {
         
         is_viewing_today = Calendar.System.today in week;
         
-        Grid week_grid = new Grid(week);
-        week_grid.show_all();
+        // cache span is split between neighbors ahead and neighbors behind this week
+        Calendar.DateSpan cache_date_span = new Calendar.DateSpan(
+            week.adjust(0 - (CACHE_NEIGHBORS_COUNT / 2)).start_date,
+            week.adjust(CACHE_NEIGHBORS_COUNT / 2).end_date);
+        cache_span = new Calendar.WeekSpan(cache_date_span, week.first_of_week);
         
-        stack.add_named(week_grid, week_grid.id);
-        stack.set_visible_child(week_grid);
+        // show this week via the stack model (which implies adding it to the model)
+        stack_model.show(week);
     }
     
     public override string to_string() {
diff --git a/src/view/week/week.vala b/src/view/week/week.vala
index 519b147..3cf1252 100644
--- a/src/view/week/week.vala
+++ b/src/view/week/week.vala
@@ -20,12 +20,14 @@ public void init() throws Error {
     Calendar.init();
     Backing.init();
     Component.init();
+    Toolkit.init();
 }
 
 public void terminate() {
     if (!Unit.do_terminate(ref init_count))
         return;
     
+    Toolkit.terminate();
     Component.terminate();
     Backing.terminate();
     Calendar.terminate();


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