Blob Blame History Raw
From b1cd7fed6abbbee9b97df0d458a05a52915e4232 Mon Sep 17 00:00:00 2001
From: Felipe Borges <felipeborges@gnome.org>
Date: Fri, 20 Dec 2019 13:53:14 +0100
Subject: [PATCH] EogWindow: Use Portals for "Open With" when possible

If we detect that Eog is running inside a Flatpak container, we
can use the OpenURI Portal[0] to open an AppChooser dialog for
the user to pick an appliation.

[0] https://flatpak.github.io/xdg-desktop-portal/portal-docs.html#gdbus-org.freedesktop.portal.OpenURI

Fixes #98
---
 data/eog-gear-menu.ui |   9 +--
 data/popup-menus.ui   |   2 -
 src/eog-util.c        | 180 ++++++++++++++++++++++++++++++++++++++++++
 src/eog-util.h        |   6 ++
 src/eog-window.c      | 140 +++++++++++---------------------
 5 files changed, 237 insertions(+), 100 deletions(-)

diff --git a/data/eog-gear-menu.ui b/data/eog-gear-menu.ui
index 1e9cb4a3..724156b7 100644
--- a/data/eog-gear-menu.ui
+++ b/data/eog-gear-menu.ui
@@ -6,11 +6,10 @@
         <attribute name="label" translatable="yes">_Open…</attribute>
         <attribute name="action">win.open</attribute>
       </item>
-      <submenu>
-        <attribute name="label" translatable="yes">Op_en With</attribute>
-        <link name="submenu" id="open-with-menu">
-        </link>
-      </submenu>
+      <item>
+        <attribute name="label" translatable="yes">Op_en With…</attribute>
+        <attribute name="action">win.open-with</attribute>
+      </item>
     </section>
     <section>
       <item>
diff --git a/data/popup-menus.ui b/data/popup-menus.ui
index aae780c0..1f19461b 100644
--- a/data/popup-menus.ui
+++ b/data/popup-menus.ui
@@ -4,8 +4,6 @@
     <section>
       <submenu>
         <attribute name="label" translatable="yes">Open _with</attribute>
-        <link name="submenu" id="open-with-menu">
-        </link>
       </submenu>
     </section>
     <section>
diff --git a/src/eog-util.c b/src/eog-util.c
index a6828a58..3e253dc9 100644
--- a/src/eog-util.c
+++ b/src/eog-util.c
@@ -22,6 +22,9 @@
  * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
+/* For O_PATH */
+#define _GNU_SOURCE
+
 #ifdef HAVE_CONFIG_H
 #include "config.h"
 #endif
@@ -37,12 +40,16 @@
 #include "eog-debug.h"
 
 #include <errno.h>
+#include <fcntl.h>
 #include <string.h>
 #include <glib.h>
 #include <glib/gprintf.h>
 #include <gtk/gtk.h>
 #include <gio/gio.h>
+#include <gio/gunixfdlist.h>
 #include <glib/gi18n.h>
+#include <sys/stat.h>
+#include <sys/types.h>
 
 void
 eog_util_show_help (const gchar *section, GtkWindow *parent)
@@ -503,3 +510,176 @@ eog_util_show_file_in_filemanager (GFile *file, GtkWindow *toplevel)
 	if (!done)
 		_eog_util_show_file_in_filemanager_fallback (file, toplevel);
 }
+
+/* Portal */
+
+gboolean
+eog_util_is_running_inside_flatpak (void)
+{
+	return g_file_test ("/.flatpak-info", G_FILE_TEST_EXISTS);
+}
+
+static void
+response_cb (GDBusConnection *connection, const char *sender_name, const char *object_path, const char *interface_name, const char *signal_name, GVariant *parameters, gpointer user_data)
+{
+	GFile *file = G_FILE (user_data);
+	guint32 response;
+	guint signal_id;
+
+	signal_id = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (file), "signal-id"));
+	g_dbus_connection_signal_unsubscribe (connection, signal_id);
+
+	g_variant_get (parameters, "(u@a{sv})", &response, NULL);
+	if (response == 0) {
+		g_debug ("Opening file");
+	} else if (response == 1) {
+		g_debug ("User cancelled opening file");
+	} else {
+		g_warning ("Failed to open file via portal");
+	}
+}
+
+static void
+open_file_complete_cb (GObject *source, GAsyncResult *result, gpointer user_data)
+{
+	GDBusProxy *proxy = G_DBUS_PROXY (source);
+	GFile *file = G_FILE (user_data);
+	GVariant *return_value = NULL;
+	const char *handle;
+	char *object_path = NULL;
+	GError *error = NULL;
+
+	return_value = g_dbus_proxy_call_with_unix_fd_list_finish (proxy, NULL, result, &error);
+	if (!return_value) {
+		if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+			g_warning ("Failed to open file via portal: %s", error->message);
+
+		goto out;
+	}
+
+	g_variant_get (return_value, "(o)", &object_path);
+	handle = (const char *)g_object_get_data (G_OBJECT (file), "handle");
+	if (strcmp (handle, object_path) != 0) {
+		GDBusConnection *connection;
+		guint signal_id;
+
+		connection = g_dbus_proxy_get_connection (proxy);
+		signal_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (file), "signal-id"));
+		g_dbus_connection_signal_unsubscribe (connection, signal_id);
+
+		signal_id = g_dbus_connection_signal_subscribe (connection,
+			"org.freedesktop.portal.Desktop",
+			"org.freedesktop.portal.Request",
+			"Response",
+			handle,
+			NULL,
+			G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
+			response_cb,
+			file,
+			NULL);
+		g_object_set_data (G_OBJECT (file), "signal-id", GUINT_TO_POINTER (signal_id));
+	}
+
+out:
+	if (return_value)
+		g_variant_unref (return_value);
+	if (object_path)
+		g_free (object_path);
+}
+
+static void
+open_with_flatpak_portal_cb (GObject *source, GAsyncResult *result, gpointer user_data)
+{
+	GVariantBuilder builder;
+	GUnixFDList *fd_list;
+	GFile *file;
+	GDBusProxy *proxy;
+	GDBusConnection *connection;
+	GError  *error = NULL;
+	guint signal_id;
+	char *sender, *token, *handle;
+	int fd;
+
+	file = G_FILE (user_data);
+	fd = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (file), "fd"));
+
+	proxy = g_dbus_proxy_new_for_bus_finish (result, &error);
+	if (!proxy) {
+		if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+			g_warning ("Failed to create D-Bus proxy for OpenURI portal: %s", error->message);
+
+		close (fd);
+		return;
+	}
+
+	connection = g_dbus_proxy_get_connection (proxy);
+	sender = g_strdup (g_dbus_connection_get_unique_name (connection) + 1);
+	for (guint i = 0; sender[i] != '\0'; i++) {
+		if (sender[i] == '.') {
+			sender[i] = '_';
+		}
+	}
+
+	token = g_strdup_printf ("eog%u", g_random_int ());
+	handle = g_strdup_printf ("/org/freedesktop/portal/desktop/request/%s/%s", sender, token);
+
+	g_object_set_data_full (G_OBJECT (file), "handle", handle, g_free);
+	g_free (sender);
+
+	signal_id = g_dbus_connection_signal_subscribe (connection,
+			"org.freedesktop.portal.Desktop",
+			"org.freedesktop.portal.Request",
+			"Response",
+			handle,
+			NULL,
+			G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
+			response_cb,
+			file,
+			NULL);
+	g_object_set_data (G_OBJECT (file), "signal-id", GUINT_TO_POINTER (signal_id));
+
+	g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT);
+	g_variant_builder_add (&builder, "{sv}", "handle_token", g_variant_new_string (token));
+	g_variant_builder_add (&builder, "{sv}", "ask", g_variant_new ("b", TRUE));
+	g_free (token);
+
+	fd_list = g_unix_fd_list_new_from_array (&fd, 1);
+	g_dbus_proxy_call_with_unix_fd_list (proxy,
+			"OpenFile",
+			g_variant_new ("(s@h@a{sv})",
+			               "",
+			              g_variant_new ("h", 0),
+				            g_variant_builder_end (&builder)),
+			G_DBUS_CALL_FLAGS_NONE,
+			-1,
+			fd_list,
+			NULL,
+			open_file_complete_cb,
+			file);
+	g_object_unref (fd_list);
+}
+
+void
+eog_util_open_file_with_flatpak_portal (GFile *file)
+{
+	const gchar *path;
+	int fd;
+
+	path = g_file_get_path (file);
+	fd = open (path, O_PATH | O_CLOEXEC);
+	if (fd == -1) {
+		g_warning ("Failed to open %s: %s", path, g_strerror (errno));
+		return;
+	}
+
+	g_object_set_data (G_OBJECT (file), "fd", GINT_TO_POINTER (fd));
+	g_dbus_proxy_new_for_bus (G_BUS_TYPE_SESSION,
+			G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES,
+			NULL,
+			"org.freedesktop.portal.Desktop",
+			"/org/freedesktop/portal/desktop",
+			"org.freedesktop.portal.OpenURI",
+			NULL,
+			open_with_flatpak_portal_cb,
+			file);
+}
diff --git a/src/eog-util.h b/src/eog-util.h
index 9f2ad4c1..d328e1f9 100644
--- a/src/eog-util.h
+++ b/src/eog-util.h
@@ -69,6 +69,12 @@ G_GNUC_INTERNAL
 void     eog_util_show_file_in_filemanager   (GFile *file,
                                               GtkWindow *toplevel);
 
+G_GNUC_INTERNAL
+gboolean eog_util_is_running_inside_flatpak  (void);
+
+G_GNUC_INTERNAL
+void     eog_util_open_file_with_flatpak_portal (GFile *file);
+
 G_END_DECLS
 
 #endif /* __EOG_UTIL_H__ */
diff --git a/src/eog-window.c b/src/eog-window.c
index d6fc3834..75d7c138 100644
--- a/src/eog-window.c
+++ b/src/eog-window.c
@@ -137,9 +137,6 @@ struct _EogWindowPrivate {
 	GtkWidget           *message_area;
 	GtkWidget           *properties_dlg;
 
-	GMenu               *open_with_menu;
-	GPtrArray           *appinfo;
-
 	GtkBuilder          *gear_menu_builder;
 
 	GtkWidget           *fullscreen_popup;
@@ -196,7 +193,6 @@ static void fullscreen_set_timeout (EogWindow *window);
 static void fullscreen_clear_timeout (EogWindow *window);
 static void slideshow_set_timeout (EogWindow *window);
 static void update_action_groups_state (EogWindow *window);
-static void eog_window_update_open_with_menu (EogWindow *window, EogImage *image);
 static void eog_window_list_store_image_added (GtkTreeModel *tree_model,
 					       GtkTreePath  *path,
 					       GtkTreeIter  *iter,
@@ -997,8 +993,6 @@ eog_window_display_image (EogWindow *window, EogImage *image)
 
 	update_status_bar (window);
 
-	eog_window_update_open_with_menu (window, image);
-
 	file = eog_image_get_file (image);
 	g_idle_add_full (G_PRIORITY_LOW,
 			 (GSourceFunc) add_file_to_recent_files,
@@ -1046,28 +1040,24 @@ _eog_window_launch_appinfo_with_files (EogWindow *window,
 }
 
 static void
-eog_window_action_open_with (GSimpleAction *action,
-			     GVariant      *parameter,
-			     gpointer       user_data)
+app_chooser_dialog_response_cb (GtkDialog *dialog,
+                                gint response_id,
+                                gpointer data)
 {
 	EogWindow *window;
 	GAppInfo *app;
 	GFile *file;
 	GList *files = NULL;
-	guint32 index = G_MAXUINT32;
-
 
-	g_return_if_fail (EOG_IS_WINDOW (user_data));
-	window = EOG_WINDOW (user_data);
+	g_return_if_fail (EOG_IS_WINDOW (data));
 
-	index = g_variant_get_uint32 (parameter);
-	if (G_UNLIKELY (index >= window->priv->appinfo->len))
-		return;
+	window = EOG_WINDOW (data);
 
-	app = g_ptr_array_index (window->priv->appinfo, index);
-	if (!app)
-		return;
+	if (response_id != GTK_RESPONSE_OK) {
+		goto out;
+	}
 
+	app = gtk_app_chooser_get_app_info (GTK_APP_CHOOSER (dialog));
 	file = eog_image_get_file (window->priv->image);
 	files = g_list_append (files, file);
 
@@ -1075,73 +1065,56 @@ eog_window_action_open_with (GSimpleAction *action,
 
 	g_list_free (files);
 	g_object_unref (file);
+
+out:
+	gtk_widget_destroy (GTK_WIDGET (dialog));
 }
 
 static void
-eog_window_update_open_with_menu (EogWindow *window, EogImage *image)
+eog_window_open_file_chooser_dialog (EogWindow *window)
 {
-	EogWindowPrivate *priv;
-	GFile *file;
+	GtkWidget *dialog;
 	GFileInfo *file_info;
-	GList *apps = NULL, *li = NULL;
-	const gchar *mime_type;
-	guint32 count = 0;
+	GFile *file;
+	const gchar *mime_type = NULL;
 
-	priv = window->priv;
+	file = eog_image_get_file (window->priv->image);
+	file_info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, 0, NULL, NULL);
+	mime_type = g_content_type_get_mime_type (
+			g_file_info_get_content_type (file_info));
+	g_object_unref (file_info);
 
-	g_menu_remove_all (priv->open_with_menu);
-	g_ptr_array_free (priv->appinfo, TRUE);
-	priv->appinfo = g_ptr_array_new_with_free_func (g_object_unref);
+	dialog = gtk_app_chooser_dialog_new_for_content_type (GTK_WINDOW (window),
+							      GTK_DIALOG_MODAL |
+							      GTK_DIALOG_DESTROY_WITH_PARENT |
+							      GTK_DIALOG_USE_HEADER_BAR,
+							      mime_type);
+	gtk_widget_show (dialog);
 
-	file = eog_image_get_file (image);
-	file_info = g_file_query_info (file,
-				       G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
-				       0, NULL, NULL);
+	g_signal_connect_object (dialog, "response",
+				 G_CALLBACK (app_chooser_dialog_response_cb),
+				 window, 0);
 
-	if (file_info == NULL) {
-		g_object_unref (file);
-		return;
-	}
-
-	mime_type = g_file_info_get_content_type (file_info);
-	apps = g_app_info_get_all_for_type (mime_type);
-	g_object_unref (file_info);
-	if (!apps) {
-		g_object_unref (file);
-		return;
-	}
+	g_object_unref (file);
+}
 
-	for (li = apps; li != NULL; li = li->next) {
-		GAppInfo *app = li->data;
-		gchar *label;
-		GIcon *icon;
-		GVariant *value;
-		GMenuItem *item;
+static void
+eog_window_action_open_with (GSimpleAction *action,
+                            GVariant       *parameter,
+                            gpointer        user_data)
+{
+	EogWindow *window;
 
-		/* Do not include eog itself */
-		if (g_ascii_strcasecmp (g_app_info_get_executable (app),
-					g_get_prgname ()) == 0) {
-			g_object_unref (app);
-			continue;
-		}
+	g_return_if_fail (EOG_IS_WINDOW (user_data));
+	window = EOG_WINDOW (user_data);
 
-		label = g_strdup (g_app_info_get_display_name (app));
-		item = g_menu_item_new (label, NULL);
-		g_free (label);
-		icon = g_app_info_get_icon (app);
-		g_menu_item_set_icon (item, icon);
+	if (eog_util_is_running_inside_flatpak ()) {
+		GFile *file = eog_image_get_file (window->priv->image);
 
-		value = g_variant_new_uint32 (count++);
-		g_menu_item_set_action_and_target_value (item,
-							 "win.open-with",
-							 value);
-		g_ptr_array_add (priv->appinfo, app);
-		g_menu_append_item (priv->open_with_menu, item);
-		g_object_unref (item);
+		eog_util_open_file_with_flatpak_portal (file);
+	} else {
+		eog_window_open_file_chooser_dialog (window);
 	}
-
-	g_object_unref (file);
-	g_list_free (apps);
 }
 
 static void
@@ -3973,7 +3946,7 @@ readonly_state_handler (GSimpleAction *action,
 static const GActionEntry window_actions[] = {
 	/* Stateless actions on the window. */
 	{ "open",          eog_window_action_file_open },
-	{ "open-with",     eog_window_action_open_with, "u" },
+	{ "open-with",     eog_window_action_open_with },
 	{ "open-folder",   eog_window_action_open_containing_folder },
 	{ "save",          eog_window_action_save },
 	{ "save-as",       eog_window_action_save_as },
@@ -4290,12 +4263,6 @@ eog_window_construct_ui (EogWindow *window)
 	gtk_header_bar_pack_end (GTK_HEADER_BAR (headerbar), fullscreen_button);
 	gtk_widget_show (fullscreen_button);
 
-	priv->open_with_menu = g_menu_new ();
-	priv->appinfo = g_ptr_array_new_with_free_func (g_object_unref);
-	builder_object = gtk_builder_get_object (builder, "open-with-menu");
-	g_menu_append_section (G_MENU (builder_object),
-			       NULL,
-			       G_MENU_MODEL (priv->open_with_menu));
 	priv->gear_menu_builder = builder;
 	builder = NULL;
 
@@ -4388,10 +4355,7 @@ eog_window_construct_ui (EogWindow *window)
 
 	builder = gtk_builder_new_from_resource ("/org/gnome/eog/ui/popup-menus.ui");
 	builder_object = gtk_builder_get_object (builder, "view-popup-menu");
-	GObject *open_with_menu = gtk_builder_get_object (builder, "open-with-menu");
-	g_menu_append_section (G_MENU (open_with_menu),
-			       NULL,
-			       G_MENU_MODEL (priv->open_with_menu));
+
 	popup_menu = gtk_menu_new_from_model (G_MENU_MODEL (builder_object));
 
 	eog_scroll_view_set_popup (EOG_SCROLL_VIEW (priv->view),
@@ -4603,16 +4567,6 @@ eog_window_dispose (GObject *object)
 		priv->image = NULL;
 	}
 
-	if (priv->open_with_menu != NULL) {
-		g_object_unref (priv->open_with_menu);
-		priv->open_with_menu = NULL;
-	}
-
-	if (priv->appinfo != NULL) {
-		g_ptr_array_free (priv->appinfo, TRUE);
-		priv->appinfo = NULL;
-	}
-
 	fullscreen_clear_timeout (window);
 
 	if (window->priv->fullscreen_popup != NULL) {
-- 
2.25.1