From 9af90886c8d74fee849b790fb3efd44662aa1fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Sun, 31 Aug 2025 14:30:35 +0200 Subject: [PATCH] Wayland: Add support for dropping files when sandboxed Adds support for receiving drops that make use of the [File Transfer][1] portal. This is the case in sandboxed environments such as Flatpak or Snap. [1]: https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileTransfer.html --- .github/workflows/build.yml | 2 +- README.md | 1 + src/CMakeLists.txt | 4 ++ src/wl_platform.h | 2 + src/wl_window.c | 128 +++++++++++++++++++++++++++++++++++- 5 files changed, 134 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ef9ed79..d7e3c530 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,7 +21,7 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev libwayland-dev libxkbcommon-dev + sudo apt install libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev libwayland-dev libxkbcommon-dev libdbus-1-dev - name: Configure Null shared library run: cmake -B build-null-shared -D GLFW_BUILD_WAYLAND=OFF -D GLFW_BUILD_X11=OFF -D BUILD_SHARED_LIBS=ON diff --git a/README.md b/README.md index 6eb6b9bd..bba49267 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ information on what to include when reporting a bug. a modal to a fallback decoration - [Wayland] Bugfix: The cursor position was not updated when clicking through from a modal to the content area + - [Wayland]: Add support for dropping files when sandboxed - [X11] Bugfix: Running without a WM could trigger an assert (#2593,#2601,#2631) - [Null] Added Vulkan 'window' surface creation via `VK_EXT_headless_surface` - [Null] Added EGL context creation on Mesa via `EGL_MESA_platform_surfaceless` diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2cbe8a73..63d51304 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -161,6 +161,10 @@ if (GLFW_BUILD_WAYLAND) wayland-egl>=0.2.7 xkbcommon>=0.5.0) + pkg_check_modules(DBUS REQUIRED dbus-1) + target_include_directories(glfw PRIVATE ${DBUS_INCLUDE_DIRS}) + target_link_libraries(glfw PRIVATE ${DBUS_LIBRARIES}) + target_include_directories(glfw PRIVATE ${Wayland_INCLUDE_DIRS}) if (NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") diff --git a/src/wl_platform.h b/src/wl_platform.h index c3e45693..aa724a16 100644 --- a/src/wl_platform.h +++ b/src/wl_platform.h @@ -338,6 +338,7 @@ typedef struct _GLFWofferWayland struct wl_data_offer* offer; GLFWbool text_plain_utf8; GLFWbool text_uri_list; + GLFWbool portal_file_transfer; } _GLFWofferWayland; typedef struct _GLFWscaleWayland @@ -450,6 +451,7 @@ typedef struct _GLFWlibraryWayland struct wl_data_offer* dragOffer; _GLFWwindow* dragFocus; uint32_t dragSerial; + GLFWbool dragUsePortal; const char* tag; diff --git a/src/wl_window.c b/src/wl_window.c index 4220d17e..babc3117 100644 --- a/src/wl_window.c +++ b/src/wl_window.c @@ -41,6 +41,7 @@ #include #include #include +#include #include "wayland-client-protocol.h" #include "xdg-shell-client-protocol.h" @@ -54,6 +55,7 @@ #define GLFW_BORDER_SIZE 4 #define GLFW_CAPTION_HEIGHT 24 +#define FILE_TRANSFER_PORTAL_MIME_TYPE "application/vnd.portal.filetransfer" static int createTmpfileCloexec(char* tmpname) { @@ -1969,6 +1971,8 @@ static void dataOfferHandleOffer(void* userData, _glfw.wl.offers[i].text_plain_utf8 = GLFW_TRUE; else if (strcmp(mimeType, "text/uri-list") == 0) _glfw.wl.offers[i].text_uri_list = GLFW_TRUE; + else if (strcmp(mimeType, FILE_TRANSFER_PORTAL_MIME_TYPE) == 0) + _glfw.wl.offers[i].portal_file_transfer = GLFW_TRUE; break; } @@ -2031,13 +2035,19 @@ static void dataDeviceHandleEnter(void* userData, _GLFWwindow* window = wl_surface_get_user_data(surface); if (window->wl.surface == surface) { - if (_glfw.wl.offers[i].text_uri_list) + GLFWbool portal = _glfw.wl.offers[i].portal_file_transfer; + if (_glfw.wl.offers[i].text_uri_list || portal) { _glfw.wl.dragOffer = offer; _glfw.wl.dragFocus = window; _glfw.wl.dragSerial = serial; + _glfw.wl.dragUsePortal = portal; - wl_data_offer_accept(offer, serial, "text/uri-list"); + if (portal) { + wl_data_offer_accept(offer, serial, FILE_TRANSFER_PORTAL_MIME_TYPE); + } else { + wl_data_offer_accept(offer, serial, "text/uri-list"); + } } } } @@ -2071,12 +2081,126 @@ static void dataDeviceHandleMotion(void* userData, { } +// Receives a dropped file that was sent using the +// [File Transfer](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileTransfer.html) portal. +// This enables us to receive files when running as a Flatpak or Snap. +static void dataDeviceHandleFileTransferPortalDrop(void* userData, + struct wl_data_device* device) +{ + char* key = readDataOfferAsString(_glfw.wl.dragOffer, FILE_TRANSFER_PORTAL_MIME_TYPE); + if (!key) + return; + + DBusError error; + dbus_error_init(&error); + DBusConnection* connection = dbus_bus_get(DBUS_BUS_SESSION, &error); + if (dbus_error_is_set(&error)) + { + _glfwInputError(GLFW_PLATFORM_ERROR, "DBus: %s", error.message); + dbus_error_free(&error); + _glfw_free(key); + return; + } + dbus_connection_set_exit_on_disconnect(connection, FALSE); + + DBusMessage* message = dbus_message_new_method_call( + "org.freedesktop.portal.Documents", + "/org/freedesktop/portal/documents", + "org.freedesktop.portal.FileTransfer", + "RetrieveFiles" + ); + if (!message) + { + _glfwInputError(GLFW_OUT_OF_MEMORY, NULL); + _glfw_free(key); + return; + } + DBusMessageIter args, options; + dbus_message_iter_init_append(message, &args); + if (!dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, &key)) + { + _glfwInputError(GLFW_OUT_OF_MEMORY, NULL); + dbus_message_unref(message); + _glfw_free(key); + return; + } + if (!dbus_message_iter_open_container(&args, DBUS_TYPE_ARRAY, "{sv}", &options)) + { + _glfwInputError(GLFW_OUT_OF_MEMORY, NULL); + dbus_message_unref(message); + _glfw_free(key); + return; + } + if (!dbus_message_iter_close_container(&args, &options)) + { + _glfwInputError(GLFW_OUT_OF_MEMORY, NULL); + dbus_message_unref(message); + _glfw_free(key); + return; + } + + DBusMessage* reply = dbus_connection_send_with_reply_and_block(connection, message, DBUS_TIMEOUT_INFINITE, &error); + if (dbus_error_is_set(&error)) + { + _glfwInputError(GLFW_PLATFORM_ERROR, "DBus: %s", error.message); + dbus_error_free(&error); + dbus_message_unref(message); + _glfw_free(key); + return; + } + + DBusMessageIter out, array; + if (!dbus_message_iter_init(reply, &out)) + { + _glfwInputError(GLFW_OUT_OF_MEMORY, NULL); + dbus_message_unref(reply); + dbus_message_unref(message); + _glfw_free(key); + return; + } + if (dbus_message_iter_get_arg_type(&out) != DBUS_TYPE_ARRAY + || dbus_message_iter_get_element_type(&out) != DBUS_TYPE_STRING) { + _glfwInputError(GLFW_PLATFORM_ERROR, "DBus: Reply is not an array of strings"); + dbus_message_unref(reply); + dbus_message_unref(message); + _glfw_free(key); + return; + } + dbus_message_iter_recurse(&out, &array); + int elements = dbus_message_iter_get_element_count(&out); + char** paths = _glfw_calloc(elements, sizeof(char*)); + if (!paths) { + _glfwInputError(GLFW_OUT_OF_MEMORY, NULL); + dbus_message_unref(reply); + dbus_message_unref(message); + _glfw_free(key); + return; + } + int i = 0; + do { + dbus_message_iter_get_basic(&array, &paths[i++]); + } while (dbus_message_iter_next(&array)); + + _glfwInputDrop(_glfw.wl.dragFocus, elements, (const char**) paths); + + _glfw_free(paths); + dbus_message_unref(reply); + dbus_message_unref(message); + _glfw_free(key); +} + static void dataDeviceHandleDrop(void* userData, struct wl_data_device* device) { if (!_glfw.wl.dragOffer) return; + if (_glfw.wl.dragUsePortal) + { + dataDeviceHandleFileTransferPortalDrop(userData, device); + return; + } + char* string = readDataOfferAsString(_glfw.wl.dragOffer, "text/uri-list"); if (string) {