diff --git a/src/shell-app-private.h b/src/shell-app-private.h index 8c6432e88..62853ccd5 100644 --- a/src/shell-app-private.h +++ b/src/shell-app-private.h @@ -27,6 +27,9 @@ void _shell_app_do_match (ShellApp *app, GSList **prefix_results, GSList **substring_results); +void _shell_app_set_dbus_name (ShellApp *app, + const char *name); + G_END_DECLS #endif /* __SHELL_APP_PRIVATE_H__ */ diff --git a/src/shell-app.c b/src/shell-app.c index 54f81a0a9..4b3ddc8f4 100644 --- a/src/shell-app.c +++ b/src/shell-app.c @@ -37,6 +37,13 @@ typedef struct { /* Whether or not we need to resort the windows; this is done on demand */ 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; /** @@ -72,11 +79,13 @@ struct _ShellApp char *casefolded_exec; }; -G_DEFINE_TYPE (ShellApp, shell_app, G_TYPE_OBJECT); - enum { PROP_0, - PROP_STATE + PROP_STATE, + PROP_ID, + PROP_DBUS_ID, + PROP_ACTION_GROUP, + PROP_MENU }; enum { @@ -89,6 +98,8 @@ static guint shell_app_signals[LAST_SIGNAL] = { 0 }; static void create_running_state (ShellApp *app); static void unref_running_state (ShellAppRunningState *state); +G_DEFINE_TYPE (ShellApp, shell_app, G_TYPE_OBJECT) + static void shell_app_get_property (GObject *gobject, guint prop_id, @@ -102,6 +113,20 @@ shell_app_get_property (GObject *gobject, case PROP_STATE: g_value_set_enum (value, app->state); 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: G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); break; @@ -151,6 +176,15 @@ window_backed_app_get_icon (ShellApp *app, 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: * @@ -948,6 +982,142 @@ _shell_app_remove_window (ShellApp *app, 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: * @app: a #ShellApp @@ -1167,13 +1337,28 @@ unref_running_state (ShellAppRunningState *state) { MetaScreen *screen; + g_assert (state->refcount > 0); + state->refcount--; if (state->refcount > 0) return; screen = shell_global_get_screen (shell_global_get ()); - 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); } @@ -1349,6 +1534,9 @@ shell_app_dispose (GObject *object) while (app->running_state->windows) _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); } @@ -1399,4 +1587,60 @@ shell_app_class_init(ShellAppClass *klass) SHELL_TYPE_APP_STATE, SHELL_APP_STATE_STOPPED, 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)); + } diff --git a/src/shell-app.h b/src/shell-app.h index 62022fef2..4aaf97513 100644 --- a/src/shell-app.h +++ b/src/shell-app.h @@ -13,6 +13,7 @@ G_BEGIN_DECLS typedef struct _ShellApp ShellApp; typedef struct _ShellAppClass ShellAppClass; typedef struct _ShellAppPrivate ShellAppPrivate; +typedef struct _ShellAppAction ShellAppAction; #define SHELL_TYPE_APP (shell_app_get_type ()) #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; const char *shell_app_get_id (ShellApp *app); + GMenuTreeEntry *shell_app_get_tree_entry (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_get_faded_icon (ShellApp *app, int size); const char *shell_app_get_name (ShellApp *app); diff --git a/src/shell-window-tracker.c b/src/shell-window-tracker.c index adb7fb788..cf87cec8d 100644 --- a/src/shell-window-tracker.c +++ b/src/shell-window-tracker.c @@ -49,6 +49,9 @@ struct _ShellWindowTracker /* */ GHashTable *window_to_app; + /* */ + GHashTable *pid_to_dbus_connection; + /* */ GHashTable *launched_pid_to_app; }; @@ -436,6 +439,8 @@ track_window (ShellWindowTracker *self, MetaWindow *window) { ShellApp *app; + GPid pid; + gchar *dbus_name; if (!shell_window_tracker_is_window_interesting (window)) return; @@ -451,6 +456,15 @@ track_window (ShellWindowTracker *self, _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); } @@ -581,13 +595,161 @@ on_startup_sequence_changed (MetaScreen *screen, 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 shell_window_tracker_init (ShellWindowTracker *self) { MetaScreen *screen; - self->window_to_app = g_hash_table_new_full (g_direct_hash, g_direct_equal, - NULL, (GDestroyNotify) g_object_unref); + g_dbus_connection_signal_subscribe (g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL), + 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); @@ -607,6 +769,7 @@ shell_window_tracker_finalize (GObject *object) g_hash_table_destroy (self->window_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); } diff --git a/src/test-gapplication.js b/src/test-gapplication.js new file mode 100755 index 000000000..98728895e --- /dev/null +++ b/src/test-gapplication.js @@ -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();