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 const auto base_dir = ResolveDocumentsPath();
260 const std::string filename = BuildSaveFilename(default_name, default_extension);
261 return (base_dir / filename).string();
262}
263
265 const std::string& default_name, const std::string& default_extension) {
266 return ShowSaveFileDialog(default_name, default_extension);
267}
268
270 const std::string& default_name, const std::string& default_extension) {
271 return ShowSaveFileDialog(default_name, default_extension);
272}
273
275 const std::string &folder) {
276 std::vector<std::string> files;
277 std::error_code ec;
278 for (const auto& entry : std::filesystem::directory_iterator(folder, ec)) {
279 if (ec) {
280 break;
281 }
282 if (entry.is_regular_file()) {
283 files.push_back(entry.path().string());
284 }
285 }
286 return files;
287}
288
290 const std::string &folder) {
291 std::vector<std::string> directories;
292 std::error_code ec;
293 for (const auto& entry : std::filesystem::directory_iterator(folder, ec)) {
294 if (ec) {
295 break;
296 }
297 if (entry.is_directory()) {
298 directories.push_back(entry.path().string());
299 }
300 }
301 return directories;
302}
303
305 NSBundle* bundle = [NSBundle mainBundle];
306 NSString* resourceDirectoryPath = [bundle bundlePath];
307 NSString* path = [resourceDirectoryPath stringByAppendingString:@"/"];
308 return [path UTF8String];
309}
310
311#elif TARGET_OS_MAC == 1
312/* macOS */
313
314#import <Cocoa/Cocoa.h>
315#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
316
317namespace {
318std::string TrimCopy(const std::string& input) {
319 const auto start = input.find_first_not_of(" \t\n\r");
320 if (start == std::string::npos) {
321 return "";
322 }
323 const auto end = input.find_last_not_of(" \t\n\r");
324 return input.substr(start, end - start + 1);
325}
326
327std::vector<std::string> SplitFilterSpec(const std::string& spec) {
328 std::vector<std::string> tokens;
329 std::string current;
330 for (char ch : spec) {
331 if (ch == ',') {
332 std::string trimmed = TrimCopy(current);
333 if (!trimmed.empty() && trimmed[0] == '.') {
334 trimmed.erase(0, 1);
335 }
336 if (!trimmed.empty()) {
337 tokens.push_back(trimmed);
338 }
339 current.clear();
340 } else {
341 current.push_back(ch);
342 }
343 }
344 std::string trimmed = TrimCopy(current);
345 if (!trimmed.empty() && trimmed[0] == '.') {
346 trimmed.erase(0, 1);
347 }
348 if (!trimmed.empty()) {
349 tokens.push_back(trimmed);
350 }
351 return tokens;
352}
353
354std::vector<std::string> CollectExtensions(
355 const yaze::util::FileDialogOptions& options, bool* allow_all) {
356 std::vector<std::string> extensions;
357 if (!allow_all) {
358 return extensions;
359 }
360 *allow_all = false;
361
362 for (const auto& filter : options.filters) {
363 const std::string spec = TrimCopy(filter.spec);
364 if (spec.empty() || spec == "*") {
365 *allow_all = true;
366 continue;
367 }
368
369 for (const auto& token : SplitFilterSpec(spec)) {
370 if (token == "*") {
371 *allow_all = true;
372 } else {
373 extensions.push_back(token);
374 }
375 }
376 }
377
378 return extensions;
379}
380
381std::string ShowOpenFileDialogBespokeWithOptions(
382 const yaze::util::FileDialogOptions& options) {
383 NSOpenPanel* openPanel = [NSOpenPanel openPanel];
384 [openPanel setCanChooseFiles:YES];
385 [openPanel setCanChooseDirectories:NO];
386 [openPanel setAllowsMultipleSelection:NO];
387
388 bool allow_all = false;
389 std::vector<std::string> extensions = CollectExtensions(options, &allow_all);
390 if (allow_all || extensions.empty()) {
391 [openPanel setAllowedFileTypes:nil];
392 } else {
393 NSMutableArray<NSString*>* allowed_types = [NSMutableArray array];
394 for (const auto& extension : extensions) {
395 NSString* ext = [NSString stringWithUTF8String:extension.c_str()];
396 if (ext) {
397 [allowed_types addObject:ext];
398 }
399 }
400 [openPanel setAllowedFileTypes:allowed_types];
401 }
402
403 if ([openPanel runModal] == NSModalResponseOK) {
404 NSURL* url = [[openPanel URLs] objectAtIndex:0];
405 NSString* path = [url path];
406 return std::string([path UTF8String]);
407 }
408
409 return "";
410}
411
412std::string ShowOpenFileDialogNFDWithOptions(
413 const yaze::util::FileDialogOptions& options) {
414#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
415 NFD_Init();
416 nfdu8char_t* out_path = NULL;
417 const nfdu8filteritem_t* filter_list = nullptr;
418 size_t filter_count = 0;
419 std::vector<nfdu8filteritem_t> filter_items;
420 std::vector<std::string> filter_names;
421 std::vector<std::string> filter_specs;
422
423 if (!options.filters.empty()) {
424 filter_items.reserve(options.filters.size());
425 filter_names.reserve(options.filters.size());
426 filter_specs.reserve(options.filters.size());
427
428 for (const auto& filter : options.filters) {
429 std::string label = filter.label.empty() ? "Files" : filter.label;
430 std::string spec = filter.spec.empty() ? "*" : filter.spec;
431 filter_names.push_back(label);
432 filter_specs.push_back(spec);
433 filter_items.push_back(
434 {filter_names.back().c_str(), filter_specs.back().c_str()});
435 }
436
437 filter_list = filter_items.data();
438 filter_count = filter_items.size();
439 }
440
441 nfdopendialogu8args_t args = {0};
442 args.filterList = filter_list;
443 args.filterCount = filter_count;
444
445 nfdresult_t result = NFD_OpenDialogU8_With(&out_path, &args);
446 if (result == NFD_OKAY) {
447 std::string file_path(out_path);
448 NFD_FreePath(out_path);
449 NFD_Quit();
450 return file_path;
451 } else if (result == NFD_CANCEL) {
452 NFD_Quit();
453 return "";
454 }
455 NFD_Quit();
456 return "";
457#else
458 return ShowOpenFileDialogBespokeWithOptions(options);
459#endif
460}
461} // namespace
462
464 return ShowOpenFileDialogBespokeWithOptions(FileDialogOptions{});
465}
466
468 const FileDialogOptions& options,
469 std::function<void(const std::string&)> callback) {
470 if (!callback) {
471 return;
472 }
473 callback(ShowOpenFileDialog(options));
474}
475
476std::string yaze::util::FileDialogWrapper::ShowSaveFileDialogBespoke(const std::string& default_name,
477 const std::string& default_extension) {
478 NSSavePanel* savePanel = [NSSavePanel savePanel];
479
480 if (!default_name.empty()) {
481 [savePanel setNameFieldStringValue:[NSString stringWithUTF8String:default_name.c_str()]];
482 }
483
484 if (!default_extension.empty()) {
485 NSString* ext = [NSString stringWithUTF8String:default_extension.c_str()];
486 [savePanel setAllowedFileTypes:@[ext]];
487 }
488
489 if ([savePanel runModal] == NSModalResponseOK) {
490 NSURL* url = [savePanel URL];
491 NSString* path = [url path];
492 return std::string([path UTF8String]);
493 }
494
495 return "";
496}
497
498// Global feature flag-based dispatch methods
500 const FileDialogOptions& options) {
501 if (core::FeatureFlags::get().kUseNativeFileDialog) {
502 return ShowOpenFileDialogNFDWithOptions(options);
503 }
504 return ShowOpenFileDialogBespokeWithOptions(options);
505}
506
508 return ShowOpenFileDialog(FileDialogOptions{});
509}
510
512 if (core::FeatureFlags::get().kUseNativeFileDialog) {
513 return ShowOpenFolderDialogNFD();
514 } else {
515 return ShowOpenFolderDialogBespoke();
516 }
517}
518
519std::string yaze::util::FileDialogWrapper::ShowSaveFileDialog(const std::string& default_name,
520 const std::string& default_extension) {
521 if (core::FeatureFlags::get().kUseNativeFileDialog) {
522 return ShowSaveFileDialogNFD(default_name, default_extension);
523 } else {
524 return ShowSaveFileDialogBespoke(default_name, default_extension);
525 }
526}
527
528// NFD implementation for macOS (fallback to bespoke if NFD not available)
530 return ShowOpenFileDialogNFDWithOptions(FileDialogOptions{});
531}
532
534#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
535 NFD_Init();
536 nfdu8char_t *out_path = NULL;
537 nfdresult_t result = NFD_PickFolderU8(&out_path, NULL);
538
539 if (result == NFD_OKAY) {
540 std::string folder_path(out_path);
541 NFD_FreePath(out_path);
542 NFD_Quit();
543 return folder_path;
544 } else if (result == NFD_CANCEL) {
545 NFD_Quit();
546 return "";
547 }
548 NFD_Quit();
549 return "";
550#else
551 // NFD not compiled in, use bespoke
552 return ShowOpenFolderDialogBespoke();
553#endif
554}
555
556std::string yaze::util::FileDialogWrapper::ShowSaveFileDialogNFD(const std::string& default_name,
557 const std::string& default_extension) {
558#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
559 NFD_Init();
560 nfdu8char_t *out_path = NULL;
561
562 nfdsavedialogu8args_t args = {0};
563 if (!default_extension.empty()) {
564 // Create filter for the save dialog
565 static nfdu8filteritem_t filters[3] = {
566 {"Theme File", "theme"},
567 {"Project File", "yaze"},
568 {"ROM File", "sfc,smc"}
569 };
570
571 if (default_extension == "theme") {
572 args.filterList = &filters[0];
573 args.filterCount = 1;
574 } else if (default_extension == "yaze") {
575 args.filterList = &filters[1];
576 args.filterCount = 1;
577 } else if (default_extension == "sfc" || default_extension == "smc") {
578 args.filterList = &filters[2];
579 args.filterCount = 1;
580 }
581 }
582
583 if (!default_name.empty()) {
584 args.defaultName = default_name.c_str();
585 }
586
587 nfdresult_t result = NFD_SaveDialogU8_With(&out_path, &args);
588 if (result == NFD_OKAY) {
589 std::string file_path(out_path);
590 NFD_FreePath(out_path);
591 NFD_Quit();
592 return file_path;
593 } else if (result == NFD_CANCEL) {
594 NFD_Quit();
595 return "";
596 }
597 NFD_Quit();
598 return "";
599#else
600 // NFD not compiled in, use bespoke
601 return ShowSaveFileDialogBespoke(default_name, default_extension);
602#endif
603}
604
606 NSOpenPanel* openPanel = [NSOpenPanel openPanel];
607 [openPanel setCanChooseFiles:NO];
608 [openPanel setCanChooseDirectories:YES];
609 [openPanel setAllowsMultipleSelection:NO];
610
611 if ([openPanel runModal] == NSModalResponseOK) {
612 NSURL* url = [[openPanel URLs] objectAtIndex:0];
613 NSString* path = [url path];
614 return std::string([path UTF8String]);
615 }
616
617 return "";
618}
619
621 const std::string& folder) {
622 std::vector<std::string> filenames;
623 NSFileManager* fileManager = [NSFileManager defaultManager];
624 NSDirectoryEnumerator* enumerator =
625 [fileManager enumeratorAtPath:[NSString stringWithUTF8String:folder.c_str()]];
626 NSString* file;
627 while (file = [enumerator nextObject]) {
628 if ([file hasPrefix:@"."]) {
629 continue;
630 }
631 filenames.push_back(std::string([file UTF8String]));
632 }
633 return filenames;
634}
635
637 const std::string& folder) {
638 std::vector<std::string> subdirectories;
639 NSFileManager* fileManager = [NSFileManager defaultManager];
640 NSDirectoryEnumerator* enumerator =
641 [fileManager enumeratorAtPath:[NSString stringWithUTF8String:folder.c_str()]];
642 NSString* file;
643 while (file = [enumerator nextObject]) {
644 if ([file hasPrefix:@"."]) {
645 continue;
646 }
647 BOOL isDirectory;
648 NSString* path =
649 [NSString stringWithFormat:@"%@/%@", [NSString stringWithUTF8String:folder.c_str()], file];
650 [fileManager fileExistsAtPath:path isDirectory:&isDirectory];
651 if (isDirectory) {
652 subdirectories.push_back(std::string([file UTF8String]));
653 }
654 }
655 return subdirectories;
656}
657
659 NSBundle* bundle = [NSBundle mainBundle];
660 NSString* resourceDirectoryPath = [bundle bundlePath];
661 NSString* path = [resourceDirectoryPath stringByAppendingString:@"/"];
662 return [path UTF8String];
663}
664
665#else
666// Unsupported platform
667#endif // TARGET_OS_MAC
668
669#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