diff --git a/src/core/linux/SDL_dbus.c b/src/core/linux/SDL_dbus.c index 2d083e8f29..df114e0e19 100644 --- a/src/core/linux/SDL_dbus.c +++ b/src/core/linux/SDL_dbus.c @@ -471,6 +471,63 @@ static bool SDL_DBus_AppendDictWithKeyValue(DBusMessageIter *iterInit, const cha return SDL_DBus_AppendDictWithKeysAndValues(iterInit, keys, values, 1); } +bool SDL_DBus_OpenURI(const char *uri, const char *window_id, const char *activation_token) +{ + const char *bus_name = "org.freedesktop.portal.Desktop"; + const char *path = "/org/freedesktop/portal/desktop"; + const char *interface = "org.freedesktop.portal.OpenURI"; + DBusMessageIter iterInit; + bool ret = false; + + if (!dbus.session_conn) { + /* We either lost connection to the session bus or were not able to + * load the D-Bus library at all. + */ + return false; + } + + DBusMessage *msg = dbus.message_new_method_call(bus_name, path, interface, "OpenURI"); + if (!msg) { + return false; + } + + if (!window_id) { + window_id = ""; + } + if (!dbus.message_append_args(msg, DBUS_TYPE_STRING, &window_id, DBUS_TYPE_STRING, &uri, DBUS_TYPE_INVALID)) { + goto done; + } + + dbus.message_iter_init_append(msg, &iterInit); + + if (activation_token) { + if (!SDL_DBus_AppendDictWithKeyValue(&iterInit, "activation_token", activation_token)) { + goto done; + } + } else { + // The array must be in the parameter list, even if empty. + DBusMessageIter iterArray; + if (!dbus.message_iter_open_container(&iterInit, DBUS_TYPE_ARRAY, "{sv}", &iterArray)) { + goto done; + } + if (!dbus.message_iter_close_container(&iterInit, &iterArray)) { + goto done; + } + } + + { + DBusMessage *reply = dbus.connection_send_with_reply_and_block(dbus.session_conn, msg, -1, NULL); + if (reply) { + ret = true; + dbus.message_unref(reply); + } + } + +done: + dbus.message_unref(msg); + return ret; +} + bool SDL_DBus_ScreensaverInhibit(bool inhibit) { const char *default_inhibit_reason = "Playing a game"; diff --git a/src/core/linux/SDL_dbus.h b/src/core/linux/SDL_dbus.h index e6f81b48ac..568dd74bb2 100644 --- a/src/core/linux/SDL_dbus.h +++ b/src/core/linux/SDL_dbus.h @@ -119,6 +119,8 @@ extern void SDL_DBus_FreeReply(DBusMessage **saved_reply); extern void SDL_DBus_ScreensaverTickle(void); extern bool SDL_DBus_ScreensaverInhibit(bool inhibit); +extern bool SDL_DBus_OpenURI(const char *uri, const char *window_id, const char *activation_token); + extern void SDL_DBus_PumpEvents(void); extern char *SDL_DBus_GetLocalMachineId(void); diff --git a/src/misc/unix/SDL_sysurl.c b/src/misc/unix/SDL_sysurl.c index 8745576e2c..32e2d476ea 100644 --- a/src/misc/unix/SDL_sysurl.c +++ b/src/misc/unix/SDL_sysurl.c @@ -33,8 +33,42 @@ extern char **environ; #endif +#ifdef HAVE_DBUS_DBUS_H +#include "../../core/linux/SDL_dbus.h" +#endif + +#ifdef SDL_VIDEO_DRIVER_WAYLAND +#include "../../video/wayland/SDL_waylandutil.h" +#endif + +// Wayland requires an activation token for the browser to take focus. +static void GetActivationToken(char **token, char **window_id) +{ +#ifdef SDL_VIDEO_DRIVER_WAYLAND + SDL_VideoDevice *vid = SDL_GetVideoDevice(); + + if (vid && SDL_strcmp(vid->name, "wayland") == 0) { + Wayland_GetActivationTokenForExport(vid, token, window_id); + } +#endif +} + bool SDL_SYS_OpenURL(const char *url) { + char *activation_token = NULL; + char *window_id = NULL; + + GetActivationToken(&activation_token, &window_id); + + // Prefer the D-Bus portal, if available. +#ifdef HAVE_DBUS_DBUS_H + if (SDL_DBus_OpenURI(url, window_id, activation_token)) { + SDL_free(activation_token); + SDL_free(window_id); + return true; + } +#endif + const char *args[] = { "xdg-open", url, NULL }; SDL_Environment *env = NULL; SDL_Process *process = NULL; @@ -47,6 +81,9 @@ bool SDL_SYS_OpenURL(const char *url) // Clear LD_PRELOAD so Chrome opens correctly when this application is launched by Steam SDL_UnsetEnvironmentVariable(env, "LD_PRELOAD"); + if (activation_token) { + SDL_SetEnvironmentVariable(env, "XDG_ACTIVATION_TOKEN", activation_token, false); + } SDL_PropertiesID props = SDL_CreateProperties(); if (!props) { @@ -64,6 +101,8 @@ bool SDL_SYS_OpenURL(const char *url) result = true; done: + SDL_free(activation_token); + SDL_free(window_id); SDL_DestroyEnvironment(env); SDL_DestroyProcess(process); diff --git a/src/video/wayland/SDL_waylandutil.c b/src/video/wayland/SDL_waylandutil.c new file mode 100644 index 0000000000..d6461cb071 --- /dev/null +++ b/src/video/wayland/SDL_waylandutil.c @@ -0,0 +1,134 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_WAYLAND + +#include "SDL_waylandevents_c.h" +#include "SDL_waylandutil.h" +#include "xdg-activation-v1-client-protocol.h" + +#define WAYLAND_HANDLE_PREFIX "wayland:" + +typedef struct Wayland_ActivationParams +{ + char **token; + bool done; +} Wayland_ActivationParams; + +static void handle_xdg_activation_done(void *data, struct xdg_activation_token_v1 *xdg_activation_token_v1, const char *token) +{ + Wayland_ActivationParams *activation_params = (Wayland_ActivationParams *)data; + *activation_params->token = SDL_strdup(token); + activation_params->done = true; + + xdg_activation_token_v1_destroy(xdg_activation_token_v1); +} + +static const struct xdg_activation_token_v1_listener xdg_activation_listener = { + handle_xdg_activation_done +}; + +bool Wayland_GetActivationTokenForExport(SDL_VideoDevice *_this, char **token, char **window_id) +{ + if (!_this || !token) { + return false; + } + + SDL_VideoData *viddata = _this->internal; + + SDL_WaylandSeat *seat = viddata->last_implicit_grab_seat; + SDL_WindowData *focus = NULL; + + if (seat) { + focus = seat->keyboard.focus; + if (!focus) { + focus = seat->pointer.focus; + } + } + + const char *xdg_activation_token = SDL_getenv("XDG_ACTIVATION_TOKEN"); + if (xdg_activation_token) { + *token = SDL_strdup(xdg_activation_token); + if (!*token) { + return false; + } + + // Unset the envvar after claiming the token. + SDL_unsetenv_unsafe("XDG_ACTIVATION_TOKEN"); + } else if (viddata->activation_manager) { + struct wl_surface *requesting_surface = focus ? focus->surface : NULL; + Wayland_ActivationParams params = { + .token = token, + .done = false + }; + + struct wl_event_queue *activation_token_queue = Wayland_DisplayCreateQueue(viddata->display, "SDL Activation Token Generation Queue"); + + struct wl_proxy *activation_manager_wrapper = WAYLAND_wl_proxy_create_wrapper(viddata->activation_manager); + WAYLAND_wl_proxy_set_queue(activation_manager_wrapper, activation_token_queue); + struct xdg_activation_token_v1 *activation_token = xdg_activation_v1_get_activation_token((struct xdg_activation_v1 *)activation_manager_wrapper); + xdg_activation_token_v1_add_listener(activation_token, &xdg_activation_listener, ¶ms); + + if (requesting_surface) { + // This specifies the surface from which the activation request is originating, not the activation target surface. + xdg_activation_token_v1_set_surface(activation_token, requesting_surface); + } + if (seat && seat->wl_seat) { + xdg_activation_token_v1_set_serial(activation_token, seat->last_implicit_grab_serial, seat->wl_seat); + } + if (focus && focus->app_id) { + // Set the app ID for external use. + xdg_activation_token_v1_set_app_id(activation_token, focus->app_id); + } + xdg_activation_token_v1_commit(activation_token); + + while (!params.done) { + WAYLAND_wl_display_dispatch_queue(viddata->display, activation_token_queue); + } + WAYLAND_wl_proxy_wrapper_destroy(activation_manager_wrapper); + WAYLAND_wl_event_queue_destroy(activation_token_queue); + + if (!*token) { + return false; + } + } + + if (focus && window_id) { + const char *id = SDL_GetStringProperty(focus->sdlwindow->props, SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_EXPORT_HANDLE_STRING, NULL); + if (id) { + const size_t len = SDL_strlen(id) + sizeof(WAYLAND_HANDLE_PREFIX) + 1; + *window_id = SDL_malloc(len); + if (!*window_id) { + SDL_free(*token); + *token = NULL; + return false; + } + + SDL_strlcpy(*window_id, WAYLAND_HANDLE_PREFIX, len); + SDL_strlcat(*window_id, id, len); + } + } + + return true; +} + +#endif // SDL_VIDEO_DRIVER_WAYLAND diff --git a/src/video/wayland/SDL_waylandutil.h b/src/video/wayland/SDL_waylandutil.h new file mode 100644 index 0000000000..daad6effb3 --- /dev/null +++ b/src/video/wayland/SDL_waylandutil.h @@ -0,0 +1,34 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_waylandutil_h_ +#define SDL_waylandutil_h_ + +#include "../SDL_sysvideo.h" + +/** + * Generates an activation token that can be passed to external clients. + * The token and window_id parameters must be freed with SDL_free() when done. + */ +extern bool Wayland_GetActivationTokenForExport(SDL_VideoDevice *_this, char **token, char **window_id); + +#endif // SDL_waylandutil_h_ diff --git a/test/testurl.c b/test/testurl.c index 56b1904e6a..a6fd9c5ec4 100644 --- a/test/testurl.c +++ b/test/testurl.c @@ -25,29 +25,27 @@ static void tryOpenURL(const char *url) int main(int argc, char **argv) { - int i; - SDLTest_CommonState *state; - - state = SDLTest_CommonCreateState(argv, 0); - - if (!SDL_Init(SDL_INIT_VIDEO)) { - SDL_Log("SDL_Init failed: %s", SDL_GetError()); - return 1; - } + const char *url = NULL; + SDLTest_CommonState *state = SDLTest_CommonCreateState(argv, 0); + bool use_gui = false; /* Parse commandline */ - for (i = 1; i < argc;) { + for (int i = 1; i < argc;) { int consumed; consumed = SDLTest_CommonArg(state, i); if (consumed == 0) { if (argv[i][0] != '-') { - tryOpenURL(argv[i]); + url = argv[i]; + consumed = 1; + } else if (SDL_strcasecmp(argv[i], "--gui") == 0) { + use_gui = true; consumed = 1; } } if (consumed <= 0) { static const char *options[] = { + "[--gui]" "[URL [...]]", NULL, }; @@ -57,6 +55,38 @@ int main(int argc, char **argv) i += consumed; } + state->flags = SDL_INIT_VIDEO; + if (!SDLTest_CommonInit(state)) { + return SDL_APP_FAILURE; + } + + if (!use_gui) { + tryOpenURL(url); + } else { + SDL_Event event; + bool quit = false; + + while (!quit) { + while (SDL_PollEvent(&event)) { + if (event.type == SDL_EVENT_KEY_DOWN) { + if (event.key.key == SDLK_SPACE) { + tryOpenURL(url); + } else if (event.key.key == SDLK_ESCAPE) { + quit = true; + } + } else if (event.type == SDL_EVENT_QUIT) { + quit = true; + } + } + + SDL_SetRenderDrawColor(state->renderers[0], 0, 0, 0, 255); + SDL_RenderClear(state->renderers[0]); + SDL_SetRenderDrawColor(state->renderers[0], 255, 255, 255, 255); + SDL_RenderDebugTextFormat(state->renderers[0], 8.f, 16.f, "Press space to open %s", url); + SDL_RenderPresent(state->renderers[0]); + } + } + SDL_Quit(); SDLTest_CommonDestroyState(state); return 0;