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 // .yazeproj is a package; an undeclared lookup yields a data type that
100 // greys the bundle out, so use the app's exported identifier.
101 UTType* type = token == "yazeproj"
102 ? [UTType typeWithIdentifier:@"org.halext.yaze.project"]
103 : nil;
104 if (!type) {
105 type = [UTType typeWithFilenameExtension:ext];
106 }
107 if (!type) {
108 NSString* identifier = [NSString stringWithUTF8String:token.c_str()];
109 type = [UTType typeWithIdentifier:identifier];
110 }
111 if (type) {
112 [types addObject:type];
113 }
114 }
115 }
116
117 if (allow_all || [types count] == 0) {
118 return @[ UTTypeData ];
119 }
120
121 return types;
122}
123
124std::filesystem::path ResolveDocumentsPath() {
126 if (docs_result.ok()) {
127 return *docs_result;
128 }
130 if (temp_result.ok()) {
131 return *temp_result;
132 }
133 std::error_code ec;
134 auto cwd = std::filesystem::current_path(ec);
135 if (!ec) {
136 return cwd;
137 }
138 return std::filesystem::path(".");
139}
140
141std::string NormalizeExtension(const std::string& ext) {
142 if (ext.empty()) {
143 return "";
144 }
145 if (ext.front() == '.') {
146 return ext;
147 }
148 return "." + ext;
149}
150
151std::string BuildSaveFilename(const std::string& default_name,
152 const std::string& default_extension) {
153 std::string name = default_name.empty() ? "yaze_output" : default_name;
154 const std::string normalized_ext = NormalizeExtension(default_extension);
155 if (!normalized_ext.empty()) {
156 auto dot_pos = name.find_last_of('.');
157 if (dot_pos == std::string::npos || dot_pos == 0) {
158 name += normalized_ext;
159 }
160 }
161 return name;
162}
163
164void ShowOpenFileDialogImpl(NSArray<UTType*>* allowed_types,
165 void (^completionHandler)(std::string)) {
166 AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
167 if (!appDelegate) {
168 completionHandler("");
169 return;
170 }
171 [appDelegate PresentDocumentPickerWithCompletionHandler:^(NSString *filePath) {
172 completionHandler(std::string([filePath UTF8String]));
173 }
174 allowedTypes:allowed_types];
175}
176
177std::string ShowOpenFileDialogSync(
178 const yaze::util::FileDialogOptions& options) {
179 __block std::string result;
180 __block bool done = false;
181 NSArray<UTType*>* allowed_types = BuildAllowedTypes(options);
182
183 auto present_picker = ^{
184 ShowOpenFileDialogImpl(allowed_types, ^(std::string filePath) {
185 result = filePath;
186 done = true;
187 });
188 };
189
190 if ([NSThread isMainThread]) {
191 present_picker();
192 // Run a nested loop to keep UI responsive while waiting on selection.
193 while (!done) {
194 [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
195 beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
196 }
197 } else {
198 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
199 dispatch_async(dispatch_get_main_queue(), ^{
200 ShowOpenFileDialogImpl(allowed_types, ^(std::string filePath) {
201 result = filePath;
202 dispatch_semaphore_signal(semaphore);
203 });
204 });
205 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
206 }
207
208 return result;
209}
210} // namespace
211
213 const FileDialogOptions& options) {
214 return ShowOpenFileDialogSync(options);
215}
216
218 return ShowOpenFileDialog(FileDialogOptions{});
219}
220
222 return ShowOpenFileDialog(FileDialogOptions{});
223}
224
226 return ShowOpenFileDialog(FileDialogOptions{});
227}
228
230 const FileDialogOptions& options,
231 std::function<void(const std::string&)> callback) {
232 if (!callback) {
233 return;
234 }
235 NSArray<UTType*>* allowed_types = BuildAllowedTypes(options);
236 auto callback_ptr =
237 std::make_shared<std::function<void(const std::string&)>>(
238 std::move(callback));
239
240 auto present_picker = ^{
241 ShowOpenFileDialogImpl(allowed_types, ^(std::string filePath) {
242 (*callback_ptr)(filePath);
243 });
244 };
245
246 if ([NSThread isMainThread]) {
247 present_picker();
248 } else {
249 dispatch_async(dispatch_get_main_queue(), present_picker);
250 }
251}
252
254 return ResolveDocumentsPath().string();
255}
256
258 const std::string& default_name, const std::string& default_extension) {
259 if (auto result = ConsumeSaveFileDialogResultForTesting()) {
260 return *result;
261 }
262 const auto base_dir = ResolveDocumentsPath();
263 const std::string filename = BuildSaveFilename(default_name, default_extension);
264 return (base_dir / filename).string();
265}
266
268 const std::string& default_name, const std::string& default_extension) {
269 return ShowSaveFileDialog(default_name, default_extension);
270}
271
273 const std::string& default_name, const std::string& default_extension) {
274 return ShowSaveFileDialog(default_name, default_extension);
275}
276
278 const std::string &folder) {
279 std::vector<std::string> files;
280 std::error_code ec;
281 for (const auto& entry : std::filesystem::directory_iterator(folder, ec)) {
282 if (ec) {
283 break;
284 }
285 if (entry.is_regular_file()) {
286 files.push_back(entry.path().string());
287 }
288 }
289 return files;
290}
291
293 const std::string &folder) {
294 std::vector<std::string> directories;
295 std::error_code ec;
296 for (const auto& entry : std::filesystem::directory_iterator(folder, ec)) {
297 if (ec) {
298 break;
299 }
300 if (entry.is_directory()) {
301 directories.push_back(entry.path().string());
302 }
303 }
304 return directories;
305}
306
308 NSBundle* bundle = [NSBundle mainBundle];
309 NSString* resourceDirectoryPath = [bundle bundlePath];
310 NSString* path = [resourceDirectoryPath stringByAppendingString:@"/"];
311 return [path UTF8String];
312}
313
314#elif TARGET_OS_MAC == 1
315/* macOS */
316
317#import <Cocoa/Cocoa.h>
318#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
319
320namespace {
321std::string TrimCopy(const std::string& input) {
322 const auto start = input.find_first_not_of(" \t\n\r");
323 if (start == std::string::npos) {
324 return "";
325 }
326 const auto end = input.find_last_not_of(" \t\n\r");
327 return input.substr(start, end - start + 1);
328}
329
330std::vector<std::string> SplitFilterSpec(const std::string& spec) {
331 std::vector<std::string> tokens;
332 std::string current;
333 for (char ch : spec) {
334 if (ch == ',') {
335 std::string trimmed = TrimCopy(current);
336 if (!trimmed.empty() && trimmed[0] == '.') {
337 trimmed.erase(0, 1);
338 }
339 if (!trimmed.empty()) {
340 tokens.push_back(trimmed);
341 }
342 current.clear();
343 } else {
344 current.push_back(ch);
345 }
346 }
347 std::string trimmed = TrimCopy(current);
348 if (!trimmed.empty() && trimmed[0] == '.') {
349 trimmed.erase(0, 1);
350 }
351 if (!trimmed.empty()) {
352 tokens.push_back(trimmed);
353 }
354 return tokens;
355}
356
357std::vector<std::string> CollectExtensions(
358 const yaze::util::FileDialogOptions& options, bool* allow_all) {
359 std::vector<std::string> extensions;
360 if (!allow_all) {
361 return extensions;
362 }
363 *allow_all = false;
364
365 for (const auto& filter : options.filters) {
366 const std::string spec = TrimCopy(filter.spec);
367 if (spec.empty() || spec == "*") {
368 *allow_all = true;
369 continue;
370 }
371
372 for (const auto& token : SplitFilterSpec(spec)) {
373 if (token == "*") {
374 *allow_all = true;
375 } else {
376 extensions.push_back(token);
377 }
378 }
379 }
380
381 return extensions;
382}
383
384std::string ShowOpenFileDialogBespokeWithOptions(
385 const yaze::util::FileDialogOptions& options) {
386 NSOpenPanel* openPanel = [NSOpenPanel openPanel];
387 [openPanel setCanChooseFiles:YES];
388 [openPanel setCanChooseDirectories:NO];
389 [openPanel setAllowsMultipleSelection:NO];
390
391 bool allow_all = false;
392 std::vector<std::string> extensions = CollectExtensions(options, &allow_all);
393 if (allow_all || extensions.empty()) {
394 [openPanel setAllowedFileTypes:nil];
395 } else {
396 NSMutableArray<NSString*>* allowed_types = [NSMutableArray array];
397 for (const auto& extension : extensions) {
398 NSString* ext = [NSString stringWithUTF8String:extension.c_str()];
399 if (ext) {
400 [allowed_types addObject:ext];
401 }
402 }
403 [openPanel setAllowedFileTypes:allowed_types];
404 }
405
406 if ([openPanel runModal] == NSModalResponseOK) {
407 NSURL* url = [[openPanel URLs] objectAtIndex:0];
408 NSString* path = [url path];
409 return std::string([path UTF8String]);
410 }
411
412 return "";
413}
414
415std::string ShowOpenFileDialogNFDWithOptions(
416 const yaze::util::FileDialogOptions& options) {
417#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
418 NFD_Init();
419 nfdu8char_t* out_path = NULL;
420 const nfdu8filteritem_t* filter_list = nullptr;
421 size_t filter_count = 0;
422 std::vector<nfdu8filteritem_t> filter_items;
423 std::vector<std::string> filter_names;
424 std::vector<std::string> filter_specs;
425
426 if (!options.filters.empty()) {
427 filter_items.reserve(options.filters.size());
428 filter_names.reserve(options.filters.size());
429 filter_specs.reserve(options.filters.size());
430
431 for (const auto& filter : options.filters) {
432 std::string label = filter.label.empty() ? "Files" : filter.label;
433 std::string spec = filter.spec.empty() ? "*" : filter.spec;
434 filter_names.push_back(label);
435 filter_specs.push_back(spec);
436 filter_items.push_back(
437 {filter_names.back().c_str(), filter_specs.back().c_str()});
438 }
439
440 filter_list = filter_items.data();
441 filter_count = filter_items.size();
442 }
443
444 nfdopendialogu8args_t args = {0};
445 args.filterList = filter_list;
446 args.filterCount = filter_count;
447
448 nfdresult_t result = NFD_OpenDialogU8_With(&out_path, &args);
449 if (result == NFD_OKAY) {
450 std::string file_path(out_path);
451 NFD_FreePath(out_path);
452 NFD_Quit();
453 return file_path;
454 } else if (result == NFD_CANCEL) {
455 NFD_Quit();
456 return "";
457 }
458 NFD_Quit();
459 return "";
460#else
461 return ShowOpenFileDialogBespokeWithOptions(options);
462#endif
463}
464} // namespace
465
467 return ShowOpenFileDialogBespokeWithOptions(FileDialogOptions{});
468}
469
471 const FileDialogOptions& options,
472 std::function<void(const std::string&)> callback) {
473 if (!callback) {
474 return;
475 }
476 callback(ShowOpenFileDialog(options));
477}
478
479std::string yaze::util::FileDialogWrapper::ShowSaveFileDialogBespoke(const std::string& default_name,
480 const std::string& default_extension) {
481 NSSavePanel* savePanel = [NSSavePanel savePanel];
482
483 if (!default_name.empty()) {
484 [savePanel setNameFieldStringValue:[NSString stringWithUTF8String:default_name.c_str()]];
485 }
486
487 if (!default_extension.empty()) {
488 NSString* ext = [NSString stringWithUTF8String:default_extension.c_str()];
489 [savePanel setAllowedFileTypes:@[ext]];
490 }
491
492 if ([savePanel runModal] == NSModalResponseOK) {
493 NSURL* url = [savePanel URL];
494 NSString* path = [url path];
495 return std::string([path UTF8String]);
496 }
497
498 return "";
499}
500
501// Global feature flag-based dispatch methods
503 const FileDialogOptions& options) {
504 if (core::FeatureFlags::get().kUseNativeFileDialog) {
505 return ShowOpenFileDialogNFDWithOptions(options);
506 }
507 return ShowOpenFileDialogBespokeWithOptions(options);
508}
509
511 return ShowOpenFileDialog(FileDialogOptions{});
512}
513
515 if (core::FeatureFlags::get().kUseNativeFileDialog) {
516 return ShowOpenFolderDialogNFD();
517 } else {
518 return ShowOpenFolderDialogBespoke();
519 }
520}
521
522std::string yaze::util::FileDialogWrapper::ShowSaveFileDialog(const std::string& default_name,
523 const std::string& default_extension) {
524 if (auto result = ConsumeSaveFileDialogResultForTesting()) {
525 return *result;
526 }
527 if (core::FeatureFlags::get().kUseNativeFileDialog) {
528 return ShowSaveFileDialogNFD(default_name, default_extension);
529 } else {
530 return ShowSaveFileDialogBespoke(default_name, default_extension);
531 }
532}
533
534// NFD implementation for macOS (fallback to bespoke if NFD not available)
536 return ShowOpenFileDialogNFDWithOptions(FileDialogOptions{});
537}
538
540#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
541 NFD_Init();
542 nfdu8char_t *out_path = NULL;
543 nfdresult_t result = NFD_PickFolderU8(&out_path, NULL);
544
545 if (result == NFD_OKAY) {
546 std::string folder_path(out_path);
547 NFD_FreePath(out_path);
548 NFD_Quit();
549 return folder_path;
550 } else if (result == NFD_CANCEL) {
551 NFD_Quit();
552 return "";
553 }
554 NFD_Quit();
555 return "";
556#else
557 // NFD not compiled in, use bespoke
558 return ShowOpenFolderDialogBespoke();
559#endif
560}
561
562std::string yaze::util::FileDialogWrapper::ShowSaveFileDialogNFD(const std::string& default_name,
563 const std::string& default_extension) {
564#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
565 NFD_Init();
566 nfdu8char_t *out_path = NULL;
567
568 nfdsavedialogu8args_t args = {0};
569 if (!default_extension.empty()) {
570 // Create filter for the save dialog
571 static nfdu8filteritem_t filters[3] = {
572 {"Theme File", "theme"},
573 {"Project File", "yaze"},
574 {"ROM File", "sfc,smc"}
575 };
576
577 if (default_extension == "theme") {
578 args.filterList = &filters[0];
579 args.filterCount = 1;
580 } else if (default_extension == "yaze") {
581 args.filterList = &filters[1];
582 args.filterCount = 1;
583 } else if (default_extension == "sfc" || default_extension == "smc") {
584 args.filterList = &filters[2];
585 args.filterCount = 1;
586 }
587 }
588
589 if (!default_name.empty()) {
590 args.defaultName = default_name.c_str();
591 }
592
593 nfdresult_t result = NFD_SaveDialogU8_With(&out_path, &args);
594 if (result == NFD_OKAY) {
595 std::string file_path(out_path);
596 NFD_FreePath(out_path);
597 NFD_Quit();
598 return file_path;
599 } else if (result == NFD_CANCEL) {
600 NFD_Quit();
601 return "";
602 }
603 NFD_Quit();
604 return "";
605#else
606 // NFD not compiled in, use bespoke
607 return ShowSaveFileDialogBespoke(default_name, default_extension);
608#endif
609}
610
612 NSOpenPanel* openPanel = [NSOpenPanel openPanel];
613 [openPanel setCanChooseFiles:NO];
614 [openPanel setCanChooseDirectories:YES];
615 [openPanel setAllowsMultipleSelection:NO];
616
617 if ([openPanel runModal] == NSModalResponseOK) {
618 NSURL* url = [[openPanel URLs] objectAtIndex:0];
619 NSString* path = [url path];
620 return std::string([path UTF8String]);
621 }
622
623 return "";
624}
625
627 const std::string& folder) {
628 std::vector<std::string> filenames;
629 NSFileManager* fileManager = [NSFileManager defaultManager];
630 NSDirectoryEnumerator* enumerator =
631 [fileManager enumeratorAtPath:[NSString stringWithUTF8String:folder.c_str()]];
632 NSString* file;
633 while (file = [enumerator nextObject]) {
634 if ([file hasPrefix:@"."]) {
635 continue;
636 }
637 filenames.push_back(std::string([file UTF8String]));
638 }
639 return filenames;
640}
641
643 const std::string& folder) {
644 std::vector<std::string> subdirectories;
645 NSFileManager* fileManager = [NSFileManager defaultManager];
646 NSDirectoryEnumerator* enumerator =
647 [fileManager enumeratorAtPath:[NSString stringWithUTF8String:folder.c_str()]];
648 NSString* file;
649 while (file = [enumerator nextObject]) {
650 if ([file hasPrefix:@"."]) {
651 continue;
652 }
653 BOOL isDirectory;
654 NSString* path =
655 [NSString stringWithFormat:@"%@/%@", [NSString stringWithUTF8String:folder.c_str()], file];
656 [fileManager fileExistsAtPath:path isDirectory:&isDirectory];
657 if (isDirectory) {
658 subdirectories.push_back(std::string([file UTF8String]));
659 }
660 }
661 return subdirectories;
662}
663
665 NSBundle* bundle = [NSBundle mainBundle];
666 NSString* resourceDirectoryPath = [bundle bundlePath];
667 NSString* path = [resourceDirectoryPath stringByAppendingString:@"/"];
668 return [path UTF8String];
669}
670
671#else
672// Unsupported platform
673#endif // TARGET_OS_MAC
674
675#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:18