[egg-list-box/gmenumodel] Add a GMenuModel example program
- From: Ryan Lortie <ryanl src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [egg-list-box/gmenumodel] Add a GMenuModel example program
- Date: Tue, 4 Jun 2013 19:07:45 +0000 (UTC)
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, ¶meter_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 = §ion->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, §ion->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 (¬_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 (¬_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, ¶meter_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, ¶meter_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]