[gnome-shell/wip/gtkmenutrackeritem: 26/32] remoteMenu: Port to GtkMenuTrackerItem



commit f3d22a7d232159789f22e96ad658c3feede997c3
Author: Jasper St. Pierre <jstpierre mecheye net>
Date:   Thu May 9 12:28:44 2013 -0400

    remoteMenu: Port to GtkMenuTrackerItem
    
    This pulls in new upstream API that Ryan will maintain, removing
    code on our side.
    
    Currently, our implementation of submenus will be gone, but this
    will be fixed in a few commits.

 js/ui/remoteMenu.js      |  232 ++++----------
 src/Makefile.am          |    4 +
 src/gtkmenutracker.c     |   69 ++++-
 src/gtkmenutracker.h     |   37 ++-
 src/gtkmenutrackeritem.c |  788 ++++++++++++++++++++++++++++++++++++++++++++++
 src/gtkmenutrackeritem.h |   84 +++++
 src/shell-menu-tracker.c |   24 +-
 src/shell-menu-tracker.h |   12 +-
 8 files changed, 1042 insertions(+), 208 deletions(-)
---
diff --git a/js/ui/remoteMenu.js b/js/ui/remoteMenu.js
index 89157bf..00c0684 100644
--- a/js/ui/remoteMenu.js
+++ b/js/ui/remoteMenu.js
@@ -1,210 +1,102 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 
 const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
 const Gio = imports.gi.Gio;
 const Lang = imports.lang;
 const Shell = imports.gi.Shell;
+const ShellMenu = imports.gi.ShellMenu;
 const St = imports.gi.St;
 
 const PopupMenu = imports.ui.popupMenu;
 
-/**
- * RemoteMenu:
- *
- * A PopupMenu that tracks a GMenuModel and shows its actions
- * (exposed by GApplication/GActionGroup)
- */
-const RemoteMenu = new Lang.Class({
-    Name: 'RemoteMenu',
-    Extends: PopupMenu.PopupMenu,
-
-    _init: function(sourceActor, model, actionGroup) {
-        this.parent(sourceActor, 0.0, St.Side.TOP);
+const RemoteMenuItemMapper = new Lang.Class({
+    Name: 'RemoteMenuItemMapper',
 
-        this.model = model;
-        this.actionGroup = actionGroup;
+    _init: function(trackerItem) {
+        this._trackerItem = trackerItem;
 
-        this._actions = {};
-        this._trackMenu(model, this);
+        this.menuItem = new PopupMenu.PopupBaseMenuItem();
+        this._label = new St.Label();
+        this.menuItem.addActor(this._label);
+        this.menuItem.actor.label_actor = this._label;
 
-        this._actionStateChangeId = this.actionGroup.connect('action-state-changed', Lang.bind(this, 
this._actionStateChanged));
-        this._actionEnableChangeId = this.actionGroup.connect('action-enabled-changed', Lang.bind(this, 
this._actionEnabledChanged));
-    },
-
-    destroy: function() {
-        if (this._actionStateChangeId) {
-            this.actionGroup.disconnect(this._actionStateChangeId);
-            this._actionStateChangeId = 0;
-        }
-
-        if (this._actionEnableChangeId) {
-            this.actionGroup.disconnect(this._actionEnableChangeId);
-            this._actionEnableChangeId = 0;
-        }
-
-        this.parent();
-    },
-
-    _actionAdded: function(model, item, index) {
-        let action_id = item.action_id;
-
-        if (!this._actions[action_id])
-            this._actions[action_id] = { enabled: this.actionGroup.get_action_enabled(action_id),
-                                         state: this.actionGroup.get_action_state(action_id),
-                                         items: [ ],
-                                       };
-        let action = this._actions[action_id];
-        let target, destroyId, specificSignalId;
-
-        if (action.state) {
-            // Docs have get_state_hint(), except that the DBus protocol
-            // has no provision for it (so ShellApp does not implement it,
-            // and neither GApplication), and g_action_get_state_hint()
-            // always returns null
-            // Funny :)
-
-            switch (String.fromCharCode(action.state.classify())) {
-            case 'b':
-                action.items.push(item);
-                item.setOrnament(action.state.get_boolean() ?
-                                 PopupMenu.Ornament.CHECK :
-                                 PopupMenu.Ornament.NONE);
-                specificSignalId = item.connect('activate', Lang.bind(this, function(item) {
-                    this.actionGroup.activate_action(action_id, null);
-                }));
-                break;
-            case 's':
-                action.items.push(item);
-                item._remoteTarget = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_TARGET, 
null).deep_unpack();
-                item.setOrnament(action.state.deep_unpack() == item._remoteTarget ?
-                                 PopupMenu.Ornament.DOT :
-                                 PopupMenu.Ornament.NONE);
-                specificSignalId = item.connect('activate', Lang.bind(this, function(item) {
-                    this.actionGroup.activate_action(action_id, GLib.Variant.new_string(item._remoteTarget));
-                }));
-                break;
-            default:
-                log('Action "%s" has state of type %s, which is not supported'.format(action_id, 
action.state.get_type_string()));
-                return;
-            }
-        } else {
-            target = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_TARGET, null);
-            action.items.push(item);
-            specificSignalId = item.connect('activate', Lang.bind(this, function() {
-                this.actionGroup.activate_action(action_id, target);
-            }));
-        }
-
-        item.actor.reactive = item.actor.can_focus = action.enabled;
-
-        destroyId = item.connect('destroy', Lang.bind(this, function() {
-            item.disconnect(destroyId);
-            item.disconnect(specificSignalId);
-
-            let pos = action.items.indexOf(item);
-            if (pos != -1)
-                action.items.splice(pos, 1);
+        this.menuItem.connect('activate', Lang.bind(this, function() {
+            this._trackerItem.activated();
         }));
-    },
 
-    _trackMenu: function(model, item) {
-        item._tracker = Shell.MenuTracker.new(model,
-                                              null, /* action namespace */
-                                              Lang.bind(this, this._insertItem, item),
-                                              Lang.bind(this, this._removeItem, item));
+        this._trackerItem.bind_property('visible', this.menuItem.actor, 'visible', 
GObject.BindingFlags.SYNC_CREATE);
 
-        item.connect('destroy', function() {
-            item._tracker.destroy();
-            item._tracker = null;
-        });
+        this._trackerItem.connect('notify::label', Lang.bind(this, this._updateLabel));
+        this._trackerItem.connect('notify::sensitive', Lang.bind(this, this._updateSensitivity));
+        this._trackerItem.connect('notify::role', Lang.bind(this, this._updateDecoration));
+        this._trackerItem.connect('notify::toggled', Lang.bind(this, this._updateDecoration));
+
+        this._updateLabel();
+        this._updateSensitivity();
+        this._updateDecoration();
     },
 
-    _createMenuItem: function(model, index) {
-        let labelValue = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_LABEL, null);
-        let label = labelValue ? labelValue.deep_unpack() : '';
+    _updateLabel: function() {
+        let label = this._trackerItem.label;
         // remove all underscores that are not followed by another underscore
         label = label.replace(/_([^_])/, '$1');
+        this._label.text = label;
+    },
 
-        let submenuModel = model.get_item_link(index, Gio.MENU_LINK_SUBMENU);
-        if (submenuModel) {
-            let item = new PopupMenu.PopupSubMenuMenuItem(label);
-            this._trackMenu(submenuModel, item.menu);
-            return item;
-        }
-
-        let item = new PopupMenu.PopupMenuItem(label);
-        let action_id = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_ACTION, null).deep_unpack();
-        item.actor.can_focus = item.actor.reactive = false;
+    _updateSensitivity: function() {
+        this.menuItem.setSensitive(this._trackerItem.sensitive);
+    },
 
-        item.action_id = action_id;
+    _updateDecoration: function() {
+        let ornamentForRole = {};
+        ornamentForRole[ShellMenu.MenuTrackerItemRole.RADIO] = PopupMenu.Ornament.DOT;
+        ornamentForRole[ShellMenu.MenuTrackerItemRole.CHECK] = PopupMenu.Ornament.CHECK;
 
-        if (this.actionGroup.has_action(action_id)) {
-            this._actionAdded(model, item, index);
-            return item;
-        }
+        let ornament = PopupMenu.Ornament.NONE;
+        if (this._trackerItem.toggled)
+            ornament = ornamentForRole[this._trackerItem.role];
 
-        let signalId = this.actionGroup.connect('action-added', Lang.bind(this, function(actionGroup, 
actionName) {
-            actionGroup.disconnect(signalId);
-            if (this._actions[actionName]) return;
+        this.menuItem.setOrnament(ornament);
+    },
+});
 
-            this._actionAdded(model, item, index);
-        }));
+const RemoteMenu = new Lang.Class({
+    Name: 'RemoteMenu',
+    Extends: PopupMenu.PopupMenu,
 
-        return item;
-    },
+    _init: function(sourceActor, model, actionGroup) {
+        this.parent(sourceActor, 0.0, St.Side.TOP);
 
-    _actionStateChanged: function(actionGroup, action_id) {
-        let action = this._actions[action_id];
-        if (!action)
-            return;
-
-        action.state = actionGroup.get_action_state(action_id);
-        if (action.items.length) {
-            switch (String.fromCharCode(action.state.classify())) {
-            case 'b':
-                for (let i = 0; i < action.items.length; i++)
-                    action.items[i].setOrnament(action.state.get_boolean() ?
-                                                Ornament.CHECK : Ornament.NONE);
-                break;
-            case 'd':
-                for (let i = 0; i < action.items.length; i++)
-                    action.items[i].setValue(action.state.get_double());
-                break;
-            case 's':
-                for (let i = 0; i < action.items.length; i++)
-                    action.items[i].setOrnament(action.items[i]._remoteTarget == action.state.deep_unpack() ?
-                                                Ornament.DOT : Ornament.NONE);
-            }
-        }
+        this._model = model;
+        this._actionGroup = actionGroup;
+        this._tracker = Shell.MenuTracker.new(this._actionGroup,
+                                              this._model,
+                                              null, /* action namespace */
+                                              Lang.bind(this, this._insertItem),
+                                              Lang.bind(this, this._removeItem));
     },
 
-    _actionEnabledChanged: function(actionGroup, action_id) {
-        let action = this._actions[action_id];
-        if (!action)
-            return;
-
-        action.enabled = actionGroup.get_action_enabled(action_id);
-        if (action.items.length) {
-            for (let i = 0; i < action.items.length; i++) {
-                let item = action.items[i];
-                item.actor.reactive = item.actor.can_focus = action.enabled;
-            }
-        }
+    destroy: function() {
+        this._tracker.destroy();
+        this.parent();
     },
 
-    _insertItem: function(position, model, item_index, action_namespace, is_separator, target) {
+    _insertItem: function(trackerItem, position) {
         let item;
 
-        if (is_separator)
+        if (trackerItem.get_is_separator()) {
             item = new PopupMenu.PopupSeparatorMenuItem();
-        else
-            item = this._createMenuItem(model, item_index);
+        } else {
+            let mapper = new RemoteMenuItemMapper(trackerItem);
+            item = mapper.menuItem;
+        }
 
-        target.addMenuItem(item, position);
+        this.addMenuItem(item, position);
     },
 
-    _removeItem: function(position, target) {
-        let items = target._getMenuItems();
+    _removeItem: function(position) {
+        let items = this._getMenuItems();
         items[position].destroy();
     },
 });
diff --git a/src/Makefile.am b/src/Makefile.am
index ec0028f..0e51ae2 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -136,6 +136,8 @@ shell_private_sources =             \
        gtkactionobservable.c           \
        gtkactionobserver.h             \
        gtkactionobserver.c             \
+       gtkmenutrackeritem.c            \
+       gtkmenutrackeritem.h            \
        gtkmenutracker.c                \
        gtkmenutracker.h                \
        $(NULL)
@@ -302,6 +304,8 @@ ShellMenu_0_1_gir_FILES = \
        gtkactionobservable.c           \
        gtkactionobserver.h             \
        gtkactionobserver.c             \
+       gtkmenutrackeritem.c            \
+       gtkmenutrackeritem.h            \
        $(NULL)
 ShellMenu_0_1_gir_SCANNERFLAGS = --namespace=ShellMenu --identifier-prefix=Gtk
 INTROSPECTION_GIRS += ShellMenu-0.1.gir
diff --git a/src/gtkmenutracker.c b/src/gtkmenutracker.c
index f49457f..ab369ab 100644
--- a/src/gtkmenutracker.c
+++ b/src/gtkmenutracker.c
@@ -23,10 +23,45 @@
 
 #include "gtkmenutracker.h"
 
+/**
+ * SECTION:gtkmenutracker
+ * @Title: GtkMenuTracker
+ * @Short_description: A helper class for interpreting #GMenuModel
+ *
+ * #GtkMenuTracker is a simple object to ease implementations of #GMenuModel.
+ * Given a #GtkActionObservable (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 #GtkMenuTracker is fairly simple. The only guarantee you must make
+ * to #GtkMenuTracker is that you must obey all insert signals and track the
+ * position of items that #GtkMenuTracker gives you. That is, #GtkMenuTracker
+ * 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.
+ *
+ * #GtkMenuTracker will give you a #GtkMenuTrackerItem 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 #GtkMenuTrackerItem are mutable, and you must
+ * listen for changes in the item. For more details, see the documentation
+ * for #GtkMenuTrackerItem 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 #GtkMenuTracker
+ * expects the position to change, so we must tell #GtkMenuTracker to ignore
+ * separators itself.
+ */
+
 typedef struct _GtkMenuTrackerSection GtkMenuTrackerSection;
 
 struct _GtkMenuTracker
 {
+  GtkActionObservable      *observable;
   GtkMenuTrackerInsertFunc  insert_func;
   GtkMenuTrackerRemoveFunc  remove_func;
   gpointer                  user_data;
@@ -159,7 +194,12 @@ gtk_menu_tracker_section_sync_separators (GtkMenuTrackerSection *section,
   if (should_have_separator > section->has_separator)
     {
       /* Add a separator */
-      (* tracker->insert_func) (offset, parent_model, parent_index, NULL, TRUE, tracker->user_data);
+      GtkMenuTrackerItem *item;
+
+      item = _gtk_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)
@@ -258,8 +298,13 @@ gtk_menu_tracker_add_items (GtkMenuTracker         *tracker,
         }
       else
         {
-          (* tracker->insert_func) (offset, model, position + n_items,
-                                    section->action_namespace, FALSE, tracker->user_data);
+          GtkMenuTrackerItem *item;
+
+          item = _gtk_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);
         }
     }
@@ -400,7 +445,8 @@ gtk_menu_tracker_section_new (GtkMenuTracker *tracker,
  * gtk_menu_tracker_free() is called.
  */
 GtkMenuTracker *
-gtk_menu_tracker_new (GMenuModel               *model,
+gtk_menu_tracker_new (GtkActionObservable      *observable,
+                      GMenuModel               *model,
                       gboolean                  with_separators,
                       const gchar              *action_namespace,
                       GtkMenuTrackerInsertFunc  insert_func,
@@ -410,6 +456,7 @@ gtk_menu_tracker_new (GMenuModel               *model,
   GtkMenuTracker *tracker;
 
   tracker = g_slice_new (GtkMenuTracker);
+  tracker->observable = g_object_ref (observable);
   tracker->insert_func = insert_func;
   tracker->remove_func = remove_func;
   tracker->user_data = user_data;
@@ -420,6 +467,19 @@ gtk_menu_tracker_new (GMenuModel               *model,
   return tracker;
 }
 
+GtkMenuTracker *
+gtk_menu_tracker_new_for_item_submenu (GtkMenuTrackerItem       *item,
+                                       GtkMenuTrackerInsertFunc  insert_func,
+                                       GtkMenuTrackerRemoveFunc  remove_func,
+                                       gpointer                  user_data)
+{
+  return gtk_menu_tracker_new (_gtk_menu_tracker_item_get_observable (item),
+                               _gtk_menu_tracker_item_get_submenu (item),
+                               TRUE,
+                               _gtk_menu_tracker_item_get_submenu_namespace (item),
+                               insert_func, remove_func, user_data);
+}
+
 /*< private >
  * gtk_menu_tracker_free:
  * @tracker: a #GtkMenuTracker
@@ -430,5 +490,6 @@ void
 gtk_menu_tracker_free (GtkMenuTracker *tracker)
 {
   gtk_menu_tracker_section_free (tracker->toplevel);
+  g_object_unref (tracker->observable);
   g_slice_free (GtkMenuTracker, tracker);
 }
diff --git a/src/gtkmenutracker.h b/src/gtkmenutracker.h
index 51810a1..96370ad 100644
--- a/src/gtkmenutracker.h
+++ b/src/gtkmenutracker.h
@@ -22,30 +22,31 @@
 #ifndef __GTK_MENU_TRACKER_H__
 #define __GTK_MENU_TRACKER_H__
 
-#include <gio/gio.h>
+#include "gtkmenutrackeritem.h"
 
 typedef struct _GtkMenuTracker GtkMenuTracker;
 
-typedef void         (* GtkMenuTrackerInsertFunc)       (gint                      position,
-                                                         GMenuModel               *model,
-                                                         gint                      item_index,
-                                                         const gchar              *action_namespace,
-                                                         gboolean                  is_separator,
-                                                         gpointer                  user_data);
+typedef void         (* GtkMenuTrackerInsertFunc)                       (GtkMenuTrackerItem       *item,
+                                                                         gint                      position,
+                                                                         gpointer                  
user_data);
 
-typedef void         (* GtkMenuTrackerRemoveFunc)       (gint                      position,
-                                                         gpointer                  user_data);
+typedef void         (* GtkMenuTrackerRemoveFunc)                       (gint                      position,
+                                                                         gpointer                  
user_data);
 
 
-G_GNUC_INTERNAL
-GtkMenuTracker *        gtk_menu_tracker_new            (GMenuModel               *model,
-                                                         gboolean                  with_separators,
-                                                         const gchar              *action_namespace,
-                                                         GtkMenuTrackerInsertFunc  insert_func,
-                                                         GtkMenuTrackerRemoveFunc  remove_func,
-                                                         gpointer                  user_data);
+GtkMenuTracker *        gtk_menu_tracker_new                            (GtkActionObservable      *observer,
+                                                                         GMenuModel               *model,
+                                                                         gboolean                  
with_separators,
+                                                                         const gchar              
*action_namespace,
+                                                                         GtkMenuTrackerInsertFunc  
insert_func,
+                                                                         GtkMenuTrackerRemoveFunc  
remove_func,
+                                                                         gpointer                  
user_data);
 
-G_GNUC_INTERNAL
-void                    gtk_menu_tracker_free           (GtkMenuTracker           *tracker);
+GtkMenuTracker *        gtk_menu_tracker_new_for_item_submenu           (GtkMenuTrackerItem       *item,
+                                                                         GtkMenuTrackerInsertFunc  
insert_func,
+                                                                         GtkMenuTrackerRemoveFunc  
remove_func,
+                                                                         gpointer                  
user_data);
+
+void                    gtk_menu_tracker_free                           (GtkMenuTracker           *tracker);
 
 #endif /* __GTK_MENU_TRACKER_H__ */
diff --git a/src/gtkmenutrackeritem.c b/src/gtkmenutrackeritem.c
new file mode 100644
index 0000000..2d712a1
--- /dev/null
+++ b/src/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: GtkMenuTrackerItem
+ * @Short_description: Small helper for model menu items
+ *
+ * A #GtkMenuTrackerItem is a small helper class used by #GtkMenuTracker 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 GtkMenuTrackerItem::toggled property is true. As an
+ * example, if the item has the role %GTK_MENU_TRACKER_ITEM_ROLE_CHECK and
+ * GtkMenuTrackerItem::toggled is %FALSE, its accessible role should be that of
+ * a check menu item, and no decoration should be drawn. But if
+ * GtkMenuTrackerItem::toggled is %TRUE, a checkmark should be drawn.
+ *
+ * All properties except for the two class-determining properties,
+ * GtkMenuTrackerItem::is-separator and GtkMenuTrackerItem::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 gtk_menu_tracker_item_activated() in
+ * response. The #GtkMenuTrackerItem 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 gtk_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 GtkMenuTrackerItem::submenu-shown property,
+ * and if a user clicks on the submenu, do not immediately show the menu,
+ * but call gtk_menu_tracker_item_request_submenu_shown() and wait for the
+ * GtkMenuTrackerItem::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,
+ * gtk_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 GtkMenuTrackerItemClass;
+
+struct _GtkMenuTrackerItem
+{
+  GObject parent_instance;
+
+  GtkActionObservable *observable;
+  gchar *action_namespace;
+  GMenuItem *item;
+  GtkMenuTrackerItemRole 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 *gtk_menu_tracker_item_pspecs[N_PROPS];
+
+static void gtk_menu_tracker_item_init_observer_iface (GtkActionObserverInterface *iface);
+G_DEFINE_TYPE_WITH_CODE (GtkMenuTrackerItem, gtk_menu_tracker_item, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_ACTION_OBSERVER, 
gtk_menu_tracker_item_init_observer_iface))
+
+GType
+gtk_menu_tracker_item_role_get_type (void)
+{
+  static gsize gtk_menu_tracker_item_role_type;
+
+  if (g_once_init_enter (&gtk_menu_tracker_item_role_type))
+    {
+      static const GEnumValue values[] = {
+        { GTK_MENU_TRACKER_ITEM_ROLE_NORMAL, "GTK_MENU_TRACKER_ITEM_ROLE_NORMAL", "normal" },
+        { GTK_MENU_TRACKER_ITEM_ROLE_CHECK, "GTK_MENU_TRACKER_ITEM_ROLE_CHECK", "check" },
+        { GTK_MENU_TRACKER_ITEM_ROLE_RADIO, "GTK_MENU_TRACKER_ITEM_ROLE_RADIO", "radio" },
+        { 0, NULL, NULL }
+      };
+      GType type;
+
+      type = g_enum_register_static ("GtkMenuTrackerItemRole", values);
+
+      g_once_init_leave (&gtk_menu_tracker_item_role_type, type);
+    }
+
+  return gtk_menu_tracker_item_role_type;
+}
+
+static void
+gtk_menu_tracker_item_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (object);
+
+  switch (prop_id)
+    {
+    case PROP_IS_SEPARATOR:
+      g_value_set_boolean (value, gtk_menu_tracker_item_get_is_separator (self));
+      break;
+    case PROP_HAS_SUBMENU:
+      g_value_set_boolean (value, gtk_menu_tracker_item_get_has_submenu (self));
+      break;
+    case PROP_LABEL:
+      g_value_set_string (value, gtk_menu_tracker_item_get_label (self));
+      break;
+    case PROP_ICON:
+      g_value_set_object (value, gtk_menu_tracker_item_get_icon (self));
+      break;
+    case PROP_SENSITIVE:
+      g_value_set_boolean (value, gtk_menu_tracker_item_get_sensitive (self));
+      break;
+    case PROP_VISIBLE:
+      g_value_set_boolean (value, gtk_menu_tracker_item_get_visible (self));
+      break;
+    case PROP_ROLE:
+      g_value_set_enum (value, gtk_menu_tracker_item_get_role (self));
+      break;
+    case PROP_TOGGLED:
+      g_value_set_boolean (value, gtk_menu_tracker_item_get_toggled (self));
+      break;
+    case PROP_ACCEL:
+      g_value_set_string (value, gtk_menu_tracker_item_get_accel (self));
+      break;
+    case PROP_SUBMENU_SHOWN:
+      g_value_set_boolean (value, gtk_menu_tracker_item_get_submenu_shown (self));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+  }
+}
+
+static void
+gtk_menu_tracker_item_finalize (GObject *object)
+{
+  GtkMenuTrackerItem *self = GTK_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 (gtk_menu_tracker_item_parent_class)->finalize (object);
+}
+
+static void
+gtk_menu_tracker_item_init (GtkMenuTrackerItem * self)
+{
+}
+
+static void
+gtk_menu_tracker_item_class_init (GtkMenuTrackerItemClass *class)
+{
+  class->get_property = gtk_menu_tracker_item_get_property;
+  class->finalize = gtk_menu_tracker_item_finalize;
+
+  gtk_menu_tracker_item_pspecs[PROP_IS_SEPARATOR] =
+    g_param_spec_boolean ("is-separator", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  gtk_menu_tracker_item_pspecs[PROP_HAS_SUBMENU] =
+    g_param_spec_boolean ("has-submenu", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  gtk_menu_tracker_item_pspecs[PROP_LABEL] =
+    g_param_spec_string ("label", "", "", NULL, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  gtk_menu_tracker_item_pspecs[PROP_ICON] =
+    g_param_spec_object ("icon", "", "", G_TYPE_ICON, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  gtk_menu_tracker_item_pspecs[PROP_SENSITIVE] =
+    g_param_spec_boolean ("sensitive", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  gtk_menu_tracker_item_pspecs[PROP_VISIBLE] =
+    g_param_spec_boolean ("visible", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  gtk_menu_tracker_item_pspecs[PROP_ROLE] =
+    g_param_spec_enum ("role", "", "",
+                       GTK_TYPE_MENU_TRACKER_ITEM_ROLE, GTK_MENU_TRACKER_ITEM_ROLE_NORMAL,
+                       G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  gtk_menu_tracker_item_pspecs[PROP_TOGGLED] =
+    g_param_spec_boolean ("toggled", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  gtk_menu_tracker_item_pspecs[PROP_ACCEL] =
+    g_param_spec_string ("accel", "", "", NULL, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  gtk_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, gtk_menu_tracker_item_pspecs);
+}
+
+static void
+gtk_menu_tracker_item_action_added (GtkActionObserver   *observer,
+                                    GtkActionObservable *observable,
+                                    const gchar         *action_name,
+                                    const GVariantType  *parameter_type,
+                                    gboolean             enabled,
+                                    GVariant            *state)
+{
+  GtkMenuTrackerItem *self = GTK_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 = GTK_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 = GTK_MENU_TRACKER_ITEM_ROLE_CHECK;
+    }
+
+  g_object_freeze_notify (G_OBJECT (self));
+
+  if (self->sensitive)
+    g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_SENSITIVE]);
+
+  if (self->toggled)
+    g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_TOGGLED]);
+
+  if (self->role != GTK_MENU_TRACKER_ITEM_ROLE_NORMAL)
+    g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_ROLE]);
+
+  g_object_thaw_notify (G_OBJECT (self));
+
+  if (action_target)
+    g_variant_unref (action_target);
+}
+
+static void
+gtk_menu_tracker_item_action_enabled_changed (GtkActionObserver   *observer,
+                                              GtkActionObservable *observable,
+                                              const gchar         *action_name,
+                                              gboolean             enabled)
+{
+  GtkMenuTrackerItem *self = GTK_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), gtk_menu_tracker_item_pspecs[PROP_SENSITIVE]);
+}
+
+static void
+gtk_menu_tracker_item_action_state_changed (GtkActionObserver   *observer,
+                                            GtkActionObservable *observable,
+                                            const gchar         *action_name,
+                                            GVariant            *state)
+{
+  GtkMenuTrackerItem *self = GTK_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), gtk_menu_tracker_item_pspecs[PROP_TOGGLED]);
+}
+
+static void
+gtk_menu_tracker_item_action_removed (GtkActionObserver   *observer,
+                                      GtkActionObservable *observable,
+                                      const gchar         *action_name)
+{
+  GtkMenuTrackerItem *self = GTK_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), gtk_menu_tracker_item_pspecs[PROP_SENSITIVE]);
+    }
+
+  if (self->toggled)
+    {
+      self->toggled = FALSE;
+      g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_TOGGLED]);
+    }
+
+  if (self->role != GTK_MENU_TRACKER_ITEM_ROLE_NORMAL)
+    {
+      self->role = GTK_MENU_TRACKER_ITEM_ROLE_NORMAL;
+      g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_ROLE]);
+    }
+
+  g_object_thaw_notify (G_OBJECT (self));
+}
+
+static void
+gtk_menu_tracker_item_init_observer_iface (GtkActionObserverInterface *iface)
+{
+  iface->action_added = gtk_menu_tracker_item_action_added;
+  iface->action_enabled_changed = gtk_menu_tracker_item_action_enabled_changed;
+  iface->action_state_changed = gtk_menu_tracker_item_action_state_changed;
+  iface->action_removed = gtk_menu_tracker_item_action_removed;
+}
+
+GtkMenuTrackerItem *
+_gtk_menu_tracker_item_new (GtkActionObservable *observable,
+                            GMenuModel          *model,
+                            gint                 item_index,
+                            const gchar         *action_namespace,
+                            gboolean             is_separator)
+{
+  GtkMenuTrackerItem *self;
+  const gchar *action_name;
+
+  g_return_val_if_fail (GTK_IS_ACTION_OBSERVABLE (observable), NULL);
+  g_return_val_if_fail (G_IS_MENU_MODEL (model), NULL);
+
+  self = g_object_new (GTK_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, GTK_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, GTK_ACTION_OBSERVER 
(self));
+          found = g_action_group_query_action (group, action_name, &enabled, &parameter_type, NULL, NULL, 
&state);
+        }
+
+      if (found)
+        gtk_menu_tracker_item_action_added (GTK_ACTION_OBSERVER (self), observable, NULL, parameter_type, 
enabled, state);
+      else
+        gtk_menu_tracker_item_action_removed (GTK_ACTION_OBSERVER (self), observable, NULL);
+
+      if (state)
+        g_variant_unref (state);
+    }
+  else
+    self->sensitive = TRUE;
+
+  return self;
+}
+
+GtkActionObservable *
+_gtk_menu_tracker_item_get_observable (GtkMenuTrackerItem *self)
+{
+  return self->observable;
+}
+
+/**
+ * gtk_menu_tracker_item_get_is_separator:
+ * @self: A #GtkMenuTrackerItem instance
+ *
+ * Returns whether the menu item is a separator. If so, only
+ * certain properties may need to be obeyed. See the documentation
+ * for #GtkMenuTrackerItem.
+ */
+gboolean
+gtk_menu_tracker_item_get_is_separator (GtkMenuTrackerItem *self)
+{
+  return self->is_separator;
+}
+
+/**
+ * gtk_menu_tracker_item_get_has_submenu:
+ * @self: A #GtkMenuTrackerItem instance
+ *
+ * Returns whether the menu item has a submenu. If so, only
+ * certain properties may need to be obeyed. See the documentation
+ * for #GtkMenuTrackerItem.
+ */
+gboolean
+gtk_menu_tracker_item_get_has_submenu (GtkMenuTrackerItem *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 *
+gtk_menu_tracker_item_get_label (GtkMenuTrackerItem *self)
+{
+  const gchar *label = NULL;
+
+  g_menu_item_get_attribute (self->item, G_MENU_ATTRIBUTE_LABEL, "&s", &label);
+
+  return label;
+}
+
+/**
+ * gtk_menu_tracker_item_get_icon:
+ *
+ * Returns: (transfer full):
+ */
+GIcon *
+gtk_menu_tracker_item_get_icon (GtkMenuTrackerItem *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
+gtk_menu_tracker_item_get_sensitive (GtkMenuTrackerItem *self)
+{
+  return self->sensitive;
+}
+
+gboolean
+gtk_menu_tracker_item_get_visible (GtkMenuTrackerItem *self)
+{
+  return TRUE;
+}
+
+GtkMenuTrackerItemRole
+gtk_menu_tracker_item_get_role (GtkMenuTrackerItem *self)
+{
+  return self->role;
+}
+
+gboolean
+gtk_menu_tracker_item_get_toggled (GtkMenuTrackerItem *self)
+{
+  return self->toggled;
+}
+
+const gchar *
+gtk_menu_tracker_item_get_accel (GtkMenuTrackerItem *self)
+{
+  const gchar *accel = NULL;
+
+  g_menu_item_get_attribute (self->item, "accel", "&s", &accel);
+
+  return accel;
+}
+
+GMenuModel *
+_gtk_menu_tracker_item_get_submenu (GtkMenuTrackerItem *self)
+{
+  return g_menu_item_get_link (self->item, "submenu");
+}
+
+gchar *
+_gtk_menu_tracker_item_get_submenu_namespace (GtkMenuTrackerItem *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
+gtk_menu_tracker_item_get_should_request_show (GtkMenuTrackerItem *self)
+{
+  return g_menu_item_get_attribute (self->item, "submenu-action", "&s", NULL);
+}
+
+gboolean
+gtk_menu_tracker_item_get_submenu_shown (GtkMenuTrackerItem *self)
+{
+  return self->submenu_shown;
+}
+
+static void
+gtk_menu_tracker_item_set_submenu_shown (GtkMenuTrackerItem *self,
+                                         gboolean            submenu_shown)
+{
+  if (submenu_shown == self->submenu_shown)
+    return;
+
+  self->submenu_shown = submenu_shown;
+  g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_SUBMENU_SHOWN]);
+}
+
+void
+gtk_menu_tracker_item_activated (GtkMenuTrackerItem *self)
+{
+  const gchar *action_name;
+  GVariant *action_target;
+
+  g_return_if_fail (GTK_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
+{
+  GtkMenuTrackerItem *item;
+  gchar              *submenu_action;
+  gboolean            first_time;
+} GtkMenuTrackerOpener;
+
+static void
+gtk_menu_tracker_opener_update (GtkMenuTrackerOpener *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)
+    gtk_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
+gtk_menu_tracker_opener_added (GActionGroup *group,
+                               const gchar  *action_name,
+                               gpointer      user_data)
+{
+  GtkMenuTrackerOpener *opener = user_data;
+
+  if (g_str_equal (action_name, opener->submenu_action))
+    gtk_menu_tracker_opener_update (opener);
+}
+
+static void
+gtk_menu_tracker_opener_removed (GActionGroup *action_group,
+                                 const gchar  *action_name,
+                                 gpointer      user_data)
+{
+  GtkMenuTrackerOpener *opener = user_data;
+
+  if (g_str_equal (action_name, opener->submenu_action))
+    gtk_menu_tracker_opener_update (opener);
+}
+
+static void
+gtk_menu_tracker_opener_changed (GActionGroup *action_group,
+                                 const gchar  *action_name,
+                                 GVariant     *new_state,
+                                 gpointer      user_data)
+{
+  GtkMenuTrackerOpener *opener = user_data;
+
+  if (g_str_equal (action_name, opener->submenu_action))
+    gtk_menu_tracker_opener_update (opener);
+}
+
+static void
+gtk_menu_tracker_opener_free (gpointer data)
+{
+  GtkMenuTrackerOpener *opener = data;
+
+  g_signal_handlers_disconnect_by_func (opener->item->observable, gtk_menu_tracker_opener_added, opener);
+  g_signal_handlers_disconnect_by_func (opener->item->observable, gtk_menu_tracker_opener_removed, opener);
+  g_signal_handlers_disconnect_by_func (opener->item->observable, gtk_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));
+
+  gtk_menu_tracker_item_set_submenu_shown (opener->item, FALSE);
+
+  g_free (opener->submenu_action);
+
+  g_slice_free (GtkMenuTrackerOpener, opener);
+}
+
+static GtkMenuTrackerOpener *
+gtk_menu_tracker_opener_new (GtkMenuTrackerItem *item,
+                             const gchar        *submenu_action)
+{
+  GtkMenuTrackerOpener *opener;
+
+  opener = g_slice_new (GtkMenuTrackerOpener);
+  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 (gtk_menu_tracker_opener_added), opener);
+  g_signal_connect (item->observable, "action-removed", G_CALLBACK (gtk_menu_tracker_opener_removed), 
opener);
+  g_signal_connect (item->observable, "action-state-changed", G_CALLBACK (gtk_menu_tracker_opener_changed), 
opener);
+
+  gtk_menu_tracker_opener_update (opener);
+
+  return opener;
+}
+
+void
+gtk_menu_tracker_item_request_submenu_shown (GtkMenuTrackerItem *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",
+                                gtk_menu_tracker_opener_new (self, submenu_action),
+                                gtk_menu_tracker_opener_free);
+      else
+        g_object_set_data (G_OBJECT (self), "submenu-opener", NULL);
+    }
+  else
+    gtk_menu_tracker_item_set_submenu_shown (self, shown);
+}
diff --git a/src/gtkmenutrackeritem.h b/src/gtkmenutrackeritem.h
new file mode 100644
index 0000000..9db30eb
--- /dev/null
+++ b/src/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 __GTK_MENU_TRACKER_ITEM_H__
+#define __GTK_MENU_TRACKER_ITEM_H__
+
+#include "gtkactionobservable.h"
+
+#define GTK_TYPE_MENU_TRACKER_ITEM                          (gtk_menu_tracker_item_get_type ())
+#define GTK_MENU_TRACKER_ITEM(inst)                         (G_TYPE_CHECK_INSTANCE_CAST ((inst), \
+                                                             GTK_TYPE_MENU_TRACKER_ITEM, GtkMenuTrackerItem))
+#define GTK_IS_MENU_TRACKER_ITEM(inst)                      (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \
+                                                             GTK_TYPE_MENU_TRACKER_ITEM))
+
+typedef struct _GtkMenuTrackerItem GtkMenuTrackerItem;
+
+#define GTK_TYPE_MENU_TRACKER_ITEM_ROLE                     (gtk_menu_tracker_item_role_get_type ())
+
+typedef enum  {
+  GTK_MENU_TRACKER_ITEM_ROLE_NORMAL,
+  GTK_MENU_TRACKER_ITEM_ROLE_CHECK,
+  GTK_MENU_TRACKER_ITEM_ROLE_RADIO,
+} GtkMenuTrackerItemRole;
+
+GType                   gtk_menu_tracker_item_get_type                  (void) G_GNUC_CONST;
+
+GType                   gtk_menu_tracker_item_role_get_type             (void) G_GNUC_CONST;
+
+GtkMenuTrackerItem *   _gtk_menu_tracker_item_new                       (GtkActionObservable *observable,
+                                                                         GMenuModel          *model,
+                                                                         gint                 item_index,
+                                                                         const gchar         
*action_namespace,
+                                                                         gboolean             is_separator);
+
+GtkActionObservable *  _gtk_menu_tracker_item_get_observable            (GtkMenuTrackerItem *self);
+
+gboolean                gtk_menu_tracker_item_get_is_separator          (GtkMenuTrackerItem *self);
+
+gboolean                gtk_menu_tracker_item_get_has_submenu           (GtkMenuTrackerItem *self);
+
+const gchar *           gtk_menu_tracker_item_get_label                 (GtkMenuTrackerItem *self);
+
+GIcon *                 gtk_menu_tracker_item_get_icon                  (GtkMenuTrackerItem *self);
+
+gboolean                gtk_menu_tracker_item_get_sensitive             (GtkMenuTrackerItem *self);
+
+gboolean                gtk_menu_tracker_item_get_visible               (GtkMenuTrackerItem *self);
+
+GtkMenuTrackerItemRole  gtk_menu_tracker_item_get_role                  (GtkMenuTrackerItem *self);
+
+gboolean                gtk_menu_tracker_item_get_toggled               (GtkMenuTrackerItem *self);
+
+const gchar *           gtk_menu_tracker_item_get_accel                 (GtkMenuTrackerItem *self);
+
+GMenuModel *           _gtk_menu_tracker_item_get_submenu               (GtkMenuTrackerItem *self);
+
+gchar *                _gtk_menu_tracker_item_get_submenu_namespace     (GtkMenuTrackerItem *self);
+
+gboolean                gtk_menu_tracker_item_get_should_request_show   (GtkMenuTrackerItem *self);
+
+void                    gtk_menu_tracker_item_activated                 (GtkMenuTrackerItem *self);
+
+void                    gtk_menu_tracker_item_request_submenu_shown     (GtkMenuTrackerItem *self,
+                                                                         gboolean            shown);
+
+gboolean                gtk_menu_tracker_item_get_submenu_shown         (GtkMenuTrackerItem *self);
+
+#endif
diff --git a/src/shell-menu-tracker.c b/src/shell-menu-tracker.c
index 788605e..ac1c8b2 100644
--- a/src/shell-menu-tracker.c
+++ b/src/shell-menu-tracker.c
@@ -25,6 +25,12 @@
 #include "shell-menu-tracker.h"
 #include "gtkmenutracker.h"
 
+/**
+ * SECTION:shell-menu-tracker
+ * @short_description: a simple wrapper around #GtkMenuTracker
+ *                     to make it bindable.
+ */
+
 struct _ShellMenuTracker
 {
   guint ref_count;
@@ -40,17 +46,12 @@ struct _ShellMenuTracker
 };
 
 static void
-shell_menu_tracker_insert_func (gint position,
-                                GMenuModel *model,
-                                gint item_index,
-                                const gchar *action_namespace,
-                                gboolean is_separator,
+shell_menu_tracker_insert_func (GtkMenuTrackerItem *item,
+                                gint position,
                                 gpointer user_data)
 {
   ShellMenuTracker *tracker = (ShellMenuTracker *) user_data;
-  tracker->insert_func (position, model, item_index,
-                        action_namespace, is_separator,
-                        tracker->insert_user_data);
+  tracker->insert_func (item, position, tracker->insert_user_data);
 }
 
 static void
@@ -63,6 +64,7 @@ shell_menu_tracker_remove_func (gint position,
 
 /**
  * shell_menu_tracker_new:
+ * @observable:
  * @model:
  * @action_namespace: (allow-none):
  * @insert_func:
@@ -73,7 +75,8 @@ shell_menu_tracker_remove_func (gint position,
  * @remove_notify:
  */
 ShellMenuTracker *
-shell_menu_tracker_new (GMenuModel                 *model,
+shell_menu_tracker_new (GtkActionObservable        *observable,
+                        GMenuModel                 *model,
                         const gchar                *action_namespace,
                         ShellMenuTrackerInsertFunc  insert_func,
                         gpointer                    insert_user_data,
@@ -92,7 +95,8 @@ shell_menu_tracker_new (GMenuModel                 *model,
   tracker->remove_user_data = remove_user_data;
   tracker->remove_notify = remove_notify;
 
-  tracker->tracker = gtk_menu_tracker_new (model,
+  tracker->tracker = gtk_menu_tracker_new (observable,
+                                           model,
                                            TRUE, /* with separators */
                                            action_namespace,
                                            shell_menu_tracker_insert_func,
diff --git a/src/shell-menu-tracker.h b/src/shell-menu-tracker.h
index 9af602e..df1482a 100644
--- a/src/shell-menu-tracker.h
+++ b/src/shell-menu-tracker.h
@@ -25,21 +25,21 @@
 
 #include <gio/gio.h>
 
+#include "gtkmenutrackeritem.h"
+
 typedef struct _ShellMenuTracker ShellMenuTracker;
 
 GType shell_menu_tracker_get_type (void) G_GNUC_CONST;
 
-typedef void         (* ShellMenuTrackerInsertFunc)       (gint                      position,
-                                                           GMenuModel               *model,
-                                                           gint                      item_index,
-                                                           const gchar              *action_namespace,
-                                                           gboolean                  is_separator,
+typedef void         (* ShellMenuTrackerInsertFunc)       (GtkMenuTrackerItem       *item,
+                                                           gint                      position,
                                                            gpointer                  user_data);
 
 typedef void         (* ShellMenuTrackerRemoveFunc)       (gint                      position,
                                                            gpointer                  user_data);
 
-ShellMenuTracker * shell_menu_tracker_new (GMenuModel                 *model,
+ShellMenuTracker * shell_menu_tracker_new (GtkActionObservable        *observable,
+                                           GMenuModel                 *model,
                                            const gchar                *action_namespace,
                                            ShellMenuTrackerInsertFunc  insert_func,
                                            gpointer                    insert_user_data,


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