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
This commit is contained in:
Ryan Lortie 2013-07-11 12:46:59 -04:00
parent f77e121650
commit 6dc5c118e4
6 changed files with 291 additions and 24 deletions

View File

@ -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
<SUBSECTION>
g_desktop_app_info_list_actions
g_desktop_app_info_get_action_name
g_desktop_app_info_launch_action
<SUBSECTION Standard>
GDesktopAppInfoClass
G_TYPE_DESKTOP_APP_INFO

View File

@ -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 <literal>OnlyShowIn</literal> or <literal>NotShowIn</literal>
* 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);
}
}

View File

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

View File

@ -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 \

View File

@ -24,6 +24,7 @@
#include <gio/gdesktopappinfo.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
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 ();

View File

@ -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"