Compare commits

...

2 Commits

Author SHA1 Message Date
Giovanni Campagna
3fd70e37bd 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
2011-11-25 15:06:17 -05:00
Giovanni Campagna
5580cfaf63 ShellApp: port to new GDBusActionGroup and GMenuProxy API
GDBusActionGroup and GMenuProxy are new objects in GIO 2.32 that
help with accessing menus and actions of remote applications.
This patch makes it possible for the shell to associate an
application with a dbus name and from that a GMenu, that will
be shown as the application menu.

https://bugzilla.gnome.org/show_bug.cgi?id=621203
2011-11-25 15:06:17 -05:00
8 changed files with 831 additions and 30 deletions

View File

@ -3,6 +3,7 @@
const Cairo = imports.cairo; const Cairo = imports.cairo;
const Clutter = imports.gi.Clutter; const Clutter = imports.gi.Clutter;
const Gio = imports.gi.Gio; const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Lang = imports.lang; const Lang = imports.lang;
const Mainloop = imports.mainloop; const Mainloop = imports.mainloop;
const Pango = imports.gi.Pango; const Pango = imports.gi.Pango;
@ -235,10 +236,12 @@ const AppMenuButton = new Lang.Class({
Name: 'AppMenuButton', Name: 'AppMenuButton',
Extends: PanelMenu.Button, Extends: PanelMenu.Button,
_init: function() { _init: function(menuManager) {
this.parent(0.0); this.parent(0.0, true);
this._startingApps = []; this._startingApps = [];
this._menuManager = menuManager;
this._targetApp = null; this._targetApp = null;
let bin = new St.Bin({ name: 'appMenu' }); let bin = new St.Bin({ name: 'appMenu' });
@ -264,10 +267,6 @@ const AppMenuButton = new Lang.Class({
this._iconBottomClip = 0; 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; this._visible = !Main.overview.visible;
if (!this._visible) if (!this._visible)
this.actor.hide(); 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) { _onAppStateChanged: function(appSys, app) {
let state = app.state; let state = app.state;
if (state != Shell.AppState.STARTING) { if (state != Shell.AppState.STARTING) {
@ -513,8 +506,10 @@ const AppMenuButton = new Lang.Class({
} }
if (targetApp == this._targetApp) { if (targetApp == this._targetApp) {
if (targetApp && targetApp.get_state() != Shell.AppState.STARTING) if (targetApp && targetApp.get_state() != Shell.AppState.STARTING) {
this.stopAnimation(); this.stopAnimation();
this._maybeSetMenu();
}
return; return;
} }
@ -528,16 +523,40 @@ const AppMenuButton = new Lang.Class({
let icon = targetApp.get_faded_icon(2 * PANEL_ICON_SIZE); let icon = targetApp.get_faded_icon(2 * PANEL_ICON_SIZE);
this._label.setText(targetApp.get_name()); 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.set_child(icon);
this._iconBox.show(); this._iconBox.show();
if (targetApp.get_state() == Shell.AppState.STARTING) if (targetApp.get_state() == Shell.AppState.STARTING)
this.startAnimation(); this.startAnimation();
else
this._maybeSetMenu();
this.emit('changed'); 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 // more cleanly with the rest of the panel
this._menus.addMenu(this._activitiesButton.menu); this._menus.addMenu(this._activitiesButton.menu);
this._appMenu = new AppMenuButton(); this._appMenu = new AppMenuButton(this._menus);
this._leftBox.add(this._appMenu.actor); this._leftBox.add(this._appMenu.actor);
this._menus.addMenu(this._appMenu.menu);
} }
/* center */ /* center */

View File

@ -96,22 +96,39 @@ const Button = new Lang.Class({
Name: 'PanelMenuButton', Name: 'PanelMenuButton',
Extends: ButtonBox, Extends: ButtonBox,
_init: function(menuAlignment) { _init: function(menuAlignment, dontCreateMenu) {
this.parent({ reactive: true, this.parent({ reactive: true,
can_focus: true, can_focus: true,
track_hover: true }); track_hover: true });
this.actor.connect('button-press-event', Lang.bind(this, this._onButtonPress)); this.actor.connect('button-press-event', Lang.bind(this, this._onButtonPress));
this.actor.connect('key-press-event', Lang.bind(this, this._onSourceKeyPress)); this.actor.connect('key-press-event', Lang.bind(this, this._onSourceKeyPress));
this.menu = new PopupMenu.PopupMenu(this.actor, menuAlignment, St.Side.TOP);
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.actor.add_style_class_name('panel-menu');
this.menu.connect('open-state-changed', Lang.bind(this, this._onOpenStateChanged)); this.menu.connect('open-state-changed', Lang.bind(this, this._onOpenStateChanged));
this.menu.actor.connect('key-press-event', Lang.bind(this, this._onMenuKeyPress)); this.menu.actor.connect('key-press-event', Lang.bind(this, this._onMenuKeyPress));
Main.uiGroup.add_actor(this.menu.actor); Main.uiGroup.add_actor(this.menu.actor);
this.menu.actor.hide(); this.menu.actor.hide();
}
}, },
_onButtonPress: function(actor, event) { _onButtonPress: function(actor, event) {
if (!this.menu)
return;
if (!this.menu.isOpen) { if (!this.menu.isOpen) {
// Setting the max-height won't do any good if the minimum height of the // 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 // 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) { _onSourceKeyPress: function(actor, event) {
if (!this.menu)
return false;
let symbol = event.get_key_symbol(); let symbol = event.get_key_symbol();
if (symbol == Clutter.KEY_space || symbol == Clutter.KEY_Return) { if (symbol == Clutter.KEY_space || symbol == Clutter.KEY_Return) {
this.menu.toggle(); this.menu.toggle();

View File

@ -2,7 +2,9 @@
const Cairo = imports.cairo; const Cairo = imports.cairo;
const Clutter = imports.gi.Clutter; const Clutter = imports.gi.Clutter;
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk; const Gtk = imports.gi.Gtk;
const Gio = imports.gi.Gio;
const Lang = imports.lang; const Lang = imports.lang;
const Shell = imports.gi.Shell; const Shell = imports.gi.Shell;
const Signals = imports.signals; 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. /* Basic implementation of a menu manager.
* Call addMenu to add menus * Call addMenu to add menus
*/ */

View File

@ -27,6 +27,9 @@ void _shell_app_do_match (ShellApp *app,
GSList **prefix_results, GSList **prefix_results,
GSList **substring_results); GSList **substring_results);
void _shell_app_set_dbus_name (ShellApp *app,
const char *name);
G_END_DECLS G_END_DECLS
#endif /* __SHELL_APP_PRIVATE_H__ */ #endif /* __SHELL_APP_PRIVATE_H__ */

View File

@ -37,6 +37,13 @@ typedef struct {
/* Whether or not we need to resort the windows; this is done on demand */ /* Whether or not we need to resort the windows; this is done on demand */
gboolean window_sort_stale : 1; gboolean window_sort_stale : 1;
/* See GApplication documentation */
guint name_watcher_id;
gchar *dbus_name;
GDBusActionGroup *remote_actions;
GMenuProxy *remote_menu;
GCancellable *dbus_cancellable;
} ShellAppRunningState; } ShellAppRunningState;
/** /**
@ -72,11 +79,13 @@ struct _ShellApp
char *casefolded_exec; char *casefolded_exec;
}; };
G_DEFINE_TYPE (ShellApp, shell_app, G_TYPE_OBJECT);
enum { enum {
PROP_0, PROP_0,
PROP_STATE PROP_STATE,
PROP_ID,
PROP_DBUS_ID,
PROP_ACTION_GROUP,
PROP_MENU
}; };
enum { enum {
@ -89,6 +98,8 @@ static guint shell_app_signals[LAST_SIGNAL] = { 0 };
static void create_running_state (ShellApp *app); static void create_running_state (ShellApp *app);
static void unref_running_state (ShellAppRunningState *state); static void unref_running_state (ShellAppRunningState *state);
G_DEFINE_TYPE (ShellApp, shell_app, G_TYPE_OBJECT)
static void static void
shell_app_get_property (GObject *gobject, shell_app_get_property (GObject *gobject,
guint prop_id, guint prop_id,
@ -102,6 +113,20 @@ shell_app_get_property (GObject *gobject,
case PROP_STATE: case PROP_STATE:
g_value_set_enum (value, app->state); g_value_set_enum (value, app->state);
break; break;
case PROP_ID:
g_value_set_string (value, shell_app_get_id (app));
break;
case PROP_DBUS_ID:
g_value_set_string (value, shell_app_get_dbus_id (app));
break;
case PROP_ACTION_GROUP:
if (app->running_state)
g_value_set_object (value, app->running_state->remote_actions);
break;
case PROP_MENU:
if (app->running_state)
g_value_set_object (value, app->running_state->remote_menu);
break;
default: default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
break; break;
@ -151,6 +176,15 @@ window_backed_app_get_icon (ShellApp *app,
return actor; return actor;
} }
const char *
shell_app_get_dbus_id (ShellApp *app)
{
if (app->running_state)
return app->running_state->dbus_name;
else
return NULL;
}
/** /**
* shell_app_create_icon_texture: * shell_app_create_icon_texture:
* *
@ -948,6 +982,142 @@ _shell_app_remove_window (ShellApp *app,
g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0); g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0);
} }
static void
on_action_group_acquired (GObject *object,
GAsyncResult *result,
gpointer user_data)
{
ShellApp *self = SHELL_APP (user_data);
ShellAppRunningState *state = self->running_state;
GError *error = NULL;
char *object_path;
state->remote_actions = g_dbus_action_group_new_finish (result,
&error);
if (error)
{
if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) &&
!g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD))
{
g_warning ("Unexpected error while reading application actions: %s", error->message);
}
g_clear_error (&error);
g_clear_object (&state->dbus_cancellable);
if (state->name_watcher_id)
{
g_bus_unwatch_name (state->name_watcher_id);
state->name_watcher_id = 0;
}
g_free (state->dbus_name);
state->dbus_name = NULL;
g_object_unref (self);
return;
}
object_path = g_strconcat ("/", state->dbus_name, NULL);
g_strdelimit (object_path, ".", '/');
state->remote_menu = g_menu_proxy_get (g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL),
state->dbus_name,
object_path);
g_object_notify (G_OBJECT (self), "dbus-id");
g_object_notify (G_OBJECT (self), "action-group");
g_object_notify (G_OBJECT (self), "menu");
g_object_unref (self);
g_free (object_path);
}
static void
on_dbus_name_appeared (GDBusConnection *bus,
const gchar *name,
const gchar *name_owner,
gpointer user_data)
{
ShellApp *self = SHELL_APP (user_data);
ShellAppRunningState *state = self->running_state;
char *object_path;
g_assert (state != NULL);
object_path = g_strconcat ("/", name, NULL);
g_strdelimit (object_path, ".", '/');
if (!state->dbus_cancellable)
state->dbus_cancellable = g_cancellable_new ();
g_dbus_action_group_new (bus,
name,
object_path,
G_DBUS_ACTION_GROUP_FLAGS_NONE,
state->dbus_cancellable,
on_action_group_acquired,
g_object_ref (self));
g_free (object_path);
}
static void
on_dbus_name_disappeared (GDBusConnection *bus,
const gchar *name,
gpointer user_data)
{
ShellApp *self = SHELL_APP (user_data);
ShellAppRunningState *state = self->running_state;
g_assert (state != NULL);
if (state->dbus_cancellable)
{
g_cancellable_cancel (state->dbus_cancellable);
g_clear_object (&state->dbus_cancellable);
}
g_clear_object (&state->remote_actions);
g_clear_object (&state->remote_menu);
g_free (state->dbus_name);
state->dbus_name = NULL;
g_bus_unwatch_name (state->name_watcher_id);
state->name_watcher_id = 0;
}
void
_shell_app_set_dbus_name (ShellApp *app,
const char *bus_name)
{
g_return_if_fail (app->running_state != NULL);
if (app->running_state->dbus_name != NULL)
{
/* already associating with another name
(can only happen if you restart the shell in the
middle of the session, in which case it will try
all names seen on the bus; otherwise, it uses
the Hello signal from GApplication and thus knows
for sure which name is the right one)
*/
return;
}
app->running_state->dbus_name = g_strdup (bus_name);
app->running_state->name_watcher_id = g_bus_watch_name (G_BUS_TYPE_SESSION,
bus_name,
G_BUS_NAME_WATCHER_FLAGS_NONE,
on_dbus_name_appeared,
on_dbus_name_disappeared,
g_object_ref (app),
g_object_unref);
}
/** /**
* shell_app_get_pids: * shell_app_get_pids:
* @app: a #ShellApp * @app: a #ShellApp
@ -1167,13 +1337,28 @@ unref_running_state (ShellAppRunningState *state)
{ {
MetaScreen *screen; MetaScreen *screen;
g_assert (state->refcount > 0);
state->refcount--; state->refcount--;
if (state->refcount > 0) if (state->refcount > 0)
return; return;
screen = shell_global_get_screen (shell_global_get ()); screen = shell_global_get_screen (shell_global_get ());
g_signal_handler_disconnect (screen, state->workspace_switch_id); g_signal_handler_disconnect (screen, state->workspace_switch_id);
if (state->dbus_cancellable)
{
g_cancellable_cancel (state->dbus_cancellable);
g_object_unref (state->dbus_cancellable);
}
g_clear_object (&state->remote_actions);
g_clear_object (&state->remote_menu);
g_free (state->dbus_name);
if (state->name_watcher_id)
g_bus_unwatch_name (state->name_watcher_id);
g_slice_free (ShellAppRunningState, state); g_slice_free (ShellAppRunningState, state);
} }
@ -1349,6 +1534,9 @@ shell_app_dispose (GObject *object)
while (app->running_state->windows) while (app->running_state->windows)
_shell_app_remove_window (app, app->running_state->windows->data); _shell_app_remove_window (app, app->running_state->windows->data);
} }
/* We should have been transitioned when we removed all of our windows */
g_assert (app->state == SHELL_APP_STATE_STOPPED);
g_assert (app->running_state == NULL);
G_OBJECT_CLASS(shell_app_parent_class)->dispose (object); G_OBJECT_CLASS(shell_app_parent_class)->dispose (object);
} }
@ -1399,4 +1587,60 @@ shell_app_class_init(ShellAppClass *klass)
SHELL_TYPE_APP_STATE, SHELL_TYPE_APP_STATE,
SHELL_APP_STATE_STOPPED, SHELL_APP_STATE_STOPPED,
G_PARAM_READABLE)); G_PARAM_READABLE));
/**
* ShellApp:id:
*
* The id of this application (a desktop filename, or a special string
* like window:0xabcd1234)
*/
g_object_class_install_property (gobject_class,
PROP_ID,
g_param_spec_string ("id",
"Application id",
"The desktop file id of this ShellApp",
NULL,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
/**
* ShellApp:dbus-id:
*
* The DBus well-known name of the application, if one can be associated
* to this ShellApp (it means that the application is using GApplication)
*/
g_object_class_install_property (gobject_class,
PROP_DBUS_ID,
g_param_spec_string ("dbus-id",
"Application DBus Id",
"The DBus well-known name of the application",
NULL,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
/**
* ShellApp:action-group:
*
* The #GDBusActionGroup associated with this ShellApp, if any. See the
* documentation of #GApplication and #GActionGroup for details.
*/
g_object_class_install_property (gobject_class,
PROP_ACTION_GROUP,
g_param_spec_object ("action-group",
"Application Action Group",
"The action group exported by the remote application",
G_TYPE_DBUS_ACTION_GROUP,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
/**
* ShellApp:menu:
*
* The #GMenuProxy associated with this ShellApp, if any. See the
* documentation of #GMenuModel for details.
*/
g_object_class_install_property (gobject_class,
PROP_MENU,
g_param_spec_object ("menu",
"Application Menu",
"The primary menu exported by the remote application",
G_TYPE_MENU_PROXY,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
} }

View File

@ -13,6 +13,7 @@ G_BEGIN_DECLS
typedef struct _ShellApp ShellApp; typedef struct _ShellApp ShellApp;
typedef struct _ShellAppClass ShellAppClass; typedef struct _ShellAppClass ShellAppClass;
typedef struct _ShellAppPrivate ShellAppPrivate; typedef struct _ShellAppPrivate ShellAppPrivate;
typedef struct _ShellAppAction ShellAppAction;
#define SHELL_TYPE_APP (shell_app_get_type ()) #define SHELL_TYPE_APP (shell_app_get_type ())
#define SHELL_APP(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), SHELL_TYPE_APP, ShellApp)) #define SHELL_APP(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), SHELL_TYPE_APP, ShellApp))
@ -36,9 +37,12 @@ typedef enum {
GType shell_app_get_type (void) G_GNUC_CONST; GType shell_app_get_type (void) G_GNUC_CONST;
const char *shell_app_get_id (ShellApp *app); const char *shell_app_get_id (ShellApp *app);
GMenuTreeEntry *shell_app_get_tree_entry (ShellApp *app); GMenuTreeEntry *shell_app_get_tree_entry (ShellApp *app);
GDesktopAppInfo *shell_app_get_app_info (ShellApp *app); GDesktopAppInfo *shell_app_get_app_info (ShellApp *app);
const char *shell_app_get_dbus_id (ShellApp *app);
ClutterActor *shell_app_create_icon_texture (ShellApp *app, int size); ClutterActor *shell_app_create_icon_texture (ShellApp *app, int size);
ClutterActor *shell_app_get_faded_icon (ShellApp *app, int size); ClutterActor *shell_app_get_faded_icon (ShellApp *app, int size);
const char *shell_app_get_name (ShellApp *app); const char *shell_app_get_name (ShellApp *app);

View File

@ -49,6 +49,9 @@ struct _ShellWindowTracker
/* <MetaWindow * window, ShellApp *app> */ /* <MetaWindow * window, ShellApp *app> */
GHashTable *window_to_app; GHashTable *window_to_app;
/* <int, gchar *> */
GHashTable *pid_to_dbus_connection;
/* <int, ShellApp *app> */ /* <int, ShellApp *app> */
GHashTable *launched_pid_to_app; GHashTable *launched_pid_to_app;
}; };
@ -436,6 +439,8 @@ track_window (ShellWindowTracker *self,
MetaWindow *window) MetaWindow *window)
{ {
ShellApp *app; ShellApp *app;
GPid pid;
gchar *dbus_name;
if (!shell_window_tracker_is_window_interesting (window)) if (!shell_window_tracker_is_window_interesting (window))
return; return;
@ -451,6 +456,15 @@ track_window (ShellWindowTracker *self,
_shell_app_add_window (app, window); _shell_app_add_window (app, window);
/* Try to associate this ShellApp with a GApplication id, if one exists */
pid = meta_window_get_pid (window);
dbus_name = g_hash_table_lookup (self->pid_to_dbus_connection, GINT_TO_POINTER ((int) pid));
if (dbus_name != NULL)
{
_shell_app_set_dbus_name (app, dbus_name);
g_hash_table_remove (self->pid_to_dbus_connection, GINT_TO_POINTER ((int) pid));
}
g_signal_emit (self, signals[TRACKED_WINDOWS_CHANGED], 0); g_signal_emit (self, signals[TRACKED_WINDOWS_CHANGED], 0);
} }
@ -581,13 +595,161 @@ on_startup_sequence_changed (MetaScreen *screen,
g_signal_emit (G_OBJECT (self), signals[STARTUP_SEQUENCE_CHANGED], 0, sequence); g_signal_emit (G_OBJECT (self), signals[STARTUP_SEQUENCE_CHANGED], 0, sequence);
} }
typedef struct {
ShellWindowTracker *tracker;
gchar *bus_name;
} LookupAppDBusData;
static void
on_get_connection_unix_pid_reply (GObject *connection,
GAsyncResult *result,
gpointer user_data)
{
LookupAppDBusData *data = user_data;
GError *error = NULL;
GVariant *reply;
guint32 pid;
ShellApp *app;
reply = g_dbus_connection_call_finish (G_DBUS_CONNECTION (connection), result, &error);
if (!reply)
{
if (!g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_NAME_HAS_NO_OWNER))
g_warning ("%s\n", error->message);
g_clear_error (&error);
goto out;
}
if (!g_variant_is_of_type (reply, G_VARIANT_TYPE ("(u)")))
{
g_variant_unref (reply);
goto out;
}
g_variant_get (reply, "(u)", &pid);
g_variant_unref (reply);
app = shell_window_tracker_get_app_from_pid (data->tracker, (int)pid);
if (app)
_shell_app_set_dbus_name (app, data->bus_name);
else
{
g_hash_table_insert (data->tracker->pid_to_dbus_connection,
GINT_TO_POINTER ((int) pid),
data->bus_name);
data->bus_name = NULL;
}
out:
g_object_unref (data->tracker);
g_free (data->bus_name);
g_slice_free (LookupAppDBusData, data);
}
static void
lookup_application_from_name (ShellWindowTracker *self,
GDBusConnection *connection,
const gchar *bus_name)
{
LookupAppDBusData *data;
data = g_slice_new0 (LookupAppDBusData);
data->tracker = g_object_ref (self);
data->bus_name = g_strdup (bus_name);
/*
* TODO: Add something to GtkApplication so it definitely knows the .desktop file.
*/
g_dbus_connection_call (connection,
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
"GetConnectionUnixProcessID",
g_variant_new ("(s)", bus_name),
NULL, 0, -1, NULL, on_get_connection_unix_pid_reply, data);
}
static void
on_application_signal (GDBusConnection *connection,
const gchar *sender_name,
const gchar *object_path,
const gchar *interface_name,
const gchar *signal_name,
GVariant *parameters,
gpointer user_data)
{
ShellWindowTracker *tracker = SHELL_WINDOW_TRACKER (user_data);
gchar *bus_name = NULL;
g_variant_get (parameters, "(&s)", &bus_name);
lookup_application_from_name (tracker, connection, bus_name);
g_variant_unref (parameters);
}
static void
on_list_names_end (GObject *object,
GAsyncResult *result,
gpointer user_data)
{
GDBusConnection *connection = G_DBUS_CONNECTION (object);
ShellWindowTracker *self = SHELL_WINDOW_TRACKER (user_data);
GError *error = NULL;
GVariantIter iter;
gchar *bus_name = NULL;
GVariant *res = g_dbus_connection_call_finish (connection, result, &error);
if (!res)
{
g_warning ("ListNames failed: %s", error->message);
g_error_free (error);
return;
}
g_variant_iter_init (&iter, g_variant_get_child_value (res, 0));
while (g_variant_iter_loop (&iter, "s", &bus_name))
{
if (bus_name[0] == ':')
{
/* unique name, uninteresting */
continue;
}
lookup_application_from_name (self, connection, bus_name);
}
g_variant_unref (res);
}
static void static void
shell_window_tracker_init (ShellWindowTracker *self) shell_window_tracker_init (ShellWindowTracker *self)
{ {
MetaScreen *screen; MetaScreen *screen;
self->window_to_app = g_hash_table_new_full (g_direct_hash, g_direct_equal, g_dbus_connection_signal_subscribe (g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL),
NULL, (GDestroyNotify) g_object_unref); NULL,
"org.gtk.Application",
"Hello",
NULL,
NULL,
G_DBUS_SIGNAL_FLAGS_NONE,
on_application_signal,
self, NULL);
g_dbus_connection_call (g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL),
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
"ListNames",
NULL, /* parameters */
G_VARIANT_TYPE ("(as)"),
G_DBUS_CALL_FLAGS_NONE,
-1, /* timeout */
NULL, /* cancellable */
on_list_names_end, self);
self->window_to_app = g_hash_table_new_full (NULL, NULL, NULL, g_object_unref);
self->pid_to_dbus_connection = g_hash_table_new_full (NULL, NULL, NULL, g_free);
self->launched_pid_to_app = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify) g_object_unref); self->launched_pid_to_app = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify) g_object_unref);
@ -607,6 +769,7 @@ shell_window_tracker_finalize (GObject *object)
g_hash_table_destroy (self->window_to_app); g_hash_table_destroy (self->window_to_app);
g_hash_table_destroy (self->launched_pid_to_app); g_hash_table_destroy (self->launched_pid_to_app);
g_hash_table_destroy (self->pid_to_dbus_connection);
G_OBJECT_CLASS (shell_window_tracker_parent_class)->finalize(object); G_OBJECT_CLASS (shell_window_tracker_parent_class)->finalize(object);
} }

99
src/test-gapplication.js Executable file
View File

@ -0,0 +1,99 @@
#!/usr/bin/env gjs
const Gdk = imports.gi.Gdk;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;
function do_action(action, parameter) {
print ("Action '" + action.name + "' invoked");
}
function do_action_param(action, parameter) {
print ("Action '" + action.name + "' invoked with parameter " + parameter.print(true));
}
function do_action_state_change(action) {
print ("Action '" + action.name + "' has now state '" + action.state.deep_unpack() + "'");
}
function main() {
Gtk.init(null, null);
let app = new Gtk.Application({ application_id: 'org.gnome.Shell.GtkApplicationTest' });
app.connect('activate', function() {
print ("Activated");
});
let group = new Gio.SimpleActionGroup();
let action = Gio.SimpleAction.new('one', null);
action.connect('activate', do_action);
group.insert(action);
let action = Gio.SimpleAction.new('two', null);
action.connect('activate', do_action);
group.insert(action);
let action = Gio.SimpleAction.new_stateful('toggle', null, GLib.Variant.new('b', false));
action.connect('activate', do_action);
action.connect('notify::state', do_action_state_change);
group.insert(action);
let action = Gio.SimpleAction.new('disable', null);
action.set_enabled(false);
action.connect('activate', do_action);
group.insert(action);
let action = Gio.SimpleAction.new('parameter-int', GLib.VariantType.new('u'));
action.connect('activate', do_action_param);
group.insert(action);
let action = Gio.SimpleAction.new('parameter-string', GLib.VariantType.new('s'));
action.connect('activate', do_action_param);
group.insert(action);
app.action_group = group;
let menu = new Gio.Menu();
menu.append('An action', 'one');
let section = new Gio.Menu();
section.append('Another action', 'two');
section.append('Same as above', 'two');
menu.append_section(null, section);
// another section, to check separators
section = new Gio.Menu();
section.append('Checkbox', 'toggle');
section.append('Disabled', 'disable');
menu.append_section(null, section);
// empty sections or submenus should be invisible
menu.append_section('Empty section', new Gio.Menu());
menu.append_submenu('Empty submenu', new Gio.Menu());
let submenu = new Gio.Menu();
submenu.append('Open c:\\', 'parameter-string::c:\\');
submenu.append('Open /home', 'parameter-string::/home');
menu.append_submenu('Recent files', submenu);
let item = Gio.MenuItem.new('Say 42', null);
item.set_action_and_target_value('parameter-int', GLib.Variant.new('u', 42));
menu.append_item(item);
let item = Gio.MenuItem.new('Say 43', null);
item.set_action_and_target_value('parameter-int', GLib.Variant.new('u', 43));
menu.append_item(item);
app.menu = menu;
app.connect('startup', function(app) {
let window = new Gtk.Window({ title: "Test Application", application: app });
window.present();
});
app.run(null);
}
main();