diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c09a00db..408c6e88d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -255,6 +255,7 @@ define_sdl_subsystem(Haptic) define_sdl_subsystem(Hidapi) define_sdl_subsystem(Power) define_sdl_subsystem(Sensor) +define_sdl_subsystem(Dialog) cmake_dependent_option(SDL_FRAMEWORK "Build SDL libraries as Apple Framework" OFF "APPLE" OFF) if(SDL_FRAMEWORK) @@ -2411,7 +2412,7 @@ elseif(HAIKU) sdl_glob_sources("${SDL3_SOURCE_DIR}/src/core/haiku/*.cc") CheckPTHREAD() - sdl_link_dependency(base LIBS root be media game device textencoding) + sdl_link_dependency(base LIBS root be media game device textencoding tracker) elseif(RISCOS) sdl_glob_sources("${SDL3_SOURCE_DIR}/src/misc/riscos/*.c") @@ -2754,6 +2755,24 @@ elseif(N3DS) sdl_glob_sources("${SDL3_SOURCE_DIR}/src/file/n3ds/*.c") endif() +if (SDL_DIALOG) + if(UNIX AND NOT APPLE AND NOT RISCOS AND NOT HAIKU) + sdl_sources(${SDL3_SOURCE_DIR}/src/dialog/unix/SDL_unixdialog.c) + sdl_sources(${SDL3_SOURCE_DIR}/src/dialog/unix/SDL_portaldialog.c) + sdl_sources(${SDL3_SOURCE_DIR}/src/dialog/unix/SDL_zenitydialog.c) + set(HAVE_SDL_DIALOG TRUE) + elseif(HAIKU) + sdl_sources(${SDL3_SOURCE_DIR}/src/dialog/haiku/SDL_haikudialog.cc) + set(HAVE_SDL_DIALOG TRUE) + elseif(WINDOWS AND NOT WINDOWS_STORE) + sdl_sources(${SDL3_SOURCE_DIR}/src/dialog/windows/SDL_windowsdialog.c) + set(HAVE_SDL_DIALOG TRUE) + elseif(APPLE) + sdl_sources(${SDL3_SOURCE_DIR}/src/dialog/cocoa/SDL_cocoadialog.m) + set(HAVE_SDL_DIALOG TRUE) + endif() +endif() + # Platform-independent options if(SDL_VIDEO) @@ -2809,6 +2828,10 @@ if(NOT HAVE_SDL_MISC) set(SDL_MISC_DUMMY 1) sdl_glob_sources("${SDL3_SOURCE_DIR}/src/misc/dummy/*.c") endif() +if(NOT HAVE_SDL_DIALOG) + set(SDL_DIALOG_DUMMY 1) + sdl_sources(${SDL3_SOURCE_DIR}/src/dialog/dummy/SDL_dummydialog.c) +endif() if(NOT HAVE_CAMERA) set(SDL_CAMERA_DRIVER_DUMMY 1) sdl_glob_sources("${SDL3_SOURCE_DIR}/src/camera/dummy/*.c") diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj index f011b1f64..ccc8492e7 100644 --- a/VisualC/SDL/SDL.vcxproj +++ b/VisualC/SDL/SDL.vcxproj @@ -504,6 +504,7 @@ + NotUsing NotUsing @@ -693,4 +694,4 @@ - \ No newline at end of file + diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters index 4f292f1bf..10eb1997a 100644 --- a/VisualC/SDL/SDL.vcxproj.filters +++ b/VisualC/SDL/SDL.vcxproj.filters @@ -937,6 +937,9 @@ cpuinfo + + dialog + dynapi @@ -1453,4 +1456,4 @@ - \ No newline at end of file + diff --git a/include/SDL3/SDL.h b/include/SDL3/SDL.h index d7716399d..cb49d7aed 100644 --- a/include/SDL3/SDL.h +++ b/include/SDL3/SDL.h @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include diff --git a/include/SDL3/SDL_dialog.h b/include/SDL3/SDL_dialog.h new file mode 100644 index 000000000..78225c9e5 --- /dev/null +++ b/include/SDL3/SDL_dialog.h @@ -0,0 +1,184 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 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. +*/ + +#ifndef SDL_dialog_h_ +#define SDL_dialog_h_ + +#include + +#include +/* Set up for C function definitions, even when using C++ */ +#ifdef __cplusplus +extern "C" { +#endif + +/** + * An entry for filters for file dialogs. + * + * `name` is a user-readable label for the filter (for example, "Office document"). + * + * `pattern` is a semicolon-separated list of file extensions (for example, + * "doc;docx"). + * + * \sa SDL_DialogFileCallback + * \sa SDL_ShowOpenFileDialog + * \sa SDL_ShowSaveFileDialog + * \sa SDL_ShowOpenFolderDialog + */ +typedef struct +{ + const char *name; + const char *pattern; +} SDL_DialogFileFilter; + +/** + * Callback used by file dialog functions. + * + * The specific usage is described in each function. + * + * If filelist is... + * - `NULL`, an error occured. Details can be obtained with SDL_GetError(). + * - A pointer to `NULL`, the user either didn't choose any file or canceled + * the dialog. + * - A pointer to non-`NULL`, the user chose one or more files. The argument is + * a null-terminated list of pointers to C strings, each containing a path. + * + * The filelist argument does not need to be freed; it will automatically be + * freed when the callback returns. + * + * The filter argument is the index of the filter that was selected, or one + * more than the size of the list (therefore the index of the terminating NULL + * entry) if no filter was selected, or -1 if the platform or method doesn't + * support fetching the selected filter or if an error occured. + * + * \sa SDL_DialogFileFilter + * \sa SDL_ShowOpenFileDialog + * \sa SDL_ShowSaveFileDialog + * \sa SDL_ShowOpenFolderDialog + */ +typedef void(SDLCALL *SDL_DialogFileCallback)(void *userdata, const char * const *filelist, int filter); + +/** + * Displays a dialog that lets the user select a file on their filesystem. + * + * This function should only be invoked from the main thread. + * + * This is an asynchronous function; it will return immediately, and the result + * will be passed to the callback. + * + * The callback will be invoked with a null-terminated list of files the user + * chose. The list will be empty if the user canceled the dialog, and it will + * be NULL if an error occured. + * + * Note that the callback may be called from a different thread than the one + * the function was invoked on. + * + * Depending on the platform, the user may be allowed to input paths that don't + * yet exist. + * + * \param callback The function to be invoked when the user selects a file and accepts, or the user cancels the dialog, or an error occurs. The first argument is a null-terminated list of C strings, representing the paths chosen by the user. The list will be empty if the user canceled the dialog, and it will be NULL if an error occured. If an error occured, it can be fetched with SDL_GetError(). The second argument is the userdata pointer passed to the function. + * \param userdata An optional pointer to pass extra data to the callback when it will be invoked. + * \param window The window that the dialog should be modal for. May be NULL. Not all platforms support this option. + * \param filters A null-terminated list of SDL_DialogFileFilter's. May be NULL. Not all platforms support this option, and platforms that do support it may allow the user to ignore the filters. + * \param default_location The default folder or file to start the dialog at. May be NULL. Not all platforms support this option. + * \param allow_many If non-zero, the user will be allowed to select multiple entries. Not all platforms support this option. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_DialogFileFilter + * \sa SDL_DialogFileCallback + * \sa SDL_ShowSaveFileDialog + * \sa SDL_ShowOpenFolderDialog + */ +extern DECLSPEC void SDLCALL SDL_ShowOpenFileDialog(SDL_DialogFileCallback callback, void *userdata, SDL_Window *window, const SDL_DialogFileFilter *filters, const char *default_location, SDL_bool allow_many); + +/** + * Displays a dialog that lets the user choose a new or existing file on their filesystem. + * + * This function should only be invoked from the main thread. + * + * This is an asynchronous function; it will return immediately, and the result + * will be passed to the callback. + * + * The callback will be invoked with a null-terminated list of files the user + * chose. The list will be empty if the user canceled the dialog, and it will + * be NULL if an error occured. + * + * Note that the callback may be called from a different thread than the one + * the function was invoked on. + * + * The chosen file may or may not already exist. + * + * \param callback The function to be invoked when the user selects a file and accepts, or the user cancels the dialog, or an error occurs. The first argument is a null-terminated list of C strings, representing the paths chosen by the user. The list will be empty if the user canceled the dialog, and it will be NULL if an error occured. If an error occured, it can be fetched with SDL_GetError(). The second argument is the userdata pointer passed to the function. + * \param userdata An optional pointer to pass extra data to the callback when it will be invoked. + * \param window The window that the dialog should be modal for. May be NULL. Not all platforms support this option. + * \param filters A null-terminated list of SDL_DialogFileFilter's. May be NULL. Not all platforms support this option, and platforms that do support it may allow the user to ignore the filters. + * \param default_location The default folder or file to start the dialog at. May be NULL. Not all platforms support this option. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_DialogFileFilter + * \sa SDL_DialogFileCallback + * \sa SDL_ShowOpenFileDialog + * \sa SDL_ShowOpenFolderDialog + */ +extern DECLSPEC void SDLCALL SDL_ShowSaveFileDialog(SDL_DialogFileCallback callback, void *userdata, SDL_Window *window, const SDL_DialogFileFilter *filters, const char *default_location); + +/** + * Displays a dialog that lets the user select a folder on their filesystem. + * + * This function should only be invoked from the main thread. + * + * This is an asynchronous function; it will return immediately, and the result + * will be passed to the callback. + * + * The callback will be invoked with a null-terminated list of files the user + * chose. The list will be empty if the user canceled the dialog, and it will + * be NULL if an error occured. + * + * Note that the callback may be called from a different thread than the one + * the function was invoked on. + * + * Depending on the platform, the user may be allowed to input paths that don't + * yet exist. + * + * \param callback The function to be invoked when the user selects a folder and accepts, or the user cancels the dialog, or an error occurs. The first argument is a null-terminated list of C strings, representing the paths chosen by the user. The list will be empty if the user canceled the dialog, and it will be NULL if an error occured. If an error occured, it can be fetched with SDL_GetError(). The second argument is the userdata pointer passed to the function. + * \param userdata An optional pointer to pass extra data to the callback when it will be invoked. + * \param window The window that the dialog should be modal for. May be NULL. Not all platforms support this option. + * \param default_location The default folder or file to start the dialog at. May be NULL. Not all platforms support this option. + * \param allow_many If non-zero, the user will be allowed to select multiple entries. Not all platforms support this option. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_DialogFileFilter + * \sa SDL_DialogFileCallback + * \sa SDL_ShowOpenFileDialog + * \sa SDL_ShowSaveFileDialog + */ +extern DECLSPEC void SDLCALL SDL_ShowOpenFolderDialog(SDL_DialogFileCallback callback, void *userdata, SDL_Window *window, const char *default_location, SDL_bool allow_many); + +/* Ends C function definitions when using C++ */ +#ifdef __cplusplus +} +#endif +#include + +#endif /* SDL_joystick_h_ */ diff --git a/src/core/linux/SDL_dbus.c b/src/core/linux/SDL_dbus.c index 60be16dc4..9e6a56b65 100644 --- a/src/core/linux/SDL_dbus.c +++ b/src/core/linux/SDL_dbus.c @@ -53,6 +53,7 @@ static int LoadDBUSSyms(void) SDL_DBUS_SYM(void (*)(DBusConnection *, dbus_bool_t), connection_set_exit_on_disconnect); SDL_DBUS_SYM(dbus_bool_t (*)(DBusConnection *), connection_get_is_connected); SDL_DBUS_SYM(dbus_bool_t (*)(DBusConnection *, DBusHandleMessageFunction, void *, DBusFreeFunction), connection_add_filter); + SDL_DBUS_SYM(dbus_bool_t (*)(DBusConnection *, DBusHandleMessageFunction, void *), connection_remove_filter); SDL_DBUS_SYM(dbus_bool_t (*)(DBusConnection *, const char *, const DBusObjectPathVTable *, void *, DBusError *), connection_try_register_object_path); SDL_DBUS_SYM(dbus_bool_t (*)(DBusConnection *, DBusMessage *, dbus_uint32_t *), connection_send); SDL_DBUS_SYM(DBusMessage *(*)(DBusConnection *, DBusMessage *, int, DBusError *), connection_send_with_reply_and_block); @@ -63,6 +64,7 @@ static int LoadDBUSSyms(void) SDL_DBUS_SYM(dbus_bool_t (*)(DBusConnection *, int), connection_read_write); SDL_DBUS_SYM(DBusDispatchStatus (*)(DBusConnection *), connection_dispatch); SDL_DBUS_SYM(dbus_bool_t (*)(DBusMessage *, const char *, const char *), message_is_signal); + SDL_DBUS_SYM(dbus_bool_t (*)(DBusMessage *, const char *), message_has_path); SDL_DBUS_SYM(DBusMessage *(*)(const char *, const char *, const char *, const char *), message_new_method_call); SDL_DBUS_SYM(dbus_bool_t (*)(DBusMessage *, int, ...), message_append_args); SDL_DBUS_SYM(dbus_bool_t (*)(DBusMessage *, int, va_list), message_append_args_valist); diff --git a/src/core/linux/SDL_dbus.h b/src/core/linux/SDL_dbus.h index c37de56aa..0be866b36 100644 --- a/src/core/linux/SDL_dbus.h +++ b/src/core/linux/SDL_dbus.h @@ -31,6 +31,9 @@ #ifndef DBUS_TIMEOUT_USE_DEFAULT #define DBUS_TIMEOUT_USE_DEFAULT -1 #endif +#ifndef DBUS_TIMEOUT_INFINITE +#define DBUS_TIMEOUT_INFINITE ((int) 0x7fffffff) +#endif typedef struct SDL_DBusContext { @@ -44,6 +47,7 @@ typedef struct SDL_DBusContext void (*connection_set_exit_on_disconnect)(DBusConnection *, dbus_bool_t); dbus_bool_t (*connection_get_is_connected)(DBusConnection *); dbus_bool_t (*connection_add_filter)(DBusConnection *, DBusHandleMessageFunction, void *, DBusFreeFunction); + dbus_bool_t (*connection_remove_filter)(DBusConnection *, DBusHandleMessageFunction, void *); dbus_bool_t (*connection_try_register_object_path)(DBusConnection *, const char *, const DBusObjectPathVTable *, void *, DBusError *); dbus_bool_t (*connection_send)(DBusConnection *, DBusMessage *, dbus_uint32_t *); @@ -55,6 +59,7 @@ typedef struct SDL_DBusContext dbus_bool_t (*connection_read_write)(DBusConnection *, int); DBusDispatchStatus (*connection_dispatch)(DBusConnection *); dbus_bool_t (*message_is_signal)(DBusMessage *, const char *, const char *); + dbus_bool_t (*message_has_path)(DBusMessage *, const char *); DBusMessage *(*message_new_method_call)(const char *, const char *, const char *, const char *); dbus_bool_t (*message_append_args)(DBusMessage *, int, ...); dbus_bool_t (*message_append_args_valist)(DBusMessage *, int, va_list); diff --git a/src/dialog/cocoa/SDL_cocoadialog.m b/src/dialog/cocoa/SDL_cocoadialog.m new file mode 100644 index 000000000..3d4a8a59b --- /dev/null +++ b/src/dialog/cocoa/SDL_cocoadialog.m @@ -0,0 +1,169 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 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" + +/* TODO: Macro? */ + +#import + +typedef enum +{ + FDT_SAVE, + FDT_OPEN, + FDT_OPENFOLDER +} cocoa_FileDialogType; + +void show_file_dialog(cocoa_FileDialogType type, SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location, SDL_bool allow_many) +{ + /* NSOpenPanel inherits from NSSavePanel */ + NSSavePanel *dialog; + NSOpenPanel *dialog_as_open; + + switch (type) { + case FDT_SAVE: + dialog = [NSSavePanel savePanel]; + break; + case FDT_OPEN: + dialog_as_open = [NSOpenPanel openPanel]; + [dialog_as_open setAllowsMultipleSelection:((allow_many == SDL_TRUE) ? YES : NO)]; + dialog = dialog_as_open; + break; + case FDT_OPENFOLDER: + dialog_as_open = [NSOpenPanel openPanel]; + [dialog_as_open setCanChooseFiles:NO]; + [dialog_as_open setCanChooseDirectories:YES]; + [dialog_as_open setAllowsMultipleSelection:((allow_many == SDL_TRUE) ? YES : NO)]; + dialog = dialog_as_open; + break; + }; + + int n = -1; + while (filters[++n].name && filters[n].pattern); + NSMutableArray *types = [[NSMutableArray alloc] initWithCapacity:n ]; + + int has_all_files = 0; + for (int i = 0; i < n; i++) { + char *pattern = SDL_strdup(filters[i].pattern); + char *pattern_ptr = pattern; + + if (!pattern_ptr) { + SDL_OutOfMemory(); + callback(userdata, NULL, -1); + return; + } + + for (char *c = pattern; *c; c++) { + if (*c == ';') { + *c = '\0'; + [types addObject: [NSString stringWithFormat: @"%s", pattern_ptr]]; + pattern_ptr = c + 1; + } else if (!((*c >= 'a' && *c <= 'z') || (*c >= 'A' && *c <= 'Z') || (*c >= '0' && *c <= '9') || *c == '.' || *c == '_' || *c == '-' || (*c == '*' && (c[1] == '\0' || c[1] == ';')))) { + SDL_SetError("Illegal character in pattern name: %c (Only alphanumeric characters, periods, underscores and hyphens allowed)", *c); + callback(userdata, NULL, -1); + SDL_free(pattern); + } else if (*c == '*') { + has_all_files = 1; + } + } + [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_GetProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, NULL); + } + + if (w) { + // [dialog beginWithCompletionHandler:^(NSInteger result) { + [dialog beginSheetModalForWindow:w completionHandler:^(NSInteger result) { + // NSModalResponseOK for >= 10.13 + if (result == NSFileHandlingPanelOKButton) { + 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 { + // NSModalResponseOK for >= 10.10 + if ([dialog runModal] == NSOKButton) { + 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); + } + } +} + +void SDL_ShowOpenFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location, SDL_bool allow_many) +{ + show_file_dialog(FDT_OPEN, callback, userdata, window, filters, default_location, allow_many); +} + +void SDL_ShowSaveFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location) +{ + show_file_dialog(FDT_SAVE, callback, userdata, window, filters, default_location, 0); +} + +void SDL_ShowOpenFolderDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const char* default_location, SDL_bool allow_many) +{ + show_file_dialog(FDT_OPENFOLDER, callback, userdata, window, NULL, default_location, allow_many); +} diff --git a/src/dialog/dummy/SDL_dummydialog.c b/src/dialog/dummy/SDL_dummydialog.c new file mode 100644 index 000000000..8740f2f2d --- /dev/null +++ b/src/dialog/dummy/SDL_dummydialog.c @@ -0,0 +1,41 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 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" + +/* TODO: Macro? */ + +void SDL_ShowOpenFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location, SDL_bool allow_many) +{ + SDL_Unsupported(); + callback(userdata, NULL, -1); +} + +void SDL_ShowSaveFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location) +{ + SDL_Unsupported(); + callback(userdata, NULL, -1); +} + +void SDL_ShowOpenFolderDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const char* default_location, SDL_bool allow_many) +{ + SDL_Unsupported(); + callback(userdata, NULL, -1); +} diff --git a/src/dialog/haiku/SDL_haikudialog.cc b/src/dialog/haiku/SDL_haikudialog.cc new file mode 100644 index 000000000..3e63fc807 --- /dev/null +++ b/src/dialog/haiku/SDL_haikudialog.cc @@ -0,0 +1,233 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 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 "../../core/haiku/SDL_BeApp.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 retval; + std::string s = str; + size_t pos = 0; + + while ((pos = s.find(split)) != std::string::npos) { + retval.push_back(s.substr(0, pos)); + s = s.substr(pos + split.size()); + } + + retval.push_back(s); + + return retval; +} + +class SDLBRefFilter : public BRefFilter +{ +public: + SDLBRefFilter(const SDL_DialogFileFilter *filters) : + BRefFilter(), + m_filters(filters) + { + } + + 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; + + const auto *filter = m_filters; + while (filter->name && filter->pattern) { + for (const auto& suffix : StringSplit(filter->pattern, ";")) { + if (StringEndsWith(result, std::string(".") + suffix)) { + return true; + } + } + filter++; + } + + return false; + } + +private: + const SDL_DialogFileFilter * const m_filters; +}; + +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 ShowDialog(bool save, SDL_DialogFileCallback callback, void *userdata, bool many, bool modal, const SDL_DialogFileFilter *filters, bool folder, const char *location) +{ + 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; + } + + // No unique_ptr's because they need to survive the end of the function + CallbackLooper *looper = new CallbackLooper(callback, userdata); + BMessenger *messenger = new BMessenger(NULL, looper); + SDLBRefFilter *filter = new SDLBRefFilter(filters); + + 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); + looper->SetToBeFreed(messenger, panel, filter); + looper->Run(); + panel->Show(); +} + +void SDL_ShowOpenFileDialog(SDL_DialogFileCallback callback, void *userdata, SDL_Window *window, const SDL_DialogFileFilter *filters, const char *default_location, SDL_bool allow_many) +{ + ShowDialog(false, callback, userdata, allow_many == SDL_TRUE, !!window, filters, false, default_location); +} + +void SDL_ShowSaveFileDialog(SDL_DialogFileCallback callback, void *userdata, SDL_Window *window, const SDL_DialogFileFilter *filters, const char *default_location) +{ + ShowDialog(true, callback, userdata, false, !!window, filters, false, default_location); +} + +void SDL_ShowOpenFolderDialog(SDL_DialogFileCallback callback, void *userdata, SDL_Window *window, const char* default_location, SDL_bool allow_many) +{ + // Use a dummy filter to avoid showing files in the dialog + SDL_DialogFileFilter filter[] = {{}}; + ShowDialog(false, callback, userdata, allow_many == SDL_TRUE, !!window, filter, true, default_location); +} diff --git a/src/dialog/unix/SDL_portaldialog.c b/src/dialog/unix/SDL_portaldialog.c new file mode 100644 index 000000000..f7610a922 --- /dev/null +++ b/src/dialog/unix/SDL_portaldialog.c @@ -0,0 +1,408 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 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/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 + +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) { + SDL_OutOfMemory(); + 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(sizeof(char), max_len); + if (!glob_pattern) { + SDL_OutOfMemory(); + 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) +{ + 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 (const SDL_DialogFileFilter *filter = filters; filter && filter->name && filter->pattern; ++filter) { + DBus_AppendFilter(dbus, &options_value_array, filter); + } + 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; + + 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) { + /* cancelled */ + const char *result_data[] = { NULL }; + signal_data->callback(signal_data->userdata, result_data, -1); /* TODO: Set this to the last selected filter */ + goto handled; + } + else if (result) { + /* some error occurred */ + signal_data->callback(signal_data->userdata, NULL, -1); + goto handled; + } + + 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(sizeof(const char *) * length); + if (!path) { + SDL_OutOfMemory(); + signal_data->callback(signal_data->userdata, NULL, -1); + goto cleanup; + } + + while (dbus->message_iter_get_arg_type(&uri_entry) == DBUS_TYPE_STRING) + { + if (current >= length - 1) { + ++length; + path = SDL_realloc(path, sizeof(const char *) * length); + if (!path) { + SDL_OutOfMemory(); + signal_data->callback(signal_data->userdata, NULL, -1); + goto cleanup; + } + } + dbus->message_iter_get_basic(&uri_entry, path + current); + + dbus->message_iter_next(&uri_entry); + ++current; + } + path[length - 1] = NULL; + signal_data->callback(signal_data->userdata, path, -1); /* TODO: Fetch the index of the filter that was used */ +cleanup: + dbus->connection_remove_filter(conn, &DBus_MessageFilter, signal_data); + + SDL_free(path); + SDL_free((void *)signal_data->path); + SDL_free(signal_data); +handled: + return DBUS_HANDLER_RESULT_HANDLED; + } +not_our_signal: + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static void DBus_OpenDialog(const char *method, const char *method_title, SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location, SDL_bool allow_many, int open_folders) +{ + SDL_DBusContext *dbus = SDL_DBus_GetContext(); + DBusMessage *msg; + DBusMessageIter params, options; + const char *signal_id; + char *handle_str, *filter; + int filter_len; + static const char *parent_window = ""; /* TODO: Consider using X11's PID or the Wayland handle */ + static uint32_t handle_id = 0; + + if (dbus == NULL) { + SDL_SetError("%s", "Failed to connect to DBus!"); + return; + } + + msg = dbus->message_new_method_call(PORTAL_DESTINATION, PORTAL_PATH, PORTAL_INTERFACE, method); + if (msg == NULL) { + SDL_SetError("%s", "Failed to send message to portal!"); + return; + } + + dbus->message_iter_init_append(msg, ¶ms); + dbus->message_iter_append_basic(¶ms, DBUS_TYPE_STRING, &parent_window); + 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) { + SDL_OutOfMemory(); + 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 == SDL_TRUE) { + DBus_AppendBoolOption(dbus, &options, "multiple", 1); + } + if (open_folders) { + DBus_AppendBoolOption(dbus, &options, "directory", 1); + } + if (filters) { + DBus_AppendFilters(dbus, &options, filters); + } + if (default_location) { + DBus_AppendByteArray(dbus, &options, "current_folder", default_location); + } + 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) + { + SDL_SetError("%s", "Invalid response received by DBus!"); + goto incorrect_type; + } + dbus->message_iter_get_basic(&reply_iter, &signal_id); + } + + dbus->message_unref(msg); + + filter_len = SDL_strlen(SIGNAL_FILTER) + SDL_strlen(signal_id) + 2; + filter = SDL_malloc(sizeof(char) * filter_len); + if (!filter) { + SDL_OutOfMemory(); + 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) { + SDL_OutOfMemory(); + goto incorrect_type; + } + data->callback = callback; + data->userdata = userdata; + data->path = SDL_strdup(signal_id); + if (!data->path) { + SDL_OutOfMemory(); + SDL_free(data); + 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); +} + +void SDL_Portal_ShowOpenFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location, SDL_bool allow_many) +{ + DBus_OpenDialog("OpenFile", "Open File", callback, userdata, window, filters, default_location, allow_many, 0); +} + +void SDL_Portal_ShowSaveFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location) +{ + DBus_OpenDialog("SaveFile", "Save File", callback, userdata, window, filters, default_location, 0, 0); +} + +void SDL_Portal_ShowOpenFolderDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const char* default_location, SDL_bool allow_many) +{ + DBus_OpenDialog("OpenFile", "Open Folder", callback, userdata, window, NULL, default_location, allow_many, 1); +} + +int SDL_Portal_detect(void) +{ + /* TODO */ + return 0; +} + +#else + +/* Dummy implementation to avoid compilation problems */ + +void SDL_Portal_ShowOpenFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location, SDL_bool allow_many) +{ + SDL_Unsupported(); + callback(userdata, NULL, -1); +} + +void SDL_Portal_ShowSaveFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location) +{ + SDL_Unsupported(); + callback(userdata, NULL, -1); +} + +void SDL_Portal_ShowOpenFolderDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const char* default_location, SDL_bool allow_many) +{ + SDL_Unsupported(); + callback(userdata, NULL, -1); +} + +int SDL_Portal_detect(void) +{ + return 0; +} + +#endif /* SDL_USE_LIBDBUS */ diff --git a/src/dialog/unix/SDL_portaldialog.h b/src/dialog/unix/SDL_portaldialog.h new file mode 100644 index 000000000..71ed3cbed --- /dev/null +++ b/src/dialog/unix/SDL_portaldialog.h @@ -0,0 +1,29 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 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_ShowOpenFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location, SDL_bool allow_many); +void SDL_Portal_ShowSaveFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location); +void SDL_Portal_ShowOpenFolderDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const char* default_location, SDL_bool allow_many); + +/** @returns non-zero if available, zero if unavailable */ +int SDL_Portal_detect(void); diff --git a/src/dialog/unix/SDL_unixdialog.c b/src/dialog/unix/SDL_unixdialog.c new file mode 100644 index 000000000..29ce2eef0 --- /dev/null +++ b/src/dialog/unix/SDL_unixdialog.c @@ -0,0 +1,85 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 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_portaldialog.h" +#include "./SDL_zenitydialog.h" + +static void (*detected_open)(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location, SDL_bool allow_many) = NULL; +static void (*detected_save)(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location) = NULL; +static void (*detected_folder)(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const char* default_location, SDL_bool allow_many) = NULL; + +/* Returns non-zero on success, 0 on failure */ +static int detect_available_methods(void) +{ + if (SDL_Portal_detect()) { + detected_open = SDL_Portal_ShowOpenFileDialog; + detected_save = SDL_Portal_ShowSaveFileDialog; + detected_folder = SDL_Portal_ShowOpenFolderDialog; + return 1; + } + + if (SDL_Zenity_detect()) { + detected_open = SDL_Zenity_ShowOpenFileDialog; + detected_save = SDL_Zenity_ShowSaveFileDialog; + detected_folder = SDL_Zenity_ShowOpenFolderDialog; + return 2; + } + + SDL_SetError("No supported method for file dialogs"); + return 0; +} + +void SDL_ShowOpenFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location, SDL_bool allow_many) +{ + /* Call detect_available_methods() again each time in case the situation changed */ + if (!detected_open && !detect_available_methods()) { + /* SetError() done by detect_available_methods() */ + callback(userdata, NULL, -1); + return; + } + + detected_open(callback, userdata, window, filters, default_location, allow_many); +} + +void SDL_ShowSaveFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location) +{ + /* Call detect_available_methods() again each time in case the situation changed */ + if (!detected_save && !detect_available_methods()) { + /* SetError() done by detect_available_methods() */ + callback(userdata, NULL, -1); + return; + } + + detected_save(callback, userdata, window, filters, default_location); +} + +void SDL_ShowOpenFolderDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const char* default_location, SDL_bool allow_many) +{ + /* Call detect_available_methods() again each time in case the situation changed */ + if (!detected_folder && !detect_available_methods()) { + /* SetError() done by detect_available_methods() */ + callback(userdata, NULL, -1); + return; + } + + detected_folder(callback, userdata, window, default_location, allow_many); +} diff --git a/src/dialog/unix/SDL_zenitydialog.c b/src/dialog/unix/SDL_zenitydialog.c new file mode 100644 index 000000000..5ba8e8dc7 --- /dev/null +++ b/src/dialog/unix/SDL_zenitydialog.c @@ -0,0 +1,488 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 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 +#include +#include +#include + +typedef enum +{ + ZENITY_MULTIPLE = 0x1, + ZENITY_DIRECTORY = 0x2, + ZENITY_SAVE = 0x4 +} zenityFlags; + +typedef struct +{ + SDL_DialogFileCallback callback; + void* userdata; + const char* filename; + const SDL_DialogFileFilter *filters; + Uint32 flags; +} zenityArgs; + +#define CLEAR_AND_RETURN() \ + { \ + while (--nextarg >= 0) { \ + SDL_free(argv[nextarg]); \ + } \ + SDL_free(argv); \ + return NULL; \ + } + +#define CHECK_OOM() \ + { \ + if (!argv[nextarg - 1]) { \ + SDL_OutOfMemory(); \ + CLEAR_AND_RETURN() \ + } \ + \ + if (nextarg > argc) { \ + SDL_SetError("Zenity dialog problem: argc (%d) < nextarg (%d)", \ + argc, nextarg); \ + CLEAR_AND_RETURN() \ + } \ + } + +/* Exec call format: + * + * /usr/bin/env zenity --file-selection --separator=\n [--multiple] + * [--directory] [--save] [--filename FILENAME] + * [--file-filter=Filter Name | *.filt *.fn ...]... + */ +static char** generate_args(const zenityArgs* info) +{ + int argc = 4; + int nextarg = 0; + char **argv = NULL; + + /* ARGC PASS */ + if (info->flags & ZENITY_MULTIPLE) { + argc++; + } + + if (info->flags & ZENITY_DIRECTORY) { + argc++; + } + + if (info->flags & ZENITY_SAVE) { + argc++; + } + + if (info->filename) { + argc += 2; + } + + if (info->filters) { + const SDL_DialogFileFilter *filter_ptr = info->filters; + + while (filter_ptr->name && filter_ptr->pattern) { + argc++; + filter_ptr++; + } + } + + argv = SDL_malloc(sizeof(char *) * argc + 1); + + if (!argv) { + SDL_OutOfMemory(); + return NULL; + } + + argv[nextarg++] = SDL_strdup("/usr/bin/env"); + CHECK_OOM() + argv[nextarg++] = SDL_strdup("zenity"); + CHECK_OOM() + argv[nextarg++] = SDL_strdup("--file-selection"); + CHECK_OOM() + argv[nextarg++] = SDL_strdup("--separator=\n"); + CHECK_OOM() + + /* ARGV PASS */ + if (info->flags & ZENITY_MULTIPLE) { + argv[nextarg++] = SDL_strdup("--multiple"); + CHECK_OOM() + } + + if (info->flags & ZENITY_DIRECTORY) { + argv[nextarg++] = SDL_strdup("--directory"); + CHECK_OOM() + } + + if (info->flags & ZENITY_SAVE) { + argv[nextarg++] = SDL_strdup("--save"); + CHECK_OOM() + } + + if (info->filename) { + argv[nextarg++] = SDL_strdup("--filename"); + CHECK_OOM() + + argv[nextarg++] = SDL_strdup(info->filename); + CHECK_OOM() + } + + if (info->filters) { + const SDL_DialogFileFilter *filter_ptr = info->filters; + + while (filter_ptr->name && filter_ptr->pattern) { + /* *Normally*, no filter arg should exceed 4096 bytes. */ + char buffer[4096]; + + SDL_snprintf(buffer, 4096, "--file-filter=%s | *.", filter_ptr->name); + size_t i_buf = SDL_strlen(buffer); + + /* "|" is a special character for Zenity */ + for (char *c = buffer; *c; c++) { + if (*c == '|') { + *c = ' '; + } + } + + for (size_t i_pat = 0; i_buf < 4095 && filter_ptr->pattern[i_pat]; i_pat++) { + const char *c = filter_ptr->pattern + i_pat; + + if (*c == ';') { + /* Disallow empty patterns (might bug Zenity) */ + int at_end = (c[1] == '\0'); + int at_mid = (c[1] == ';'); + int at_beg = (i_pat == 0); + if (at_end || at_mid || at_beg) { + const char *pos_str = ""; + + if (at_end) { + pos_str = "end"; + } else if (at_mid) { + pos_str = "middle"; + } else if (at_beg) { + pos_str = "beginning"; + } + + SDL_SetError("Empty pattern file extension (at %s of list)", pos_str); + CLEAR_AND_RETURN() + } + + if (i_buf + 3 >= 4095) { + i_buf += 3; + break; + } + + buffer[i_buf++] = ' '; + buffer[i_buf++] = '*'; + buffer[i_buf++] = '.'; + } else if (*c == '*' && (c[1] == '\0' || c[1] == ';') && (i_pat == 0 || *(c - 1) == ';')) { + buffer[i_buf++] = '*'; + } else if (!((*c >= 'a' && *c <= 'z') || (*c >= 'A' && *c <= 'Z') || (*c >= '0' && *c <= '9') || *c == '.' || *c == '_' || *c == '-')) { + SDL_SetError("Illegal character in pattern name: %c (Only alphanumeric characters, periods, underscores and hyphens allowed)", *c); + CLEAR_AND_RETURN() + } else { + buffer[i_buf++] = *c; + } + } + + if (i_buf >= 4095) { + SDL_SetError("Filter '%s' wouldn't fit in a 4096 byte buffer; please report your use case if you need filters that long", filter_ptr->name); + CLEAR_AND_RETURN() + } + + buffer[i_buf] = '\0'; + + argv[nextarg++] = SDL_strdup(buffer); + CHECK_OOM() + + filter_ptr++; + } + } + + argv[nextarg++] = NULL; + + return argv; +} + +void free_args(char **argv) +{ + char **ptr = argv; + + while (*ptr) { + SDL_free(*ptr); + ptr++; + } + + SDL_free(argv); +} + +/* TODO: Zenity survives termination of the parent */ + +static void run_zenity(zenityArgs* arg_struct) +{ + SDL_DialogFileCallback callback = arg_struct->callback; + void* userdata = arg_struct->userdata; + + int out[2]; + pid_t process; + int status = -1; + + if (pipe(out) < 0) { + SDL_SetError("Could not create pipe: %s", strerror(errno)); + callback(userdata, NULL, -1); + return; + } + + /* Args are only needed in the forked process, but generating them early + allows catching the error messages in the main process */ + char **args = generate_args(arg_struct); + + if (!args) { + /* SDL_SetError will have been called already */ + callback(userdata, NULL, -1); + return; + } + + process = fork(); + + if (process < 0) { + SDL_SetError("Could not fork process: %s", strerror(errno)); + close(out[0]); + close(out[1]); + free_args(args); + callback(userdata, NULL, -1); + return; + } else if (process == 0){ + dup2(out[1], STDOUT_FILENO); + close(STDERR_FILENO); /* Hide errors from Zenity to stderr */ + close(out[0]); + close(out[1]); + + /* Recent versions of Zenity have different exit codes, but picks up + different codes from the environment */ + SDL_setenv("ZENITY_OK", "0", 1); + SDL_setenv("ZENITY_CANCEL", "1", 1); + SDL_setenv("ZENITY_ESC", "1", 1); + SDL_setenv("ZENITY_EXTRA", "2", 1); + SDL_setenv("ZENITY_ERROR", "2", 1); + SDL_setenv("ZENITY_TIMEOUT", "2", 1); + + execv(args[0], args); + + exit(errno + 128); + } else { + char readbuffer[2048]; + size_t bytes_read = 0, bytes_last_read; + char *container = NULL; + close(out[1]); + free_args(args); + + while ((bytes_last_read = read(out[0], readbuffer, sizeof(readbuffer)))) { + char *new_container = SDL_realloc(container, bytes_read + bytes_last_read); + if (!new_container) { + SDL_OutOfMemory(); + SDL_free(container); + close(out[0]); + callback(userdata, NULL, -1); + return; + } + container = new_container; + SDL_memcpy(container + bytes_read, readbuffer, bytes_last_read); + bytes_read += bytes_last_read; + } + close(out[0]); + + if (waitpid(process, &status, 0) == -1) { + SDL_SetError("waitpid failed"); + SDL_free(container); + callback(userdata, NULL, -1); + return; + } + + if (WIFEXITED(status)) { + status = WEXITSTATUS(status); + } + + size_t narray = 1; + char **array = (char **) SDL_malloc((narray + 1) * sizeof(char *)); + + if (!array) { + SDL_OutOfMemory(); + SDL_free(container); + callback(userdata, NULL, -1); + return; + } + + 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) { + SDL_OutOfMemory(); + SDL_free(container); + SDL_free(array); + callback(userdata, NULL, -1); + return; + } + 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 (may be zenity or execv+128)", status); + callback(userdata, NULL, -1); + } + + SDL_free(array); + SDL_free(container); + } +} + +static int run_zenity_thread(void* ptr) +{ + run_zenity(ptr); + SDL_free(ptr); + return 0; +} + +void SDL_Zenity_ShowOpenFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location, SDL_bool allow_many) +{ + zenityArgs *args; + SDL_Thread *thread; + + args = SDL_malloc(sizeof(*args)); + if (!args) { + SDL_OutOfMemory(); + callback(userdata, NULL, -1); + return; + } + + args->callback = callback; + args->userdata = userdata; + args->filename = default_location; + args->filters = filters; + args->flags = (allow_many == SDL_TRUE) ? ZENITY_MULTIPLE : 0; + + thread = SDL_CreateThread(run_zenity_thread, "SDL_ShowOpenFileDialog", (void *) args); + + if (thread == NULL) { + callback(userdata, NULL, -1); + return; + } + + SDL_DetachThread(thread); +} + +void SDL_Zenity_ShowSaveFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location) +{ + zenityArgs *args; + SDL_Thread *thread; + + args = SDL_malloc(sizeof(zenityArgs)); + if (args == NULL) { + SDL_OutOfMemory(); + callback(userdata, NULL, -1); + return; + } + + args->callback = callback; + args->userdata = userdata; + args->filename = default_location; + args->filters = filters; + args->flags = ZENITY_SAVE; + + thread = SDL_CreateThread(run_zenity_thread, "SDL_ShowSaveFileDialog", (void *) args); + + if (thread == NULL) { + callback(userdata, NULL, -1); + return; + } + + SDL_DetachThread(thread); +} + +void SDL_Zenity_ShowOpenFolderDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const char* default_location, SDL_bool allow_many) +{ + zenityArgs *args; + SDL_Thread *thread; + + args = SDL_malloc(sizeof(zenityArgs)); + if (args == NULL) { + SDL_OutOfMemory(); + callback(userdata, NULL, -1); + return; + } + + args->callback = callback; + args->userdata = userdata; + args->filename = default_location; + args->filters = NULL; + args->flags = ((allow_many == SDL_TRUE) ? ZENITY_MULTIPLE : 0) | ZENITY_DIRECTORY; + + thread = SDL_CreateThread(run_zenity_thread, "SDL_ShowOpenFolderDialog", (void *) args); + + if (thread == NULL) { + callback(userdata, NULL, -1); + return; + } + + SDL_DetachThread(thread); +} + +int SDL_Zenity_detect(void) +{ + pid_t process; + int status = -1; + + process = fork(); + + if (process < 0) { + SDL_SetError("Could not fork process: %s", strerror(errno)); + return 0; + } else if (process == 0){ + /* Disable output */ + close(STDERR_FILENO); + close(STDOUT_FILENO); + execl("/usr/bin/env", "/usr/bin/env", "zenity", "--version", NULL); + exit(errno + 128); + } else { + if (waitpid(process, &status, 0) == -1) { + SDL_SetError("waitpid failed"); + return 0; + } + + if (WIFEXITED(status)) { + status = WEXITSTATUS(status); + } + + return !status; + } +} diff --git a/src/dialog/unix/SDL_zenitydialog.h b/src/dialog/unix/SDL_zenitydialog.h new file mode 100644 index 000000000..9d7203b67 --- /dev/null +++ b/src/dialog/unix/SDL_zenitydialog.h @@ -0,0 +1,29 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 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_Zenity_ShowOpenFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location, SDL_bool allow_many); +void SDL_Zenity_ShowSaveFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location); +void SDL_Zenity_ShowOpenFolderDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const char* default_location, SDL_bool allow_many); + +/** @returns non-zero if available, zero if unavailable */ +int SDL_Zenity_detect(void); diff --git a/src/dialog/windows/SDL_windowsdialog.c b/src/dialog/windows/SDL_windowsdialog.c new file mode 100644 index 000000000..0a58e02ce --- /dev/null +++ b/src/dialog/windows/SDL_windowsdialog.c @@ -0,0 +1,460 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 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" + +/* TODO: Macro? */ + +/* TODO: Better includes? */ +#include +#include +#include "../../core/windows/SDL_windows.h" +#include "../../thread/SDL_systhread.h" + +typedef struct +{ + int is_save; + const SDL_DialogFileFilter *filters; + const char* default_file; + const char* default_folder; + SDL_Window* parent; + DWORD flags; + SDL_DialogFileCallback callback; + void* userdata; +} winArgs; + +typedef struct +{ + SDL_Window* parent; + SDL_DialogFileCallback callback; + void* userdata; +} winFArgs; + +/** Converts dialog.nFilterIndex to SDL-compatible value */ +int getFilterIndex(int as_reported_by_windows, const SDL_DialogFileFilter *filters) +{ + int filter_index = as_reported_by_windows - 1; + + if (filter_index < 0) { + filter_index = 0; + for (const SDL_DialogFileFilter *filter = filters; filter && filter->name && filter->pattern; filter++) { + filter_index++; + } + } + + return filter_index; +} + +/* TODO: The new version of file dialogs */ +void windows_ShowFileDialog(void *ptr) +{ + winArgs *args = (winArgs *) ptr; + int is_save = args->is_save; + const SDL_DialogFileFilter *filters = args->filters; + const char* default_file = args->default_file; + const char* default_folder = args->default_folder; + SDL_Window* parent = args->parent; + DWORD flags = args->flags; + SDL_DialogFileCallback callback = args->callback; + void* userdata = args->userdata; + + /* 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_GetProperty(SDL_GetWindowProperties(parent), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); + } + + wchar_t filebuffer[MAX_PATH] = L""; + wchar_t initfolder[MAX_PATH] = L""; + + /* Necessary for the return code below */ + SDL_memset(filebuffer, 0, MAX_PATH * sizeof(wchar_t)); + + if (default_file) { + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, default_file, -1, filebuffer, MAX_PATH); + } + + if (default_folder) { + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, default_folder, -1, filebuffer, MAX_PATH); + } + + size_t len = 0; + for (const SDL_DialogFileFilter *filter = filters; filter && filter->name && filter->pattern; filter++) { + const char *pattern_ptr = filter->pattern; + len += SDL_strlen(filter->name) + SDL_strlen(filter->pattern) + 4; + while (*pattern_ptr) { + if (*pattern_ptr == ';') { + len += 2; + } + pattern_ptr++; + } + } + wchar_t *filterlist = SDL_malloc((len + 1) * sizeof(wchar_t)); + + if (!filterlist) { + SDL_OutOfMemory(); + callback(userdata, NULL, -1); + return; + } + + wchar_t *filter_ptr = filterlist; + for (const SDL_DialogFileFilter *filter = filters; filter && filter->name && filter->pattern; filter++) { + size_t l = SDL_strlen(filter->name); + const char *pattern_ptr = filter->pattern; + + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, filter->name, -1, filter_ptr, MAX_PATH); + filter_ptr += l + 1; + + *filter_ptr++ = L'*'; + *filter_ptr++ = L'.'; + while (*pattern_ptr) { + if (*pattern_ptr == ';') { + *filter_ptr++ = L';'; + *filter_ptr++ = L'*'; + *filter_ptr++ = L'.'; + } else if (*pattern_ptr == '*' && (pattern_ptr[1] == '\0' || pattern_ptr[1] == ';')) { + *filter_ptr++ = L'*'; + } else if (!((*pattern_ptr >= 'a' && *pattern_ptr <= 'z') || (*pattern_ptr >= 'A' && *pattern_ptr <= 'Z') || (*pattern_ptr >= '0' && *pattern_ptr <= '9') || *pattern_ptr == '.' || *pattern_ptr == '_' || *pattern_ptr == '-')) { + SDL_SetError("Illegal character in pattern name: %c (Only alphanumeric characters, periods, underscores and hyphens allowed)", *pattern_ptr); + callback(userdata, NULL, -1); + } else { + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, pattern_ptr, 1, filter_ptr, 1); + filter_ptr++; + } + pattern_ptr++; + } + *filter_ptr++ = '\0'; + } + *filter_ptr = '\0'; + + + OPENFILENAMEW dialog; + dialog.lStructSize = sizeof(OPENFILENAME); + dialog.hwndOwner = window; + dialog.hInstance = 0; + dialog.lpstrFilter = filterlist; + dialog.lpstrCustomFilter = NULL; + dialog.nMaxCustFilter = 0; + dialog.nFilterIndex = 0; + dialog.lpstrFile = filebuffer; + dialog.nMaxFile = MAX_PATH; + dialog.lpstrFileTitle = *filebuffer ? filebuffer : NULL; + dialog.nMaxFileTitle = MAX_PATH; + dialog.lpstrInitialDir = *initfolder ? initfolder : NULL; + dialog.lpstrTitle = NULL; + dialog.Flags = flags | OFN_EXPLORER | OFN_HIDEREADONLY; + 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(filterlist); + + 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, filters)); + 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) { + SDL_OutOfMemory(); + callback(userdata, NULL, -1); + 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); + 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) { + SDL_OutOfMemory(); + + for (size_t i = 0; i < nfiles - 1; i++) { + SDL_free(chosen_files_list[i]); + } + + SDL_free(chosen_files_list); + callback(userdata, NULL, -1); + 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); + return; + } + + file_ptr += SDL_strlen(chosen_file) + 1 - diff; + + chosen_files_list[nfiles - 1] = SDL_strdup(chosen_file); + + if (!chosen_files_list[nfiles - 1]) { + SDL_OutOfMemory(); + + for (size_t i = 0; i < nfiles - 1; i++) { + SDL_free(chosen_files_list[i]); + } + + SDL_free(chosen_files_list); + callback(userdata, NULL, -1); + return; + } + } + + callback(userdata, (const char * const*) chosen_files_list, getFilterIndex(dialog.nFilterIndex, filters)); + + 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, filters)); + } else { + SDL_SetError("Windows error, CommDlgExtendedError: %ld", pCommDlgExtendedError()); + callback(userdata, NULL, -1); + } + } +} + +int windows_file_dialog_thread(void* ptr) +{ + windows_ShowFileDialog(ptr); + SDL_free(ptr); + 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; + + if (window) { + parent = (HWND) SDL_GetProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); + } + + wchar_t buffer[MAX_PATH]; + + BROWSEINFOW dialog; + dialog.hwndOwner = parent; + dialog.pidlRoot = NULL; + /* Windows docs say this is `LPTSTR` - apparently it's actually `LPWSTR`*/ + dialog.pszDisplayName = buffer; + dialog.lpszTitle = NULL; + dialog.ulFlags = BIF_USENEWUI; + dialog.lpfn = NULL; + dialog.lParam = 0; + dialog.iImage = 0; + + if (SHBrowseForFolderW(&dialog)) { + 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); + SDL_free(ptr); + return 0; +} + +void SDL_ShowOpenFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location, SDL_bool allow_many) +{ + winArgs *args; + SDL_Thread *thread; + + args = SDL_malloc(sizeof(winArgs)); + if (args == NULL) { + SDL_OutOfMemory(); + callback(userdata, NULL, -1); + return; + } + + args->is_save = 0; + args->filters = filters; + args->default_file = default_location; + args->default_folder = NULL; + args->parent = window; + args->flags = (allow_many == SDL_TRUE) ? OFN_ALLOWMULTISELECT : 0; + args->callback = callback; + args->userdata = userdata; + + thread = SDL_CreateThreadInternal(windows_file_dialog_thread, "SDL_ShowOpenFileDialog", 0, (void *) args); + + if (thread == NULL) { + callback(userdata, NULL, -1); + return; + } + + SDL_DetachThread(thread); +} + +void SDL_ShowSaveFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const SDL_DialogFileFilter *filters, const char* default_location) +{ + winArgs *args; + SDL_Thread *thread; + + args = SDL_malloc(sizeof(winArgs)); + if (args == NULL) { + SDL_OutOfMemory(); + callback(userdata, NULL, -1); + return; + } + + args->is_save = 1; + args->filters = filters; + args->default_file = default_location; + args->default_folder = NULL; + args->parent = window; + args->flags = 0; + args->callback = callback; + args->userdata = userdata; + + thread = SDL_CreateThreadInternal(windows_file_dialog_thread, "SDL_ShowSaveFileDialog", 0, (void *) args); + + if (thread == NULL) { + callback(userdata, NULL, -1); + return; + } + + SDL_DetachThread(thread); +} + +void SDL_ShowOpenFolderDialog(SDL_DialogFileCallback callback, void* userdata, SDL_Window* window, const char* default_location, SDL_bool allow_many) +{ + winFArgs *args; + SDL_Thread *thread; + + args = SDL_malloc(sizeof(winFArgs)); + if (args == NULL) { + SDL_OutOfMemory(); + callback(userdata, NULL, -1); + return; + } + + args->parent = window; + args->callback = callback; + args->userdata = userdata; + + thread = SDL_CreateThreadInternal(windows_folder_dialog_thread, "SDL_ShowOpenFolderDialog", 0, (void *) args); + + if (thread == NULL) { + callback(userdata, NULL, -1); + return; + } + + SDL_DetachThread(thread); +} diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index 67dec60c3..9e3c0f7a8 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -976,6 +976,9 @@ SDL3_0.0.0 { SDL_AddVulkanRenderSemaphores; SDL_GetNumJoystickBalls; SDL_GetJoystickBall; + SDL_ShowOpenFileDialog; + SDL_ShowSaveFileDialog; + SDL_ShowOpenFolderDialog; # extra symbols go here (don't modify this line) local: *; }; diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h index 5ee503eb9..14eaed1d3 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -1001,3 +1001,6 @@ #define SDL_AddVulkanRenderSemaphores SDL_AddVulkanRenderSemaphores_REAL #define SDL_GetNumJoystickBalls SDL_GetNumJoystickBalls_REAL #define SDL_GetJoystickBall SDL_GetJoystickBall_REAL +#define SDL_ShowOpenFileDialog SDL_ShowOpenFileDialog_REAL +#define SDL_ShowSaveFileDialog SDL_ShowSaveFileDialog_REAL +#define SDL_ShowOpenFolderDialog SDL_ShowOpenFolderDialog_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index 77494c653..e08c5f2d2 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -1026,3 +1026,6 @@ SDL_DYNAPI_PROC(void*,SDL_bsearch_r,(const void *a, const void *b, size_t c, siz SDL_DYNAPI_PROC(int,SDL_AddVulkanRenderSemaphores,(SDL_Renderer *a, Uint32 b, Sint64 c, Sint64 d),(a,b,c,d),return) SDL_DYNAPI_PROC(int,SDL_GetNumJoystickBalls,(SDL_Joystick *a),(a),return) SDL_DYNAPI_PROC(int,SDL_GetJoystickBall,(SDL_Joystick *a, int b, int *c, int *d),(a,b,c,d),return) +SDL_DYNAPI_PROC(void,SDL_ShowOpenFileDialog,(SDL_DialogFileCallback a, void *b, SDL_Window *c, const SDL_DialogFileFilter *d, const char *e, int f),(a,b,c,d,e,f),) +SDL_DYNAPI_PROC(void,SDL_ShowSaveFileDialog,(SDL_DialogFileCallback a, void *b, SDL_Window *c, const SDL_DialogFileFilter *d, const char *e),(a,b,c,d,e),) +SDL_DYNAPI_PROC(void,SDL_ShowOpenFolderDialog,(SDL_DialogFileCallback a, void *b, SDL_Window *c, const char *d, int e),(a,b,c,d,e),) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7bc3049da..9efaf646f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -412,6 +412,7 @@ add_sdl_test_executable(testcustomcursor SOURCES testcustomcursor.c) add_sdl_test_executable(testvulkan NO_C90 SOURCES testvulkan.c) add_sdl_test_executable(testoffscreen SOURCES testoffscreen.c) add_sdl_test_executable(testpopup SOURCES testpopup.c) +add_sdl_test_executable(testdialog SOURCES testdialog.c) if (HAVE_WAYLAND) # Set the GENERATED property on the protocol file, since it is first created at build time diff --git a/test/testdialog.c b/test/testdialog.c new file mode 100644 index 000000000..26be7b640 --- /dev/null +++ b/test/testdialog.c @@ -0,0 +1,161 @@ +/* + Copyright (C) 1997-2024 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. +*/ +/* Sample program: Create open and save dialogs. */ + +#include +#include +#include + +const SDL_DialogFileFilter filters[4] = { + { "All files", "*" }, + { "JPG images", "jpg;jpeg" }, + { "PNG images", "png" }, + { NULL, NULL } +}; + +static void SDLCALL callback(void* userdata, const char* const* files, int filter) { + if (files) { + const char* filter_name = "(filter fetching unsupported)"; + + if (filter != -1) { + if (filter < sizeof(filters) / sizeof(*filters)) { + filter_name = filters[filter].name; + } else { + filter_name = "(No filter was selected)"; + } + } + + SDL_Log("Filter used: '%s'\n", filter_name); + + while (*files) { + SDL_Log("'%s'\n", *files); + files++; + } + } else { + SDL_Log("Error: %s\n", SDL_GetError()); + } +} + +int main(int argc, char *argv[]) { + SDL_Window *w; + SDL_Renderer *r; + SDLTest_CommonState *state; + const SDL_FRect open_file_rect = { 50, 50, 220, 140 }; + const SDL_FRect save_file_rect = { 50, 290, 220, 140 }; + const SDL_FRect open_folder_rect = { 370, 50, 220, 140 }; + int i; + char *initial_path = NULL; + char path_with_trailing_slash[2048]; + + /* Initialize test framework */ + state = SDLTest_CommonCreateState(argv, 0); + if (state == NULL) { + return 1; + } + + /* Enable standard application logging */ + SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO); + + /* Parse commandline */ + for (i = 1; i < argc;) { + int consumed; + + consumed = SDLTest_CommonArg(state, i); + if (!consumed) { + } + if (consumed <= 0) { + static const char *options[] = { NULL }; + SDLTest_CommonLogUsage(state, argv[0], options); + return 1; + } + + i += consumed; + } + + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + SDL_Log("SDL_Init failed (%s)", SDL_GetError()); + return 1; + } + if (SDL_CreateWindowAndRenderer(640, 480, 0, &w, &r) < 0) { + SDL_Log("Failed to create window and/or renderer: %s\n", SDL_GetError()); + return 1; + } + + initial_path = SDL_GetUserFolder(SDL_FOLDER_HOME); + + if (!initial_path) { + SDL_Log("Will not use an initial path, couldn't get the home directory path: %s\n", SDL_GetError()); + path_with_trailing_slash[0] = '\0'; + } else { + SDL_snprintf(path_with_trailing_slash, sizeof(path_with_trailing_slash), "%s/", initial_path); + SDL_free(initial_path); + } + + while (1) { + int quit = 0; + SDL_Event e; + while (SDL_PollEvent(&e)) { + if (e.type == SDL_EVENT_QUIT) { + quit = 1; + break; + } else if (e.type == SDL_EVENT_MOUSE_BUTTON_UP) { + const SDL_FPoint p = { e.button.x, e.button.y }; + /* + * Arguments, in order: + * - A function to call when files are chosen (or dialog is canceled, or error happens) + * - A user-managed void pointer to pass to the function when it will be invoked + * - The window to bind the dialog to, or NULL if none + * - A list of filters for the files, see SDL_DialogFileFilter above (not for SDL_ShowOpenFolderDialog) + * - The path where the dialog should start. May be a folder or a file + * - Nonzero if the user is allowed to choose multiple entries (not for SDL_ShowSaveFileDialog) + */ + if (SDL_PointInRectFloat(&p, &open_file_rect)) { + SDL_ShowOpenFileDialog(callback, NULL, w, filters, path_with_trailing_slash, 1); + } else if (SDL_PointInRectFloat(&p, &open_folder_rect)) { + SDL_ShowOpenFolderDialog(callback, NULL, w, path_with_trailing_slash, 1); + } else if (SDL_PointInRectFloat(&p, &save_file_rect)) { + SDL_ShowSaveFileDialog(callback, NULL, w, filters, path_with_trailing_slash); + } + } + } + if (quit) { + break; + } + SDL_Delay(100); + + SDL_SetRenderDrawColor(r, 0, 0, 0, SDL_ALPHA_OPAQUE); + SDL_RenderClear(r); + + SDL_SetRenderDrawColor(r, 255, 0, 0, SDL_ALPHA_OPAQUE); + SDL_RenderFillRect(r, &open_file_rect); + + SDL_SetRenderDrawColor(r, 0, 255, 0, SDL_ALPHA_OPAQUE); + SDL_RenderFillRect(r, &save_file_rect); + + SDL_SetRenderDrawColor(r, 0, 0, 255, SDL_ALPHA_OPAQUE); + SDL_RenderFillRect(r, &open_folder_rect); + + SDL_SetRenderDrawColor(r, 0, 0, 0, SDL_ALPHA_OPAQUE); + SDLTest_DrawString(r, open_file_rect.x+5, open_file_rect.y+open_file_rect.h/2, "Open File..."); + SDLTest_DrawString(r, save_file_rect.x+5, save_file_rect.y+save_file_rect.h/2, "Save File..."); + SDLTest_DrawString(r, open_folder_rect.x+5, open_folder_rect.y+open_folder_rect.h/2, "Open Folder..."); + + SDL_RenderPresent(r); + } + + SDL_DestroyRenderer(r); + SDL_DestroyWindow(w); + SDLTest_CleanupTextDrawing(); + SDL_Quit(); + SDLTest_CommonDestroyState(state); + return 0; +}