Bladeren bron

File dialog improvements

- Add a globally-accessible function to handle the parsing of filter extensions
- Remove the ability of putting the wildcard ('*') among other patterns; it's either a list of patterns or a single '*' now
- Add a hint to select between portals and Zenity on Unix
Semphris 1 jaar geleden
bovenliggende
commit
6ad390fc50

+ 1 - 0
CMakeLists.txt

@@ -2873,6 +2873,7 @@ elseif(N3DS)
 endif()
 
 if (SDL_DIALOG)
+  sdl_sources(${SDL3_SOURCE_DIR}/src/dialog/SDL_dialog_utils.c)
   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)

+ 1 - 0
VisualC-GDK/SDL/SDL.vcxproj

@@ -507,6 +507,7 @@
     </ClCompile>
     <ClCompile Include="..\..\src\camera\dummy\SDL_camera_dummy.c" />
     <ClCompile Include="..\..\src\camera\SDL_camera.c" />
+    <ClCompile Include="..\..\src\dialog\SDL_dialog_utils.c" />
     <ClCompile Include="..\..\src\filesystem\SDL_filesystem.c" />
     <ClCompile Include="..\..\src\filesystem\windows\SDL_sysfsops.c" />
     <ClCompile Include="..\..\src\main\generic\SDL_sysmain_callbacks.c" />

+ 3 - 0
VisualC-GDK/SDL/SDL.vcxproj.filters

@@ -4,6 +4,9 @@
     <ClCompile Include="..\..\src\core\gdk\SDL_gdk.cpp" />
     <ClCompile Include="..\..\src\core\windows\pch.c" />
     <ClCompile Include="..\..\src\core\windows\pch_cpp.cpp" />
+    <ClCompile Include="..\..\src\dialog\SDL_dialog_utils.c">
+      <Filter>dialog</Filter>
+    </ClCompile>
     <ClCompile Include="..\..\src\filesystem\SDL_filesystem.c">
       <Filter>filesystem</Filter>
     </ClCompile>

+ 1 - 0
VisualC-WinRT/SDL-UWP.vcxproj

@@ -315,6 +315,7 @@
       <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
       <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
     </ClCompile>
+    <ClCompile Include="..\src\dialog\SDL_dialog_utils.c" />
     <ClCompile Include="..\src\events\SDL_clipboardevents.c" />
     <ClCompile Include="..\src\events\SDL_displayevents.c" />
     <ClCompile Include="..\src\events\SDL_dropevents.c" />

+ 6 - 0
VisualC-WinRT/SDL-UWP.vcxproj.filters

@@ -31,6 +31,9 @@
     <Filter Include="time\windows">
       <UniqueIdentifier>{0000012051ca8361c8e1013aee1d0000}</UniqueIdentifier>
     </Filter>
+    <Filter Include="dialog">
+      <UniqueIdentifier>{0000c99bfadbbcb05a474a8472910000}</UniqueIdentifier>
+    </Filter>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="..\include\SDL3\SDL_begin_code.h">
@@ -567,6 +570,9 @@
     <ClCompile Include="..\src\cpuinfo\SDL_cpuinfo.c">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\src\dialog\SDL_dialog_utils.c">
+      <Filter>dialog</Filter>
+    </ClCompile>
     <ClCompile Include="..\src\dynapi\SDL_dynapi.c">
       <Filter>Source Files</Filter>
     </ClCompile>

+ 1 - 0
VisualC/SDL/SDL.vcxproj

@@ -404,6 +404,7 @@
     <ClCompile Include="..\..\src\camera\dummy\SDL_camera_dummy.c" />
     <ClCompile Include="..\..\src\camera\mediafoundation\SDL_camera_mediafoundation.c" />
     <ClCompile Include="..\..\src\camera\SDL_camera.c" />
+    <ClCompile Include="..\..\src\dialog\SDL_dialog_utils.c" />
     <ClCompile Include="..\..\src\filesystem\SDL_filesystem.c" />
     <ClCompile Include="..\..\src\filesystem\windows\SDL_sysfsops.c" />
     <ClCompile Include="..\..\src\main\generic\SDL_sysmain_callbacks.c" />

+ 6 - 0
VisualC/SDL/SDL.vcxproj.filters

@@ -196,6 +196,9 @@
     <Filter Include="time\windows">
       <UniqueIdentifier>{0000d7fda065b13b0ca4ab262c380000}</UniqueIdentifier>
     </Filter>
+    <Filter Include="dialog">
+      <UniqueIdentifier>{00008dfdfa0190856fbf3c7db52d0000}</UniqueIdentifier>
+    </Filter>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="..\..\include\SDL3\SDL_begin_code.h">
@@ -883,6 +886,9 @@
     <ClCompile Include="..\..\src\camera\SDL_camera.c">
       <Filter>camera</Filter>
     </ClCompile>
+    <ClCompile Include="..\..\src\dialog\SDL_dialog_utils.c">
+      <Filter>dialog</Filter>
+    </ClCompile>
     <ClCompile Include="..\..\src\filesystem\SDL_filesystem.c">
       <Filter>filesystem</Filter>
     </ClCompile>

+ 4 - 0
Xcode/SDL/SDL.xcodeproj/project.pbxproj

@@ -513,6 +513,7 @@
 		F3FA5A242B59ACE000FEAD97 /* yuv_rgb_lsx.h in Headers */ = {isa = PBXBuildFile; fileRef = F3FA5A1B2B59ACE000FEAD97 /* yuv_rgb_lsx.h */; };
 		F3FA5A252B59ACE000FEAD97 /* yuv_rgb_common.h in Headers */ = {isa = PBXBuildFile; fileRef = F3FA5A1C2B59ACE000FEAD97 /* yuv_rgb_common.h */; };
 		FA73671D19A540EF004122E4 /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA73671C19A540EF004122E4 /* CoreVideo.framework */; platformFilters = (ios, maccatalyst, macos, tvos, watchos, ); };
+		0000140640E77F73F1DF0000 /* SDL_dialog_utils.c in Sources */ = {isa = PBXBuildFile; fileRef = 0000F6C6A072ED4E3D660000 /* SDL_dialog_utils.c */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -1054,6 +1055,7 @@
 		F59C710600D5CB5801000001 /* SDL.info */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = text; path = SDL.info; sourceTree = "<group>"; };
 		F5A2EF3900C6A39A01000001 /* BUGS.txt */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = text; name = BUGS.txt; path = ../../BUGS.txt; sourceTree = SOURCE_ROOT; };
 		FA73671C19A540EF004122E4 /* CoreVideo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreVideo.framework; path = System/Library/Frameworks/CoreVideo.framework; sourceTree = SDKROOT; };
+		0000F6C6A072ED4E3D660000 /* SDL_dialog_utils.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = SDL_dialog_utils.c; path = SDL_dialog_utils.c; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -2233,6 +2235,7 @@
 			children = (
 				F37E18552BA50ED50098C111 /* cocoa */,
 				F37E18562BA50F2A0098C111 /* dummy */,
+				0000F6C6A072ED4E3D660000 /* SDL_dialog_utils.c */,
 			);
 			path = dialog;
 			sourceTree = "<group>";
@@ -2872,6 +2875,7 @@
 				0000481D255AF155B42C0000 /* SDL_sysfsops.c in Sources */,
 				0000494CC93F3E624D3C0000 /* SDL_systime.c in Sources */,
 				000095FA1BDE436CF3AF0000 /* SDL_time.c in Sources */,
+				0000140640E77F73F1DF0000 /* SDL_dialog_utils.c in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 3 - 1
include/SDL3/SDL_dialog.h

@@ -37,7 +37,9 @@ extern "C" {
  * `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").
+ * "doc;docx"). File extensions may only contain alphanumeric characters,
+ * hyphens, underscores and periods. Alternatively, the whole string can be a
+ * single asterisk ("*"), which serves as an "All files" filter.
  *
  * \sa SDL_DialogFileCallback
  * \sa SDL_ShowOpenFileDialog

+ 20 - 0
include/SDL3/SDL_hints.h

@@ -414,6 +414,26 @@ extern "C" {
  */
 #define SDL_HINT_JOYSTICK_DIRECTINPUT "SDL_JOYSTICK_DIRECTINPUT"
 
+/**
+ * A variable that specifies a dialog backend to use.
+ *
+ * By default, SDL will try all available dialog backends in a reasonable order until it finds one that can work, but this hint allows the app or user to force a specific target.
+ *
+ * If the specified target does not exist or is not available, the dialog-related function calls will fail.
+ *
+ * This hint currently only applies to platforms using the generic "Unix" dialog implementation, but may be extended to more platforms in the future. Note that some Unix and Unix-like platforms have their own implementation, such as macOS and Haiku.
+ *
+ * The variable can be set to the following values:
+ *   NULL          - Select automatically (default, all platforms)
+ *   "portal"      - Use XDG Portals through DBus (Unix only)
+ *   "zenity"      - Use the Zenity program (Unix only)
+ *
+ * More options may be added in the future.
+ *
+ * This hint can be set anytime.
+ */
+#define SDL_HINT_FILE_DIALOG_DRIVER "SDL_FILE_DIALOG_DRIVER"
+
 /**
  * Override for SDL_GetDisplayUsableBounds()
  *

+ 237 - 0
src/dialog/SDL_dialog_utils.c

@@ -0,0 +1,237 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
+
+  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, 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;
+    int new_length;
+
+    combined = SDL_strdup(prefix);
+
+    if (!combined) {
+        SDL_OutOfMemory();
+        return NULL;
+    }
+
+    for (const SDL_DialogFileFilter *f = filters; f->name; f++) {
+        converted = convert_filter(*f, ntf, filt_prefix, filt_separator,
+                                   filt_suffix, ext_prefix, ext_separator,
+                                   ext_suffix);
+
+        if (!converted) {
+            return NULL;
+        }
+
+        terminator = f[1].name ? separator : suffix;
+        new_length = SDL_strlen(combined) + SDL_strlen(converted)
+                   + SDL_strlen(terminator);
+
+        new_combined = SDL_realloc(combined, new_length);
+
+        if (!new_combined) {
+            SDL_free(converted);
+            SDL_free(combined);
+            SDL_OutOfMemory();
+            return NULL;
+        }
+
+        combined = new_combined;
+
+        SDL_strlcat(combined, converted, new_length);
+        SDL_strlcat(combined, terminator, new_length);
+    }
+
+    return combined;
+}
+
+char *convert_filter(const 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;
+    int 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);
+        SDL_OutOfMemory();
+        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;
+    int 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) {
+        SDL_OutOfMemory();
+        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)
+{
+    if (filters) {
+        for (const SDL_DialogFileFilter *f = filters; f->name; f++) {
+             const char *msg = validate_list(f->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;
+}

+ 57 - 0
src/dialog/SDL_dialog_utils.h

@@ -0,0 +1,57 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
+
+  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. */
+/* <prefix>[filter]{<separator>[filter]...}<suffix> */
+char *convert_filters(const SDL_DialogFileFilter *filters, 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. */
+/* <prefix>[filter name]<separator>[filter extension list]<suffix> */
+char *convert_filter(const 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 extenstion list of a filter into a single string. */
+/* <prefix>[extension]{<separator>[extension]...}<suffix> */
+char *convert_ext_list(const char *list, const char *prefix,
+                       const char *suffix, const char *separator);
+
+/* 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);
+const char *validate_list(const char *list);

+ 15 - 4
src/dialog/cocoa/SDL_cocoadialog.m

@@ -19,6 +19,7 @@
   3. This notice may not be removed or altered from any source distribution.
 */
 #include "SDL_internal.h"
+#include "../SDL_dialog_utils.h"
 
 #import <Cocoa/Cocoa.h>
 #import <UniformTypeIdentifiers/UTType.h>
@@ -36,6 +37,20 @@ void show_file_dialog(cocoa_FileDialogType type, SDL_DialogFileCallback callback
     SDL_SetError("tvOS and iOS don't support path-based file dialogs");
     callback(userdata, NULL, -1);
 #else
+    const char *msg = validate_filters(filters);
+
+    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;
+    }
+
     /* NSOpenPanel inherits from NSSavePanel */
     NSSavePanel *dialog;
     NSOpenPanel *dialog_as_open;
@@ -83,10 +98,6 @@ void show_file_dialog(cocoa_FileDialogType type, SDL_DialogFileCallback callback
                     [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;
             }

+ 29 - 3
src/dialog/haiku/SDL_haikudialog.cc

@@ -19,6 +19,9 @@
   3. This notice may not be removed or altered from any source distribution.
 */
 #include "SDL_internal.h"
+extern "C" {
+#include "../SDL_dialog_utils.h"
+}
 #include "../../core/haiku/SDL_BeApp.h"
 
 #include <string>
@@ -197,10 +200,33 @@ void ShowDialog(bool save, SDL_DialogFileCallback callback, void *userdata, bool
         return;
     }
 
+    const char *msg = validate_filters(filters);
+
+    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 CallbackLooper(callback, userdata);
-    BMessenger *messenger = new BMessenger(NULL, looper);
-    SDLBRefFilter *filter = new SDLBRefFilter(filters);
+    CallbackLooper *looper = new(std::nothrow) CallbackLooper(callback, userdata);
+    BMessenger *messenger = new(std::nothrow) BMessenger(NULL, looper);
+    SDLBRefFilter *filter = new(std::nothrow) SDLBRefFilter(filters);
+
+    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;

+ 9 - 1
src/dialog/unix/SDL_portaldialog.c

@@ -19,7 +19,7 @@
   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 "../../core/linux/SDL_dbus.h"
 
@@ -270,6 +270,14 @@ static void DBus_OpenDialog(const char *method, const char *method_title, SDL_Di
     static char *default_parent_window = "";
     SDL_PropertiesID props = SDL_GetWindowProperties(window);
 
+    const char *err_msg = validate_filters(filters);
+
+    if (err_msg) {
+        SDL_SetError("%s", err_msg);
+        callback(userdata, NULL, -1);
+        return;
+    }
+
     if (dbus == NULL) {
         SDL_SetError("Failed to connect to DBus");
         return;

+ 40 - 15
src/dialog/unix/SDL_unixdialog.c

@@ -27,31 +27,56 @@ static void (*detected_open)(SDL_DialogFileCallback callback, void* userdata, SD
 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;
 
+static int detect_available_methods(const char *value);
+
+void SDLCALL hint_callback(void *userdata, const char *name, const char *oldValue, const char *newValue)
+{
+    detect_available_methods(newValue);
+}
+
+static void set_callback(void)
+{
+    static SDL_bool is_set = SDL_FALSE;
+
+    if (is_set == SDL_FALSE) {
+        is_set = SDL_TRUE;
+        SDL_AddHintCallback(SDL_HINT_FILE_DIALOG_DRIVER, hint_callback, NULL);
+    }
+}
+
 /* Returns non-zero on success, 0 on failure */
-static int detect_available_methods(void)
+static int detect_available_methods(const char *value)
 {
-    if (SDL_Portal_detect()) {
-        detected_open = SDL_Portal_ShowOpenFileDialog;
-        detected_save = SDL_Portal_ShowSaveFileDialog;
-        detected_folder = SDL_Portal_ShowOpenFolderDialog;
-        return 1;
+    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_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;
+    if (driver == NULL || SDL_strcmp(driver, "zenity") == 0) {
+        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");
+    SDL_SetError("File dialog driver unsupported");
     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()) {
+    if (!detected_open && !detect_available_methods(NULL)) {
         /* SetError() done by detect_available_methods() */
         callback(userdata, NULL, -1);
         return;
@@ -63,7 +88,7 @@ void SDL_ShowOpenFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL
 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()) {
+    if (!detected_save && !detect_available_methods(NULL)) {
         /* SetError() done by detect_available_methods() */
         callback(userdata, NULL, -1);
         return;
@@ -75,7 +100,7 @@ void SDL_ShowSaveFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL
 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()) {
+    if (!detected_folder && !detect_available_methods(NULL)) {
         /* SetError() done by detect_available_methods() */
         callback(userdata, NULL, -1);
         return;

+ 22 - 59
src/dialog/unix/SDL_zenitydialog.c

@@ -19,7 +19,7 @@
   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 <errno.h>
 #include <sys/types.h>
@@ -65,6 +65,22 @@ typedef struct
         }                                                                     \
     }
 
+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;
+}
+
 /* Exec call format:
  *
  *     /usr/bin/env zenity --file-selection --separator=\n [--multiple]
@@ -147,68 +163,15 @@ static char** generate_args(const zenityArgs* info)
         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;
-                    }
+            char *filter_str = convert_filter(*filter_ptr, zenity_clean_name,
+                                              "--file-filter=", " | ", "",
+                                              "*.", " *.", "");
 
-                    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);
+            if (!filter_str) {
                 CLEAR_AND_RETURN()
             }
 
-            buffer[i_buf] = '\0';
-
-            argv[nextarg++] = SDL_strdup(buffer);
+            argv[nextarg++] = filter_str;
             CHECK_OOM()
 
             filter_ptr++;

+ 46 - 42
src/dialog/windows/SDL_windowsdialog.c

@@ -19,6 +19,7 @@
   3. This notice may not be removed or altered from any source distribution.
 */
 #include "SDL_internal.h"
+#include "../SDL_dialog_utils.h"
 
 #include <windows.h>
 #include <shlobj.h>
@@ -122,61 +123,42 @@ void windows_ShowFileDialog(void *ptr)
         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));
+    /* '\x01' is used in place of a null byte */
+    char *filterlist = convert_filters(filters, NULL, "", "", "\x01", "",
+                                       "\x01", "\x01", "*.", ";*.", "");
 
     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++;
+    int filter_len = SDL_strlen(filterlist);
+
+    for (char *c = filterlist; *c; c++) {
+        if (*c == '\x01') {
+            *c = '\0';
         }
-        *filter_ptr++ = '\0';
     }
-    *filter_ptr = '\0';
 
+    int filter_wlen = MultiByteToWideChar(CP_UTF8, 0, filterlist, filter_len, NULL, 0);
+    wchar_t *filter_wchar = SDL_malloc(filter_wlen * sizeof(wchar_t));
+
+    if (!filter_wchar) {
+        SDL_OutOfMemory();
+        SDL_free(filterlist);
+        callback(userdata, NULL, -1);
+        return;
+    }
+
+    MultiByteToWideChar(CP_UTF8, 0, filterlist, filter_len, filter_wchar, filter_wlen);
+
+    SDL_free(filterlist);
 
     OPENFILENAMEW dialog;
     dialog.lStructSize = sizeof(OPENFILENAME);
     dialog.hwndOwner = window;
     dialog.hInstance = 0;
-    dialog.lpstrFilter = filterlist;
+    dialog.lpstrFilter = filter_wchar;
     dialog.lpstrCustomFilter = NULL;
     dialog.nMaxCustFilter = 0;
     dialog.nFilterIndex = 0;
@@ -198,7 +180,7 @@ void windows_ShowFileDialog(void *ptr)
 
     BOOL result = pGetAnyFileName(&dialog);
 
-    SDL_free(filterlist);
+    SDL_free(filter_wchar);
 
     if (result) {
         if (!(flags & OFN_ALLOWMULTISELECT)) {
@@ -401,6 +383,13 @@ void SDL_ShowOpenFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL
     winArgs *args;
     SDL_Thread *thread;
 
+    if (SDL_GetHint(SDL_HINT_FILE_DIALOG_DRIVER) != NULL) {
+        SDL_Log("%s", SDL_GetHint(SDL_HINT_FILE_DIALOG_DRIVER));
+        SDL_SetError("File dialog driver unsupported");
+        callback(userdata, NULL, -1);
+        return;
+    }
+
     args = SDL_malloc(sizeof(winArgs));
     if (args == NULL) {
         SDL_OutOfMemory();
@@ -421,6 +410,7 @@ void SDL_ShowOpenFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL
 
     if (thread == NULL) {
         callback(userdata, NULL, -1);
+        SDL_free(args);
         return;
     }
 
@@ -432,6 +422,12 @@ void SDL_ShowSaveFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL
     winArgs *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 = SDL_malloc(sizeof(winArgs));
     if (args == NULL) {
         SDL_OutOfMemory();
@@ -452,6 +448,7 @@ void SDL_ShowSaveFileDialog(SDL_DialogFileCallback callback, void* userdata, SDL
 
     if (thread == NULL) {
         callback(userdata, NULL, -1);
+        SDL_free(args);
         return;
     }
 
@@ -463,6 +460,12 @@ void SDL_ShowOpenFolderDialog(SDL_DialogFileCallback callback, void* userdata, S
     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 = SDL_malloc(sizeof(winFArgs));
     if (args == NULL) {
         SDL_OutOfMemory();
@@ -479,6 +482,7 @@ void SDL_ShowOpenFolderDialog(SDL_DialogFileCallback callback, void* userdata, S
 
     if (thread == NULL) {
         callback(userdata, NULL, -1);
+        SDL_free(args);
         return;
     }