yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
file_dialog.mm
Go to the documentation of this file.
1#include "util/file_util.h"
2
3#include <filesystem>
4#include <iostream>
5#include <memory>
6#include <string>
7#include <vector>
8
9#include "core/features.h"
10#include "util/platform_paths.h"
11
12#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
13#include <nfd.h>
14#endif
15
16#if defined(__APPLE__) && defined(__MACH__)
17/* Apple OSX and iOS (Darwin). */
18#include <Foundation/Foundation.h>
19#include <TargetConditionals.h>
20
21#import <CoreText/CoreText.h>
22
23#if TARGET_IPHONE_SIMULATOR == 1 || TARGET_OS_IPHONE == 1
24/* iOS in Xcode simulator */
25#import <dispatch/dispatch.h>
26#import <UIKit/UIKit.h>
27#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
28
30
31@interface AppDelegate : UIResponder <UIApplicationDelegate, UIDocumentPickerDelegate>
32@end
33
34@interface AppDelegate (FileDialog)
35- (void)PresentDocumentPickerWithCompletionHandler:
36 (void (^)(NSString *selectedFile))completionHandler
37 allowedTypes:(NSArray<UTType*> *)allowedTypes;
38@end
39
40namespace {
41std::string TrimCopy(const std::string& input) {
42 const auto start = input.find_first_not_of(" \t\n\r");
43 if (start == std::string::npos) {
44 return "";
45 }
46 const auto end = input.find_last_not_of(" \t\n\r");
47 return input.substr(start, end - start + 1);
48}
49
50std::vector<std::string> SplitFilterSpec(const std::string& spec) {
51 std::vector<std::string> tokens;
52 std::string current;
53 for (char ch : spec) {
54 if (ch == ',') {
55 std::string trimmed = TrimCopy(current);
56 if (!trimmed.empty() && trimmed[0] == '.') {
57 trimmed.erase(0, 1);
58 }
59 if (!trimmed.empty()) {
60 tokens.push_back(trimmed);
61 }
62 current.clear();
63 } else {
64 current.push_back(ch);
65 }
66 }
67 std::string trimmed = TrimCopy(current);
68 if (!trimmed.empty() && trimmed[0] == '.') {
69 trimmed.erase(0, 1);
70 }
71 if (!trimmed.empty()) {
72 tokens.push_back(trimmed);
73 }
74 return tokens;
75}
76
77NSArray<UTType*>* BuildAllowedTypes(const yaze::util::FileDialogOptions& options) {
78 if (options.filters.empty()) {
79 return @[ UTTypeData ];
80 }
81
82 bool allow_all = false;
83 NSMutableArray<UTType*>* types = [NSMutableArray array];
84
85 for (const auto& filter : options.filters) {
86 const std::string spec = TrimCopy(filter.spec);
87 if (spec.empty() || spec == "*") {
88 allow_all = true;
89 continue;
90 }
91
92 for (const auto& token : SplitFilterSpec(spec)) {
93 if (token == "*") {
94 allow_all = true;
95 continue;
96 }
97
98 NSString* ext = [NSString stringWithUTF8String:token.c_str()];
99 UTType* type = [UTType typeWithFilenameExtension:ext];
100 if (!type) {
101 NSString* identifier = [NSString stringWithUTF8String:token.c_str()];
102 type = [UTType typeWithIdentifier:identifier];
103 }
104 if (type) {
105 [types addObject:type];
106 }
107 }
108 }
109
110 if (allow_all || [types count] == 0) {
111 return @[ UTTypeData ];
112 }
113
114 return types;
115}
116
117std::filesystem::path ResolveDocumentsPath() {
119 if (docs_result.ok()) {
120 return *docs_result;
121 }
123 if (temp_result.ok()) {
124 return *temp_result;
125 }
126 std::error_code ec;
127 auto cwd = std::filesystem::current_path(ec);
128 if (!ec) {
129 return cwd;
130 }
131 return std::filesystem::path(".");
132}
133
134std::string NormalizeExtension(const std::string& ext) {
135 if (ext.empty()) {
136 return "";
137 }
138 if (ext.front() == '.') {
139 return ext;
140 }
141 return "." + ext;
142}
143
144std::string BuildSaveFilename(const std::string& default_name,
145 const std::string& default_extension) {
146 std::string name = default_name.empty() ? "yaze_output" : default_name;
147 const std::string normalized_ext = NormalizeExtension(default_extension);
148 if (!normalized_ext.empty()) {
149 auto dot_pos = name.find_last_of('.');
150 if (dot_pos == std::string::npos || dot_pos == 0) {
151 name += normalized_ext;
152 }
153 }
154 return name;
155}
156
157void ShowOpenFileDialogImpl(NSArray<UTType*>* allowed_types,
158 void (^completionHandler)(std::string)) {
159 AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
160 if (!appDelegate) {
161 completionHandler("");
162 return;
163 }
164 [appDelegate PresentDocumentPickerWithCompletionHandler:^(NSString *filePath) {
165 completionHandler(std::string([filePath UTF8String]));
166 }
167 allowedTypes:allowed_types];
168}
169
170std::string ShowOpenFileDialogSync(
171 const yaze::util::FileDialogOptions& options) {
172 __block std::string result;
173 __block bool done = false;
174 NSArray<UTType*>* allowed_types = BuildAllowedTypes(options);
175
176 auto present_picker = ^{
177 ShowOpenFileDialogImpl(allowed_types, ^(std::string filePath) {
178 result = filePath;
179 done = true;
180 });
181 };
182
183 if ([NSThread isMainThread]) {
184 present_picker();
185 // Run a nested loop to keep UI responsive while waiting on selection.
186 while (!done) {
187 [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
188 beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
189 }
190 } else {
191 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
192 dispatch_async(dispatch_get_main_queue(), ^{
193 ShowOpenFileDialogImpl(allowed_types, ^(std::string filePath) {
194 result = filePath;
195 dispatch_semaphore_signal(semaphore);
196 });
197 });
198 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
199 }
200
201 return result;
202}
203} // namespace
204
206 const FileDialogOptions& options) {
207 return ShowOpenFileDialogSync(options);
208}
209
211 return ShowOpenFileDialog(FileDialogOptions{});
212}
213
215 return ShowOpenFileDialog(FileDialogOptions{});
216}
217
219 return ShowOpenFileDialog(FileDialogOptions{});
220}
221
223 const FileDialogOptions& options,
224 std::function<void(const std::string&)> callback) {
225 if (!callback) {
226 return;
227 }
228 NSArray<UTType*>* allowed_types = BuildAllowedTypes(options);
229 auto callback_ptr =
230 std::make_shared<std::function<void(const std::string&)>>(
231 std::move(callback));
232
233 auto present_picker = ^{
234 ShowOpenFileDialogImpl(allowed_types, ^(std::string filePath) {
235 (*callback_ptr)(filePath);
236 });
237 };
238
239 if ([NSThread isMainThread]) {
240 present_picker();
241 } else {
242 dispatch_async(dispatch_get_main_queue(), present_picker);
243 }
244}
245
247 return ResolveDocumentsPath().string();
248}
249
251 const std::string& default_name, const std::string& default_extension) {
252 const auto base_dir = ResolveDocumentsPath();
253 const std::string filename = BuildSaveFilename(default_name, default_extension);
254 return (base_dir / filename).string();
255}
256
258 const std::string& default_name, const std::string& default_extension) {
259 return ShowSaveFileDialog(default_name, default_extension);
260}
261
263 const std::string& default_name, const std::string& default_extension) {
264 return ShowSaveFileDialog(default_name, default_extension);
265}
266
268 const std::string &folder) {
269 std::vector<std::string> files;
270 std::error_code ec;
271 for (const auto& entry : std::filesystem::directory_iterator(folder, ec)) {
272 if (ec) {
273 break;
274 }
275 if (entry.is_regular_file()) {
276 files.push_back(entry.path().string());
277 }
278 }
279 return files;
280}
281
283 const std::string &folder) {
284 std::vector<std::string> directories;
285 std::error_code ec;
286 for (const auto& entry : std::filesystem::directory_iterator(folder, ec)) {
287 if (ec) {
288 break;
289 }
290 if (entry.is_directory()) {
291 directories.push_back(entry.path().string());
292 }
293 }
294 return directories;
295}
296
298 NSBundle* bundle = [NSBundle mainBundle];
299 NSString* resourceDirectoryPath = [bundle bundlePath];
300 NSString* path = [resourceDirectoryPath stringByAppendingString:@"/"];
301 return [path UTF8String];
302}
303
304#elif TARGET_OS_MAC == 1
305/* macOS */
306
307#import <Cocoa/Cocoa.h>
308#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
309
310namespace {
311std::string TrimCopy(const std::string& input) {
312 const auto start = input.find_first_not_of(" \t\n\r");
313 if (start == std::string::npos) {
314 return "";
315 }
316 const auto end = input.find_last_not_of(" \t\n\r");
317 return input.substr(start, end - start + 1);
318}
319
320std::vector<std::string> SplitFilterSpec(const std::string& spec) {
321 std::vector<std::string> tokens;
322 std::string current;
323 for (char ch : spec) {
324 if (ch == ',') {
325 std::string trimmed = TrimCopy(current);
326 if (!trimmed.empty() && trimmed[0] == '.') {
327 trimmed.erase(0, 1);
328 }
329 if (!trimmed.empty()) {
330 tokens.push_back(trimmed);
331 }
332 current.clear();
333 } else {
334 current.push_back(ch);
335 }
336 }
337 std::string trimmed = TrimCopy(current);
338 if (!trimmed.empty() && trimmed[0] == '.') {
339 trimmed.erase(0, 1);
340 }
341 if (!trimmed.empty()) {
342 tokens.push_back(trimmed);
343 }
344 return tokens;
345}
346
347std::vector<std::string> CollectExtensions(
348 const yaze::util::FileDialogOptions& options, bool* allow_all) {
349 std::vector<std::string> extensions;
350 if (!allow_all) {
351 return extensions;
352 }
353 *allow_all = false;
354
355 for (const auto& filter : options.filters) {
356 const std::string spec = TrimCopy(filter.spec);
357 if (spec.empty() || spec == "*") {
358 *allow_all = true;
359 continue;
360 }
361
362 for (const auto& token : SplitFilterSpec(spec)) {
363 if (token == "*") {
364 *allow_all = true;
365 } else {
366 extensions.push_back(token);
367 }
368 }
369 }
370
371 return extensions;
372}
373
374std::string ShowOpenFileDialogBespokeWithOptions(
375 const yaze::util::FileDialogOptions& options) {
376 NSOpenPanel* openPanel = [NSOpenPanel openPanel];
377 [openPanel setCanChooseFiles:YES];
378 [openPanel setCanChooseDirectories:NO];
379 [openPanel setAllowsMultipleSelection:NO];
380
381 bool allow_all = false;
382 std::vector<std::string> extensions = CollectExtensions(options, &allow_all);
383 if (allow_all || extensions.empty()) {
384 [openPanel setAllowedFileTypes:nil];
385 } else {
386 NSMutableArray<NSString*>* allowed_types = [NSMutableArray array];
387 for (const auto& extension : extensions) {
388 NSString* ext = [NSString stringWithUTF8String:extension.c_str()];
389 if (ext) {
390 [allowed_types addObject:ext];
391 }
392 }
393 [openPanel setAllowedFileTypes:allowed_types];
394 }
395
396 if ([openPanel runModal] == NSModalResponseOK) {
397 NSURL* url = [[openPanel URLs] objectAtIndex:0];
398 NSString* path = [url path];
399 return std::string([path UTF8String]);
400 }
401
402 return "";
403}
404
405std::string ShowOpenFileDialogNFDWithOptions(
406 const yaze::util::FileDialogOptions& options) {
407#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
408 NFD_Init();
409 nfdu8char_t* out_path = NULL;
410 const nfdu8filteritem_t* filter_list = nullptr;
411 size_t filter_count = 0;
412 std::vector<nfdu8filteritem_t> filter_items;
413 std::vector<std::string> filter_names;
414 std::vector<std::string> filter_specs;
415
416 if (!options.filters.empty()) {
417 filter_items.reserve(options.filters.size());
418 filter_names.reserve(options.filters.size());
419 filter_specs.reserve(options.filters.size());
420
421 for (const auto& filter : options.filters) {
422 std::string label = filter.label.empty() ? "Files" : filter.label;
423 std::string spec = filter.spec.empty() ? "*" : filter.spec;
424 filter_names.push_back(label);
425 filter_specs.push_back(spec);
426 filter_items.push_back(
427 {filter_names.back().c_str(), filter_specs.back().c_str()});
428 }
429
430 filter_list = filter_items.data();
431 filter_count = filter_items.size();
432 }
433
434 nfdopendialogu8args_t args = {0};
435 args.filterList = filter_list;
436 args.filterCount = filter_count;
437
438 nfdresult_t result = NFD_OpenDialogU8_With(&out_path, &args);
439 if (result == NFD_OKAY) {
440 std::string file_path(out_path);
441 NFD_FreePath(out_path);
442 NFD_Quit();
443 return file_path;
444 } else if (result == NFD_CANCEL) {
445 NFD_Quit();
446 return "";
447 }
448 NFD_Quit();
449 return "";
450#else
451 return ShowOpenFileDialogBespokeWithOptions(options);
452#endif
453}
454} // namespace
455
457 return ShowOpenFileDialogBespokeWithOptions(FileDialogOptions{});
458}
459
461 const FileDialogOptions& options,
462 std::function<void(const std::string&)> callback) {
463 if (!callback) {
464 return;
465 }
466 callback(ShowOpenFileDialog(options));
467}
468
469std::string yaze::util::FileDialogWrapper::ShowSaveFileDialogBespoke(const std::string& default_name,
470 const std::string& default_extension) {
471 NSSavePanel* savePanel = [NSSavePanel savePanel];
472
473 if (!default_name.empty()) {
474 [savePanel setNameFieldStringValue:[NSString stringWithUTF8String:default_name.c_str()]];
475 }
476
477 if (!default_extension.empty()) {
478 NSString* ext = [NSString stringWithUTF8String:default_extension.c_str()];
479 [savePanel setAllowedFileTypes:@[ext]];
480 }
481
482 if ([savePanel runModal] == NSModalResponseOK) {
483 NSURL* url = [savePanel URL];
484 NSString* path = [url path];
485 return std::string([path UTF8String]);
486 }
487
488 return "";
489}
490
491// Global feature flag-based dispatch methods
493 const FileDialogOptions& options) {
494 if (core::FeatureFlags::get().kUseNativeFileDialog) {
495 return ShowOpenFileDialogNFDWithOptions(options);
496 }
497 return ShowOpenFileDialogBespokeWithOptions(options);
498}
499
501 return ShowOpenFileDialog(FileDialogOptions{});
502}
503
505 if (core::FeatureFlags::get().kUseNativeFileDialog) {
506 return ShowOpenFolderDialogNFD();
507 } else {
508 return ShowOpenFolderDialogBespoke();
509 }
510}
511
512std::string yaze::util::FileDialogWrapper::ShowSaveFileDialog(const std::string& default_name,
513 const std::string& default_extension) {
514 if (core::FeatureFlags::get().kUseNativeFileDialog) {
515 return ShowSaveFileDialogNFD(default_name, default_extension);
516 } else {
517 return ShowSaveFileDialogBespoke(default_name, default_extension);
518 }
519}
520
521// NFD implementation for macOS (fallback to bespoke if NFD not available)
523 return ShowOpenFileDialogNFDWithOptions(FileDialogOptions{});
524}
525
527#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
528 NFD_Init();
529 nfdu8char_t *out_path = NULL;
530 nfdresult_t result = NFD_PickFolderU8(&out_path, NULL);
531
532 if (result == NFD_OKAY) {
533 std::string folder_path(out_path);
534 NFD_FreePath(out_path);
535 NFD_Quit();
536 return folder_path;
537 } else if (result == NFD_CANCEL) {
538 NFD_Quit();
539 return "";
540 }
541 NFD_Quit();
542 return "";
543#else
544 // NFD not compiled in, use bespoke
545 return ShowOpenFolderDialogBespoke();
546#endif
547}
548
549std::string yaze::util::FileDialogWrapper::ShowSaveFileDialogNFD(const std::string& default_name,
550 const std::string& default_extension) {
551#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
552 NFD_Init();
553 nfdu8char_t *out_path = NULL;
554
555 nfdsavedialogu8args_t args = {0};
556 if (!default_extension.empty()) {
557 // Create filter for the save dialog
558 static nfdu8filteritem_t filters[3] = {
559 {"Theme File", "theme"},
560 {"Project File", "yaze"},
561 {"ROM File", "sfc,smc"}
562 };
563
564 if (default_extension == "theme") {
565 args.filterList = &filters[0];
566 args.filterCount = 1;
567 } else if (default_extension == "yaze") {
568 args.filterList = &filters[1];
569 args.filterCount = 1;
570 } else if (default_extension == "sfc" || default_extension == "smc") {
571 args.filterList = &filters[2];
572 args.filterCount = 1;
573 }
574 }
575
576 if (!default_name.empty()) {
577 args.defaultName = default_name.c_str();
578 }
579
580 nfdresult_t result = NFD_SaveDialogU8_With(&out_path, &args);
581 if (result == NFD_OKAY) {
582 std::string file_path(out_path);
583 NFD_FreePath(out_path);
584 NFD_Quit();
585 return file_path;
586 } else if (result == NFD_CANCEL) {
587 NFD_Quit();
588 return "";
589 }
590 NFD_Quit();
591 return "";
592#else
593 // NFD not compiled in, use bespoke
594 return ShowSaveFileDialogBespoke(default_name, default_extension);
595#endif
596}
597
599 NSOpenPanel* openPanel = [NSOpenPanel openPanel];
600 [openPanel setCanChooseFiles:NO];
601 [openPanel setCanChooseDirectories:YES];
602 [openPanel setAllowsMultipleSelection:NO];
603
604 if ([openPanel runModal] == NSModalResponseOK) {
605 NSURL* url = [[openPanel URLs] objectAtIndex:0];
606 NSString* path = [url path];
607 return std::string([path UTF8String]);
608 }
609
610 return "";
611}
612
614 const std::string& folder) {
615 std::vector<std::string> filenames;
616 NSFileManager* fileManager = [NSFileManager defaultManager];
617 NSDirectoryEnumerator* enumerator =
618 [fileManager enumeratorAtPath:[NSString stringWithUTF8String:folder.c_str()]];
619 NSString* file;
620 while (file = [enumerator nextObject]) {
621 if ([file hasPrefix:@"."]) {
622 continue;
623 }
624 filenames.push_back(std::string([file UTF8String]));
625 }
626 return filenames;
627}
628
630 const std::string& folder) {
631 std::vector<std::string> subdirectories;
632 NSFileManager* fileManager = [NSFileManager defaultManager];
633 NSDirectoryEnumerator* enumerator =
634 [fileManager enumeratorAtPath:[NSString stringWithUTF8String:folder.c_str()]];
635 NSString* file;
636 while (file = [enumerator nextObject]) {
637 if ([file hasPrefix:@"."]) {
638 continue;
639 }
640 BOOL isDirectory;
641 NSString* path =
642 [NSString stringWithFormat:@"%@/%@", [NSString stringWithUTF8String:folder.c_str()], file];
643 [fileManager fileExistsAtPath:path isDirectory:&isDirectory];
644 if (isDirectory) {
645 subdirectories.push_back(std::string([file UTF8String]));
646 }
647 }
648 return subdirectories;
649}
650
652 NSBundle* bundle = [NSBundle mainBundle];
653 NSString* resourceDirectoryPath = [bundle bundlePath];
654 NSString* path = [resourceDirectoryPath stringByAppendingString:@"/"];
655 return [path UTF8String];
656}
657
658#else
659// Unsupported platform
660#endif // TARGET_OS_MAC
661
662#endif // __APPLE__ && __MACH__
static void ShowOpenFileDialogAsync(const FileDialogOptions &options, std::function< void(const std::string &)> callback)
static std::string ShowSaveFileDialogBespoke(const std::string &default_name="", const std::string &default_extension="")
static std::string ShowOpenFileDialogBespoke()
static std::string ShowSaveFileDialogNFD(const std::string &default_name="", const std::string &default_extension="")
static std::string ShowOpenFolderDialogNFD()
static std::string ShowSaveFileDialog(const std::string &default_name="", const std::string &default_extension="")
ShowSaveFileDialog opens a save file dialog and returns the selected filepath. Uses global feature fl...
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
static std::string ShowOpenFolderDialog()
ShowOpenFolderDialog opens a file dialog and returns the selected folder path. Uses global feature fl...
static std::vector< std::string > GetFilesInFolder(const std::string &folder_path)
static std::vector< std::string > GetSubdirectoriesInFolder(const std::string &folder_path)
static std::string ShowOpenFolderDialogBespoke()
static std::string ShowOpenFileDialogNFD()
static absl::StatusOr< std::filesystem::path > GetTempDirectory()
Get a temporary directory for the application.
static absl::StatusOr< std::filesystem::path > GetUserDocumentsDirectory()
Get the user's Documents directory.
std::string GetBundleResourcePath()
GetBundleResourcePath returns the path to the bundle resource directory. Specific to MacOS.
std::vector< FileDialogFilter > filters
Definition file_util.h:17