summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGiovanni Campagna <gcampagna@src.gnome.org>2011-05-15 18:55:23 +0200
committerColin Walters <walters@verbum.org>2011-11-25 15:06:17 -0500
commit3fd70e37bd3c25be744613f50a0a5fdf175d879d (patch)
treebfe5b00a7ef595fab0dd979fb2b5f64d0ca6713d
parent5580cfaf634e3c0a8c1ead15d66c0bc2baee3ab5 (diff)
downloadgnome-shell-wip/menus.tar.gz
Application Menu: add support for showing GApplication actionswip/menus
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
-rw-r--r--js/ui/panel.js52
-rw-r--r--js/ui/panelMenu.js34
-rw-r--r--js/ui/popupMenu.js250
3 files changed, 312 insertions, 24 deletions
diff --git a/js/ui/panel.js b/js/ui/panel.js
index a9344bc76..78a14a104 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 927311b0e..126d2cc26 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 5652fc4ac..a4f190549 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,254 @@ 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.change_action_state(action_id, GLib.Variant.new_boolean(item.state));
+ }));
+ break;
+ case 'd':
+ item = new PopupSliderMenuItem(label, action.state.get_double());
+ action.items.push(item);
+ // value-changed is emitted for each motion-event, maybe an idle is more appropriate here?
+ specificSignalId = item.connect('value-changed', Lang.bind(this, function(item) {
+ this.actionGroup.change_action_state(action_id, GLib.Variant.new_double(item.value));
+ }));
+ 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.change_action_state(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
*/