summaryrefslogtreecommitdiff
path: root/contrib/SDL-3.2.8/src/dialog/windows/SDL_windowsdialog.c
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/SDL-3.2.8/src/dialog/windows/SDL_windowsdialog.c')
-rw-r--r--contrib/SDL-3.2.8/src/dialog/windows/SDL_windowsdialog.c611
1 files changed, 611 insertions, 0 deletions
diff --git a/contrib/SDL-3.2.8/src/dialog/windows/SDL_windowsdialog.c b/contrib/SDL-3.2.8/src/dialog/windows/SDL_windowsdialog.c
new file mode 100644
index 0000000..2de224f
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/dialog/windows/SDL_windowsdialog.c
@@ -0,0 +1,611 @@
1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21#include "SDL_internal.h"
22#include "../SDL_dialog.h"
23#include "../SDL_dialog_utils.h"
24
25#include <windows.h>
26#include <commdlg.h>
27#include <shlobj.h>
28#include "../../core/windows/SDL_windows.h"
29#include "../../thread/SDL_systhread.h"
30
31// If this number is too small, selecting too many files will give an error
32#define SELECTLIST_SIZE 65536
33
34typedef struct
35{
36 bool is_save;
37 wchar_t *filters_str;
38 char *default_file;
39 SDL_Window *parent;
40 DWORD flags;
41 SDL_DialogFileCallback callback;
42 void *userdata;
43 char *title;
44 char *accept;
45 char *cancel;
46} winArgs;
47
48typedef struct
49{
50 SDL_Window *parent;
51 SDL_DialogFileCallback callback;
52 char *default_folder;
53 void *userdata;
54 char *title;
55 char *accept;
56 char *cancel;
57} winFArgs;
58
59void freeWinArgs(winArgs *args)
60{
61 SDL_free(args->default_file);
62 SDL_free(args->filters_str);
63 SDL_free(args->title);
64 SDL_free(args->accept);
65 SDL_free(args->cancel);
66
67 SDL_free(args);
68}
69
70void freeWinFArgs(winFArgs *args)
71{
72 SDL_free(args->default_folder);
73 SDL_free(args->title);
74 SDL_free(args->accept);
75 SDL_free(args->cancel);
76
77 SDL_free(args);
78}
79
80/** Converts dialog.nFilterIndex to SDL-compatible value */
81int getFilterIndex(int as_reported_by_windows)
82{
83 return as_reported_by_windows - 1;
84}
85
86char *clear_filt_names(const char *filt)
87{
88 char *cleared = SDL_strdup(filt);
89
90 for (char *c = cleared; *c; c++) {
91 /* 0x01 bytes are used as temporary replacement for the various 0x00
92 bytes required by Win32 (one null byte between each filter, two at
93 the end of the filters). Filter out these bytes from the filter names
94 to avoid early-ending the filters if someone puts two consecutive
95 0x01 bytes in their filter names. */
96 if (*c == '\x01') {
97 *c = ' ';
98 }
99 }
100
101 return cleared;
102}
103
104// TODO: The new version of file dialogs
105void windows_ShowFileDialog(void *ptr)
106{
107 winArgs *args = (winArgs *) ptr;
108 bool is_save = args->is_save;
109 const char *default_file = args->default_file;
110 SDL_Window *parent = args->parent;
111 DWORD flags = args->flags;
112 SDL_DialogFileCallback callback = args->callback;
113 void *userdata = args->userdata;
114 const char *title = args->title;
115 wchar_t *filter_wchar = args->filters_str;
116
117 /* GetOpenFileName and GetSaveFileName have the same signature
118 (yes, LPOPENFILENAMEW even for the save dialog) */
119 typedef BOOL (WINAPI *pfnGetAnyFileNameW)(LPOPENFILENAMEW);
120 typedef DWORD (WINAPI *pfnCommDlgExtendedError)(void);
121 HMODULE lib = LoadLibraryW(L"Comdlg32.dll");
122 pfnGetAnyFileNameW pGetAnyFileName = NULL;
123 pfnCommDlgExtendedError pCommDlgExtendedError = NULL;
124
125 if (lib) {
126 pGetAnyFileName = (pfnGetAnyFileNameW) GetProcAddress(lib, is_save ? "GetSaveFileNameW" : "GetOpenFileNameW");
127 pCommDlgExtendedError = (pfnCommDlgExtendedError) GetProcAddress(lib, "CommDlgExtendedError");
128 } else {
129 SDL_SetError("Couldn't load Comdlg32.dll");
130 callback(userdata, NULL, -1);
131 return;
132 }
133
134 if (!pGetAnyFileName) {
135 SDL_SetError("Couldn't load GetOpenFileName/GetSaveFileName from library");
136 callback(userdata, NULL, -1);
137 return;
138 }
139
140 if (!pCommDlgExtendedError) {
141 SDL_SetError("Couldn't load CommDlgExtendedError from library");
142 callback(userdata, NULL, -1);
143 return;
144 }
145
146 HWND window = NULL;
147
148 if (parent) {
149 window = (HWND) SDL_GetPointerProperty(SDL_GetWindowProperties(parent), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
150 }
151
152 wchar_t *filebuffer; // lpstrFile
153 wchar_t initfolder[MAX_PATH] = L""; // lpstrInitialDir
154
155 /* If SELECTLIST_SIZE is too large, putting filebuffer on the stack might
156 cause an overflow */
157 filebuffer = (wchar_t *) SDL_malloc(SELECTLIST_SIZE * sizeof(wchar_t));
158
159 // Necessary for the return code below
160 SDL_memset(filebuffer, 0, SELECTLIST_SIZE * sizeof(wchar_t));
161
162 if (default_file) {
163 /* On Windows 10, 11 and possibly others, lpstrFile can be initialized
164 with a path and the dialog will start at that location, but *only if
165 the path contains a filename*. If it ends with a folder (directory
166 separator), it fails with 0x3002 (12290) FNERR_INVALIDFILENAME. For
167 that specific case, lpstrInitialDir must be used instead, but just
168 for that case, because lpstrInitialDir doesn't support file names.
169
170 On top of that, lpstrInitialDir hides a special algorithm that
171 decides which folder to actually use as starting point, which may or
172 may not be the one provided, or some other unrelated folder. Also,
173 the algorithm changes between platforms. Assuming the documentation
174 is correct, the algorithm is there under 'lpstrInitialDir':
175
176 https://learn.microsoft.com/en-us/windows/win32/api/commdlg/ns-commdlg-openfilenamew
177
178 Finally, lpstrFile does not support forward slashes. lpstrInitialDir
179 does, though. */
180
181 char last_c = default_file[SDL_strlen(default_file) - 1];
182
183 if (last_c == '\\' || last_c == '/') {
184 MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, default_file, -1, initfolder, MAX_PATH);
185 } else {
186 MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, default_file, -1, filebuffer, MAX_PATH);
187
188 for (int i = 0; i < SELECTLIST_SIZE; i++) {
189 if (filebuffer[i] == L'/') {
190 filebuffer[i] = L'\\';
191 }
192 }
193 }
194 }
195
196 wchar_t *title_w = NULL;
197
198 if (title) {
199 title_w = WIN_UTF8ToStringW(title);
200 if (!title_w) {
201 SDL_free(filebuffer);
202 callback(userdata, NULL, -1);
203 return;
204 }
205 }
206
207 OPENFILENAMEW dialog;
208 dialog.lStructSize = sizeof(OPENFILENAME);
209 dialog.hwndOwner = window;
210 dialog.hInstance = 0;
211 dialog.lpstrFilter = filter_wchar;
212 dialog.lpstrCustomFilter = NULL;
213 dialog.nMaxCustFilter = 0;
214 dialog.nFilterIndex = 0;
215 dialog.lpstrFile = filebuffer;
216 dialog.nMaxFile = SELECTLIST_SIZE;
217 dialog.lpstrFileTitle = NULL;
218 dialog.lpstrInitialDir = *initfolder ? initfolder : NULL;
219 dialog.lpstrTitle = title_w;
220 dialog.Flags = flags | OFN_EXPLORER | OFN_HIDEREADONLY | OFN_NOCHANGEDIR;
221 dialog.nFileOffset = 0;
222 dialog.nFileExtension = 0;
223 dialog.lpstrDefExt = NULL;
224 dialog.lCustData = 0;
225 dialog.lpfnHook = NULL;
226 dialog.lpTemplateName = NULL;
227 // Skipped many mac-exclusive and reserved members
228 dialog.FlagsEx = 0;
229
230 BOOL result = pGetAnyFileName(&dialog);
231
232 SDL_free(title_w);
233
234 if (result) {
235 if (!(flags & OFN_ALLOWMULTISELECT)) {
236 // File is a C string stored in dialog.lpstrFile
237 char *chosen_file = WIN_StringToUTF8W(dialog.lpstrFile);
238 const char *opts[2] = { chosen_file, NULL };
239 callback(userdata, opts, getFilterIndex(dialog.nFilterIndex));
240 SDL_free(chosen_file);
241 } else {
242 /* File is either a C string if the user chose a single file, else
243 it's a series of strings formatted like:
244
245 "C:\\path\\to\\folder\0filename1.ext\0filename2.ext\0\0"
246
247 The code below will only stop on a double NULL in all cases, so
248 it is important that the rest of the buffer has been zeroed. */
249 char chosen_folder[MAX_PATH];
250 char chosen_file[MAX_PATH];
251 wchar_t *file_ptr = dialog.lpstrFile;
252 size_t nfiles = 0;
253 size_t chosen_folder_size;
254 char **chosen_files_list = (char **) SDL_malloc(sizeof(char *) * (nfiles + 1));
255
256 if (!chosen_files_list) {
257 callback(userdata, NULL, -1);
258 SDL_free(filebuffer);
259 return;
260 }
261
262 chosen_files_list[nfiles] = NULL;
263
264 if (WideCharToMultiByte(CP_UTF8, 0, file_ptr, -1, chosen_folder, MAX_PATH, NULL, NULL) >= MAX_PATH) {
265 SDL_SetError("Path too long or invalid character in path");
266 SDL_free(chosen_files_list);
267 callback(userdata, NULL, -1);
268 SDL_free(filebuffer);
269 return;
270 }
271
272 chosen_folder_size = SDL_strlen(chosen_folder);
273 SDL_strlcpy(chosen_file, chosen_folder, MAX_PATH);
274 chosen_file[chosen_folder_size] = '\\';
275
276 file_ptr += SDL_strlen(chosen_folder) + 1;
277
278 while (*file_ptr) {
279 nfiles++;
280 char **new_cfl = (char **) SDL_realloc(chosen_files_list, sizeof(char*) * (nfiles + 1));
281
282 if (!new_cfl) {
283 for (size_t i = 0; i < nfiles - 1; i++) {
284 SDL_free(chosen_files_list[i]);
285 }
286
287 SDL_free(chosen_files_list);
288 callback(userdata, NULL, -1);
289 SDL_free(filebuffer);
290 return;
291 }
292
293 chosen_files_list = new_cfl;
294 chosen_files_list[nfiles] = NULL;
295
296 int diff = ((int) chosen_folder_size) + 1;
297
298 if (WideCharToMultiByte(CP_UTF8, 0, file_ptr, -1, chosen_file + diff, MAX_PATH - diff, NULL, NULL) >= MAX_PATH - diff) {
299 SDL_SetError("Path too long or invalid character in path");
300
301 for (size_t i = 0; i < nfiles - 1; i++) {
302 SDL_free(chosen_files_list[i]);
303 }
304
305 SDL_free(chosen_files_list);
306 callback(userdata, NULL, -1);
307 SDL_free(filebuffer);
308 return;
309 }
310
311 file_ptr += SDL_strlen(chosen_file) + 1 - diff;
312
313 chosen_files_list[nfiles - 1] = SDL_strdup(chosen_file);
314
315 if (!chosen_files_list[nfiles - 1]) {
316 for (size_t i = 0; i < nfiles - 1; i++) {
317 SDL_free(chosen_files_list[i]);
318 }
319
320 SDL_free(chosen_files_list);
321 callback(userdata, NULL, -1);
322 SDL_free(filebuffer);
323 return;
324 }
325 }
326
327 // If the user chose only one file, it's all just one string
328 if (nfiles == 0) {
329 nfiles++;
330 char **new_cfl = (char **) SDL_realloc(chosen_files_list, sizeof(char*) * (nfiles + 1));
331
332 if (!new_cfl) {
333 SDL_free(chosen_files_list);
334 callback(userdata, NULL, -1);
335 SDL_free(filebuffer);
336 return;
337 }
338
339 chosen_files_list = new_cfl;
340 chosen_files_list[nfiles] = NULL;
341 chosen_files_list[nfiles - 1] = SDL_strdup(chosen_folder);
342
343 if (!chosen_files_list[nfiles - 1]) {
344 SDL_free(chosen_files_list);
345 callback(userdata, NULL, -1);
346 SDL_free(filebuffer);
347 return;
348 }
349 }
350
351 callback(userdata, (const char * const*) chosen_files_list, getFilterIndex(dialog.nFilterIndex));
352
353 for (size_t i = 0; i < nfiles; i++) {
354 SDL_free(chosen_files_list[i]);
355 }
356
357 SDL_free(chosen_files_list);
358 }
359 } else {
360 DWORD error = pCommDlgExtendedError();
361 // Error code 0 means the user clicked the cancel button.
362 if (error == 0) {
363 /* Unlike SDL's handling of errors, Windows does reset the error
364 code to 0 after calling GetOpenFileName if another Windows
365 function before set a different error code, so it's safe to
366 check for success. */
367 const char *opts[1] = { NULL };
368 callback(userdata, opts, getFilterIndex(dialog.nFilterIndex));
369 } else {
370 SDL_SetError("Windows error, CommDlgExtendedError: %ld", pCommDlgExtendedError());
371 callback(userdata, NULL, -1);
372 }
373 }
374
375 SDL_free(filebuffer);
376}
377
378int windows_file_dialog_thread(void *ptr)
379{
380 windows_ShowFileDialog(ptr);
381 freeWinArgs(ptr);
382 return 0;
383}
384
385int CALLBACK browse_callback_proc(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData)
386{
387 switch (uMsg) {
388 case BFFM_INITIALIZED:
389 if (lpData) {
390 SendMessage(hwnd, BFFM_SETSELECTION, TRUE, lpData);
391 }
392 break;
393 case BFFM_SELCHANGED:
394 break;
395 case BFFM_VALIDATEFAILED:
396 break;
397 default:
398 break;
399 }
400 return 0;
401}
402
403void windows_ShowFolderDialog(void *ptr)
404{
405 winFArgs *args = (winFArgs *) ptr;
406 SDL_Window *window = args->parent;
407 SDL_DialogFileCallback callback = args->callback;
408 void *userdata = args->userdata;
409 HWND parent = NULL;
410 const char *title = args->title;
411
412 if (window) {
413 parent = (HWND) SDL_GetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
414 }
415
416 wchar_t *title_w = NULL;
417
418 if (title) {
419 title_w = WIN_UTF8ToStringW(title);
420 if (!title_w) {
421 callback(userdata, NULL, -1);
422 return;
423 }
424 }
425
426 wchar_t buffer[MAX_PATH];
427
428 BROWSEINFOW dialog;
429 dialog.hwndOwner = parent;
430 dialog.pidlRoot = NULL;
431 dialog.pszDisplayName = buffer;
432 dialog.lpszTitle = title_w;
433 dialog.ulFlags = BIF_USENEWUI;
434 dialog.lpfn = browse_callback_proc;
435 dialog.lParam = (LPARAM)args->default_folder;
436 dialog.iImage = 0;
437
438 LPITEMIDLIST lpItem = SHBrowseForFolderW(&dialog);
439
440 SDL_free(title_w);
441
442 if (lpItem != NULL) {
443 SHGetPathFromIDListW(lpItem, buffer);
444 char *chosen_file = WIN_StringToUTF8W(buffer);
445 const char *files[2] = { chosen_file, NULL };
446 callback(userdata, (const char * const*) files, -1);
447 SDL_free(chosen_file);
448 } else {
449 const char *files[1] = { NULL };
450 callback(userdata, (const char * const*) files, -1);
451 }
452}
453
454int windows_folder_dialog_thread(void *ptr)
455{
456 windows_ShowFolderDialog(ptr);
457 freeWinFArgs((winFArgs *)ptr);
458 return 0;
459}
460
461wchar_t *win_get_filters(const SDL_DialogFileFilter *filters, int nfilters)
462{
463 wchar_t *filter_wchar = NULL;
464
465 if (filters) {
466 // '\x01' is used in place of a null byte
467 // suffix needs two null bytes in case the filter list is empty
468 char *filterlist = convert_filters(filters, nfilters, clear_filt_names,
469 "", "", "\x01\x01", "", "\x01",
470 "\x01", "*.", ";*.", "");
471
472 if (!filterlist) {
473 return NULL;
474 }
475
476 int filter_len = (int)SDL_strlen(filterlist);
477
478 for (char *c = filterlist; *c; c++) {
479 if (*c == '\x01') {
480 *c = '\0';
481 }
482 }
483
484 int filter_wlen = MultiByteToWideChar(CP_UTF8, 0, filterlist, filter_len, NULL, 0);
485 filter_wchar = (wchar_t *)SDL_malloc(filter_wlen * sizeof(wchar_t));
486 if (!filter_wchar) {
487 SDL_free(filterlist);
488 return NULL;
489 }
490
491 MultiByteToWideChar(CP_UTF8, 0, filterlist, filter_len, filter_wchar, filter_wlen);
492
493 SDL_free(filterlist);
494 }
495
496 return filter_wchar;
497}
498
499static 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)
500{
501 winArgs *args;
502 SDL_Thread *thread;
503 wchar_t *filters_str;
504
505 if (SDL_GetHint(SDL_HINT_FILE_DIALOG_DRIVER) != NULL) {
506 SDL_SetError("File dialog driver unsupported");
507 callback(userdata, NULL, -1);
508 return;
509 }
510
511 args = (winArgs *)SDL_malloc(sizeof(*args));
512 if (args == NULL) {
513 callback(userdata, NULL, -1);
514 return;
515 }
516
517 filters_str = win_get_filters(filters, nfilters);
518
519 if (!filters_str && filters) {
520 callback(userdata, NULL, -1);
521 SDL_free(args);
522 return;
523 }
524
525 args->is_save = is_save;
526 args->filters_str = filters_str;
527 args->default_file = default_location ? SDL_strdup(default_location) : NULL;
528 args->parent = window;
529 args->flags = allow_many ? OFN_ALLOWMULTISELECT : 0;
530 args->callback = callback;
531 args->userdata = userdata;
532 args->title = title ? SDL_strdup(title) : NULL;
533 args->accept = accept ? SDL_strdup(accept) : NULL;
534 args->cancel = cancel ? SDL_strdup(cancel) : NULL;
535
536 thread = SDL_CreateThread(windows_file_dialog_thread, "SDL_Windows_ShowFileDialog", (void *) args);
537
538 if (thread == NULL) {
539 callback(userdata, NULL, -1);
540 // The thread won't have run, therefore the data won't have been freed
541 freeWinArgs(args);
542 return;
543 }
544
545 SDL_DetachThread(thread);
546}
547
548void 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)
549{
550 winFArgs *args;
551 SDL_Thread *thread;
552
553 if (SDL_GetHint(SDL_HINT_FILE_DIALOG_DRIVER) != NULL) {
554 SDL_SetError("File dialog driver unsupported");
555 callback(userdata, NULL, -1);
556 return;
557 }
558
559 args = (winFArgs *)SDL_malloc(sizeof(*args));
560 if (args == NULL) {
561 callback(userdata, NULL, -1);
562 return;
563 }
564
565 args->parent = window;
566 args->callback = callback;
567 args->default_folder = default_location ? SDL_strdup(default_location) : NULL;
568 args->userdata = userdata;
569 args->title = title ? SDL_strdup(title) : NULL;
570 args->accept = accept ? SDL_strdup(accept) : NULL;
571 args->cancel = cancel ? SDL_strdup(cancel) : NULL;
572
573 thread = SDL_CreateThread(windows_folder_dialog_thread, "SDL_Windows_ShowFolderDialog", (void *) args);
574
575 if (thread == NULL) {
576 callback(userdata, NULL, -1);
577 // The thread won't have run, therefore the data won't have been freed
578 freeWinFArgs(args);
579 return;
580 }
581
582 SDL_DetachThread(thread);
583}
584
585void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props)
586{
587 /* The internal functions will start threads, and the properties may be freed as soon as this function returns.
588 Save a copy of what we need before invoking the functions and starting the threads. */
589 SDL_Window *window = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, NULL);
590 SDL_DialogFileFilter *filters = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_FILTERS_POINTER, NULL);
591 int nfilters = (int) SDL_GetNumberProperty(props, SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, 0);
592 bool allow_many = SDL_GetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, false);
593 const char *default_location = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, NULL);
594 const char *title = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_TITLE_STRING, NULL);
595 const char *accept = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_ACCEPT_STRING, NULL);
596 const char *cancel = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_CANCEL_STRING, NULL);
597 bool is_save = false;
598
599 switch (type) {
600 case SDL_FILEDIALOG_SAVEFILE:
601 is_save = true;
602 SDL_FALLTHROUGH;
603 case SDL_FILEDIALOG_OPENFILE:
604 ShowFileDialog(callback, userdata, window, filters, nfilters, default_location, allow_many, is_save, title, accept, cancel);
605 break;
606
607 case SDL_FILEDIALOG_OPENFOLDER:
608 ShowFolderDialog(callback, userdata, window, default_location, allow_many, title, accept, cancel);
609 break;
610 };
611}