From 99421b64d0bfd52def3656c26f46c2fd0160eb6c Mon Sep 17 00:00:00 2001 From: Colin Kinloch 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 11092c011..ba961be04 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 9234c5926..7a6feeb3c 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 6660b18be..1ffaf730f 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 90e4584a5..68d2f5b6c 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; } }