12#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
16#if defined(__APPLE__) && defined(__MACH__)
18#include <Foundation/Foundation.h>
19#include <TargetConditionals.h>
21#import <CoreText/CoreText.h>
23#if TARGET_IPHONE_SIMULATOR == 1 || TARGET_OS_IPHONE == 1
25#import <dispatch/dispatch.h>
26#import <UIKit/UIKit.h>
27#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
31@interface AppDelegate : UIResponder <UIApplicationDelegate, UIDocumentPickerDelegate>
34@interface AppDelegate (FileDialog)
35- (void)PresentDocumentPickerWithCompletionHandler:
36 (
void (^)(NSString *selectedFile))completionHandler
37 allowedTypes:(NSArray<UTType*> *)allowedTypes;
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) {
46 const auto end = input.find_last_not_of(
" \t\n\r");
47 return input.substr(start, end - start + 1);
50std::vector<std::string> SplitFilterSpec(
const std::string& spec) {
51 std::vector<std::string> tokens;
53 for (
char ch : spec) {
55 std::string trimmed = TrimCopy(current);
56 if (!trimmed.empty() && trimmed[0] ==
'.') {
59 if (!trimmed.empty()) {
60 tokens.push_back(trimmed);
64 current.push_back(ch);
67 std::string trimmed = TrimCopy(current);
68 if (!trimmed.empty() && trimmed[0] ==
'.') {
71 if (!trimmed.empty()) {
72 tokens.push_back(trimmed);
79 return @[ UTTypeData ];
82 bool allow_all =
false;
83 NSMutableArray<UTType*>* types = [NSMutableArray array];
85 for (
const auto& filter : options.filters) {
86 const std::string spec = TrimCopy(filter.spec);
87 if (spec.empty() || spec ==
"*") {
92 for (
const auto& token : SplitFilterSpec(spec)) {
98 NSString* ext = [NSString stringWithUTF8String:token.c_str()];
99 UTType* type = [UTType typeWithFilenameExtension:ext];
101 NSString* identifier = [NSString stringWithUTF8String:token.c_str()];
102 type = [UTType typeWithIdentifier:identifier];
105 [types addObject:type];
110 if (allow_all || [types count] == 0) {
111 return @[ UTTypeData ];
117std::filesystem::path ResolveDocumentsPath() {
119 if (docs_result.ok()) {
123 if (temp_result.ok()) {
127 auto cwd = std::filesystem::current_path(ec);
131 return std::filesystem::path(
".");
134std::string NormalizeExtension(
const std::string& ext) {
138 if (ext.front() ==
'.') {
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;
157void ShowOpenFileDialogImpl(NSArray<UTType*>* allowed_types,
158 void (^completionHandler)(std::string)) {
161 completionHandler(
"");
164 [appDelegate PresentDocumentPickerWithCompletionHandler:^(NSString *filePath) {
165 completionHandler(std::string([filePath UTF8String]));
167 allowedTypes:allowed_types];
170std::string ShowOpenFileDialogSync(
172 __block std::string result;
173 __block
bool done =
false;
174 NSArray<UTType*>* allowed_types = BuildAllowedTypes(options);
176 auto present_picker = ^{
177 ShowOpenFileDialogImpl(allowed_types, ^(std::string filePath) {
183 if ([NSThread isMainThread]) {
187 [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
188 beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
191 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
192 dispatch_async(dispatch_get_main_queue(), ^{
193 ShowOpenFileDialogImpl(allowed_types, ^(std::string filePath) {
195 dispatch_semaphore_signal(semaphore);
198 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
206 const FileDialogOptions& options) {
207 return ShowOpenFileDialogSync(options);
211 return ShowOpenFileDialog(FileDialogOptions{});
215 return ShowOpenFileDialog(FileDialogOptions{});
219 return ShowOpenFileDialog(FileDialogOptions{});
223 const FileDialogOptions& options,
224 std::function<
void(
const std::string&)> callback) {
228 NSArray<UTType*>* allowed_types = BuildAllowedTypes(options);
230 std::make_shared<std::function<void(
const std::string&)>>(
231 std::move(callback));
233 auto present_picker = ^{
234 ShowOpenFileDialogImpl(allowed_types, ^(std::string filePath) {
235 (*callback_ptr)(filePath);
239 if ([NSThread isMainThread]) {
242 dispatch_async(dispatch_get_main_queue(), present_picker);
247 return ResolveDocumentsPath().string();
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();
258 const std::string& default_name,
const std::string& default_extension) {
259 return ShowSaveFileDialog(default_name, default_extension);
263 const std::string& default_name,
const std::string& default_extension) {
264 return ShowSaveFileDialog(default_name, default_extension);
268 const std::string &folder) {
269 std::vector<std::string> files;
271 for (
const auto& entry : std::filesystem::directory_iterator(folder, ec)) {
275 if (entry.is_regular_file()) {
276 files.push_back(entry.path().string());
283 const std::string &folder) {
284 std::vector<std::string> directories;
286 for (
const auto& entry : std::filesystem::directory_iterator(folder, ec)) {
290 if (entry.is_directory()) {
291 directories.push_back(entry.path().string());
298 NSBundle* bundle = [NSBundle mainBundle];
299 NSString* resourceDirectoryPath = [bundle bundlePath];
300 NSString* path = [resourceDirectoryPath stringByAppendingString:@"/"];
301 return [path UTF8String];
304#elif TARGET_OS_MAC == 1
307#import <Cocoa/Cocoa.h>
308#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
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) {
316 const auto end = input.find_last_not_of(
" \t\n\r");
317 return input.substr(start, end - start + 1);
320std::vector<std::string> SplitFilterSpec(
const std::string& spec) {
321 std::vector<std::string> tokens;
323 for (
char ch : spec) {
325 std::string trimmed = TrimCopy(current);
326 if (!trimmed.empty() && trimmed[0] ==
'.') {
329 if (!trimmed.empty()) {
330 tokens.push_back(trimmed);
334 current.push_back(ch);
337 std::string trimmed = TrimCopy(current);
338 if (!trimmed.empty() && trimmed[0] ==
'.') {
341 if (!trimmed.empty()) {
342 tokens.push_back(trimmed);
347std::vector<std::string> CollectExtensions(
349 std::vector<std::string> extensions;
355 for (
const auto& filter : options.filters) {
356 const std::string spec = TrimCopy(filter.spec);
357 if (spec.empty() || spec ==
"*") {
362 for (
const auto& token : SplitFilterSpec(spec)) {
366 extensions.push_back(token);
374std::string ShowOpenFileDialogBespokeWithOptions(
376 NSOpenPanel* openPanel = [NSOpenPanel openPanel];
377 [openPanel setCanChooseFiles:YES];
378 [openPanel setCanChooseDirectories:NO];
379 [openPanel setAllowsMultipleSelection:NO];
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];
386 NSMutableArray<NSString*>* allowed_types = [NSMutableArray array];
387 for (
const auto& extension : extensions) {
388 NSString* ext = [NSString stringWithUTF8String:extension.c_str()];
390 [allowed_types addObject:ext];
393 [openPanel setAllowedFileTypes:allowed_types];
396 if ([openPanel runModal] == NSModalResponseOK) {
397 NSURL* url = [[openPanel URLs] objectAtIndex:0];
398 NSString* path = [url path];
399 return std::string([path UTF8String]);
405std::string ShowOpenFileDialogNFDWithOptions(
407#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
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;
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());
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()});
430 filter_list = filter_items.data();
431 filter_count = filter_items.size();
434 nfdopendialogu8args_t args = {0};
435 args.filterList = filter_list;
436 args.filterCount = filter_count;
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);
444 }
else if (result == NFD_CANCEL) {
451 return ShowOpenFileDialogBespokeWithOptions(options);
457 return ShowOpenFileDialogBespokeWithOptions(FileDialogOptions{});
461 const FileDialogOptions& options,
462 std::function<
void(
const std::string&)> callback) {
466 callback(ShowOpenFileDialog(options));
470 const std::string& default_extension) {
471 NSSavePanel* savePanel = [NSSavePanel savePanel];
473 if (!default_name.empty()) {
474 [savePanel setNameFieldStringValue:[NSString stringWithUTF8String:default_name.c_str()]];
477 if (!default_extension.empty()) {
478 NSString* ext = [NSString stringWithUTF8String:default_extension.c_str()];
479 [savePanel setAllowedFileTypes:@[ext]];
482 if ([savePanel runModal] == NSModalResponseOK) {
483 NSURL* url = [savePanel URL];
484 NSString* path = [url path];
485 return std::string([path UTF8String]);
493 const FileDialogOptions& options) {
494 if (core::FeatureFlags::get().kUseNativeFileDialog) {
495 return ShowOpenFileDialogNFDWithOptions(options);
497 return ShowOpenFileDialogBespokeWithOptions(options);
501 return ShowOpenFileDialog(FileDialogOptions{});
505 if (core::FeatureFlags::get().kUseNativeFileDialog) {
506 return ShowOpenFolderDialogNFD();
508 return ShowOpenFolderDialogBespoke();
513 const std::string& default_extension) {
514 if (core::FeatureFlags::get().kUseNativeFileDialog) {
515 return ShowSaveFileDialogNFD(default_name, default_extension);
517 return ShowSaveFileDialogBespoke(default_name, default_extension);
523 return ShowOpenFileDialogNFDWithOptions(FileDialogOptions{});
527#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
529 nfdu8char_t *out_path = NULL;
530 nfdresult_t result = NFD_PickFolderU8(&out_path, NULL);
532 if (result == NFD_OKAY) {
533 std::string folder_path(out_path);
534 NFD_FreePath(out_path);
537 }
else if (result == NFD_CANCEL) {
545 return ShowOpenFolderDialogBespoke();
550 const std::string& default_extension) {
551#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
553 nfdu8char_t *out_path = NULL;
555 nfdsavedialogu8args_t args = {0};
556 if (!default_extension.empty()) {
558 static nfdu8filteritem_t filters[3] = {
559 {
"Theme File",
"theme"},
560 {
"Project File",
"yaze"},
561 {
"ROM File",
"sfc,smc"}
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;
576 if (!default_name.empty()) {
577 args.defaultName = default_name.c_str();
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);
586 }
else if (result == NFD_CANCEL) {
594 return ShowSaveFileDialogBespoke(default_name, default_extension);
599 NSOpenPanel* openPanel = [NSOpenPanel openPanel];
600 [openPanel setCanChooseFiles:NO];
601 [openPanel setCanChooseDirectories:YES];
602 [openPanel setAllowsMultipleSelection:NO];
604 if ([openPanel runModal] == NSModalResponseOK) {
605 NSURL* url = [[openPanel URLs] objectAtIndex:0];
606 NSString* path = [url path];
607 return std::string([path UTF8String]);
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()]];
620 while (file = [enumerator nextObject]) {
621 if ([file hasPrefix:
@"."]) {
624 filenames.push_back(std::string([file UTF8String]));
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()]];
636 while (file = [enumerator nextObject]) {
637 if ([file hasPrefix:
@"."]) {
642 [NSString stringWithFormat:@"%@/%@", [NSString stringWithUTF8String:folder.c_str()], file];
643 [fileManager fileExistsAtPath:path isDirectory:&isDirectory];
645 subdirectories.push_back(std::string([file UTF8String]));
648 return subdirectories;
652 NSBundle* bundle = [NSBundle mainBundle];
653 NSString* resourceDirectoryPath = [bundle bundlePath];
654 NSString* path = [resourceDirectoryPath stringByAppendingString:@"/"];
655 return [path UTF8String];
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()
std::string GetBundleResourcePath()
GetBundleResourcePath returns the path to the bundle resource directory. Specific to MacOS.
std::vector< FileDialogFilter > filters