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 --- contrib/SDL-3.2.8/src/dialog/SDL_dialog.c | 131 +++++ contrib/SDL-3.2.8/src/dialog/SDL_dialog.h | 22 + contrib/SDL-3.2.8/src/dialog/SDL_dialog_utils.c | 256 +++++++++ contrib/SDL-3.2.8/src/dialog/SDL_dialog_utils.h | 59 ++ .../src/dialog/android/SDL_androiddialog.c | 58 ++ .../SDL-3.2.8/src/dialog/cocoa/SDL_cocoadialog.m | 188 +++++++ .../SDL-3.2.8/src/dialog/dummy/SDL_dummydialog.c | 33 ++ .../SDL-3.2.8/src/dialog/haiku/SDL_haikudialog.cc | 293 ++++++++++ .../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 + .../src/dialog/windows/SDL_windowsdialog.c | 611 +++++++++++++++++++++ 14 files changed, 2697 insertions(+) create mode 100644 contrib/SDL-3.2.8/src/dialog/SDL_dialog.c create mode 100644 contrib/SDL-3.2.8/src/dialog/SDL_dialog.h create mode 100644 contrib/SDL-3.2.8/src/dialog/SDL_dialog_utils.c create mode 100644 contrib/SDL-3.2.8/src/dialog/SDL_dialog_utils.h create mode 100644 contrib/SDL-3.2.8/src/dialog/android/SDL_androiddialog.c create mode 100644 contrib/SDL-3.2.8/src/dialog/cocoa/SDL_cocoadialog.m create mode 100644 contrib/SDL-3.2.8/src/dialog/dummy/SDL_dummydialog.c create mode 100644 contrib/SDL-3.2.8/src/dialog/haiku/SDL_haikudialog.cc 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 create mode 100644 contrib/SDL-3.2.8/src/dialog/windows/SDL_windowsdialog.c (limited to 'contrib/SDL-3.2.8/src/dialog') diff --git a/contrib/SDL-3.2.8/src/dialog/SDL_dialog.c b/contrib/SDL-3.2.8/src/dialog/SDL_dialog.c new file mode 100644 index 0000000..a77e443 --- /dev/null +++ b/contrib/SDL-3.2.8/src/dialog/SDL_dialog.c @@ -0,0 +1,131 @@ +/* + 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_dialog_utils.h" + +void SDL_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props) +{ + if (!callback) { + return; + } +#ifdef SDL_DIALOG_DISABLED + SDL_SetError("SDL not built with dialog support"); + callback(userdata, NULL, -1); +#else + 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, -1); + + if (filters && nfilters == -1) { + SDL_SetError("Set filter pointers, but didn't set number of filters (SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER)"); + callback(userdata, NULL, -1); + return; + } + + const char *msg = validate_filters(filters, nfilters); + + if (msg) { + SDL_SetError("Invalid dialog file filters: %s", msg); + callback(userdata, NULL, -1); + return; + } + + switch (type) { + case SDL_FILEDIALOG_OPENFILE: + case SDL_FILEDIALOG_SAVEFILE: + case SDL_FILEDIALOG_OPENFOLDER: + SDL_SYS_ShowFileDialogWithProperties(type, callback, userdata, props); + break; + + default: + SDL_SetError("Unsupported file dialog type: %d", (int) type); + callback(userdata, NULL, -1); + break; + }; +#endif +} + +void SDL_ShowOpenFileDialog(SDL_DialogFileCallback callback, void *userdata, SDL_Window *window, const SDL_DialogFileFilter *filters, int nfilters, const char *default_location, bool allow_many) +{ +#ifdef SDL_DIALOG_DISABLED + if (!callback) { + return; + } + SDL_SetError("SDL not built with dialog support"); + callback(userdata, NULL, -1); +#else + SDL_PropertiesID props = SDL_CreateProperties(); + + SDL_SetPointerProperty(props, SDL_PROP_FILE_DIALOG_FILTERS_POINTER, (void *) filters); + SDL_SetNumberProperty(props, SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, nfilters); + SDL_SetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, window); + SDL_SetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, default_location); + SDL_SetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, allow_many); + + SDL_ShowFileDialogWithProperties(SDL_FILEDIALOG_OPENFILE, callback, userdata, props); + + SDL_DestroyProperties(props); +#endif +} + +void SDL_ShowSaveFileDialog(SDL_DialogFileCallback callback, void *userdata, SDL_Window *window, const SDL_DialogFileFilter *filters, int nfilters, const char *default_location) +{ +#ifdef SDL_DIALOG_DISABLED + if (!callback) { + return; + } + SDL_SetError("SDL not built with dialog support"); + callback(userdata, NULL, -1); +#else + SDL_PropertiesID props = SDL_CreateProperties(); + + SDL_SetPointerProperty(props, SDL_PROP_FILE_DIALOG_FILTERS_POINTER, (void *) filters); + SDL_SetNumberProperty(props, SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, nfilters); + SDL_SetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, window); + SDL_SetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, default_location); + + SDL_ShowFileDialogWithProperties(SDL_FILEDIALOG_SAVEFILE, callback, userdata, props); + + SDL_DestroyProperties(props); +#endif +} + +void SDL_ShowOpenFolderDialog(SDL_DialogFileCallback callback, void *userdata, SDL_Window *window, const char *default_location, bool allow_many) +{ +#ifdef SDL_DIALOG_DISABLED + if (!callback) { + return; + } + SDL_SetError("SDL not built with dialog support"); + callback(userdata, NULL, -1); +#else + SDL_PropertiesID props = SDL_CreateProperties(); + + SDL_SetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, window); + SDL_SetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, default_location); + SDL_SetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, allow_many); + + SDL_ShowFileDialogWithProperties(SDL_FILEDIALOG_OPENFOLDER, callback, userdata, props); + + SDL_DestroyProperties(props); +#endif +} diff --git a/contrib/SDL-3.2.8/src/dialog/SDL_dialog.h b/contrib/SDL-3.2.8/src/dialog/SDL_dialog.h new file mode 100644 index 0000000..beee7dd --- /dev/null +++ b/contrib/SDL-3.2.8/src/dialog/SDL_dialog.h @@ -0,0 +1,22 @@ +/* + 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. +*/ + +void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props); diff --git a/contrib/SDL-3.2.8/src/dialog/SDL_dialog_utils.c b/contrib/SDL-3.2.8/src/dialog/SDL_dialog_utils.c new file mode 100644 index 0000000..8d2b186 --- /dev/null +++ b/contrib/SDL-3.2.8/src/dialog/SDL_dialog_utils.c @@ -0,0 +1,256 @@ +/* + 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" + +char *convert_filters(const SDL_DialogFileFilter *filters, int nfilters, + NameTransform ntf, const char *prefix, + const char *separator, const char *suffix, + const char *filt_prefix, const char *filt_separator, + const char *filt_suffix, const char *ext_prefix, + const char *ext_separator, const char *ext_suffix) +{ + char *combined; + char *new_combined; + char *converted; + const char *terminator; + size_t new_length; + int i; + + if (!filters) { + SDL_SetError("Called convert_filters() with NULL filters (SDL bug)"); + return NULL; + } + + combined = SDL_strdup(prefix); + + if (!combined) { + return NULL; + } + + for (i = 0; i < nfilters; i++) { + const SDL_DialogFileFilter *f = &filters[i]; + + converted = convert_filter(*f, ntf, filt_prefix, filt_separator, + filt_suffix, ext_prefix, ext_separator, + ext_suffix); + + if (!converted) { + SDL_free(combined); + return NULL; + } + + terminator = ((i + 1) < nfilters) ? separator : suffix; + new_length = SDL_strlen(combined) + SDL_strlen(converted) + + SDL_strlen(terminator) + 1; + + new_combined = (char *)SDL_realloc(combined, new_length); + + if (!new_combined) { + SDL_free(converted); + SDL_free(combined); + return NULL; + } + + combined = new_combined; + + SDL_strlcat(combined, converted, new_length); + SDL_strlcat(combined, terminator, new_length); + SDL_free(converted); + } + + new_length = SDL_strlen(combined) + SDL_strlen(suffix) + 1; + + new_combined = (char *)SDL_realloc(combined, new_length); + + if (!new_combined) { + SDL_free(combined); + return NULL; + } + + combined = new_combined; + + SDL_strlcat(combined, suffix, new_length); + + return combined; +} + +char *convert_filter(SDL_DialogFileFilter filter, NameTransform ntf, + const char *prefix, const char *separator, + const char *suffix, const char *ext_prefix, + const char *ext_separator, const char *ext_suffix) +{ + char *converted; + char *name_filtered; + size_t total_length; + char *list; + + list = convert_ext_list(filter.pattern, ext_prefix, ext_separator, + ext_suffix); + + if (!list) { + return NULL; + } + + if (ntf) { + name_filtered = ntf(filter.name); + } else { + // Useless strdup, but easier to read and maintain code this way + name_filtered = SDL_strdup(filter.name); + } + + if (!name_filtered) { + SDL_free(list); + return NULL; + } + + total_length = SDL_strlen(prefix) + SDL_strlen(name_filtered) + + SDL_strlen(separator) + SDL_strlen(list) + + SDL_strlen(suffix) + 1; + + converted = (char *) SDL_malloc(total_length); + + if (!converted) { + SDL_free(list); + SDL_free(name_filtered); + return NULL; + } + + SDL_snprintf(converted, total_length, "%s%s%s%s%s", prefix, name_filtered, + separator, list, suffix); + + SDL_free(list); + SDL_free(name_filtered); + + return converted; +} + +char *convert_ext_list(const char *list, const char *prefix, + const char *separator, const char *suffix) +{ + char *converted; + int semicolons; + size_t total_length; + + semicolons = 0; + + for (const char *c = list; *c; c++) { + semicolons += (*c == ';'); + } + + total_length = + SDL_strlen(list) - semicolons // length of list contents + + semicolons * SDL_strlen(separator) // length of separators + + SDL_strlen(prefix) + SDL_strlen(suffix) // length of prefix/suffix + + 1; // terminating null byte + + converted = (char *) SDL_malloc(total_length); + + if (!converted) { + return NULL; + } + + *converted = '\0'; + + SDL_strlcat(converted, prefix, total_length); + + /* Some platforms may prefer to handle the asterisk manually, but this + function offers to handle it for ease of use. */ + if (SDL_strcmp(list, "*") == 0) { + SDL_strlcat(converted, "*", total_length); + } else { + for (const char *c = list; *c; c++) { + if ((*c >= 'a' && *c <= 'z') || (*c >= 'A' && *c <= 'Z') + || (*c >= '0' && *c <= '9') || *c == '-' || *c == '_' + || *c == '.') { + char str[2]; + str[0] = *c; + str[1] = '\0'; + SDL_strlcat(converted, str, total_length); + } else if (*c == ';') { + if (c == list || c[-1] == ';') { + SDL_SetError("Empty pattern not allowed"); + SDL_free(converted); + return NULL; + } + + SDL_strlcat(converted, separator, total_length); + } else { + SDL_SetError("Invalid character '%c' in pattern (Only [a-zA-Z0-9_.-] allowed, or a single *)", *c); + SDL_free(converted); + return NULL; + } + } + } + + if (list[SDL_strlen(list) - 1] == ';') { + SDL_SetError("Empty pattern not allowed"); + SDL_free(converted); + return NULL; + } + + SDL_strlcat(converted, suffix, total_length); + + return converted; +} + +const char *validate_filters(const SDL_DialogFileFilter *filters, int nfilters) +{ + if (filters) { + for (int i = 0; i < nfilters; i++) { + const char *msg = validate_list(filters[i].pattern); + + if (msg) { + return msg; + } + } + } + + return NULL; +} + +const char *validate_list(const char *list) +{ + if (SDL_strcmp(list, "*") == 0) { + return NULL; + } else { + for (const char *c = list; *c; c++) { + if ((*c >= 'a' && *c <= 'z') || (*c >= 'A' && *c <= 'Z') + || (*c >= '0' && *c <= '9') || *c == '-' || *c == '_' + || *c == '.') { + continue; + } else if (*c == ';') { + if (c == list || c[-1] == ';') { + return "Empty pattern not allowed"; + } + } else { + return "Invalid character in pattern (Only [a-zA-Z0-9_.-] allowed, or a single *)"; + } + } + } + + if (list[SDL_strlen(list) - 1] == ';') { + return "Empty pattern not allowed"; + } + + return NULL; +} diff --git a/contrib/SDL-3.2.8/src/dialog/SDL_dialog_utils.h b/contrib/SDL-3.2.8/src/dialog/SDL_dialog_utils.h new file mode 100644 index 0000000..1343dd7 --- /dev/null +++ b/contrib/SDL-3.2.8/src/dialog/SDL_dialog_utils.h @@ -0,0 +1,59 @@ +/* + 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" + +/* The following are utility functions to help implementations. + They are ordered by scope largeness, decreasing. All implementations + should use them, as they check for invalid filters. Where they are unused, + the validate_* function further down below should be used. */ + +/* Transform the name given in argument into something viable for the engine. + Useful if there are special characters to avoid on certain platforms (such + as "|" with Zenity). */ +typedef char *(NameTransform)(const char * name); + +// Converts all the filters into a single string. +// [filter]{[filter]...} +char *convert_filters(const SDL_DialogFileFilter *filters, int nfilters, + NameTransform ntf, const char *prefix, + const char *separator, const char *suffix, + const char *filt_prefix, const char *filt_separator, + const char *filt_suffix, const char *ext_prefix, + const char *ext_separator, const char *ext_suffix); + +// Converts one filter into a single string. +// [filter name][filter extension list] +char *convert_filter(SDL_DialogFileFilter filter, NameTransform ntf, + const char *prefix, const char *separator, + const char *suffix, const char *ext_prefix, + const char *ext_separator, const char *ext_suffix); + +// Converts the extension list of a filter into a single string. +// [extension]{[extension]...} +char *convert_ext_list(const char *list, const char *prefix, + const char *separator, const char *suffix); + +/* Must be used if convert_* functions aren't used */ +// Returns an error message if there's a problem, NULL otherwise +const char *validate_filters(const SDL_DialogFileFilter *filters, + int nfilters); + +const char *validate_list(const char *list); diff --git a/contrib/SDL-3.2.8/src/dialog/android/SDL_androiddialog.c b/contrib/SDL-3.2.8/src/dialog/android/SDL_androiddialog.c new file mode 100644 index 0000000..5eaf945 --- /dev/null +++ b/contrib/SDL-3.2.8/src/dialog/android/SDL_androiddialog.c @@ -0,0 +1,58 @@ +/* + 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 "../../core/android/SDL_android.h" + +void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props) +{ + 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); + bool is_save; + + if (SDL_GetHint(SDL_HINT_FILE_DIALOG_DRIVER) != NULL) { + SDL_SetError("File dialog driver unsupported (don't set SDL_HINT_FILE_DIALOG_DRIVER)"); + callback(userdata, NULL, -1); + return; + } + + switch (type) { + case SDL_FILEDIALOG_OPENFILE: + is_save = false; + break; + + case SDL_FILEDIALOG_SAVEFILE: + is_save = true; + break; + + case SDL_FILEDIALOG_OPENFOLDER: + SDL_Unsupported(); + callback(userdata, NULL, -1); + return; + }; + + if (!Android_JNI_OpenFileDialog(callback, userdata, filters, nfilters, is_save, allow_many)) { + // SDL_SetError is already called when it fails + callback(userdata, NULL, -1); + } +} diff --git a/contrib/SDL-3.2.8/src/dialog/cocoa/SDL_cocoadialog.m b/contrib/SDL-3.2.8/src/dialog/cocoa/SDL_cocoadialog.m new file mode 100644 index 0000000..fb9c5ad --- /dev/null +++ b/contrib/SDL-3.2.8/src/dialog/cocoa/SDL_cocoadialog.m @@ -0,0 +1,188 @@ +/* + 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_dialog_utils.h" + +#ifdef SDL_PLATFORM_MACOS + +#import +#import + +void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props) +{ + 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* title = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_TITLE_STRING, NULL); + const char* accept = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_ACCEPT_STRING, NULL); + + if (filters) { + const char *msg = validate_filters(filters, nfilters); + + if (msg) { + SDL_SetError("%s", msg); + callback(userdata, NULL, -1); + return; + } + } + + if (SDL_GetHint(SDL_HINT_FILE_DIALOG_DRIVER) != NULL) { + SDL_SetError("File dialog driver unsupported (don't set SDL_HINT_FILE_DIALOG_DRIVER)"); + callback(userdata, NULL, -1); + return; + } + + // NSOpenPanel inherits from NSSavePanel + NSSavePanel *dialog; + NSOpenPanel *dialog_as_open; + + switch (type) { + case SDL_FILEDIALOG_SAVEFILE: + dialog = [NSSavePanel savePanel]; + break; + + case SDL_FILEDIALOG_OPENFILE: + dialog_as_open = [NSOpenPanel openPanel]; + [dialog_as_open setAllowsMultipleSelection:((allow_many == true) ? YES : NO)]; + dialog = dialog_as_open; + break; + + case SDL_FILEDIALOG_OPENFOLDER: + dialog_as_open = [NSOpenPanel openPanel]; + [dialog_as_open setCanChooseFiles:NO]; + [dialog_as_open setCanChooseDirectories:YES]; + [dialog_as_open setAllowsMultipleSelection:((allow_many == true) ? YES : NO)]; + dialog = dialog_as_open; + break; + }; + + if (title) { + [dialog setTitle:[NSString stringWithUTF8String:title]]; + } + + if (accept) { + [dialog setPrompt:[NSString stringWithUTF8String:accept]]; + } + + if (filters) { + // On macOS 11.0 and up, this is an array of UTType. Prior to that, it's an array of NSString + NSMutableArray *types = [[NSMutableArray alloc] initWithCapacity:nfilters ]; + + int has_all_files = 0; + for (int i = 0; i < nfilters; i++) { + char *pattern = SDL_strdup(filters[i].pattern); + char *pattern_ptr = pattern; + + if (!pattern_ptr) { + callback(userdata, NULL, -1); + return; + } + + for (char *c = pattern; *c; c++) { + if (*c == ';') { + *c = '\0'; + if(@available(macOS 11.0, *)) { + [types addObject: [UTType typeWithFilenameExtension:[NSString stringWithFormat: @"%s", pattern_ptr]]]; + } else { + [types addObject: [NSString stringWithFormat: @"%s", pattern_ptr]]; + } + pattern_ptr = c + 1; + } else if (*c == '*') { + has_all_files = 1; + } + } + if(@available(macOS 11.0, *)) { + [types addObject: [UTType typeWithFilenameExtension:[NSString stringWithFormat: @"%s", pattern_ptr]]]; + } else { + [types addObject: [NSString stringWithFormat: @"%s", pattern_ptr]]; + } + + SDL_free(pattern); + } + + if (!has_all_files) { + if (@available(macOS 11.0, *)) { + [dialog setAllowedContentTypes:types]; + } else { + [dialog setAllowedFileTypes:types]; + } + } + } + + // Keep behavior consistent with other platforms + [dialog setAllowsOtherFileTypes:YES]; + + if (default_location) { + [dialog setDirectoryURL:[NSURL fileURLWithPath:[NSString stringWithUTF8String:default_location]]]; + } + + NSWindow *w = NULL; + + if (window) { + w = (__bridge NSWindow *)SDL_GetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, NULL); + } + + if (w) { + // [dialog beginWithCompletionHandler:^(NSInteger result) { + [dialog beginSheetModalForWindow:w completionHandler:^(NSInteger result) { + if (result == NSModalResponseOK) { + if (dialog_as_open) { + NSArray* urls = [dialog_as_open URLs]; + const char *files[[urls count] + 1]; + for (int i = 0; i < [urls count]; i++) { + files[i] = [[[urls objectAtIndex:i] path] UTF8String]; + } + files[[urls count]] = NULL; + callback(userdata, files, -1); + } else { + const char *files[2] = { [[[dialog URL] path] UTF8String], NULL }; + callback(userdata, files, -1); + } + } else if (result == NSModalResponseCancel) { + const char *files[1] = { NULL }; + callback(userdata, files, -1); + } + }]; + } else { + if ([dialog runModal] == NSModalResponseOK) { + if (dialog_as_open) { + NSArray* urls = [dialog_as_open URLs]; + const char *files[[urls count] + 1]; + for (int i = 0; i < [urls count]; i++) { + files[i] = [[[urls objectAtIndex:i] path] UTF8String]; + } + files[[urls count]] = NULL; + callback(userdata, files, -1); + } else { + const char *files[2] = { [[[dialog URL] path] UTF8String], NULL }; + callback(userdata, files, -1); + } + } else { + const char *files[1] = { NULL }; + callback(userdata, files, -1); + } + } +} + +#endif // SDL_PLATFORM_MACOS diff --git a/contrib/SDL-3.2.8/src/dialog/dummy/SDL_dummydialog.c b/contrib/SDL-3.2.8/src/dialog/dummy/SDL_dummydialog.c new file mode 100644 index 0000000..121a090 --- /dev/null +++ b/contrib/SDL-3.2.8/src/dialog/dummy/SDL_dummydialog.c @@ -0,0 +1,33 @@ +/* + 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" + +#ifdef SDL_DIALOG_DUMMY + +void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props) +{ + SDL_Unsupported(); + callback(userdata, NULL, -1); +} + +#endif // SDL_DIALOG_DUMMY diff --git a/contrib/SDL-3.2.8/src/dialog/haiku/SDL_haikudialog.cc b/contrib/SDL-3.2.8/src/dialog/haiku/SDL_haikudialog.cc new file mode 100644 index 0000000..d60e343 --- /dev/null +++ b/contrib/SDL-3.2.8/src/dialog/haiku/SDL_haikudialog.cc @@ -0,0 +1,293 @@ +/* + 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 "C" { +#include "../SDL_dialog.h" +#include "../SDL_dialog_utils.h" +} +#include "../../core/haiku/SDL_BeApp.h" +#include "../../video/haiku/SDL_BWin.h" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +bool StringEndsWith(const std::string& str, const std::string& end) +{ + return str.size() >= end.size() && !str.compare(str.size() - end.size(), end.size(), end); +} + +std::vector StringSplit(const std::string& str, const std::string& split) +{ + std::vector result; + std::string s = str; + size_t pos = 0; + + while ((pos = s.find(split)) != std::string::npos) { + result.push_back(s.substr(0, pos)); + s = s.substr(pos + split.size()); + } + + result.push_back(s); + + return result; +} + +class SDLBRefFilter : public BRefFilter +{ +public: + SDLBRefFilter(const SDL_DialogFileFilter *filters, int nfilters) : + BRefFilter(), + m_filters(filters), + m_nfilters(nfilters) + { + } + + virtual bool Filter(const entry_ref *ref, BNode *node, struct stat_beos *stat, const char *mimeType) override + { + BEntry entry(ref); + BPath path; + entry.GetPath(&path); + std::string result = path.Path(); + + if (!m_filters) + return true; + + struct stat info; + node->GetStat(&info); + if (S_ISDIR(info.st_mode)) + return true; + + for (int i = 0; i < m_nfilters; i++) { + for (const auto& suffix : StringSplit(m_filters[i].pattern, ";")) { + if (StringEndsWith(result, std::string(".") + suffix)) { + return true; + } + } + } + + return false; + } + +private: + const SDL_DialogFileFilter * const m_filters; + int m_nfilters; +}; + +class CallbackLooper : public BLooper +{ +public: + CallbackLooper(SDL_DialogFileCallback callback, void *userdata) : + m_callback(callback), + m_userdata(userdata), + m_files(), + m_messenger(), + m_panel(), + m_filter() + { + } + + ~CallbackLooper() + { + delete m_messenger; + delete m_panel; + delete m_filter; + } + + void SetToBeFreed(BMessenger *messenger, BFilePanel *panel, SDLBRefFilter *filter) + { + m_messenger = messenger; + m_panel = panel; + m_filter = filter; + } + + virtual void MessageReceived(BMessage *msg) override + { + entry_ref file; + BPath path; + BEntry entry; + std::string result; + const char *filename; + int32 nFiles = 0; + + switch (msg->what) + { + case B_REFS_RECEIVED: // Open + msg->GetInfo("refs", NULL, &nFiles); + for (int i = 0; i < nFiles; i++) { + msg->FindRef("refs", i, &file); + entry.SetTo(&file); + entry.GetPath(&path); + result = path.Path(); + m_files.push_back(result); + } + break; + + case B_SAVE_REQUESTED: // Save + msg->FindRef("directory", &file); + entry.SetTo(&file); + entry.GetPath(&path); + result = path.Path(); + result += "/"; + msg->FindString("name", &filename); + result += filename; + m_files.push_back(result); + break; + + case B_CANCEL: // Whenever the dialog is closed (Cancel but also after Open and Save) + { + nFiles = m_files.size(); + const char* files[nFiles + 1]; + for (int i = 0; i < nFiles; i++) { + files[i] = m_files[i].c_str(); + } + files[nFiles] = NULL; + m_callback(m_userdata, files, -1); + Quit(); + SDL_QuitBeApp(); + delete this; + } + break; + + default: + BHandler::MessageReceived(msg); + break; + } + } + +private: + SDL_DialogFileCallback m_callback; + void *m_userdata; + std::vector m_files; + + // Only to free stuff later + BMessenger *m_messenger; + BFilePanel *m_panel; + SDLBRefFilter *m_filter; +}; + +void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props) +{ + SDL_Window* window = (SDL_Window*) SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, NULL); + SDL_DialogFileFilter* filters = (SDL_DialogFileFilter*) SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_FILTERS_POINTER, NULL); + int nfilters = (int) SDL_GetNumberProperty(props, SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, 0); + bool many = SDL_GetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, false); + const char* location = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, NULL); + const char* title = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_TITLE_STRING, NULL); + const char* accept = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_ACCEPT_STRING, NULL); + const char* cancel = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_CANCEL_STRING, NULL); + + bool modal = !!window; + + bool save = false; + bool folder = false; + + switch (type) { + case SDL_FILEDIALOG_SAVEFILE: + save = true; + break; + + case SDL_FILEDIALOG_OPENFILE: + break; + + case SDL_FILEDIALOG_OPENFOLDER: + folder = true; + break; + }; + + if (!SDL_InitBeApp()) { + char* err = SDL_strdup(SDL_GetError()); + SDL_SetError("Couldn't init Be app: %s", err); + SDL_free(err); + callback(userdata, NULL, -1); + return; + } + + if (filters) { + const char *msg = validate_filters(filters, nfilters); + + if (msg) { + SDL_SetError("%s", msg); + callback(userdata, NULL, -1); + return; + } + } + + if (SDL_GetHint(SDL_HINT_FILE_DIALOG_DRIVER) != NULL) { + SDL_SetError("File dialog driver unsupported"); + callback(userdata, NULL, -1); + return; + } + + // No unique_ptr's because they need to survive the end of the function + CallbackLooper *looper = new(std::nothrow) CallbackLooper(callback, userdata); + BMessenger *messenger = new(std::nothrow) BMessenger(NULL, looper); + SDLBRefFilter *filter = new(std::nothrow) SDLBRefFilter(filters, nfilters); + + if (looper == NULL || messenger == NULL || filter == NULL) { + SDL_free(looper); + SDL_free(messenger); + SDL_free(filter); + SDL_OutOfMemory(); + callback(userdata, NULL, -1); + return; + } + + BEntry entry; + entry_ref entryref; + if (location) { + entry.SetTo(location); + entry.GetRef(&entryref); + } + + BFilePanel *panel = new BFilePanel(save ? B_SAVE_PANEL : B_OPEN_PANEL, messenger, location ? &entryref : NULL, folder ? B_DIRECTORY_NODE : B_FILE_NODE, many, NULL, filter, modal); + + if (title) { + panel->Window()->SetTitle(title); + } + + if (accept) { + panel->SetButtonLabel(B_DEFAULT_BUTTON, accept); + } + + if (cancel) { + panel->SetButtonLabel(B_CANCEL_BUTTON, cancel); + } + + if (window) { + SDL_BWin *bwin = (SDL_BWin *)(window->internal); + panel->Window()->SetLook(B_MODAL_WINDOW_LOOK); + panel->Window()->SetFeel(B_MODAL_SUBSET_WINDOW_FEEL); + panel->Window()->AddToSubset(bwin); + } + + looper->SetToBeFreed(messenger, panel, filter); + looper->Run(); + panel->Show(); +} 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); diff --git a/contrib/SDL-3.2.8/src/dialog/windows/SDL_windowsdialog.c b/contrib/SDL-3.2.8/src/dialog/windows/SDL_windowsdialog.c new file mode 100644 index 0000000..2de224f --- /dev/null +++ b/contrib/SDL-3.2.8/src/dialog/windows/SDL_windowsdialog.c @@ -0,0 +1,611 @@ +/* + 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_dialog_utils.h" + +#include +#include +#include +#include "../../core/windows/SDL_windows.h" +#include "../../thread/SDL_systhread.h" + +// If this number is too small, selecting too many files will give an error +#define SELECTLIST_SIZE 65536 + +typedef struct +{ + bool is_save; + wchar_t *filters_str; + char *default_file; + SDL_Window *parent; + DWORD flags; + SDL_DialogFileCallback callback; + void *userdata; + char *title; + char *accept; + char *cancel; +} winArgs; + +typedef struct +{ + SDL_Window *parent; + SDL_DialogFileCallback callback; + char *default_folder; + void *userdata; + char *title; + char *accept; + char *cancel; +} winFArgs; + +void freeWinArgs(winArgs *args) +{ + SDL_free(args->default_file); + SDL_free(args->filters_str); + SDL_free(args->title); + SDL_free(args->accept); + SDL_free(args->cancel); + + SDL_free(args); +} + +void freeWinFArgs(winFArgs *args) +{ + SDL_free(args->default_folder); + SDL_free(args->title); + SDL_free(args->accept); + SDL_free(args->cancel); + + SDL_free(args); +} + +/** Converts dialog.nFilterIndex to SDL-compatible value */ +int getFilterIndex(int as_reported_by_windows) +{ + return as_reported_by_windows - 1; +} + +char *clear_filt_names(const char *filt) +{ + char *cleared = SDL_strdup(filt); + + for (char *c = cleared; *c; c++) { + /* 0x01 bytes are used as temporary replacement for the various 0x00 + bytes required by Win32 (one null byte between each filter, two at + the end of the filters). Filter out these bytes from the filter names + to avoid early-ending the filters if someone puts two consecutive + 0x01 bytes in their filter names. */ + if (*c == '\x01') { + *c = ' '; + } + } + + return cleared; +} + +// TODO: The new version of file dialogs +void windows_ShowFileDialog(void *ptr) +{ + winArgs *args = (winArgs *) ptr; + bool is_save = args->is_save; + const char *default_file = args->default_file; + SDL_Window *parent = args->parent; + DWORD flags = args->flags; + SDL_DialogFileCallback callback = args->callback; + void *userdata = args->userdata; + const char *title = args->title; + wchar_t *filter_wchar = args->filters_str; + + /* GetOpenFileName and GetSaveFileName have the same signature + (yes, LPOPENFILENAMEW even for the save dialog) */ + typedef BOOL (WINAPI *pfnGetAnyFileNameW)(LPOPENFILENAMEW); + typedef DWORD (WINAPI *pfnCommDlgExtendedError)(void); + HMODULE lib = LoadLibraryW(L"Comdlg32.dll"); + pfnGetAnyFileNameW pGetAnyFileName = NULL; + pfnCommDlgExtendedError pCommDlgExtendedError = NULL; + + if (lib) { + pGetAnyFileName = (pfnGetAnyFileNameW) GetProcAddress(lib, is_save ? "GetSaveFileNameW" : "GetOpenFileNameW"); + pCommDlgExtendedError = (pfnCommDlgExtendedError) GetProcAddress(lib, "CommDlgExtendedError"); + } else { + SDL_SetError("Couldn't load Comdlg32.dll"); + callback(userdata, NULL, -1); + return; + } + + if (!pGetAnyFileName) { + SDL_SetError("Couldn't load GetOpenFileName/GetSaveFileName from library"); + callback(userdata, NULL, -1); + return; + } + + if (!pCommDlgExtendedError) { + SDL_SetError("Couldn't load CommDlgExtendedError from library"); + callback(userdata, NULL, -1); + return; + } + + HWND window = NULL; + + if (parent) { + window = (HWND) SDL_GetPointerProperty(SDL_GetWindowProperties(parent), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); + } + + wchar_t *filebuffer; // lpstrFile + wchar_t initfolder[MAX_PATH] = L""; // lpstrInitialDir + + /* If SELECTLIST_SIZE is too large, putting filebuffer on the stack might + cause an overflow */ + filebuffer = (wchar_t *) SDL_malloc(SELECTLIST_SIZE * sizeof(wchar_t)); + + // Necessary for the return code below + SDL_memset(filebuffer, 0, SELECTLIST_SIZE * sizeof(wchar_t)); + + if (default_file) { + /* On Windows 10, 11 and possibly others, lpstrFile can be initialized + with a path and the dialog will start at that location, but *only if + the path contains a filename*. If it ends with a folder (directory + separator), it fails with 0x3002 (12290) FNERR_INVALIDFILENAME. For + that specific case, lpstrInitialDir must be used instead, but just + for that case, because lpstrInitialDir doesn't support file names. + + On top of that, lpstrInitialDir hides a special algorithm that + decides which folder to actually use as starting point, which may or + may not be the one provided, or some other unrelated folder. Also, + the algorithm changes between platforms. Assuming the documentation + is correct, the algorithm is there under 'lpstrInitialDir': + + https://learn.microsoft.com/en-us/windows/win32/api/commdlg/ns-commdlg-openfilenamew + + Finally, lpstrFile does not support forward slashes. lpstrInitialDir + does, though. */ + + char last_c = default_file[SDL_strlen(default_file) - 1]; + + if (last_c == '\\' || last_c == '/') { + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, default_file, -1, initfolder, MAX_PATH); + } else { + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, default_file, -1, filebuffer, MAX_PATH); + + for (int i = 0; i < SELECTLIST_SIZE; i++) { + if (filebuffer[i] == L'/') { + filebuffer[i] = L'\\'; + } + } + } + } + + wchar_t *title_w = NULL; + + if (title) { + title_w = WIN_UTF8ToStringW(title); + if (!title_w) { + SDL_free(filebuffer); + callback(userdata, NULL, -1); + return; + } + } + + OPENFILENAMEW dialog; + dialog.lStructSize = sizeof(OPENFILENAME); + dialog.hwndOwner = window; + dialog.hInstance = 0; + dialog.lpstrFilter = filter_wchar; + dialog.lpstrCustomFilter = NULL; + dialog.nMaxCustFilter = 0; + dialog.nFilterIndex = 0; + dialog.lpstrFile = filebuffer; + dialog.nMaxFile = SELECTLIST_SIZE; + dialog.lpstrFileTitle = NULL; + dialog.lpstrInitialDir = *initfolder ? initfolder : NULL; + dialog.lpstrTitle = title_w; + dialog.Flags = flags | OFN_EXPLORER | OFN_HIDEREADONLY | OFN_NOCHANGEDIR; + dialog.nFileOffset = 0; + dialog.nFileExtension = 0; + dialog.lpstrDefExt = NULL; + dialog.lCustData = 0; + dialog.lpfnHook = NULL; + dialog.lpTemplateName = NULL; + // Skipped many mac-exclusive and reserved members + dialog.FlagsEx = 0; + + BOOL result = pGetAnyFileName(&dialog); + + SDL_free(title_w); + + if (result) { + if (!(flags & OFN_ALLOWMULTISELECT)) { + // File is a C string stored in dialog.lpstrFile + char *chosen_file = WIN_StringToUTF8W(dialog.lpstrFile); + const char *opts[2] = { chosen_file, NULL }; + callback(userdata, opts, getFilterIndex(dialog.nFilterIndex)); + SDL_free(chosen_file); + } else { + /* File is either a C string if the user chose a single file, else + it's a series of strings formatted like: + + "C:\\path\\to\\folder\0filename1.ext\0filename2.ext\0\0" + + The code below will only stop on a double NULL in all cases, so + it is important that the rest of the buffer has been zeroed. */ + char chosen_folder[MAX_PATH]; + char chosen_file[MAX_PATH]; + wchar_t *file_ptr = dialog.lpstrFile; + size_t nfiles = 0; + size_t chosen_folder_size; + char **chosen_files_list = (char **) SDL_malloc(sizeof(char *) * (nfiles + 1)); + + if (!chosen_files_list) { + callback(userdata, NULL, -1); + SDL_free(filebuffer); + return; + } + + chosen_files_list[nfiles] = NULL; + + if (WideCharToMultiByte(CP_UTF8, 0, file_ptr, -1, chosen_folder, MAX_PATH, NULL, NULL) >= MAX_PATH) { + SDL_SetError("Path too long or invalid character in path"); + SDL_free(chosen_files_list); + callback(userdata, NULL, -1); + SDL_free(filebuffer); + return; + } + + chosen_folder_size = SDL_strlen(chosen_folder); + SDL_strlcpy(chosen_file, chosen_folder, MAX_PATH); + chosen_file[chosen_folder_size] = '\\'; + + file_ptr += SDL_strlen(chosen_folder) + 1; + + while (*file_ptr) { + nfiles++; + char **new_cfl = (char **) SDL_realloc(chosen_files_list, sizeof(char*) * (nfiles + 1)); + + if (!new_cfl) { + for (size_t i = 0; i < nfiles - 1; i++) { + SDL_free(chosen_files_list[i]); + } + + SDL_free(chosen_files_list); + callback(userdata, NULL, -1); + SDL_free(filebuffer); + return; + } + + chosen_files_list = new_cfl; + chosen_files_list[nfiles] = NULL; + + int diff = ((int) chosen_folder_size) + 1; + + if (WideCharToMultiByte(CP_UTF8, 0, file_ptr, -1, chosen_file + diff, MAX_PATH - diff, NULL, NULL) >= MAX_PATH - diff) { + SDL_SetError("Path too long or invalid character in path"); + + for (size_t i = 0; i < nfiles - 1; i++) { + SDL_free(chosen_files_list[i]); + } + + SDL_free(chosen_files_list); + callback(userdata, NULL, -1); + SDL_free(filebuffer); + return; + } + + file_ptr += SDL_strlen(chosen_file) + 1 - diff; + + chosen_files_list[nfiles - 1] = SDL_strdup(chosen_file); + + if (!chosen_files_list[nfiles - 1]) { + for (size_t i = 0; i < nfiles - 1; i++) { + SDL_free(chosen_files_list[i]); + } + + SDL_free(chosen_files_list); + callback(userdata, NULL, -1); + SDL_free(filebuffer); + return; + } + } + + // If the user chose only one file, it's all just one string + if (nfiles == 0) { + nfiles++; + char **new_cfl = (char **) SDL_realloc(chosen_files_list, sizeof(char*) * (nfiles + 1)); + + if (!new_cfl) { + SDL_free(chosen_files_list); + callback(userdata, NULL, -1); + SDL_free(filebuffer); + return; + } + + chosen_files_list = new_cfl; + chosen_files_list[nfiles] = NULL; + chosen_files_list[nfiles - 1] = SDL_strdup(chosen_folder); + + if (!chosen_files_list[nfiles - 1]) { + SDL_free(chosen_files_list); + callback(userdata, NULL, -1); + SDL_free(filebuffer); + return; + } + } + + callback(userdata, (const char * const*) chosen_files_list, getFilterIndex(dialog.nFilterIndex)); + + for (size_t i = 0; i < nfiles; i++) { + SDL_free(chosen_files_list[i]); + } + + SDL_free(chosen_files_list); + } + } else { + DWORD error = pCommDlgExtendedError(); + // Error code 0 means the user clicked the cancel button. + if (error == 0) { + /* Unlike SDL's handling of errors, Windows does reset the error + code to 0 after calling GetOpenFileName if another Windows + function before set a different error code, so it's safe to + check for success. */ + const char *opts[1] = { NULL }; + callback(userdata, opts, getFilterIndex(dialog.nFilterIndex)); + } else { + SDL_SetError("Windows error, CommDlgExtendedError: %ld", pCommDlgExtendedError()); + callback(userdata, NULL, -1); + } + } + + SDL_free(filebuffer); +} + +int windows_file_dialog_thread(void *ptr) +{ + windows_ShowFileDialog(ptr); + freeWinArgs(ptr); + return 0; +} + +int CALLBACK browse_callback_proc(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData) +{ + switch (uMsg) { + case BFFM_INITIALIZED: + if (lpData) { + SendMessage(hwnd, BFFM_SETSELECTION, TRUE, lpData); + } + break; + case BFFM_SELCHANGED: + break; + case BFFM_VALIDATEFAILED: + break; + default: + break; + } + return 0; +} + +void windows_ShowFolderDialog(void *ptr) +{ + winFArgs *args = (winFArgs *) ptr; + SDL_Window *window = args->parent; + SDL_DialogFileCallback callback = args->callback; + void *userdata = args->userdata; + HWND parent = NULL; + const char *title = args->title; + + if (window) { + parent = (HWND) SDL_GetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); + } + + wchar_t *title_w = NULL; + + if (title) { + title_w = WIN_UTF8ToStringW(title); + if (!title_w) { + callback(userdata, NULL, -1); + return; + } + } + + wchar_t buffer[MAX_PATH]; + + BROWSEINFOW dialog; + dialog.hwndOwner = parent; + dialog.pidlRoot = NULL; + dialog.pszDisplayName = buffer; + dialog.lpszTitle = title_w; + dialog.ulFlags = BIF_USENEWUI; + dialog.lpfn = browse_callback_proc; + dialog.lParam = (LPARAM)args->default_folder; + dialog.iImage = 0; + + LPITEMIDLIST lpItem = SHBrowseForFolderW(&dialog); + + SDL_free(title_w); + + if (lpItem != NULL) { + SHGetPathFromIDListW(lpItem, buffer); + char *chosen_file = WIN_StringToUTF8W(buffer); + const char *files[2] = { chosen_file, NULL }; + callback(userdata, (const char * const*) files, -1); + SDL_free(chosen_file); + } else { + const char *files[1] = { NULL }; + callback(userdata, (const char * const*) files, -1); + } +} + +int windows_folder_dialog_thread(void *ptr) +{ + windows_ShowFolderDialog(ptr); + freeWinFArgs((winFArgs *)ptr); + return 0; +} + +wchar_t *win_get_filters(const SDL_DialogFileFilter *filters, int nfilters) +{ + wchar_t *filter_wchar = NULL; + + if (filters) { + // '\x01' is used in place of a null byte + // suffix needs two null bytes in case the filter list is empty + char *filterlist = convert_filters(filters, nfilters, clear_filt_names, + "", "", "\x01\x01", "", "\x01", + "\x01", "*.", ";*.", ""); + + if (!filterlist) { + return NULL; + } + + int filter_len = (int)SDL_strlen(filterlist); + + for (char *c = filterlist; *c; c++) { + if (*c == '\x01') { + *c = '\0'; + } + } + + int filter_wlen = MultiByteToWideChar(CP_UTF8, 0, filterlist, filter_len, NULL, 0); + filter_wchar = (wchar_t *)SDL_malloc(filter_wlen * sizeof(wchar_t)); + if (!filter_wchar) { + SDL_free(filterlist); + return NULL; + } + + MultiByteToWideChar(CP_UTF8, 0, filterlist, filter_len, filter_wchar, filter_wlen); + + SDL_free(filterlist); + } + + return filter_wchar; +} + +static void ShowFileDialog(SDL_DialogFileCallback callback, void *userdata, SDL_Window *window, const SDL_DialogFileFilter *filters, int nfilters, const char *default_location, bool allow_many, bool is_save, const char *title, const char *accept, const char *cancel) +{ + winArgs *args; + SDL_Thread *thread; + wchar_t *filters_str; + + if (SDL_GetHint(SDL_HINT_FILE_DIALOG_DRIVER) != NULL) { + SDL_SetError("File dialog driver unsupported"); + callback(userdata, NULL, -1); + return; + } + + args = (winArgs *)SDL_malloc(sizeof(*args)); + if (args == NULL) { + callback(userdata, NULL, -1); + return; + } + + filters_str = win_get_filters(filters, nfilters); + + if (!filters_str && filters) { + callback(userdata, NULL, -1); + SDL_free(args); + return; + } + + args->is_save = is_save; + args->filters_str = filters_str; + args->default_file = default_location ? SDL_strdup(default_location) : NULL; + args->parent = window; + args->flags = allow_many ? OFN_ALLOWMULTISELECT : 0; + args->callback = callback; + args->userdata = userdata; + args->title = title ? SDL_strdup(title) : NULL; + args->accept = accept ? SDL_strdup(accept) : NULL; + args->cancel = cancel ? SDL_strdup(cancel) : NULL; + + thread = SDL_CreateThread(windows_file_dialog_thread, "SDL_Windows_ShowFileDialog", (void *) args); + + if (thread == NULL) { + callback(userdata, NULL, -1); + // The thread won't have run, therefore the data won't have been freed + freeWinArgs(args); + return; + } + + SDL_DetachThread(thread); +} + +void ShowFolderDialog(SDL_DialogFileCallback callback, void *userdata, SDL_Window *window, const char *default_location, bool allow_many, const char *title, const char *accept, const char *cancel) +{ + winFArgs *args; + SDL_Thread *thread; + + if (SDL_GetHint(SDL_HINT_FILE_DIALOG_DRIVER) != NULL) { + SDL_SetError("File dialog driver unsupported"); + callback(userdata, NULL, -1); + return; + } + + args = (winFArgs *)SDL_malloc(sizeof(*args)); + if (args == NULL) { + callback(userdata, NULL, -1); + return; + } + + args->parent = window; + args->callback = callback; + args->default_folder = default_location ? SDL_strdup(default_location) : NULL; + args->userdata = userdata; + args->title = title ? SDL_strdup(title) : NULL; + args->accept = accept ? SDL_strdup(accept) : NULL; + args->cancel = cancel ? SDL_strdup(cancel) : NULL; + + thread = SDL_CreateThread(windows_folder_dialog_thread, "SDL_Windows_ShowFolderDialog", (void *) args); + + if (thread == NULL) { + callback(userdata, NULL, -1); + // The thread won't have run, therefore the data won't have been freed + freeWinFArgs(args); + return; + } + + SDL_DetachThread(thread); +} + +void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props) +{ + /* The internal functions will start threads, and the properties may be freed as soon as this function returns. + Save a copy of what we need before invoking the functions and starting the threads. */ + 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 *title = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_TITLE_STRING, NULL); + const char *accept = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_ACCEPT_STRING, NULL); + const char *cancel = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_CANCEL_STRING, NULL); + bool is_save = false; + + switch (type) { + case SDL_FILEDIALOG_SAVEFILE: + is_save = true; + SDL_FALLTHROUGH; + case SDL_FILEDIALOG_OPENFILE: + ShowFileDialog(callback, userdata, window, filters, nfilters, default_location, allow_many, is_save, title, accept, cancel); + break; + + case SDL_FILEDIALOG_OPENFOLDER: + ShowFolderDialog(callback, userdata, window, default_location, allow_many, title, accept, cancel); + break; + }; +} -- cgit v1.2.3