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
This commit is contained in:
Giovanni Campagna 2011-05-10 18:29:52 +02:00 committed by Colin Walters
parent 17c46c2452
commit d0c36bb732
3 changed files with 361 additions and 4 deletions

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 {
@ -88,6 +97,15 @@ 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);
static void on_dbus_name_appeared (GDBusConnection *bus,
const gchar *name,
const gchar *name_owner,
gpointer user_data);
static void on_dbus_name_disappeared (GDBusConnection *bus,
const gchar *name,
gpointer user_data);
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,
@ -102,6 +120,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 +183,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:
* *
@ -903,6 +944,34 @@ shell_app_on_ws_switch (MetaScreen *screen,
g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0); g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0);
} }
static void
on_dbus_application_id_changed (MetaWindow *window,
GParamSpec *pspec,
gpointer user_data)
{
const char *appid;
ShellApp *app = SHELL_APP (user_data);
/* Ignore changes in the appid after it's set, shouldn't happen */
if (app->running_state->dbus_name != NULL)
return;
appid = meta_window_get_dbus_application_id (window);
if (!appid)
return;
g_assert (app->running_state != NULL);
app->running_state->dbus_name = g_strdup (appid);
app->running_state->name_watcher_id = g_bus_watch_name (G_BUS_TYPE_SESSION,
appid,
G_BUS_NAME_WATCHER_FLAGS_NONE,
on_dbus_name_appeared,
on_dbus_name_disappeared,
g_object_ref (app),
g_object_unref);
}
void void
_shell_app_add_window (ShellApp *app, _shell_app_add_window (ShellApp *app,
MetaWindow *window) MetaWindow *window)
@ -923,6 +992,9 @@ _shell_app_add_window (ShellApp *app,
if (app->state != SHELL_APP_STATE_STARTING) if (app->state != SHELL_APP_STATE_STARTING)
shell_app_state_transition (app, SHELL_APP_STATE_RUNNING); shell_app_state_transition (app, SHELL_APP_STATE_RUNNING);
g_signal_connect (window, "notify::dbus-application-id", G_CALLBACK(on_dbus_application_id_changed), app);
on_dbus_application_id_changed (window, NULL, app);
g_object_thaw_notify (G_OBJECT (app)); g_object_thaw_notify (G_OBJECT (app));
g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0); g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0);
@ -939,6 +1011,7 @@ _shell_app_remove_window (ShellApp *app,
g_signal_handlers_disconnect_by_func (window, G_CALLBACK(shell_app_on_unmanaged), app); g_signal_handlers_disconnect_by_func (window, G_CALLBACK(shell_app_on_unmanaged), app);
g_signal_handlers_disconnect_by_func (window, G_CALLBACK(shell_app_on_user_time_changed), app); g_signal_handlers_disconnect_by_func (window, G_CALLBACK(shell_app_on_user_time_changed), app);
g_signal_handlers_disconnect_by_func (window, G_CALLBACK(on_dbus_application_id_changed), app);
g_object_unref (window); g_object_unref (window);
app->running_state->windows = g_slist_remove (app->running_state->windows, window); app->running_state->windows = g_slist_remove (app->running_state->windows, window);
@ -948,6 +1021,113 @@ _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;
}
/** /**
* shell_app_get_pids: * shell_app_get_pids:
* @app: a #ShellApp * @app: a #ShellApp
@ -1167,13 +1347,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 +1544,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 +1597,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);

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();