[egg-list-box/gmenumodel] Add a GMenuModel example program



commit 639141323951a10b98817e4f772966f7a3585d25
Author: Ryan Lortie <desrt desrt ca>
Date:   Tue Jun 4 15:07:09 2013 -0400

    Add a GMenuModel example program

 Makefile.am           |   18 +-
 gtk.vapi              |   45 +++
 gtkactionmuxer.c      |  778 ++++++++++++++++++++++++++++++++++++++++++++++++
 gtkactionmuxer.h      |   52 ++++
 gtkactionobservable.c |   78 +++++
 gtkactionobservable.h |   60 ++++
 gtkactionobserver.c   |  159 ++++++++++
 gtkactionobserver.h   |   83 ++++++
 gtkmenutracker.c      |  495 +++++++++++++++++++++++++++++++
 gtkmenutracker.h      |   52 ++++
 gtkmenutrackeritem.c  |  788 +++++++++++++++++++++++++++++++++++++++++++++++++
 gtkmenutrackeritem.h  |   84 ++++++
 menumodel.vala        |  155 ++++++++++
 13 files changed, 2846 insertions(+), 1 deletions(-)
---
diff --git a/Makefile.am b/Makefile.am
index 282ff54..9fcc9df 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -27,7 +27,23 @@ libeggflowbox_la_SOURCES = \
 
 libeggflowbox_la_LIBADD = $(LISTBOX_LIBS)
 
-noinst_PROGRAMS = test-list test-scrolled test-focus test-sel test-flow-box
+noinst_PROGRAMS = test-list test-scrolled test-focus test-sel test-flow-box menumodel
+
+menumodel_LDADD = $(LSITBOX_LIBS) libegglistbox.la
+menumodel_SOURCES = \
+       menumodel.vala          \
+       gtkactionobserver.c     \
+       gtkactionobserver.h     \
+       gtkactionobservable.c   \
+       gtkactionobservable.h   \
+       gtkactionmuxer.c        \
+       gtkactionmuxer.h        \
+       gtkmenutracker.c        \
+       gtkmenutracker.h        \
+       gtkmenutrackeritem.c    \
+       gtkmenutrackeritem.h    \
+       egglistbox.vapi         \
+       gtk.vapi
 
 test_sel_SOURCES = \
        test-sel.c \
diff --git a/gtk.vapi b/gtk.vapi
new file mode 100644
index 0000000..b37f40f
--- /dev/null
+++ b/gtk.vapi
@@ -0,0 +1,45 @@
+namespace Not {
+       [CCode (cheader_filename = "gtkactionobservable.h")]
+       public interface ActionObservable : GLib.ActionGroup {
+       }
+
+       [CCode (cheader_filename = "gtkactionobserver.h")]
+       public interface ActionObserver : GLib.Object {
+       }
+
+       [CCode (cheader_filename = "gtkactionmuxer.h")]
+       public class ActionMuxer : GLib.Object, GLib.ActionGroup, ActionObservable {
+               public ActionMuxer ();
+               public void insert (string group_name, GLib.ActionGroup? group);
+       }
+
+       [CCode (cheader_filename = "gtkmenutracker.h", free_function = "not_menu_tracker_free")]
+       [Compact]
+       public class MenuTracker {
+               public delegate void InsertFunc (MenuTrackerItem item, int position);
+               public delegate void RemoveFunc (int position);
+               public MenuTracker (ActionObservable observable, GLib.MenuModel model, bool with_separators, 
string? action_namespace, [CCode (delegate_target_pos = 6.9)] InsertFunc insert_func, [CCode 
(delegate_target_pos = 6.9)] RemoveFunc remove_func);
+               public MenuTracker.for_item_submenu (MenuTrackerItem item, [CCode (delegate_target_pos = 
3.9)] InsertFunc insert_func, [CCode (delegate_target_pos = 3.9)] RemoveFunc remove_func);
+       }
+
+       [CCode (cheader_filename = "gtkmenutrackeritem.h")]
+       public class MenuTrackerItem : GLib.Object {
+               public enum Role {
+                       NORMAL,
+                       CHECK,
+                       RADIO
+               }
+
+               public bool is_separator { get; }
+               public bool has_submenu { get; }
+               public string? label { get; }
+               public GLib.Icon? icon { get; }
+               public bool sensitive { get; }
+               public Role role { get; }
+               public bool toggled { get; }
+               public string? accel { get; }
+               public bool submenu_shown { get; }
+
+               void activated ();
+       }
+}
diff --git a/gtkactionmuxer.c b/gtkactionmuxer.c
new file mode 100644
index 0000000..599e257
--- /dev/null
+++ b/gtkactionmuxer.c
@@ -0,0 +1,778 @@
+/*
+ * Copyright © 2011 Canonical Limited
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the licence, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Ryan Lortie <desrt desrt ca>
+ */
+
+#include "config.h"
+
+#include "gtkactionmuxer.h"
+
+#include "gtkactionobservable.h"
+#include "gtkactionobserver.h"
+
+#include <string.h>
+
+/**
+ * SECTION:gtkactionmuxer
+ * @short_description: Aggregate and monitor several action groups
+ *
+ * #NotActionMuxer is a #GActionGroup and #NotActionObservable that is
+ * capable of containing other #GActionGroup instances.
+ *
+ * The typical use is aggregating all of the actions applicable to a
+ * particular context into a single action group, with namespacing.
+ *
+ * Consider the case of two action groups -- one containing actions
+ * applicable to an entire application (such as 'quit') and one
+ * containing actions applicable to a particular window in the
+ * application (such as 'fullscreen').
+ *
+ * In this case, each of these action groups could be added to a
+ * #NotActionMuxer with the prefixes "app" and "win", respectively.  This
+ * would expose the actions as "app.quit" and "win.fullscreen" on the
+ * #GActionGroup interface presented by the #NotActionMuxer.
+ *
+ * Activations and state change requests on the #NotActionMuxer are wired
+ * through to the underlying action group in the expected way.
+ *
+ * This class is typically only used at the site of "consumption" of
+ * actions (eg: when displaying a menu that contains many actions on
+ * different objects).
+ */
+
+static void     not_action_muxer_group_iface_init         (GActionGroupInterface        *iface);
+static void     not_action_muxer_observable_iface_init    (NotActionObservableInterface *iface);
+
+typedef GObjectClass NotActionMuxerClass;
+
+struct _NotActionMuxer
+{
+  GObject parent_instance;
+
+  GHashTable *observed_actions;
+  GHashTable *groups;
+  NotActionMuxer *parent;
+};
+
+G_DEFINE_TYPE_WITH_CODE (NotActionMuxer, not_action_muxer, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, not_action_muxer_group_iface_init)
+                         G_IMPLEMENT_INTERFACE (NOT_TYPE_ACTION_OBSERVABLE, 
not_action_muxer_observable_iface_init))
+
+enum
+{
+  PROP_0,
+  PROP_PARENT,
+  NUM_PROPERTIES
+};
+
+static GParamSpec *properties[NUM_PROPERTIES];
+
+typedef struct
+{
+  NotActionMuxer *muxer;
+  GSList       *watchers;
+  gchar        *fullname;
+} Action;
+
+typedef struct
+{
+  NotActionMuxer *muxer;
+  GActionGroup *group;
+  gchar        *prefix;
+  gulong        handler_ids[4];
+} Group;
+
+static void
+not_action_muxer_append_group_actions (gpointer key,
+                                       gpointer value,
+                                       gpointer user_data)
+{
+  const gchar *prefix = key;
+  Group *group = value;
+  GArray *actions = user_data;
+  gchar **group_actions;
+  gchar **action;
+
+  group_actions = g_action_group_list_actions (group->group);
+  for (action = group_actions; *action; action++)
+    {
+      gchar *fullname;
+
+      fullname = g_strconcat (prefix, ".", *action, NULL);
+      g_array_append_val (actions, fullname);
+    }
+
+  g_strfreev (group_actions);
+}
+
+static gchar **
+not_action_muxer_list_actions (GActionGroup *action_group)
+{
+  NotActionMuxer *muxer = NOT_ACTION_MUXER (action_group);
+  GArray *actions;
+
+  actions = g_array_new (TRUE, FALSE, sizeof (gchar *));
+
+  for ( ; muxer != NULL; muxer = muxer->parent)
+    {
+      g_hash_table_foreach (muxer->groups,
+                            not_action_muxer_append_group_actions,
+                            actions);
+    }
+
+  return (gchar **) g_array_free (actions, FALSE);
+}
+
+static Group *
+not_action_muxer_find_group (NotActionMuxer  *muxer,
+                             const gchar     *full_name,
+                             const gchar    **action_name)
+{
+  const gchar *dot;
+  gchar *prefix;
+  Group *group;
+
+  dot = strchr (full_name, '.');
+
+  if (!dot)
+    return NULL;
+
+  prefix = g_strndup (full_name, dot - full_name);
+  group = g_hash_table_lookup (muxer->groups, prefix);
+  g_free (prefix);
+
+  if (action_name)
+    *action_name = dot + 1;
+
+  return group;
+}
+
+static void
+not_action_muxer_action_enabled_changed (NotActionMuxer *muxer,
+                                         const gchar    *action_name,
+                                         gboolean        enabled)
+{
+  Action *action;
+  GSList *node;
+
+  action = g_hash_table_lookup (muxer->observed_actions, action_name);
+  for (node = action ? action->watchers : NULL; node; node = node->next)
+    gtk_action_observer_action_enabled_changed (node->data, NOT_ACTION_OBSERVABLE (muxer), action_name, 
enabled);
+  g_action_group_action_enabled_changed (G_ACTION_GROUP (muxer), action_name, enabled);
+}
+
+static void
+not_action_muxer_group_action_enabled_changed (GActionGroup *action_group,
+                                               const gchar  *action_name,
+                                               gboolean      enabled,
+                                               gpointer      user_data)
+{
+  Group *group = user_data;
+  gchar *fullname;
+
+  fullname = g_strconcat (group->prefix, ".", action_name, NULL);
+  not_action_muxer_action_enabled_changed (group->muxer, fullname, enabled);
+
+  g_free (fullname);
+}
+
+static void
+not_action_muxer_parent_action_enabled_changed (GActionGroup *action_group,
+                                                const gchar  *action_name,
+                                                gboolean      enabled,
+                                                gpointer      user_data)
+{
+  NotActionMuxer *muxer = user_data;
+
+  not_action_muxer_action_enabled_changed (muxer, action_name, enabled);
+}
+
+static void
+not_action_muxer_action_state_changed (NotActionMuxer *muxer,
+                                       const gchar    *action_name,
+                                       GVariant       *state)
+{
+  Action *action;
+  GSList *node;
+
+  action = g_hash_table_lookup (muxer->observed_actions, action_name);
+  for (node = action ? action->watchers : NULL; node; node = node->next)
+    gtk_action_observer_action_state_changed (node->data, NOT_ACTION_OBSERVABLE (muxer), action_name, state);
+  g_action_group_action_state_changed (G_ACTION_GROUP (muxer), action_name, state);
+}
+
+static void
+not_action_muxer_group_action_state_changed (GActionGroup *action_group,
+                                             const gchar  *action_name,
+                                             GVariant     *state,
+                                             gpointer      user_data)
+{
+  Group *group = user_data;
+  gchar *fullname;
+
+  fullname = g_strconcat (group->prefix, ".", action_name, NULL);
+  not_action_muxer_action_state_changed (group->muxer, fullname, state);
+
+  g_free (fullname);
+}
+
+static void
+not_action_muxer_parent_action_state_changed (GActionGroup *action_group,
+                                              const gchar  *action_name,
+                                              GVariant     *state,
+                                              gpointer      user_data)
+{
+  NotActionMuxer *muxer = user_data;
+
+  not_action_muxer_action_state_changed (muxer, action_name, state);
+}
+
+static void
+not_action_muxer_action_added (NotActionMuxer *muxer,
+                               const gchar    *action_name,
+                               GActionGroup   *original_group,
+                               const gchar    *orignal_action_name)
+{
+  const GVariantType *parameter_type;
+  gboolean enabled;
+  GVariant *state;
+  Action *action;
+
+  action = g_hash_table_lookup (muxer->observed_actions, action_name);
+
+  if (action && action->watchers &&
+      g_action_group_query_action (original_group, orignal_action_name,
+                                   &enabled, &parameter_type, NULL, NULL, &state))
+    {
+      GSList *node;
+
+      for (node = action->watchers; node; node = node->next)
+        gtk_action_observer_action_added (node->data,
+                                        NOT_ACTION_OBSERVABLE (muxer),
+                                        action_name, parameter_type, enabled, state);
+
+      if (state)
+        g_variant_unref (state);
+    }
+
+  g_action_group_action_added (G_ACTION_GROUP (muxer), action_name);
+}
+
+static void
+not_action_muxer_action_added_to_group (GActionGroup *action_group,
+                                        const gchar  *action_name,
+                                        gpointer      user_data)
+{
+  Group *group = user_data;
+  gchar *fullname;
+
+  fullname = g_strconcat (group->prefix, ".", action_name, NULL);
+  not_action_muxer_action_added (group->muxer, fullname, action_group, action_name);
+
+  g_free (fullname);
+}
+
+static void
+not_action_muxer_action_added_to_parent (GActionGroup *action_group,
+                                         const gchar  *action_name,
+                                         gpointer      user_data)
+{
+  NotActionMuxer *muxer = user_data;
+
+  not_action_muxer_action_added (muxer, action_name, action_group, action_name);
+}
+
+static void
+not_action_muxer_action_removed (NotActionMuxer *muxer,
+                                 const gchar    *action_name)
+{
+  Action *action;
+  GSList *node;
+
+  action = g_hash_table_lookup (muxer->observed_actions, action_name);
+  for (node = action ? action->watchers : NULL; node; node = node->next)
+    gtk_action_observer_action_removed (node->data, NOT_ACTION_OBSERVABLE (muxer), action_name);
+  g_action_group_action_removed (G_ACTION_GROUP (muxer), action_name);
+}
+
+static void
+not_action_muxer_action_removed_from_group (GActionGroup *action_group,
+                                            const gchar  *action_name,
+                                            gpointer      user_data)
+{
+  Group *group = user_data;
+  gchar *fullname;
+
+  fullname = g_strconcat (group->prefix, ".", action_name, NULL);
+  not_action_muxer_action_removed (group->muxer, fullname);
+
+  g_free (fullname);
+}
+
+static void
+not_action_muxer_action_removed_from_parent (GActionGroup *action_group,
+                                             const gchar  *action_name,
+                                             gpointer      user_data)
+{
+  NotActionMuxer *muxer = user_data;
+
+  not_action_muxer_action_removed (muxer, action_name);
+}
+
+static gboolean
+not_action_muxer_query_action (GActionGroup        *action_group,
+                               const gchar         *action_name,
+                               gboolean            *enabled,
+                               const GVariantType **parameter_type,
+                               const GVariantType **state_type,
+                               GVariant           **state_hint,
+                               GVariant           **state)
+{
+  NotActionMuxer *muxer = NOT_ACTION_MUXER (action_group);
+  Group *group;
+  const gchar *unprefixed_name;
+
+  group = not_action_muxer_find_group (muxer, action_name, &unprefixed_name);
+
+  if (group)
+    return g_action_group_query_action (group->group, unprefixed_name, enabled,
+                                        parameter_type, state_type, state_hint, state);
+
+  if (muxer->parent)
+    return g_action_group_query_action (G_ACTION_GROUP (muxer->parent), action_name,
+                                        enabled, parameter_type,
+                                        state_type, state_hint, state);
+
+  return FALSE;
+}
+
+static void
+not_action_muxer_activate_action (GActionGroup *action_group,
+                                  const gchar  *action_name,
+                                  GVariant     *parameter)
+{
+  NotActionMuxer *muxer = NOT_ACTION_MUXER (action_group);
+  Group *group;
+  const gchar *unprefixed_name;
+
+  group = not_action_muxer_find_group (muxer, action_name, &unprefixed_name);
+
+  if (group)
+    g_action_group_activate_action (group->group, unprefixed_name, parameter);
+  else if (muxer->parent)
+    g_action_group_activate_action (G_ACTION_GROUP (muxer->parent), action_name, parameter);
+}
+
+static void
+not_action_muxer_change_action_state (GActionGroup *action_group,
+                                      const gchar  *action_name,
+                                      GVariant     *state)
+{
+  NotActionMuxer *muxer = NOT_ACTION_MUXER (action_group);
+  Group *group;
+  const gchar *unprefixed_name;
+
+  group = not_action_muxer_find_group (muxer, action_name, &unprefixed_name);
+
+  if (group)
+    g_action_group_change_action_state (group->group, unprefixed_name, state);
+  else if (muxer->parent)
+    g_action_group_change_action_state (G_ACTION_GROUP (muxer->parent), action_name, state);
+}
+
+static void
+not_action_muxer_unregister_internal (Action   *action,
+                                      gpointer  observer)
+{
+  NotActionMuxer *muxer = action->muxer;
+  GSList **ptr;
+
+  for (ptr = &action->watchers; *ptr; ptr = &(*ptr)->next)
+    if ((*ptr)->data == observer)
+      {
+        *ptr = g_slist_remove (*ptr, observer);
+
+        if (action->watchers == NULL)
+            g_hash_table_remove (muxer->observed_actions, action->fullname);
+
+        break;
+      }
+}
+
+static void
+not_action_muxer_weak_notify (gpointer  data,
+                              GObject  *where_the_object_was)
+{
+  Action *action = data;
+
+  not_action_muxer_unregister_internal (action, where_the_object_was);
+}
+
+static void
+not_action_muxer_register_observer (NotActionObservable *observable,
+                                    const gchar         *name,
+                                    NotActionObserver   *observer)
+{
+  NotActionMuxer *muxer = NOT_ACTION_MUXER (observable);
+  Action *action;
+
+  action = g_hash_table_lookup (muxer->observed_actions, name);
+
+  if (action == NULL)
+    {
+      action = g_slice_new (Action);
+      action->muxer = muxer;
+      action->fullname = g_strdup (name);
+      action->watchers = NULL;
+
+      g_hash_table_insert (muxer->observed_actions, action->fullname, action);
+    }
+
+  action->watchers = g_slist_prepend (action->watchers, observer);
+  g_object_weak_ref (G_OBJECT (observer), not_action_muxer_weak_notify, action);
+}
+
+static void
+not_action_muxer_unregister_observer (NotActionObservable *observable,
+                                      const gchar         *name,
+                                      NotActionObserver   *observer)
+{
+  NotActionMuxer *muxer = NOT_ACTION_MUXER (observable);
+  Action *action;
+
+  action = g_hash_table_lookup (muxer->observed_actions, name);
+  g_object_weak_unref (G_OBJECT (observer), not_action_muxer_weak_notify, action);
+  not_action_muxer_unregister_internal (action, observer);
+}
+
+static void
+not_action_muxer_free_group (gpointer data)
+{
+  Group *group = data;
+  gint i;
+
+  /* 'for loop' or 'four loop'? */
+  for (i = 0; i < 4; i++)
+    g_signal_handler_disconnect (group->group, group->handler_ids[i]);
+
+  g_object_unref (group->group);
+  g_free (group->prefix);
+
+  g_slice_free (Group, group);
+}
+
+static void
+not_action_muxer_free_action (gpointer data)
+{
+  Action *action = data;
+  GSList *it;
+
+  for (it = action->watchers; it; it = it->next)
+    g_object_weak_unref (G_OBJECT (it->data), not_action_muxer_weak_notify, action);
+
+  g_slist_free (action->watchers);
+  g_free (action->fullname);
+
+  g_slice_free (Action, action);
+}
+
+static void
+not_action_muxer_finalize (GObject *object)
+{
+  NotActionMuxer *muxer = NOT_ACTION_MUXER (object);
+
+  g_assert_cmpint (g_hash_table_size (muxer->observed_actions), ==, 0);
+  g_hash_table_unref (muxer->observed_actions);
+  g_hash_table_unref (muxer->groups);
+
+  G_OBJECT_CLASS (not_action_muxer_parent_class)
+    ->finalize (object);
+}
+
+static void
+not_action_muxer_dispose (GObject *object)
+{
+  NotActionMuxer *muxer = NOT_ACTION_MUXER (object);
+
+  if (muxer->parent)
+  {
+    g_signal_handlers_disconnect_by_func (muxer->parent, not_action_muxer_action_added_to_parent, muxer);
+    g_signal_handlers_disconnect_by_func (muxer->parent, not_action_muxer_action_removed_from_parent, muxer);
+    g_signal_handlers_disconnect_by_func (muxer->parent, not_action_muxer_parent_action_enabled_changed, 
muxer);
+    g_signal_handlers_disconnect_by_func (muxer->parent, not_action_muxer_parent_action_state_changed, 
muxer);
+
+    g_clear_object (&muxer->parent);
+  }
+
+  g_hash_table_remove_all (muxer->observed_actions);
+
+  G_OBJECT_CLASS (not_action_muxer_parent_class)
+    ->dispose (object);
+}
+
+static void
+not_action_muxer_get_property (GObject    *object,
+                               guint       property_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  NotActionMuxer *muxer = NOT_ACTION_MUXER (object);
+
+  switch (property_id)
+    {
+    case PROP_PARENT:
+      g_value_set_object (value, not_action_muxer_get_parent (muxer));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    }
+}
+
+static void
+not_action_muxer_set_property (GObject      *object,
+                               guint         property_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  NotActionMuxer *muxer = NOT_ACTION_MUXER (object);
+
+  switch (property_id)
+    {
+    case PROP_PARENT:
+      not_action_muxer_set_parent (muxer, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    }
+}
+
+static void
+not_action_muxer_init (NotActionMuxer *muxer)
+{
+  muxer->observed_actions = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, 
not_action_muxer_free_action);
+  muxer->groups = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, not_action_muxer_free_group);
+}
+
+static void
+not_action_muxer_observable_iface_init (NotActionObservableInterface *iface)
+{
+  iface->register_observer = not_action_muxer_register_observer;
+  iface->unregister_observer = not_action_muxer_unregister_observer;
+}
+
+static void
+not_action_muxer_group_iface_init (GActionGroupInterface *iface)
+{
+  iface->list_actions = not_action_muxer_list_actions;
+  iface->query_action = not_action_muxer_query_action;
+  iface->activate_action = not_action_muxer_activate_action;
+  iface->change_action_state = not_action_muxer_change_action_state;
+}
+
+static void
+not_action_muxer_class_init (GObjectClass *class)
+{
+  class->get_property = not_action_muxer_get_property;
+  class->set_property = not_action_muxer_set_property;
+  class->finalize = not_action_muxer_finalize;
+  class->dispose = not_action_muxer_dispose;
+
+  properties[PROP_PARENT] = g_param_spec_object ("parent", "Parent",
+                                                 "The parent muxer",
+                                                 NOT_TYPE_ACTION_MUXER,
+                                                 G_PARAM_READWRITE |
+                                                 G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (class, NUM_PROPERTIES, properties);
+}
+
+/**
+ * not_action_muxer_insert:
+ * @muxer: a #NotActionMuxer
+ * @prefix: the prefix string for the action group
+ * @action_group: a #GActionGroup
+ *
+ * Adds the actions in @action_group to the list of actions provided by
+ * @muxer.  @prefix is prefixed to each action name, such that for each
+ * action <varname>x</varname> in @action_group, there is an equivalent
+ * action @prefix<literal>.</literal><varname>x</varname> in @muxer.
+ *
+ * For example, if @prefix is "<literal>app</literal>" and @action_group
+ * contains an action called "<literal>quit</literal>", then @muxer will
+ * now contain an action called "<literal>app.quit</literal>".
+ *
+ * If any #NotActionObservers are registered for actions in the group,
+ * "action_added" notifications will be emitted, as appropriate.
+ *
+ * @prefix must not contain a dot ('.').
+ */
+void
+not_action_muxer_insert (NotActionMuxer *muxer,
+                         const gchar    *prefix,
+                         GActionGroup   *action_group)
+{
+  gchar **actions;
+  Group *group;
+  gint i;
+
+  /* TODO: diff instead of ripout and replace */
+  not_action_muxer_remove (muxer, prefix);
+
+  group = g_slice_new (Group);
+  group->muxer = muxer;
+  group->group = g_object_ref (action_group);
+  group->prefix = g_strdup (prefix);
+
+  g_hash_table_insert (muxer->groups, group->prefix, group);
+
+  actions = g_action_group_list_actions (group->group);
+  for (i = 0; actions[i]; i++)
+    not_action_muxer_action_added_to_group (group->group, actions[i], group);
+  g_strfreev (actions);
+
+  group->handler_ids[0] = g_signal_connect (group->group, "action-added",
+                                            G_CALLBACK (not_action_muxer_action_added_to_group), group);
+  group->handler_ids[1] = g_signal_connect (group->group, "action-removed",
+                                            G_CALLBACK (not_action_muxer_action_removed_from_group), group);
+  group->handler_ids[2] = g_signal_connect (group->group, "action-enabled-changed",
+                                            G_CALLBACK (not_action_muxer_group_action_enabled_changed), 
group);
+  group->handler_ids[3] = g_signal_connect (group->group, "action-state-changed",
+                                            G_CALLBACK (not_action_muxer_group_action_state_changed), group);
+}
+
+/**
+ * not_action_muxer_remove:
+ * @muxer: a #NotActionMuxer
+ * @prefix: the prefix of the action group to remove
+ *
+ * Removes a #GActionGroup from the #NotActionMuxer.
+ *
+ * If any #NotActionObservers are registered for actions in the group,
+ * "action_removed" notifications will be emitted, as appropriate.
+ */
+void
+not_action_muxer_remove (NotActionMuxer *muxer,
+                         const gchar    *prefix)
+{
+  Group *group;
+
+  group = g_hash_table_lookup (muxer->groups, prefix);
+
+  if (group != NULL)
+    {
+      gchar **actions;
+      gint i;
+
+      g_hash_table_steal (muxer->groups, prefix);
+
+      actions = g_action_group_list_actions (group->group);
+      for (i = 0; actions[i]; i++)
+        not_action_muxer_action_removed_from_group (group->group, actions[i], group);
+      g_strfreev (actions);
+
+      not_action_muxer_free_group (group);
+    }
+}
+
+/**
+ * not_action_muxer_new:
+ *
+ * Creates a new #NotActionMuxer.
+ */
+NotActionMuxer *
+not_action_muxer_new (void)
+{
+  return g_object_new (NOT_TYPE_ACTION_MUXER, NULL);
+}
+
+/**
+ * not_action_muxer_get_parent:
+ * @muxer: a #NotActionMuxer
+ *
+ * Returns: (transfer none): the parent of @muxer, or NULL.
+ */
+NotActionMuxer *
+not_action_muxer_get_parent (NotActionMuxer *muxer)
+{
+  g_return_val_if_fail (NOT_IS_ACTION_MUXER (muxer), NULL);
+
+  return muxer->parent;
+}
+
+/**
+ * not_action_muxer_set_parent:
+ * @muxer: a #NotActionMuxer
+ * @parent: (allow-none): the new parent #NotActionMuxer
+ *
+ * Sets the parent of @muxer to @parent.
+ */
+void
+not_action_muxer_set_parent (NotActionMuxer *muxer,
+                             NotActionMuxer *parent)
+{
+  g_return_if_fail (NOT_IS_ACTION_MUXER (muxer));
+  g_return_if_fail (parent == NULL || NOT_IS_ACTION_MUXER (parent));
+
+  if (muxer->parent == parent)
+    return;
+
+  if (muxer->parent != NULL)
+    {
+      gchar **actions;
+      gchar **it;
+
+      actions = g_action_group_list_actions (G_ACTION_GROUP (muxer->parent));
+      for (it = actions; *it; it++)
+        not_action_muxer_action_removed (muxer, *it);
+      g_strfreev (actions);
+
+      g_signal_handlers_disconnect_by_func (muxer->parent, not_action_muxer_action_added_to_parent, muxer);
+      g_signal_handlers_disconnect_by_func (muxer->parent, not_action_muxer_action_removed_from_parent, 
muxer);
+      g_signal_handlers_disconnect_by_func (muxer->parent, not_action_muxer_parent_action_enabled_changed, 
muxer);
+      g_signal_handlers_disconnect_by_func (muxer->parent, not_action_muxer_parent_action_state_changed, 
muxer);
+
+      g_object_unref (muxer->parent);
+    }
+
+  muxer->parent = parent;
+
+  if (muxer->parent != NULL)
+    {
+      gchar **actions;
+      gchar **it;
+
+      g_object_ref (muxer->parent);
+
+      actions = g_action_group_list_actions (G_ACTION_GROUP (muxer->parent));
+      for (it = actions; *it; it++)
+        not_action_muxer_action_added (muxer, *it, G_ACTION_GROUP (muxer->parent), *it);
+      g_strfreev (actions);
+
+      g_signal_connect (muxer->parent, "action-added",
+                        G_CALLBACK (not_action_muxer_action_added_to_parent), muxer);
+      g_signal_connect (muxer->parent, "action-removed",
+                        G_CALLBACK (not_action_muxer_action_removed_from_parent), muxer);
+      g_signal_connect (muxer->parent, "action-enabled-changed",
+                        G_CALLBACK (not_action_muxer_parent_action_enabled_changed), muxer);
+      g_signal_connect (muxer->parent, "action-state-changed",
+                        G_CALLBACK (not_action_muxer_parent_action_state_changed), muxer);
+    }
+
+  g_object_notify_by_pspec (G_OBJECT (muxer), properties[PROP_PARENT]);
+}
diff --git a/gtkactionmuxer.h b/gtkactionmuxer.h
new file mode 100644
index 0000000..f02e906
--- /dev/null
+++ b/gtkactionmuxer.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright © 2011 Canonical Limited
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the licence, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Ryan Lortie <desrt desrt ca>
+ */
+
+#ifndef __NOT_ACTION_MUXER_H__
+#define __NOT_ACTION_MUXER_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define NOT_TYPE_ACTION_MUXER                               (not_action_muxer_get_type ())
+#define NOT_ACTION_MUXER(inst)                              (G_TYPE_CHECK_INSTANCE_CAST ((inst),             
        \
+                                                             NOT_TYPE_ACTION_MUXER, NotActionMuxer))
+#define NOT_IS_ACTION_MUXER(inst)                           (G_TYPE_CHECK_INSTANCE_TYPE ((inst),             
        \
+                                                             NOT_TYPE_ACTION_MUXER))
+
+typedef struct _NotActionMuxer                              NotActionMuxer;
+
+GType                   not_action_muxer_get_type                       (void);
+NotActionMuxer *        not_action_muxer_new                            (void);
+
+void                    not_action_muxer_insert                         (NotActionMuxer *muxer,
+                                                                         const gchar    *prefix,
+                                                                         GActionGroup   *action_group);
+
+void                    not_action_muxer_remove                         (NotActionMuxer *muxer,
+                                                                         const gchar    *prefix);
+
+NotActionMuxer *        not_action_muxer_get_parent                     (NotActionMuxer *muxer);
+
+void                    not_action_muxer_set_parent                     (NotActionMuxer *muxer,
+                                                                         NotActionMuxer *parent);
+
+G_END_DECLS
+
+#endif /* __NOT_ACTION_MUXER_H__ */
diff --git a/gtkactionobservable.c b/gtkactionobservable.c
new file mode 100644
index 0000000..264b116
--- /dev/null
+++ b/gtkactionobservable.c
@@ -0,0 +1,78 @@
+/*
+ * Copyright © 2011 Canonical Limited
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * licence or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Ryan Lortie <desrt desrt ca>
+ */
+
+#include "config.h"
+
+#include "gtkactionobservable.h"
+
+G_DEFINE_INTERFACE (NotActionObservable, gtk_action_observable, G_TYPE_OBJECT)
+
+/*
+ * SECTION:gtkactionobserable
+ * @short_description: an interface implemented by objects that report
+ *                     changes to actions
+ */
+
+void
+gtk_action_observable_default_init (NotActionObservableInterface *iface)
+{
+}
+
+/**
+ * gtk_action_observable_register_observer:
+ * @observable: a #NotActionObservable
+ * @action_name: the name of the action
+ * @observer: the #NotActionObserver to which the events will be reported
+ *
+ * Registers @observer as being interested in changes to @action_name on
+ * @observable.
+ */
+void
+gtk_action_observable_register_observer (NotActionObservable *observable,
+                                         const gchar         *action_name,
+                                         NotActionObserver   *observer)
+{
+  g_return_if_fail (NOT_IS_ACTION_OBSERVABLE (observable));
+
+  NOT_ACTION_OBSERVABLE_GET_IFACE (observable)
+    ->register_observer (observable, action_name, observer);
+}
+
+/**
+ * gtk_action_observable_unregister_observer:
+ * @observable: a #NotActionObservable
+ * @action_name: the name of the action
+ * @observer: the #NotActionObserver to which the events will be reported
+ *
+ * Removes the registration of @observer as being interested in changes
+ * to @action_name on @observable.
+ *
+ * If the observer was registered multiple times, it must be
+ * unregistered an equal number of times.
+ */
+void
+gtk_action_observable_unregister_observer (NotActionObservable *observable,
+                                           const gchar         *action_name,
+                                           NotActionObserver   *observer)
+{
+  g_return_if_fail (NOT_IS_ACTION_OBSERVABLE (observable));
+
+  NOT_ACTION_OBSERVABLE_GET_IFACE (observable)
+    ->unregister_observer (observable, action_name, observer);
+}
diff --git a/gtkactionobservable.h b/gtkactionobservable.h
new file mode 100644
index 0000000..dd0a311
--- /dev/null
+++ b/gtkactionobservable.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright © 2011 Canonical Limited
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * licence or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Ryan Lortie <desrt desrt ca>
+ */
+
+#ifndef __NOT_ACTION_OBSERVABLE_H__
+#define __NOT_ACTION_OBSERVABLE_H__
+
+#include "gtkactionobserver.h"
+
+G_BEGIN_DECLS
+
+#define NOT_TYPE_ACTION_OBSERVABLE                          (gtk_action_observable_get_type ())
+#define NOT_ACTION_OBSERVABLE(inst)                         (G_TYPE_CHECK_INSTANCE_CAST ((inst),             
        \
+                                                             NOT_TYPE_ACTION_OBSERVABLE, 
NotActionObservable))
+#define NOT_IS_ACTION_OBSERVABLE(inst)                      (G_TYPE_CHECK_INSTANCE_TYPE ((inst),             
        \
+                                                             NOT_TYPE_ACTION_OBSERVABLE))
+#define NOT_ACTION_OBSERVABLE_GET_IFACE(inst)               (G_TYPE_INSTANCE_GET_INTERFACE ((inst),          
        \
+                                                             NOT_TYPE_ACTION_OBSERVABLE,                     
        \
+                                                             NotActionObservableInterface))
+
+typedef struct _NotActionObservableInterface                NotActionObservableInterface;
+
+struct _NotActionObservableInterface
+{
+  GTypeInterface g_iface;
+
+  void (* register_observer)   (NotActionObservable *observable,
+                                const gchar         *action_name,
+                                NotActionObserver   *observer);
+  void (* unregister_observer) (NotActionObservable *observable,
+                                const gchar         *action_name,
+                                NotActionObserver   *observer);
+};
+
+GType                   gtk_action_observable_get_type                  (void);
+void                    gtk_action_observable_register_observer         (NotActionObservable *observable,
+                                                                         const gchar         *action_name,
+                                                                         NotActionObserver   *observer);
+void                    gtk_action_observable_unregister_observer       (NotActionObservable *observable,
+                                                                         const gchar         *action_name,
+                                                                         NotActionObserver   *observer);
+
+G_END_DECLS
+
+#endif /* __NOT_ACTION_OBSERVABLE_H__ */
diff --git a/gtkactionobserver.c b/gtkactionobserver.c
new file mode 100644
index 0000000..f40d95c
--- /dev/null
+++ b/gtkactionobserver.c
@@ -0,0 +1,159 @@
+/*
+ * Copyright © 2011 Canonical Limited
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * licence or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Ryan Lortie <desrt desrt ca>
+ */
+
+#include "config.h"
+
+#include "gtkactionobserver.h"
+
+G_DEFINE_INTERFACE (NotActionObserver, gtk_action_observer, G_TYPE_OBJECT)
+
+/**
+ * SECTION:gtkactionobserver
+ * @short_description: an interface implemented by objects that are
+ *                     interested in monitoring actions for changes
+ *
+ * NotActionObserver is a simple interface allowing objects that wish to
+ * be notified of changes to actions to be notified of those changes.
+ *
+ * It is also possible to monitor changes to action groups using
+ * #GObject signals, but there are a number of reasons that this
+ * approach could become problematic:
+ *
+ *  - there are four separate signals that must be manually connected
+ *    and disconnected
+ *
+ *  - when a large number of different observers wish to monitor a
+ *    (usually disjoint) set of actions within the same action group,
+ *    there is only one way to avoid having all notifications delivered
+ *    to all observers: signal detail.  In order to use signal detail,
+ *    each action name must be quarked, which is not always practical.
+ *
+ *  - even if quarking is acceptable, #GObject signal details are
+ *    implemented by scanning a linked list, so there is no real
+ *    decrease in complexity
+ */
+
+void
+gtk_action_observer_default_init (NotActionObserverInterface *class)
+{
+}
+
+/**
+ * gtk_action_observer_action_added:
+ * @observer: a #NotActionObserver
+ * @observable: the source of the event
+ * @action_name: the name of the action
+ * @enabled: %TRUE if the action is now enabled
+ * @parameter_type: the parameter type for action invocations, or %NULL
+ *                  if no parameter is required
+ * @state: the current state of the action, or %NULL if the action is
+ *         stateless
+ *
+ * This function is called when an action that the observer is
+ * registered to receive events for is added.
+ *
+ * This function should only be called by objects with which the
+ * observer has explicitly registered itself to receive events.
+ */
+void
+gtk_action_observer_action_added (NotActionObserver   *observer,
+                                  NotActionObservable *observable,
+                                  const gchar         *action_name,
+                                  const GVariantType  *parameter_type,
+                                  gboolean             enabled,
+                                  GVariant            *state)
+{
+  g_return_if_fail (NOT_IS_ACTION_OBSERVER (observer));
+
+  NOT_ACTION_OBSERVER_GET_IFACE (observer)
+    ->action_added (observer, observable, action_name, parameter_type, enabled, state);
+}
+
+/**
+ * gtk_action_observer_action_enabled_changed:
+ * @observer: a #NotActionObserver
+ * @observable: the source of the event
+ * @action_name: the name of the action
+ * @enabled: %TRUE if the action is now enabled
+ *
+ * This function is called when an action that the observer is
+ * registered to receive events for becomes enabled or disabled.
+ *
+ * This function should only be called by objects with which the
+ * observer has explicitly registered itself to receive events.
+ */
+void
+gtk_action_observer_action_enabled_changed (NotActionObserver   *observer,
+                                            NotActionObservable *observable,
+                                            const gchar         *action_name,
+                                            gboolean             enabled)
+{
+  g_return_if_fail (NOT_IS_ACTION_OBSERVER (observer));
+
+  NOT_ACTION_OBSERVER_GET_IFACE (observer)
+    ->action_enabled_changed (observer, observable, action_name, enabled);
+}
+
+/**
+ * gtk_action_observer_action_state_changed:
+ * @observer: a #NotActionObserver
+ * @observable: the source of the event
+ * @action_name: the name of the action
+ * @state: the new state of the action
+ *
+ * This function is called when an action that the observer is
+ * registered to receive events for changes to its state.
+ *
+ * This function should only be called by objects with which the
+ * observer has explicitly registered itself to receive events.
+ */
+void
+gtk_action_observer_action_state_changed (NotActionObserver   *observer,
+                                          NotActionObservable *observable,
+                                          const gchar         *action_name,
+                                          GVariant            *state)
+{
+  g_return_if_fail (NOT_IS_ACTION_OBSERVER (observer));
+
+  NOT_ACTION_OBSERVER_GET_IFACE (observer)
+    ->action_state_changed (observer, observable, action_name, state);
+}
+
+/**
+ * gtk_action_observer_action_removed:
+ * @observer: a #NotActionObserver
+ * @observable: the source of the event
+ * @action_name: the name of the action
+ *
+ * This function is called when an action that the observer is
+ * registered to receive events for is removed.
+ *
+ * This function should only be called by objects with which the
+ * observer has explicitly registered itself to receive events.
+ */
+void
+gtk_action_observer_action_removed (NotActionObserver   *observer,
+                                    NotActionObservable *observable,
+                                    const gchar         *action_name)
+{
+  g_return_if_fail (NOT_IS_ACTION_OBSERVER (observer));
+
+  NOT_ACTION_OBSERVER_GET_IFACE (observer)
+    ->action_removed (observer, observable, action_name);
+}
diff --git a/gtkactionobserver.h b/gtkactionobserver.h
new file mode 100644
index 0000000..e2b60fa
--- /dev/null
+++ b/gtkactionobserver.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright © 2011 Canonical Limited
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * licence or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Ryan Lortie <desrt desrt ca>
+ */
+
+#ifndef __NOT_ACTION_OBSERVER_H__
+#define __NOT_ACTION_OBSERVER_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define NOT_TYPE_ACTION_OBSERVER                            (gtk_action_observer_get_type ())
+#define NOT_ACTION_OBSERVER(inst)                           (G_TYPE_CHECK_INSTANCE_CAST ((inst),             
        \
+                                                             NOT_TYPE_ACTION_OBSERVER, NotActionObserver))
+#define NOT_IS_ACTION_OBSERVER(inst)                        (G_TYPE_CHECK_INSTANCE_TYPE ((inst),             
        \
+                                                             NOT_TYPE_ACTION_OBSERVER))
+#define NOT_ACTION_OBSERVER_GET_IFACE(inst)                 (G_TYPE_INSTANCE_GET_INTERFACE ((inst),          
        \
+                                                             NOT_TYPE_ACTION_OBSERVER, 
NotActionObserverInterface))
+
+typedef struct _NotActionObserverInterface                  NotActionObserverInterface;
+typedef struct _NotActionObservable                         NotActionObservable;
+typedef struct _NotActionObserver                           NotActionObserver;
+
+struct _NotActionObserverInterface
+{
+  GTypeInterface g_iface;
+
+  void (* action_added)           (NotActionObserver    *observer,
+                                   NotActionObservable  *observable,
+                                   const gchar          *action_name,
+                                   const GVariantType   *parameter_type,
+                                   gboolean              enabled,
+                                   GVariant             *state);
+  void (* action_enabled_changed) (NotActionObserver    *observer,
+                                   NotActionObservable  *observable,
+                                   const gchar          *action_name,
+                                   gboolean              enabled);
+  void (* action_state_changed)   (NotActionObserver    *observer,
+                                   NotActionObservable  *observable,
+                                   const gchar          *action_name,
+                                   GVariant             *state);
+  void (* action_removed)         (NotActionObserver    *observer,
+                                   NotActionObservable  *observable,
+                                   const gchar          *action_name);
+};
+
+GType                   gtk_action_observer_get_type                    (void);
+void                    gtk_action_observer_action_added                (NotActionObserver   *observer,
+                                                                         NotActionObservable *observable,
+                                                                         const gchar         *action_name,
+                                                                         const GVariantType  *parameter_type,
+                                                                         gboolean             enabled,
+                                                                         GVariant            *state);
+void                    gtk_action_observer_action_enabled_changed      (NotActionObserver   *observer,
+                                                                         NotActionObservable *observable,
+                                                                         const gchar         *action_name,
+                                                                         gboolean             enabled);
+void                    gtk_action_observer_action_state_changed        (NotActionObserver   *observer,
+                                                                         NotActionObservable *observable,
+                                                                         const gchar         *action_name,
+                                                                         GVariant            *state);
+void                    gtk_action_observer_action_removed              (NotActionObserver   *observer,
+                                                                         NotActionObservable *observable,
+                                                                         const gchar         *action_name);
+
+G_END_DECLS
+
+#endif /* __NOT_ACTION_OBSERVER_H__ */
diff --git a/gtkmenutracker.c b/gtkmenutracker.c
new file mode 100644
index 0000000..1c2bfc8
--- /dev/null
+++ b/gtkmenutracker.c
@@ -0,0 +1,495 @@
+/*
+ * Copyright © 2013 Canonical Limited
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the licence, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: Ryan Lortie <desrt desrt ca>
+ */
+
+#include "config.h"
+
+#include "gtkmenutracker.h"
+
+/**
+ * SECTION:gtkmenutracker
+ * @Title: NotMenuTracker
+ * @Short_description: A helper class for interpreting #GMenuModel
+ *
+ * #NotMenuTracker is a simple object to ease implementations of #GMenuModel.
+ * Given a #NotActionObservable (usually a #GActionMuxer) along with a
+ * #GMenuModel, it will tell you which menu items to create and where to place
+ * them. If a menu item is removed, it will tell you the position of the menu
+ * item to remove.
+ *
+ * Using #NotMenuTracker is fairly simple. The only guarantee you must make
+ * to #NotMenuTracker is that you must obey all insert signals and track the
+ * position of items that #NotMenuTracker gives you. That is, #NotMenuTracker
+ * expects positions of all the latter items to change when it calls your
+ * insertion callback with an early position, as it may ask you to remove
+ * an item with a readjusted position later.
+ *
+ * #NotMenuTracker will give you a #NotMenuTrackerItem in your callback. You
+ * must hold onto this object until a remove signal is emitted. This item
+ * represents a single menu item, which can be one of three classes: normal item,
+ * separator, or submenu.
+ *
+ * Certain properties on the #NotMenuTrackerItem are mutable, and you must
+ * listen for changes in the item. For more details, see the documentation
+ * for #NotMenuTrackerItem along with https://live.gnome.org/GApplication/GMenuModel.
+ *
+ * The idea of @with_separators is for special cases where menu models may
+ * be tracked in places where separators are not available, like in toplevel
+ * "File", "Edit" menu bars. Ignoring separator items is wrong, as #NotMenuTracker
+ * expects the position to change, so we must tell #NotMenuTracker to ignore
+ * separators itself.
+ */
+
+typedef struct _NotMenuTrackerSection NotMenuTrackerSection;
+
+struct _NotMenuTracker
+{
+  NotActionObservable      *observable;
+  NotMenuTrackerInsertFunc  insert_func;
+  NotMenuTrackerRemoveFunc  remove_func;
+  gpointer                  user_data;
+
+  NotMenuTrackerSection    *toplevel;
+};
+
+struct _NotMenuTrackerSection
+{
+  GMenuModel *model;
+  GSList     *items;
+  gchar      *action_namespace;
+
+  guint       with_separators : 1;
+  guint       has_separator   : 1;
+
+  gulong      handler;
+};
+
+static NotMenuTrackerSection *  not_menu_tracker_section_new    (NotMenuTracker        *tracker,
+                                                                 GMenuModel            *model,
+                                                                 gboolean               with_separators,
+                                                                 gint                   offset,
+                                                                 const gchar           *action_namespace);
+static void                    not_menu_tracker_section_free    (NotMenuTrackerSection *section);
+
+static NotMenuTrackerSection *
+not_menu_tracker_section_find_model (NotMenuTrackerSection *section,
+                                     GMenuModel            *model,
+                                     gint                  *offset)
+{
+  GSList *item;
+
+  if (section->has_separator)
+    (*offset)++;
+
+  if (section->model == model)
+    return section;
+
+  for (item = section->items; item; item = item->next)
+    {
+      NotMenuTrackerSection *subsection = item->data;
+
+      if (subsection)
+        {
+          NotMenuTrackerSection *found_section;
+
+          found_section = not_menu_tracker_section_find_model (subsection, model, offset);
+
+          if (found_section)
+            return found_section;
+        }
+      else
+        (*offset)++;
+    }
+
+  return FALSE;
+}
+
+/* this is responsible for syncing the showing of a separator for a
+ * single subsection (and its children).
+ *
+ * we only ever show separators if we have _actual_ children (ie: we do
+ * not show a separator if the section contains only empty child
+ * sections).  it's difficult to determine this on-the-fly, so we have
+ * this separate function to come back later and figure it out.
+ *
+ * 'section' is that section.
+ *
+ * 'tracker' is passed in so that we can emit callbacks when we decide
+ * to add/remove separators.
+ *
+ * 'offset' is passed in so we know which position to emit in our
+ * callbacks.  ie: if we add a separator right at the top of this
+ * section then we would emit it with this offset.  deeper inside, we
+ * adjust accordingly.
+ *
+ * could_have_separator is true in two situations:
+ *
+ *  - our parent section had with_separators defined and we are not the
+ *    first section (ie: we should add a separator if we have content in
+ *    order to divide us from the items above)
+ *
+ *  - if we had a 'label' attribute set for this section
+ *
+ * parent_model and parent_index are passed in so that we can give them
+ * to the insertion callback so that it can see the label (and anything
+ * else that happens to be defined on the section).
+ *
+ * we iterate each item in ourselves.  for subsections, we recursively
+ * run ourselves to sync separators.  after we are done, we notice if we
+ * have any items in us or if we are completely empty and sync if our
+ * separator is shown or not.
+ */
+static gint
+not_menu_tracker_section_sync_separators (NotMenuTrackerSection *section,
+                                          NotMenuTracker        *tracker,
+                                          gint                   offset,
+                                          gboolean               could_have_separator,
+                                          GMenuModel            *parent_model,
+                                          gint                   parent_index)
+{
+  gboolean should_have_separator;
+  gint n_items = 0;
+  GSList *item;
+  gint i = 0;
+
+  for (item = section->items; item; item = item->next)
+    {
+      NotMenuTrackerSection *subsection = item->data;
+
+      if (subsection)
+        {
+          gboolean could_have_separator;
+
+          could_have_separator = (section->with_separators && i > 0) ||
+                                 g_menu_model_get_item_attribute (section->model, i, "label", "s", NULL);
+
+          n_items += not_menu_tracker_section_sync_separators (subsection, tracker, offset + n_items,
+                                                               could_have_separator, section->model, i);
+        }
+      else
+        n_items++;
+
+      i++;
+    }
+
+  should_have_separator = could_have_separator && n_items != 0;
+
+  if (should_have_separator > section->has_separator)
+    {
+      /* Add a separator */
+      NotMenuTrackerItem *item;
+
+      item = _not_menu_tracker_item_new (tracker->observable, parent_model, parent_index, NULL, TRUE);
+      (* tracker->insert_func) (item, offset, tracker->user_data);
+      g_object_unref (item);
+
+      section->has_separator = TRUE;
+    }
+  else if (should_have_separator < section->has_separator)
+    {
+      /* Remove a separator */
+      (* tracker->remove_func) (offset, tracker->user_data);
+      section->has_separator = FALSE;
+    }
+
+  n_items += section->has_separator;
+
+  return n_items;
+}
+
+static gint
+not_menu_tracker_section_measure (NotMenuTrackerSection *section)
+{
+  GSList *item;
+  gint n_items;
+
+  if (section == NULL)
+    return 1;
+
+  n_items = 0;
+
+  if (section->has_separator)
+    n_items++;
+
+  for (item = section->items; item; item = item->next)
+    n_items += not_menu_tracker_section_measure (item->data);
+
+  return n_items;
+}
+
+static void
+not_menu_tracker_remove_items (NotMenuTracker  *tracker,
+                               GSList         **change_point,
+                               gint             offset,
+                               gint             n_items)
+{
+  gint i;
+
+  for (i = 0; i < n_items; i++)
+    {
+      NotMenuTrackerSection *subsection;
+      gint n;
+
+      subsection = (*change_point)->data;
+      *change_point = g_slist_delete_link (*change_point, *change_point);
+
+      n = not_menu_tracker_section_measure (subsection);
+      not_menu_tracker_section_free (subsection);
+
+      while (n--)
+        (* tracker->remove_func) (offset, tracker->user_data);
+    }
+}
+
+static void
+not_menu_tracker_add_items (NotMenuTracker         *tracker,
+                            NotMenuTrackerSection  *section,
+                            GSList                **change_point,
+                            gint                    offset,
+                            GMenuModel             *model,
+                            gint                    position,
+                            gint                    n_items)
+{
+  while (n_items--)
+    {
+      GMenuModel *submenu;
+
+      submenu = g_menu_model_get_item_link (model, position + n_items, G_MENU_LINK_SECTION);
+      g_assert (submenu != model);
+      if (submenu != NULL)
+        {
+          NotMenuTrackerSection *subsection;
+          gchar *action_namespace = NULL;
+
+          g_menu_model_get_item_attribute (model, position + n_items,
+                                           G_MENU_ATTRIBUTE_ACTION_NAMESPACE, "s", &action_namespace);
+
+          if (section->action_namespace)
+            {
+              gchar *namespace;
+
+              namespace = g_strjoin (".", section->action_namespace, action_namespace, NULL);
+              subsection = not_menu_tracker_section_new (tracker, submenu, FALSE, offset, namespace);
+              g_free (namespace);
+            }
+          else
+            subsection = not_menu_tracker_section_new (tracker, submenu, FALSE, offset, 
section->action_namespace);
+
+          *change_point = g_slist_prepend (*change_point, subsection);
+          g_free (action_namespace);
+          g_object_unref (submenu);
+        }
+      else
+        {
+          NotMenuTrackerItem *item;
+
+          item = _not_menu_tracker_item_new (tracker->observable, model, position + n_items,
+                                             section->action_namespace, FALSE);
+          (* tracker->insert_func) (item, offset, tracker->user_data);
+          g_object_unref (item);
+
+          *change_point = g_slist_prepend (*change_point, NULL);
+        }
+    }
+}
+
+static void
+not_menu_tracker_model_changed (GMenuModel *model,
+                                gint        position,
+                                gint        removed,
+                                gint        added,
+                                gpointer    user_data)
+{
+  NotMenuTracker *tracker = user_data;
+  NotMenuTrackerSection *section;
+  GSList **change_point;
+  gint offset = 0;
+  gint i;
+
+  /* First find which section the changed model corresponds to, and the
+   * position of that section within the overall menu.
+   */
+  section = not_menu_tracker_section_find_model (tracker->toplevel, model, &offset);
+
+  /* Next, seek through that section to the change point.  This gives us
+   * the correct GSList** to make the change to and also finds the final
+   * offset at which we will make the changes (by measuring the number
+   * of items within each item of the section before the change point).
+   */
+  change_point = &section->items;
+  for (i = 0; i < position; i++)
+    {
+      offset += not_menu_tracker_section_measure ((*change_point)->data);
+      change_point = &(*change_point)->next;
+    }
+
+  /* We remove items in order and add items in reverse order.  This
+   * means that the offset used for all inserts and removes caused by a
+   * single change will be the same.
+   *
+   * This also has a performance advantage: NotMenuShell stores the
+   * menu items in a linked list.  In the case where we are creating a
+   * menu for the first time, adding the items in reverse order means
+   * that we only ever insert at index zero, prepending the list.  This
+   * means that we can populate in O(n) time instead of O(n^2) that we
+   * would do by appending.
+   */
+  not_menu_tracker_remove_items (tracker, change_point, offset, removed);
+  not_menu_tracker_add_items (tracker, section, change_point, offset, model, position, added);
+
+  /* The offsets for insertion/removal of separators will be all over
+   * the place, however...
+   */
+  not_menu_tracker_section_sync_separators (tracker->toplevel, tracker, 0, FALSE, NULL, 0);
+}
+
+static void
+not_menu_tracker_section_free (NotMenuTrackerSection *section)
+{
+  if (section == NULL)
+    return;
+
+  g_signal_handler_disconnect (section->model, section->handler);
+  g_slist_free_full (section->items, (GDestroyNotify) not_menu_tracker_section_free);
+  g_free (section->action_namespace);
+  g_object_unref (section->model);
+  g_slice_free (NotMenuTrackerSection, section);
+}
+
+static NotMenuTrackerSection *
+not_menu_tracker_section_new (NotMenuTracker *tracker,
+                              GMenuModel     *model,
+                              gboolean        with_separators,
+                              gint            offset,
+                              const gchar    *action_namespace)
+{
+  NotMenuTrackerSection *section;
+
+  section = g_slice_new0 (NotMenuTrackerSection);
+  section->model = g_object_ref (model);
+  section->with_separators = with_separators;
+  section->action_namespace = g_strdup (action_namespace);
+
+  not_menu_tracker_add_items (tracker, section, &section->items, offset, model, 0, g_menu_model_get_n_items 
(model));
+  section->handler = g_signal_connect (model, "items-changed", G_CALLBACK (not_menu_tracker_model_changed), 
tracker);
+
+  return section;
+}
+
+/*< private >
+ * not_menu_tracker_new:
+ * @model: the model to flatten
+ * @with_separators: if the toplevel should have separators (ie: TRUE
+ *   for menus, FALSE for menubars)
+ * @action_namespace: the passed-in action namespace
+ * @insert_func: insert callback
+ * @remove_func: remove callback
+ * @user_data user data for callbacks
+ *
+ * Creates a NotMenuTracker for @model, holding a ref on @model for as
+ * long as the tracker is alive.
+ *
+ * This flattens out the model, merging sections and inserting
+ * separators where appropriate.  It monitors for changes and performs
+ * updates on the fly.  It also handles action_namespace for subsections
+ * (but you will need to handle it yourself for submenus).
+ *
+ * When the tracker is first created, @insert_func will be called many
+ * times to populate the menu with the initial contents of @model
+ * (unless it is empty), before not_menu_tracker_new() returns.  For
+ * this reason, the menu that is using the tracker ought to be empty
+ * when it creates the tracker.
+ *
+ * Future changes to @model will result in more calls to @insert_func
+ * and @remove_func.
+ *
+ * The position argument to both functions is the linear 0-based
+ * position in the menu at which the item in question should be inserted
+ * or removed.
+ *
+ * For @insert_func, @model and @item_index are used to get the
+ * information about the menu item to insert.  @action_namespace is the
+ * action namespace that actions referred to from that item should place
+ * themselves in.  Note that if the item is a submenu and the
+ * "action-namespace" attribute is defined on the item, it will _not_ be
+ * applied to the @action_namespace argument as it is meant for the
+ * items inside of the submenu, not the submenu item itself.
+ *
+ * @is_separator is set to %TRUE in case the item being added is a
+ * separator.  @model and @item_index will still be meaningfully set in
+ * this case -- to the section menu item corresponding to the separator.
+ * This is useful if the section specifies a label, for example.  If
+ * there is an "action-namespace" attribute on this menu item then it
+ * should be ignored by the consumer because #NotMenuTracker has already
+ * handled it.
+ *
+ * When using #NotMenuTracker there is no need to hold onto @model or
+ * monitor it for changes.  The model will be unreffed when
+ * not_menu_tracker_free() is called.
+ */
+NotMenuTracker *
+not_menu_tracker_new (NotActionObservable      *observable,
+                      GMenuModel               *model,
+                      gboolean                  with_separators,
+                      const gchar              *action_namespace,
+                      NotMenuTrackerInsertFunc  insert_func,
+                      NotMenuTrackerRemoveFunc  remove_func,
+                      gpointer                  user_data)
+{
+  NotMenuTracker *tracker;
+
+  tracker = g_slice_new (NotMenuTracker);
+  tracker->observable = g_object_ref (observable);
+  tracker->insert_func = insert_func;
+  tracker->remove_func = remove_func;
+  tracker->user_data = user_data;
+
+  tracker->toplevel = not_menu_tracker_section_new (tracker, model, with_separators, 0, action_namespace);
+  not_menu_tracker_section_sync_separators (tracker->toplevel, tracker, 0, FALSE, NULL, 0);
+
+  return tracker;
+}
+
+NotMenuTracker *
+not_menu_tracker_new_for_item_submenu (NotMenuTrackerItem       *item,
+                                       NotMenuTrackerInsertFunc  insert_func,
+                                       NotMenuTrackerRemoveFunc  remove_func,
+                                       gpointer                  user_data)
+{
+  return not_menu_tracker_new (_not_menu_tracker_item_get_observable (item),
+                               _not_menu_tracker_item_get_submenu (item),
+                               TRUE,
+                               _not_menu_tracker_item_get_submenu_namespace (item),
+                               insert_func, remove_func, user_data);
+}
+
+/*< private >
+ * not_menu_tracker_free:
+ * @tracker: a #NotMenuTracker
+ *
+ * Frees the tracker, ...
+ */
+void
+not_menu_tracker_free (NotMenuTracker *tracker)
+{
+  not_menu_tracker_section_free (tracker->toplevel);
+  g_object_unref (tracker->observable);
+  g_slice_free (NotMenuTracker, tracker);
+}
diff --git a/gtkmenutracker.h b/gtkmenutracker.h
new file mode 100644
index 0000000..e7763ea
--- /dev/null
+++ b/gtkmenutracker.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright © 2013 Canonical Limited
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the licence, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: Ryan Lortie <desrt desrt ca>
+ */
+
+#ifndef __NOT_MENU_TRACKER_H__
+#define __NOT_MENU_TRACKER_H__
+
+#include "gtkmenutrackeritem.h"
+
+typedef struct _NotMenuTracker NotMenuTracker;
+
+typedef void         (* NotMenuTrackerInsertFunc)                       (NotMenuTrackerItem       *item,
+                                                                         gint                      position,
+                                                                         gpointer                  
user_data);
+
+typedef void         (* NotMenuTrackerRemoveFunc)                       (gint                      position,
+                                                                         gpointer                  
user_data);
+
+
+NotMenuTracker *        not_menu_tracker_new                            (NotActionObservable      *observer,
+                                                                         GMenuModel               *model,
+                                                                         gboolean                  
with_separators,
+                                                                         const gchar              
*action_namespace,
+                                                                         NotMenuTrackerInsertFunc  
insert_func,
+                                                                         NotMenuTrackerRemoveFunc  
remove_func,
+                                                                         gpointer                  
user_data);
+
+NotMenuTracker *        not_menu_tracker_new_for_item_submenu           (NotMenuTrackerItem       *item,
+                                                                         NotMenuTrackerInsertFunc  
insert_func,
+                                                                         NotMenuTrackerRemoveFunc  
remove_func,
+                                                                         gpointer                  
user_data);
+
+void                    not_menu_tracker_free                           (NotMenuTracker           *tracker);
+
+#endif /* __NOT_MENU_TRACKER_H__ */
diff --git a/gtkmenutrackeritem.c b/gtkmenutrackeritem.c
new file mode 100644
index 0000000..01ddd1e
--- /dev/null
+++ b/gtkmenutrackeritem.c
@@ -0,0 +1,788 @@
+/*
+ * Copyright © 2013 Canonical Limited
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the licence, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: Ryan Lortie <desrt desrt ca>
+ */
+
+#include "config.h"
+
+#include "gtkmenutrackeritem.h"
+
+/**
+ * SECTION:gtkmenutrackeritem
+ * @Title: NotMenuTrackerItem
+ * @Short_description: Small helper for model menu items
+ *
+ * A #NotMenuTrackerItem is a small helper class used by #NotMenuTracker to
+ * represent menu items. It has one of three classes: normal item, separator,
+ * or submenu.
+ *
+ * If an item is one of the non-normal classes (submenu, separator), only the
+ * label of the item needs to be respected. Otherwise, all the properties
+ * of the item contribute to the item's appearance and state.
+ *
+ * Implementing the appearance of the menu item is up to toolkits, and certain
+ * toolkits may choose to ignore certain properties, like icon or accel. The
+ * role of the item determines its accessibility role, along with its
+ * decoration if the NotMenuTrackerItem::toggled property is true. As an
+ * example, if the item has the role %NOT_MENU_TRACKER_ITEM_ROLE_CHECK and
+ * NotMenuTrackerItem::toggled is %FALSE, its accessible role should be that of
+ * a check menu item, and no decoration should be drawn. But if
+ * NotMenuTrackerItem::toggled is %TRUE, a checkmark should be drawn.
+ *
+ * All properties except for the two class-determining properties,
+ * NotMenuTrackerItem::is-separator and NotMenuTrackerItem::has-submenu are
+ * allowed to change, so listen to the notify signals to update your item's
+ * appearance. When using a GObject library, this can conveniently be done
+ * with g_object_bind_property() and #GBinding, and this is how this is
+ * implemented in GTK+; the appearance side is implemented in #GtkModelMenuItem.
+ *
+ * When an item is clicked, simply call not_menu_tracker_item_activated() in
+ * response. The #NotMenuTrackerItem will take care of everything related to
+ * activating the item and will itself update the state of all items in
+ * response.
+ *
+ * Submenus are a special case of menu item. When an item is a submenu, you
+ * should create a submenu for it with not_menu_tracker_new_item_for_submenu(),
+ * and apply the same menu tracking logic you would for a toplevel menu.
+ * Applications using submenus may want to lazily build their submenus in
+ * response to the user clicking on it, as building a submenu may be expensive.
+ *
+ * Thus, the submenu has two special controls -- the submenu's visibility
+ * should be controlled by the NotMenuTrackerItem::submenu-shown property,
+ * and if a user clicks on the submenu, do not immediately show the menu,
+ * but call not_menu_tracker_item_request_submenu_shown() and wait for the
+ * NotMenuTrackerItem::submenu-shown property to update. If the user navigates,
+ * the application may want to be notified so it can cancel the expensive
+ * operation that it was using to build the submenu. Thus,
+ * not_menu_tracker_item_request_submenu_shown() takes a boolean parameter.
+ * Use %TRUE when the user wants to open the submenu, and %FALSE when the
+ * user wants to close the submenu.
+ */
+
+typedef GObjectClass NotMenuTrackerItemClass;
+
+struct _NotMenuTrackerItem
+{
+  GObject parent_instance;
+
+  NotActionObservable *observable;
+  gchar *action_namespace;
+  GMenuItem *item;
+  NotMenuTrackerItemRole role : 4;
+  guint is_separator : 1;
+  guint can_activate : 1;
+  guint sensitive : 1;
+  guint toggled : 1;
+  guint submenu_shown : 1;
+  guint submenu_requested : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_IS_SEPARATOR,
+  PROP_HAS_SUBMENU,
+  PROP_LABEL,
+  PROP_ICON,
+  PROP_SENSITIVE,
+  PROP_VISIBLE,
+  PROP_ROLE,
+  PROP_TOGGLED,
+  PROP_ACCEL,
+  PROP_SUBMENU_SHOWN,
+  N_PROPS
+};
+
+static GParamSpec *not_menu_tracker_item_pspecs[N_PROPS];
+
+static void not_menu_tracker_item_init_observer_iface (NotActionObserverInterface *iface);
+G_DEFINE_TYPE_WITH_CODE (NotMenuTrackerItem, not_menu_tracker_item, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (NOT_TYPE_ACTION_OBSERVER, 
not_menu_tracker_item_init_observer_iface))
+
+GType
+not_menu_tracker_item_role_get_type (void)
+{
+  static gsize not_menu_tracker_item_role_type;
+
+  if (g_once_init_enter (&not_menu_tracker_item_role_type))
+    {
+      static const GEnumValue values[] = {
+        { NOT_MENU_TRACKER_ITEM_ROLE_NORMAL, "NOT_MENU_TRACKER_ITEM_ROLE_NORMAL", "normal" },
+        { NOT_MENU_TRACKER_ITEM_ROLE_CHECK, "NOT_MENU_TRACKER_ITEM_ROLE_CHECK", "check" },
+        { NOT_MENU_TRACKER_ITEM_ROLE_RADIO, "NOT_MENU_TRACKER_ITEM_ROLE_RADIO", "radio" },
+        { 0, NULL, NULL }
+      };
+      GType type;
+
+      type = g_enum_register_static ("NotMenuTrackerItemRole", values);
+
+      g_once_init_leave (&not_menu_tracker_item_role_type, type);
+    }
+
+  return not_menu_tracker_item_role_type;
+}
+
+static void
+not_menu_tracker_item_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  NotMenuTrackerItem *self = NOT_MENU_TRACKER_ITEM (object);
+
+  switch (prop_id)
+    {
+    case PROP_IS_SEPARATOR:
+      g_value_set_boolean (value, not_menu_tracker_item_get_is_separator (self));
+      break;
+    case PROP_HAS_SUBMENU:
+      g_value_set_boolean (value, not_menu_tracker_item_get_has_submenu (self));
+      break;
+    case PROP_LABEL:
+      g_value_set_string (value, not_menu_tracker_item_get_label (self));
+      break;
+    case PROP_ICON:
+      g_value_set_object (value, not_menu_tracker_item_get_icon (self));
+      break;
+    case PROP_SENSITIVE:
+      g_value_set_boolean (value, not_menu_tracker_item_get_sensitive (self));
+      break;
+    case PROP_VISIBLE:
+      g_value_set_boolean (value, not_menu_tracker_item_get_visible (self));
+      break;
+    case PROP_ROLE:
+      g_value_set_enum (value, not_menu_tracker_item_get_role (self));
+      break;
+    case PROP_TOGGLED:
+      g_value_set_boolean (value, not_menu_tracker_item_get_toggled (self));
+      break;
+    case PROP_ACCEL:
+      g_value_set_string (value, not_menu_tracker_item_get_accel (self));
+      break;
+    case PROP_SUBMENU_SHOWN:
+      g_value_set_boolean (value, not_menu_tracker_item_get_submenu_shown (self));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+  }
+}
+
+static void
+not_menu_tracker_item_finalize (GObject *object)
+{
+  NotMenuTrackerItem *self = NOT_MENU_TRACKER_ITEM (object);
+
+  g_free (self->action_namespace);
+
+  if (self->observable)
+    g_object_unref (self->observable);
+
+  g_object_unref (self->item);
+
+  G_OBJECT_CLASS (not_menu_tracker_item_parent_class)->finalize (object);
+}
+
+static void
+not_menu_tracker_item_init (NotMenuTrackerItem * self)
+{
+}
+
+static void
+not_menu_tracker_item_class_init (NotMenuTrackerItemClass *class)
+{
+  class->get_property = not_menu_tracker_item_get_property;
+  class->finalize = not_menu_tracker_item_finalize;
+
+  not_menu_tracker_item_pspecs[PROP_IS_SEPARATOR] =
+    g_param_spec_boolean ("is-separator", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  not_menu_tracker_item_pspecs[PROP_HAS_SUBMENU] =
+    g_param_spec_boolean ("has-submenu", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  not_menu_tracker_item_pspecs[PROP_LABEL] =
+    g_param_spec_string ("label", "", "", NULL, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  not_menu_tracker_item_pspecs[PROP_ICON] =
+    g_param_spec_object ("icon", "", "", G_TYPE_ICON, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  not_menu_tracker_item_pspecs[PROP_SENSITIVE] =
+    g_param_spec_boolean ("sensitive", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  not_menu_tracker_item_pspecs[PROP_VISIBLE] =
+    g_param_spec_boolean ("visible", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  not_menu_tracker_item_pspecs[PROP_ROLE] =
+    g_param_spec_enum ("role", "", "",
+                       NOT_TYPE_MENU_TRACKER_ITEM_ROLE, NOT_MENU_TRACKER_ITEM_ROLE_NORMAL,
+                       G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  not_menu_tracker_item_pspecs[PROP_TOGGLED] =
+    g_param_spec_boolean ("toggled", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  not_menu_tracker_item_pspecs[PROP_ACCEL] =
+    g_param_spec_string ("accel", "", "", NULL, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  not_menu_tracker_item_pspecs[PROP_SUBMENU_SHOWN] =
+    g_param_spec_boolean ("submenu-shown", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+
+  g_object_class_install_properties (class, N_PROPS, not_menu_tracker_item_pspecs);
+}
+
+static void
+not_menu_tracker_item_action_added (NotActionObserver   *observer,
+                                    NotActionObservable *observable,
+                                    const gchar         *action_name,
+                                    const GVariantType  *parameter_type,
+                                    gboolean             enabled,
+                                    GVariant            *state)
+{
+  NotMenuTrackerItem *self = NOT_MENU_TRACKER_ITEM (observer);
+  GVariant *action_target;
+
+  action_target = g_menu_item_get_attribute_value (self->item, G_MENU_ATTRIBUTE_TARGET, NULL);
+
+  self->can_activate = (action_target == NULL && parameter_type == NULL) ||
+                        (action_target != NULL && parameter_type != NULL &&
+                        g_variant_is_of_type (action_target, parameter_type));
+
+  if (!self->can_activate)
+    {
+      if (action_target)
+        g_variant_unref (action_target);
+      return;
+    }
+
+  self->sensitive = enabled;
+
+  if (action_target != NULL && state != NULL)
+    {
+      self->toggled = g_variant_equal (state, action_target);
+      self->role = NOT_MENU_TRACKER_ITEM_ROLE_RADIO;
+    }
+
+  else if (state != NULL && g_variant_is_of_type (state, G_VARIANT_TYPE_BOOLEAN))
+    {
+      self->toggled = g_variant_get_boolean (state);
+      self->role = NOT_MENU_TRACKER_ITEM_ROLE_CHECK;
+    }
+
+  g_object_freeze_notify (G_OBJECT (self));
+
+  if (self->sensitive)
+    g_object_notify_by_pspec (G_OBJECT (self), not_menu_tracker_item_pspecs[PROP_SENSITIVE]);
+
+  if (self->toggled)
+    g_object_notify_by_pspec (G_OBJECT (self), not_menu_tracker_item_pspecs[PROP_TOGGLED]);
+
+  if (self->role != NOT_MENU_TRACKER_ITEM_ROLE_NORMAL)
+    g_object_notify_by_pspec (G_OBJECT (self), not_menu_tracker_item_pspecs[PROP_ROLE]);
+
+  g_object_thaw_notify (G_OBJECT (self));
+
+  if (action_target)
+    g_variant_unref (action_target);
+}
+
+static void
+not_menu_tracker_item_action_enabled_changed (NotActionObserver   *observer,
+                                              NotActionObservable *observable,
+                                              const gchar         *action_name,
+                                              gboolean             enabled)
+{
+  NotMenuTrackerItem *self = NOT_MENU_TRACKER_ITEM (observer);
+
+  if (!self->can_activate)
+    return;
+
+  if (self->sensitive == enabled)
+    return;
+
+  self->sensitive = enabled;
+
+  g_object_notify_by_pspec (G_OBJECT (self), not_menu_tracker_item_pspecs[PROP_SENSITIVE]);
+}
+
+static void
+not_menu_tracker_item_action_state_changed (NotActionObserver   *observer,
+                                            NotActionObservable *observable,
+                                            const gchar         *action_name,
+                                            GVariant            *state)
+{
+  NotMenuTrackerItem *self = NOT_MENU_TRACKER_ITEM (observer);
+  GVariant *action_target;
+  gboolean was_toggled;
+
+  if (!self->can_activate)
+    return;
+
+  action_target = g_menu_item_get_attribute_value (self->item, G_MENU_ATTRIBUTE_TARGET, NULL);
+  was_toggled = self->toggled;
+
+  if (action_target)
+    {
+      self->toggled = g_variant_equal (state, action_target);
+      g_variant_unref (action_target);
+    }
+
+  else if (g_variant_is_of_type (state, G_VARIANT_TYPE_BOOLEAN))
+    self->toggled = g_variant_get_boolean (state);
+
+  else
+    self->toggled = FALSE;
+
+  if (self->toggled != was_toggled)
+    g_object_notify_by_pspec (G_OBJECT (self), not_menu_tracker_item_pspecs[PROP_TOGGLED]);
+}
+
+static void
+not_menu_tracker_item_action_removed (NotActionObserver   *observer,
+                                      NotActionObservable *observable,
+                                      const gchar         *action_name)
+{
+  NotMenuTrackerItem *self = NOT_MENU_TRACKER_ITEM (observer);
+
+  if (!self->can_activate)
+    return;
+
+  g_object_freeze_notify (G_OBJECT (self));
+
+  if (self->sensitive)
+    {
+      self->sensitive = FALSE;
+      g_object_notify_by_pspec (G_OBJECT (self), not_menu_tracker_item_pspecs[PROP_SENSITIVE]);
+    }
+
+  if (self->toggled)
+    {
+      self->toggled = FALSE;
+      g_object_notify_by_pspec (G_OBJECT (self), not_menu_tracker_item_pspecs[PROP_TOGGLED]);
+    }
+
+  if (self->role != NOT_MENU_TRACKER_ITEM_ROLE_NORMAL)
+    {
+      self->role = NOT_MENU_TRACKER_ITEM_ROLE_NORMAL;
+      g_object_notify_by_pspec (G_OBJECT (self), not_menu_tracker_item_pspecs[PROP_ROLE]);
+    }
+
+  g_object_thaw_notify (G_OBJECT (self));
+}
+
+static void
+not_menu_tracker_item_init_observer_iface (NotActionObserverInterface *iface)
+{
+  iface->action_added = not_menu_tracker_item_action_added;
+  iface->action_enabled_changed = not_menu_tracker_item_action_enabled_changed;
+  iface->action_state_changed = not_menu_tracker_item_action_state_changed;
+  iface->action_removed = not_menu_tracker_item_action_removed;
+}
+
+NotMenuTrackerItem *
+_not_menu_tracker_item_new (NotActionObservable *observable,
+                            GMenuModel          *model,
+                            gint                 item_index,
+                            const gchar         *action_namespace,
+                            gboolean             is_separator)
+{
+  NotMenuTrackerItem *self;
+  const gchar *action_name;
+
+  g_return_val_if_fail (NOT_IS_ACTION_OBSERVABLE (observable), NULL);
+  g_return_val_if_fail (G_IS_MENU_MODEL (model), NULL);
+
+  self = g_object_new (NOT_TYPE_MENU_TRACKER_ITEM, NULL);
+  self->item = g_menu_item_new_from_model (model, item_index);
+  self->action_namespace = g_strdup (action_namespace);
+  self->observable = g_object_ref (observable);
+  self->is_separator = is_separator;
+
+  if (!is_separator && g_menu_item_get_attribute (self->item, "action", "&s", &action_name))
+    {
+      GActionGroup *group = G_ACTION_GROUP (observable);
+      const GVariantType *parameter_type;
+      gboolean enabled;
+      GVariant *state;
+      gboolean found;
+
+      state = NULL;
+
+      if (action_namespace)
+        {
+          gchar *full_action;
+
+          full_action = g_strjoin (".", action_namespace, action_name, NULL);
+          gtk_action_observable_register_observer (self->observable, full_action, NOT_ACTION_OBSERVER 
(self));
+          found = g_action_group_query_action (group, full_action, &enabled, &parameter_type, NULL, NULL, 
&state);
+          g_free (full_action);
+        }
+      else
+        {
+          gtk_action_observable_register_observer (self->observable, action_name, NOT_ACTION_OBSERVER 
(self));
+          found = g_action_group_query_action (group, action_name, &enabled, &parameter_type, NULL, NULL, 
&state);
+        }
+
+      if (found)
+        not_menu_tracker_item_action_added (NOT_ACTION_OBSERVER (self), observable, NULL, parameter_type, 
enabled, state);
+      else
+        not_menu_tracker_item_action_removed (NOT_ACTION_OBSERVER (self), observable, NULL);
+
+      if (state)
+        g_variant_unref (state);
+    }
+  else
+    self->sensitive = TRUE;
+
+  return self;
+}
+
+NotActionObservable *
+_not_menu_tracker_item_get_observable (NotMenuTrackerItem *self)
+{
+  return self->observable;
+}
+
+/**
+ * not_menu_tracker_item_get_is_separator:
+ * @self: A #NotMenuTrackerItem instance
+ *
+ * Returns whether the menu item is a separator. If so, only
+ * certain properties may need to be obeyed. See the documentation
+ * for #NotMenuTrackerItem.
+ */
+gboolean
+not_menu_tracker_item_get_is_separator (NotMenuTrackerItem *self)
+{
+  return self->is_separator;
+}
+
+/**
+ * not_menu_tracker_item_get_has_submenu:
+ * @self: A #NotMenuTrackerItem instance
+ *
+ * Returns whether the menu item has a submenu. If so, only
+ * certain properties may need to be obeyed. See the documentation
+ * for #NotMenuTrackerItem.
+ */
+gboolean
+not_menu_tracker_item_get_has_submenu (NotMenuTrackerItem *self)
+{
+  GMenuModel *link;
+
+  link = g_menu_item_get_link (self->item, G_MENU_LINK_SUBMENU);
+
+  if (link)
+    {
+      g_object_unref (link);
+      return TRUE;
+    }
+  else
+    return FALSE;
+}
+
+const gchar *
+not_menu_tracker_item_get_label (NotMenuTrackerItem *self)
+{
+  const gchar *label = NULL;
+
+  g_menu_item_get_attribute (self->item, G_MENU_ATTRIBUTE_LABEL, "&s", &label);
+
+  return label;
+}
+
+/**
+ * not_menu_tracker_item_get_icon:
+ *
+ * Returns: (transfer full):
+ */
+GIcon *
+not_menu_tracker_item_get_icon (NotMenuTrackerItem *self)
+{
+  GVariant *icon_data;
+  GIcon *icon;
+
+  icon_data = g_menu_item_get_attribute_value (self->item, "icon", NULL);
+
+  if (icon_data == NULL)
+    return NULL;
+
+  icon = g_icon_deserialize (icon_data);
+  g_variant_unref (icon_data);
+
+  return icon;
+}
+
+gboolean
+not_menu_tracker_item_get_sensitive (NotMenuTrackerItem *self)
+{
+  return self->sensitive;
+}
+
+gboolean
+not_menu_tracker_item_get_visible (NotMenuTrackerItem *self)
+{
+  return TRUE;
+}
+
+NotMenuTrackerItemRole
+not_menu_tracker_item_get_role (NotMenuTrackerItem *self)
+{
+  return self->role;
+}
+
+gboolean
+not_menu_tracker_item_get_toggled (NotMenuTrackerItem *self)
+{
+  return self->toggled;
+}
+
+const gchar *
+not_menu_tracker_item_get_accel (NotMenuTrackerItem *self)
+{
+  const gchar *accel = NULL;
+
+  g_menu_item_get_attribute (self->item, "accel", "&s", &accel);
+
+  return accel;
+}
+
+GMenuModel *
+_not_menu_tracker_item_get_submenu (NotMenuTrackerItem *self)
+{
+  return g_menu_item_get_link (self->item, "submenu");
+}
+
+gchar *
+_not_menu_tracker_item_get_submenu_namespace (NotMenuTrackerItem *self)
+{
+  const gchar *namespace;
+
+  if (g_menu_item_get_attribute (self->item, "action-namespace", "&s", &namespace))
+    {
+      if (self->action_namespace)
+        return g_strjoin (".", self->action_namespace, namespace, NULL);
+      else
+        return g_strdup (namespace);
+    }
+  else
+    return g_strdup (self->action_namespace);
+}
+
+gboolean
+not_menu_tracker_item_get_should_request_show (NotMenuTrackerItem *self)
+{
+  return g_menu_item_get_attribute (self->item, "submenu-action", "&s", NULL);
+}
+
+gboolean
+not_menu_tracker_item_get_submenu_shown (NotMenuTrackerItem *self)
+{
+  return self->submenu_shown;
+}
+
+static void
+not_menu_tracker_item_set_submenu_shown (NotMenuTrackerItem *self,
+                                         gboolean            submenu_shown)
+{
+  if (submenu_shown == self->submenu_shown)
+    return;
+
+  self->submenu_shown = submenu_shown;
+  g_object_notify_by_pspec (G_OBJECT (self), not_menu_tracker_item_pspecs[PROP_SUBMENU_SHOWN]);
+}
+
+void
+not_menu_tracker_item_activated (NotMenuTrackerItem *self)
+{
+  const gchar *action_name;
+  GVariant *action_target;
+
+  g_return_if_fail (NOT_IS_MENU_TRACKER_ITEM (self));
+
+  if (!self->can_activate)
+    return;
+
+  g_menu_item_get_attribute (self->item, G_MENU_ATTRIBUTE_ACTION, "&s", &action_name);
+  action_target = g_menu_item_get_attribute_value (self->item, G_MENU_ATTRIBUTE_TARGET, NULL);
+
+  if (self->action_namespace)
+    {
+      gchar *full_action;
+
+      full_action = g_strjoin (".", self->action_namespace, action_name, NULL);
+      g_action_group_activate_action (G_ACTION_GROUP (self->observable), full_action, action_target);
+      g_free (full_action);
+    }
+  else
+    g_action_group_activate_action (G_ACTION_GROUP (self->observable), action_name, action_target);
+
+  if (action_target)
+    g_variant_unref (action_target);
+}
+
+typedef struct
+{
+  NotMenuTrackerItem *item;
+  gchar              *submenu_action;
+  gboolean            first_time;
+} NotMenuTrackerOpener;
+
+static void
+not_menu_tracker_opener_update (NotMenuTrackerOpener *opener)
+{
+  GActionGroup *group = G_ACTION_GROUP (opener->item->observable);
+  gboolean is_open = TRUE;
+
+  /* We consider the menu as being "open" if the action does not exist
+   * or if there is another problem (no state, wrong state type, etc.).
+   * If the action exists, with the correct state then we consider it
+   * open if we have ever seen this state equal to TRUE.
+   *
+   * In the event that we see the state equal to FALSE, we force it back
+   * to TRUE.  We do not signal that the menu was closed because this is
+   * likely to create UI thrashing.
+   *
+   * The only way the menu can have a true-to-false submenu-shown
+   * transition is if the user calls _request_submenu_shown (FALSE).
+   * That is handled in _free() below.
+   */
+
+  if (g_action_group_has_action (group, opener->submenu_action))
+    {
+      GVariant *state = g_action_group_get_action_state (group, opener->submenu_action);
+
+      if (state)
+        {
+          if (g_variant_is_of_type (state, G_VARIANT_TYPE_BOOLEAN))
+            is_open = g_variant_get_boolean (state);
+          g_variant_unref (state);
+        }
+    }
+
+  /* If it is already open, signal that.
+   *
+   * If it is not open, ask it to open.
+   */
+  if (is_open)
+    not_menu_tracker_item_set_submenu_shown (opener->item, TRUE);
+
+  if (!is_open || opener->first_time)
+    {
+      g_action_group_change_action_state (group, opener->submenu_action, g_variant_new_boolean (TRUE));
+      opener->first_time = FALSE;
+    }
+}
+
+static void
+not_menu_tracker_opener_added (GActionGroup *group,
+                               const gchar  *action_name,
+                               gpointer      user_data)
+{
+  NotMenuTrackerOpener *opener = user_data;
+
+  if (g_str_equal (action_name, opener->submenu_action))
+    not_menu_tracker_opener_update (opener);
+}
+
+static void
+not_menu_tracker_opener_removed (GActionGroup *action_group,
+                                 const gchar  *action_name,
+                                 gpointer      user_data)
+{
+  NotMenuTrackerOpener *opener = user_data;
+
+  if (g_str_equal (action_name, opener->submenu_action))
+    not_menu_tracker_opener_update (opener);
+}
+
+static void
+not_menu_tracker_opener_changed (GActionGroup *action_group,
+                                 const gchar  *action_name,
+                                 GVariant     *new_state,
+                                 gpointer      user_data)
+{
+  NotMenuTrackerOpener *opener = user_data;
+
+  if (g_str_equal (action_name, opener->submenu_action))
+    not_menu_tracker_opener_update (opener);
+}
+
+static void
+not_menu_tracker_opener_free (gpointer data)
+{
+  NotMenuTrackerOpener *opener = data;
+
+  g_signal_handlers_disconnect_by_func (opener->item->observable, not_menu_tracker_opener_added, opener);
+  g_signal_handlers_disconnect_by_func (opener->item->observable, not_menu_tracker_opener_removed, opener);
+  g_signal_handlers_disconnect_by_func (opener->item->observable, not_menu_tracker_opener_changed, opener);
+
+  g_action_group_change_action_state (G_ACTION_GROUP (opener->item->observable),
+                                      opener->submenu_action,
+                                      g_variant_new_boolean (FALSE));
+
+  not_menu_tracker_item_set_submenu_shown (opener->item, FALSE);
+
+  g_free (opener->submenu_action);
+
+  g_slice_free (NotMenuTrackerOpener, opener);
+}
+
+static NotMenuTrackerOpener *
+not_menu_tracker_opener_new (NotMenuTrackerItem *item,
+                             const gchar        *submenu_action)
+{
+  NotMenuTrackerOpener *opener;
+
+  opener = g_slice_new (NotMenuTrackerOpener);
+  opener->first_time = TRUE;
+  opener->item = item;
+
+  if (item->action_namespace)
+    opener->submenu_action = g_strjoin (".", item->action_namespace, submenu_action, NULL);
+  else
+    opener->submenu_action = g_strdup (submenu_action);
+
+  g_signal_connect (item->observable, "action-added", G_CALLBACK (not_menu_tracker_opener_added), opener);
+  g_signal_connect (item->observable, "action-removed", G_CALLBACK (not_menu_tracker_opener_removed), 
opener);
+  g_signal_connect (item->observable, "action-state-changed", G_CALLBACK (not_menu_tracker_opener_changed), 
opener);
+
+  not_menu_tracker_opener_update (opener);
+
+  return opener;
+}
+
+void
+not_menu_tracker_item_request_submenu_shown (NotMenuTrackerItem *self,
+                                             gboolean            shown)
+{
+  const gchar *submenu_action;
+  gboolean has_submenu_action;
+
+  if (shown == self->submenu_requested)
+    return;
+
+  has_submenu_action = g_menu_item_get_attribute (self->item, "submenu-action", "&s", &submenu_action);
+
+  self->submenu_requested = shown;
+
+  /* If we have a submenu action, start a submenu opener and wait
+   * for the reply from the client. Otherwise, simply open the
+   * submenu immediately.
+   */
+  if (has_submenu_action)
+    {
+      if (shown)
+        g_object_set_data_full (G_OBJECT (self), "submenu-opener",
+                                not_menu_tracker_opener_new (self, submenu_action),
+                                not_menu_tracker_opener_free);
+      else
+        g_object_set_data (G_OBJECT (self), "submenu-opener", NULL);
+    }
+  else
+    not_menu_tracker_item_set_submenu_shown (self, shown);
+}
diff --git a/gtkmenutrackeritem.h b/gtkmenutrackeritem.h
new file mode 100644
index 0000000..cf68d10
--- /dev/null
+++ b/gtkmenutrackeritem.h
@@ -0,0 +1,84 @@
+/*
+ * Copyright © 2011, 2013 Canonical Limited
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the licence, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Ryan Lortie <desrt desrt ca>
+ */
+
+#ifndef __NOT_MENU_TRACKER_ITEM_H__
+#define __NOT_MENU_TRACKER_ITEM_H__
+
+#include "gtkactionobservable.h"
+
+#define NOT_TYPE_MENU_TRACKER_ITEM                          (not_menu_tracker_item_get_type ())
+#define NOT_MENU_TRACKER_ITEM(inst)                         (G_TYPE_CHECK_INSTANCE_CAST ((inst), \
+                                                             NOT_TYPE_MENU_TRACKER_ITEM, NotMenuTrackerItem))
+#define NOT_IS_MENU_TRACKER_ITEM(inst)                      (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \
+                                                             NOT_TYPE_MENU_TRACKER_ITEM))
+
+typedef struct _NotMenuTrackerItem NotMenuTrackerItem;
+
+#define NOT_TYPE_MENU_TRACKER_ITEM_ROLE                     (not_menu_tracker_item_role_get_type ())
+
+typedef enum  {
+  NOT_MENU_TRACKER_ITEM_ROLE_NORMAL,
+  NOT_MENU_TRACKER_ITEM_ROLE_CHECK,
+  NOT_MENU_TRACKER_ITEM_ROLE_RADIO,
+} NotMenuTrackerItemRole;
+
+GType                   not_menu_tracker_item_get_type                  (void) G_GNUC_CONST;
+
+GType                   not_menu_tracker_item_role_get_type             (void) G_GNUC_CONST;
+
+NotMenuTrackerItem *   _not_menu_tracker_item_new                       (NotActionObservable *observable,
+                                                                         GMenuModel          *model,
+                                                                         gint                 item_index,
+                                                                         const gchar         
*action_namespace,
+                                                                         gboolean             is_separator);
+
+NotActionObservable *  _not_menu_tracker_item_get_observable            (NotMenuTrackerItem *self);
+
+gboolean                not_menu_tracker_item_get_is_separator          (NotMenuTrackerItem *self);
+
+gboolean                not_menu_tracker_item_get_has_submenu           (NotMenuTrackerItem *self);
+
+const gchar *           not_menu_tracker_item_get_label                 (NotMenuTrackerItem *self);
+
+GIcon *                 not_menu_tracker_item_get_icon                  (NotMenuTrackerItem *self);
+
+gboolean                not_menu_tracker_item_get_sensitive             (NotMenuTrackerItem *self);
+
+gboolean                not_menu_tracker_item_get_visible               (NotMenuTrackerItem *self);
+
+NotMenuTrackerItemRole  not_menu_tracker_item_get_role                  (NotMenuTrackerItem *self);
+
+gboolean                not_menu_tracker_item_get_toggled               (NotMenuTrackerItem *self);
+
+const gchar *           not_menu_tracker_item_get_accel                 (NotMenuTrackerItem *self);
+
+GMenuModel *           _not_menu_tracker_item_get_submenu               (NotMenuTrackerItem *self);
+
+gchar *                _not_menu_tracker_item_get_submenu_namespace     (NotMenuTrackerItem *self);
+
+gboolean                not_menu_tracker_item_get_should_request_show   (NotMenuTrackerItem *self);
+
+void                    not_menu_tracker_item_activated                 (NotMenuTrackerItem *self);
+
+void                    not_menu_tracker_item_request_submenu_shown     (NotMenuTrackerItem *self,
+                                                                         gboolean            shown);
+
+gboolean                not_menu_tracker_item_get_submenu_shown         (NotMenuTrackerItem *self);
+
+#endif
diff --git a/menumodel.vala b/menumodel.vala
new file mode 100644
index 0000000..fae5eb5
--- /dev/null
+++ b/menumodel.vala
@@ -0,0 +1,155 @@
+public class MenuBoxItem : Gtk.Label {
+       public SequenceIter<MenuBoxItem> iter;
+       public Not.MenuTrackerItem item;
+
+       public MenuBoxItem (Not.MenuTrackerItem item) {
+               this.item = item;
+
+               if (item.is_separator) {
+                       hide ();
+                       return;
+               }
+
+               label = item.label;
+               xalign = (float) 0.0;
+
+               show ();
+       }
+}
+
+public class MenuBox : Egg.ListBox {
+       Not.MenuTracker tracker;
+       Sequence<MenuBoxItem> children;
+       string? _search;
+       public string? search { set { _search = value; refilter (); } }
+
+       bool filter_item (Gtk.Widget widget) {
+               var item = widget as MenuBoxItem;
+
+               return _search == null || item.label.contains (_search);
+       }
+
+       int sort_items (Gtk.Widget widget_a, Gtk.Widget widget_b) {
+               var item_a = widget_a as MenuBoxItem;
+               var item_b = widget_b as MenuBoxItem;
+
+               return item_a.iter.get_position () - item_b.iter.get_position ();
+       }
+
+       void update_separator (ref Gtk.Widget? separator, Gtk.Widget after_widget, Gtk.Widget? before_widget) 
{
+               var before = before_widget as MenuBoxItem;
+               var after = after_widget as MenuBoxItem;
+               var before_iter = (before == null) ? children.get_begin_iter () : before.iter;
+               var after_iter = after.iter;
+               Not.MenuTrackerItem? found;
+
+               found = null;
+               for (var iter = before_iter; iter != after_iter; iter = iter.next ()) {
+                       var item = iter.get ().item;
+
+                       if (item.is_separator) {
+                               found = item;
+                       }
+               }
+
+               if (found != null) {
+                       var label = found.label;
+
+                       if (label != null) {
+                               separator = new Gtk.Label ("-- " + found.label + " --");
+                       } else if (before != null) {
+                               separator = new Gtk.Label ("----");
+                       }
+               } else {
+                       separator = null;
+               }
+       }
+
+       private void insert_item (Not.MenuTrackerItem item, int position) {
+               var iter = children.get_iter_at_pos (position);
+               var widget = new MenuBoxItem (item);
+               widget.iter = Sequence<MenuBoxItem>.insert_before (iter, widget);
+               if (!widget.item.is_separator) {
+                       add (widget);
+               }
+               resort ();
+       }
+
+       private void remove_item (int position) {
+               var iter = children.get_iter_at_pos (position);
+               var widget = iter.get ();
+               Sequence.remove (iter);
+               widget.destroy ();
+               resort ();
+       }
+
+       private void setup () {
+               children = new Sequence<MenuBoxItem> ();
+               set_filter_func (filter_item);
+               set_separator_funcs (update_separator);
+               set_sort_func (sort_items);
+       }
+
+       public MenuBox (Not.ActionObservable observable, MenuModel menu) {
+               setup ();
+
+               tracker = new Not.MenuTracker (observable, menu, true, null, insert_item, remove_item);
+       }
+
+       private MenuBox.for_submenu_item (Not.MenuTrackerItem item) {
+               setup ();
+       }
+
+}
+class Window : Gtk.ApplicationWindow {
+       public Window (Gtk.Application app) {
+               Object (application: app);
+
+               var menu = new Menu ();
+
+               var ni = new Menu ();
+               ni.append ("New", "app.new-window");
+               menu.append_section (null, ni);
+
+               var fi = new Menu ();
+               fi.append ("Fullscreen", "win.fullscreen");
+               menu.append_section (null, fi);
+
+               var cp = new Menu ();
+               cp.append ("Copy", "win.copy");
+               cp.append ("Cut", "win.cut");
+               cp.append ("Paste", "win.paste");
+
+               menu.append_section ("Edit Functions", cp);
+
+               var qi = new Menu ();
+               qi.append ("Quit", "app.quit");
+
+               menu.append_section (null, qi);
+
+               var muxer = new Not.ActionMuxer ();
+               muxer.insert ("win", this);
+               muxer.insert ("app", app);
+
+               var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6);
+               var entry = new Gtk.Entry ();
+               var menubox = new MenuBox (muxer, menu);
+               box.add (entry);
+               box.add (menubox);
+
+               entry.bind_property ("text", menubox, "search");
+
+               add (box);
+               show_all ();
+       }
+}
+
+class MyApp : Gtk.Application {
+       protected override void activate () {
+               new Window (this);
+       }
+
+       static int main (string[] args) {
+               return new MyApp ().run (args);
+       }
+}


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