[gnome-shell/wip/menus-rebase2: 2/3] Application Menu: add support for showing GApplication actions



commit f884dbbfb2b0f2de4727354704dc6f00b0432b68
Author: Giovanni Campagna <gcampagna src gnome org>
Date:   Sun May 15 18:55:23 2011 +0200

    Application Menu: add support for showing GApplication actions
    
    Use the new GApplication support in ShellApp to create the application
    menu. Supports plain (no state), boolean and double actions.
    Includes a test application (as no other application uses GApplication
    for actions)
    
    https://bugzilla.gnome.org/show_bug.cgi?id=621203

 js/ui/panel.js           |   52 +++++++---
 js/ui/panelMenu.js       |   34 +++++--
 js/ui/popupMenu.js       |  242 ++++++++++++++++++++++++++++++++++++++++++++++
 src/test-gapplication.js |    7 +-
 4 files changed, 310 insertions(+), 25 deletions(-)
---
diff --git a/js/ui/panel.js b/js/ui/panel.js
index a9344bc..78a14a1 100644
--- a/js/ui/panel.js
+++ b/js/ui/panel.js
@@ -3,6 +3,7 @@
 const Cairo = imports.cairo;
 const Clutter = imports.gi.Clutter;
 const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
 const Lang = imports.lang;
 const Mainloop = imports.mainloop;
 const Pango = imports.gi.Pango;
@@ -235,10 +236,12 @@ const AppMenuButton = new Lang.Class({
     Name: 'AppMenuButton',
     Extends: PanelMenu.Button,
 
-    _init: function() {
-        this.parent(0.0);
+    _init: function(menuManager) {
+        this.parent(0.0, true);
+
         this._startingApps = [];
 
+        this._menuManager = menuManager;
         this._targetApp = null;
 
         let bin = new St.Bin({ name: 'appMenu' });
@@ -264,10 +267,6 @@ const AppMenuButton = new Lang.Class({
 
         this._iconBottomClip = 0;
 
-        this._quitMenu = new PopupMenu.PopupMenuItem('');
-        this.menu.addMenuItem(this._quitMenu);
-        this._quitMenu.connect('activate', Lang.bind(this, this._onQuit));
-
         this._visible = !Main.overview.visible;
         if (!this._visible)
             this.actor.hide();
@@ -446,12 +445,6 @@ const AppMenuButton = new Lang.Class({
         }
     },
 
-    _onQuit: function() {
-        if (this._targetApp == null)
-            return;
-        this._targetApp.request_quit();
-    },
-
     _onAppStateChanged: function(appSys, app) {
         let state = app.state;
         if (state != Shell.AppState.STARTING) {
@@ -513,8 +506,10 @@ const AppMenuButton = new Lang.Class({
         }
 
         if (targetApp == this._targetApp) {
-            if (targetApp && targetApp.get_state() != Shell.AppState.STARTING)
+            if (targetApp && targetApp.get_state() != Shell.AppState.STARTING) {
                 this.stopAnimation();
+                this._maybeSetMenu();
+            }
             return;
         }
 
@@ -528,16 +523,40 @@ const AppMenuButton = new Lang.Class({
         let icon = targetApp.get_faded_icon(2 * PANEL_ICON_SIZE);
 
         this._label.setText(targetApp.get_name());
-        // TODO - _quit() doesn't really work on apps in state STARTING yet
-        this._quitMenu.label.set_text(_("Quit %s").format(targetApp.get_name()));
 
         this._iconBox.set_child(icon);
         this._iconBox.show();
 
         if (targetApp.get_state() == Shell.AppState.STARTING)
             this.startAnimation();
+        else
+            this._maybeSetMenu();
 
         this.emit('changed');
+    },
+
+    _maybeSetMenu: function() {
+        let menu;
+
+        if (this._targetApp.action_group) {
+            if (this.menu instanceof PopupMenu.RemoteMenu &&
+                this.menu.actionGroup == this._targetApp.action_group)
+                return;
+
+            menu = new PopupMenu.RemoteMenu(this.actor, this._targetApp.menu, this._targetApp.action_group);
+        } else {
+            if (this.menu && !(this.menu instanceof PopupMenu.RemoteMenu))
+                return;
+
+            // fallback to older menu
+            menu = new PopupMenu.PopupMenu(this.actor, 0.0, St.Side.TOP, 0);
+            menu.addAction(_("Quit"), Lang.bind(this, function() {
+                this._targetApp.request_quit();
+            }));
+        }
+
+        this.setMenu(menu);
+        this._menuManager.addMenu(menu);
     }
 });
 
@@ -924,9 +943,8 @@ const Panel = new Lang.Class({
             // more cleanly with the rest of the panel
             this._menus.addMenu(this._activitiesButton.menu);
 
-            this._appMenu = new AppMenuButton();
+            this._appMenu = new AppMenuButton(this._menus);
             this._leftBox.add(this._appMenu.actor);
-            this._menus.addMenu(this._appMenu.menu);
         }
 
         /* center */
diff --git a/js/ui/panelMenu.js b/js/ui/panelMenu.js
index 927311b..126d2cc 100644
--- a/js/ui/panelMenu.js
+++ b/js/ui/panelMenu.js
@@ -96,22 +96,39 @@ const Button = new Lang.Class({
     Name: 'PanelMenuButton',
     Extends: ButtonBox,
 
-    _init: function(menuAlignment) {
+    _init: function(menuAlignment, dontCreateMenu) {
         this.parent({ reactive: true,
                       can_focus: true,
                       track_hover: true });
 
         this.actor.connect('button-press-event', Lang.bind(this, this._onButtonPress));
         this.actor.connect('key-press-event', Lang.bind(this, this._onSourceKeyPress));
-        this.menu = new PopupMenu.PopupMenu(this.actor, menuAlignment, St.Side.TOP);
-        this.menu.actor.add_style_class_name('panel-menu');
-        this.menu.connect('open-state-changed', Lang.bind(this, this._onOpenStateChanged));
-        this.menu.actor.connect('key-press-event', Lang.bind(this, this._onMenuKeyPress));
-        Main.uiGroup.add_actor(this.menu.actor);
-        this.menu.actor.hide();
+
+        if (dontCreateMenu)
+            this.menu = null;
+        else
+            this.setMenu(new PopupMenu.PopupMenu(this.actor, menuAlignment, St.Side.TOP, 0));
+    },
+
+    setMenu: function(menu) {
+        if (this.menu)
+            this.menu.destroy();
+
+        this.menu = menu;
+        if (this.menu) {
+            this.menu.actor.add_style_class_name('panel-menu');
+            this.menu.connect('open-state-changed', Lang.bind(this, this._onOpenStateChanged));
+            this.menu.actor.connect('key-press-event', Lang.bind(this, this._onMenuKeyPress));
+
+            Main.uiGroup.add_actor(this.menu.actor);
+            this.menu.actor.hide();
+        }
     },
 
     _onButtonPress: function(actor, event) {
+        if (!this.menu)
+            return;
+
         if (!this.menu.isOpen) {
             // Setting the max-height won't do any good if the minimum height of the
             // menu is higher then the screen; it's useful if part of the menu is
@@ -125,6 +142,9 @@ const Button = new Lang.Class({
     },
 
     _onSourceKeyPress: function(actor, event) {
+        if (!this.menu)
+            return false;
+
         let symbol = event.get_key_symbol();
         if (symbol == Clutter.KEY_space || symbol == Clutter.KEY_Return) {
             this.menu.toggle();
diff --git a/js/ui/popupMenu.js b/js/ui/popupMenu.js
index 5652fc4..c6dc3df 100644
--- a/js/ui/popupMenu.js
+++ b/js/ui/popupMenu.js
@@ -2,7 +2,9 @@
 
 const Cairo = imports.cairo;
 const Clutter = imports.gi.Clutter;
+const GLib = imports.gi.GLib;
 const Gtk = imports.gi.Gtk;
+const Gio = imports.gi.Gio;
 const Lang = imports.lang;
 const Shell = imports.gi.Shell;
 const Signals = imports.signals;
@@ -1692,6 +1694,246 @@ const PopupComboBoxMenuItem = new Lang.Class({
     }
 });
 
+/**
+ * RemoteMenu:
+ *
+ * A PopupMenu that tracks a GMenuModel and shows its actions
+ * (exposed by GApplication/GActionGroup)
+ */
+const RemoteMenu = new Lang.Class({
+    Name: 'RemoteMenu',
+    Extends: PopupMenu,
+
+    _init: function(sourceActor, model, actionGroup) {
+        this.parent(sourceActor, 0.0, St.Side.TOP);
+
+        this.model = model;
+        this.actionGroup = actionGroup;
+
+        this._actions = { };
+        this._modelChanged(this.model, 0, 0, this.model.get_n_items(), this);
+
+        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();
+    },
+
+    _createMenuItem: function(model, index) {
+        let section_link = model.get_item_link(index, Gio.MENU_LINK_SECTION);
+        if (section_link) {
+            let item = new PopupMenuSection();
+            this._modelChanged(section_link, 0, 0, section_link.get_n_items(), item);
+            return [item, true, ''];
+        }
+
+        // labels are not checked for existance, as they're required for all items
+        let label = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_LABEL, null).deep_unpack();
+        // remove all underscores that are not followed by another underscore
+        label = label.replace(/_([^_])/, '$1');
+        let submenu_link = model.get_item_link(index, Gio.MENU_LINK_SUBMENU);
+
+        if (submenu_link) {
+            let item = new PopupSubMenuMenuItem(label);
+            this._modelChanged(submenu_link, 0, 0, submenu_link.get_n_items(), item.menu);
+            return [item, false, ''];
+        }
+
+        let action_id = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_ACTION, null).deep_unpack();
+        if (!this.actionGroup.has_action(action_id)) {
+            // the action may not be there yet, wait for action-added
+            return [null, false, 'action-added'];
+        }
+
+        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 item, 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':
+                item = new PopupSwitchMenuItem(label, action.state.get_boolean());
+                action.items.push(item);
+                specificSignalId = item.connect('toggled', Lang.bind(this, function(item) {
+                    this.actionGroup.activate_action(action_id, null);
+                }));
+                break;
+            case 's':
+                item = new PopupMenuItem(label);
+                item._remoteTarget = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_TARGET, null).deep_unpack();
+                action.items.push(item);
+                item.setShowDot(action.state.deep_unpack() == item._remoteTarget);
+                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 [null, false, 'action-state-changed'];
+            }
+        } else {
+            target = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_TARGET, null);
+            item = new PopupMenuItem(label);
+            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;
+        if (action.enabled)
+            item.actor.remove_style_pseudo_class('insensitive');
+        else
+            item.actor.add_style_pseudo_class('insensitive');
+
+        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);
+        }));
+
+        return [item, false, ''];
+    }, 
+
+    _modelChanged: function(model, position, removed, added, target) {
+        let j, k;
+        let j0, k0;
+
+        let currentItems = target._getMenuItems();
+
+        for (j0 = 0, k0 = 0; j0 < position; j0++, k0++) {
+            if (currentItems[k0] instanceof PopupSeparatorMenuItem)
+                k0++;
+        }
+
+        if (removed == -1) {
+            // special flag to indicate we should destroy everything
+            for (k = k0; k < currentItems.length; k++)
+                currentItems[k].destroy();
+        } else {
+            for (j = j0, k = k0; j < j0 + removed; j++, k++) {
+                currentItems[k].destroy();
+
+                if (currentItems[k] instanceof PopupSeparatorMenuItem)
+                    j--;
+            }
+        }
+
+        for (j = j0, k = k0; j < j0 + added; j++, k++) {
+            let [item, addSeparator, changeSignal] = this._createMenuItem(model, j);
+
+            if (item) {
+                // separators must be added in the parent to make autohiding work
+                if (addSeparator) {
+                    target.addMenuItem(new PopupSeparatorMenuItem(), k+1);
+                    k++;
+                }
+
+                target.addMenuItem(item, k);
+
+                if (addSeparator) {
+                    target.addMenuItem(new PopupSeparatorMenuItem(), k+1);
+                    k++;
+                }
+            } else if (changeSignal) {
+                let signalId = this.actionGroup.connect(changeSignal, Lang.bind(this, function() {
+                    this.actionGroup.disconnect(signalId);
+
+                    // force a full update
+                    this._modelChanged(model, 0, -1, model.get_n_items(), target);
+                }));
+            }
+        }
+
+        if (!model._changedId) {
+            model._changedId = model.connect('items-changed', Lang.bind(this, this._modelChanged, target));
+            model._destroyId = target.connect('destroy', function() {
+                if (model._changedId)
+                    model.disconnect(model._changedId);
+                if (model._destroyId)
+                    target.disconnect(model._destroyId);
+                model._changedId = 0;
+                model._destroyId = 0;
+            });
+        }
+
+        if (target instanceof PopupMenuSection) {
+            target.actor.visible = target.numMenuItems != 0;
+        } else {
+            let sourceItem = target.sourceActor._delegate;
+            if (sourceItem instanceof PopupSubMenuMenuItem)
+                sourceItem.actor.visible = target.numMenuItems != 0;
+        }
+    },
+
+    _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].setToggleState(action.state.get_boolean());
+                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].setShowDot(action.items[i]._remoteTarget == action.state.deep_unpack());
+            }
+        }
+    },
+
+    _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;
+
+                if (action.enabled)
+                    item.actor.remove_style_pseudo_class('insensitive');
+                else
+                    item.actor.add_style_pseudo_class('insensitive');
+            }
+        }
+    }
+});
+
 /* Basic implementation of a menu manager.
  * Call addMenu to add menus
  */
diff --git a/src/test-gapplication.js b/src/test-gapplication.js
index 9872889..5f58b2d 100755
--- a/src/test-gapplication.js
+++ b/src/test-gapplication.js
@@ -13,6 +13,11 @@ function do_action_param(action, parameter) {
     print ("Action '" + action.name + "' invoked with parameter " + parameter.print(true));
 }
 
+function do_action_toggle(action) {
+    action.set_state(GLib.Variant.new('b', !action.state.deep_unpack()));
+    print ("Toggled");
+}
+
 function do_action_state_change(action) {
     print ("Action '" + action.name + "' has now state '" + action.state.deep_unpack() + "'");
 }
@@ -36,7 +41,7 @@ function main() {
     group.insert(action);
 
     let action = Gio.SimpleAction.new_stateful('toggle', null, GLib.Variant.new('b', false));
-    action.connect('activate', do_action);
+    action.connect('activate', do_action_toggle);
     action.connect('notify::state', do_action_state_change);
     group.insert(action);
 



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