[california/wip/725767-week] Hook up Week.Controller to StackModel
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [california/wip/725767-week] Hook up Week.Controller to StackModel
- Date: Fri, 2 May 2014 23:03:30 +0000 (UTC)
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]