SDL: File dialog improvements

From 6ad390fc5093bcbf785b7ad103484501d11d8c2a Mon Sep 17 00:00:00 2001
From: Semphris <[EMAIL REDACTED]>
Date: Tue, 2 Apr 2024 16:03:58 -0400
Subject: [PATCH] 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
---
 CMakeLists.txt                          |   1 +
 VisualC-GDK/SDL/SDL.vcxproj             |   1 +
 VisualC-GDK/SDL/SDL.vcxproj.filters     |   3 +
 VisualC-WinRT/SDL-UWP.vcxproj           |   1 +
 VisualC-WinRT/SDL-UWP.vcxproj.filters   |   6 +
 VisualC/SDL/SDL.vcxproj                 |   1 +
 VisualC/SDL/SDL.vcxproj.filters         |   6 +
 Xcode/SDL/SDL.xcodeproj/project.pbxproj |   4 +
 include/SDL3/SDL_dialog.h               |   4 +-
 include/SDL3/SDL_hints.h                |  20 ++
 src/dialog/SDL_dialog_utils.c           | 237 ++++++++++++++++++++++++
 src/dialog/SDL_dialog_utils.h           |  57 ++++++
 src/dialog/cocoa/SDL_cocoadialog.m      |  19 +-
 src/dialog/haiku/SDL_haikudialog.cc     |  32 +++-
 src/dialog/unix/SDL_portaldialog.c      |  10 +-
 src/dialog/unix/SDL_unixdialog.c        |  55 ++++--
 src/dialog/unix/SDL_zenitydialog.c      |  81 +++-----
 src/dialog/windows/SDL_windowsdialog.c  |  88 ++++-----
 18 files changed, 501 insertions(+), 125 deletions(-)
 create mode 100644 src/dialog/SDL_dialog_utils.c
 create mode 100644 src/dialog/SDL_dialog_utils.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index c0f065c3f946d..0cd4903280b31 100644
--- a/CMakeLists.txt
+++ b/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)
diff --git a/VisualC-GDK/SDL/SDL.vcxproj b/VisualC-GDK/SDL/SDL.vcxproj
index d68a2f60ae272..f14f2186a6729 100644
--- a/VisualC-GDK/SDL/SDL.vcxproj
+++ b/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" />
diff --git a/VisualC-GDK/SDL/SDL.vcxproj.filters b/VisualC-GDK/SDL/SDL.vcxproj.filters
index 722b2ed3c1236..b6747abd15d18 100644
--- a/VisualC-GDK/SDL/SDL.vcxproj.filters
+++ b/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>
diff --git a/VisualC-WinRT/SDL-UWP.vcxproj b/VisualC-WinRT/SDL-UWP.vcxproj
index ba706bb950dab..3dbbe892ffa14 100644
--- a/VisualC-WinRT/SDL-UWP.vcxproj
+++ b/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" />
diff --git a/VisualC-WinRT/SDL-UWP.vcxproj.filters b/VisualC-WinRT/SDL-UWP.vcxproj.filters
index a8da66c1a79d6..092edfd292e45 100644
--- a/VisualC-WinRT/SDL-UWP.vcxproj.filters
+++ b/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>
diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj
index 7cc8405942042..c3143c47cffdf 100644
--- a/VisualC/SDL/SDL.vcxproj
+++ b/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" />
diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters
index 77b54b847a45f..66dab4deaa731 100644
--- a/VisualC/SDL/SDL.vcxproj.filters
+++ b/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>
diff --git a/Xcode/SDL/SDL.xcodeproj/project.pbxproj b/Xcode/SDL/SDL.xcodeproj/project.pbxproj
index 4ba76a901415f..b913cb7f895c0 100644
--- a/Xcode/SDL/SDL.xcodeproj/project.pbxproj
+++ b/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;
 		};
diff --git a/include/SDL3/SDL_dialog.h b/include/SDL3/SDL_dialog.h
index d4a7577cb2512..134c8194d18e5 100644
--- a/include/SDL3/SDL_dialog.h
+++ b/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
diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h
index 5c04617ad5907..5b34233d644bd 100644
--- a/include/SDL3/SDL_hints.h
+++ b/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()
  *
diff --git a/src/dialog/SDL_dialog_utils.c b/src/dialog/SDL_dialog_utils.c
new file mode 100644
index 0000000000000..5d29a23fe4194
--- /dev/null
+++ b/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;
+}
diff --git a/src/dialog/SDL_dialog_utils.h b/src/dialog/SDL_dialog_utils.h
new file mode 100644
index 0000000000000..ed9fd3111bc89
--- /dev/null
+++ b/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);
diff --git a/src/dialog/cocoa/SDL_cocoadialog.m b/src/dialog/cocoa/SDL_cocoadialog.m
index 970b1884f5ae9..be596cecc8e47 100644
--- a/src/dialog/cocoa/SDL_cocoadialog.m
+++ b/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;
             }
diff --git a/src/dialog/haiku/SDL_haikudialog.cc b/src/dialog/haiku/SDL_haikudialog.cc
index 3e63fc807266b..ac28415c615f4 100644
--- a/src/dialog/haiku/SDL_haikudialog.cc
+++ b/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;
diff --git a/src/dialog/unix/SDL_portaldialog.c b/src/dialog/unix/SDL_portaldialog.c
index e56478bb0e785..7f28cb6d25b0c 100644
--- a/src/dialog/unix/SDL_portaldialog.c
+++ b/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;
diff --git a/src/dialog/unix/SDL_unixdialog.c b/src/dialog/unix/SDL_unixdialog.c
index 29ce2eef0a234..ae57833fe8e11 100644
--- a/src/dialog/unix/SDL_unixdialog.c
+++ b/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;
diff --git a/src/dialog/unix/SDL_zenitydialog.c b/src/dialog/unix/SDL_zenitydialog.c
index 5ba8e8dc721b0..cef8826c550c7 100644
--- a/src/dialog/unix/SDL_zenitydialog.c
+++ b/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) {
-         

(Patch may be truncated, please check the link at the top of this post.)