From 5a079a2d114f96d4847d1ee305d5b7c16eeec50e Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 27 Dec 2025 12:03:39 -0800 Subject: Initial commit --- .../SDL-3.2.8/src/dialog/unix/SDL_portaldialog.c | 545 +++++++++++++++++++++ .../SDL-3.2.8/src/dialog/unix/SDL_portaldialog.h | 27 + contrib/SDL-3.2.8/src/dialog/unix/SDL_unixdialog.c | 81 +++ .../SDL-3.2.8/src/dialog/unix/SDL_zenitydialog.c | 366 ++++++++++++++ .../SDL-3.2.8/src/dialog/unix/SDL_zenitydialog.h | 27 + 5 files changed, 1046 insertions(+) create mode 100644 contrib/SDL-3.2.8/src/dialog/unix/SDL_portaldialog.c create mode 100644 contrib/SDL-3.2.8/src/dialog/unix/SDL_portaldialog.h create mode 100644 contrib/SDL-3.2.8/src/dialog/unix/SDL_unixdialog.c create mode 100644 contrib/SDL-3.2.8/src/dialog/unix/SDL_zenitydialog.c create mode 100644 contrib/SDL-3.2.8/src/dialog/unix/SDL_zenitydialog.h (limited to 'contrib/SDL-3.2.8/src/dialog/unix') diff --git a/contrib/SDL-3.2.8/src/dialog/unix/SDL_portaldialog.c b/contrib/SDL-3.2.8/src/dialog/unix/SDL_portaldialog.c new file mode 100644 index 0000000..efecd12 --- /dev/null +++ b/contrib/SDL-3.2.8/src/dialog/unix/SDL_portaldialog.c @@ -0,0 +1,545 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 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" +#include "../SDL_dialog_utils.h" + +#include "../../core/linux/SDL_dbus.h" + +#ifdef SDL_USE_LIBDBUS + +#include +#include +#include +#include + +#define PORTAL_DESTINATION "org.freedesktop.portal.Desktop" +#define PORTAL_PATH "/org/freedesktop/portal/desktop" +#define PORTAL_INTERFACE "org.freedesktop.portal.FileChooser" + +#define SIGNAL_SENDER "org.freedesktop.portal.Desktop" +#define SIGNAL_INTERFACE "org.freedesktop.portal.Request" +#define SIGNAL_NAME "Response" +#define SIGNAL_FILTER "type='signal', sender='"SIGNAL_SENDER"', interface='"SIGNAL_INTERFACE"', member='"SIGNAL_NAME"', path='" + +#define HANDLE_LEN 10 + +#define WAYLAND_HANDLE_PREFIX "wayland:" +#define X11_HANDLE_PREFIX "x11:" + +typedef struct { + SDL_DialogFileCallback callback; + void *userdata; + const char *path; +} SignalCallback; + +static void DBus_AppendStringOption(SDL_DBusContext *dbus, DBusMessageIter *options, const char *key, const char *value) +{ + DBusMessageIter options_pair, options_value; + + dbus->message_iter_open_container(options, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair); + dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &key); + dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, "s", &options_value); + dbus->message_iter_append_basic(&options_value, DBUS_TYPE_STRING, &value); + dbus->message_iter_close_container(&options_pair, &options_value); + dbus->message_iter_close_container(options, &options_pair); +} + +static void DBus_AppendBoolOption(SDL_DBusContext *dbus, DBusMessageIter *options, const char *key, int value) +{ + DBusMessageIter options_pair, options_value; + + dbus->message_iter_open_container(options, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair); + dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &key); + dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, "b", &options_value); + dbus->message_iter_append_basic(&options_value, DBUS_TYPE_BOOLEAN, &value); + dbus->message_iter_close_container(&options_pair, &options_value); + dbus->message_iter_close_container(options, &options_pair); +} + +static void DBus_AppendFilter(SDL_DBusContext *dbus, DBusMessageIter *parent, const SDL_DialogFileFilter filter) +{ + DBusMessageIter filter_entry, filter_array, filter_array_entry; + char *state = NULL, *patterns, *pattern, *glob_pattern; + int zero = 0; + + dbus->message_iter_open_container(parent, DBUS_TYPE_STRUCT, NULL, &filter_entry); + dbus->message_iter_append_basic(&filter_entry, DBUS_TYPE_STRING, &filter.name); + dbus->message_iter_open_container(&filter_entry, DBUS_TYPE_ARRAY, "(us)", &filter_array); + + patterns = SDL_strdup(filter.pattern); + if (!patterns) { + goto cleanup; + } + + pattern = SDL_strtok_r(patterns, ";", &state); + while (pattern) { + size_t max_len = SDL_strlen(pattern) + 3; + + dbus->message_iter_open_container(&filter_array, DBUS_TYPE_STRUCT, NULL, &filter_array_entry); + dbus->message_iter_append_basic(&filter_array_entry, DBUS_TYPE_UINT32, &zero); + + glob_pattern = SDL_calloc(max_len, sizeof(char)); + if (!glob_pattern) { + goto cleanup; + } + glob_pattern[0] = '*'; + /* Special case: The '*' filter doesn't need to be rewritten */ + if (pattern[0] != '*' || pattern[1]) { + glob_pattern[1] = '.'; + SDL_strlcat(glob_pattern + 2, pattern, max_len); + } + dbus->message_iter_append_basic(&filter_array_entry, DBUS_TYPE_STRING, &glob_pattern); + SDL_free(glob_pattern); + + dbus->message_iter_close_container(&filter_array, &filter_array_entry); + pattern = SDL_strtok_r(NULL, ";", &state); + } + +cleanup: + SDL_free(patterns); + + dbus->message_iter_close_container(&filter_entry, &filter_array); + dbus->message_iter_close_container(parent, &filter_entry); +} + +static void DBus_AppendFilters(SDL_DBusContext *dbus, DBusMessageIter *options, const SDL_DialogFileFilter *filters, int nfilters) +{ + DBusMessageIter options_pair, options_value, options_value_array; + static const char *filters_name = "filters"; + + dbus->message_iter_open_container(options, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair); + dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &filters_name); + dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, "a(sa(us))", &options_value); + dbus->message_iter_open_container(&options_value, DBUS_TYPE_ARRAY, "(sa(us))", &options_value_array); + for (int i = 0; i < nfilters; i++) { + DBus_AppendFilter(dbus, &options_value_array, filters[i]); + } + dbus->message_iter_close_container(&options_value, &options_value_array); + dbus->message_iter_close_container(&options_pair, &options_value); + dbus->message_iter_close_container(options, &options_pair); +} + +static void DBus_AppendByteArray(SDL_DBusContext *dbus, DBusMessageIter *options, const char *key, const char *value) +{ + DBusMessageIter options_pair, options_value, options_array; + + dbus->message_iter_open_container(options, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair); + dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &key); + dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, "ay", &options_value); + dbus->message_iter_open_container(&options_value, DBUS_TYPE_ARRAY, "y", &options_array); + do { + dbus->message_iter_append_basic(&options_array, DBUS_TYPE_BYTE, value); + } while (*value++); + dbus->message_iter_close_container(&options_value, &options_array); + dbus->message_iter_close_container(&options_pair, &options_value); + dbus->message_iter_close_container(options, &options_pair); +} + +static DBusHandlerResult DBus_MessageFilter(DBusConnection *conn, DBusMessage *msg, void *data) +{ + SDL_DBusContext *dbus = SDL_DBus_GetContext(); + SignalCallback *signal_data = (SignalCallback *)data; + + if (dbus->message_is_signal(msg, SIGNAL_INTERFACE, SIGNAL_NAME) && + dbus->message_has_path(msg, signal_data->path)) { + DBusMessageIter signal_iter, result_array, array_entry, value_entry, uri_entry; + uint32_t result; + size_t length = 2, current = 0; + const char **path = NULL; + + dbus->message_iter_init(msg, &signal_iter); + // Check if the parameters are what we expect + if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_UINT32) { + goto not_our_signal; + } + dbus->message_iter_get_basic(&signal_iter, &result); + + if (result == 1 || result == 2) { + // cancelled + const char *result_data[] = { NULL }; + signal_data->callback(signal_data->userdata, result_data, -1); // TODO: Set this to the last selected filter + goto done; + + } else if (result) { + // some error occurred + signal_data->callback(signal_data->userdata, NULL, -1); + goto done; + } + + if (!dbus->message_iter_next(&signal_iter)) { + goto not_our_signal; + } + + if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_ARRAY) { + goto not_our_signal; + } + + dbus->message_iter_recurse(&signal_iter, &result_array); + + while (dbus->message_iter_get_arg_type(&result_array) == DBUS_TYPE_DICT_ENTRY) { + const char *method; + + dbus->message_iter_recurse(&result_array, &array_entry); + if (dbus->message_iter_get_arg_type(&array_entry) != DBUS_TYPE_STRING) { + goto not_our_signal; + } + + dbus->message_iter_get_basic(&array_entry, &method); + if (!SDL_strcmp(method, "uris")) { + // we only care about the selected file paths + break; + } + + if (!dbus->message_iter_next(&result_array)) { + goto not_our_signal; + } + } + + if (!dbus->message_iter_next(&array_entry)) { + goto not_our_signal; + } + + if (dbus->message_iter_get_arg_type(&array_entry) != DBUS_TYPE_VARIANT) { + goto not_our_signal; + } + dbus->message_iter_recurse(&array_entry, &value_entry); + + if (dbus->message_iter_get_arg_type(&value_entry) != DBUS_TYPE_ARRAY) { + goto not_our_signal; + } + dbus->message_iter_recurse(&value_entry, &uri_entry); + + path = SDL_malloc(length * sizeof(const char *)); + if (!path) { + signal_data->callback(signal_data->userdata, NULL, -1); + goto done; + } + + while (dbus->message_iter_get_arg_type(&uri_entry) == DBUS_TYPE_STRING) { + const char *uri = NULL; + + if (current >= length - 1) { + ++length; + const char **newpath = SDL_realloc(path, length * sizeof(const char *)); + if (!newpath) { + signal_data->callback(signal_data->userdata, NULL, -1); + goto done; + } + path = newpath; + } + + dbus->message_iter_get_basic(&uri_entry, &uri); + + // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileChooser.html + // Returned paths will always start with 'file://'; SDL_URIToLocal() truncates it. + char *decoded_uri = SDL_malloc(SDL_strlen(uri) + 1); + if (SDL_URIToLocal(uri, decoded_uri)) { + path[current] = decoded_uri; + } else { + SDL_free(decoded_uri); + SDL_SetError("Portal dialogs: Unsupported protocol: %s", uri); + signal_data->callback(signal_data->userdata, NULL, -1); + goto done; + } + + dbus->message_iter_next(&uri_entry); + ++current; + } + path[current] = NULL; + signal_data->callback(signal_data->userdata, path, -1); // TODO: Fetch the index of the filter that was used +done: + dbus->connection_remove_filter(conn, &DBus_MessageFilter, signal_data); + + if (path) { + for (size_t i = 0; i < current; ++i) { + SDL_free((char *)path[i]); + } + SDL_free(path); + } + SDL_free((void *)signal_data->path); + SDL_free(signal_data); + return DBUS_HANDLER_RESULT_HANDLED; + } + +not_our_signal: + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +void SDL_Portal_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props) +{ + const char *method; + const char *method_title; + + SDL_Window* window = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, NULL); + SDL_DialogFileFilter *filters = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_FILTERS_POINTER, NULL); + int nfilters = (int) SDL_GetNumberProperty(props, SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, 0); + bool allow_many = SDL_GetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, false); + const char* default_location = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, NULL); + const char* accept = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_ACCEPT_STRING, NULL); + bool open_folders = false; + + switch (type) { + case SDL_FILEDIALOG_OPENFILE: + method = "OpenFile"; + method_title = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_TITLE_STRING, "Open File"); + break; + + case SDL_FILEDIALOG_SAVEFILE: + method = "SaveFile"; + method_title = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_TITLE_STRING, "Save File"); + break; + + case SDL_FILEDIALOG_OPENFOLDER: + method = "OpenFile"; + method_title = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_TITLE_STRING, "Open Folder"); + open_folders = true; + break; + + default: + /* This is already checked in ../SDL_dialog.c; this silences compiler warnings */ + SDL_SetError("Invalid file dialog type: %d", type); + callback(userdata, NULL, -1); + return; + } + + SDL_DBusContext *dbus = SDL_DBus_GetContext(); + DBusMessage *msg; + DBusMessageIter params, options; + const char *signal_id = NULL; + char *handle_str, *filter; + int filter_len; + static uint32_t handle_id = 0; + static char *default_parent_window = ""; + SDL_PropertiesID window_props = SDL_GetWindowProperties(window); + + const char *err_msg = validate_filters(filters, nfilters); + + if (err_msg) { + SDL_SetError("%s", err_msg); + callback(userdata, NULL, -1); + return; + } + + if (dbus == NULL) { + SDL_SetError("Failed to connect to DBus"); + callback(userdata, NULL, -1); + return; + } + + msg = dbus->message_new_method_call(PORTAL_DESTINATION, PORTAL_PATH, PORTAL_INTERFACE, method); + if (msg == NULL) { + SDL_SetError("Failed to send message to portal"); + callback(userdata, NULL, -1); + return; + } + + dbus->message_iter_init_append(msg, ¶ms); + + handle_str = default_parent_window; + if (window_props) { + const char *parent_handle = SDL_GetStringProperty(window_props, SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_EXPORT_HANDLE_STRING, NULL); + if (parent_handle) { + size_t len = SDL_strlen(parent_handle); + len += sizeof(WAYLAND_HANDLE_PREFIX) + 1; + handle_str = SDL_malloc(len * sizeof(char)); + if (!handle_str) { + callback(userdata, NULL, -1); + return; + } + + SDL_snprintf(handle_str, len, "%s%s", WAYLAND_HANDLE_PREFIX, parent_handle); + } else { + const Uint64 xid = (Uint64)SDL_GetNumberProperty(window_props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0); + if (xid) { + const size_t len = sizeof(X11_HANDLE_PREFIX) + 24; // A 64-bit number can be 20 characters max. + handle_str = SDL_malloc(len * sizeof(char)); + if (!handle_str) { + callback(userdata, NULL, -1); + return; + } + + // The portal wants X11 window ID numbers in hex. + SDL_snprintf(handle_str, len, "%s%" SDL_PRIx64, X11_HANDLE_PREFIX, xid); + } + } + } + + dbus->message_iter_append_basic(¶ms, DBUS_TYPE_STRING, &handle_str); + if (handle_str != default_parent_window) { + SDL_free(handle_str); + } + + dbus->message_iter_append_basic(¶ms, DBUS_TYPE_STRING, &method_title); + dbus->message_iter_open_container(¶ms, DBUS_TYPE_ARRAY, "{sv}", &options); + + handle_str = SDL_malloc(sizeof(char) * (HANDLE_LEN + 1)); + if (!handle_str) { + callback(userdata, NULL, -1); + return; + } + SDL_snprintf(handle_str, HANDLE_LEN, "%u", ++handle_id); + DBus_AppendStringOption(dbus, &options, "handle_token", handle_str); + SDL_free(handle_str); + + DBus_AppendBoolOption(dbus, &options, "modal", !!window); + if (allow_many) { + DBus_AppendBoolOption(dbus, &options, "multiple", 1); + } + if (open_folders) { + DBus_AppendBoolOption(dbus, &options, "directory", 1); + } + if (filters) { + DBus_AppendFilters(dbus, &options, filters, nfilters); + } + if (default_location) { + DBus_AppendByteArray(dbus, &options, "current_folder", default_location); + } + if (accept) { + DBus_AppendStringOption(dbus, &options, "accept_label", accept); + } + dbus->message_iter_close_container(¶ms, &options); + + DBusMessage *reply = dbus->connection_send_with_reply_and_block(dbus->session_conn, msg, DBUS_TIMEOUT_INFINITE, NULL); + if (reply) { + DBusMessageIter reply_iter; + dbus->message_iter_init(reply, &reply_iter); + + if (dbus->message_iter_get_arg_type(&reply_iter) == DBUS_TYPE_OBJECT_PATH) { + dbus->message_iter_get_basic(&reply_iter, &signal_id); + } + } + + if (!signal_id) { + SDL_SetError("Invalid response received by DBus"); + callback(userdata, NULL, -1); + goto incorrect_type; + } + + dbus->message_unref(msg); + + filter_len = SDL_strlen(SIGNAL_FILTER) + SDL_strlen(signal_id) + 2; + filter = SDL_malloc(sizeof(char) * filter_len); + if (!filter) { + callback(userdata, NULL, -1); + goto incorrect_type; + } + + SDL_snprintf(filter, filter_len, SIGNAL_FILTER"%s'", signal_id); + dbus->bus_add_match(dbus->session_conn, filter, NULL); + SDL_free(filter); + + SignalCallback *data = SDL_malloc(sizeof(SignalCallback)); + if (!data) { + callback(userdata, NULL, -1); + goto incorrect_type; + } + data->callback = callback; + data->userdata = userdata; + data->path = SDL_strdup(signal_id); + if (!data->path) { + SDL_free(data); + callback(userdata, NULL, -1); + goto incorrect_type; + } + + /* TODO: This should be registered before opening the portal, or the filter will not catch + the message if it is sent before we register the filter. + */ + dbus->connection_add_filter(dbus->session_conn, + &DBus_MessageFilter, data, NULL); + dbus->connection_flush(dbus->session_conn); + +incorrect_type: + dbus->message_unref(reply); +} + +bool SDL_Portal_detect(void) +{ + SDL_DBusContext *dbus = SDL_DBus_GetContext(); + DBusMessage *msg = NULL, *reply = NULL; + char *reply_str = NULL; + DBusMessageIter reply_iter; + static int portal_present = -1; + + // No need for this if the result is cached. + if (portal_present != -1) { + return (portal_present > 0); + } + + portal_present = 0; + + if (!dbus) { + SDL_SetError("%s", "Failed to connect to DBus!"); + return false; + } + + // Use introspection to get the available services. + msg = dbus->message_new_method_call(PORTAL_DESTINATION, PORTAL_PATH, "org.freedesktop.DBus.Introspectable", "Introspect"); + if (!msg) { + goto done; + } + + reply = dbus->connection_send_with_reply_and_block(dbus->session_conn, msg, DBUS_TIMEOUT_USE_DEFAULT, NULL); + dbus->message_unref(msg); + if (!reply) { + goto done; + } + + if (!dbus->message_iter_init(reply, &reply_iter)) { + goto done; + } + + if (dbus->message_iter_get_arg_type(&reply_iter) != DBUS_TYPE_STRING) { + goto done; + } + + /* Introspection gives us a dump of all the services on the destination in XML format, so search the + * giant string for the file chooser protocol. + */ + dbus->message_iter_get_basic(&reply_iter, &reply_str); + if (SDL_strstr(reply_str, PORTAL_INTERFACE)) { + portal_present = 1; // Found it! + } + +done: + if (reply) { + dbus->message_unref(reply); + } + + return (portal_present > 0); +} + +#else + +// Dummy implementation to avoid compilation problems + +void SDL_Portal_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props) +{ + SDL_Unsupported(); + callback(userdata, NULL, -1); +} + +bool SDL_Portal_detect(void) +{ + return false; +} + +#endif // SDL_USE_LIBDBUS diff --git a/contrib/SDL-3.2.8/src/dialog/unix/SDL_portaldialog.h b/contrib/SDL-3.2.8/src/dialog/unix/SDL_portaldialog.h new file mode 100644 index 0000000..4497287 --- /dev/null +++ b/contrib/SDL-3.2.8/src/dialog/unix/SDL_portaldialog.h @@ -0,0 +1,27 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 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" + +void SDL_Portal_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props); + +/** @returns non-zero if available, zero if unavailable */ +bool SDL_Portal_detect(void); diff --git a/contrib/SDL-3.2.8/src/dialog/unix/SDL_unixdialog.c b/contrib/SDL-3.2.8/src/dialog/unix/SDL_unixdialog.c new file mode 100644 index 0000000..bec2ac9 --- /dev/null +++ b/contrib/SDL-3.2.8/src/dialog/unix/SDL_unixdialog.c @@ -0,0 +1,81 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 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" + +#include "../SDL_dialog.h" +#include "./SDL_portaldialog.h" +#include "./SDL_zenitydialog.h" + +static void (*detected_function)(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props) = NULL; + +void SDLCALL hint_callback(void *userdata, const char *name, const char *oldValue, const char *newValue); + +static void set_callback(void) +{ + static bool is_set = false; + + if (is_set == false) { + is_set = true; + SDL_AddHintCallback(SDL_HINT_FILE_DIALOG_DRIVER, hint_callback, NULL); + } +} + +// Returns non-zero on success, 0 on failure +static int detect_available_methods(const char *value) +{ + const char *driver = value ? value : SDL_GetHint(SDL_HINT_FILE_DIALOG_DRIVER); + + set_callback(); + + if (driver == NULL || SDL_strcmp(driver, "portal") == 0) { + if (SDL_Portal_detect()) { + detected_function = SDL_Portal_ShowFileDialogWithProperties; + return 1; + } + } + + if (driver == NULL || SDL_strcmp(driver, "zenity") == 0) { + if (SDL_Zenity_detect()) { + detected_function = SDL_Zenity_ShowFileDialogWithProperties; + return 2; + } + } + + SDL_SetError("File dialog driver unsupported (supported values for SDL_HINT_FILE_DIALOG_DRIVER are 'zenity' and 'portal')"); + return 0; +} + +void SDLCALL hint_callback(void *userdata, const char *name, const char *oldValue, const char *newValue) +{ + detect_available_methods(newValue); +} + +void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props) +{ + // Call detect_available_methods() again each time in case the situation changed + if (!detected_function && !detect_available_methods(NULL)) { + // SetError() done by detect_available_methods() + callback(userdata, NULL, -1); + return; + } + + detected_function(type, callback, userdata, props); +} diff --git a/contrib/SDL-3.2.8/src/dialog/unix/SDL_zenitydialog.c b/contrib/SDL-3.2.8/src/dialog/unix/SDL_zenitydialog.c new file mode 100644 index 0000000..4632c8e --- /dev/null +++ b/contrib/SDL-3.2.8/src/dialog/unix/SDL_zenitydialog.c @@ -0,0 +1,366 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 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" + +#include "../SDL_dialog_utils.h" + +#define X11_HANDLE_MAX_WIDTH 28 +typedef struct +{ + SDL_DialogFileCallback callback; + void *userdata; + void *argv; + + /* Zenity only works with X11 handles apparently */ + char x11_window_handle[X11_HANDLE_MAX_WIDTH]; + /* These are part of argv, but are tracked separately for deallocation purposes */ + int nfilters; + char **filters_slice; + char *filename; + char *title; + char *accept; + char *cancel; +} zenityArgs; + +static char *zenity_clean_name(const char *name) +{ + char *newname = SDL_strdup(name); + + /* Filter out "|", which Zenity considers a special character. Let's hope + there aren't others. TODO: find something better. */ + for (char *c = newname; *c; c++) { + if (*c == '|') { + // Zenity doesn't support escaping with '\' + *c = '/'; + } + } + + return newname; +} + +static bool get_x11_window_handle(SDL_PropertiesID props, char *out) +{ + SDL_Window *window = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, NULL); + if (!window) { + return false; + } + SDL_PropertiesID window_props = SDL_GetWindowProperties(window); + if (!window_props) { + return false; + } + Uint64 handle = (Uint64)SDL_GetNumberProperty(window_props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0); + if (!handle) { + return false; + } + if (SDL_snprintf(out, X11_HANDLE_MAX_WIDTH, "0x%" SDL_PRIx64, handle) >= X11_HANDLE_MAX_WIDTH) { + return false; + }; + return true; +} + +/* Exec call format: + * + * zenity --file-selection --separator=\n [--multiple] + * [--directory] [--save --confirm-overwrite] + * [--filename FILENAME] [--modal --attach 0x11w1nd0w] + * [--title TITLE] [--ok-label ACCEPT] + * [--cancel-label CANCEL] + * [--file-filter=Filter Name | *.filt *.fn ...]... + */ +static zenityArgs *create_zenity_args(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props) +{ + zenityArgs *args = SDL_calloc(1, sizeof(*args)); + if (!args) { + return NULL; + } + args->callback = callback; + args->userdata = userdata; + args->nfilters = SDL_GetNumberProperty(props, SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, 0); + + const char **argv = SDL_malloc( + sizeof(*argv) * (3 /* zenity --file-selection --separator=\n */ + + 1 /* --multiple */ + + 2 /* --directory | --save --confirm-overwrite */ + + 2 /* --filename [file] */ + + 3 /* --modal --attach [handle] */ + + 2 /* --title [title] */ + + 2 /* --ok-label [label] */ + + 2 /* --cancel-label [label] */ + + args->nfilters + 1 /* NULL */)); + if (!argv) { + goto cleanup; + } + args->argv = argv; + + /* Properties can be destroyed as soon as the function returns; copy over what we need. */ +#define COPY_STRING_PROPERTY(dst, prop) \ + { \ + const char *str = SDL_GetStringProperty(props, prop, NULL); \ + if (str) { \ + dst = SDL_strdup(str); \ + if (!dst) { \ + goto cleanup; \ + } \ + } \ + } + + COPY_STRING_PROPERTY(args->filename, SDL_PROP_FILE_DIALOG_LOCATION_STRING); + COPY_STRING_PROPERTY(args->title, SDL_PROP_FILE_DIALOG_TITLE_STRING); + COPY_STRING_PROPERTY(args->accept, SDL_PROP_FILE_DIALOG_ACCEPT_STRING); + COPY_STRING_PROPERTY(args->cancel, SDL_PROP_FILE_DIALOG_CANCEL_STRING); +#undef COPY_STRING_PROPERTY + + // ARGV PASS + int argc = 0; + argv[argc++] = "zenity"; + argv[argc++] = "--file-selection"; + argv[argc++] = "--separator=\n"; + + if (SDL_GetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, false)) { + argv[argc++] = "--multiple"; + } + + switch (type) { + case SDL_FILEDIALOG_OPENFILE: + break; + + case SDL_FILEDIALOG_SAVEFILE: + argv[argc++] = "--save"; + /* Asking before overwriting while saving seems like a sane default */ + argv[argc++] = "--confirm-overwrite"; + break; + + case SDL_FILEDIALOG_OPENFOLDER: + argv[argc++] = "--directory"; + break; + }; + + if (args->filename) { + argv[argc++] = "--filename"; + argv[argc++] = args->filename; + } + + if (get_x11_window_handle(props, args->x11_window_handle)) { + argv[argc++] = "--modal"; + argv[argc++] = "--attach"; + argv[argc++] = args->x11_window_handle; + } + + if (args->title) { + argv[argc++] = "--title"; + argv[argc++] = args->title; + } + + if (args->accept) { + argv[argc++] = "--ok-label"; + argv[argc++] = args->accept; + } + + if (args->cancel) { + argv[argc++] = "--cancel-label"; + argv[argc++] = args->cancel; + } + + const SDL_DialogFileFilter *filters = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_FILTERS_POINTER, NULL); + if (filters) { + args->filters_slice = (char **)&argv[argc]; + for (int i = 0; i < args->nfilters; i++) { + char *filter_str = convert_filter(filters[i], + zenity_clean_name, + "--file-filter=", " | ", "", + "*.", " *.", ""); + + if (!filter_str) { + while (i--) { + SDL_free(args->filters_slice[i]); + } + goto cleanup; + } + + args->filters_slice[i] = filter_str; + } + argc += args->nfilters; + } + + argv[argc] = NULL; + return args; + +cleanup: + SDL_free(args->filename); + SDL_free(args->title); + SDL_free(args->accept); + SDL_free(args->cancel); + SDL_free(argv); + SDL_free(args); + return NULL; +} + +// TODO: Zenity survives termination of the parent + +static void run_zenity(SDL_DialogFileCallback callback, void *userdata, void *argv) +{ + SDL_Process *process = NULL; + SDL_Environment *env = NULL; + int status = -1; + size_t bytes_read = 0; + char *container = NULL; + size_t narray = 1; + char **array = NULL; + bool result = false; + + env = SDL_CreateEnvironment(true); + if (!env) { + goto done; + } + + /* Recent versions of Zenity have different exit codes, but picks up + different codes from the environment */ + SDL_SetEnvironmentVariable(env, "ZENITY_OK", "0", true); + SDL_SetEnvironmentVariable(env, "ZENITY_CANCEL", "1", true); + SDL_SetEnvironmentVariable(env, "ZENITY_ESC", "1", true); + SDL_SetEnvironmentVariable(env, "ZENITY_EXTRA", "2", true); + SDL_SetEnvironmentVariable(env, "ZENITY_ERROR", "2", true); + SDL_SetEnvironmentVariable(env, "ZENITY_TIMEOUT", "2", true); + + SDL_PropertiesID props = SDL_CreateProperties(); + SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, argv); + SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ENVIRONMENT_POINTER, env); + SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDIN_NUMBER, SDL_PROCESS_STDIO_NULL); + SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_APP); + SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER, SDL_PROCESS_STDIO_NULL); + process = SDL_CreateProcessWithProperties(props); + SDL_DestroyProperties(props); + if (!process) { + goto done; + } + + container = SDL_ReadProcess(process, &bytes_read, &status); + if (!container) { + goto done; + } + + array = (char **)SDL_malloc((narray + 1) * sizeof(char *)); + if (!array) { + goto done; + } + array[0] = container; + array[1] = NULL; + + for (int i = 0; i < bytes_read; i++) { + if (container[i] == '\n') { + container[i] = '\0'; + // Reading from a process often leaves a trailing \n, so ignore the last one + if (i < bytes_read - 1) { + array[narray] = container + i + 1; + narray++; + char **new_array = (char **)SDL_realloc(array, (narray + 1) * sizeof(char *)); + if (!new_array) { + goto done; + } + array = new_array; + array[narray] = NULL; + } + } + } + + // 0 = the user chose one or more files, 1 = the user canceled the dialog + if (status == 0 || status == 1) { + callback(userdata, (const char *const *)array, -1); + } else { + SDL_SetError("Could not run zenity: exit code %d", status); + callback(userdata, NULL, -1); + } + + result = true; + +done: + SDL_free(array); + SDL_free(container); + SDL_DestroyEnvironment(env); + SDL_DestroyProcess(process); + + if (!result) { + callback(userdata, NULL, -1); + } +} + +static void free_zenity_args(zenityArgs *args) +{ + if (args->filters_slice) { + for (int i = 0; i < args->nfilters; i++) { + SDL_free(args->filters_slice[i]); + } + } + SDL_free(args->filename); + SDL_free(args->title); + SDL_free(args->accept); + SDL_free(args->cancel); + SDL_free(args->argv); + SDL_free(args); +} + +static int run_zenity_thread(void *ptr) +{ + zenityArgs *args = ptr; + run_zenity(args->callback, args->userdata, args->argv); + free_zenity_args(args); + return 0; +} + +void SDL_Zenity_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props) +{ + zenityArgs *args = create_zenity_args(type, callback, userdata, props); + if (!args) { + callback(userdata, NULL, -1); + return; + } + + SDL_Thread *thread = SDL_CreateThread(run_zenity_thread, "SDL_ZenityFileDialog", (void *)args); + + if (!thread) { + free_zenity_args(args); + callback(userdata, NULL, -1); + return; + } + + SDL_DetachThread(thread); +} + +bool SDL_Zenity_detect(void) +{ + const char *args[] = { + "zenity", "--version", NULL + }; + int status = -1; + + SDL_PropertiesID props = SDL_CreateProperties(); + SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, args); + SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDIN_NUMBER, SDL_PROCESS_STDIO_NULL); + SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_NULL); + SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER, SDL_PROCESS_STDIO_NULL); + SDL_Process *process = SDL_CreateProcessWithProperties(props); + SDL_DestroyProperties(props); + if (process) { + SDL_WaitProcess(process, true, &status); + SDL_DestroyProcess(process); + } + return (status == 0); +} diff --git a/contrib/SDL-3.2.8/src/dialog/unix/SDL_zenitydialog.h b/contrib/SDL-3.2.8/src/dialog/unix/SDL_zenitydialog.h new file mode 100644 index 0000000..4cfe892 --- /dev/null +++ b/contrib/SDL-3.2.8/src/dialog/unix/SDL_zenitydialog.h @@ -0,0 +1,27 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 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" + +extern void SDL_Zenity_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props); + +/** @returns non-zero if available, zero if unavailable */ +extern bool SDL_Zenity_detect(void); -- cgit v1.2.3