chore(build): use SDL3

This commit is contained in:
phaneron 2025-04-12 04:38:19 -04:00
parent 9d04a35d87
commit b3c0734a9e
3286 changed files with 866354 additions and 554996 deletions

View file

@ -0,0 +1,131 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 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.h"
#include "SDL_dialog_utils.h"
void SDL_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props)
{
if (!callback) {
return;
}
#ifdef SDL_DIALOG_DISABLED
SDL_SetError("SDL not built with dialog support");
callback(userdata, NULL, -1);
#else
SDL_DialogFileFilter *filters = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_FILTERS_POINTER, NULL);
int nfilters = (int) SDL_GetNumberProperty(props, SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, -1);
if (filters && nfilters == -1) {
SDL_SetError("Set filter pointers, but didn't set number of filters (SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER)");
callback(userdata, NULL, -1);
return;
}
const char *msg = validate_filters(filters, nfilters);
if (msg) {
SDL_SetError("Invalid dialog file filters: %s", msg);
callback(userdata, NULL, -1);
return;
}
switch (type) {
case SDL_FILEDIALOG_OPENFILE:
case SDL_FILEDIALOG_SAVEFILE:
case SDL_FILEDIALOG_OPENFOLDER:
SDL_SYS_ShowFileDialogWithProperties(type, callback, userdata, props);
break;
default:
SDL_SetError("Unsupported file dialog type: %d", (int) type);
callback(userdata, NULL, -1);
break;
};
#endif
}
void SDL_ShowOpenFileDialog(SDL_DialogFileCallback callback, void *userdata, SDL_Window *window, const SDL_DialogFileFilter *filters, int nfilters, const char *default_location, bool allow_many)
{
#ifdef SDL_DIALOG_DISABLED
if (!callback) {
return;
}
SDL_SetError("SDL not built with dialog support");
callback(userdata, NULL, -1);
#else
SDL_PropertiesID props = SDL_CreateProperties();
SDL_SetPointerProperty(props, SDL_PROP_FILE_DIALOG_FILTERS_POINTER, (void *) filters);
SDL_SetNumberProperty(props, SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, nfilters);
SDL_SetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, window);
SDL_SetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, default_location);
SDL_SetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, allow_many);
SDL_ShowFileDialogWithProperties(SDL_FILEDIALOG_OPENFILE, callback, userdata, props);
SDL_DestroyProperties(props);
#endif
}
void SDL_ShowSaveFileDialog(SDL_DialogFileCallback callback, void *userdata, SDL_Window *window, const SDL_DialogFileFilter *filters, int nfilters, const char *default_location)
{
#ifdef SDL_DIALOG_DISABLED
if (!callback) {
return;
}
SDL_SetError("SDL not built with dialog support");
callback(userdata, NULL, -1);
#else
SDL_PropertiesID props = SDL_CreateProperties();
SDL_SetPointerProperty(props, SDL_PROP_FILE_DIALOG_FILTERS_POINTER, (void *) filters);
SDL_SetNumberProperty(props, SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, nfilters);
SDL_SetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, window);
SDL_SetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, default_location);
SDL_ShowFileDialogWithProperties(SDL_FILEDIALOG_SAVEFILE, callback, userdata, props);
SDL_DestroyProperties(props);
#endif
}
void SDL_ShowOpenFolderDialog(SDL_DialogFileCallback callback, void *userdata, SDL_Window *window, const char *default_location, bool allow_many)
{
#ifdef SDL_DIALOG_DISABLED
if (!callback) {
return;
}
SDL_SetError("SDL not built with dialog support");
callback(userdata, NULL, -1);
#else
SDL_PropertiesID props = SDL_CreateProperties();
SDL_SetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, window);
SDL_SetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, default_location);
SDL_SetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, allow_many);
SDL_ShowFileDialogWithProperties(SDL_FILEDIALOG_OPENFOLDER, callback, userdata, props);
SDL_DestroyProperties(props);
#endif
}

View file

@ -0,0 +1,22 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 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.
*/
void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props);

View file

@ -0,0 +1,256 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 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, int nfilters,
NameTransform ntf, const char *prefix,
const char *separator, const char *suffix,
const char *filt_prefix, const char *filt_separator,
const char *filt_suffix, const char *ext_prefix,
const char *ext_separator, const char *ext_suffix)
{
char *combined;
char *new_combined;
char *converted;
const char *terminator;
size_t new_length;
int i;
if (!filters) {
SDL_SetError("Called convert_filters() with NULL filters (SDL bug)");
return NULL;
}
combined = SDL_strdup(prefix);
if (!combined) {
return NULL;
}
for (i = 0; i < nfilters; i++) {
const SDL_DialogFileFilter *f = &filters[i];
converted = convert_filter(*f, ntf, filt_prefix, filt_separator,
filt_suffix, ext_prefix, ext_separator,
ext_suffix);
if (!converted) {
SDL_free(combined);
return NULL;
}
terminator = ((i + 1) < nfilters) ? separator : suffix;
new_length = SDL_strlen(combined) + SDL_strlen(converted)
+ SDL_strlen(terminator) + 1;
new_combined = (char *)SDL_realloc(combined, new_length);
if (!new_combined) {
SDL_free(converted);
SDL_free(combined);
return NULL;
}
combined = new_combined;
SDL_strlcat(combined, converted, new_length);
SDL_strlcat(combined, terminator, new_length);
SDL_free(converted);
}
new_length = SDL_strlen(combined) + SDL_strlen(suffix) + 1;
new_combined = (char *)SDL_realloc(combined, new_length);
if (!new_combined) {
SDL_free(combined);
return NULL;
}
combined = new_combined;
SDL_strlcat(combined, suffix, new_length);
return combined;
}
char *convert_filter(SDL_DialogFileFilter filter, NameTransform ntf,
const char *prefix, const char *separator,
const char *suffix, const char *ext_prefix,
const char *ext_separator, const char *ext_suffix)
{
char *converted;
char *name_filtered;
size_t total_length;
char *list;
list = convert_ext_list(filter.pattern, ext_prefix, ext_separator,
ext_suffix);
if (!list) {
return NULL;
}
if (ntf) {
name_filtered = ntf(filter.name);
} else {
// Useless strdup, but easier to read and maintain code this way
name_filtered = SDL_strdup(filter.name);
}
if (!name_filtered) {
SDL_free(list);
return NULL;
}
total_length = SDL_strlen(prefix) + SDL_strlen(name_filtered)
+ SDL_strlen(separator) + SDL_strlen(list)
+ SDL_strlen(suffix) + 1;
converted = (char *) SDL_malloc(total_length);
if (!converted) {
SDL_free(list);
SDL_free(name_filtered);
return NULL;
}
SDL_snprintf(converted, total_length, "%s%s%s%s%s", prefix, name_filtered,
separator, list, suffix);
SDL_free(list);
SDL_free(name_filtered);
return converted;
}
char *convert_ext_list(const char *list, const char *prefix,
const char *separator, const char *suffix)
{
char *converted;
int semicolons;
size_t total_length;
semicolons = 0;
for (const char *c = list; *c; c++) {
semicolons += (*c == ';');
}
total_length =
SDL_strlen(list) - semicolons // length of list contents
+ semicolons * SDL_strlen(separator) // length of separators
+ SDL_strlen(prefix) + SDL_strlen(suffix) // length of prefix/suffix
+ 1; // terminating null byte
converted = (char *) SDL_malloc(total_length);
if (!converted) {
return NULL;
}
*converted = '\0';
SDL_strlcat(converted, prefix, total_length);
/* Some platforms may prefer to handle the asterisk manually, but this
function offers to handle it for ease of use. */
if (SDL_strcmp(list, "*") == 0) {
SDL_strlcat(converted, "*", total_length);
} else {
for (const char *c = list; *c; c++) {
if ((*c >= 'a' && *c <= 'z') || (*c >= 'A' && *c <= 'Z')
|| (*c >= '0' && *c <= '9') || *c == '-' || *c == '_'
|| *c == '.') {
char str[2];
str[0] = *c;
str[1] = '\0';
SDL_strlcat(converted, str, total_length);
} else if (*c == ';') {
if (c == list || c[-1] == ';') {
SDL_SetError("Empty pattern not allowed");
SDL_free(converted);
return NULL;
}
SDL_strlcat(converted, separator, total_length);
} else {
SDL_SetError("Invalid character '%c' in pattern (Only [a-zA-Z0-9_.-] allowed, or a single *)", *c);
SDL_free(converted);
return NULL;
}
}
}
if (list[SDL_strlen(list) - 1] == ';') {
SDL_SetError("Empty pattern not allowed");
SDL_free(converted);
return NULL;
}
SDL_strlcat(converted, suffix, total_length);
return converted;
}
const char *validate_filters(const SDL_DialogFileFilter *filters, int nfilters)
{
if (filters) {
for (int i = 0; i < nfilters; i++) {
const char *msg = validate_list(filters[i].pattern);
if (msg) {
return msg;
}
}
}
return NULL;
}
const char *validate_list(const char *list)
{
if (SDL_strcmp(list, "*") == 0) {
return NULL;
} else {
for (const char *c = list; *c; c++) {
if ((*c >= 'a' && *c <= 'z') || (*c >= 'A' && *c <= 'Z')
|| (*c >= '0' && *c <= '9') || *c == '-' || *c == '_'
|| *c == '.') {
continue;
} else if (*c == ';') {
if (c == list || c[-1] == ';') {
return "Empty pattern not allowed";
}
} else {
return "Invalid character in pattern (Only [a-zA-Z0-9_.-] allowed, or a single *)";
}
}
}
if (list[SDL_strlen(list) - 1] == ';') {
return "Empty pattern not allowed";
}
return NULL;
}

View file

@ -0,0 +1,59 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 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, int nfilters,
NameTransform ntf, const char *prefix,
const char *separator, const char *suffix,
const char *filt_prefix, const char *filt_separator,
const char *filt_suffix, const char *ext_prefix,
const char *ext_separator, const char *ext_suffix);
// Converts one filter into a single string.
// <prefix>[filter name]<separator>[filter extension list]<suffix>
char *convert_filter(SDL_DialogFileFilter filter, NameTransform ntf,
const char *prefix, const char *separator,
const char *suffix, const char *ext_prefix,
const char *ext_separator, const char *ext_suffix);
// Converts the extension list of a filter into a single string.
// <prefix>[extension]{<separator>[extension]...}<suffix>
char *convert_ext_list(const char *list, const char *prefix,
const char *separator, const char *suffix);
/* Must be used if convert_* functions aren't used */
// Returns an error message if there's a problem, NULL otherwise
const char *validate_filters(const SDL_DialogFileFilter *filters,
int nfilters);
const char *validate_list(const char *list);

View file

@ -0,0 +1,58 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 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.h"
#include "../../core/android/SDL_android.h"
void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props)
{
SDL_DialogFileFilter *filters = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_FILTERS_POINTER, NULL);
int nfilters = (int) SDL_GetNumberProperty(props, SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, 0);
bool allow_many = SDL_GetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, false);
bool is_save;
if (SDL_GetHint(SDL_HINT_FILE_DIALOG_DRIVER) != NULL) {
SDL_SetError("File dialog driver unsupported (don't set SDL_HINT_FILE_DIALOG_DRIVER)");
callback(userdata, NULL, -1);
return;
}
switch (type) {
case SDL_FILEDIALOG_OPENFILE:
is_save = false;
break;
case SDL_FILEDIALOG_SAVEFILE:
is_save = true;
break;
case SDL_FILEDIALOG_OPENFOLDER:
SDL_Unsupported();
callback(userdata, NULL, -1);
return;
};
if (!Android_JNI_OpenFileDialog(callback, userdata, filters, nfilters, is_save, allow_many)) {
// SDL_SetError is already called when it fails
callback(userdata, NULL, -1);
}
}

View file

@ -0,0 +1,188 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 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.h"
#include "../SDL_dialog_utils.h"
#ifdef SDL_PLATFORM_MACOS
#import <Cocoa/Cocoa.h>
#import <UniformTypeIdentifiers/UTType.h>
void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props)
{
SDL_Window* window = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, NULL);
SDL_DialogFileFilter *filters = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_FILTERS_POINTER, NULL);
int nfilters = (int) SDL_GetNumberProperty(props, SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, 0);
bool allow_many = SDL_GetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, false);
const char* default_location = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, NULL);
const char* title = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_TITLE_STRING, NULL);
const char* accept = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_ACCEPT_STRING, NULL);
if (filters) {
const char *msg = validate_filters(filters, nfilters);
if (msg) {
SDL_SetError("%s", msg);
callback(userdata, NULL, -1);
return;
}
}
if (SDL_GetHint(SDL_HINT_FILE_DIALOG_DRIVER) != NULL) {
SDL_SetError("File dialog driver unsupported (don't set SDL_HINT_FILE_DIALOG_DRIVER)");
callback(userdata, NULL, -1);
return;
}
// NSOpenPanel inherits from NSSavePanel
NSSavePanel *dialog;
NSOpenPanel *dialog_as_open;
switch (type) {
case SDL_FILEDIALOG_SAVEFILE:
dialog = [NSSavePanel savePanel];
break;
case SDL_FILEDIALOG_OPENFILE:
dialog_as_open = [NSOpenPanel openPanel];
[dialog_as_open setAllowsMultipleSelection:((allow_many == true) ? YES : NO)];
dialog = dialog_as_open;
break;
case SDL_FILEDIALOG_OPENFOLDER:
dialog_as_open = [NSOpenPanel openPanel];
[dialog_as_open setCanChooseFiles:NO];
[dialog_as_open setCanChooseDirectories:YES];
[dialog_as_open setAllowsMultipleSelection:((allow_many == true) ? YES : NO)];
dialog = dialog_as_open;
break;
};
if (title) {
[dialog setTitle:[NSString stringWithUTF8String:title]];
}
if (accept) {
[dialog setPrompt:[NSString stringWithUTF8String:accept]];
}
if (filters) {
// On macOS 11.0 and up, this is an array of UTType. Prior to that, it's an array of NSString
NSMutableArray *types = [[NSMutableArray alloc] initWithCapacity:nfilters ];
int has_all_files = 0;
for (int i = 0; i < nfilters; i++) {
char *pattern = SDL_strdup(filters[i].pattern);
char *pattern_ptr = pattern;
if (!pattern_ptr) {
callback(userdata, NULL, -1);
return;
}
for (char *c = pattern; *c; c++) {
if (*c == ';') {
*c = '\0';
if(@available(macOS 11.0, *)) {
[types addObject: [UTType typeWithFilenameExtension:[NSString stringWithFormat: @"%s", pattern_ptr]]];
} else {
[types addObject: [NSString stringWithFormat: @"%s", pattern_ptr]];
}
pattern_ptr = c + 1;
} else if (*c == '*') {
has_all_files = 1;
}
}
if(@available(macOS 11.0, *)) {
[types addObject: [UTType typeWithFilenameExtension:[NSString stringWithFormat: @"%s", pattern_ptr]]];
} else {
[types addObject: [NSString stringWithFormat: @"%s", pattern_ptr]];
}
SDL_free(pattern);
}
if (!has_all_files) {
if (@available(macOS 11.0, *)) {
[dialog setAllowedContentTypes:types];
} else {
[dialog setAllowedFileTypes:types];
}
}
}
// Keep behavior consistent with other platforms
[dialog setAllowsOtherFileTypes:YES];
if (default_location) {
[dialog setDirectoryURL:[NSURL fileURLWithPath:[NSString stringWithUTF8String:default_location]]];
}
NSWindow *w = NULL;
if (window) {
w = (__bridge NSWindow *)SDL_GetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, NULL);
}
if (w) {
// [dialog beginWithCompletionHandler:^(NSInteger result) {
[dialog beginSheetModalForWindow:w completionHandler:^(NSInteger result) {
if (result == NSModalResponseOK) {
if (dialog_as_open) {
NSArray* urls = [dialog_as_open URLs];
const char *files[[urls count] + 1];
for (int i = 0; i < [urls count]; i++) {
files[i] = [[[urls objectAtIndex:i] path] UTF8String];
}
files[[urls count]] = NULL;
callback(userdata, files, -1);
} else {
const char *files[2] = { [[[dialog URL] path] UTF8String], NULL };
callback(userdata, files, -1);
}
} else if (result == NSModalResponseCancel) {
const char *files[1] = { NULL };
callback(userdata, files, -1);
}
}];
} else {
if ([dialog runModal] == NSModalResponseOK) {
if (dialog_as_open) {
NSArray* urls = [dialog_as_open URLs];
const char *files[[urls count] + 1];
for (int i = 0; i < [urls count]; i++) {
files[i] = [[[urls objectAtIndex:i] path] UTF8String];
}
files[[urls count]] = NULL;
callback(userdata, files, -1);
} else {
const char *files[2] = { [[[dialog URL] path] UTF8String], NULL };
callback(userdata, files, -1);
}
} else {
const char *files[1] = { NULL };
callback(userdata, files, -1);
}
}
}
#endif // SDL_PLATFORM_MACOS

View file

@ -0,0 +1,33 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 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.h"
#ifdef SDL_DIALOG_DUMMY
void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props)
{
SDL_Unsupported();
callback(userdata, NULL, -1);
}
#endif // SDL_DIALOG_DUMMY

View file

@ -0,0 +1,293 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 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"
extern "C" {
#include "../SDL_dialog.h"
#include "../SDL_dialog_utils.h"
}
#include "../../core/haiku/SDL_BeApp.h"
#include "../../video/haiku/SDL_BWin.h"
#include <string>
#include <vector>
#include <sys/stat.h>
#include <FilePanel.h>
#include <Entry.h>
#include <Looper.h>
#include <Messenger.h>
#include <Path.h>
#include <TypeConstants.h>
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<std::string> StringSplit(const std::string& str, const std::string& split)
{
std::vector<std::string> result;
std::string s = str;
size_t pos = 0;
while ((pos = s.find(split)) != std::string::npos) {
result.push_back(s.substr(0, pos));
s = s.substr(pos + split.size());
}
result.push_back(s);
return result;
}
class SDLBRefFilter : public BRefFilter
{
public:
SDLBRefFilter(const SDL_DialogFileFilter *filters, int nfilters) :
BRefFilter(),
m_filters(filters),
m_nfilters(nfilters)
{
}
virtual bool Filter(const entry_ref *ref, BNode *node, struct stat_beos *stat, const char *mimeType) override
{
BEntry entry(ref);
BPath path;
entry.GetPath(&path);
std::string result = path.Path();
if (!m_filters)
return true;
struct stat info;
node->GetStat(&info);
if (S_ISDIR(info.st_mode))
return true;
for (int i = 0; i < m_nfilters; i++) {
for (const auto& suffix : StringSplit(m_filters[i].pattern, ";")) {
if (StringEndsWith(result, std::string(".") + suffix)) {
return true;
}
}
}
return false;
}
private:
const SDL_DialogFileFilter * const m_filters;
int m_nfilters;
};
class CallbackLooper : public BLooper
{
public:
CallbackLooper(SDL_DialogFileCallback callback, void *userdata) :
m_callback(callback),
m_userdata(userdata),
m_files(),
m_messenger(),
m_panel(),
m_filter()
{
}
~CallbackLooper()
{
delete m_messenger;
delete m_panel;
delete m_filter;
}
void SetToBeFreed(BMessenger *messenger, BFilePanel *panel, SDLBRefFilter *filter)
{
m_messenger = messenger;
m_panel = panel;
m_filter = filter;
}
virtual void MessageReceived(BMessage *msg) override
{
entry_ref file;
BPath path;
BEntry entry;
std::string result;
const char *filename;
int32 nFiles = 0;
switch (msg->what)
{
case B_REFS_RECEIVED: // Open
msg->GetInfo("refs", NULL, &nFiles);
for (int i = 0; i < nFiles; i++) {
msg->FindRef("refs", i, &file);
entry.SetTo(&file);
entry.GetPath(&path);
result = path.Path();
m_files.push_back(result);
}
break;
case B_SAVE_REQUESTED: // Save
msg->FindRef("directory", &file);
entry.SetTo(&file);
entry.GetPath(&path);
result = path.Path();
result += "/";
msg->FindString("name", &filename);
result += filename;
m_files.push_back(result);
break;
case B_CANCEL: // Whenever the dialog is closed (Cancel but also after Open and Save)
{
nFiles = m_files.size();
const char* files[nFiles + 1];
for (int i = 0; i < nFiles; i++) {
files[i] = m_files[i].c_str();
}
files[nFiles] = NULL;
m_callback(m_userdata, files, -1);
Quit();
SDL_QuitBeApp();
delete this;
}
break;
default:
BHandler::MessageReceived(msg);
break;
}
}
private:
SDL_DialogFileCallback m_callback;
void *m_userdata;
std::vector<std::string> m_files;
// Only to free stuff later
BMessenger *m_messenger;
BFilePanel *m_panel;
SDLBRefFilter *m_filter;
};
void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props)
{
SDL_Window* window = (SDL_Window*) SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, NULL);
SDL_DialogFileFilter* filters = (SDL_DialogFileFilter*) SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_FILTERS_POINTER, NULL);
int nfilters = (int) SDL_GetNumberProperty(props, SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, 0);
bool many = SDL_GetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, false);
const char* location = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, NULL);
const char* title = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_TITLE_STRING, NULL);
const char* accept = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_ACCEPT_STRING, NULL);
const char* cancel = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_CANCEL_STRING, NULL);
bool modal = !!window;
bool save = false;
bool folder = false;
switch (type) {
case SDL_FILEDIALOG_SAVEFILE:
save = true;
break;
case SDL_FILEDIALOG_OPENFILE:
break;
case SDL_FILEDIALOG_OPENFOLDER:
folder = true;
break;
};
if (!SDL_InitBeApp()) {
char* err = SDL_strdup(SDL_GetError());
SDL_SetError("Couldn't init Be app: %s", err);
SDL_free(err);
callback(userdata, NULL, -1);
return;
}
if (filters) {
const char *msg = validate_filters(filters, nfilters);
if (msg) {
SDL_SetError("%s", msg);
callback(userdata, NULL, -1);
return;
}
}
if (SDL_GetHint(SDL_HINT_FILE_DIALOG_DRIVER) != NULL) {
SDL_SetError("File dialog driver unsupported");
callback(userdata, NULL, -1);
return;
}
// No unique_ptr's because they need to survive the end of the function
CallbackLooper *looper = new(std::nothrow) CallbackLooper(callback, userdata);
BMessenger *messenger = new(std::nothrow) BMessenger(NULL, looper);
SDLBRefFilter *filter = new(std::nothrow) SDLBRefFilter(filters, nfilters);
if (looper == NULL || messenger == NULL || filter == NULL) {
SDL_free(looper);
SDL_free(messenger);
SDL_free(filter);
SDL_OutOfMemory();
callback(userdata, NULL, -1);
return;
}
BEntry entry;
entry_ref entryref;
if (location) {
entry.SetTo(location);
entry.GetRef(&entryref);
}
BFilePanel *panel = new BFilePanel(save ? B_SAVE_PANEL : B_OPEN_PANEL, messenger, location ? &entryref : NULL, folder ? B_DIRECTORY_NODE : B_FILE_NODE, many, NULL, filter, modal);
if (title) {
panel->Window()->SetTitle(title);
}
if (accept) {
panel->SetButtonLabel(B_DEFAULT_BUTTON, accept);
}
if (cancel) {
panel->SetButtonLabel(B_CANCEL_BUTTON, cancel);
}
if (window) {
SDL_BWin *bwin = (SDL_BWin *)(window->internal);
panel->Window()->SetLook(B_MODAL_WINDOW_LOOK);
panel->Window()->SetFeel(B_MODAL_SUBSET_WINDOW_FEEL);
panel->Window()->AddToSubset(bwin);
}
looper->SetToBeFreed(messenger, panel, filter);
looper->Run();
panel->Show();
}

View file

@ -0,0 +1,545 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 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"
#include "../../core/linux/SDL_dbus.h"
#ifdef SDL_USE_LIBDBUS
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define PORTAL_DESTINATION "org.freedesktop.portal.Desktop"
#define PORTAL_PATH "/org/freedesktop/portal/desktop"
#define PORTAL_INTERFACE "org.freedesktop.portal.FileChooser"
#define SIGNAL_SENDER "org.freedesktop.portal.Desktop"
#define SIGNAL_INTERFACE "org.freedesktop.portal.Request"
#define SIGNAL_NAME "Response"
#define SIGNAL_FILTER "type='signal', sender='"SIGNAL_SENDER"', interface='"SIGNAL_INTERFACE"', member='"SIGNAL_NAME"', path='"
#define HANDLE_LEN 10
#define WAYLAND_HANDLE_PREFIX "wayland:"
#define X11_HANDLE_PREFIX "x11:"
typedef struct {
SDL_DialogFileCallback callback;
void *userdata;
const char *path;
} SignalCallback;
static void DBus_AppendStringOption(SDL_DBusContext *dbus, DBusMessageIter *options, const char *key, const char *value)
{
DBusMessageIter options_pair, options_value;
dbus->message_iter_open_container(options, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair);
dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &key);
dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, "s", &options_value);
dbus->message_iter_append_basic(&options_value, DBUS_TYPE_STRING, &value);
dbus->message_iter_close_container(&options_pair, &options_value);
dbus->message_iter_close_container(options, &options_pair);
}
static void DBus_AppendBoolOption(SDL_DBusContext *dbus, DBusMessageIter *options, const char *key, int value)
{
DBusMessageIter options_pair, options_value;
dbus->message_iter_open_container(options, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair);
dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &key);
dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, "b", &options_value);
dbus->message_iter_append_basic(&options_value, DBUS_TYPE_BOOLEAN, &value);
dbus->message_iter_close_container(&options_pair, &options_value);
dbus->message_iter_close_container(options, &options_pair);
}
static void DBus_AppendFilter(SDL_DBusContext *dbus, DBusMessageIter *parent, const SDL_DialogFileFilter filter)
{
DBusMessageIter filter_entry, filter_array, filter_array_entry;
char *state = NULL, *patterns, *pattern, *glob_pattern;
int zero = 0;
dbus->message_iter_open_container(parent, DBUS_TYPE_STRUCT, NULL, &filter_entry);
dbus->message_iter_append_basic(&filter_entry, DBUS_TYPE_STRING, &filter.name);
dbus->message_iter_open_container(&filter_entry, DBUS_TYPE_ARRAY, "(us)", &filter_array);
patterns = SDL_strdup(filter.pattern);
if (!patterns) {
goto cleanup;
}
pattern = SDL_strtok_r(patterns, ";", &state);
while (pattern) {
size_t max_len = SDL_strlen(pattern) + 3;
dbus->message_iter_open_container(&filter_array, DBUS_TYPE_STRUCT, NULL, &filter_array_entry);
dbus->message_iter_append_basic(&filter_array_entry, DBUS_TYPE_UINT32, &zero);
glob_pattern = SDL_calloc(max_len, sizeof(char));
if (!glob_pattern) {
goto cleanup;
}
glob_pattern[0] = '*';
/* Special case: The '*' filter doesn't need to be rewritten */
if (pattern[0] != '*' || pattern[1]) {
glob_pattern[1] = '.';
SDL_strlcat(glob_pattern + 2, pattern, max_len);
}
dbus->message_iter_append_basic(&filter_array_entry, DBUS_TYPE_STRING, &glob_pattern);
SDL_free(glob_pattern);
dbus->message_iter_close_container(&filter_array, &filter_array_entry);
pattern = SDL_strtok_r(NULL, ";", &state);
}
cleanup:
SDL_free(patterns);
dbus->message_iter_close_container(&filter_entry, &filter_array);
dbus->message_iter_close_container(parent, &filter_entry);
}
static void DBus_AppendFilters(SDL_DBusContext *dbus, DBusMessageIter *options, const SDL_DialogFileFilter *filters, int nfilters)
{
DBusMessageIter options_pair, options_value, options_value_array;
static const char *filters_name = "filters";
dbus->message_iter_open_container(options, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair);
dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &filters_name);
dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, "a(sa(us))", &options_value);
dbus->message_iter_open_container(&options_value, DBUS_TYPE_ARRAY, "(sa(us))", &options_value_array);
for (int i = 0; i < nfilters; i++) {
DBus_AppendFilter(dbus, &options_value_array, filters[i]);
}
dbus->message_iter_close_container(&options_value, &options_value_array);
dbus->message_iter_close_container(&options_pair, &options_value);
dbus->message_iter_close_container(options, &options_pair);
}
static void DBus_AppendByteArray(SDL_DBusContext *dbus, DBusMessageIter *options, const char *key, const char *value)
{
DBusMessageIter options_pair, options_value, options_array;
dbus->message_iter_open_container(options, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair);
dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &key);
dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, "ay", &options_value);
dbus->message_iter_open_container(&options_value, DBUS_TYPE_ARRAY, "y", &options_array);
do {
dbus->message_iter_append_basic(&options_array, DBUS_TYPE_BYTE, value);
} while (*value++);
dbus->message_iter_close_container(&options_value, &options_array);
dbus->message_iter_close_container(&options_pair, &options_value);
dbus->message_iter_close_container(options, &options_pair);
}
static DBusHandlerResult DBus_MessageFilter(DBusConnection *conn, DBusMessage *msg, void *data)
{
SDL_DBusContext *dbus = SDL_DBus_GetContext();
SignalCallback *signal_data = (SignalCallback *)data;
if (dbus->message_is_signal(msg, SIGNAL_INTERFACE, SIGNAL_NAME) &&
dbus->message_has_path(msg, signal_data->path)) {
DBusMessageIter signal_iter, result_array, array_entry, value_entry, uri_entry;
uint32_t result;
size_t length = 2, current = 0;
const char **path = NULL;
dbus->message_iter_init(msg, &signal_iter);
// Check if the parameters are what we expect
if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_UINT32) {
goto not_our_signal;
}
dbus->message_iter_get_basic(&signal_iter, &result);
if (result == 1 || result == 2) {
// cancelled
const char *result_data[] = { NULL };
signal_data->callback(signal_data->userdata, result_data, -1); // TODO: Set this to the last selected filter
goto done;
} else if (result) {
// some error occurred
signal_data->callback(signal_data->userdata, NULL, -1);
goto done;
}
if (!dbus->message_iter_next(&signal_iter)) {
goto not_our_signal;
}
if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_ARRAY) {
goto not_our_signal;
}
dbus->message_iter_recurse(&signal_iter, &result_array);
while (dbus->message_iter_get_arg_type(&result_array) == DBUS_TYPE_DICT_ENTRY) {
const char *method;
dbus->message_iter_recurse(&result_array, &array_entry);
if (dbus->message_iter_get_arg_type(&array_entry) != DBUS_TYPE_STRING) {
goto not_our_signal;
}
dbus->message_iter_get_basic(&array_entry, &method);
if (!SDL_strcmp(method, "uris")) {
// we only care about the selected file paths
break;
}
if (!dbus->message_iter_next(&result_array)) {
goto not_our_signal;
}
}
if (!dbus->message_iter_next(&array_entry)) {
goto not_our_signal;
}
if (dbus->message_iter_get_arg_type(&array_entry) != DBUS_TYPE_VARIANT) {
goto not_our_signal;
}
dbus->message_iter_recurse(&array_entry, &value_entry);
if (dbus->message_iter_get_arg_type(&value_entry) != DBUS_TYPE_ARRAY) {
goto not_our_signal;
}
dbus->message_iter_recurse(&value_entry, &uri_entry);
path = SDL_malloc(length * sizeof(const char *));
if (!path) {
signal_data->callback(signal_data->userdata, NULL, -1);
goto done;
}
while (dbus->message_iter_get_arg_type(&uri_entry) == DBUS_TYPE_STRING) {
const char *uri = NULL;
if (current >= length - 1) {
++length;
const char **newpath = SDL_realloc(path, length * sizeof(const char *));
if (!newpath) {
signal_data->callback(signal_data->userdata, NULL, -1);
goto done;
}
path = newpath;
}
dbus->message_iter_get_basic(&uri_entry, &uri);
// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileChooser.html
// Returned paths will always start with 'file://'; SDL_URIToLocal() truncates it.
char *decoded_uri = SDL_malloc(SDL_strlen(uri) + 1);
if (SDL_URIToLocal(uri, decoded_uri)) {
path[current] = decoded_uri;
} else {
SDL_free(decoded_uri);
SDL_SetError("Portal dialogs: Unsupported protocol: %s", uri);
signal_data->callback(signal_data->userdata, NULL, -1);
goto done;
}
dbus->message_iter_next(&uri_entry);
++current;
}
path[current] = NULL;
signal_data->callback(signal_data->userdata, path, -1); // TODO: Fetch the index of the filter that was used
done:
dbus->connection_remove_filter(conn, &DBus_MessageFilter, signal_data);
if (path) {
for (size_t i = 0; i < current; ++i) {
SDL_free((char *)path[i]);
}
SDL_free(path);
}
SDL_free((void *)signal_data->path);
SDL_free(signal_data);
return DBUS_HANDLER_RESULT_HANDLED;
}
not_our_signal:
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
void SDL_Portal_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props)
{
const char *method;
const char *method_title;
SDL_Window* window = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, NULL);
SDL_DialogFileFilter *filters = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_FILTERS_POINTER, NULL);
int nfilters = (int) SDL_GetNumberProperty(props, SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, 0);
bool allow_many = SDL_GetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, false);
const char* default_location = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, NULL);
const char* accept = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_ACCEPT_STRING, NULL);
bool open_folders = false;
switch (type) {
case SDL_FILEDIALOG_OPENFILE:
method = "OpenFile";
method_title = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_TITLE_STRING, "Open File");
break;
case SDL_FILEDIALOG_SAVEFILE:
method = "SaveFile";
method_title = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_TITLE_STRING, "Save File");
break;
case SDL_FILEDIALOG_OPENFOLDER:
method = "OpenFile";
method_title = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_TITLE_STRING, "Open Folder");
open_folders = true;
break;
default:
/* This is already checked in ../SDL_dialog.c; this silences compiler warnings */
SDL_SetError("Invalid file dialog type: %d", type);
callback(userdata, NULL, -1);
return;
}
SDL_DBusContext *dbus = SDL_DBus_GetContext();
DBusMessage *msg;
DBusMessageIter params, options;
const char *signal_id = NULL;
char *handle_str, *filter;
int filter_len;
static uint32_t handle_id = 0;
static char *default_parent_window = "";
SDL_PropertiesID window_props = SDL_GetWindowProperties(window);
const char *err_msg = validate_filters(filters, nfilters);
if (err_msg) {
SDL_SetError("%s", err_msg);
callback(userdata, NULL, -1);
return;
}
if (dbus == NULL) {
SDL_SetError("Failed to connect to DBus");
callback(userdata, NULL, -1);
return;
}
msg = dbus->message_new_method_call(PORTAL_DESTINATION, PORTAL_PATH, PORTAL_INTERFACE, method);
if (msg == NULL) {
SDL_SetError("Failed to send message to portal");
callback(userdata, NULL, -1);
return;
}
dbus->message_iter_init_append(msg, &params);
handle_str = default_parent_window;
if (window_props) {
const char *parent_handle = SDL_GetStringProperty(window_props, SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_EXPORT_HANDLE_STRING, NULL);
if (parent_handle) {
size_t len = SDL_strlen(parent_handle);
len += sizeof(WAYLAND_HANDLE_PREFIX) + 1;
handle_str = SDL_malloc(len * sizeof(char));
if (!handle_str) {
callback(userdata, NULL, -1);
return;
}
SDL_snprintf(handle_str, len, "%s%s", WAYLAND_HANDLE_PREFIX, parent_handle);
} else {
const Uint64 xid = (Uint64)SDL_GetNumberProperty(window_props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
if (xid) {
const size_t len = sizeof(X11_HANDLE_PREFIX) + 24; // A 64-bit number can be 20 characters max.
handle_str = SDL_malloc(len * sizeof(char));
if (!handle_str) {
callback(userdata, NULL, -1);
return;
}
// The portal wants X11 window ID numbers in hex.
SDL_snprintf(handle_str, len, "%s%" SDL_PRIx64, X11_HANDLE_PREFIX, xid);
}
}
}
dbus->message_iter_append_basic(&params, DBUS_TYPE_STRING, &handle_str);
if (handle_str != default_parent_window) {
SDL_free(handle_str);
}
dbus->message_iter_append_basic(&params, DBUS_TYPE_STRING, &method_title);
dbus->message_iter_open_container(&params, DBUS_TYPE_ARRAY, "{sv}", &options);
handle_str = SDL_malloc(sizeof(char) * (HANDLE_LEN + 1));
if (!handle_str) {
callback(userdata, NULL, -1);
return;
}
SDL_snprintf(handle_str, HANDLE_LEN, "%u", ++handle_id);
DBus_AppendStringOption(dbus, &options, "handle_token", handle_str);
SDL_free(handle_str);
DBus_AppendBoolOption(dbus, &options, "modal", !!window);
if (allow_many) {
DBus_AppendBoolOption(dbus, &options, "multiple", 1);
}
if (open_folders) {
DBus_AppendBoolOption(dbus, &options, "directory", 1);
}
if (filters) {
DBus_AppendFilters(dbus, &options, filters, nfilters);
}
if (default_location) {
DBus_AppendByteArray(dbus, &options, "current_folder", default_location);
}
if (accept) {
DBus_AppendStringOption(dbus, &options, "accept_label", accept);
}
dbus->message_iter_close_container(&params, &options);
DBusMessage *reply = dbus->connection_send_with_reply_and_block(dbus->session_conn, msg, DBUS_TIMEOUT_INFINITE, NULL);
if (reply) {
DBusMessageIter reply_iter;
dbus->message_iter_init(reply, &reply_iter);
if (dbus->message_iter_get_arg_type(&reply_iter) == DBUS_TYPE_OBJECT_PATH) {
dbus->message_iter_get_basic(&reply_iter, &signal_id);
}
}
if (!signal_id) {
SDL_SetError("Invalid response received by DBus");
callback(userdata, NULL, -1);
goto incorrect_type;
}
dbus->message_unref(msg);
filter_len = SDL_strlen(SIGNAL_FILTER) + SDL_strlen(signal_id) + 2;
filter = SDL_malloc(sizeof(char) * filter_len);
if (!filter) {
callback(userdata, NULL, -1);
goto incorrect_type;
}
SDL_snprintf(filter, filter_len, SIGNAL_FILTER"%s'", signal_id);
dbus->bus_add_match(dbus->session_conn, filter, NULL);
SDL_free(filter);
SignalCallback *data = SDL_malloc(sizeof(SignalCallback));
if (!data) {
callback(userdata, NULL, -1);
goto incorrect_type;
}
data->callback = callback;
data->userdata = userdata;
data->path = SDL_strdup(signal_id);
if (!data->path) {
SDL_free(data);
callback(userdata, NULL, -1);
goto incorrect_type;
}
/* TODO: This should be registered before opening the portal, or the filter will not catch
the message if it is sent before we register the filter.
*/
dbus->connection_add_filter(dbus->session_conn,
&DBus_MessageFilter, data, NULL);
dbus->connection_flush(dbus->session_conn);
incorrect_type:
dbus->message_unref(reply);
}
bool SDL_Portal_detect(void)
{
SDL_DBusContext *dbus = SDL_DBus_GetContext();
DBusMessage *msg = NULL, *reply = NULL;
char *reply_str = NULL;
DBusMessageIter reply_iter;
static int portal_present = -1;
// No need for this if the result is cached.
if (portal_present != -1) {
return (portal_present > 0);
}
portal_present = 0;
if (!dbus) {
SDL_SetError("%s", "Failed to connect to DBus!");
return false;
}
// Use introspection to get the available services.
msg = dbus->message_new_method_call(PORTAL_DESTINATION, PORTAL_PATH, "org.freedesktop.DBus.Introspectable", "Introspect");
if (!msg) {
goto done;
}
reply = dbus->connection_send_with_reply_and_block(dbus->session_conn, msg, DBUS_TIMEOUT_USE_DEFAULT, NULL);
dbus->message_unref(msg);
if (!reply) {
goto done;
}
if (!dbus->message_iter_init(reply, &reply_iter)) {
goto done;
}
if (dbus->message_iter_get_arg_type(&reply_iter) != DBUS_TYPE_STRING) {
goto done;
}
/* Introspection gives us a dump of all the services on the destination in XML format, so search the
* giant string for the file chooser protocol.
*/
dbus->message_iter_get_basic(&reply_iter, &reply_str);
if (SDL_strstr(reply_str, PORTAL_INTERFACE)) {
portal_present = 1; // Found it!
}
done:
if (reply) {
dbus->message_unref(reply);
}
return (portal_present > 0);
}
#else
// Dummy implementation to avoid compilation problems
void SDL_Portal_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props)
{
SDL_Unsupported();
callback(userdata, NULL, -1);
}
bool SDL_Portal_detect(void)
{
return false;
}
#endif // SDL_USE_LIBDBUS

View file

@ -0,0 +1,27 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 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"
void SDL_Portal_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props);
/** @returns non-zero if available, zero if unavailable */
bool SDL_Portal_detect(void);

View file

@ -0,0 +1,81 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 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.h"
#include "./SDL_portaldialog.h"
#include "./SDL_zenitydialog.h"
static void (*detected_function)(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props) = NULL;
void SDLCALL hint_callback(void *userdata, const char *name, const char *oldValue, const char *newValue);
static void set_callback(void)
{
static bool is_set = false;
if (is_set == false) {
is_set = true;
SDL_AddHintCallback(SDL_HINT_FILE_DIALOG_DRIVER, hint_callback, NULL);
}
}
// Returns non-zero on success, 0 on failure
static int detect_available_methods(const char *value)
{
const char *driver = value ? value : SDL_GetHint(SDL_HINT_FILE_DIALOG_DRIVER);
set_callback();
if (driver == NULL || SDL_strcmp(driver, "portal") == 0) {
if (SDL_Portal_detect()) {
detected_function = SDL_Portal_ShowFileDialogWithProperties;
return 1;
}
}
if (driver == NULL || SDL_strcmp(driver, "zenity") == 0) {
if (SDL_Zenity_detect()) {
detected_function = SDL_Zenity_ShowFileDialogWithProperties;
return 2;
}
}
SDL_SetError("File dialog driver unsupported (supported values for SDL_HINT_FILE_DIALOG_DRIVER are 'zenity' and 'portal')");
return 0;
}
void SDLCALL hint_callback(void *userdata, const char *name, const char *oldValue, const char *newValue)
{
detect_available_methods(newValue);
}
void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props)
{
// Call detect_available_methods() again each time in case the situation changed
if (!detected_function && !detect_available_methods(NULL)) {
// SetError() done by detect_available_methods()
callback(userdata, NULL, -1);
return;
}
detected_function(type, callback, userdata, props);
}

View file

@ -0,0 +1,366 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 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"
#define X11_HANDLE_MAX_WIDTH 28
typedef struct
{
SDL_DialogFileCallback callback;
void *userdata;
void *argv;
/* Zenity only works with X11 handles apparently */
char x11_window_handle[X11_HANDLE_MAX_WIDTH];
/* These are part of argv, but are tracked separately for deallocation purposes */
int nfilters;
char **filters_slice;
char *filename;
char *title;
char *accept;
char *cancel;
} zenityArgs;
static char *zenity_clean_name(const char *name)
{
char *newname = SDL_strdup(name);
/* Filter out "|", which Zenity considers a special character. Let's hope
there aren't others. TODO: find something better. */
for (char *c = newname; *c; c++) {
if (*c == '|') {
// Zenity doesn't support escaping with '\'
*c = '/';
}
}
return newname;
}
static bool get_x11_window_handle(SDL_PropertiesID props, char *out)
{
SDL_Window *window = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, NULL);
if (!window) {
return false;
}
SDL_PropertiesID window_props = SDL_GetWindowProperties(window);
if (!window_props) {
return false;
}
Uint64 handle = (Uint64)SDL_GetNumberProperty(window_props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
if (!handle) {
return false;
}
if (SDL_snprintf(out, X11_HANDLE_MAX_WIDTH, "0x%" SDL_PRIx64, handle) >= X11_HANDLE_MAX_WIDTH) {
return false;
};
return true;
}
/* Exec call format:
*
* zenity --file-selection --separator=\n [--multiple]
* [--directory] [--save --confirm-overwrite]
* [--filename FILENAME] [--modal --attach 0x11w1nd0w]
* [--title TITLE] [--ok-label ACCEPT]
* [--cancel-label CANCEL]
* [--file-filter=Filter Name | *.filt *.fn ...]...
*/
static zenityArgs *create_zenity_args(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props)
{
zenityArgs *args = SDL_calloc(1, sizeof(*args));
if (!args) {
return NULL;
}
args->callback = callback;
args->userdata = userdata;
args->nfilters = SDL_GetNumberProperty(props, SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, 0);
const char **argv = SDL_malloc(
sizeof(*argv) * (3 /* zenity --file-selection --separator=\n */
+ 1 /* --multiple */
+ 2 /* --directory | --save --confirm-overwrite */
+ 2 /* --filename [file] */
+ 3 /* --modal --attach [handle] */
+ 2 /* --title [title] */
+ 2 /* --ok-label [label] */
+ 2 /* --cancel-label [label] */
+ args->nfilters + 1 /* NULL */));
if (!argv) {
goto cleanup;
}
args->argv = argv;
/* Properties can be destroyed as soon as the function returns; copy over what we need. */
#define COPY_STRING_PROPERTY(dst, prop) \
{ \
const char *str = SDL_GetStringProperty(props, prop, NULL); \
if (str) { \
dst = SDL_strdup(str); \
if (!dst) { \
goto cleanup; \
} \
} \
}
COPY_STRING_PROPERTY(args->filename, SDL_PROP_FILE_DIALOG_LOCATION_STRING);
COPY_STRING_PROPERTY(args->title, SDL_PROP_FILE_DIALOG_TITLE_STRING);
COPY_STRING_PROPERTY(args->accept, SDL_PROP_FILE_DIALOG_ACCEPT_STRING);
COPY_STRING_PROPERTY(args->cancel, SDL_PROP_FILE_DIALOG_CANCEL_STRING);
#undef COPY_STRING_PROPERTY
// ARGV PASS
int argc = 0;
argv[argc++] = "zenity";
argv[argc++] = "--file-selection";
argv[argc++] = "--separator=\n";
if (SDL_GetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, false)) {
argv[argc++] = "--multiple";
}
switch (type) {
case SDL_FILEDIALOG_OPENFILE:
break;
case SDL_FILEDIALOG_SAVEFILE:
argv[argc++] = "--save";
/* Asking before overwriting while saving seems like a sane default */
argv[argc++] = "--confirm-overwrite";
break;
case SDL_FILEDIALOG_OPENFOLDER:
argv[argc++] = "--directory";
break;
};
if (args->filename) {
argv[argc++] = "--filename";
argv[argc++] = args->filename;
}
if (get_x11_window_handle(props, args->x11_window_handle)) {
argv[argc++] = "--modal";
argv[argc++] = "--attach";
argv[argc++] = args->x11_window_handle;
}
if (args->title) {
argv[argc++] = "--title";
argv[argc++] = args->title;
}
if (args->accept) {
argv[argc++] = "--ok-label";
argv[argc++] = args->accept;
}
if (args->cancel) {
argv[argc++] = "--cancel-label";
argv[argc++] = args->cancel;
}
const SDL_DialogFileFilter *filters = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_FILTERS_POINTER, NULL);
if (filters) {
args->filters_slice = (char **)&argv[argc];
for (int i = 0; i < args->nfilters; i++) {
char *filter_str = convert_filter(filters[i],
zenity_clean_name,
"--file-filter=", " | ", "",
"*.", " *.", "");
if (!filter_str) {
while (i--) {
SDL_free(args->filters_slice[i]);
}
goto cleanup;
}
args->filters_slice[i] = filter_str;
}
argc += args->nfilters;
}
argv[argc] = NULL;
return args;
cleanup:
SDL_free(args->filename);
SDL_free(args->title);
SDL_free(args->accept);
SDL_free(args->cancel);
SDL_free(argv);
SDL_free(args);
return NULL;
}
// TODO: Zenity survives termination of the parent
static void run_zenity(SDL_DialogFileCallback callback, void *userdata, void *argv)
{
SDL_Process *process = NULL;
SDL_Environment *env = NULL;
int status = -1;
size_t bytes_read = 0;
char *container = NULL;
size_t narray = 1;
char **array = NULL;
bool result = false;
env = SDL_CreateEnvironment(true);
if (!env) {
goto done;
}
/* Recent versions of Zenity have different exit codes, but picks up
different codes from the environment */
SDL_SetEnvironmentVariable(env, "ZENITY_OK", "0", true);
SDL_SetEnvironmentVariable(env, "ZENITY_CANCEL", "1", true);
SDL_SetEnvironmentVariable(env, "ZENITY_ESC", "1", true);
SDL_SetEnvironmentVariable(env, "ZENITY_EXTRA", "2", true);
SDL_SetEnvironmentVariable(env, "ZENITY_ERROR", "2", true);
SDL_SetEnvironmentVariable(env, "ZENITY_TIMEOUT", "2", true);
SDL_PropertiesID props = SDL_CreateProperties();
SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, argv);
SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ENVIRONMENT_POINTER, env);
SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDIN_NUMBER, SDL_PROCESS_STDIO_NULL);
SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_APP);
SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER, SDL_PROCESS_STDIO_NULL);
process = SDL_CreateProcessWithProperties(props);
SDL_DestroyProperties(props);
if (!process) {
goto done;
}
container = SDL_ReadProcess(process, &bytes_read, &status);
if (!container) {
goto done;
}
array = (char **)SDL_malloc((narray + 1) * sizeof(char *));
if (!array) {
goto done;
}
array[0] = container;
array[1] = NULL;
for (int i = 0; i < bytes_read; i++) {
if (container[i] == '\n') {
container[i] = '\0';
// Reading from a process often leaves a trailing \n, so ignore the last one
if (i < bytes_read - 1) {
array[narray] = container + i + 1;
narray++;
char **new_array = (char **)SDL_realloc(array, (narray + 1) * sizeof(char *));
if (!new_array) {
goto done;
}
array = new_array;
array[narray] = NULL;
}
}
}
// 0 = the user chose one or more files, 1 = the user canceled the dialog
if (status == 0 || status == 1) {
callback(userdata, (const char *const *)array, -1);
} else {
SDL_SetError("Could not run zenity: exit code %d", status);
callback(userdata, NULL, -1);
}
result = true;
done:
SDL_free(array);
SDL_free(container);
SDL_DestroyEnvironment(env);
SDL_DestroyProcess(process);
if (!result) {
callback(userdata, NULL, -1);
}
}
static void free_zenity_args(zenityArgs *args)
{
if (args->filters_slice) {
for (int i = 0; i < args->nfilters; i++) {
SDL_free(args->filters_slice[i]);
}
}
SDL_free(args->filename);
SDL_free(args->title);
SDL_free(args->accept);
SDL_free(args->cancel);
SDL_free(args->argv);
SDL_free(args);
}
static int run_zenity_thread(void *ptr)
{
zenityArgs *args = ptr;
run_zenity(args->callback, args->userdata, args->argv);
free_zenity_args(args);
return 0;
}
void SDL_Zenity_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props)
{
zenityArgs *args = create_zenity_args(type, callback, userdata, props);
if (!args) {
callback(userdata, NULL, -1);
return;
}
SDL_Thread *thread = SDL_CreateThread(run_zenity_thread, "SDL_ZenityFileDialog", (void *)args);
if (!thread) {
free_zenity_args(args);
callback(userdata, NULL, -1);
return;
}
SDL_DetachThread(thread);
}
bool SDL_Zenity_detect(void)
{
const char *args[] = {
"zenity", "--version", NULL
};
int status = -1;
SDL_PropertiesID props = SDL_CreateProperties();
SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, args);
SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDIN_NUMBER, SDL_PROCESS_STDIO_NULL);
SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_NULL);
SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER, SDL_PROCESS_STDIO_NULL);
SDL_Process *process = SDL_CreateProcessWithProperties(props);
SDL_DestroyProperties(props);
if (process) {
SDL_WaitProcess(process, true, &status);
SDL_DestroyProcess(process);
}
return (status == 0);
}

View file

@ -0,0 +1,27 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 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"
extern void SDL_Zenity_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props);
/** @returns non-zero if available, zero if unavailable */
extern bool SDL_Zenity_detect(void);

View file

@ -0,0 +1,611 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 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.h"
#include "../SDL_dialog_utils.h"
#include <windows.h>
#include <commdlg.h>
#include <shlobj.h>
#include "../../core/windows/SDL_windows.h"
#include "../../thread/SDL_systhread.h"
// If this number is too small, selecting too many files will give an error
#define SELECTLIST_SIZE 65536
typedef struct
{
bool is_save;
wchar_t *filters_str;
char *default_file;
SDL_Window *parent;
DWORD flags;
SDL_DialogFileCallback callback;
void *userdata;
char *title;
char *accept;
char *cancel;
} winArgs;
typedef struct
{
SDL_Window *parent;
SDL_DialogFileCallback callback;
char *default_folder;
void *userdata;
char *title;
char *accept;
char *cancel;
} winFArgs;
void freeWinArgs(winArgs *args)
{
SDL_free(args->default_file);
SDL_free(args->filters_str);
SDL_free(args->title);
SDL_free(args->accept);
SDL_free(args->cancel);
SDL_free(args);
}
void freeWinFArgs(winFArgs *args)
{
SDL_free(args->default_folder);
SDL_free(args->title);
SDL_free(args->accept);
SDL_free(args->cancel);
SDL_free(args);
}
/** Converts dialog.nFilterIndex to SDL-compatible value */
int getFilterIndex(int as_reported_by_windows)
{
return as_reported_by_windows - 1;
}
char *clear_filt_names(const char *filt)
{
char *cleared = SDL_strdup(filt);
for (char *c = cleared; *c; c++) {
/* 0x01 bytes are used as temporary replacement for the various 0x00
bytes required by Win32 (one null byte between each filter, two at
the end of the filters). Filter out these bytes from the filter names
to avoid early-ending the filters if someone puts two consecutive
0x01 bytes in their filter names. */
if (*c == '\x01') {
*c = ' ';
}
}
return cleared;
}
// TODO: The new version of file dialogs
void windows_ShowFileDialog(void *ptr)
{
winArgs *args = (winArgs *) ptr;
bool is_save = args->is_save;
const char *default_file = args->default_file;
SDL_Window *parent = args->parent;
DWORD flags = args->flags;
SDL_DialogFileCallback callback = args->callback;
void *userdata = args->userdata;
const char *title = args->title;
wchar_t *filter_wchar = args->filters_str;
/* GetOpenFileName and GetSaveFileName have the same signature
(yes, LPOPENFILENAMEW even for the save dialog) */
typedef BOOL (WINAPI *pfnGetAnyFileNameW)(LPOPENFILENAMEW);
typedef DWORD (WINAPI *pfnCommDlgExtendedError)(void);
HMODULE lib = LoadLibraryW(L"Comdlg32.dll");
pfnGetAnyFileNameW pGetAnyFileName = NULL;
pfnCommDlgExtendedError pCommDlgExtendedError = NULL;
if (lib) {
pGetAnyFileName = (pfnGetAnyFileNameW) GetProcAddress(lib, is_save ? "GetSaveFileNameW" : "GetOpenFileNameW");
pCommDlgExtendedError = (pfnCommDlgExtendedError) GetProcAddress(lib, "CommDlgExtendedError");
} else {
SDL_SetError("Couldn't load Comdlg32.dll");
callback(userdata, NULL, -1);
return;
}
if (!pGetAnyFileName) {
SDL_SetError("Couldn't load GetOpenFileName/GetSaveFileName from library");
callback(userdata, NULL, -1);
return;
}
if (!pCommDlgExtendedError) {
SDL_SetError("Couldn't load CommDlgExtendedError from library");
callback(userdata, NULL, -1);
return;
}
HWND window = NULL;
if (parent) {
window = (HWND) SDL_GetPointerProperty(SDL_GetWindowProperties(parent), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
}
wchar_t *filebuffer; // lpstrFile
wchar_t initfolder[MAX_PATH] = L""; // lpstrInitialDir
/* If SELECTLIST_SIZE is too large, putting filebuffer on the stack might
cause an overflow */
filebuffer = (wchar_t *) SDL_malloc(SELECTLIST_SIZE * sizeof(wchar_t));
// Necessary for the return code below
SDL_memset(filebuffer, 0, SELECTLIST_SIZE * sizeof(wchar_t));
if (default_file) {
/* On Windows 10, 11 and possibly others, lpstrFile can be initialized
with a path and the dialog will start at that location, but *only if
the path contains a filename*. If it ends with a folder (directory
separator), it fails with 0x3002 (12290) FNERR_INVALIDFILENAME. For
that specific case, lpstrInitialDir must be used instead, but just
for that case, because lpstrInitialDir doesn't support file names.
On top of that, lpstrInitialDir hides a special algorithm that
decides which folder to actually use as starting point, which may or
may not be the one provided, or some other unrelated folder. Also,
the algorithm changes between platforms. Assuming the documentation
is correct, the algorithm is there under 'lpstrInitialDir':
https://learn.microsoft.com/en-us/windows/win32/api/commdlg/ns-commdlg-openfilenamew
Finally, lpstrFile does not support forward slashes. lpstrInitialDir
does, though. */
char last_c = default_file[SDL_strlen(default_file) - 1];
if (last_c == '\\' || last_c == '/') {
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, default_file, -1, initfolder, MAX_PATH);
} else {
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, default_file, -1, filebuffer, MAX_PATH);
for (int i = 0; i < SELECTLIST_SIZE; i++) {
if (filebuffer[i] == L'/') {
filebuffer[i] = L'\\';
}
}
}
}
wchar_t *title_w = NULL;
if (title) {
title_w = WIN_UTF8ToStringW(title);
if (!title_w) {
SDL_free(filebuffer);
callback(userdata, NULL, -1);
return;
}
}
OPENFILENAMEW dialog;
dialog.lStructSize = sizeof(OPENFILENAME);
dialog.hwndOwner = window;
dialog.hInstance = 0;
dialog.lpstrFilter = filter_wchar;
dialog.lpstrCustomFilter = NULL;
dialog.nMaxCustFilter = 0;
dialog.nFilterIndex = 0;
dialog.lpstrFile = filebuffer;
dialog.nMaxFile = SELECTLIST_SIZE;
dialog.lpstrFileTitle = NULL;
dialog.lpstrInitialDir = *initfolder ? initfolder : NULL;
dialog.lpstrTitle = title_w;
dialog.Flags = flags | OFN_EXPLORER | OFN_HIDEREADONLY | OFN_NOCHANGEDIR;
dialog.nFileOffset = 0;
dialog.nFileExtension = 0;
dialog.lpstrDefExt = NULL;
dialog.lCustData = 0;
dialog.lpfnHook = NULL;
dialog.lpTemplateName = NULL;
// Skipped many mac-exclusive and reserved members
dialog.FlagsEx = 0;
BOOL result = pGetAnyFileName(&dialog);
SDL_free(title_w);
if (result) {
if (!(flags & OFN_ALLOWMULTISELECT)) {
// File is a C string stored in dialog.lpstrFile
char *chosen_file = WIN_StringToUTF8W(dialog.lpstrFile);
const char *opts[2] = { chosen_file, NULL };
callback(userdata, opts, getFilterIndex(dialog.nFilterIndex));
SDL_free(chosen_file);
} else {
/* File is either a C string if the user chose a single file, else
it's a series of strings formatted like:
"C:\\path\\to\\folder\0filename1.ext\0filename2.ext\0\0"
The code below will only stop on a double NULL in all cases, so
it is important that the rest of the buffer has been zeroed. */
char chosen_folder[MAX_PATH];
char chosen_file[MAX_PATH];
wchar_t *file_ptr = dialog.lpstrFile;
size_t nfiles = 0;
size_t chosen_folder_size;
char **chosen_files_list = (char **) SDL_malloc(sizeof(char *) * (nfiles + 1));
if (!chosen_files_list) {
callback(userdata, NULL, -1);
SDL_free(filebuffer);
return;
}
chosen_files_list[nfiles] = NULL;
if (WideCharToMultiByte(CP_UTF8, 0, file_ptr, -1, chosen_folder, MAX_PATH, NULL, NULL) >= MAX_PATH) {
SDL_SetError("Path too long or invalid character in path");
SDL_free(chosen_files_list);
callback(userdata, NULL, -1);
SDL_free(filebuffer);
return;
}
chosen_folder_size = SDL_strlen(chosen_folder);
SDL_strlcpy(chosen_file, chosen_folder, MAX_PATH);
chosen_file[chosen_folder_size] = '\\';
file_ptr += SDL_strlen(chosen_folder) + 1;
while (*file_ptr) {
nfiles++;
char **new_cfl = (char **) SDL_realloc(chosen_files_list, sizeof(char*) * (nfiles + 1));
if (!new_cfl) {
for (size_t i = 0; i < nfiles - 1; i++) {
SDL_free(chosen_files_list[i]);
}
SDL_free(chosen_files_list);
callback(userdata, NULL, -1);
SDL_free(filebuffer);
return;
}
chosen_files_list = new_cfl;
chosen_files_list[nfiles] = NULL;
int diff = ((int) chosen_folder_size) + 1;
if (WideCharToMultiByte(CP_UTF8, 0, file_ptr, -1, chosen_file + diff, MAX_PATH - diff, NULL, NULL) >= MAX_PATH - diff) {
SDL_SetError("Path too long or invalid character in path");
for (size_t i = 0; i < nfiles - 1; i++) {
SDL_free(chosen_files_list[i]);
}
SDL_free(chosen_files_list);
callback(userdata, NULL, -1);
SDL_free(filebuffer);
return;
}
file_ptr += SDL_strlen(chosen_file) + 1 - diff;
chosen_files_list[nfiles - 1] = SDL_strdup(chosen_file);
if (!chosen_files_list[nfiles - 1]) {
for (size_t i = 0; i < nfiles - 1; i++) {
SDL_free(chosen_files_list[i]);
}
SDL_free(chosen_files_list);
callback(userdata, NULL, -1);
SDL_free(filebuffer);
return;
}
}
// If the user chose only one file, it's all just one string
if (nfiles == 0) {
nfiles++;
char **new_cfl = (char **) SDL_realloc(chosen_files_list, sizeof(char*) * (nfiles + 1));
if (!new_cfl) {
SDL_free(chosen_files_list);
callback(userdata, NULL, -1);
SDL_free(filebuffer);
return;
}
chosen_files_list = new_cfl;
chosen_files_list[nfiles] = NULL;
chosen_files_list[nfiles - 1] = SDL_strdup(chosen_folder);
if (!chosen_files_list[nfiles - 1]) {
SDL_free(chosen_files_list);
callback(userdata, NULL, -1);
SDL_free(filebuffer);
return;
}
}
callback(userdata, (const char * const*) chosen_files_list, getFilterIndex(dialog.nFilterIndex));
for (size_t i = 0; i < nfiles; i++) {
SDL_free(chosen_files_list[i]);
}
SDL_free(chosen_files_list);
}
} else {
DWORD error = pCommDlgExtendedError();
// Error code 0 means the user clicked the cancel button.
if (error == 0) {
/* Unlike SDL's handling of errors, Windows does reset the error
code to 0 after calling GetOpenFileName if another Windows
function before set a different error code, so it's safe to
check for success. */
const char *opts[1] = { NULL };
callback(userdata, opts, getFilterIndex(dialog.nFilterIndex));
} else {
SDL_SetError("Windows error, CommDlgExtendedError: %ld", pCommDlgExtendedError());
callback(userdata, NULL, -1);
}
}
SDL_free(filebuffer);
}
int windows_file_dialog_thread(void *ptr)
{
windows_ShowFileDialog(ptr);
freeWinArgs(ptr);
return 0;
}
int CALLBACK browse_callback_proc(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData)
{
switch (uMsg) {
case BFFM_INITIALIZED:
if (lpData) {
SendMessage(hwnd, BFFM_SETSELECTION, TRUE, lpData);
}
break;
case BFFM_SELCHANGED:
break;
case BFFM_VALIDATEFAILED:
break;
default:
break;
}
return 0;
}
void windows_ShowFolderDialog(void *ptr)
{
winFArgs *args = (winFArgs *) ptr;
SDL_Window *window = args->parent;
SDL_DialogFileCallback callback = args->callback;
void *userdata = args->userdata;
HWND parent = NULL;
const char *title = args->title;
if (window) {
parent = (HWND) SDL_GetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
}
wchar_t *title_w = NULL;
if (title) {
title_w = WIN_UTF8ToStringW(title);
if (!title_w) {
callback(userdata, NULL, -1);
return;
}
}
wchar_t buffer[MAX_PATH];
BROWSEINFOW dialog;
dialog.hwndOwner = parent;
dialog.pidlRoot = NULL;
dialog.pszDisplayName = buffer;
dialog.lpszTitle = title_w;
dialog.ulFlags = BIF_USENEWUI;
dialog.lpfn = browse_callback_proc;
dialog.lParam = (LPARAM)args->default_folder;
dialog.iImage = 0;
LPITEMIDLIST lpItem = SHBrowseForFolderW(&dialog);
SDL_free(title_w);
if (lpItem != NULL) {
SHGetPathFromIDListW(lpItem, buffer);
char *chosen_file = WIN_StringToUTF8W(buffer);
const char *files[2] = { chosen_file, NULL };
callback(userdata, (const char * const*) files, -1);
SDL_free(chosen_file);
} else {
const char *files[1] = { NULL };
callback(userdata, (const char * const*) files, -1);
}
}
int windows_folder_dialog_thread(void *ptr)
{
windows_ShowFolderDialog(ptr);
freeWinFArgs((winFArgs *)ptr);
return 0;
}
wchar_t *win_get_filters(const SDL_DialogFileFilter *filters, int nfilters)
{
wchar_t *filter_wchar = NULL;
if (filters) {
// '\x01' is used in place of a null byte
// suffix needs two null bytes in case the filter list is empty
char *filterlist = convert_filters(filters, nfilters, clear_filt_names,
"", "", "\x01\x01", "", "\x01",
"\x01", "*.", ";*.", "");
if (!filterlist) {
return NULL;
}
int filter_len = (int)SDL_strlen(filterlist);
for (char *c = filterlist; *c; c++) {
if (*c == '\x01') {
*c = '\0';
}
}
int filter_wlen = MultiByteToWideChar(CP_UTF8, 0, filterlist, filter_len, NULL, 0);
filter_wchar = (wchar_t *)SDL_malloc(filter_wlen * sizeof(wchar_t));
if (!filter_wchar) {
SDL_free(filterlist);
return NULL;
}
MultiByteToWideChar(CP_UTF8, 0, filterlist, filter_len, filter_wchar, filter_wlen);
SDL_free(filterlist);
}
return filter_wchar;
}
static void ShowFileDialog(SDL_DialogFileCallback callback, void *userdata, SDL_Window *window, const SDL_DialogFileFilter *filters, int nfilters, const char *default_location, bool allow_many, bool is_save, const char *title, const char *accept, const char *cancel)
{
winArgs *args;
SDL_Thread *thread;
wchar_t *filters_str;
if (SDL_GetHint(SDL_HINT_FILE_DIALOG_DRIVER) != NULL) {
SDL_SetError("File dialog driver unsupported");
callback(userdata, NULL, -1);
return;
}
args = (winArgs *)SDL_malloc(sizeof(*args));
if (args == NULL) {
callback(userdata, NULL, -1);
return;
}
filters_str = win_get_filters(filters, nfilters);
if (!filters_str && filters) {
callback(userdata, NULL, -1);
SDL_free(args);
return;
}
args->is_save = is_save;
args->filters_str = filters_str;
args->default_file = default_location ? SDL_strdup(default_location) : NULL;
args->parent = window;
args->flags = allow_many ? OFN_ALLOWMULTISELECT : 0;
args->callback = callback;
args->userdata = userdata;
args->title = title ? SDL_strdup(title) : NULL;
args->accept = accept ? SDL_strdup(accept) : NULL;
args->cancel = cancel ? SDL_strdup(cancel) : NULL;
thread = SDL_CreateThread(windows_file_dialog_thread, "SDL_Windows_ShowFileDialog", (void *) args);
if (thread == NULL) {
callback(userdata, NULL, -1);
// The thread won't have run, therefore the data won't have been freed
freeWinArgs(args);
return;
}
SDL_DetachThread(thread);
}
void ShowFolderDialog(SDL_DialogFileCallback callback, void *userdata, SDL_Window *window, const char *default_location, bool allow_many, const char *title, const char *accept, const char *cancel)
{
winFArgs *args;
SDL_Thread *thread;
if (SDL_GetHint(SDL_HINT_FILE_DIALOG_DRIVER) != NULL) {
SDL_SetError("File dialog driver unsupported");
callback(userdata, NULL, -1);
return;
}
args = (winFArgs *)SDL_malloc(sizeof(*args));
if (args == NULL) {
callback(userdata, NULL, -1);
return;
}
args->parent = window;
args->callback = callback;
args->default_folder = default_location ? SDL_strdup(default_location) : NULL;
args->userdata = userdata;
args->title = title ? SDL_strdup(title) : NULL;
args->accept = accept ? SDL_strdup(accept) : NULL;
args->cancel = cancel ? SDL_strdup(cancel) : NULL;
thread = SDL_CreateThread(windows_folder_dialog_thread, "SDL_Windows_ShowFolderDialog", (void *) args);
if (thread == NULL) {
callback(userdata, NULL, -1);
// The thread won't have run, therefore the data won't have been freed
freeWinFArgs(args);
return;
}
SDL_DetachThread(thread);
}
void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props)
{
/* The internal functions will start threads, and the properties may be freed as soon as this function returns.
Save a copy of what we need before invoking the functions and starting the threads. */
SDL_Window *window = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, NULL);
SDL_DialogFileFilter *filters = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_FILTERS_POINTER, NULL);
int nfilters = (int) SDL_GetNumberProperty(props, SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, 0);
bool allow_many = SDL_GetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, false);
const char *default_location = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, NULL);
const char *title = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_TITLE_STRING, NULL);
const char *accept = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_ACCEPT_STRING, NULL);
const char *cancel = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_CANCEL_STRING, NULL);
bool is_save = false;
switch (type) {
case SDL_FILEDIALOG_SAVEFILE:
is_save = true;
SDL_FALLTHROUGH;
case SDL_FILEDIALOG_OPENFILE:
ShowFileDialog(callback, userdata, window, filters, nfilters, default_location, allow_many, is_save, title, accept, cancel);
break;
case SDL_FILEDIALOG_OPENFOLDER:
ShowFolderDialog(callback, userdata, window, default_location, allow_many, title, accept, cancel);
break;
};
}