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 const auto base_dir = ResolveDocumentsPath();
260 const std::string filename = BuildSaveFilename(default_name, default_extension);
261 return (base_dir / filename).string();
265 const std::string& default_name,
const std::string& default_extension) {
266 return ShowSaveFileDialog(default_name, default_extension);
270 const std::string& default_name,
const std::string& default_extension) {
271 return ShowSaveFileDialog(default_name, default_extension);
275 const std::string &folder) {
276 std::vector<std::string> files;
278 for (
const auto& entry : std::filesystem::directory_iterator(folder, ec)) {
282 if (entry.is_regular_file()) {
283 files.push_back(entry.path().string());
290 const std::string &folder) {
291 std::vector<std::string> directories;
293 for (
const auto& entry : std::filesystem::directory_iterator(folder, ec)) {
297 if (entry.is_directory()) {
298 directories.push_back(entry.path().string());
305 NSBundle* bundle = [NSBundle mainBundle];
306 NSString* resourceDirectoryPath = [bundle bundlePath];
307 NSString* path = [resourceDirectoryPath stringByAppendingString:@"/"];
308 return [path UTF8String];
311#elif TARGET_OS_MAC == 1
314#import <Cocoa/Cocoa.h>
315#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
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) {
323 const auto end = input.find_last_not_of(
" \t\n\r");
324 return input.substr(start, end - start + 1);
327std::vector<std::string> SplitFilterSpec(
const std::string& spec) {
328 std::vector<std::string> tokens;
330 for (
char ch : spec) {
332 std::string trimmed =
TrimCopy(current);
333 if (!trimmed.empty() && trimmed[0] ==
'.') {
336 if (!trimmed.empty()) {
337 tokens.push_back(trimmed);
341 current.push_back(ch);
344 std::string trimmed =
TrimCopy(current);
345 if (!trimmed.empty() && trimmed[0] ==
'.') {
348 if (!trimmed.empty()) {
349 tokens.push_back(trimmed);
354std::vector<std::string> CollectExtensions(
356 std::vector<std::string> extensions;
362 for (
const auto& filter : options.filters) {
363 const std::string spec =
TrimCopy(filter.spec);
364 if (spec.empty() || spec ==
"*") {
369 for (
const auto& token : SplitFilterSpec(spec)) {
373 extensions.push_back(token);
381std::string ShowOpenFileDialogBespokeWithOptions(
383 NSOpenPanel* openPanel = [NSOpenPanel openPanel];
384 [openPanel setCanChooseFiles:YES];
385 [openPanel setCanChooseDirectories:NO];
386 [openPanel setAllowsMultipleSelection:NO];
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];
393 NSMutableArray<NSString*>* allowed_types = [NSMutableArray array];
394 for (
const auto& extension : extensions) {
395 NSString* ext = [NSString stringWithUTF8String:extension.c_str()];
397 [allowed_types addObject:ext];
400 [openPanel setAllowedFileTypes:allowed_types];
403 if ([openPanel runModal] == NSModalResponseOK) {
404 NSURL* url = [[openPanel URLs] objectAtIndex:0];
405 NSString* path = [url path];
406 return std::string([path UTF8String]);
412std::string ShowOpenFileDialogNFDWithOptions(
414#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
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;
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());
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()});
437 filter_list = filter_items.data();
438 filter_count = filter_items.size();
441 nfdopendialogu8args_t args = {0};
442 args.filterList = filter_list;
443 args.filterCount = filter_count;
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);
451 }
else if (result == NFD_CANCEL) {
458 return ShowOpenFileDialogBespokeWithOptions(options);
464 return ShowOpenFileDialogBespokeWithOptions(FileDialogOptions{});
468 const FileDialogOptions& options,
469 std::function<
void(
const std::string&)> callback) {
473 callback(ShowOpenFileDialog(options));
477 const std::string& default_extension) {
478 NSSavePanel* savePanel = [NSSavePanel savePanel];
480 if (!default_name.empty()) {
481 [savePanel setNameFieldStringValue:[NSString stringWithUTF8String:default_name.c_str()]];
484 if (!default_extension.empty()) {
485 NSString* ext = [NSString stringWithUTF8String:default_extension.c_str()];
486 [savePanel setAllowedFileTypes:@[ext]];
489 if ([savePanel runModal] == NSModalResponseOK) {
490 NSURL* url = [savePanel URL];
491 NSString* path = [url path];
492 return std::string([path UTF8String]);
500 const FileDialogOptions& options) {
501 if (core::FeatureFlags::get().kUseNativeFileDialog) {
502 return ShowOpenFileDialogNFDWithOptions(options);
504 return ShowOpenFileDialogBespokeWithOptions(options);
508 return ShowOpenFileDialog(FileDialogOptions{});
512 if (core::FeatureFlags::get().kUseNativeFileDialog) {
513 return ShowOpenFolderDialogNFD();
515 return ShowOpenFolderDialogBespoke();
520 const std::string& default_extension) {
521 if (core::FeatureFlags::get().kUseNativeFileDialog) {
522 return ShowSaveFileDialogNFD(default_name, default_extension);
524 return ShowSaveFileDialogBespoke(default_name, default_extension);
530 return ShowOpenFileDialogNFDWithOptions(FileDialogOptions{});
534#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
536 nfdu8char_t *out_path = NULL;
537 nfdresult_t result = NFD_PickFolderU8(&out_path, NULL);
539 if (result == NFD_OKAY) {
540 std::string folder_path(out_path);
541 NFD_FreePath(out_path);
544 }
else if (result == NFD_CANCEL) {
552 return ShowOpenFolderDialogBespoke();
557 const std::string& default_extension) {
558#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
560 nfdu8char_t *out_path = NULL;
562 nfdsavedialogu8args_t args = {0};
563 if (!default_extension.empty()) {
565 static nfdu8filteritem_t filters[3] = {
566 {
"Theme File",
"theme"},
567 {
"Project File",
"yaze"},
568 {
"ROM File",
"sfc,smc"}
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;
583 if (!default_name.empty()) {
584 args.defaultName = default_name.c_str();
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);
593 }
else if (result == NFD_CANCEL) {
601 return ShowSaveFileDialogBespoke(default_name, default_extension);
606 NSOpenPanel* openPanel = [NSOpenPanel openPanel];
607 [openPanel setCanChooseFiles:NO];
608 [openPanel setCanChooseDirectories:YES];
609 [openPanel setAllowsMultipleSelection:NO];
611 if ([openPanel runModal] == NSModalResponseOK) {
612 NSURL* url = [[openPanel URLs] objectAtIndex:0];
613 NSString* path = [url path];
614 return std::string([path UTF8String]);
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()]];
627 while (file = [enumerator nextObject]) {
628 if ([file hasPrefix:
@"."]) {
631 filenames.push_back(std::string([file UTF8String]));
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()]];
643 while (file = [enumerator nextObject]) {
644 if ([file hasPrefix:
@"."]) {
649 [NSString stringWithFormat:@"%@/%@", [NSString stringWithUTF8String:folder.c_str()], file];
650 [fileManager fileExistsAtPath:path isDirectory:&isDirectory];
652 subdirectories.push_back(std::string([file UTF8String]));
655 return subdirectories;
659 NSBundle* bundle = [NSBundle mainBundle];
660 NSString* resourceDirectoryPath = [bundle bundlePath];
661 NSString* path = [resourceDirectoryPath stringByAppendingString:@"/"];
662 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