From 6dc5c118e4e52d6a6abaed4d6191fe655917ee4d Mon Sep 17 00:00:00 2001 From: Ryan Lortie Date: Thu, 11 Jul 2013 12:46:59 -0400 Subject: [PATCH] Implement the Desktop Action specification For some time, the desktop file specification has supported "additional application actions". This is intended to allow for additional methods of starting an app, such as a mail client having a "Compose New Message" action that brings up the compose window instead of the folder list. This patch adds support for this with a relatively minimal API. In the case that the application is a GApplication and DBusActivatable, desktop actions are translated into GActions that have been added to the application with g_action_map_add_action(). This more or less closes the loop on being able to activate an application with an action invocation (instead of 'activate'). https://bugzilla.gnome.org/show_bug.cgi?id=664444 --- docs/reference/gio/gio-sections.txt | 4 + gio/gdesktopappinfo.c | 219 ++++++++++++++++++++++++++++++++---- gio/gdesktopappinfo.h | 12 ++ gio/tests/Makefile.am | 1 + gio/tests/desktop-app-info.c | 78 +++++++++++++ glib/gkeyfile.h | 1 + 6 files changed, 291 insertions(+), 24 deletions(-) diff --git a/docs/reference/gio/gio-sections.txt b/docs/reference/gio/gio-sections.txt index 3674a3d..e18a05e 100644 --- a/docs/reference/gio/gio-sections.txt +++ b/docs/reference/gio/gio-sections.txt @@ -1518,6 +1518,10 @@ g_desktop_app_info_get_boolean g_desktop_app_info_has_key GDesktopAppLaunchCallback g_desktop_app_info_launch_uris_as_manager + +g_desktop_app_info_list_actions +g_desktop_app_info_get_action_name +g_desktop_app_info_launch_action GDesktopAppInfoClass G_TYPE_DESKTOP_APP_INFO diff --git a/gio/gdesktopappinfo.c b/gio/gdesktopappinfo.c index 37bd45f..59673be 100644 --- a/gio/gdesktopappinfo.c +++ b/gio/gdesktopappinfo.c @@ -115,6 +115,7 @@ struct _GDesktopAppInfo char *categories; char *startup_wm_class; char **mime_types; + char **actions; guint nodisplay : 1; guint hidden : 1; @@ -200,6 +201,7 @@ g_desktop_app_info_finalize (GObject *object) g_free (info->startup_wm_class); g_strfreev (info->mime_types); g_free (info->app_id); + g_strfreev (info->actions); G_OBJECT_CLASS (g_desktop_app_info_parent_class)->finalize (object); } @@ -380,7 +382,12 @@ g_desktop_app_info_load_from_keyfile (GDesktopAppInfo *info, info->startup_wm_class = g_key_file_get_string (key_file, G_KEY_FILE_DESKTOP_GROUP, STARTUP_WM_CLASS_KEY, NULL); info->mime_types = g_key_file_get_string_list (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_MIME_TYPE, NULL, NULL); bus_activatable = g_key_file_get_boolean (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_DBUS_ACTIVATABLE, NULL); - + info->actions = g_key_file_get_string_list (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ACTIONS, NULL, NULL); + + /* Remove the special-case: no Actions= key just means 0 extra actions */ + if (info->actions == NULL) + info->actions = g_new0 (gchar *, 0 + 1); + info->icon = NULL; if (info->icon_name) { @@ -1050,17 +1057,18 @@ expand_macro (char macro, static gboolean expand_application_parameters (GDesktopAppInfo *info, + const gchar *exec_line, GList **uris, int *argc, char ***argv, GError **error) { GList *uri_list = *uris; - const char *p = info->exec; + const char *p = exec_line; GString *expanded_exec; gboolean res; - if (info->exec == NULL) + if (exec_line == NULL) { g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, _("Desktop file didn't specify Exec field")); @@ -1303,6 +1311,7 @@ notify_desktop_launch (GDBusConnection *session_bus, static gboolean g_desktop_app_info_launch_uris_with_spawn (GDesktopAppInfo *info, GDBusConnection *session_bus, + const gchar *exec_line, GList *uris, GAppLaunchContext *launch_context, GSpawnFlags spawn_flags, @@ -1335,8 +1344,7 @@ g_desktop_app_info_launch_uris_with_spawn (GDesktopAppInfo *info, char *display, *sn_id; old_uris = uris; - if (!expand_application_parameters (info, &uris, - &argc, &argv, error)) + if (!expand_application_parameters (info, exec_line, &uris, &argc, &argv, error)) goto out; /* Get the subset of URIs we're launching with this process */ @@ -1478,6 +1486,33 @@ object_path_from_appid (const gchar *app_id) return path; } +static GVariant * +g_desktop_app_info_make_platform_data (GDesktopAppInfo *info, + GList *uris, + GAppLaunchContext *launch_context) +{ + GVariantBuilder builder; + + g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT); + + if (launch_context) + { + GList *launched_files = create_files_for_uris (uris); + + if (info->startup_notify) + { + gchar *sn_id; + + sn_id = g_app_launch_context_get_startup_notify_id (launch_context, G_APP_INFO (info), launched_files); + g_variant_builder_add (&builder, "{sv}", "desktop-startup-id", g_variant_new_take_string (sn_id)); + } + + g_list_free_full (launched_files, g_object_unref); + } + + return g_variant_builder_end (&builder); +} + static gboolean g_desktop_app_info_launch_uris_with_dbus (GDesktopAppInfo *info, GDBusConnection *session_bus, @@ -1501,24 +1536,7 @@ g_desktop_app_info_launch_uris_with_dbus (GDesktopAppInfo *info, g_variant_builder_close (&builder); } - g_variant_builder_open (&builder, G_VARIANT_TYPE_VARDICT); - - if (launch_context) - { - GList *launched_files = create_files_for_uris (uris); - - if (info->startup_notify) - { - gchar *sn_id; - - sn_id = g_app_launch_context_get_startup_notify_id (launch_context, G_APP_INFO (info), launched_files); - g_variant_builder_add (&builder, "{sv}", "desktop-startup-id", g_variant_new_take_string (sn_id)); - } - - g_list_free_full (launched_files, g_object_unref); - } - - g_variant_builder_close (&builder); + g_variant_builder_add_value (&builder, g_desktop_app_info_make_platform_data (info, uris, launch_context)); /* This is non-blocking API. Similar to launching via fork()/exec() * we don't wait around to see if the program crashed during startup. @@ -1553,7 +1571,7 @@ g_desktop_app_info_launch_uris_internal (GAppInfo *appinfo, if (session_bus && info->app_id) g_desktop_app_info_launch_uris_with_dbus (info, session_bus, uris, launch_context); else - success = g_desktop_app_info_launch_uris_with_spawn (info, session_bus, uris, launch_context, + success = g_desktop_app_info_launch_uris_with_spawn (info, session_bus, info->exec, uris, launch_context, spawn_flags, user_setup, user_setup_data, pid_callback, pid_callback_data, error); @@ -3699,3 +3717,156 @@ g_desktop_app_info_has_key (GDesktopAppInfo *info, return g_key_file_has_key (info->keyfile, G_KEY_FILE_DESKTOP_GROUP, key, NULL); } + +/** + * g_desktop_app_info_list_actions: + * @info: a #GDesktopAppInfo + * + * Returns the list of "additional application actions" supported on the + * desktop file, as per the desktop file specification. + * + * As per the specification, this is the list of actions that are + * explicitly listed in the "Actions" key of the [Desktop Entry] group. + * + * Similar to g_app_info_get_all(), this returns all listed actions and + * ignores OnlyShowIn or NotShowIn + * keys. Use g_desktop_app_info_should_show_action() to determine if an + * action should actually be shown. + * + * Returns: (array zero-terminated=1) (element-type utf8) (transfer none): a list of strings, always non-%NULL + * + * Since: 2.38 + **/ +const gchar * const * +g_desktop_app_info_list_actions (GDesktopAppInfo *info) +{ + g_return_val_if_fail (G_IS_DESKTOP_APP_INFO (info), NULL); + + return (const gchar **) info->actions; +} + +static gboolean +app_info_has_action (GDesktopAppInfo *info, + const gchar *action_name) +{ + gint i; + + for (i = 0; info->actions[i]; i++) + if (g_str_equal (info->actions[i], action_name)) + return TRUE; + + return FALSE; +} + +/** + * g_desktop_app_info_get_action_name: + * @info: a #GDesktopAppInfo + * @action_name: the name of the action as from + * g_desktop_app_info_list_actions() + * + * Gets the user-visible display name of the "additional application + * action" specified by @action_name. + * + * This corresponds to the "Name" key within the keyfile group for the + * action. + * + * Returns: (transfer full): the locale-specific action name + * + * Since: 2.38 + */ +gchar * +g_desktop_app_info_get_action_name (GDesktopAppInfo *info, + const gchar *action_name) +{ + gchar *group_name; + gchar *result; + + g_return_val_if_fail (G_IS_DESKTOP_APP_INFO (info), NULL); + g_return_val_if_fail (action_name != NULL, NULL); + g_return_if_fail (app_info_has_action (info, action_name)); + + group_name = g_strdup_printf ("Desktop Action %s", action_name); + result = g_key_file_get_locale_string (info->keyfile, group_name, "Name", NULL, NULL); + g_free (group_name); + + /* The spec says that the Name field must be given. + * + * If it's not, let's follow the behaviour of our get_name() + * implementation above and never return %NULL. + */ + if (result == NULL) + result = g_strdup (_("Unnamed")); + + return result; +} + +/** + * g_desktop_app_info_launch_action: + * @info: a #GDesktopAppInfo + * @action_name: the name of the action as from + * g_desktop_app_info_list_actions() + * @launch_context: (allow-none): a #GAppLaunchContext + * + * Activates the named application action. + * + * You may only call this function on action names that were + * returned from g_desktop_app_info_list_actions(). + * + * Note that if the main entry of the desktop file indicates that the + * application supports startup notification, and @launch_context is + * non-%NULL, then startup notification will be used when activating the + * action (and as such, invocation of the action on the receiving side + * must signal the end of startup notification when it is completed). + * This is the expected behaviour of applications declaring additional + * actions, as per the desktop file specification. + * + * As with g_app_info_launch() there is no way to detect failures that + * occur while using this function. + * + * Since: 2.38 + */ +void +g_desktop_app_info_launch_action (GDesktopAppInfo *info, + const gchar *action_name, + GAppLaunchContext *launch_context) +{ + GDBusConnection *session_bus; + + g_return_if_fail (G_IS_DESKTOP_APP_INFO (info)); + g_return_if_fail (action_name != NULL); + g_return_if_fail (app_info_has_action (info, action_name)); + + session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + + if (session_bus && info->app_id) + { + gchar *object_path; + + object_path = object_path_from_appid (info->app_id); + g_dbus_connection_call (session_bus, info->app_id, object_path, + "org.freedesktop.Application", "ActivateAction", + g_variant_new ("(sav@a{sv})", action_name, NULL, + g_desktop_app_info_make_platform_data (info, NULL, launch_context)), + NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + g_free (object_path); + } + else + { + gchar *group_name; + gchar *exec_line; + + group_name = g_strdup_printf ("Desktop Action %s", action_name); + exec_line = g_key_file_get_string (info->keyfile, group_name, "Exec", NULL); + g_free (group_name); + + if (exec_line) + g_desktop_app_info_launch_uris_with_spawn (info, session_bus, exec_line, NULL, launch_context, + _SPAWN_FLAGS_DEFAULT, NULL, NULL, NULL, NULL, NULL); + } + + if (session_bus != NULL) + { + g_dbus_connection_flush (session_bus, NULL, NULL, NULL); + g_object_unref (session_bus); + } +} diff --git a/gio/gdesktopappinfo.h b/gio/gdesktopappinfo.h index b1be8d5..5f7f68a 100644 --- a/gio/gdesktopappinfo.h +++ b/gio/gdesktopappinfo.h @@ -86,6 +86,18 @@ GLIB_AVAILABLE_IN_2_36 gboolean g_desktop_app_info_get_boolean (GDesktopAppInfo *info, const char *key); +GLIB_AVAILABLE_IN_2_38 +const gchar * const * g_desktop_app_info_list_actions (GDesktopAppInfo *info); + +GLIB_AVAILABLE_IN_2_38 +void g_desktop_app_info_launch_action (GDesktopAppInfo *info, + const gchar *action_name, + GAppLaunchContext *launch_context); + +GLIB_AVAILABLE_IN_2_38 +gchar * g_desktop_app_info_get_action_name (GDesktopAppInfo *info, + const gchar *action_name); + #ifndef G_DISABLE_DEPRECATED #define G_TYPE_DESKTOP_APP_INFO_LOOKUP (g_desktop_app_info_lookup_get_type ()) diff --git a/gio/tests/Makefile.am b/gio/tests/Makefile.am index 00cb9f4..8c6cd54 100644 --- a/gio/tests/Makefile.am +++ b/gio/tests/Makefile.am @@ -226,6 +226,7 @@ uninstalled_test_programs += \ $(NULL) dist_test_data += \ + appinfo-test-actions.desktop \ appinfo-test-gnome.desktop \ appinfo-test-notgnome.desktop \ appinfo-test.desktop \ diff --git a/gio/tests/desktop-app-info.c b/gio/tests/desktop-app-info.c index 26e4e9e..a87246e 100644 --- a/gio/tests/desktop-app-info.c +++ b/gio/tests/desktop-app-info.c @@ -24,6 +24,7 @@ #include #include #include +#include static char *basedir; @@ -373,6 +374,82 @@ test_extra_getters (void) g_object_unref (appinfo); } +static void +wait_for_file (const gchar *want_this, + const gchar *but_not_this, + const gchar *or_this) +{ + gint retries = 600; + + /* I hate time-based conditions in tests, but this will wait up to one + * whole minute for "touch file" to finish running. I think it should + * be OK. + * + * 600 * 100ms = 60 seconds. + */ + while (access (want_this, F_OK) != 0) + { + g_usleep (100000); /* 100ms */ + g_assert (retries); + retries--; + } + + g_assert (access (but_not_this, F_OK) != 0); + g_assert (access (or_this, F_OK) != 0); + + unlink (want_this); + unlink (but_not_this); + unlink (or_this); +} + +static void +test_actions (void) +{ + const gchar * const *actions; + GDesktopAppInfo *appinfo; + gchar *name; + + appinfo = g_desktop_app_info_new_from_filename (g_test_get_filename (G_TEST_DIST, "appinfo-test-actions.desktop", NULL)); + g_assert (appinfo != NULL); + + actions = g_desktop_app_info_list_actions (appinfo); + g_assert_cmpstr (actions[0], ==, "frob"); + g_assert_cmpstr (actions[1], ==, "tweak"); + g_assert_cmpstr (actions[2], ==, "twiddle"); + g_assert_cmpstr (actions[3], ==, "broken"); + g_assert_cmpstr (actions[4], ==, NULL); + + name = g_desktop_app_info_get_action_name (appinfo, "frob"); + g_assert_cmpstr (name, ==, "Frobnicate"); + g_free (name); + + name = g_desktop_app_info_get_action_name (appinfo, "tweak"); + g_assert_cmpstr (name, ==, "Tweak"); + g_free (name); + + name = g_desktop_app_info_get_action_name (appinfo, "twiddle"); + g_assert_cmpstr (name, ==, "Twiddle"); + g_free (name); + + name = g_desktop_app_info_get_action_name (appinfo, "broken"); + g_assert (name != NULL); + g_assert (g_utf8_validate (name, -1, NULL)); + g_free (name); + + unlink ("frob"); unlink ("tweak"); unlink ("twiddle"); + + g_desktop_app_info_launch_action (appinfo, "frob", NULL); + wait_for_file ("frob", "tweak", "twiddle"); + + g_desktop_app_info_launch_action (appinfo, "tweak", NULL); + wait_for_file ("tweak", "frob", "twiddle"); + + g_desktop_app_info_launch_action (appinfo, "twiddle", NULL); + wait_for_file ("twiddle", "frob", "tweak"); + + g_object_unref (appinfo); +} + int main (int argc, char *argv[]) @@ -390,6 +467,7 @@ main (int argc, g_test_add_func ("/desktop-app-info/fallback", test_fallback); g_test_add_func ("/desktop-app-info/lastused", test_last_used); g_test_add_func ("/desktop-app-info/extra-getters", test_extra_getters); + g_test_add_func ("/desktop-app-info/actions", test_actions); result = g_test_run (); diff --git a/glib/gkeyfile.h b/glib/gkeyfile.h index eaf0cce..b37070e 100644 --- a/glib/gkeyfile.h +++ b/glib/gkeyfile.h @@ -306,6 +306,7 @@ gboolean g_key_file_remove_group (GKeyFile *key_file, #define G_KEY_FILE_DESKTOP_KEY_STARTUP_WM_CLASS "StartupWMClass" #define G_KEY_FILE_DESKTOP_KEY_URL "URL" #define G_KEY_FILE_DESKTOP_KEY_DBUS_ACTIVATABLE "DBusActivatable" +#define G_KEY_FILE_DESKTOP_KEY_ACTIONS "Actions" #define G_KEY_FILE_DESKTOP_TYPE_APPLICATION "Application" #define G_KEY_FILE_DESKTOP_TYPE_LINK "Link" -- 2.7.4