Implement the Desktop Action specification
authorRyan Lortie <desrt@desrt.ca>
Thu, 11 Jul 2013 16:46:59 +0000 (12:46 -0400)
committerRyan Lortie <desrt@desrt.ca>
Thu, 11 Jul 2013 16:48:08 +0000 (12:48 -0400)
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
gio/gdesktopappinfo.c
gio/gdesktopappinfo.h
gio/tests/Makefile.am
gio/tests/desktop-app-info.c
glib/gkeyfile.h

index 3674a3d..e18a05e 100644 (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
index 37bd45f..59673be 100644 (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);
+    }
+}
index b1be8d5..5f7f68a 100644 (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 ())
index 00cb9f4..8c6cd54 100644 (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                    \
index 26e4e9e..a87246e 100644 (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 ();
 
index eaf0cce..b37070e 100644 (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"