SDL: linux: Add portal drag and drop (99421)

From 99421b64d0bfd52def3656c26f46c2fd0160eb6c Mon Sep 17 00:00:00 2001
From: Colin Kinloch <[EMAIL REDACTED]>
Date: Wed, 20 Sep 2023 16:13:31 +0100
Subject: [PATCH] linux: Add portal drag and drop

---
 src/core/linux/SDL_dbus.c                  | 66 ++++++++++++++++
 src/core/linux/SDL_dbus.h                  |  2 +
 src/video/wayland/SDL_waylanddatamanager.h |  1 +
 src/video/wayland/SDL_waylandevents.c      | 88 +++++++++++++++++-----
 4 files changed, 137 insertions(+), 20 deletions(-)

diff --git a/src/core/linux/SDL_dbus.c b/src/core/linux/SDL_dbus.c
index 11092c011d95..ba961be04285 100644
--- a/src/core/linux/SDL_dbus.c
+++ b/src/core/linux/SDL_dbus.c
@@ -565,4 +565,70 @@ char *SDL_DBus_GetLocalMachineId(void)
 
     return NULL;
 }
+
+/*
+ * Convert file drops with mime type "application/vnd.portal.filetransfer" to file paths
+ * Result must be freed with dbus->free_string_array().
+ * https://flatpak.github.io/xdg-desktop-portal/#gdbus-method-org-freedesktop-portal-FileTransfer.RetrieveFiles
+ */
+char **SDL_DBus_DocumentsPortalRetrieveFiles(const char *key, int *path_count)
+{
+    DBusError err;
+    DBusMessageIter iter, iterDict;
+    char **paths = NULL;
+    DBusMessage *reply = NULL;
+    DBusMessage *msg = dbus.message_new_method_call("org.freedesktop.portal.Documents",    /* Node */
+                                                    "/org/freedesktop/portal/documents",   /* Path */
+                                                    "org.freedesktop.portal.FileTransfer", /* Interface */
+                                                    "RetrieveFiles");                      /* Method */
+
+    /* Make sure we have a connection to the dbus session bus */
+    if (!SDL_DBus_GetContext() || !dbus.session_conn) {
+        /* We either cannot connect to the session bus or were unable to
+         * load the D-Bus library at all. */
+        return NULL;
+    }
+
+    dbus.error_init(&err);
+
+    /* First argument is a "application/vnd.portal.filetransfer" key from a DnD or clipboard event */
+    if (!dbus.message_append_args(msg, DBUS_TYPE_STRING, &key, DBUS_TYPE_INVALID)) {
+        SDL_OutOfMemory();
+        dbus.message_unref(msg);
+        goto failed;
+    }
+
+    /* Second argument is a variant dictionary for options.
+     * The spec doesn't define any entries yet so it's empty. */
+    dbus.message_iter_init_append(msg, &iter);
+    if (!dbus.message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &iterDict) ||
+        !dbus.message_iter_close_container(&iter,  &iterDict)) {
+        SDL_OutOfMemory();
+        dbus.message_unref(msg);
+        goto failed;
+    }
+
+    reply = dbus.connection_send_with_reply_and_block(dbus.session_conn, msg, DBUS_TIMEOUT_USE_DEFAULT, &err);
+    dbus.message_unref(msg);
+
+    if (reply) {
+        dbus.message_get_args(reply, &err, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING, &paths, path_count, DBUS_TYPE_INVALID);
+        dbus.message_unref(reply);
+    }
+
+    if (paths) {
+        return paths;
+    }
+
+failed:
+    if (dbus.error_is_set(&err)) {
+        SDL_SetError("%s: %s", err.name, err.message);
+        dbus.error_free(&err);
+    } else {
+        SDL_SetError("Error retrieving paths for documents portal \"%s\"", key);
+    }
+
+    return NULL;
+}
+
 #endif
diff --git a/src/core/linux/SDL_dbus.h b/src/core/linux/SDL_dbus.h
index 9234c5926cbc..7a6feeb3c9ec 100644
--- a/src/core/linux/SDL_dbus.h
+++ b/src/core/linux/SDL_dbus.h
@@ -102,6 +102,8 @@ extern SDL_bool SDL_DBus_ScreensaverInhibit(SDL_bool inhibit);
 extern void SDL_DBus_PumpEvents(void);
 extern char *SDL_DBus_GetLocalMachineId(void);
 
+extern char **SDL_DBus_DocumentsPortalRetrieveFiles(const char *key, int *files_count);
+
 #endif /* HAVE_DBUS_DBUS_H */
 
 #endif /* SDL_dbus_h_ */
diff --git a/src/video/wayland/SDL_waylanddatamanager.h b/src/video/wayland/SDL_waylanddatamanager.h
index 6660b18be403..1ffaf730f2c2 100644
--- a/src/video/wayland/SDL_waylanddatamanager.h
+++ b/src/video/wayland/SDL_waylanddatamanager.h
@@ -29,6 +29,7 @@
 
 #define TEXT_MIME "text/plain;charset=utf-8"
 #define FILE_MIME "text/uri-list"
+#define FILE_PORTAL_MIME "application/vnd.portal.filetransfer"
 
 typedef struct
 {
diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index 90e4584a5d8f..68d2f5b6c40c 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -1848,18 +1848,26 @@ static void data_device_handle_enter(void *data, struct wl_data_device *wl_data_
         data_device->drag_offer = wl_data_offer_get_user_data(id);
 
         /* TODO: SDL Support more mime types */
-        has_mime = Wayland_data_offer_has_mime(
-            data_device->drag_offer, FILE_MIME);
-
-        /* If drag_mime is NULL this will decline the offer */
-        wl_data_offer_accept(id, serial,
-                             (has_mime == SDL_TRUE) ? FILE_MIME : NULL);
+#ifdef SDL_USE_LIBDBUS
+        if (Wayland_data_offer_has_mime(data_device->drag_offer, FILE_PORTAL_MIME)) {
+            has_mime = SDL_TRUE;
+            wl_data_offer_accept(id, serial, FILE_PORTAL_MIME);
+        }
+#endif
+        if (Wayland_data_offer_has_mime(data_device->drag_offer, FILE_MIME)) {
+            has_mime = SDL_TRUE;
+            wl_data_offer_accept(id, serial, FILE_MIME);
+        }
 
         /* SDL only supports "copy" style drag and drop */
-        if (has_mime == SDL_TRUE) {
+        if (has_mime) {
             dnd_action = WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY;
+        } else {
+            /* drag_mime is NULL this will decline the offer */
+            wl_data_offer_accept(id, serial, NULL);
         }
-        if (wl_data_offer_get_version(data_device->drag_offer->offer) >= 3) {
+        if (wl_data_offer_get_version(data_device->drag_offer->offer) >=
+            WL_DATA_OFFER_SET_ACTIONS_SINCE_VERSION) {
             wl_data_offer_set_actions(data_device->drag_offer->offer,
                                       dnd_action, dnd_action);
         }
@@ -2041,21 +2049,61 @@ static void data_device_handle_drop(void *data, struct wl_data_device *wl_data_d
     if (data_device->drag_offer != NULL) {
         /* TODO: SDL Support more mime types */
         size_t length;
-        void *buffer = Wayland_data_offer_receive(data_device->drag_offer,
-                                                  FILE_MIME, &length);
-        if (buffer) {
-            char *saveptr = NULL;
-            char *token = SDL_strtok_r((char *)buffer, "\r\n", &saveptr);
-            while (token != NULL) {
-                char *fn = Wayland_URIToLocal(token);
-                if (fn) {
-                    SDL_SendDropFile(data_device->dnd_window, fn);
+        SDL_bool drop_handled = SDL_FALSE;
+#ifdef SDL_USE_LIBDBUS
+        if (Wayland_data_offer_has_mime(
+            data_device->drag_offer, FILE_PORTAL_MIME)) {
+            void *buffer = Wayland_data_offer_receive(data_device->drag_offer,
+                                                      FILE_PORTAL_MIME, &length);
+            if (buffer) {
+                SDL_DBusContext *dbus = SDL_DBus_GetContext();
+                if (dbus) {
+                    int path_count = 0;
+                    char **paths = SDL_DBus_DocumentsPortalRetrieveFiles(buffer, &path_count);
+                    /* If dropped files contain a directory the list is empty */
+                    if (paths && path_count > 0) {
+                        for (int i = 0; i < path_count; i++) {
+                            SDL_SendDropFile(data_device->dnd_window, paths[i]);
+                        }
+                        dbus->free_string_array(paths);
+                        SDL_SendDropComplete(data_device->dnd_window);
+                        drop_handled = SDL_TRUE;
+                    }
                 }
-                token = SDL_strtok_r(NULL, "\r\n", &saveptr);
+                SDL_free(buffer);
             }
-            SDL_SendDropComplete(data_device->dnd_window);
-            SDL_free(buffer);
         }
+#endif
+        /* If XDG document portal fails fallback.
+         * When running a flatpak sandbox this will most likely be a list of
+         * non paths that are not visible to the application
+         */
+        if (!drop_handled && Wayland_data_offer_has_mime(
+            data_device->drag_offer, FILE_MIME)) {
+            void *buffer = Wayland_data_offer_receive(data_device->drag_offer,
+                                                      FILE_MIME, &length);
+            if (buffer) {
+                char *saveptr = NULL;
+                char *token = SDL_strtok_r((char *)buffer, "\r\n", &saveptr);
+                while (token != NULL) {
+                    char *fn = Wayland_URIToLocal(token);
+                    if (fn) {
+                        SDL_SendDropFile(data_device->dnd_window, fn);
+                    }
+                    token = SDL_strtok_r(NULL, "\r\n", &saveptr);
+                }
+                SDL_SendDropComplete(data_device->dnd_window);
+                SDL_free(buffer);
+                drop_handled = SDL_TRUE;
+            }
+        }
+
+        if (drop_handled && wl_data_offer_get_version(data_device->drag_offer->offer) >=
+            WL_DATA_OFFER_FINISH_SINCE_VERSION) {
+            wl_data_offer_finish(data_device->drag_offer->offer);
+        }
+        Wayland_data_offer_destroy(data_device->drag_offer);
+        data_device->drag_offer = NULL;
     }
 }