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()];
101 UTType* type = token ==
"yazeproj"
102 ? [UTType typeWithIdentifier:@"org.halext.yaze.project"]
105 type = [UTType typeWithFilenameExtension:ext];
108 NSString* identifier = [NSString stringWithUTF8String:token.c_str()];
109 type = [UTType typeWithIdentifier:identifier];
112 [types addObject:type];
117 if (allow_all || [types count] == 0) {
118 return @[ UTTypeData ];
124std::filesystem::path ResolveDocumentsPath() {
126 if (docs_result.ok()) {
130 if (temp_result.ok()) {
134 auto cwd = std::filesystem::current_path(ec);
138 return std::filesystem::path(
".");
141std::string NormalizeExtension(
const std::string& ext) {
145 if (ext.front() ==
'.') {
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;
164void ShowOpenFileDialogImpl(NSArray<UTType*>* allowed_types,
165 void (^completionHandler)(std::string)) {
168 completionHandler(
"");
171 [appDelegate PresentDocumentPickerWithCompletionHandler:^(NSString *filePath) {
172 completionHandler(std::string([filePath UTF8String]));
174 allowedTypes:allowed_types];
177std::string ShowOpenFileDialogSync(
179 __block std::string result;
180 __block
bool done =
false;
181 NSArray<UTType*>* allowed_types = BuildAllowedTypes(options);
183 auto present_picker = ^{
184 ShowOpenFileDialogImpl(allowed_types, ^(std::string filePath) {
190 if ([NSThread isMainThread]) {
194 [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
195 beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
198 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
199 dispatch_async(dispatch_get_main_queue(), ^{
200 ShowOpenFileDialogImpl(allowed_types, ^(std::string filePath) {
202 dispatch_semaphore_signal(semaphore);
205 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
213 const FileDialogOptions& options) {
214 return ShowOpenFileDialogSync(options);
218 return ShowOpenFileDialog(FileDialogOptions{});
222 return ShowOpenFileDialog(FileDialogOptions{});
226 return ShowOpenFileDialog(FileDialogOptions{});
230 const FileDialogOptions& options,
231 std::function<
void(
const std::string&)> callback) {
235 NSArray<UTType*>* allowed_types = BuildAllowedTypes(options);
237 std::make_shared<std::function<void(
const std::string&)>>(
238 std::move(callback));
240 auto present_picker = ^{
241 ShowOpenFileDialogImpl(allowed_types, ^(std::string filePath) {
242 (*callback_ptr)(filePath);
246 if ([NSThread isMainThread]) {
249 dispatch_async(dispatch_get_main_queue(), present_picker);
254 return ResolveDocumentsPath().string();
258 const std::string& default_name,
const std::string& default_extension) {
259 if (
auto result = ConsumeSaveFileDialogResultForTesting()) {
262 const auto base_dir = ResolveDocumentsPath();
263 const std::string filename = BuildSaveFilename(default_name, default_extension);
264 return (base_dir / filename).string();
268 const std::string& default_name,
const std::string& default_extension) {
269 return ShowSaveFileDialog(default_name, default_extension);
273 const std::string& default_name,
const std::string& default_extension) {
274 return ShowSaveFileDialog(default_name, default_extension);
278 const std::string &folder) {
279 std::vector<std::string> files;
281 for (
const auto& entry : std::filesystem::directory_iterator(folder, ec)) {
285 if (entry.is_regular_file()) {
286 files.push_back(entry.path().string());
293 const std::string &folder) {
294 std::vector<std::string> directories;
296 for (
const auto& entry : std::filesystem::directory_iterator(folder, ec)) {
300 if (entry.is_directory()) {
301 directories.push_back(entry.path().string());
308 NSBundle* bundle = [NSBundle mainBundle];
309 NSString* resourceDirectoryPath = [bundle bundlePath];
310 NSString* path = [resourceDirectoryPath stringByAppendingString:@"/"];
311 return [path UTF8String];
314#elif TARGET_OS_MAC == 1
317#import <Cocoa/Cocoa.h>
318#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
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) {
326 const auto end = input.find_last_not_of(
" \t\n\r");
327 return input.substr(start, end - start + 1);
330std::vector<std::string> SplitFilterSpec(
const std::string& spec) {
331 std::vector<std::string> tokens;
333 for (
char ch : spec) {
335 std::string trimmed =
TrimCopy(current);
336 if (!trimmed.empty() && trimmed[0] ==
'.') {
339 if (!trimmed.empty()) {
340 tokens.push_back(trimmed);
344 current.push_back(ch);
347 std::string trimmed =
TrimCopy(current);
348 if (!trimmed.empty() && trimmed[0] ==
'.') {
351 if (!trimmed.empty()) {
352 tokens.push_back(trimmed);
357std::vector<std::string> CollectExtensions(
359 std::vector<std::string> extensions;
365 for (
const auto& filter : options.filters) {
366 const std::string spec =
TrimCopy(filter.spec);
367 if (spec.empty() || spec ==
"*") {
372 for (
const auto& token : SplitFilterSpec(spec)) {
376 extensions.push_back(token);
384std::string ShowOpenFileDialogBespokeWithOptions(
386 NSOpenPanel* openPanel = [NSOpenPanel openPanel];
387 [openPanel setCanChooseFiles:YES];
388 [openPanel setCanChooseDirectories:NO];
389 [openPanel setAllowsMultipleSelection:NO];
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];
396 NSMutableArray<NSString*>* allowed_types = [NSMutableArray array];
397 for (
const auto& extension : extensions) {
398 NSString* ext = [NSString stringWithUTF8String:extension.c_str()];
400 [allowed_types addObject:ext];
403 [openPanel setAllowedFileTypes:allowed_types];
406 if ([openPanel runModal] == NSModalResponseOK) {
407 NSURL* url = [[openPanel URLs] objectAtIndex:0];
408 NSString* path = [url path];
409 return std::string([path UTF8String]);
415std::string ShowOpenFileDialogNFDWithOptions(
417#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
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;
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());
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()});
440 filter_list = filter_items.data();
441 filter_count = filter_items.size();
444 nfdopendialogu8args_t args = {0};
445 args.filterList = filter_list;
446 args.filterCount = filter_count;
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);
454 }
else if (result == NFD_CANCEL) {
461 return ShowOpenFileDialogBespokeWithOptions(options);
467 return ShowOpenFileDialogBespokeWithOptions(FileDialogOptions{});
471 const FileDialogOptions& options,
472 std::function<
void(
const std::string&)> callback) {
476 callback(ShowOpenFileDialog(options));
480 const std::string& default_extension) {
481 NSSavePanel* savePanel = [NSSavePanel savePanel];
483 if (!default_name.empty()) {
484 [savePanel setNameFieldStringValue:[NSString stringWithUTF8String:default_name.c_str()]];
487 if (!default_extension.empty()) {
488 NSString* ext = [NSString stringWithUTF8String:default_extension.c_str()];
489 [savePanel setAllowedFileTypes:@[ext]];
492 if ([savePanel runModal] == NSModalResponseOK) {
493 NSURL* url = [savePanel URL];
494 NSString* path = [url path];
495 return std::string([path UTF8String]);
503 const FileDialogOptions& options) {
504 if (core::FeatureFlags::get().kUseNativeFileDialog) {
505 return ShowOpenFileDialogNFDWithOptions(options);
507 return ShowOpenFileDialogBespokeWithOptions(options);
511 return ShowOpenFileDialog(FileDialogOptions{});
515 if (core::FeatureFlags::get().kUseNativeFileDialog) {
516 return ShowOpenFolderDialogNFD();
518 return ShowOpenFolderDialogBespoke();
523 const std::string& default_extension) {
524 if (
auto result = ConsumeSaveFileDialogResultForTesting()) {
527 if (core::FeatureFlags::get().kUseNativeFileDialog) {
528 return ShowSaveFileDialogNFD(default_name, default_extension);
530 return ShowSaveFileDialogBespoke(default_name, default_extension);
536 return ShowOpenFileDialogNFDWithOptions(FileDialogOptions{});
540#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
542 nfdu8char_t *out_path = NULL;
543 nfdresult_t result = NFD_PickFolderU8(&out_path, NULL);
545 if (result == NFD_OKAY) {
546 std::string folder_path(out_path);
547 NFD_FreePath(out_path);
550 }
else if (result == NFD_CANCEL) {
558 return ShowOpenFolderDialogBespoke();
563 const std::string& default_extension) {
564#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
566 nfdu8char_t *out_path = NULL;
568 nfdsavedialogu8args_t args = {0};
569 if (!default_extension.empty()) {
571 static nfdu8filteritem_t filters[3] = {
572 {
"Theme File",
"theme"},
573 {
"Project File",
"yaze"},
574 {
"ROM File",
"sfc,smc"}
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;
589 if (!default_name.empty()) {
590 args.defaultName = default_name.c_str();
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);
599 }
else if (result == NFD_CANCEL) {
607 return ShowSaveFileDialogBespoke(default_name, default_extension);
612 NSOpenPanel* openPanel = [NSOpenPanel openPanel];
613 [openPanel setCanChooseFiles:NO];
614 [openPanel setCanChooseDirectories:YES];
615 [openPanel setAllowsMultipleSelection:NO];
617 if ([openPanel runModal] == NSModalResponseOK) {
618 NSURL* url = [[openPanel URLs] objectAtIndex:0];
619 NSString* path = [url path];
620 return std::string([path UTF8String]);
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()]];
633 while (file = [enumerator nextObject]) {
634 if ([file hasPrefix:
@"."]) {
637 filenames.push_back(std::string([file UTF8String]));
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()]];
649 while (file = [enumerator nextObject]) {
650 if ([file hasPrefix:
@"."]) {
655 [NSString stringWithFormat:@"%@/%@", [NSString stringWithUTF8String:folder.c_str()], file];
656 [fileManager fileExistsAtPath:path isDirectory:&isDirectory];
658 subdirectories.push_back(std::string([file UTF8String]));
661 return subdirectories;
665 NSBundle* bundle = [NSBundle mainBundle];
666 NSString* resourceDirectoryPath = [bundle bundlePath];
667 NSString* path = [resourceDirectoryPath stringByAppendingString:@"/"];
668 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