From 70a8da90ad3e4320d0a3d35484145cd0507b082d Mon Sep 17 00:00:00 2001 From: Bartosz Taudul Date: Wed, 20 Apr 2022 18:09:28 +0200 Subject: [PATCH] Switch NFD to NFD Extended. --- nfd/common.h | 21 - nfd/nfd.h | 291 +++++++-- nfd/nfd_cocoa.m | 535 ++++++++-------- nfd/nfd_common.c | 142 ----- nfd/nfd_common.h | 37 -- nfd/nfd_gtk.c | 381 ----------- nfd/nfd_gtk.cpp | 631 +++++++++++++++++++ nfd/nfd_portal.cpp | 1414 +++++++++++++++++++++++++++++++++++++++++ nfd/nfd_win.cpp | 1491 +++++++++++++++++++++++++------------------- 9 files changed, 3415 insertions(+), 1528 deletions(-) delete mode 100644 nfd/common.h delete mode 100644 nfd/nfd_common.c delete mode 100644 nfd/nfd_common.h delete mode 100644 nfd/nfd_gtk.c create mode 100644 nfd/nfd_gtk.cpp create mode 100644 nfd/nfd_portal.cpp diff --git a/nfd/common.h b/nfd/common.h deleted file mode 100644 index 7745d323..00000000 --- a/nfd/common.h +++ /dev/null @@ -1,21 +0,0 @@ -/* - Native File Dialog - - Internal, common across platforms - - http://www.frogtoss.com/labs - */ - - -#ifndef _NFD_COMMON_H -#define _NFD_COMMON_H - -#define NFD_MAX_STRLEN 256 -#define _NFD_UNUSED(x) ((void)x) - -void *NFDi_Malloc( size_t bytes ); -void NFDi_Free( void *ptr ); -void NFDi_SetError( const char *msg ); -void NFDi_SafeStrncpy( char *dst, const char *src, size_t maxCopy ); - -#endif diff --git a/nfd/nfd.h b/nfd/nfd.h index 7a1e5601..eb9ba6d8 100644 --- a/nfd/nfd.h +++ b/nfd/nfd.h @@ -1,76 +1,281 @@ /* - Native File Dialog + Native File Dialog Extended + Repository: https://github.com/btzy/nativefiledialog-extended + License: Zlib + Authors: Bernard Teo, Michael Labbe - User API - - http://www.frogtoss.com/labs + This header contains the functions that can be called by user code. */ - #ifndef _NFD_H #define _NFD_H #ifdef __cplusplus extern "C" { -#endif +#endif // __cplusplus #include +#ifdef _WIN32 +/* denotes UTF-16 char */ +typedef wchar_t nfdnchar_t; +#else /* denotes UTF-8 char */ -typedef char nfdchar_t; +typedef char nfdnchar_t; +#endif // _WIN32 /* opaque data structure -- see NFD_PathSet_* */ +typedef void nfdpathset_t; +#ifndef NFD_PORTAL typedef struct { - nfdchar_t *buf; - size_t *indices; /* byte offsets into buf */ - size_t count; /* number of indices into buf */ -}nfdpathset_t; + void* ptr; +} nfdpathsetenum_t; +#else +typedef struct { + void* d1; + void* d2; + unsigned int d3; + int d4; + int d5; + int d6; + int d7; + int d8; + int d9; + int d10; + int d11; + int p1; + void* p2; + void* p3; +} nfdpathsetenum_t; +#endif + +typedef unsigned int nfdfiltersize_t; typedef enum { - NFD_ERROR, /* programmatic error */ - NFD_OKAY, /* user pressed okay, or successful return */ - NFD_CANCEL /* user pressed cancel */ -}nfdresult_t; - + NFD_ERROR, /* programmatic error */ + NFD_OKAY, /* user pressed okay, or successful return */ + NFD_CANCEL /* user pressed cancel */ +} nfdresult_t; -/* nfd_.c */ +typedef struct { + const nfdnchar_t* name; + const nfdnchar_t* spec; +} nfdnfilteritem_t; -/* single file open dialog */ -nfdresult_t NFD_OpenDialog( const nfdchar_t *filterList, - const nfdchar_t *defaultPath, - nfdchar_t **outPath, - void* owner ); +/* free a file path that was returned by the dialogs */ +/* Note: use NFD_PathSet_FreePath to free path from pathset instead of this function */ +void NFD_FreePathN(nfdnchar_t* filePath); -/* multiple file open dialog */ -nfdresult_t NFD_OpenDialogMultiple( const nfdchar_t *filterList, - const nfdchar_t *defaultPath, - nfdpathset_t *outPaths ); +/* initialize NFD - call this for every thread that might use NFD, before calling any other NFD + * functions on that thread */ +nfdresult_t NFD_Init(void); + +/* call this to de-initialize NFD, if NFD_Init returned NFD_OKAY */ +void NFD_Quit(void); + +/* single file open dialog */ +/* It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function returns + * NFD_OKAY */ +/* If filterCount is zero, filterList is ignored (you can use NULL) */ +/* If defaultPath is NULL, the operating system will decide */ +nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath); + +/* multiple file open dialog */ +/* It is the caller's responsibility to free `outPaths` via NFD_PathSet_Free() if this function + * returns NFD_OKAY */ +/* If filterCount is zero, filterList is ignored (you can use NULL) */ +/* If defaultPath is NULL, the operating system will decide */ +nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath); /* save dialog */ -nfdresult_t NFD_SaveDialog( const nfdchar_t *filterList, - const nfdchar_t *defaultPath, - nfdchar_t **outPath, - void* owner ); - +/* It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function returns + * NFD_OKAY */ +/* If filterCount is zero, filterList is ignored (you can use NULL) */ +/* If defaultPath is NULL, the operating system will decide */ +nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath, + const nfdnchar_t* defaultName); /* select folder dialog */ -nfdresult_t NFD_PickFolder( const nfdchar_t *defaultPath, - nfdchar_t **outPath); +/* It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function returns + * NFD_OKAY */ +/* If defaultPath is NULL, the operating system will decide */ +nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath); -/* nfd_common.c */ +/* Get last error -- set when nfdresult_t returns NFD_ERROR */ +/* Returns the last error that was set, or NULL if there is no error. */ +/* The memory is owned by NFD and should not be freed by user code. */ +/* This is *always* ASCII printable characters, so it can be interpreted as UTF-8 without any + * conversion. */ +const char* NFD_GetError(void); +/* clear the error */ +void NFD_ClearError(void); + +/* path set operations */ +#ifdef _WIN32 +typedef unsigned long nfdpathsetsize_t; +#elif __APPLE__ +typedef unsigned long nfdpathsetsize_t; +#else +typedef unsigned int nfdpathsetsize_t; +#endif // _WIN32, __APPLE__ + +/* Gets the number of entries stored in pathSet */ +/* note that some paths might be invalid (NFD_ERROR will be returned by NFD_PathSet_GetPath), so we + * might not actually have this number of usable paths */ +nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count); +/* Gets the UTF-8 path at offset index */ +/* It is the caller's responsibility to free `outPath` via NFD_PathSet_FreePathN() if this function + * returns NFD_OKAY */ +nfdresult_t NFD_PathSet_GetPathN(const nfdpathset_t* pathSet, + nfdpathsetsize_t index, + nfdnchar_t** outPath); +/* Free the path gotten by NFD_PathSet_GetPathN */ +#ifdef _WIN32 +#define NFD_PathSet_FreePathN NFD_FreePathN +#elif __APPLE__ +#define NFD_PathSet_FreePathN NFD_FreePathN +#else +void NFD_PathSet_FreePathN(const nfdnchar_t* filePath); +#endif // _WIN32, __APPLE__ + +/* Gets an enumerator of the path set. */ +/* It is the caller's responsibility to free `enumerator` via NFD_PathSet_FreeEnum() if this + * function returns NFD_OKAY, and it should be freed before freeing the pathset. */ +nfdresult_t NFD_PathSet_GetEnum(const nfdpathset_t* pathSet, nfdpathsetenum_t* outEnumerator); +/* Frees an enumerator of the path set. */ +void NFD_PathSet_FreeEnum(nfdpathsetenum_t* enumerator); +/* Gets the next item from the path set enumerator. + * If there are no more items, then *outPaths will be set to NULL. */ +/* It is the caller's responsibility to free `*outPath` via NFD_PathSet_FreePath() if this + * function returns NFD_OKAY and `*outPath` is not null */ +nfdresult_t NFD_PathSet_EnumNextN(nfdpathsetenum_t* enumerator, nfdnchar_t** outPath); + +/* Free the pathSet */ +void NFD_PathSet_Free(const nfdpathset_t* pathSet); + +#ifdef _WIN32 + +/* say that the U8 versions of functions are not just #defined to be the native versions */ +#define NFD_DIFFERENT_NATIVE_FUNCTIONS + +typedef char nfdu8char_t; + +typedef struct { + const nfdu8char_t* name; + const nfdu8char_t* spec; +} nfdu8filteritem_t; + +/* UTF-8 compatibility functions */ + +/* free a file path that was returned */ +void NFD_FreePathU8(nfdu8char_t* outPath); + +/* single file open dialog */ +/* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns + * NFD_OKAY */ +nfdresult_t NFD_OpenDialogU8(nfdu8char_t** outPath, + const nfdu8filteritem_t* filterList, + nfdfiltersize_t count, + const nfdu8char_t* defaultPath); + +/* multiple file open dialog */ +/* It is the caller's responsibility to free `outPaths` via NFD_PathSet_Free() if this function + * returns NFD_OKAY */ +nfdresult_t NFD_OpenDialogMultipleU8(const nfdpathset_t** outPaths, + const nfdu8filteritem_t* filterList, + nfdfiltersize_t count, + const nfdu8char_t* defaultPath); + +/* save dialog */ +/* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns + * NFD_OKAY */ +nfdresult_t NFD_SaveDialogU8(nfdu8char_t** outPath, + const nfdu8filteritem_t* filterList, + nfdfiltersize_t count, + const nfdu8char_t* defaultPath, + const nfdu8char_t* defaultName); + +/* select folder dialog */ +/* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns + * NFD_OKAY */ +nfdresult_t NFD_PickFolderU8(nfdu8char_t** outPath, const nfdu8char_t* defaultPath); -/* get last error -- set when nfdresult_t returns NFD_ERROR */ -const char *NFD_GetError( void ); -/* get the number of entries stored in pathSet */ -size_t NFD_PathSet_GetCount( const nfdpathset_t *pathSet ); /* Get the UTF-8 path at offset index */ -nfdchar_t *NFD_PathSet_GetPath( const nfdpathset_t *pathSet, size_t index ); -/* Free the pathSet */ -void NFD_PathSet_Free( nfdpathset_t *pathSet ); +/* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns + * NFD_OKAY */ +nfdresult_t NFD_PathSet_GetPathU8(const nfdpathset_t* pathSet, + nfdpathsetsize_t index, + nfdu8char_t** outPath); +/* Gets the next item from the path set enumerator. + * If there are no more items, then *outPaths will be set to NULL. */ +/* It is the caller's responsibility to free `*outPath` via NFD_PathSet_FreePathU8() if this + * function returns NFD_OKAY and `*outPath` is not null */ +nfdresult_t NFD_PathSet_EnumNextU8(nfdpathsetenum_t* enumerator, nfdu8char_t** outPath); + +#define NFD_PathSet_FreePathU8 NFD_FreePathU8 + +#ifdef NFD_NATIVE +typedef nfdnchar_t nfdchar_t; +typedef nfdnfilteritem_t nfdfilteritem_t; +#define NFD_FreePath NFD_FreePathN +#define NFD_OpenDialog NFD_OpenDialogN +#define NFD_OpenDialogMultiple NFD_OpenDialogMultipleN +#define NFD_SaveDialog NFD_SaveDialogN +#define NFD_PickFolder NFD_PickFolderN +#define NFD_PathSet_GetPath NFD_PathSet_GetPathN +#define NFD_PathSet_FreePath NFD_PathSet_FreePathN +#define NFD_PathSet_EnumNext NFD_PathSet_EnumNextN +#else +typedef nfdu8char_t nfdchar_t; +typedef nfdu8filteritem_t nfdfilteritem_t; +#define NFD_FreePath NFD_FreePathU8 +#define NFD_OpenDialog NFD_OpenDialogU8 +#define NFD_OpenDialogMultiple NFD_OpenDialogMultipleU8 +#define NFD_SaveDialog NFD_SaveDialogU8 +#define NFD_PickFolder NFD_PickFolderU8 +#define NFD_PathSet_GetPath NFD_PathSet_GetPathU8 +#define NFD_PathSet_FreePath NFD_PathSet_FreePathU8 +#define NFD_PathSet_EnumNext NFD_PathSet_EnumNextU8 +#endif // NFD_NATIVE + +#else // _WIN32 + +/* the native charset is already UTF-8 */ +typedef nfdnchar_t nfdchar_t; +typedef nfdnfilteritem_t nfdfilteritem_t; +#define NFD_FreePath NFD_FreePathN +#define NFD_OpenDialog NFD_OpenDialogN +#define NFD_OpenDialogMultiple NFD_OpenDialogMultipleN +#define NFD_SaveDialog NFD_SaveDialogN +#define NFD_PickFolder NFD_PickFolderN +#define NFD_PathSet_GetPath NFD_PathSet_GetPathN +#define NFD_PathSet_FreePath NFD_PathSet_FreePathN +#define NFD_PathSet_EnumNext NFD_PathSet_EnumNextN +typedef nfdnchar_t nfdu8char_t; +typedef nfdnfilteritem_t nfdu8filteritem_t; +#define NFD_FreePathU8 NFD_FreePathN +#define NFD_OpenDialogU8 NFD_OpenDialogN +#define NFD_OpenDialogMultipleU8 NFD_OpenDialogMultipleN +#define NFD_SaveDialogU8 NFD_SaveDialogN +#define NFD_PickFolderU8 NFD_PickFolderN +#define NFD_PathSet_GetPathU8 NFD_PathSet_GetPathN +#define NFD_PathSet_FreePathU8 NFD_PathSet_FreePathN +#define NFD_PathSet_EnumNextU8 NFD_PathSet_EnumNextN + +#endif // _WIN32 #ifdef __cplusplus } -#endif +#endif // __cplusplus -#endif +#endif // _NFD_H diff --git a/nfd/nfd_cocoa.m b/nfd/nfd_cocoa.m index 86508d52..5d74b13d 100644 --- a/nfd/nfd_cocoa.m +++ b/nfd/nfd_cocoa.m @@ -1,286 +1,321 @@ /* - Native File Dialog - - http://www.frogtoss.com/labs + Native File Dialog Extended + Repository: https://github.com/btzy/nativefiledialog-extended + License: Zlib + Authors: Bernard Teo, Michael Labbe */ #include #include "nfd.h" -#include "nfd_common.h" -static NSArray *BuildAllowedFileTypes( const char *filterList ) -{ +static const char* g_errorstr = NULL; + +static void NFDi_SetError(const char* msg) { + g_errorstr = msg; +} + +static void* NFDi_Malloc(size_t bytes) { + void* ptr = malloc(bytes); + if (!ptr) NFDi_SetError("NFDi_Malloc failed."); + + return ptr; +} + +static void NFDi_Free(void* ptr) { + assert(ptr); + free(ptr); +} + +static NSArray* BuildAllowedFileTypes(const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount) { // Commas and semicolons are the same thing on this platform - NSMutableArray *buildFilterList = [[NSMutableArray alloc] init]; + NSMutableArray* buildFilterList = [[NSMutableArray alloc] init]; - char typebuf[NFD_MAX_STRLEN] = {0}; - - size_t filterListLen = strlen(filterList); - char *p_typebuf = typebuf; - for ( size_t i = 0; i < filterListLen+1; ++i ) - { - if ( filterList[i] == ',' || filterList[i] == ';' || filterList[i] == '\0' ) - { - ++p_typebuf; - *p_typebuf = '\0'; - NSString *thisType = [NSString stringWithUTF8String: typebuf]; - [buildFilterList addObject:thisType]; - p_typebuf = typebuf; - *p_typebuf = '\0'; - } - else - { - *p_typebuf = filterList[i]; - ++p_typebuf; + for (nfdfiltersize_t filterIndex = 0; filterIndex != filterCount; ++filterIndex) { + // this is the spec to parse (we don't use the friendly name on OS X) + const nfdnchar_t* filterSpec = filterList[filterIndex].spec; + const nfdnchar_t* p_currentFilterBegin = filterSpec; + for (const nfdnchar_t* p_filterSpec = filterSpec; *p_filterSpec; ++p_filterSpec) { + if (*p_filterSpec == ',') { + // add the extension to the array + NSString* filterStr = [[[NSString alloc] + initWithBytes:(const void*)p_currentFilterBegin + length:(sizeof(nfdnchar_t) * (p_filterSpec - p_currentFilterBegin)) + encoding:NSUTF8StringEncoding] autorelease]; + [buildFilterList addObject:filterStr]; + p_currentFilterBegin = p_filterSpec + 1; + } } + // add the extension to the array + NSString* filterStr = [NSString stringWithUTF8String:p_currentFilterBegin]; + [buildFilterList addObject:filterStr]; } - NSArray *returnArray = [NSArray arrayWithArray:buildFilterList]; + NSArray* returnArray = [NSArray arrayWithArray:buildFilterList]; [buildFilterList release]; + + assert([returnArray count] != 0); + return returnArray; } -static void AddFilterListToDialog( NSSavePanel *dialog, const char *filterList ) -{ - if ( !filterList || strlen(filterList) == 0 ) - return; +static void AddFilterListToDialog(NSSavePanel* dialog, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount) { + // note: NSOpenPanel inherits from NSSavePanel. - NSArray *allowedFileTypes = BuildAllowedFileTypes( filterList ); - if ( [allowedFileTypes count] != 0 ) - { - [dialog setAllowedFileTypes:allowedFileTypes]; + if (!filterCount) return; + + assert(filterList); + + // make NSArray of file types + NSArray* allowedFileTypes = BuildAllowedFileTypes(filterList, filterCount); + + // set it on the dialog + [dialog setAllowedFileTypes:allowedFileTypes]; +} + +static void SetDefaultPath(NSSavePanel* dialog, const nfdnchar_t* defaultPath) { + if (!defaultPath || !*defaultPath) return; + + NSString* defaultPathString = [NSString stringWithUTF8String:defaultPath]; + NSURL* url = [NSURL fileURLWithPath:defaultPathString isDirectory:YES]; + [dialog setDirectoryURL:url]; +} + +static void SetDefaultName(NSSavePanel* dialog, const nfdnchar_t* defaultName) { + if (!defaultName || !*defaultName) return; + + NSString* defaultNameString = [NSString stringWithUTF8String:defaultName]; + [dialog setNameFieldStringValue:defaultNameString]; +} + +static nfdresult_t CopyUtf8String(const char* utf8Str, nfdnchar_t** out) { + // byte count, not char count + size_t len = strlen(utf8Str); + + // Too bad we have to use additional memory for all the result paths, + // because we cannot reconstitute an NSString from a char* to release it properly. + *out = (nfdnchar_t*)NFDi_Malloc(len + 1); + if (*out) { + strcpy(*out, utf8Str); + return NFD_OKAY; + } + + return NFD_ERROR; +} + +/* public */ + +const char* NFD_GetError(void) { + return g_errorstr; +} + +void NFD_FreePathN(nfdnchar_t* filePath) { + NFDi_Free((void*)filePath); +} + +static NSApplicationActivationPolicy old_app_policy; + +nfdresult_t NFD_Init(void) { + NSApplication* app = [NSApplication sharedApplication]; + old_app_policy = [app activationPolicy]; + if (old_app_policy == NSApplicationActivationPolicyProhibited) { + if (![app setActivationPolicy:NSApplicationActivationPolicyAccessory]) { + NFDi_SetError("Failed to set activation policy."); + return NFD_ERROR; + } + } + return NFD_OKAY; +} + +/* call this to de-initialize NFD, if NFD_Init returned NFD_OKAY */ +void NFD_Quit(void) { + [[NSApplication sharedApplication] setActivationPolicy:old_app_policy]; +} + +nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath) { + nfdresult_t result = NFD_CANCEL; + @autoreleasepool { + NSWindow* keyWindow = [[NSApplication sharedApplication] keyWindow]; + + NSOpenPanel* dialog = [NSOpenPanel openPanel]; + [dialog setAllowsMultipleSelection:NO]; + + // Build the filter list + AddFilterListToDialog(dialog, filterList, filterCount); + + // Set the starting directory + SetDefaultPath(dialog, defaultPath); + + if ([dialog runModal] == NSModalResponseOK) { + const NSURL* url = [dialog URL]; + const char* utf8Path = [[url path] UTF8String]; + result = CopyUtf8String(utf8Path, outPath); + } + + // return focus to the key window (i.e. main window) + [keyWindow makeKeyAndOrderFront:nil]; + } + return result; +} + +nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath) { + nfdresult_t result = NFD_CANCEL; + @autoreleasepool { + NSWindow* keyWindow = [[NSApplication sharedApplication] keyWindow]; + + NSOpenPanel* dialog = [NSOpenPanel openPanel]; + [dialog setAllowsMultipleSelection:YES]; + + // Build the filter list + AddFilterListToDialog(dialog, filterList, filterCount); + + // Set the starting directory + SetDefaultPath(dialog, defaultPath); + + if ([dialog runModal] == NSModalResponseOK) { + const NSArray* urls = [dialog URLs]; + + if ([urls count] > 0) { + // have at least one URL, we return this NSArray + [urls retain]; + *outPaths = (const nfdpathset_t*)urls; + result = NFD_OKAY; + } + } + + // return focus to the key window (i.e. main window) + [keyWindow makeKeyAndOrderFront:nil]; + } + return result; +} + +nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath, + const nfdnchar_t* defaultName) { + nfdresult_t result = NFD_CANCEL; + @autoreleasepool { + NSWindow* keyWindow = [[NSApplication sharedApplication] keyWindow]; + + NSSavePanel* dialog = [NSSavePanel savePanel]; + [dialog setExtensionHidden:NO]; + // allow other file types, to give the user an escape hatch since you can't select "*.*" on + // Mac + [dialog setAllowsOtherFileTypes:TRUE]; + + // Build the filter list + AddFilterListToDialog(dialog, filterList, filterCount); + + // Set the starting directory + SetDefaultPath(dialog, defaultPath); + + // Set the default file name + SetDefaultName(dialog, defaultName); + + if ([dialog runModal] == NSModalResponseOK) { + const NSURL* url = [dialog URL]; + const char* utf8Path = [[url path] UTF8String]; + result = CopyUtf8String(utf8Path, outPath); + } + + // return focus to the key window (i.e. main window) + [keyWindow makeKeyAndOrderFront:nil]; + } + return result; +} + +nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) { + nfdresult_t result = NFD_CANCEL; + @autoreleasepool { + NSWindow* keyWindow = [[NSApplication sharedApplication] keyWindow]; + + NSOpenPanel* dialog = [NSOpenPanel openPanel]; + [dialog setAllowsMultipleSelection:NO]; + [dialog setCanChooseDirectories:YES]; + [dialog setCanCreateDirectories:YES]; + [dialog setCanChooseFiles:NO]; + + // Set the starting directory + SetDefaultPath(dialog, defaultPath); + + if ([dialog runModal] == NSModalResponseOK) { + const NSURL* url = [dialog URL]; + const char* utf8Path = [[url path] UTF8String]; + result = CopyUtf8String(utf8Path, outPath); + } + + // return focus to the key window (i.e. main window) + [keyWindow makeKeyAndOrderFront:nil]; + } + return result; +} + +nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count) { + const NSArray* urls = (const NSArray*)pathSet; + *count = [urls count]; + return NFD_OKAY; +} + +nfdresult_t NFD_PathSet_GetPathN(const nfdpathset_t* pathSet, + nfdpathsetsize_t index, + nfdnchar_t** outPath) { + const NSArray* urls = (const NSArray*)pathSet; + + @autoreleasepool { + // autoreleasepool needed because UTF8String method might use the pool + const NSURL* url = [urls objectAtIndex:index]; + const char* utf8Path = [[url path] UTF8String]; + return CopyUtf8String(utf8Path, outPath); } } -static void SetDefaultPath( NSSavePanel *dialog, const nfdchar_t *defaultPath ) -{ - if ( !defaultPath || strlen(defaultPath) == 0 ) - return; - - NSString *defaultPathString = [NSString stringWithUTF8String: defaultPath]; - NSURL *url = [NSURL fileURLWithPath:defaultPathString isDirectory:YES]; - [dialog setDirectoryURL:url]; +void NFD_PathSet_Free(const nfdpathset_t* pathSet) { + const NSArray* urls = (const NSArray*)pathSet; + [urls release]; } +nfdresult_t NFD_PathSet_GetEnum(const nfdpathset_t* pathSet, nfdpathsetenum_t* outEnumerator) { + const NSArray* urls = (const NSArray*)pathSet; -/* fixme: pathset should be pathSet */ -static nfdresult_t AllocPathSet( NSArray *urls, nfdpathset_t *pathset ) -{ - assert(pathset); - assert([urls count]); - - pathset->count = (size_t)[urls count]; - pathset->indices = NFDi_Malloc( sizeof(size_t)*pathset->count ); - if ( !pathset->indices ) - { - return NFD_ERROR; - } - - // count the total space needed for buf - size_t bufsize = 0; - for ( NSURL *url in urls ) - { - NSString *path = [url path]; - bufsize += [path lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1; - } - - pathset->buf = NFDi_Malloc( sizeof(nfdchar_t) * bufsize ); - if ( !pathset->buf ) - { - return NFD_ERROR; - } - - // fill buf - nfdchar_t *p_buf = pathset->buf; - size_t count = 0; - for ( NSURL *url in urls ) - { - NSString *path = [url path]; - const nfdchar_t *utf8Path = [path UTF8String]; - size_t byteLen = [path lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1; - memcpy( p_buf, utf8Path, byteLen ); - - ptrdiff_t index = p_buf - pathset->buf; - assert( index >= 0 ); - pathset->indices[count] = (size_t)index; - - p_buf += byteLen; - ++count; + @autoreleasepool { + // autoreleasepool needed because NSEnumerator uses it + NSEnumerator* enumerator = [urls objectEnumerator]; + [enumerator retain]; + outEnumerator->ptr = (void*)enumerator; } return NFD_OKAY; } -/* public */ - - -nfdresult_t NFD_OpenDialog( const nfdchar_t *filterList, - const nfdchar_t *defaultPath, - nfdchar_t **outPath, - void* owner ) -{ - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - - NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow]; - NSOpenPanel *dialog = [NSOpenPanel openPanel]; - [dialog setAllowsMultipleSelection:NO]; - - // Build the filter list - AddFilterListToDialog(dialog, filterList); - - // Set the starting directory - SetDefaultPath(dialog, defaultPath); - - nfdresult_t nfdResult = NFD_CANCEL; - if ( [dialog runModal] == NSModalResponseOK ) - { - NSURL *url = [dialog URL]; - const char *utf8Path = [[url path] UTF8String]; - - // byte count, not char count - size_t len = strlen(utf8Path);//NFDi_UTF8_Strlen(utf8Path); - - *outPath = NFDi_Malloc( len+1 ); - if ( !*outPath ) - { - [pool release]; - [keyWindow makeKeyAndOrderFront:nil]; - return NFD_ERROR; - } - memcpy( *outPath, utf8Path, len+1 ); /* copy null term */ - nfdResult = NFD_OKAY; - } - [pool release]; - - [keyWindow makeKeyAndOrderFront:nil]; - return nfdResult; +void NFD_PathSet_FreeEnum(nfdpathsetenum_t* enumerator) { + NSEnumerator* real_enum = (NSEnumerator*)enumerator->ptr; + [real_enum release]; } +nfdresult_t NFD_PathSet_EnumNextN(nfdpathsetenum_t* enumerator, nfdnchar_t** outPath) { + NSEnumerator* real_enum = (NSEnumerator*)enumerator->ptr; -nfdresult_t NFD_OpenDialogMultiple( const nfdchar_t *filterList, - const nfdchar_t *defaultPath, - nfdpathset_t *outPaths ) -{ - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow]; - - NSOpenPanel *dialog = [NSOpenPanel openPanel]; - [dialog setAllowsMultipleSelection:YES]; - - // Build the fiter list. - AddFilterListToDialog(dialog, filterList); - - // Set the starting directory - SetDefaultPath(dialog, defaultPath); - - nfdresult_t nfdResult = NFD_CANCEL; - if ( [dialog runModal] == NSModalResponseOK ) - { - NSArray *urls = [dialog URLs]; - - if ( [urls count] == 0 ) - { - [pool release]; - [keyWindow makeKeyAndOrderFront:nil]; - return NFD_CANCEL; + @autoreleasepool { + // autoreleasepool needed because NSURL uses it + const NSURL* url = [real_enum nextObject]; + if (url) { + const char* utf8Path = [[url path] UTF8String]; + return CopyUtf8String(utf8Path, outPath); + } else { + *outPath = NULL; + return NFD_OKAY; } - - if ( AllocPathSet( urls, outPaths ) == NFD_ERROR ) - { - [pool release]; - [keyWindow makeKeyAndOrderFront:nil]; - return NFD_ERROR; - } - - nfdResult = NFD_OKAY; } - [pool release]; - - [keyWindow makeKeyAndOrderFront:nil]; - return nfdResult; -} - - -nfdresult_t NFD_SaveDialog( const nfdchar_t *filterList, - const nfdchar_t *defaultPath, - nfdchar_t **outPath, - void* owner ) -{ - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow]; - - NSSavePanel *dialog = [NSSavePanel savePanel]; - [dialog setExtensionHidden:NO]; - - // Build the filter list. - AddFilterListToDialog(dialog, filterList); - - // Set the starting directory - SetDefaultPath(dialog, defaultPath); - - nfdresult_t nfdResult = NFD_CANCEL; - if ( [dialog runModal] == NSModalResponseOK ) - { - NSURL *url = [dialog URL]; - const char *utf8Path = [[url path] UTF8String]; - - size_t byteLen = [url.path lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1; - - *outPath = NFDi_Malloc( byteLen ); - if ( !*outPath ) - { - [pool release]; - [keyWindow makeKeyAndOrderFront:nil]; - return NFD_ERROR; - } - memcpy( *outPath, utf8Path, byteLen ); - nfdResult = NFD_OKAY; - } - - [pool release]; - [keyWindow makeKeyAndOrderFront:nil]; - return nfdResult; -} - -nfdresult_t NFD_PickFolder(const nfdchar_t *defaultPath, - nfdchar_t **outPath) -{ - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - - NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow]; - NSOpenPanel *dialog = [NSOpenPanel openPanel]; - [dialog setAllowsMultipleSelection:NO]; - [dialog setCanChooseDirectories:YES]; - [dialog setCanCreateDirectories:YES]; - [dialog setCanChooseFiles:NO]; - - // Set the starting directory - SetDefaultPath(dialog, defaultPath); - - nfdresult_t nfdResult = NFD_CANCEL; - if ( [dialog runModal] == NSModalResponseOK ) - { - NSURL *url = [dialog URL]; - const char *utf8Path = [[url path] UTF8String]; - - // byte count, not char count - size_t len = strlen(utf8Path);//NFDi_UTF8_Strlen(utf8Path); - - *outPath = NFDi_Malloc( len+1 ); - if ( !*outPath ) - { - [pool release]; - [keyWindow makeKeyAndOrderFront:nil]; - return NFD_ERROR; - } - memcpy( *outPath, utf8Path, len+1 ); /* copy null term */ - nfdResult = NFD_OKAY; - } - [pool release]; - - [keyWindow makeKeyAndOrderFront:nil]; - return nfdResult; } diff --git a/nfd/nfd_common.c b/nfd/nfd_common.c deleted file mode 100644 index 269fbd21..00000000 --- a/nfd/nfd_common.c +++ /dev/null @@ -1,142 +0,0 @@ -/* - Native File Dialog - - http://www.frogtoss.com/labs - */ - -#include -#include -#include -#include "nfd_common.h" - -static char g_errorstr[NFD_MAX_STRLEN] = {0}; - -/* public routines */ - -const char *NFD_GetError( void ) -{ - return g_errorstr; -} - -size_t NFD_PathSet_GetCount( const nfdpathset_t *pathset ) -{ - assert(pathset); - return pathset->count; -} - -nfdchar_t *NFD_PathSet_GetPath( const nfdpathset_t *pathset, size_t num ) -{ - assert(pathset); - assert(num < pathset->count); - - return pathset->buf + pathset->indices[num]; -} - -void NFD_PathSet_Free( nfdpathset_t *pathset ) -{ - assert(pathset); - NFDi_Free( pathset->indices ); - NFDi_Free( pathset->buf ); -} - -/* internal routines */ - -void *NFDi_Malloc( size_t bytes ) -{ - void *ptr = malloc(bytes); - if ( !ptr ) - NFDi_SetError("NFDi_Malloc failed."); - - return ptr; -} - -void NFDi_Free( void *ptr ) -{ - assert(ptr); - free(ptr); -} - -void NFDi_SetError( const char *msg ) -{ - int bTruncate = NFDi_SafeStrncpy( g_errorstr, msg, NFD_MAX_STRLEN ); - assert( !bTruncate ); _NFD_UNUSED(bTruncate); -} - - -int NFDi_SafeStrncpy( char *dst, const char *src, size_t maxCopy ) -{ - size_t n = maxCopy; - char *d = dst; - - assert( src ); - assert( dst ); - - while ( n > 0 && *src != '\0' ) - { - *d++ = *src++; - --n; - } - - /* Truncation case - - terminate string and return true */ - if ( n == 0 ) - { - dst[maxCopy-1] = '\0'; - return 1; - } - - /* No truncation. Append a single NULL and return. */ - *d = '\0'; - return 0; -} - - -/* adapted from microutf8 */ -size_t NFDi_UTF8_Strlen( const nfdchar_t *str ) -{ - /* This function doesn't properly check validity of UTF-8 character - sequence, it is supposed to use only with valid UTF-8 strings. */ - - size_t character_count = 0; - size_t i = 0; /* Counter used to iterate over string. */ - nfdchar_t maybe_bom[4]; - - /* If there is UTF-8 BOM ignore it. */ - if (strlen(str) > 2) - { - strncpy(maybe_bom, str, 3); - maybe_bom[3] = 0; - if (strcmp(maybe_bom, (nfdchar_t*)NFD_UTF8_BOM) == 0) - i += 3; - } - - while(str[i]) - { - if (str[i] >> 7 == 0) - { - /* If bit pattern begins with 0 we have ascii character. */ - ++character_count; - } - else if (str[i] >> 6 == 3) - { - /* If bit pattern begins with 11 it is beginning of UTF-8 byte sequence. */ - ++character_count; - } - else if (str[i] >> 6 == 2) - ; /* If bit pattern begins with 10 it is middle of utf-8 byte sequence. */ - else - { - /* In any other case this is not valid UTF-8. */ - return -1; - } - ++i; - } - - return character_count; -} - -int NFDi_IsFilterSegmentChar( char ch ) -{ - return (ch==','||ch==';'||ch=='\0'); -} - diff --git a/nfd/nfd_common.h b/nfd/nfd_common.h deleted file mode 100644 index a3f6b4ad..00000000 --- a/nfd/nfd_common.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - Native File Dialog - - Internal, common across platforms - - http://www.frogtoss.com/labs - */ - - -#ifndef _NFD_COMMON_H -#define _NFD_COMMON_H - -#include "nfd.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define NFD_MAX_STRLEN 256 -#define _NFD_UNUSED(x) ((void)x) - -#define NFD_UTF8_BOM "\xEF\xBB\xBF" - - -void *NFDi_Malloc( size_t bytes ); -void NFDi_Free( void *ptr ); -void NFDi_SetError( const char *msg ); -int NFDi_SafeStrncpy( char *dst, const char *src, size_t maxCopy ); -size_t NFDi_UTF8_Strlen( const nfdchar_t *str ); -int NFDi_IsFilterSegmentChar( char ch ); - -#ifdef __cplusplus -} -#endif - - -#endif diff --git a/nfd/nfd_gtk.c b/nfd/nfd_gtk.c deleted file mode 100644 index d0e79d08..00000000 --- a/nfd/nfd_gtk.c +++ /dev/null @@ -1,381 +0,0 @@ -/* - Native File Dialog - - http://www.frogtoss.com/labs -*/ - -#include -#include -#include -#include -#include "nfd.h" -#include "nfd_common.h" - - -const char INIT_FAIL_MSG[] = "gtk_init_check failed to initilaize GTK+"; - - -static void AddTypeToFilterName( const char *typebuf, char *filterName, size_t bufsize ) -{ - const char SEP[] = ", "; - - size_t len = strlen(filterName); - if ( len != 0 ) - { - strncat( filterName, SEP, bufsize - len - 1 ); - len += strlen(SEP); - } - - strncat( filterName, typebuf, bufsize - len - 1 ); -} - -static void AddFiltersToDialog( GtkWidget *dialog, const char *filterList ) -{ - GtkFileFilter *filter; - char typebuf[NFD_MAX_STRLEN] = {0}; - const char *p_filterList = filterList; - char *p_typebuf = typebuf; - char filterName[NFD_MAX_STRLEN] = {0}; - - if ( !filterList || strlen(filterList) == 0 ) - return; - - filter = gtk_file_filter_new(); - while ( 1 ) - { - - if ( NFDi_IsFilterSegmentChar(*p_filterList) ) - { - char typebufWildcard[NFD_MAX_STRLEN]; - /* add another type to the filter */ - assert( strlen(typebuf) > 0 ); - assert( strlen(typebuf) < NFD_MAX_STRLEN-1 ); - - snprintf( typebufWildcard, NFD_MAX_STRLEN, "*.%s", typebuf ); - AddTypeToFilterName( typebuf, filterName, NFD_MAX_STRLEN ); - - gtk_file_filter_add_pattern( filter, typebufWildcard ); - - p_typebuf = typebuf; - memset( typebuf, 0, sizeof(char) * NFD_MAX_STRLEN ); - } - - if ( *p_filterList == ';' || *p_filterList == '\0' ) - { - /* end of filter -- add it to the dialog */ - - gtk_file_filter_set_name( filter, filterName ); - gtk_file_chooser_add_filter( GTK_FILE_CHOOSER(dialog), filter ); - - filterName[0] = '\0'; - - if ( *p_filterList == '\0' ) - break; - - filter = gtk_file_filter_new(); - } - - if ( !NFDi_IsFilterSegmentChar( *p_filterList ) ) - { - *p_typebuf = *p_filterList; - p_typebuf++; - } - - p_filterList++; - } - - /* always append a wildcard option to the end*/ - - filter = gtk_file_filter_new(); - gtk_file_filter_set_name( filter, "*.*" ); - gtk_file_filter_add_pattern( filter, "*" ); - gtk_file_chooser_add_filter( GTK_FILE_CHOOSER(dialog), filter ); -} - -static void SetDefaultPath( GtkWidget *dialog, const char *defaultPath ) -{ - if ( !defaultPath || strlen(defaultPath) == 0 ) - return; - - /* GTK+ manual recommends not specifically setting the default path. - We do it anyway in order to be consistent across platforms. - - If consistency with the native OS is preferred, this is the line - to comment out. -ml */ - gtk_file_chooser_set_current_folder( GTK_FILE_CHOOSER(dialog), defaultPath ); -} - -static nfdresult_t AllocPathSet( GSList *fileList, nfdpathset_t *pathSet ) -{ - size_t bufSize = 0; - GSList *node; - nfdchar_t *p_buf; - size_t count = 0; - - assert(fileList); - assert(pathSet); - - pathSet->count = (size_t)g_slist_length( fileList ); - assert( pathSet->count > 0 ); - - pathSet->indices = NFDi_Malloc( sizeof(size_t)*pathSet->count ); - if ( !pathSet->indices ) - { - return NFD_ERROR; - } - - /* count the total space needed for buf */ - for ( node = fileList; node; node = node->next ) - { - assert(node->data); - bufSize += strlen( (const gchar*)node->data ) + 1; - } - - pathSet->buf = NFDi_Malloc( sizeof(nfdchar_t) * bufSize ); - - /* fill buf */ - p_buf = pathSet->buf; - for ( node = fileList; node; node = node->next ) - { - nfdchar_t *path = (nfdchar_t*)(node->data); - size_t byteLen = strlen(path)+1; - ptrdiff_t index; - - memcpy( p_buf, path, byteLen ); - g_free(node->data); - - index = p_buf - pathSet->buf; - assert( index >= 0 ); - pathSet->indices[count] = (size_t)index; - - p_buf += byteLen; - ++count; - } - - g_slist_free( fileList ); - - return NFD_OKAY; -} - -static void WaitForCleanup(void) -{ - while (gtk_events_pending()) - gtk_main_iteration(); -} - -/* public */ - -nfdresult_t NFD_OpenDialog( const nfdchar_t *filterList, - const nfdchar_t *defaultPath, - nfdchar_t **outPath, - void* owner ) -{ - GtkWidget *dialog; - nfdresult_t result; - - if ( !gtk_init_check( NULL, NULL ) ) - { - NFDi_SetError(INIT_FAIL_MSG); - return NFD_ERROR; - } - - dialog = gtk_file_chooser_dialog_new( "Open File", - NULL, - GTK_FILE_CHOOSER_ACTION_OPEN, - "_Cancel", GTK_RESPONSE_CANCEL, - "_Open", GTK_RESPONSE_ACCEPT, - NULL ); - - /* Build the filter list */ - AddFiltersToDialog(dialog, filterList); - - /* Set the default path */ - SetDefaultPath(dialog, defaultPath); - - result = NFD_CANCEL; - if ( gtk_dialog_run( GTK_DIALOG(dialog) ) == GTK_RESPONSE_ACCEPT ) - { - char *filename; - - filename = gtk_file_chooser_get_filename( GTK_FILE_CHOOSER(dialog) ); - - { - size_t len = strlen(filename); - *outPath = NFDi_Malloc( len + 1 ); - memcpy( *outPath, filename, len + 1 ); - if ( !*outPath ) - { - g_free( filename ); - gtk_widget_destroy(dialog); - return NFD_ERROR; - } - } - g_free( filename ); - - result = NFD_OKAY; - } - - WaitForCleanup(); - gtk_widget_destroy(dialog); - WaitForCleanup(); - - return result; -} - - -nfdresult_t NFD_OpenDialogMultiple( const nfdchar_t *filterList, - const nfdchar_t *defaultPath, - nfdpathset_t *outPaths ) -{ - GtkWidget *dialog; - nfdresult_t result; - - if ( !gtk_init_check( NULL, NULL ) ) - { - NFDi_SetError(INIT_FAIL_MSG); - return NFD_ERROR; - } - - dialog = gtk_file_chooser_dialog_new( "Open Files", - NULL, - GTK_FILE_CHOOSER_ACTION_OPEN, - "_Cancel", GTK_RESPONSE_CANCEL, - "_Open", GTK_RESPONSE_ACCEPT, - NULL ); - gtk_file_chooser_set_select_multiple( GTK_FILE_CHOOSER(dialog), TRUE ); - - /* Build the filter list */ - AddFiltersToDialog(dialog, filterList); - - /* Set the default path */ - SetDefaultPath(dialog, defaultPath); - - result = NFD_CANCEL; - if ( gtk_dialog_run( GTK_DIALOG(dialog) ) == GTK_RESPONSE_ACCEPT ) - { - GSList *fileList = gtk_file_chooser_get_filenames( GTK_FILE_CHOOSER(dialog) ); - if ( AllocPathSet( fileList, outPaths ) == NFD_ERROR ) - { - gtk_widget_destroy(dialog); - return NFD_ERROR; - } - - result = NFD_OKAY; - } - - WaitForCleanup(); - gtk_widget_destroy(dialog); - WaitForCleanup(); - - return result; -} - -nfdresult_t NFD_SaveDialog( const nfdchar_t *filterList, - const nfdchar_t *defaultPath, - nfdchar_t **outPath, - void* owner ) -{ - GtkWidget *dialog; - nfdresult_t result; - - if ( !gtk_init_check( NULL, NULL ) ) - { - NFDi_SetError(INIT_FAIL_MSG); - return NFD_ERROR; - } - - dialog = gtk_file_chooser_dialog_new( "Save File", - NULL, - GTK_FILE_CHOOSER_ACTION_SAVE, - "_Cancel", GTK_RESPONSE_CANCEL, - "_Save", GTK_RESPONSE_ACCEPT, - NULL ); - gtk_file_chooser_set_do_overwrite_confirmation( GTK_FILE_CHOOSER(dialog), TRUE ); - - /* Build the filter list */ - AddFiltersToDialog(dialog, filterList); - - /* Set the default path */ - SetDefaultPath(dialog, defaultPath); - - result = NFD_CANCEL; - if ( gtk_dialog_run( GTK_DIALOG(dialog) ) == GTK_RESPONSE_ACCEPT ) - { - char *filename; - filename = gtk_file_chooser_get_filename( GTK_FILE_CHOOSER(dialog) ); - - { - size_t len = strlen(filename); - *outPath = NFDi_Malloc( len + 1 ); - memcpy( *outPath, filename, len + 1 ); - if ( !*outPath ) - { - g_free( filename ); - gtk_widget_destroy(dialog); - return NFD_ERROR; - } - } - g_free(filename); - - result = NFD_OKAY; - } - - WaitForCleanup(); - gtk_widget_destroy(dialog); - WaitForCleanup(); - - return result; -} - -nfdresult_t NFD_PickFolder(const nfdchar_t *defaultPath, - nfdchar_t **outPath) -{ - GtkWidget *dialog; - nfdresult_t result; - - if (!gtk_init_check(NULL, NULL)) - { - NFDi_SetError(INIT_FAIL_MSG); - return NFD_ERROR; - } - - dialog = gtk_file_chooser_dialog_new( "Select folder", - NULL, - GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, - "_Cancel", GTK_RESPONSE_CANCEL, - "_Select", GTK_RESPONSE_ACCEPT, - NULL ); - gtk_file_chooser_set_do_overwrite_confirmation( GTK_FILE_CHOOSER(dialog), TRUE ); - - - /* Set the default path */ - SetDefaultPath(dialog, defaultPath); - - result = NFD_CANCEL; - if ( gtk_dialog_run( GTK_DIALOG(dialog) ) == GTK_RESPONSE_ACCEPT ) - { - char *filename; - filename = gtk_file_chooser_get_filename( GTK_FILE_CHOOSER(dialog) ); - - { - size_t len = strlen(filename); - *outPath = NFDi_Malloc( len + 1 ); - memcpy( *outPath, filename, len + 1 ); - if ( !*outPath ) - { - g_free( filename ); - gtk_widget_destroy(dialog); - return NFD_ERROR; - } - } - g_free(filename); - - result = NFD_OKAY; - } - - WaitForCleanup(); - gtk_widget_destroy(dialog); - WaitForCleanup(); - - return result; -} diff --git a/nfd/nfd_gtk.cpp b/nfd/nfd_gtk.cpp new file mode 100644 index 00000000..dda56a88 --- /dev/null +++ b/nfd/nfd_gtk.cpp @@ -0,0 +1,631 @@ +/* + Native File Dialog Extended + Repository: https://github.com/btzy/nativefiledialog-extended + License: Zlib + Authors: Bernard Teo, Michael Labbe + + Note: We do not check for malloc failure on Linux - Linux overcommits memory! +*/ + +#include +#include +#if defined(GDK_WINDOWING_X11) +#include +#endif +#include +#include +#include +#include + +#include "nfd.h" + +namespace { + +template +struct Free_Guard { + T* data; + Free_Guard(T* freeable) noexcept : data(freeable) {} + ~Free_Guard() { NFDi_Free(data); } +}; + +template +struct FreeCheck_Guard { + T* data; + FreeCheck_Guard(T* freeable = nullptr) noexcept : data(freeable) {} + ~FreeCheck_Guard() { + if (data) NFDi_Free(data); + } +}; + +/* current error */ +const char* g_errorstr = nullptr; + +void NFDi_SetError(const char* msg) { + g_errorstr = msg; +} + +template +T* NFDi_Malloc(size_t bytes) { + void* ptr = malloc(bytes); + if (!ptr) NFDi_SetError("NFDi_Malloc failed."); + + return static_cast(ptr); +} + +template +void NFDi_Free(T* ptr) { + assert(ptr); + free(static_cast(ptr)); +} + +template +T* copy(const T* begin, const T* end, T* out) { + for (; begin != end; ++begin) { + *out++ = *begin; + } + return out; +} + +// Does not own the filter and extension. +struct Pair_GtkFileFilter_FileExtension { + GtkFileFilter* filter; + const nfdnchar_t* extensionBegin; + const nfdnchar_t* extensionEnd; +}; + +struct ButtonClickedArgs { + Pair_GtkFileFilter_FileExtension* map; + GtkFileChooser* chooser; +}; + +void AddFiltersToDialog(GtkFileChooser* chooser, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount) { + if (filterCount) { + assert(filterList); + + // we have filters to add ... format and add them + + for (nfdfiltersize_t index = 0; index != filterCount; ++index) { + GtkFileFilter* filter = gtk_file_filter_new(); + + // count number of file extensions + size_t sep = 1; + for (const nfdnchar_t* p_spec = filterList[index].spec; *p_spec; ++p_spec) { + if (*p_spec == L',') { + ++sep; + } + } + + // friendly name conversions: "png,jpg" -> "Image files + // (png, jpg)" + + // calculate space needed (including the trailing '\0') + size_t nameSize = + sep + strlen(filterList[index].spec) + 3 + strlen(filterList[index].name); + + // malloc the required memory + nfdnchar_t* nameBuf = NFDi_Malloc(sizeof(nfdnchar_t) * nameSize); + + nfdnchar_t* p_nameBuf = nameBuf; + for (const nfdnchar_t* p_filterName = filterList[index].name; *p_filterName; + ++p_filterName) { + *p_nameBuf++ = *p_filterName; + } + *p_nameBuf++ = ' '; + *p_nameBuf++ = '('; + const nfdnchar_t* p_extensionStart = filterList[index].spec; + for (const nfdnchar_t* p_spec = filterList[index].spec; true; ++p_spec) { + if (*p_spec == ',' || !*p_spec) { + if (*p_spec == ',') { + *p_nameBuf++ = ','; + *p_nameBuf++ = ' '; + } + + // +1 for the trailing '\0' + nfdnchar_t* extnBuf = NFDi_Malloc(sizeof(nfdnchar_t) * + (p_spec - p_extensionStart + 3)); + nfdnchar_t* p_extnBufEnd = extnBuf; + *p_extnBufEnd++ = '*'; + *p_extnBufEnd++ = '.'; + p_extnBufEnd = copy(p_extensionStart, p_spec, p_extnBufEnd); + *p_extnBufEnd++ = '\0'; + assert((size_t)(p_extnBufEnd - extnBuf) == + sizeof(nfdnchar_t) * (p_spec - p_extensionStart + 3)); + gtk_file_filter_add_pattern(filter, extnBuf); + NFDi_Free(extnBuf); + + if (*p_spec) { + // update the extension start point + p_extensionStart = p_spec + 1; + } else { + // reached the '\0' character + break; + } + } else { + *p_nameBuf++ = *p_spec; + } + } + *p_nameBuf++ = ')'; + *p_nameBuf++ = '\0'; + assert((size_t)(p_nameBuf - nameBuf) == sizeof(nfdnchar_t) * nameSize); + + // add to the filter + gtk_file_filter_set_name(filter, nameBuf); + + // free the memory + NFDi_Free(nameBuf); + + // add filter to chooser + gtk_file_chooser_add_filter(chooser, filter); + } + } + + /* always append a wildcard option to the end*/ + + GtkFileFilter* filter = gtk_file_filter_new(); + gtk_file_filter_set_name(filter, "All files"); + gtk_file_filter_add_pattern(filter, "*"); + gtk_file_chooser_add_filter(chooser, filter); +} + +// returns null-terminated map (trailing .filter is null) +Pair_GtkFileFilter_FileExtension* AddFiltersToDialogWithMap(GtkFileChooser* chooser, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount) { + Pair_GtkFileFilter_FileExtension* map = NFDi_Malloc( + sizeof(Pair_GtkFileFilter_FileExtension) * (filterCount + 1)); + + if (filterCount) { + assert(filterList); + + // we have filters to add ... format and add them + + for (nfdfiltersize_t index = 0; index != filterCount; ++index) { + GtkFileFilter* filter = gtk_file_filter_new(); + + // store filter in map + map[index].filter = filter; + map[index].extensionBegin = filterList[index].spec; + map[index].extensionEnd = nullptr; + + // count number of file extensions + size_t sep = 1; + for (const nfdnchar_t* p_spec = filterList[index].spec; *p_spec; ++p_spec) { + if (*p_spec == L',') { + ++sep; + } + } + + // friendly name conversions: "png,jpg" -> "Image files + // (png, jpg)" + + // calculate space needed (including the trailing '\0') + size_t nameSize = + sep + strlen(filterList[index].spec) + 3 + strlen(filterList[index].name); + + // malloc the required memory + nfdnchar_t* nameBuf = NFDi_Malloc(sizeof(nfdnchar_t) * nameSize); + + nfdnchar_t* p_nameBuf = nameBuf; + for (const nfdnchar_t* p_filterName = filterList[index].name; *p_filterName; + ++p_filterName) { + *p_nameBuf++ = *p_filterName; + } + *p_nameBuf++ = ' '; + *p_nameBuf++ = '('; + const nfdnchar_t* p_extensionStart = filterList[index].spec; + for (const nfdnchar_t* p_spec = filterList[index].spec; true; ++p_spec) { + if (*p_spec == ',' || !*p_spec) { + if (*p_spec == ',') { + *p_nameBuf++ = ','; + *p_nameBuf++ = ' '; + } + + // +1 for the trailing '\0' + nfdnchar_t* extnBuf = NFDi_Malloc(sizeof(nfdnchar_t) * + (p_spec - p_extensionStart + 3)); + nfdnchar_t* p_extnBufEnd = extnBuf; + *p_extnBufEnd++ = '*'; + *p_extnBufEnd++ = '.'; + p_extnBufEnd = copy(p_extensionStart, p_spec, p_extnBufEnd); + *p_extnBufEnd++ = '\0'; + assert((size_t)(p_extnBufEnd - extnBuf) == + sizeof(nfdnchar_t) * (p_spec - p_extensionStart + 3)); + gtk_file_filter_add_pattern(filter, extnBuf); + NFDi_Free(extnBuf); + + // store current pointer in map (if it's + // the first one) + if (map[index].extensionEnd == nullptr) { + map[index].extensionEnd = p_spec; + } + + if (*p_spec) { + // update the extension start point + p_extensionStart = p_spec + 1; + } else { + // reached the '\0' character + break; + } + } else { + *p_nameBuf++ = *p_spec; + } + } + *p_nameBuf++ = ')'; + *p_nameBuf++ = '\0'; + assert((size_t)(p_nameBuf - nameBuf) == sizeof(nfdnchar_t) * nameSize); + + // add to the filter + gtk_file_filter_set_name(filter, nameBuf); + + // free the memory + NFDi_Free(nameBuf); + + // add filter to chooser + gtk_file_chooser_add_filter(chooser, filter); + } + } + // set trailing map index to null + map[filterCount].filter = nullptr; + + /* always append a wildcard option to the end*/ + GtkFileFilter* filter = gtk_file_filter_new(); + gtk_file_filter_set_name(filter, "All files"); + gtk_file_filter_add_pattern(filter, "*"); + gtk_file_chooser_add_filter(chooser, filter); + + return map; +} + +void SetDefaultPath(GtkFileChooser* chooser, const char* defaultPath) { + if (!defaultPath || !*defaultPath) return; + + /* GTK+ manual recommends not specifically setting the default path. + We do it anyway in order to be consistent across platforms. + + If consistency with the native OS is preferred, this is the line + to comment out. -ml */ + gtk_file_chooser_set_current_folder(chooser, defaultPath); +} + +void SetDefaultName(GtkFileChooser* chooser, const char* defaultName) { + if (!defaultName || !*defaultName) return; + + gtk_file_chooser_set_current_name(chooser, defaultName); +} + +void WaitForCleanup() { + while (gtk_events_pending()) gtk_main_iteration(); +} + +struct Widget_Guard { + GtkWidget* data; + Widget_Guard(GtkWidget* widget) : data(widget) {} + ~Widget_Guard() { + WaitForCleanup(); + gtk_widget_destroy(data); + WaitForCleanup(); + } +}; + +void FileActivatedSignalHandler(GtkButton* saveButton, void* userdata) { + (void)saveButton; // silence the unused arg warning + + ButtonClickedArgs* args = static_cast(userdata); + GtkFileChooser* chooser = args->chooser; + char* currentFileName = gtk_file_chooser_get_current_name(chooser); + if (*currentFileName) { // string is not empty + + // find a '.' in the file name + const char* p_period = currentFileName; + for (; *p_period; ++p_period) { + if (*p_period == '.') { + break; + } + } + + if (!*p_period) { // there is no '.', so append the default extension + Pair_GtkFileFilter_FileExtension* filterMap = + static_cast(args->map); + GtkFileFilter* currentFilter = gtk_file_chooser_get_filter(chooser); + if (currentFilter) { + for (; filterMap->filter; ++filterMap) { + if (filterMap->filter == currentFilter) break; + } + } + if (filterMap->filter) { + // memory for appended string (including '.' and + // trailing '\0') + char* appendedFileName = NFDi_Malloc( + sizeof(char) * ((p_period - currentFileName) + + (filterMap->extensionEnd - filterMap->extensionBegin) + 2)); + char* p_fileName = copy(currentFileName, p_period, appendedFileName); + *p_fileName++ = '.'; + p_fileName = copy(filterMap->extensionBegin, filterMap->extensionEnd, p_fileName); + *p_fileName++ = '\0'; + + assert(p_fileName - appendedFileName == + (p_period - currentFileName) + + (filterMap->extensionEnd - filterMap->extensionBegin) + 2); + + // set the appended file name + gtk_file_chooser_set_current_name(chooser, appendedFileName); + + // free the memory + NFDi_Free(appendedFileName); + } + } + } + + // free the memory + g_free(currentFileName); +} + +// wrapper for gtk_dialog_run() that brings the dialog to the front +// see issues at: +// https://github.com/btzy/nativefiledialog-extended/issues/31 +// https://github.com/mlabbe/nativefiledialog/pull/92 +// https://github.com/guillaumechereau/noc/pull/11 +gint RunDialogWithFocus(GtkDialog* dialog) { +#if defined(GDK_WINDOWING_X11) + gtk_widget_show_all(GTK_WIDGET(dialog)); // show the dialog so that it gets a display + if (GDK_IS_X11_DISPLAY(gtk_widget_get_display(GTK_WIDGET(dialog)))) { + GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(dialog)); + gdk_window_set_events( + window, + static_cast(gdk_window_get_events(window) | GDK_PROPERTY_CHANGE_MASK)); + gtk_window_present_with_time(GTK_WINDOW(dialog), gdk_x11_get_server_time(window)); + } +#endif + return gtk_dialog_run(dialog); +} + +} // namespace + +const char* NFD_GetError(void) { + return g_errorstr; +} + +void NFD_ClearError(void) { + NFDi_SetError(nullptr); +} + +/* public */ + +nfdresult_t NFD_Init(void) { + // Init GTK + if (!gtk_init_check(NULL, NULL)) { + NFDi_SetError("Failed to initialize GTK+ with gtk_init_check."); + return NFD_ERROR; + } + return NFD_OKAY; +} +void NFD_Quit(void) { + // do nothing, GTK cannot be de-initialized +} + +void NFD_FreePathN(nfdnchar_t* filePath) { + assert(filePath); + g_free(filePath); +} + +nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath) { + GtkWidget* widget = gtk_file_chooser_dialog_new("Open File", + nullptr, + GTK_FILE_CHOOSER_ACTION_OPEN, + "_Cancel", + GTK_RESPONSE_CANCEL, + "_Open", + GTK_RESPONSE_ACCEPT, + nullptr); + + // guard to destroy the widget when returning from this function + Widget_Guard widgetGuard(widget); + + /* Build the filter list */ + AddFiltersToDialog(GTK_FILE_CHOOSER(widget), filterList, filterCount); + + /* Set the default path */ + SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath); + + if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) { + // write out the file name + *outPath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget)); + + return NFD_OKAY; + } else { + return NFD_CANCEL; + } +} + +nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath) { + GtkWidget* widget = gtk_file_chooser_dialog_new("Open Files", + nullptr, + GTK_FILE_CHOOSER_ACTION_OPEN, + "_Cancel", + GTK_RESPONSE_CANCEL, + "_Open", + GTK_RESPONSE_ACCEPT, + nullptr); + + // guard to destroy the widget when returning from this function + Widget_Guard widgetGuard(widget); + + // set select multiple + gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(widget), TRUE); + + /* Build the filter list */ + AddFiltersToDialog(GTK_FILE_CHOOSER(widget), filterList, filterCount); + + /* Set the default path */ + SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath); + + if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) { + // write out the file name + GSList* fileList = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(widget)); + + *outPaths = static_cast(fileList); + return NFD_OKAY; + } else { + return NFD_CANCEL; + } +} + +nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath, + const nfdnchar_t* defaultName) { + GtkWidget* widget = gtk_file_chooser_dialog_new("Save File", + nullptr, + GTK_FILE_CHOOSER_ACTION_SAVE, + "_Cancel", + GTK_RESPONSE_CANCEL, + nullptr); + + // guard to destroy the widget when returning from this function + Widget_Guard widgetGuard(widget); + + GtkWidget* saveButton = gtk_dialog_add_button(GTK_DIALOG(widget), "_Save", GTK_RESPONSE_ACCEPT); + + // Prompt on overwrite + gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(widget), TRUE); + + /* Build the filter list */ + ButtonClickedArgs buttonClickedArgs; + buttonClickedArgs.chooser = GTK_FILE_CHOOSER(widget); + buttonClickedArgs.map = + AddFiltersToDialogWithMap(GTK_FILE_CHOOSER(widget), filterList, filterCount); + + /* Set the default path */ + SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath); + + /* Set the default file name */ + SetDefaultName(GTK_FILE_CHOOSER(widget), defaultName); + + /* set the handler to add file extension */ + gulong handlerID = g_signal_connect(G_OBJECT(saveButton), + "pressed", + G_CALLBACK(FileActivatedSignalHandler), + static_cast(&buttonClickedArgs)); + + /* invoke the dialog (blocks until dialog is closed) */ + gint result = RunDialogWithFocus(GTK_DIALOG(widget)); + /* unset the handler */ + g_signal_handler_disconnect(G_OBJECT(saveButton), handlerID); + + /* free the filter map */ + NFDi_Free(buttonClickedArgs.map); + + if (result == GTK_RESPONSE_ACCEPT) { + // write out the file name + *outPath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget)); + + return NFD_OKAY; + } else { + return NFD_CANCEL; + } +} + +nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) { + GtkWidget* widget = gtk_file_chooser_dialog_new("Select folder", + nullptr, + GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, + "_Cancel", + GTK_RESPONSE_CANCEL, + "_Select", + GTK_RESPONSE_ACCEPT, + nullptr); + + // guard to destroy the widget when returning from this function + Widget_Guard widgetGuard(widget); + + /* Set the default path */ + SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath); + + if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) { + // write out the file name + *outPath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget)); + + return NFD_OKAY; + } else { + return NFD_CANCEL; + } +} + +nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count) { + assert(pathSet); + // const_cast because methods on GSList aren't const, but it should act + // like const to the caller + GSList* fileList = const_cast(static_cast(pathSet)); + + *count = g_slist_length(fileList); + return NFD_OKAY; +} + +nfdresult_t NFD_PathSet_GetPathN(const nfdpathset_t* pathSet, + nfdpathsetsize_t index, + nfdnchar_t** outPath) { + assert(pathSet); + // const_cast because methods on GSList aren't const, but it should act + // like const to the caller + GSList* fileList = const_cast(static_cast(pathSet)); + + // Note: this takes linear time... but should be good enough + *outPath = static_cast(g_slist_nth_data(fileList, index)); + + return NFD_OKAY; +} + +void NFD_PathSet_FreePathN(const nfdnchar_t* filePath) { + assert(filePath); + (void)filePath; // prevent warning in release build + // no-op, because NFD_PathSet_Free does the freeing for us +} + +void NFD_PathSet_Free(const nfdpathset_t* pathSet) { + assert(pathSet); + // const_cast because methods on GSList aren't const, but it should act + // like const to the caller + GSList* fileList = const_cast(static_cast(pathSet)); + + // free all the nodes + for (GSList* node = fileList; node; node = node->next) { + assert(node->data); + g_free(node->data); + } + + // free the path set memory + g_slist_free(fileList); +} + +nfdresult_t NFD_PathSet_GetEnum(const nfdpathset_t* pathSet, nfdpathsetenum_t* outEnumerator) { + // The pathset (GSList) is already a linked list, so the enumeration is itself + outEnumerator->ptr = const_cast(pathSet); + + return NFD_OKAY; +} + +void NFD_PathSet_FreeEnum(nfdpathsetenum_t*) { + // Do nothing, because the enumeration is the pathset itself +} + +nfdresult_t NFD_PathSet_EnumNextN(nfdpathsetenum_t* enumerator, nfdnchar_t** outPath) { + const GSList* fileList = static_cast(enumerator->ptr); + + if (fileList) { + *outPath = static_cast(fileList->data); + enumerator->ptr = static_cast(fileList->next); + } else { + *outPath = nullptr; + } + + return NFD_OKAY; +} diff --git a/nfd/nfd_portal.cpp b/nfd/nfd_portal.cpp new file mode 100644 index 00000000..f5a9302f --- /dev/null +++ b/nfd/nfd_portal.cpp @@ -0,0 +1,1414 @@ +/* + Native File Dialog Extended + Repository: https://github.com/btzy/nativefiledialog-extended + License: Zlib + Authors: Bernard Teo + + Note: We do not check for malloc failure on Linux - Linux overcommits memory! +*/ + +#include +#include +#include +#include +#include +#include +#include +#include // for the random token string +#include // for access() + +#include "nfd.h" + +/* +Define NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION to 0 if you don't want the file extension to be +appended when missing. Linux programs usually doesn't append the file extension, but for consistency +with other OSes we append it by default. +*/ +#ifndef NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION +#define NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION 1 +#endif + +namespace { + +template +T* NFDi_Malloc(size_t bytes) { + void* ptr = malloc(bytes); + assert(ptr); // Linux malloc never fails + + return static_cast(ptr); +} + +template +void NFDi_Free(T* ptr) { + assert(ptr); + free(static_cast(ptr)); +} + +template +struct Free_Guard { + T* data; + Free_Guard(T* freeable) noexcept : data(freeable) {} + ~Free_Guard() { NFDi_Free(data); } +}; + +template +struct FreeCheck_Guard { + T* data; + FreeCheck_Guard(T* freeable = nullptr) noexcept : data(freeable) {} + ~FreeCheck_Guard() { + if (data) NFDi_Free(data); + } +}; + +struct DBusMessage_Guard { + DBusMessage* data; + DBusMessage_Guard(DBusMessage* freeable) noexcept : data(freeable) {} + ~DBusMessage_Guard() { dbus_message_unref(data); } +}; + +/* D-Bus connection handle */ +DBusConnection* dbus_conn; +/* current D-Bus error */ +DBusError dbus_err; +/* current error (may be a pointer to the D-Bus error message above, or a pointer to some string + * literal) */ +const char* err_ptr = nullptr; +/* the unique name of our connection, used for the Request handle; owned by D-Bus so we don't free + * it */ +const char* dbus_unique_name; + +void NFDi_SetError(const char* msg) { + err_ptr = msg; +} + +template +T* copy(const T* begin, const T* end, T* out) { + for (; begin != end; ++begin) { + *out++ = *begin; + } + return out; +} + +template +T* transform(const T* begin, const T* end, T* out, Callback callback) { + for (; begin != end; ++begin) { + *out++ = callback(*begin); + } + return out; +} + +constexpr const char* STR_EMPTY = ""; +constexpr const char* STR_OPEN_FILE = "Open File"; +constexpr const char* STR_OPEN_FILES = "Open Files"; +constexpr const char* STR_SAVE_FILE = "Save File"; +constexpr const char* STR_SELECT_FOLDER = "Select Folder"; +constexpr const char* STR_HANDLE_TOKEN = "handle_token"; +constexpr const char* STR_MULTIPLE = "multiple"; +constexpr const char* STR_DIRECTORY = "directory"; +constexpr const char* STR_FILTERS = "filters"; +constexpr const char* STR_CURRENT_FILTER = "current_filter"; +constexpr const char* STR_CURRENT_NAME = "current_name"; +constexpr const char* STR_CURRENT_FOLDER = "current_folder"; +constexpr const char* STR_CURRENT_FILE = "current_file"; +constexpr const char* STR_ALL_FILES = "All files"; +constexpr const char* STR_ASTERISK = "*"; + +template +void AppendOpenFileQueryTitle(DBusMessageIter&); +template <> +void AppendOpenFileQueryTitle(DBusMessageIter& iter) { + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_OPEN_FILE); +} +template <> +void AppendOpenFileQueryTitle(DBusMessageIter& iter) { + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_OPEN_FILES); +} +template <> +void AppendOpenFileQueryTitle(DBusMessageIter& iter) { + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_SELECT_FOLDER); +} + +void AppendSaveFileQueryTitle(DBusMessageIter& iter) { + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_SAVE_FILE); +} + +void AppendOpenFileQueryDictEntryHandleToken(DBusMessageIter& sub_iter, const char* handle_token) { + DBusMessageIter sub_sub_iter; + DBusMessageIter variant_iter; + dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter); + dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_HANDLE_TOKEN); + dbus_message_iter_open_container(&sub_sub_iter, DBUS_TYPE_VARIANT, "s", &variant_iter); + dbus_message_iter_append_basic(&variant_iter, DBUS_TYPE_STRING, &handle_token); + dbus_message_iter_close_container(&sub_sub_iter, &variant_iter); + dbus_message_iter_close_container(&sub_iter, &sub_sub_iter); +} + +template +void AppendOpenFileQueryDictEntryMultiple(DBusMessageIter&); +template <> +void AppendOpenFileQueryDictEntryMultiple(DBusMessageIter& sub_iter) { + DBusMessageIter sub_sub_iter; + DBusMessageIter variant_iter; + dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter); + dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_MULTIPLE); + dbus_message_iter_open_container(&sub_sub_iter, DBUS_TYPE_VARIANT, "b", &variant_iter); + { + int b = true; + dbus_message_iter_append_basic(&variant_iter, DBUS_TYPE_BOOLEAN, &b); + } + dbus_message_iter_close_container(&sub_sub_iter, &variant_iter); + dbus_message_iter_close_container(&sub_iter, &sub_sub_iter); +} +template <> +void AppendOpenFileQueryDictEntryMultiple(DBusMessageIter&) {} + +template +void AppendOpenFileQueryDictEntryDirectory(DBusMessageIter&); +template <> +void AppendOpenFileQueryDictEntryDirectory(DBusMessageIter& sub_iter) { + DBusMessageIter sub_sub_iter; + DBusMessageIter variant_iter; + dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter); + dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_DIRECTORY); + dbus_message_iter_open_container(&sub_sub_iter, DBUS_TYPE_VARIANT, "b", &variant_iter); + { + int b = true; + dbus_message_iter_append_basic(&variant_iter, DBUS_TYPE_BOOLEAN, &b); + } + dbus_message_iter_close_container(&sub_sub_iter, &variant_iter); + dbus_message_iter_close_container(&sub_iter, &sub_sub_iter); +} +template <> +void AppendOpenFileQueryDictEntryDirectory(DBusMessageIter&) {} + +void AppendSingleFilter(DBusMessageIter& base_iter, const nfdnfilteritem_t& filter) { + DBusMessageIter filter_list_struct_iter; + DBusMessageIter filter_sublist_iter; + DBusMessageIter filter_sublist_struct_iter; + dbus_message_iter_open_container( + &base_iter, DBUS_TYPE_STRUCT, nullptr, &filter_list_struct_iter); + // count number of file extensions + size_t sep = 1; + for (const char* p = filter.spec; *p; ++p) { + if (*p == L',') { + ++sep; + } + } + { + const size_t name_len = strlen(filter.name); + const size_t spec_len = strlen(filter.spec); + char* buf = static_cast(alloca(sep + name_len + 2 + spec_len + 1)); + char* buf_end = buf; + buf_end = copy(filter.name, filter.name + name_len, buf_end); + *buf_end++ = ' '; + *buf_end++ = '('; + const char* spec_ptr = filter.spec; + do { + *buf_end++ = *spec_ptr; + if (*spec_ptr == ',') *buf_end++ = ' '; + ++spec_ptr; + } while (*spec_ptr != '\0'); + *buf_end++ = ')'; + *buf_end = '\0'; + dbus_message_iter_append_basic(&filter_list_struct_iter, DBUS_TYPE_STRING, &buf); + } + { + dbus_message_iter_open_container( + &filter_list_struct_iter, DBUS_TYPE_ARRAY, "(us)", &filter_sublist_iter); + const char* extn_begin = filter.spec; + const char* extn_end = extn_begin; + while (true) { + dbus_message_iter_open_container( + &filter_sublist_iter, DBUS_TYPE_STRUCT, nullptr, &filter_sublist_struct_iter); + { + const unsigned zero = 0; + dbus_message_iter_append_basic( + &filter_sublist_struct_iter, DBUS_TYPE_UINT32, &zero); + } + do { + ++extn_end; + } while (*extn_end != ',' && *extn_end != '\0'); + char* buf = static_cast(alloca(2 + (extn_end - extn_begin) + 1)); + char* buf_end = buf; + *buf_end++ = '*'; + *buf_end++ = '.'; + buf_end = copy(extn_begin, extn_end, buf_end); + *buf_end = '\0'; + dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_STRING, &buf); + dbus_message_iter_close_container(&filter_sublist_iter, &filter_sublist_struct_iter); + if (*extn_end == '\0') { + break; + } + extn_begin = extn_end + 1; + extn_end = extn_begin; + } + } + dbus_message_iter_close_container(&filter_list_struct_iter, &filter_sublist_iter); + dbus_message_iter_close_container(&base_iter, &filter_list_struct_iter); +} + +bool AppendSingleFilterCheckExtn(DBusMessageIter& base_iter, + const nfdnfilteritem_t& filter, + const nfdnchar_t* match_extn) { + DBusMessageIter filter_list_struct_iter; + DBusMessageIter filter_sublist_iter; + DBusMessageIter filter_sublist_struct_iter; + dbus_message_iter_open_container( + &base_iter, DBUS_TYPE_STRUCT, nullptr, &filter_list_struct_iter); + // count number of file extensions + size_t sep = 1; + for (const char* p = filter.spec; *p; ++p) { + if (*p == L',') { + ++sep; + } + } + { + const size_t name_len = strlen(filter.name); + const size_t spec_len = strlen(filter.spec); + char* buf = static_cast(alloca(sep + name_len + 2 + spec_len + 1)); + char* buf_end = buf; + buf_end = copy(filter.name, filter.name + name_len, buf_end); + *buf_end++ = ' '; + *buf_end++ = '('; + const char* spec_ptr = filter.spec; + do { + *buf_end++ = *spec_ptr; + if (*spec_ptr == ',') *buf_end++ = ' '; + ++spec_ptr; + } while (*spec_ptr != '\0'); + *buf_end++ = ')'; + *buf_end = '\0'; + dbus_message_iter_append_basic(&filter_list_struct_iter, DBUS_TYPE_STRING, &buf); + } + bool extn_matched = false; + { + dbus_message_iter_open_container( + &filter_list_struct_iter, DBUS_TYPE_ARRAY, "(us)", &filter_sublist_iter); + const char* extn_begin = filter.spec; + const char* extn_end = extn_begin; + while (true) { + dbus_message_iter_open_container( + &filter_sublist_iter, DBUS_TYPE_STRUCT, nullptr, &filter_sublist_struct_iter); + { + const unsigned zero = 0; + dbus_message_iter_append_basic( + &filter_sublist_struct_iter, DBUS_TYPE_UINT32, &zero); + } + do { + ++extn_end; + } while (*extn_end != ',' && *extn_end != '\0'); + char* buf = static_cast(alloca(2 + (extn_end - extn_begin) + 1)); + char* buf_end = buf; + *buf_end++ = '*'; + *buf_end++ = '.'; + buf_end = copy(extn_begin, extn_end, buf_end); + *buf_end = '\0'; + dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_STRING, &buf); + dbus_message_iter_close_container(&filter_sublist_iter, &filter_sublist_struct_iter); + if (!extn_matched) { + const char* match_extn_p; + const char* p; + for (p = extn_begin, match_extn_p = match_extn; p != extn_end && *match_extn_p; + ++p, ++match_extn_p) { + if (*p != *match_extn_p) break; + } + if (p == extn_end && !*match_extn_p) { + extn_matched = true; + } + } + if (*extn_end == '\0') { + break; + } + extn_begin = extn_end + 1; + extn_end = extn_begin; + } + } + dbus_message_iter_close_container(&filter_list_struct_iter, &filter_sublist_iter); + dbus_message_iter_close_container(&base_iter, &filter_list_struct_iter); + return extn_matched; +} + +void AppendWildcardFilter(DBusMessageIter& base_iter) { + DBusMessageIter filter_list_struct_iter; + DBusMessageIter filter_sublist_iter; + DBusMessageIter filter_sublist_struct_iter; + dbus_message_iter_open_container( + &base_iter, DBUS_TYPE_STRUCT, nullptr, &filter_list_struct_iter); + dbus_message_iter_append_basic(&filter_list_struct_iter, DBUS_TYPE_STRING, &STR_ALL_FILES); + dbus_message_iter_open_container( + &filter_list_struct_iter, DBUS_TYPE_ARRAY, "(us)", &filter_sublist_iter); + dbus_message_iter_open_container( + &filter_sublist_iter, DBUS_TYPE_STRUCT, nullptr, &filter_sublist_struct_iter); + { + const unsigned zero = 0; + dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_UINT32, &zero); + } + dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_STRING, &STR_ASTERISK); + dbus_message_iter_close_container(&filter_sublist_iter, &filter_sublist_struct_iter); + dbus_message_iter_close_container(&filter_list_struct_iter, &filter_sublist_iter); + dbus_message_iter_close_container(&base_iter, &filter_list_struct_iter); +} + +template +void AppendOpenFileQueryDictEntryFilters(DBusMessageIter&, + const nfdnfilteritem_t*, + nfdfiltersize_t); +template <> +void AppendOpenFileQueryDictEntryFilters(DBusMessageIter& sub_iter, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount) { + if (filterCount != 0) { + DBusMessageIter sub_sub_iter; + DBusMessageIter variant_iter; + DBusMessageIter filter_list_iter; + + // filters + dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter); + dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_FILTERS); + dbus_message_iter_open_container( + &sub_sub_iter, DBUS_TYPE_VARIANT, "a(sa(us))", &variant_iter); + dbus_message_iter_open_container( + &variant_iter, DBUS_TYPE_ARRAY, "(sa(us))", &filter_list_iter); + for (nfdfiltersize_t i = 0; i != filterCount; ++i) { + AppendSingleFilter(filter_list_iter, filterList[i]); + } + AppendWildcardFilter(filter_list_iter); + dbus_message_iter_close_container(&variant_iter, &filter_list_iter); + dbus_message_iter_close_container(&sub_sub_iter, &variant_iter); + dbus_message_iter_close_container(&sub_iter, &sub_sub_iter); + + // current_filter + dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter); + dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_CURRENT_FILTER); + dbus_message_iter_open_container( + &sub_sub_iter, DBUS_TYPE_VARIANT, "(sa(us))", &variant_iter); + AppendSingleFilter(variant_iter, filterList[0]); + dbus_message_iter_close_container(&sub_sub_iter, &variant_iter); + dbus_message_iter_close_container(&sub_iter, &sub_sub_iter); + } +} +template <> +void AppendOpenFileQueryDictEntryFilters(DBusMessageIter&, + const nfdnfilteritem_t*, + nfdfiltersize_t) {} + +void AppendSaveFileQueryDictEntryFilters(DBusMessageIter& sub_iter, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultName) { + if (filterCount != 0) { + DBusMessageIter sub_sub_iter; + DBusMessageIter variant_iter; + DBusMessageIter filter_list_iter; + + // The extension of the defaultName (without the '.'). If NULL, it means that there is no + // extension. + const nfdnchar_t* extn = NULL; + if (defaultName) { + const nfdnchar_t* p = defaultName; + while (*p) ++p; + while (*--p != '.') + ; + ++p; + if (*p) extn = p; + } + bool extn_matched = false; + size_t selected_filter_index; + + // filters + dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter); + dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_FILTERS); + dbus_message_iter_open_container( + &sub_sub_iter, DBUS_TYPE_VARIANT, "a(sa(us))", &variant_iter); + dbus_message_iter_open_container( + &variant_iter, DBUS_TYPE_ARRAY, "(sa(us))", &filter_list_iter); + for (nfdfiltersize_t i = 0; i != filterCount; ++i) { + if (!extn_matched && extn) { + extn_matched = AppendSingleFilterCheckExtn(filter_list_iter, filterList[i], extn); + if (extn_matched) selected_filter_index = i; + } else { + AppendSingleFilter(filter_list_iter, filterList[i]); + } + } + AppendWildcardFilter(filter_list_iter); + dbus_message_iter_close_container(&variant_iter, &filter_list_iter); + dbus_message_iter_close_container(&sub_sub_iter, &variant_iter); + dbus_message_iter_close_container(&sub_iter, &sub_sub_iter); + + // current_filter + dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter); + dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_CURRENT_FILTER); + dbus_message_iter_open_container( + &sub_sub_iter, DBUS_TYPE_VARIANT, "(sa(us))", &variant_iter); + if (extn_matched) { + AppendSingleFilter(variant_iter, filterList[selected_filter_index]); + } else { + AppendWildcardFilter(variant_iter); + } + dbus_message_iter_close_container(&sub_sub_iter, &variant_iter); + dbus_message_iter_close_container(&sub_iter, &sub_sub_iter); + } +} + +void AppendSaveFileQueryDictEntryCurrentName(DBusMessageIter& sub_iter, const char* name) { + if (!name) return; + DBusMessageIter sub_sub_iter; + DBusMessageIter variant_iter; + dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter); + dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_CURRENT_NAME); + dbus_message_iter_open_container(&sub_sub_iter, DBUS_TYPE_VARIANT, "s", &variant_iter); + dbus_message_iter_append_basic(&variant_iter, DBUS_TYPE_STRING, &name); + dbus_message_iter_close_container(&sub_sub_iter, &variant_iter); + dbus_message_iter_close_container(&sub_iter, &sub_sub_iter); +} + +void AppendSaveFileQueryDictEntryCurrentFolder(DBusMessageIter& sub_iter, const char* path) { + if (!path) return; + DBusMessageIter sub_sub_iter; + DBusMessageIter variant_iter; + DBusMessageIter array_iter; + dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter); + dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_CURRENT_FOLDER); + dbus_message_iter_open_container(&sub_sub_iter, DBUS_TYPE_VARIANT, "ay", &variant_iter); + dbus_message_iter_open_container(&variant_iter, DBUS_TYPE_ARRAY, "y", &array_iter); + // Append string as byte array, including the terminating null byte as required by the portal. + const char* p = path; + do { + dbus_message_iter_append_basic(&array_iter, DBUS_TYPE_BYTE, p); + } while (*p++); + dbus_message_iter_close_container(&variant_iter, &array_iter); + dbus_message_iter_close_container(&sub_sub_iter, &variant_iter); + dbus_message_iter_close_container(&sub_iter, &sub_sub_iter); +} + +void AppendSaveFileQueryDictEntryCurrentFile(DBusMessageIter& sub_iter, + const char* path, + const char* name) { + if (!path || !name) return; + const size_t path_len = strlen(path); + const size_t name_len = strlen(name); + char* pathname; + char* pathname_end; + size_t pathname_len; + if (path_len && path[path_len - 1] == '/') { + pathname_len = path_len + name_len; + pathname = NFDi_Malloc(pathname_len + 1); + pathname_end = pathname; + pathname_end = copy(path, path + path_len, pathname_end); + pathname_end = copy(name, name + name_len, pathname_end); + *pathname_end++ = '\0'; + } else { + pathname_len = path_len + 1 + name_len; + pathname = NFDi_Malloc(pathname_len + 1); + pathname_end = pathname; + pathname_end = copy(path, path + path_len, pathname_end); + *pathname_end++ = '/'; + pathname_end = copy(name, name + name_len, pathname_end); + *pathname_end++ = '\0'; + } + Free_Guard guard(pathname); + if (access(pathname, F_OK) != 0) return; + DBusMessageIter sub_sub_iter; + DBusMessageIter variant_iter; + DBusMessageIter array_iter; + dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter); + dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_CURRENT_FILE); + dbus_message_iter_open_container(&sub_sub_iter, DBUS_TYPE_VARIANT, "ay", &variant_iter); + dbus_message_iter_open_container(&variant_iter, DBUS_TYPE_ARRAY, "y", &array_iter); + // This includes the terminating null character, which is required by the portal. + for (const char* p = pathname; p != pathname_end; ++p) { + dbus_message_iter_append_basic(&array_iter, DBUS_TYPE_BYTE, p); + } + dbus_message_iter_close_container(&variant_iter, &array_iter); + dbus_message_iter_close_container(&sub_sub_iter, &variant_iter); + dbus_message_iter_close_container(&sub_iter, &sub_sub_iter); +} + +// Append OpenFile() portal params to the given query. +template +void AppendOpenFileQueryParams(DBusMessage* query, + const char* handle_token, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount) { + DBusMessageIter iter; + dbus_message_iter_init_append(query, &iter); + + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_EMPTY); + + AppendOpenFileQueryTitle(iter); + + DBusMessageIter sub_iter; + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &sub_iter); + AppendOpenFileQueryDictEntryHandleToken(sub_iter, handle_token); + AppendOpenFileQueryDictEntryMultiple(sub_iter); + AppendOpenFileQueryDictEntryDirectory(sub_iter); + AppendOpenFileQueryDictEntryFilters(sub_iter, filterList, filterCount); + dbus_message_iter_close_container(&iter, &sub_iter); +} + +// Append SaveFile() portal params to the given query. +void AppendSaveFileQueryParams(DBusMessage* query, + const char* handle_token, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath, + const nfdnchar_t* defaultName) { + DBusMessageIter iter; + dbus_message_iter_init_append(query, &iter); + + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_EMPTY); + + AppendSaveFileQueryTitle(iter); + + DBusMessageIter sub_iter; + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &sub_iter); + AppendOpenFileQueryDictEntryHandleToken(sub_iter, handle_token); + AppendSaveFileQueryDictEntryFilters(sub_iter, filterList, filterCount, defaultName); + AppendSaveFileQueryDictEntryCurrentName(sub_iter, defaultName); + AppendSaveFileQueryDictEntryCurrentFolder(sub_iter, defaultPath); + AppendSaveFileQueryDictEntryCurrentFile(sub_iter, defaultPath, defaultName); + dbus_message_iter_close_container(&iter, &sub_iter); +} + +nfdresult_t ReadDictImpl(const char*, DBusMessageIter&) { + return NFD_OKAY; +} + +template +nfdresult_t ReadDictImpl(const char* key, + DBusMessageIter& iter, + const char*& candidate_key, + Callback& candidate_callback, + Args&... args) { + if (strcmp(key, candidate_key) == 0) { + // this is the correct callback + return candidate_callback(iter); + } else { + return ReadDictImpl(key, iter, args...); + } +} + +// Read a dictionary from the given iterator. The type of the element under this iterator will be +// checked. The args are alternately key and callback. Key is a const char*, and callback is a +// function that returns nfdresult_t. Return NFD_ERROR to stop processing and return immediately. +template +nfdresult_t ReadDict(DBusMessageIter iter, Args... args) { + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + NFDi_SetError("D-Bus response signal argument is not an array."); + return NFD_ERROR; + } + DBusMessageIter sub_iter; + dbus_message_iter_recurse(&iter, &sub_iter); + while (dbus_message_iter_get_arg_type(&sub_iter) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter de_iter; + dbus_message_iter_recurse(&sub_iter, &de_iter); + if (dbus_message_iter_get_arg_type(&de_iter) != DBUS_TYPE_STRING) { + NFDi_SetError("D-Bus response signal dict entry does not start with a string."); + return NFD_ERROR; + } + const char* key; + dbus_message_iter_get_basic(&de_iter, &key); + if (!dbus_message_iter_next(&de_iter)) { + NFDi_SetError("D-Bus response signal dict entry is missing one or more arguments."); + return NFD_ERROR; + } + // unwrap the variant + if (dbus_message_iter_get_arg_type(&de_iter) != DBUS_TYPE_VARIANT) { + NFDi_SetError("D-Bus response signal dict entry value is not a variant."); + return NFD_ERROR; + } + DBusMessageIter de_variant_iter; + dbus_message_iter_recurse(&de_iter, &de_variant_iter); + if (ReadDictImpl(key, de_variant_iter, args...) == NFD_ERROR) return NFD_ERROR; + if (!dbus_message_iter_next(&sub_iter)) break; + } + return NFD_OKAY; +} + +// Read the message, returning an iterator to the `results` dictionary of the response. If response +// was okay, then returns NFD_OKAY and set `resultsIter` to the results dictionary iterator (this is +// the iterator to the entire dictionary (which has type DBUS_TYPE_ARRAY), not an iterator to the +// first item in the dictionary). It does not check that this iterator is DBUS_TYPE_ARRAY; you +// should use ReadDict() which will check it. Otherwise, returns NFD_CANCEL or NFD_ERROR as +// appropriate, and does not modify `resultsIter`. `resultsIter` can be copied by value. +nfdresult_t ReadResponseResults(DBusMessage* msg, DBusMessageIter& resultsIter) { + DBusMessageIter iter; + if (!dbus_message_iter_init(msg, &iter)) { + NFDi_SetError("D-Bus response signal is missing one or more arguments."); + return NFD_ERROR; + } + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_UINT32) { + NFDi_SetError("D-Bus response signal argument is not a uint32."); + return NFD_ERROR; + } + dbus_uint32_t resp_code; + dbus_message_iter_get_basic(&iter, &resp_code); + if (resp_code != 0) { + if (resp_code == 1) { + // User pressed cancel + return NFD_CANCEL; + } else { + // Some error occurred + NFDi_SetError("D-Bus file dialog interaction was ended abruptly."); + return NFD_ERROR; + } + } + // User successfully responded + if (!dbus_message_iter_next(&iter)) { + NFDi_SetError("D-Bus response signal is missing one or more arguments."); + return NFD_ERROR; + } + resultsIter = iter; + return NFD_OKAY; +} + +// Read the message. If response was okay, then returns NFD_OKAY and set `uriIter` to the URI array +// iterator. Otherwise, returns NFD_CANCEL or NFD_ERROR as appropriate, and does not modify +// `uriIter`. `uriIter` can be copied by value. +nfdresult_t ReadResponseUris(DBusMessage* msg, DBusMessageIter& uriIter) { + DBusMessageIter iter; + const nfdresult_t res = ReadResponseResults(msg, iter); + if (res != NFD_OKAY) return res; + bool has_uris = false; + if (ReadDict(iter, "uris", [&uriIter, &has_uris](DBusMessageIter& uris_iter) { + if (dbus_message_iter_get_arg_type(&uris_iter) != DBUS_TYPE_ARRAY) { + NFDi_SetError("D-Bus response signal URI iter is not an array."); + return NFD_ERROR; + } + dbus_message_iter_recurse(&uris_iter, &uriIter); + has_uris = true; + return NFD_OKAY; + }) == NFD_ERROR) + return NFD_ERROR; + + if (!has_uris) { + NFDi_SetError("D-Bus response signal has no URI field."); + return NFD_ERROR; + } + return NFD_OKAY; +} + +// Same as ReadResponseUris, but does not perform any message type checks. +// You should only use this if you previously used ReadResponseUris and it returned NFD_OKAY! +void ReadResponseUrisUnchecked(DBusMessage* msg, DBusMessageIter& uriIter) { + DBusMessageIter iter; + dbus_message_iter_init(msg, &iter); + dbus_message_iter_next(&iter); + ReadDict(iter, "uris", [&uriIter](DBusMessageIter& uris_iter) { + dbus_message_iter_recurse(&uris_iter, &uriIter); + return NFD_OKAY; + }); +} +nfdpathsetsize_t ReadResponseUrisUncheckedGetArraySize(DBusMessage* msg) { + DBusMessageIter iter; + dbus_message_iter_init(msg, &iter); + dbus_message_iter_next(&iter); + nfdpathsetsize_t sz = 0; // Initialization will never be used, but we initialize it to prevent + // the uninitialized warning otherwise. + ReadDict(iter, "uris", [&sz](DBusMessageIter& uris_iter) { + sz = dbus_message_iter_get_element_count(&uris_iter); + return NFD_OKAY; + }); + return sz; +} + +// Read the response URI. If response was okay, then returns NFD_OKAY and set file to it (the +// pointer is set to some string owned by msg, so you should not manually free it). Otherwise, +// returns NFD_CANCEL or NFD_ERROR as appropriate, and does not modify `file`. +nfdresult_t ReadResponseUrisSingle(DBusMessage* msg, const char*& file) { + DBusMessageIter uri_iter; + const nfdresult_t res = ReadResponseUris(msg, uri_iter); + if (res != NFD_OKAY) return res; // can be NFD_CANCEL or NFD_ERROR + if (dbus_message_iter_get_arg_type(&uri_iter) != DBUS_TYPE_STRING) { + NFDi_SetError("D-Bus response signal URI sub iter is not an string."); + return NFD_ERROR; + } + dbus_message_iter_get_basic(&uri_iter, &file); + return NFD_OKAY; +} + +#if NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION == 1 +// Read the response URI and selected extension (in the form "*.abc" or "*") (if any). If response +// was okay, then returns NFD_OKAY and set file and extn to them (the pointer is set to some string +// owned by msg, so you should not manually free it). `file` is the user-entered file name, and +// `extn` is the selected file extension (the first one if there are multiple extensions in the +// selected option) (this is NULL if "All files" is selected). Otherwise, returns NFD_CANCEL or +// NFD_ERROR as appropriate, and does not modify `file` and `extn`. +nfdresult_t ReadResponseUrisSingleAndCurrentExtension(DBusMessage* msg, + const char*& file, + const char*& extn) { + DBusMessageIter iter; + const nfdresult_t res = ReadResponseResults(msg, iter); + if (res != NFD_OKAY) return res; + const char* tmp_file = nullptr; + const char* tmp_extn = nullptr; + if (ReadDict( + iter, + "uris", + [&tmp_file](DBusMessageIter& uris_iter) { + if (dbus_message_iter_get_arg_type(&uris_iter) != DBUS_TYPE_ARRAY) { + NFDi_SetError("D-Bus response signal URI iter is not an array."); + return NFD_ERROR; + } + DBusMessageIter uri_iter; + dbus_message_iter_recurse(&uris_iter, &uri_iter); + if (dbus_message_iter_get_arg_type(&uri_iter) != DBUS_TYPE_STRING) { + NFDi_SetError("D-Bus response signal URI sub iter is not a string."); + return NFD_ERROR; + } + dbus_message_iter_get_basic(&uri_iter, &tmp_file); + return NFD_OKAY; + }, + "current_filter", + [&tmp_extn](DBusMessageIter& current_filter_iter) { + // current_filter is best_effort, so if we fail, we still return NFD_OKAY. + if (dbus_message_iter_get_arg_type(¤t_filter_iter) != DBUS_TYPE_STRUCT) { + // NFDi_SetError("D-Bus response signal current_filter iter is not a struct."); + return NFD_OKAY; + } + DBusMessageIter current_filter_struct_iter; + dbus_message_iter_recurse(¤t_filter_iter, ¤t_filter_struct_iter); + if (!dbus_message_iter_next(¤t_filter_struct_iter)) { + // NFDi_SetError("D-Bus response signal current_filter struct iter ended + // prematurely."); + return NFD_OKAY; + } + if (dbus_message_iter_get_arg_type(¤t_filter_struct_iter) != + DBUS_TYPE_ARRAY) { + // NFDi_SetError("D-Bus response signal URI sub iter is not an string."); + return NFD_OKAY; + } + DBusMessageIter current_filter_array_iter; + dbus_message_iter_recurse(¤t_filter_struct_iter, ¤t_filter_array_iter); + if (dbus_message_iter_get_arg_type(¤t_filter_array_iter) != + DBUS_TYPE_STRUCT) { + // NFDi_SetError("D-Bus response signal current_filter iter is not a struct."); + return NFD_OKAY; + } + DBusMessageIter current_filter_extn_iter; + dbus_message_iter_recurse(¤t_filter_array_iter, ¤t_filter_extn_iter); + if (dbus_message_iter_get_arg_type(¤t_filter_extn_iter) != DBUS_TYPE_UINT32) { + // NFDi_SetError("D-Bus response signal URI sub iter is not an string."); + return NFD_OKAY; + } + dbus_uint32_t type; + dbus_message_iter_get_basic(¤t_filter_extn_iter, &type); + if (type != 0) { + // NFDi_SetError("Wrong filter type."); + return NFD_OKAY; + } + if (!dbus_message_iter_next(¤t_filter_extn_iter)) { + // NFDi_SetError("D-Bus response signal current_filter struct iter ended + // prematurely."); + return NFD_OKAY; + } + if (dbus_message_iter_get_arg_type(¤t_filter_extn_iter) != DBUS_TYPE_STRING) { + // NFDi_SetError("D-Bus response signal URI sub iter is not an string."); + return NFD_OKAY; + } + dbus_message_iter_get_basic(¤t_filter_extn_iter, &tmp_extn); + return NFD_OKAY; + }) == NFD_ERROR) + return NFD_ERROR; + + if (!tmp_file) { + NFDi_SetError("D-Bus response signal has no URI field."); + return NFD_ERROR; + } + file = tmp_file; + extn = tmp_extn; + return NFD_OKAY; +} +#endif + +// Appends up to 64 random chars to the given pointer. Returns the end of the appended chars. +char* Generate64RandomChars(char* out) { + size_t amount = 32; + while (amount > 0) { + unsigned char buf[32]; + ssize_t res = getrandom(buf, amount, 0); + if (res == -1) { + if (errno == EINTR) + continue; + else + break; // too bad, urandom isn't working well + } + amount -= res; + // we encode each random char using two chars, since they must be [A-Z][a-z][0-9]_ + for (size_t i = 0; i != static_cast(res); ++i) { + *out++ = 'A' + static_cast(buf[i] & 15); + *out++ = 'A' + static_cast(buf[i] >> 4); + } + } + return out; +} + +constexpr const char STR_RESPONSE_HANDLE_PREFIX[] = "/org/freedesktop/portal/desktop/request/"; +constexpr size_t STR_RESPONSE_HANDLE_PREFIX_LEN = + sizeof(STR_RESPONSE_HANDLE_PREFIX) - 1; // -1 to remove the \0. + +// Allocates and returns a path like "/org/freedesktop/portal/desktop/request/SENDER/TOKEN" with +// randomly generated TOKEN as recommended by flatpak. `handle_token_ptr` is a pointer to the +// TOKEN part. +char* MakeUniqueObjectPath(const char** handle_token_ptr) { + const char* sender = dbus_unique_name; + if (*sender == ':') ++sender; + const size_t sender_len = strlen(sender); + const size_t sz = STR_RESPONSE_HANDLE_PREFIX_LEN + sender_len + 1 + + 64; // 1 for '/', followed by 64 random chars + char* path = NFDi_Malloc(sz + 1); + char* path_ptr = path; + path_ptr = copy(STR_RESPONSE_HANDLE_PREFIX, + STR_RESPONSE_HANDLE_PREFIX + STR_RESPONSE_HANDLE_PREFIX_LEN, + path_ptr); + path_ptr = transform( + sender, sender + sender_len, path_ptr, [](char ch) { return ch != '.' ? ch : '_'; }); + *path_ptr++ = '/'; + *handle_token_ptr = path_ptr; + path_ptr = Generate64RandomChars(path_ptr); + *path_ptr = '\0'; + return path; +} + +constexpr const char STR_RESPONSE_SUBSCRIPTION_PATH_1[] = + "type='signal',sender='org.freedesktop.portal.Desktop',path='"; +constexpr const char STR_RESPONSE_SUBSCRIPTION_PATH_1_LEN = + sizeof(STR_RESPONSE_SUBSCRIPTION_PATH_1) - 1; +constexpr const char STR_RESPONSE_SUBSCRIPTION_PATH_2[] = + "',interface='org.freedesktop.portal.Request',member='Response',destination='"; +constexpr const char STR_RESPONSE_SUBSCRIPTION_PATH_2_LEN = + sizeof(STR_RESPONSE_SUBSCRIPTION_PATH_2) - 1; +constexpr const char STR_RESPONSE_SUBSCRIPTION_PATH_3[] = "'"; +constexpr const char STR_RESPONSE_SUBSCRIPTION_PATH_3_LEN = + sizeof(STR_RESPONSE_SUBSCRIPTION_PATH_3) - 1; + +class DBusSignalSubscriptionHandler { + private: + char* sub_cmd; + + public: + DBusSignalSubscriptionHandler() : sub_cmd(nullptr) {} + ~DBusSignalSubscriptionHandler() { + if (sub_cmd) Unsubscribe(); + } + + nfdresult_t Subscribe(const char* handle_path) { + if (sub_cmd) Unsubscribe(); + sub_cmd = MakeResponseSubscriptionPath(handle_path, dbus_unique_name); + DBusError err; + dbus_error_init(&err); + dbus_bus_add_match(dbus_conn, sub_cmd, &err); + if (dbus_error_is_set(&err)) { + dbus_error_free(&dbus_err); + dbus_move_error(&err, &dbus_err); + NFDi_SetError(dbus_err.message); + return NFD_ERROR; + } + return NFD_OKAY; + } + + void Unsubscribe() { + DBusError err; + dbus_error_init(&err); + dbus_bus_remove_match(dbus_conn, sub_cmd, &err); + NFDi_Free(sub_cmd); + sub_cmd = nullptr; + dbus_error_free( + &err); // silence unsubscribe errors, because this is intuitively part of 'cleanup' + } + + private: + static char* MakeResponseSubscriptionPath(const char* handle_path, const char* unique_name) { + const size_t handle_path_len = strlen(handle_path); + const size_t unique_name_len = strlen(unique_name); + const size_t sz = STR_RESPONSE_SUBSCRIPTION_PATH_1_LEN + handle_path_len + + STR_RESPONSE_SUBSCRIPTION_PATH_2_LEN + unique_name_len + + STR_RESPONSE_SUBSCRIPTION_PATH_3_LEN; + char* res = NFDi_Malloc(sz + 1); + char* res_ptr = res; + res_ptr = copy(STR_RESPONSE_SUBSCRIPTION_PATH_1, + STR_RESPONSE_SUBSCRIPTION_PATH_1 + STR_RESPONSE_SUBSCRIPTION_PATH_1_LEN, + res_ptr); + res_ptr = copy(handle_path, handle_path + handle_path_len, res_ptr); + res_ptr = copy(STR_RESPONSE_SUBSCRIPTION_PATH_2, + STR_RESPONSE_SUBSCRIPTION_PATH_2 + STR_RESPONSE_SUBSCRIPTION_PATH_2_LEN, + res_ptr); + res_ptr = copy(unique_name, unique_name + unique_name_len, res_ptr); + res_ptr = copy(STR_RESPONSE_SUBSCRIPTION_PATH_3, + STR_RESPONSE_SUBSCRIPTION_PATH_3 + STR_RESPONSE_SUBSCRIPTION_PATH_3_LEN, + res_ptr); + *res_ptr = '\0'; + return res; + } +}; + +constexpr const char FILE_URI_PREFIX[] = "file://"; +constexpr size_t FILE_URI_PREFIX_LEN = sizeof(FILE_URI_PREFIX) - 1; + +// If fileUri starts with "file://", strips that prefix and copies it to a new buffer, and make +// outPath point to it, and returns NFD_OKAY. Otherwise, does not modify outPath and returns +// NFD_ERROR (with the correct error set) +nfdresult_t AllocAndCopyFilePath(const char* fileUri, char*& outPath) { + const char* prefix_begin = FILE_URI_PREFIX; + const char* const prefix_end = FILE_URI_PREFIX + FILE_URI_PREFIX_LEN; + for (; prefix_begin != prefix_end; ++prefix_begin, ++fileUri) { + if (*prefix_begin != *fileUri) { + NFDi_SetError("D-Bus freedesktop portal returned a URI that is not a file URI."); + return NFD_ERROR; + } + } + size_t len = strlen(fileUri); + char* path_without_prefix = NFDi_Malloc(len + 1); + copy(fileUri, fileUri + (len + 1), path_without_prefix); + outPath = path_without_prefix; + return NFD_OKAY; +} + +#if NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION == 1 +bool TryGetValidExtension(const char* extn, + const char*& trimmed_extn, + const char*& trimmed_extn_end) { + if (!extn) return false; + if (*extn != '*') return false; + ++extn; + if (*extn != '.') return false; + trimmed_extn = extn; + for (++extn; *extn != '\0'; ++extn) + ; + ++extn; + trimmed_extn_end = extn; + return true; +} + +// Like AllocAndCopyFilePath, but if `fileUri` has no extension and `extn` is usable, appends the +// extension. `extn` could be null, in which case no extension will ever be appended. `extn` is +// expected to be either in the form "*.abc" or "*", but this function will check for it, and ignore +// the extension if it is not in the correct form. +nfdresult_t AllocAndCopyFilePathWithExtn(const char* fileUri, const char* extn, char*& outPath) { + const char* prefix_begin = FILE_URI_PREFIX; + const char* const prefix_end = FILE_URI_PREFIX + FILE_URI_PREFIX_LEN; + for (; prefix_begin != prefix_end; ++prefix_begin, ++fileUri) { + if (*prefix_begin != *fileUri) { + NFDi_SetError("D-Bus freedesktop portal returned a URI that is not a file URI."); + return NFD_ERROR; + } + } + + const char* file_end = fileUri; + for (; *file_end != '\0'; ++file_end) + ; + const char* file_it = file_end; + do { + --file_it; + } while (*file_it != '/' && *file_it != '.'); + const char* trimmed_extn; // includes the '.' + const char* trimmed_extn_end; // includes the '\0' + if (*file_it == '.' || !TryGetValidExtension(extn, trimmed_extn, trimmed_extn_end)) { + // has file extension already or no valid extension in `extn` + ++file_end; // includes the '\0' + char* path_without_prefix = NFDi_Malloc(file_end - fileUri); + copy(fileUri, file_end, path_without_prefix); + outPath = path_without_prefix; + } else { + // no file extension and we have a valid extension + char* path_without_prefix = + NFDi_Malloc((file_end - fileUri) + (trimmed_extn_end - trimmed_extn)); + char* out = copy(fileUri, file_end, path_without_prefix); + copy(trimmed_extn, trimmed_extn_end, out); + outPath = path_without_prefix; + } + return NFD_OKAY; +} +#endif + +// DBus wrapper function that helps invoke the portal for all OpenFile() variants. +// This function returns NFD_OKAY iff outMsg gets set (to the returned message). +// Caller is responsible for freeing the outMsg using dbus_message_unref() (or use +// DBusMessage_Guard). +template +nfdresult_t NFD_DBus_OpenFile(DBusMessage*& outMsg, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount) { + const char* handle_token_ptr; + char* handle_obj_path = MakeUniqueObjectPath(&handle_token_ptr); + Free_Guard handle_obj_path_guard(handle_obj_path); + + DBusError err; // need a separate error object because we don't want to mess with the old one + // if it's stil set + dbus_error_init(&err); + + // Subscribe to the signal using the handle_obj_path + DBusSignalSubscriptionHandler signal_sub; + nfdresult_t res = signal_sub.Subscribe(handle_obj_path); + if (res != NFD_OKAY) return res; + + // TODO: use XOpenDisplay()/XGetInputFocus() to find xid of window... but what should one do on + // Wayland? + + DBusMessage* query = dbus_message_new_method_call("org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.FileChooser", + "OpenFile"); + DBusMessage_Guard query_guard(query); + AppendOpenFileQueryParams( + query, handle_token_ptr, filterList, filterCount); + + DBusMessage* reply = + dbus_connection_send_with_reply_and_block(dbus_conn, query, DBUS_TIMEOUT_INFINITE, &err); + if (!reply) { + dbus_error_free(&dbus_err); + dbus_move_error(&err, &dbus_err); + NFDi_SetError(dbus_err.message); + return NFD_ERROR; + } + DBusMessage_Guard reply_guard(reply); + + // Check the reply and update our signal subscription if necessary + { + DBusMessageIter iter; + if (!dbus_message_iter_init(reply, &iter)) { + NFDi_SetError("D-Bus reply is missing an argument."); + return NFD_ERROR; + } + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_OBJECT_PATH) { + NFDi_SetError("D-Bus reply is not an object path."); + return NFD_ERROR; + } + + const char* path; + dbus_message_iter_get_basic(&iter, &path); + if (strcmp(path, handle_obj_path) != 0) { + // needs to change our signal subscription + signal_sub.Subscribe(path); + } + } + + // Wait and read the response + // const char* file = nullptr; + do { + while (true) { + DBusMessage* msg = dbus_connection_pop_message(dbus_conn); + if (!msg) break; + + if (dbus_message_is_signal(msg, "org.freedesktop.portal.Request", "Response")) { + // this is the response we're looking for + outMsg = msg; + return NFD_OKAY; + } + + dbus_message_unref(msg); + } + } while (dbus_connection_read_write(dbus_conn, -1)); + + NFDi_SetError("D-Bus freedesktop portal did not give us a reply."); + return NFD_ERROR; +} + +// DBus wrapper function that helps invoke the portal for the SaveFile() API. +// This function returns NFD_OKAY iff outMsg gets set (to the returned message). +// Caller is responsible for freeing the outMsg using dbus_message_unref() (or use +// DBusMessage_Guard). +nfdresult_t NFD_DBus_SaveFile(DBusMessage*& outMsg, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath, + const nfdnchar_t* defaultName) { + const char* handle_token_ptr; + char* handle_obj_path = MakeUniqueObjectPath(&handle_token_ptr); + Free_Guard handle_obj_path_guard(handle_obj_path); + + DBusError err; // need a separate error object because we don't want to mess with the old one + // if it's stil set + dbus_error_init(&err); + + // Subscribe to the signal using the handle_obj_path + DBusSignalSubscriptionHandler signal_sub; + nfdresult_t res = signal_sub.Subscribe(handle_obj_path); + if (res != NFD_OKAY) return res; + + // TODO: use XOpenDisplay()/XGetInputFocus() to find xid of window... but what should one do on + // Wayland? + + DBusMessage* query = dbus_message_new_method_call("org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.FileChooser", + "SaveFile"); + DBusMessage_Guard query_guard(query); + AppendSaveFileQueryParams( + query, handle_token_ptr, filterList, filterCount, defaultPath, defaultName); + + DBusMessage* reply = + dbus_connection_send_with_reply_and_block(dbus_conn, query, DBUS_TIMEOUT_INFINITE, &err); + if (!reply) { + dbus_error_free(&dbus_err); + dbus_move_error(&err, &dbus_err); + NFDi_SetError(dbus_err.message); + return NFD_ERROR; + } + DBusMessage_Guard reply_guard(reply); + + // Check the reply and update our signal subscription if necessary + { + DBusMessageIter iter; + if (!dbus_message_iter_init(reply, &iter)) { + NFDi_SetError("D-Bus reply is missing an argument."); + return NFD_ERROR; + } + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_OBJECT_PATH) { + NFDi_SetError("D-Bus reply is not an object path."); + return NFD_ERROR; + } + + const char* path; + dbus_message_iter_get_basic(&iter, &path); + if (strcmp(path, handle_obj_path) != 0) { + // needs to change our signal subscription + signal_sub.Subscribe(path); + } + } + + // Wait and read the response + // const char* file = nullptr; + do { + while (true) { + DBusMessage* msg = dbus_connection_pop_message(dbus_conn); + if (!msg) break; + + if (dbus_message_is_signal(msg, "org.freedesktop.portal.Request", "Response")) { + // this is the response we're looking for + outMsg = msg; + return NFD_OKAY; + } + + dbus_message_unref(msg); + } + } while (dbus_connection_read_write(dbus_conn, -1)); + + NFDi_SetError("D-Bus freedesktop portal did not give us a reply."); + return NFD_ERROR; +} + +} // namespace + +/* public */ + +const char* NFD_GetError(void) { + return err_ptr; +} + +void NFD_ClearError(void) { + NFDi_SetError(nullptr); + dbus_error_free(&dbus_err); +} + +nfdresult_t NFD_Init(void) { + // Initialize dbus_error + dbus_error_init(&dbus_err); + // Get DBus connection + dbus_conn = dbus_bus_get(DBUS_BUS_SESSION, &dbus_err); + if (!dbus_conn) { + NFDi_SetError(dbus_err.message); + return NFD_ERROR; + } + dbus_unique_name = dbus_bus_get_unique_name(dbus_conn); + if (!dbus_unique_name) { + NFDi_SetError("Unable to get the unique name of our D-Bus connection."); + return NFD_ERROR; + } + return NFD_OKAY; +} +void NFD_Quit(void) { + dbus_connection_unref(dbus_conn); + // Note: We do not free dbus_error since NFD_Init might set it. + // To avoid leaking memory, the caller should explicitly call NFD_ClearError after reading the + // error. +} + +void NFD_FreePathN(nfdnchar_t* filePath) { + assert(filePath); + NFDi_Free(filePath); +} + +nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath) { + (void)defaultPath; // Default path not supported for portal backend + + DBusMessage* msg; + { + const nfdresult_t res = NFD_DBus_OpenFile(msg, filterList, filterCount); + if (res != NFD_OKAY) { + return res; + } + } + DBusMessage_Guard msg_guard(msg); + + const char* file; + { + const nfdresult_t res = ReadResponseUrisSingle(msg, file); + if (res != NFD_OKAY) { + return res; + } + } + + return AllocAndCopyFilePath(file, *outPath); +} + +nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath) { + (void)defaultPath; // Default path not supported for portal backend + + DBusMessage* msg; + { + const nfdresult_t res = NFD_DBus_OpenFile(msg, filterList, filterCount); + if (res != NFD_OKAY) { + return res; + } + } + + DBusMessageIter uri_iter; + const nfdresult_t res = ReadResponseUris(msg, uri_iter); + if (res != NFD_OKAY) { + dbus_message_unref(msg); + return res; + } + + *outPaths = msg; + return NFD_OKAY; +} + +nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath, + const nfdnchar_t* defaultName) { + DBusMessage* msg; + { + const nfdresult_t res = + NFD_DBus_SaveFile(msg, filterList, filterCount, defaultPath, defaultName); + if (res != NFD_OKAY) { + return res; + } + } + DBusMessage_Guard msg_guard(msg); + +#if NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION == 1 + const char* file; + const char* extn; + { + const nfdresult_t res = ReadResponseUrisSingleAndCurrentExtension(msg, file, extn); + if (res != NFD_OKAY) { + return res; + } + } + + return AllocAndCopyFilePathWithExtn(file, extn, *outPath); +#else + const char* file; + { + const nfdresult_t res = ReadResponseUrisSingle(msg, file); + if (res != NFD_OKAY) { + return res; + } + } + + return AllocAndCopyFilePath(file, *outPath); +#endif +} + +nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) { + (void)defaultPath; // Default path not supported for portal backend + + DBusMessage* msg; + { + const nfdresult_t res = NFD_DBus_OpenFile(msg, nullptr, 0); + if (res != NFD_OKAY) { + return res; + } + } + DBusMessage_Guard msg_guard(msg); + + const char* file; + { + const nfdresult_t res = ReadResponseUrisSingle(msg, file); + if (res != NFD_OKAY) { + return res; + } + } + + return AllocAndCopyFilePath(file, *outPath); +} + +nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count) { + assert(pathSet); + DBusMessage* msg = const_cast(static_cast(pathSet)); + *count = ReadResponseUrisUncheckedGetArraySize(msg); + return NFD_OKAY; +} + +nfdresult_t NFD_PathSet_GetPathN(const nfdpathset_t* pathSet, + nfdpathsetsize_t index, + nfdnchar_t** outPath) { + assert(pathSet); + DBusMessage* msg = const_cast(static_cast(pathSet)); + DBusMessageIter uri_iter; + ReadResponseUrisUnchecked(msg, uri_iter); + while (index > 0) { + --index; + if (!dbus_message_iter_next(&uri_iter)) { + NFDi_SetError("Index out of bounds."); + return NFD_ERROR; + } + } + if (dbus_message_iter_get_arg_type(&uri_iter) != DBUS_TYPE_STRING) { + NFDi_SetError("D-Bus response signal URI sub iter is not an string."); + return NFD_ERROR; + } + const char* file; + dbus_message_iter_get_basic(&uri_iter, &file); + return AllocAndCopyFilePath(file, *outPath); +} + +void NFD_PathSet_FreePathN(const nfdnchar_t* filePath) { + assert(filePath); + NFD_FreePathN(const_cast(filePath)); +} + +void NFD_PathSet_Free(const nfdpathset_t* pathSet) { + assert(pathSet); + DBusMessage* msg = const_cast(static_cast(pathSet)); + dbus_message_unref(msg); +} + +nfdresult_t NFD_PathSet_GetEnum(const nfdpathset_t* pathSet, nfdpathsetenum_t* outEnumerator) { + assert(pathSet); + DBusMessage* msg = const_cast(static_cast(pathSet)); + ReadResponseUrisUnchecked(msg, *reinterpret_cast(outEnumerator)); + return NFD_OKAY; +} + +void NFD_PathSet_FreeEnum(nfdpathsetenum_t*) { + // Do nothing, because the enumeration is just a message iterator +} + +nfdresult_t NFD_PathSet_EnumNextN(nfdpathsetenum_t* enumerator, nfdnchar_t** outPath) { + DBusMessageIter& uri_iter = *reinterpret_cast(enumerator); + const int arg_type = dbus_message_iter_get_arg_type(&uri_iter); + if (arg_type == DBUS_TYPE_INVALID) { + *outPath = nullptr; + return NFD_OKAY; + } + if (arg_type != DBUS_TYPE_STRING) { + NFDi_SetError("D-Bus response signal URI sub iter is not an string."); + return NFD_ERROR; + } + const char* file; + dbus_message_iter_get_basic(&uri_iter, &file); + const nfdresult_t res = AllocAndCopyFilePath(file, *outPath); + if (res != NFD_OKAY) return res; + dbus_message_iter_next(&uri_iter); + return NFD_OKAY; +} diff --git a/nfd/nfd_win.cpp b/nfd/nfd_win.cpp index 87c0b130..991e5071 100644 --- a/nfd/nfd_win.cpp +++ b/nfd/nfd_win.cpp @@ -1,13 +1,10 @@ /* - Native File Dialog - - http://www.frogtoss.com/labs + Native File Dialog Extended + Repository: https://github.com/btzy/nativefiledialog-extended + License: Zlib + Author: Bernard Teo */ -#define _CRTDBG_MAP_ALLOC -#include -#include - /* only locally define UNICODE in this compilation unit */ #ifndef UNICODE #define UNICODE @@ -19,768 +16,954 @@ #define _WIN32_WINNT _WIN32_WINNT_VISTA #endif -#include -#include -#include -#include - -struct IUnknown; // Workaround for "combaseapi.h(229): error C2187: syntax error: 'identifier' was unexpected here" when using /permissive- -#include - -#include "nfd_common.h" - - -// allocs the space in outPath -- call free() -static void CopyWCharToNFDChar( const wchar_t *inStr, nfdchar_t **outStr ) -{ - int inStrCharacterCount = static_cast(wcslen(inStr)); - int bytesNeeded = WideCharToMultiByte( CP_UTF8, 0, - inStr, inStrCharacterCount, - NULL, 0, NULL, NULL ); - assert( bytesNeeded ); - bytesNeeded += 1; - - *outStr = (nfdchar_t*)NFDi_Malloc( bytesNeeded ); - if ( !*outStr ) - return; - - int bytesWritten = WideCharToMultiByte( CP_UTF8, 0, - inStr, -1, - *outStr, bytesNeeded, - NULL, NULL ); - assert( bytesWritten ); _NFD_UNUSED( bytesWritten ); -} - -/* includes NULL terminator byte in return */ -static size_t GetUTF8ByteCountForWChar( const wchar_t *str ) -{ - size_t bytesNeeded = WideCharToMultiByte( CP_UTF8, 0, - str, -1, - NULL, 0, NULL, NULL ); - assert( bytesNeeded ); - return bytesNeeded+1; -} - -// write to outPtr -- no free() necessary. No memory stomp tests are done -- they must be done -// before entering this function. -static int CopyWCharToExistingNFDCharBuffer( const wchar_t *inStr, nfdchar_t *outPtr ) -{ - int inStrCharacterCount = static_cast(wcslen(inStr)); - int bytesNeeded = static_cast(GetUTF8ByteCountForWChar( inStr )); - - /* invocation copies null term */ - int bytesWritten = WideCharToMultiByte( CP_UTF8, 0, - inStr, -1, - outPtr, bytesNeeded, - NULL, 0 ); - assert( bytesWritten ); - - return bytesWritten; - -} - - -// allocs the space in outStr -- call free() -static void CopyNFDCharToWChar( const nfdchar_t *inStr, wchar_t **outStr ) -{ - int inStrByteCount = static_cast(strlen(inStr)); - int charsNeeded = MultiByteToWideChar(CP_UTF8, 0, - inStr, inStrByteCount, - NULL, 0 ); - assert( charsNeeded ); - assert( !*outStr ); - charsNeeded += 1; // terminator - - *outStr = (wchar_t*)NFDi_Malloc( charsNeeded * sizeof(wchar_t) ); - if ( !*outStr ) - return; - - int ret = MultiByteToWideChar(CP_UTF8, 0, - inStr, inStrByteCount, - *outStr, charsNeeded); - (*outStr)[charsNeeded-1] = '\0'; - -#ifdef _DEBUG - int inStrCharacterCount = static_cast(NFDi_UTF8_Strlen(inStr)); - assert( ret == inStrCharacterCount ); -#else - _NFD_UNUSED(ret); +#if _MSC_VER +// see +// https://developercommunity.visualstudio.com/content/problem/185399/error-c2760-in-combaseapih-with-windows-sdk-81-and.html +struct IUnknown; // Workaround for "combaseapi.h(229): error C2187: syntax error: 'identifier' was + // unexpected here" when using /permissive- #endif + +#include +#include +#include +#include +#include +#include "nfd.h" + +namespace { + +/* current error */ +const char* g_errorstr = nullptr; + +void NFDi_SetError(const char* msg) { + g_errorstr = msg; } +template +T* NFDi_Malloc(size_t bytes) { + void* ptr = malloc(bytes); + if (!ptr) NFDi_SetError("NFDi_Malloc failed."); -/* ext is in format "jpg", no wildcards or separators */ -static int AppendExtensionToSpecBuf( const char *ext, char *specBuf, size_t specBufLen ) -{ - const char SEP[] = ";"; - assert( specBufLen > strlen(ext)+3 ); - - if ( strlen(specBuf) > 0 ) - { - strncat( specBuf, SEP, specBufLen - strlen(specBuf) - 1 ); - specBufLen += strlen(SEP); - } - - char extWildcard[NFD_MAX_STRLEN]; - int bytesWritten = sprintf_s( extWildcard, NFD_MAX_STRLEN, "*.%s", ext ); - assert( bytesWritten == strlen(ext)+2 ); - - strncat( specBuf, extWildcard, specBufLen - strlen(specBuf) - 1 ); - - return NFD_OKAY; + return static_cast(ptr); } -static nfdresult_t AddFiltersToDialog( ::IFileDialog *fileOpenDialog, const char *filterList ) -{ - const wchar_t EMPTY_WSTR[] = L""; - const wchar_t WILDCARD[] = L"*.*"; +template +void NFDi_Free(T* ptr) { + assert(ptr); + free(static_cast(ptr)); +} - if ( !filterList || strlen(filterList) == 0 ) - return NFD_OKAY; +/* guard objects */ +template +struct Release_Guard { + T* data; + Release_Guard(T* releasable) noexcept : data(releasable) {} + ~Release_Guard() { data->Release(); } +}; - // Count rows to alloc - UINT filterCount = 1; /* guaranteed to have one filter on a correct, non-empty parse */ - const char *p_filterList; - for ( p_filterList = filterList; *p_filterList; ++p_filterList ) - { - if ( *p_filterList == ';' ) - ++filterCount; - } +template +struct Free_Guard { + T* data; + Free_Guard(T* freeable) noexcept : data(freeable) {} + ~Free_Guard() { NFDi_Free(data); } +}; - assert(filterCount); - if ( !filterCount ) - { - NFDi_SetError("Error parsing filters."); - return NFD_ERROR; +template +struct FreeCheck_Guard { + T* data; + FreeCheck_Guard(T* freeable = nullptr) noexcept : data(freeable) {} + ~FreeCheck_Guard() { + if (data) NFDi_Free(data); } +}; +/* helper functions */ +nfdresult_t AddFiltersToDialog(::IFileDialog* fileOpenDialog, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount) { /* filterCount plus 1 because we hardcode the *.* wildcard after the while loop */ - COMDLG_FILTERSPEC *specList = (COMDLG_FILTERSPEC*)NFDi_Malloc( sizeof(COMDLG_FILTERSPEC) * ((size_t)filterCount + 1) ); - if ( !specList ) - { + COMDLG_FILTERSPEC* specList = + NFDi_Malloc(sizeof(COMDLG_FILTERSPEC) * (filterCount + 1)); + if (!specList) { return NFD_ERROR; } - for (UINT i = 0; i < filterCount+1; ++i ) - { - specList[i].pszName = NULL; - specList[i].pszSpec = NULL; - } - size_t specIdx = 0; - p_filterList = filterList; - char typebuf[NFD_MAX_STRLEN] = {0}; /* one per comma or semicolon */ - char *p_typebuf = typebuf; - char filterName[NFD_MAX_STRLEN] = {0}; - - char specbuf[NFD_MAX_STRLEN] = {0}; /* one per semicolon */ - - while ( 1 ) - { - if ( NFDi_IsFilterSegmentChar(*p_filterList) ) - { - /* append a type to the specbuf (pending filter) */ - AppendExtensionToSpecBuf( typebuf, specbuf, NFD_MAX_STRLEN ); - - p_typebuf = typebuf; - memset( typebuf, 0, sizeof(char)*NFD_MAX_STRLEN ); + /* ad-hoc RAII object to free memory when destructing */ + struct COMDLG_FILTERSPEC_Guard { + COMDLG_FILTERSPEC* _specList; + nfdfiltersize_t index; + COMDLG_FILTERSPEC_Guard(COMDLG_FILTERSPEC* specList) noexcept + : _specList(specList), index(0) {} + ~COMDLG_FILTERSPEC_Guard() { + for (--index; index != static_cast(-1); --index) { + NFDi_Free(const_cast(_specList[index].pszSpec)); + } + NFDi_Free(_specList); } + }; - if ( *p_filterList == ';' || *p_filterList == '\0' ) - { - /* end of filter -- add it to specList */ - - CopyNFDCharToWChar( specbuf, (wchar_t**)&specList[specIdx].pszName ); - CopyNFDCharToWChar( specbuf, (wchar_t**)&specList[specIdx].pszSpec ); - - memset( specbuf, 0, sizeof(char)*NFD_MAX_STRLEN ); - ++specIdx; - if ( specIdx == filterCount ) - break; + COMDLG_FILTERSPEC_Guard specListGuard(specList); + + if (filterCount) { + assert(filterList); + + // we have filters to add ... format and add them + + // use the index that comes from the RAII object (instead of making a copy), so the RAII + // object will know which memory to free + nfdfiltersize_t& index = specListGuard.index; + + for (; index != filterCount; ++index) { + // set the friendly name of this filter + specList[index].pszName = filterList[index].name; + + // set the specification of this filter... + + // count number of file extensions + size_t sep = 1; + for (const nfdnchar_t* p_spec = filterList[index].spec; *p_spec; ++p_spec) { + if (*p_spec == L',') { + ++sep; + } + } + + // calculate space needed (including the trailing '\0') + size_t specSize = sep * 2 + wcslen(filterList[index].spec) + 1; + + // malloc the required memory and populate it + nfdnchar_t* specBuf = NFDi_Malloc(sizeof(nfdnchar_t) * specSize); + + if (!specBuf) { + // automatic freeing of memory via COMDLG_FILTERSPEC_Guard + return NFD_ERROR; + } + + // convert "png,jpg" to "*.png;*.jpg" as required by Windows ... + nfdnchar_t* p_specBuf = specBuf; + *p_specBuf++ = L'*'; + *p_specBuf++ = L'.'; + for (const nfdnchar_t* p_spec = filterList[index].spec; *p_spec; ++p_spec) { + if (*p_spec == L',') { + *p_specBuf++ = L';'; + *p_specBuf++ = L'*'; + *p_specBuf++ = L'.'; + } else { + *p_specBuf++ = *p_spec; + } + } + *p_specBuf++ = L'\0'; + + // assert that we had allocated exactly the correct amount of memory that we used + assert(static_cast(p_specBuf - specBuf) == specSize); + + // save the buffer to the guard object + specList[index].pszSpec = specBuf; } - - if ( !NFDi_IsFilterSegmentChar( *p_filterList )) - { - *p_typebuf = *p_filterList; - ++p_typebuf; - } - - ++p_filterList; } /* Add wildcard */ - specList[specIdx].pszSpec = WILDCARD; - specList[specIdx].pszName = WILDCARD; - - fileOpenDialog->SetFileTypes( filterCount+1, specList ); + specList[filterCount].pszName = L"All files"; + specList[filterCount].pszSpec = L"*.*"; - /* free speclist */ - for ( size_t i = 0; i < filterCount; ++i ) - { - NFDi_Free( (void*)specList[i].pszSpec ); - } - NFDi_Free( specList ); - - return NFD_OKAY; -} - -static nfdresult_t AllocPathSet( IShellItemArray *shellItems, nfdpathset_t *pathSet ) -{ - const char ERRORMSG[] = "Error allocating pathset."; - - assert(shellItems); - assert(pathSet); - - // How many items in shellItems? - DWORD numShellItems; - HRESULT result = shellItems->GetCount(&numShellItems); - if ( !SUCCEEDED(result) ) - { - NFDi_SetError(ERRORMSG); + // add the filter to the dialog + if (!SUCCEEDED(fileOpenDialog->SetFileTypes(filterCount + 1, specList))) { + NFDi_SetError("Failed to set the allowable file types for the drop-down menu."); return NFD_ERROR; } - pathSet->count = static_cast(numShellItems); - assert( pathSet->count > 0 ); - - pathSet->indices = (size_t*)NFDi_Malloc( sizeof(size_t)*pathSet->count ); - if ( !pathSet->indices ) - { - return NFD_ERROR; - } - - /* count the total bytes needed for buf */ - size_t bufSize = 0; - for ( DWORD i = 0; i < numShellItems; ++i ) - { - ::IShellItem *shellItem; - result = shellItems->GetItemAt(i, &shellItem); - if ( !SUCCEEDED(result) ) - { - NFDi_SetError(ERRORMSG); - return NFD_ERROR; - } - - // Confirm SFGAO_FILESYSTEM is true for this shellitem, or ignore it. - SFGAOF attribs; - result = shellItem->GetAttributes( SFGAO_FILESYSTEM, &attribs ); - if ( !SUCCEEDED(result) ) - { - NFDi_SetError(ERRORMSG); - return NFD_ERROR; - } - if ( !(attribs & SFGAO_FILESYSTEM) ) - continue; - - LPWSTR name; - shellItem->GetDisplayName(SIGDN_FILESYSPATH, &name); - - // Calculate length of name with UTF-8 encoding - bufSize += GetUTF8ByteCountForWChar( name ); - - CoTaskMemFree(name); - } - - assert(bufSize); - - pathSet->buf = (nfdchar_t*)NFDi_Malloc( sizeof(nfdchar_t) * bufSize ); - memset( pathSet->buf, 0, sizeof(nfdchar_t) * bufSize ); - - /* fill buf */ - nfdchar_t *p_buf = pathSet->buf; - for (DWORD i = 0; i < numShellItems; ++i ) - { - ::IShellItem *shellItem; - result = shellItems->GetItemAt(i, &shellItem); - if ( !SUCCEEDED(result) ) - { - NFDi_SetError(ERRORMSG); - return NFD_ERROR; - } - - // Confirm SFGAO_FILESYSTEM is true for this shellitem, or ignore it. - SFGAOF attribs; - result = shellItem->GetAttributes( SFGAO_FILESYSTEM, &attribs ); - if ( !SUCCEEDED(result) ) - { - NFDi_SetError(ERRORMSG); - return NFD_ERROR; - } - if ( !(attribs & SFGAO_FILESYSTEM) ) - continue; - - LPWSTR name; - shellItem->GetDisplayName(SIGDN_FILESYSPATH, &name); - - int bytesWritten = CopyWCharToExistingNFDCharBuffer(name, p_buf); - CoTaskMemFree(name); - - ptrdiff_t index = p_buf - pathSet->buf; - assert( index >= 0 ); - pathSet->indices[i] = static_cast(index); - - p_buf += bytesWritten; - } - + // automatic freeing of memory via COMDLG_FILTERSPEC_Guard return NFD_OKAY; } - -static nfdresult_t SetDefaultPath( IFileDialog *dialog, const char *defaultPath ) -{ - if ( !defaultPath || strlen(defaultPath) == 0 ) +/* call after AddFiltersToDialog */ +nfdresult_t SetDefaultExtension(::IFileDialog* fileOpenDialog, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount) { + // if there are no filters, then don't set default extensions + if (!filterCount) { return NFD_OKAY; + } - wchar_t *defaultPathW = {0}; - CopyNFDCharToWChar( defaultPath, &defaultPathW ); + assert(filterList); - IShellItem *folder; - HRESULT result = SHCreateItemFromParsingName( defaultPathW, NULL, IID_PPV_ARGS(&folder) ); + // set the first item as the default index, and set the default extension + if (!SUCCEEDED(fileOpenDialog->SetFileTypeIndex(1))) { + NFDi_SetError("Failed to set the selected file type index."); + return NFD_ERROR; + } + + // set the first item as the default file extension + const nfdnchar_t* p_spec = filterList[0].spec; + for (; *p_spec; ++p_spec) { + if (*p_spec == ',') { + break; + } + } + if (*p_spec) { + // multiple file extensions for this type (need to allocate memory) + size_t numChars = p_spec - filterList[0].spec; + // allocate one more char space for the '\0' + nfdnchar_t* extnBuf = NFDi_Malloc(sizeof(nfdnchar_t) * (numChars + 1)); + if (!extnBuf) { + return NFD_ERROR; + } + Free_Guard extnBufGuard(extnBuf); + + // copy the extension + for (size_t i = 0; i != numChars; ++i) { + extnBuf[i] = filterList[0].spec[i]; + } + // pad with trailing '\0' + extnBuf[numChars] = L'\0'; + + if (!SUCCEEDED(fileOpenDialog->SetDefaultExtension(extnBuf))) { + NFDi_SetError("Failed to set default extension."); + return NFD_ERROR; + } + } else { + // single file extension for this type (no need to allocate memory) + if (!SUCCEEDED(fileOpenDialog->SetDefaultExtension(filterList[0].spec))) { + NFDi_SetError("Failed to set default extension."); + return NFD_ERROR; + } + } + + return NFD_OKAY; +} + +nfdresult_t SetDefaultPath(IFileDialog* dialog, const nfdnchar_t* defaultPath) { + if (!defaultPath || !*defaultPath) return NFD_OKAY; + + IShellItem* folder; + HRESULT result = SHCreateItemFromParsingName(defaultPath, nullptr, IID_PPV_ARGS(&folder)); // Valid non results. - if ( result == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || result == HRESULT_FROM_WIN32(ERROR_INVALID_DRIVE) ) - { - NFDi_Free( defaultPathW ); + if (result == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || + result == HRESULT_FROM_WIN32(ERROR_INVALID_DRIVE)) { return NFD_OKAY; } - if ( !SUCCEEDED(result) ) - { - NFDi_SetError("Error creating ShellItem"); - NFDi_Free( defaultPathW ); + if (!SUCCEEDED(result)) { + NFDi_SetError("Failed to create ShellItem for setting the default path."); + return NFD_ERROR; + } + + Release_Guard folderGuard(folder); + + // SetDefaultFolder() might use another recently used folder if available, so the user doesn't + // need to keep navigating back to the default folder (recommended by Windows). change to + // SetFolder() if you always want to use the default folder + if (!SUCCEEDED(dialog->SetDefaultFolder(folder))) { + NFDi_SetError("Failed to set default path."); return NFD_ERROR; } - - // Could also call SetDefaultFolder(), but this guarantees defaultPath -- more consistency across API. - dialog->SetFolder( folder ); - NFDi_Free( defaultPathW ); - folder->Release(); - return NFD_OKAY; } +nfdresult_t SetDefaultName(IFileDialog* dialog, const nfdnchar_t* defaultName) { + if (!defaultName || !*defaultName) return NFD_OKAY; + + if (!SUCCEEDED(dialog->SetFileName(defaultName))) { + NFDi_SetError("Failed to set default file name."); + return NFD_ERROR; + } + + return NFD_OKAY; +} + +nfdresult_t AddOptions(IFileDialog* dialog, FILEOPENDIALOGOPTIONS options) { + FILEOPENDIALOGOPTIONS existingOptions; + if (!SUCCEEDED(dialog->GetOptions(&existingOptions))) { + NFDi_SetError("Failed to get options."); + return NFD_ERROR; + } + if (!SUCCEEDED(dialog->SetOptions(existingOptions | options))) { + NFDi_SetError("Failed to set options."); + return NFD_ERROR; + } + return NFD_OKAY; +} +} // namespace + +const char* NFD_GetError(void) { + return g_errorstr; +} + +void NFD_ClearError(void) { + NFDi_SetError(nullptr); +} + /* public */ +namespace { +// The user might have initialized with COINIT_MULTITHREADED before, +// in which case we will fail to do CoInitializeEx(), but file dialogs will still work. +// See https://github.com/mlabbe/nativefiledialog/issues/72 for more information. +bool needs_uninitialize; +} // namespace -nfdresult_t NFD_OpenDialog( const nfdchar_t *filterList, - const nfdchar_t *defaultPath, - nfdchar_t **outPath, - void* owner ) -{ - HRESULT result; - nfdresult_t nfdResult = NFD_ERROR; - +nfdresult_t NFD_Init(void) { // Init COM library. - HRESULT coResult = ::CoInitializeEx(NULL, - ::COINIT_APARTMENTTHREADED | - ::COINIT_DISABLE_OLE1DDE ); + HRESULT result = + ::CoInitializeEx(nullptr, ::COINIT_APARTMENTTHREADED | ::COINIT_DISABLE_OLE1DDE); - ::IFileOpenDialog *fileOpenDialog(NULL); - - if ( !SUCCEEDED(coResult)) - { - fileOpenDialog = NULL; - NFDi_SetError("Could not initialize COM."); - goto end; + if (SUCCEEDED(result)) { + needs_uninitialize = true; + return NFD_OKAY; + } else if (result == RPC_E_CHANGED_MODE) { + // If this happens, the user already initialized COM using COINIT_MULTITHREADED, + // so COM will still work, but we shouldn't uninitialize it later. + needs_uninitialize = false; + return NFD_OKAY; + } else { + NFDi_SetError("Failed to initialize COM."); + return NFD_ERROR; } - - // Create dialog - result = ::CoCreateInstance(::CLSID_FileOpenDialog, NULL, - CLSCTX_ALL, ::IID_IFileOpenDialog, - reinterpret_cast(&fileOpenDialog) ); - - if ( !SUCCEEDED(result) ) - { - NFDi_SetError("Could not create dialog."); - goto end; - } - - // Build the filter list - if ( !AddFiltersToDialog( fileOpenDialog, filterList ) ) - { - goto end; - } - - // Set the default path - if ( !SetDefaultPath( fileOpenDialog, defaultPath ) ) - { - goto end; - } - - // Show the dialog. - result = fileOpenDialog->Show((HWND)owner); - if ( SUCCEEDED(result) ) - { - // Get the file name - ::IShellItem *shellItem(NULL); - result = fileOpenDialog->GetResult(&shellItem); - if ( !SUCCEEDED(result) ) - { - NFDi_SetError("Could not get shell item from dialog."); - goto end; - } - wchar_t *filePath(NULL); - result = shellItem->GetDisplayName(::SIGDN_FILESYSPATH, &filePath); - if ( !SUCCEEDED(result) ) - { - NFDi_SetError("Could not get file path for selected."); - shellItem->Release(); - goto end; - } - - CopyWCharToNFDChar( filePath, outPath ); - CoTaskMemFree(filePath); - if ( !*outPath ) - { - /* error is malloc-based, error message would be redundant */ - shellItem->Release(); - goto end; - } - - nfdResult = NFD_OKAY; - shellItem->Release(); - } - else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED) ) - { - nfdResult = NFD_CANCEL; - } - else - { - NFDi_SetError("File dialog box show failed."); - nfdResult = NFD_ERROR; - } - -end: - if (fileOpenDialog) - fileOpenDialog->Release(); - - if (SUCCEEDED(coResult)) - ::CoUninitialize(); - - return nfdResult; +} +void NFD_Quit(void) { + if (needs_uninitialize) ::CoUninitialize(); } -nfdresult_t NFD_OpenDialogMultiple( const nfdchar_t *filterList, - const nfdchar_t *defaultPath, - nfdpathset_t *outPaths ) -{ - nfdresult_t nfdResult = NFD_ERROR; - - // Init COM library. - HRESULT coResult = ::CoInitializeEx(NULL, - ::COINIT_APARTMENTTHREADED | - ::COINIT_DISABLE_OLE1DDE ); - if ( !SUCCEEDED(coResult)) - { - NFDi_SetError("Could not initialize COM."); +void NFD_FreePathN(nfdnchar_t* filePath) { + assert(filePath); + ::CoTaskMemFree(filePath); +} + +nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath) { + ::IFileOpenDialog* fileOpenDialog; + + // Create dialog + HRESULT result = ::CoCreateInstance(::CLSID_FileOpenDialog, + nullptr, + CLSCTX_ALL, + ::IID_IFileOpenDialog, + reinterpret_cast(&fileOpenDialog)); + + if (!SUCCEEDED(result)) { + NFDi_SetError("Could not create dialog."); return NFD_ERROR; } - ::IFileOpenDialog *fileOpenDialog(NULL); - - // Create dialog - HRESULT result = ::CoCreateInstance(::CLSID_FileOpenDialog, NULL, - CLSCTX_ALL, ::IID_IFileOpenDialog, - reinterpret_cast(&fileOpenDialog) ); - - if ( !SUCCEEDED(result) ) - { - fileOpenDialog = NULL; - NFDi_SetError("Could not create dialog."); - goto end; - } + // make sure we remember to free the dialog + Release_Guard<::IFileOpenDialog> fileOpenDialogGuard(fileOpenDialog); // Build the filter list - if ( !AddFiltersToDialog( fileOpenDialog, filterList ) ) - { - goto end; + if (!AddFiltersToDialog(fileOpenDialog, filterList, filterCount)) { + return NFD_ERROR; + } + + // Set auto-completed default extension + if (!SetDefaultExtension(fileOpenDialog, filterList, filterCount)) { + return NFD_ERROR; } // Set the default path - if ( !SetDefaultPath( fileOpenDialog, defaultPath ) ) - { - goto end; + if (!SetDefaultPath(fileOpenDialog, defaultPath)) { + return NFD_ERROR; } - // Set a flag for multiple options - DWORD dwFlags; - result = fileOpenDialog->GetOptions(&dwFlags); - if ( !SUCCEEDED(result) ) - { - NFDi_SetError("Could not get options."); - goto end; + // Only show file system items + if (!AddOptions(fileOpenDialog, ::FOS_FORCEFILESYSTEM)) { + return NFD_ERROR; } - result = fileOpenDialog->SetOptions(dwFlags | FOS_ALLOWMULTISELECT); - if ( !SUCCEEDED(result) ) - { - NFDi_SetError("Could not set options."); - goto end; - } - + // Show the dialog. - result = fileOpenDialog->Show(NULL); - if ( SUCCEEDED(result) ) - { - IShellItemArray *shellItems; - result = fileOpenDialog->GetResults( &shellItems ); - if ( !SUCCEEDED(result) ) - { + result = fileOpenDialog->Show(nullptr); + if (SUCCEEDED(result)) { + // Get the file name + ::IShellItem* psiResult; + result = fileOpenDialog->GetResult(&psiResult); + if (!SUCCEEDED(result)) { + NFDi_SetError("Could not get shell item from dialog."); + return NFD_ERROR; + } + Release_Guard<::IShellItem> psiResultGuard(psiResult); + + nfdnchar_t* filePath; + result = psiResult->GetDisplayName(::SIGDN_FILESYSPATH, &filePath); + if (!SUCCEEDED(result)) { + NFDi_SetError("Could not get file path from shell item returned by dialog."); + return NFD_ERROR; + } + + *outPath = filePath; + + return NFD_OKAY; + } else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED)) { + return NFD_CANCEL; + } else { + NFDi_SetError("File dialog box show failed."); + return NFD_ERROR; + } +} + +nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath) { + ::IFileOpenDialog* fileOpenDialog(nullptr); + + // Create dialog + HRESULT result = ::CoCreateInstance(::CLSID_FileOpenDialog, + nullptr, + CLSCTX_ALL, + ::IID_IFileOpenDialog, + reinterpret_cast(&fileOpenDialog)); + + if (!SUCCEEDED(result)) { + NFDi_SetError("Could not create dialog."); + return NFD_ERROR; + } + + // make sure we remember to free the dialog + Release_Guard<::IFileOpenDialog> fileOpenDialogGuard(fileOpenDialog); + + // Build the filter list + if (!AddFiltersToDialog(fileOpenDialog, filterList, filterCount)) { + return NFD_ERROR; + } + + // Set auto-completed default extension + if (!SetDefaultExtension(fileOpenDialog, filterList, filterCount)) { + return NFD_ERROR; + } + + // Set the default path + if (!SetDefaultPath(fileOpenDialog, defaultPath)) { + return NFD_ERROR; + } + + // Set a flag for multiple options and file system items only + if (!AddOptions(fileOpenDialog, ::FOS_FORCEFILESYSTEM | ::FOS_ALLOWMULTISELECT)) { + return NFD_ERROR; + } + + // Show the dialog. + result = fileOpenDialog->Show(nullptr); + if (SUCCEEDED(result)) { + ::IShellItemArray* shellItems; + result = fileOpenDialog->GetResults(&shellItems); + if (!SUCCEEDED(result)) { NFDi_SetError("Could not get shell items."); - goto end; - } - - if ( AllocPathSet( shellItems, outPaths ) == NFD_ERROR ) - { - shellItems->Release(); - goto end; + return NFD_ERROR; } - shellItems->Release(); - nfdResult = NFD_OKAY; - } - else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED) ) - { - nfdResult = NFD_CANCEL; - } - else - { + // save the path set to the output + *outPaths = static_cast(shellItems); + + return NFD_OKAY; + } else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED)) { + return NFD_CANCEL; + } else { NFDi_SetError("File dialog box show failed."); - nfdResult = NFD_ERROR; + return NFD_ERROR; } - -end: - if ( fileOpenDialog ) - fileOpenDialog->Release(); - - if (SUCCEEDED(coResult)) - ::CoUninitialize(); - - return nfdResult; } -nfdresult_t NFD_SaveDialog( const nfdchar_t *filterList, - const nfdchar_t *defaultPath, - nfdchar_t **outPath, - void* owner ) -{ - nfdresult_t nfdResult = NFD_ERROR; - - // Init COM library. - HRESULT coResult = ::CoInitializeEx(NULL, - ::COINIT_APARTMENTTHREADED | - ::COINIT_DISABLE_OLE1DDE ); - if ( !SUCCEEDED(coResult)) - { - NFDi_SetError("Could not initialize COM."); +nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath, + const nfdnchar_t* defaultName) { + ::IFileSaveDialog* fileSaveDialog; + + // Create dialog + HRESULT result = ::CoCreateInstance(::CLSID_FileSaveDialog, + nullptr, + CLSCTX_ALL, + ::IID_IFileSaveDialog, + reinterpret_cast(&fileSaveDialog)); + + if (!SUCCEEDED(result)) { + NFDi_SetError("Could not create dialog."); return NFD_ERROR; } - ::IFileSaveDialog *fileSaveDialog(NULL); - - // Create dialog - HRESULT result = ::CoCreateInstance(::CLSID_FileSaveDialog, NULL, - CLSCTX_ALL, ::IID_IFileSaveDialog, - reinterpret_cast(&fileSaveDialog) ); - - if ( !SUCCEEDED(result) ) - { - fileSaveDialog = NULL; - NFDi_SetError("Could not create dialog."); - goto end; - } + // make sure we remember to free the dialog + Release_Guard<::IFileSaveDialog> fileSaveDialogGuard(fileSaveDialog); // Build the filter list - if ( !AddFiltersToDialog( fileSaveDialog, filterList ) ) - { - goto end; + if (!AddFiltersToDialog(fileSaveDialog, filterList, filterCount)) { + return NFD_ERROR; + } + + // Set default extension + if (!SetDefaultExtension(fileSaveDialog, filterList, filterCount)) { + return NFD_ERROR; } // Set the default path - if ( !SetDefaultPath( fileSaveDialog, defaultPath ) ) - { - goto end; + if (!SetDefaultPath(fileSaveDialog, defaultPath)) { + return NFD_ERROR; + } + + // Set the default name + if (!SetDefaultName(fileSaveDialog, defaultName)) { + return NFD_ERROR; + } + + // Only show file system items + if (!AddOptions(fileSaveDialog, ::FOS_FORCEFILESYSTEM)) { + return NFD_ERROR; } // Show the dialog. - result = fileSaveDialog->Show((HWND)owner); - if ( SUCCEEDED(result) ) - { + result = fileSaveDialog->Show(nullptr); + if (SUCCEEDED(result)) { // Get the file name - ::IShellItem *shellItem; - result = fileSaveDialog->GetResult(&shellItem); - if ( !SUCCEEDED(result) ) - { + ::IShellItem* psiResult; + result = fileSaveDialog->GetResult(&psiResult); + if (!SUCCEEDED(result)) { NFDi_SetError("Could not get shell item from dialog."); - goto end; + return NFD_ERROR; } - wchar_t *filePath(NULL); - result = shellItem->GetDisplayName(::SIGDN_FILESYSPATH, &filePath); - if ( !SUCCEEDED(result) ) - { - shellItem->Release(); - NFDi_SetError("Could not get file path for selected."); - goto end; + Release_Guard<::IShellItem> psiResultGuard(psiResult); + + nfdnchar_t* filePath; + result = psiResult->GetDisplayName(::SIGDN_FILESYSPATH, &filePath); + if (!SUCCEEDED(result)) { + NFDi_SetError("Could not get file path from shell item returned by dialog."); + return NFD_ERROR; } - CopyWCharToNFDChar( filePath, outPath ); - CoTaskMemFree(filePath); - if ( !*outPath ) - { - /* error is malloc-based, error message would be redundant */ - shellItem->Release(); - goto end; - } + *outPath = filePath; - nfdResult = NFD_OKAY; - shellItem->Release(); - } - else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED) ) - { - nfdResult = NFD_CANCEL; - } - else - { + return NFD_OKAY; + } else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED)) { + return NFD_CANCEL; + } else { NFDi_SetError("File dialog box show failed."); - nfdResult = NFD_ERROR; + return NFD_ERROR; } - -end: - if ( fileSaveDialog ) - fileSaveDialog->Release(); - - if (SUCCEEDED(coResult)) - ::CoUninitialize(); - - return nfdResult; } -class AutoCoInit -{ -public: - AutoCoInit() - { - mResult = ::CoInitializeEx(NULL, - ::COINIT_APARTMENTTHREADED | - ::COINIT_DISABLE_OLE1DDE); - } +nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) { + ::IFileOpenDialog* fileOpenDialog; - ~AutoCoInit() - { - if (SUCCEEDED(mResult)) - { - ::CoUninitialize(); - } - } - - HRESULT Result() const { return mResult; } -private: - HRESULT mResult; -}; - -// VS2010 hasn't got a copy of CComPtr - this was first added in the 2003 SDK, so we make our own small CComPtr instead -template -class ComPtr -{ -public: - ComPtr() : mPtr(NULL) { } - ~ComPtr() - { - if (mPtr) - { - mPtr->Release(); - } - } - - T* Ptr() const { return mPtr; } - T** operator&() { return &mPtr; } - T* operator->() const { return mPtr; } -private: - // Don't allow copy or assignment - ComPtr(const ComPtr&); - ComPtr& operator = (const ComPtr&) const; - T* mPtr; -}; - -nfdresult_t NFD_PickFolder(const nfdchar_t *defaultPath, - nfdchar_t **outPath) -{ - // Init COM - AutoCoInit autoCoInit; - if (!SUCCEEDED(autoCoInit.Result())) - { - NFDi_SetError("CoInitializeEx failed."); + // Create dialog + if (!SUCCEEDED(::CoCreateInstance(::CLSID_FileOpenDialog, + nullptr, + CLSCTX_ALL, + ::IID_IFileOpenDialog, + reinterpret_cast(&fileOpenDialog)))) { + NFDi_SetError("Could not create dialog."); return NFD_ERROR; } - // Create the file dialog COM object - ComPtr pFileDialog; - if (!SUCCEEDED(CoCreateInstance(CLSID_FileOpenDialog, - NULL, - CLSCTX_ALL, - IID_PPV_ARGS(&pFileDialog)))) - { - NFDi_SetError("CoCreateInstance for CLSID_FileOpenDialog failed."); - return NFD_ERROR; - } + Release_Guard<::IFileOpenDialog> fileOpenDialogGuard(fileOpenDialog); // Set the default path - if (SetDefaultPath(pFileDialog.Ptr(), defaultPath) != NFD_OKAY) - { - NFDi_SetError("SetDefaultPath failed."); + if (!SetDefaultPath(fileOpenDialog, defaultPath)) { return NFD_ERROR; } - // Get the dialogs options - DWORD dwOptions = 0; - if (!SUCCEEDED(pFileDialog->GetOptions(&dwOptions))) - { - NFDi_SetError("GetOptions for IFileDialog failed."); - return NFD_ERROR; - } - - // Add in FOS_PICKFOLDERS which hides files and only allows selection of folders - if (!SUCCEEDED(pFileDialog->SetOptions(dwOptions | FOS_PICKFOLDERS))) - { - NFDi_SetError("SetOptions for IFileDialog failed."); + // Only show items that are folders and on the file system + if (!AddOptions(fileOpenDialog, ::FOS_FORCEFILESYSTEM | ::FOS_PICKFOLDERS)) { return NFD_ERROR; } // Show the dialog to the user - const HRESULT result = pFileDialog->Show(NULL); - if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED)) - { + const HRESULT result = fileOpenDialog->Show(nullptr); + if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED)) { return NFD_CANCEL; - } - else if (!SUCCEEDED(result)) - { - NFDi_SetError("Show for IFileDialog failed."); + } else if (!SUCCEEDED(result)) { + NFDi_SetError("File dialog box show failed."); return NFD_ERROR; } // Get the shell item result - ComPtr pShellItem; - if (!SUCCEEDED(pFileDialog->GetResult(&pShellItem))) - { - NFDi_SetError("Could not get shell item from dialog."); + ::IShellItem* psiResult; + if (!SUCCEEDED(fileOpenDialog->GetResult(&psiResult))) { return NFD_ERROR; } + Release_Guard<::IShellItem> psiResultGuard(psiResult); + // Finally get the path - wchar_t *path = NULL; - if (!SUCCEEDED(pShellItem->GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, &path))) - { - NFDi_SetError("GetDisplayName for IShellItem failed."); + nfdnchar_t* filePath; + // Why are we not using SIGDN_FILESYSPATH? + if (!SUCCEEDED(psiResult->GetDisplayName(::SIGDN_DESKTOPABSOLUTEPARSING, &filePath))) { + NFDi_SetError("Could not get file path from shell item returned by dialog."); return NFD_ERROR; } - // Convert string - CopyWCharToNFDChar(path, outPath); - CoTaskMemFree(path); - if (!*outPath) - { - // error is malloc-based, error message would be redundant - return NFD_ERROR; - } + *outPath = filePath; return NFD_OKAY; } + +nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count) { + assert(pathSet); + // const_cast because methods on IShellItemArray aren't const, but it should act like const to + // the caller + ::IShellItemArray* psiaPathSet = + const_cast<::IShellItemArray*>(static_cast(pathSet)); + + DWORD numPaths; + if (!SUCCEEDED(psiaPathSet->GetCount(&numPaths))) { + NFDi_SetError("Could not get path count."); + return NFD_ERROR; + } + *count = numPaths; + return NFD_OKAY; +} + +nfdresult_t NFD_PathSet_GetPathN(const nfdpathset_t* pathSet, + nfdpathsetsize_t index, + nfdnchar_t** outPath) { + assert(pathSet); + // const_cast because methods on IShellItemArray aren't const, but it should act like const to + // the caller + ::IShellItemArray* psiaPathSet = + const_cast<::IShellItemArray*>(static_cast(pathSet)); + + ::IShellItem* psiPath; + if (!SUCCEEDED(psiaPathSet->GetItemAt(index, &psiPath))) { + NFDi_SetError("Could not get shell item."); + return NFD_ERROR; + } + + Release_Guard<::IShellItem> psiPathGuard(psiPath); + + nfdnchar_t* name; + if (!SUCCEEDED(psiPath->GetDisplayName(::SIGDN_FILESYSPATH, &name))) { + NFDi_SetError("Could not get file path from shell item."); + return NFD_ERROR; + } + + *outPath = name; + return NFD_OKAY; +} + +nfdresult_t NFD_PathSet_GetEnum(const nfdpathset_t* pathSet, nfdpathsetenum_t* outEnumerator) { + assert(pathSet); + // const_cast because methods on IShellItemArray aren't const, but it should act like const to + // the caller + ::IShellItemArray* psiaPathSet = + const_cast<::IShellItemArray*>(static_cast(pathSet)); + + ::IEnumShellItems* pesiPaths; + if (!SUCCEEDED(psiaPathSet->EnumItems(&pesiPaths))) { + NFDi_SetError("Could not get enumerator."); + return NFD_ERROR; + } + + outEnumerator->ptr = static_cast(pesiPaths); + return NFD_OKAY; +} + +void NFD_PathSet_FreeEnum(nfdpathsetenum_t* enumerator) { + assert(enumerator->ptr); + + ::IEnumShellItems* pesiPaths = static_cast<::IEnumShellItems*>(enumerator->ptr); + + // free the enumerator memory + pesiPaths->Release(); +} + +nfdresult_t NFD_PathSet_EnumNextN(nfdpathsetenum_t* enumerator, nfdnchar_t** outPath) { + assert(enumerator->ptr); + + ::IEnumShellItems* pesiPaths = static_cast<::IEnumShellItems*>(enumerator->ptr); + + ::IShellItem* psiPath; + HRESULT res = pesiPaths->Next(1, &psiPath, NULL); + if (!SUCCEEDED(res)) { + NFDi_SetError("Could not get next item of enumerator."); + return NFD_ERROR; + } + if (res != S_OK) { + *outPath = nullptr; + return NFD_OKAY; + } + + Release_Guard<::IShellItem> psiPathGuard(psiPath); + + nfdnchar_t* name; + if (!SUCCEEDED(psiPath->GetDisplayName(::SIGDN_FILESYSPATH, &name))) { + NFDi_SetError("Could not get file path from shell item."); + return NFD_ERROR; + } + + *outPath = name; + return NFD_OKAY; +} + +void NFD_PathSet_Free(const nfdpathset_t* pathSet) { + assert(pathSet); + // const_cast because methods on IShellItemArray aren't const, but it should act like const to + // the caller + ::IShellItemArray* psiaPathSet = + const_cast<::IShellItemArray*>(static_cast(pathSet)); + + // free the path set memory + psiaPathSet->Release(); +} + +namespace { +// allocs the space in outStr -- call NFDi_Free() +nfdresult_t CopyCharToWChar(const nfdu8char_t* inStr, nfdnchar_t*& outStr) { + int charsNeeded = MultiByteToWideChar(CP_UTF8, 0, inStr, -1, nullptr, 0); + assert(charsNeeded); + + nfdnchar_t* tmp_outStr = NFDi_Malloc(sizeof(nfdnchar_t) * charsNeeded); + if (!tmp_outStr) { + return NFD_ERROR; + } + + int ret = MultiByteToWideChar(CP_UTF8, 0, inStr, -1, tmp_outStr, charsNeeded); + assert(ret && ret == charsNeeded); + (void)ret; // prevent warning in release build + outStr = tmp_outStr; + return NFD_OKAY; +} + +// allocs the space in outPath -- call NFDi_Free() +nfdresult_t CopyWCharToNFDChar(const nfdnchar_t* inStr, nfdu8char_t*& outStr) { + int bytesNeeded = WideCharToMultiByte(CP_UTF8, 0, inStr, -1, nullptr, 0, nullptr, nullptr); + assert(bytesNeeded); + + nfdu8char_t* tmp_outStr = NFDi_Malloc(sizeof(nfdu8char_t) * bytesNeeded); + if (!tmp_outStr) { + return NFD_ERROR; + } + + int ret = WideCharToMultiByte(CP_UTF8, 0, inStr, -1, tmp_outStr, bytesNeeded, nullptr, nullptr); + assert(ret && ret == bytesNeeded); + (void)ret; // prevent warning in release build + outStr = tmp_outStr; + return NFD_OKAY; +} + +struct FilterItem_Guard { + nfdnfilteritem_t* data; + nfdfiltersize_t index; + FilterItem_Guard() noexcept : data(nullptr), index(0) {} + ~FilterItem_Guard() { + assert(data || index == 0); + for (--index; index != static_cast(-1); --index) { + NFDi_Free(const_cast(data[index].spec)); + NFDi_Free(const_cast(data[index].name)); + } + if (data) NFDi_Free(data); + } +}; + +nfdresult_t CopyFilterItem(const nfdu8filteritem_t* filterList, + nfdfiltersize_t count, + FilterItem_Guard& filterItemsNGuard) { + if (count) { + nfdnfilteritem_t*& filterItemsN = filterItemsNGuard.data; + filterItemsN = NFDi_Malloc(sizeof(nfdnfilteritem_t) * count); + if (!filterItemsN) { + return NFD_ERROR; + } + + nfdfiltersize_t& index = filterItemsNGuard.index; + for (; index != count; ++index) { + nfdresult_t res = CopyCharToWChar(filterList[index].name, + const_cast(filterItemsN[index].name)); + if (!res) { + return NFD_ERROR; + } + res = CopyCharToWChar(filterList[index].spec, + const_cast(filterItemsN[index].spec)); + if (!res) { + // remember to free the name, because we also created it (and it won't be protected + // by the guard, because we have not incremented the index) + NFDi_Free(const_cast(filterItemsN[index].name)); + return NFD_ERROR; + } + } + } + return NFD_OKAY; +} +nfdresult_t ConvertU8ToNative(const nfdu8char_t* u8Text, FreeCheck_Guard& nativeText) { + if (u8Text) { + nfdresult_t res = CopyCharToWChar(u8Text, nativeText.data); + if (!res) { + return NFD_ERROR; + } + } + return NFD_OKAY; +} +void NormalizePathSeparator(nfdnchar_t* path) { + if (path) { + for (; *path; ++path) { + if (*path == L'/') *path = L'\\'; + } + } +} +} // namespace + +void NFD_FreePathU8(nfdu8char_t* outPath) { + NFDi_Free(outPath); +} + +nfdresult_t NFD_OpenDialogU8(nfdu8char_t** outPath, + const nfdu8filteritem_t* filterList, + nfdfiltersize_t count, + const nfdu8char_t* defaultPath) { + // populate the real nfdnfilteritem_t + FilterItem_Guard filterItemsNGuard; + if (!CopyFilterItem(filterList, count, filterItemsNGuard)) { + return NFD_ERROR; + } + + // convert and normalize the default path, but only if it is not nullptr + FreeCheck_Guard defaultPathNGuard; + ConvertU8ToNative(defaultPath, defaultPathNGuard); + NormalizePathSeparator(defaultPathNGuard.data); + + // call the native function + nfdnchar_t* outPathN; + nfdresult_t res = + NFD_OpenDialogN(&outPathN, filterItemsNGuard.data, count, defaultPathNGuard.data); + + if (res != NFD_OKAY) { + return res; + } + + // convert the outPath to UTF-8 + res = CopyWCharToNFDChar(outPathN, *outPath); + + // free the native out path, and return the result + NFD_FreePathN(outPathN); + return res; +} + +/* multiple file open dialog */ +/* It is the caller's responsibility to free `outPaths` via NFD_PathSet_Free() if this function + * returns NFD_OKAY */ +nfdresult_t NFD_OpenDialogMultipleU8(const nfdpathset_t** outPaths, + const nfdu8filteritem_t* filterList, + nfdfiltersize_t count, + const nfdu8char_t* defaultPath) { + // populate the real nfdnfilteritem_t + FilterItem_Guard filterItemsNGuard; + if (!CopyFilterItem(filterList, count, filterItemsNGuard)) { + return NFD_ERROR; + } + + // convert and normalize the default path, but only if it is not nullptr + FreeCheck_Guard defaultPathNGuard; + ConvertU8ToNative(defaultPath, defaultPathNGuard); + NormalizePathSeparator(defaultPathNGuard.data); + + // call the native function + return NFD_OpenDialogMultipleN(outPaths, filterItemsNGuard.data, count, defaultPathNGuard.data); +} + +/* save dialog */ +/* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns + * NFD_OKAY */ +nfdresult_t NFD_SaveDialogU8(nfdu8char_t** outPath, + const nfdu8filteritem_t* filterList, + nfdfiltersize_t count, + const nfdu8char_t* defaultPath, + const nfdu8char_t* defaultName) { + // populate the real nfdnfilteritem_t + FilterItem_Guard filterItemsNGuard; + if (!CopyFilterItem(filterList, count, filterItemsNGuard)) { + return NFD_ERROR; + } + + // convert and normalize the default path, but only if it is not nullptr + FreeCheck_Guard defaultPathNGuard; + ConvertU8ToNative(defaultPath, defaultPathNGuard); + NormalizePathSeparator(defaultPathNGuard.data); + + // convert the default name, but only if it is not nullptr + FreeCheck_Guard defaultNameNGuard; + ConvertU8ToNative(defaultName, defaultNameNGuard); + + // call the native function + nfdnchar_t* outPathN; + nfdresult_t res = NFD_SaveDialogN( + &outPathN, filterItemsNGuard.data, count, defaultPathNGuard.data, defaultNameNGuard.data); + + if (res != NFD_OKAY) { + return res; + } + + // convert the outPath to UTF-8 + res = CopyWCharToNFDChar(outPathN, *outPath); + + // free the native out path, and return the result + NFD_FreePathN(outPathN); + return res; +} + +/* select folder dialog */ +/* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns + * NFD_OKAY */ +nfdresult_t NFD_PickFolderU8(nfdu8char_t** outPath, const nfdu8char_t* defaultPath) { + // convert and normalize the default path, but only if it is not nullptr + FreeCheck_Guard defaultPathNGuard; + ConvertU8ToNative(defaultPath, defaultPathNGuard); + NormalizePathSeparator(defaultPathNGuard.data); + + // call the native function + nfdnchar_t* outPathN; + nfdresult_t res = NFD_PickFolderN(&outPathN, defaultPathNGuard.data); + + if (res != NFD_OKAY) { + return res; + } + + // convert the outPath to UTF-8 + res = CopyWCharToNFDChar(outPathN, *outPath); + + // free the native out path, and return the result + NFD_FreePathN(outPathN); + return res; +} + +/* Get the UTF-8 path at offset index */ +/* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns + * NFD_OKAY */ +nfdresult_t NFD_PathSet_GetPathU8(const nfdpathset_t* pathSet, + nfdpathsetsize_t index, + nfdu8char_t** outPath) { + // call the native function + nfdnchar_t* outPathN; + nfdresult_t res = NFD_PathSet_GetPathN(pathSet, index, &outPathN); + + if (res != NFD_OKAY) { + return res; + } + + // convert the outPath to UTF-8 + res = CopyWCharToNFDChar(outPathN, *outPath); + + // free the native out path, and return the result + NFD_FreePathN(outPathN); + return res; +} + +nfdresult_t NFD_PathSet_EnumNextU8(nfdpathsetenum_t* enumerator, nfdu8char_t** outPath) { + // call the native function + nfdnchar_t* outPathN; + nfdresult_t res = NFD_PathSet_EnumNextN(enumerator, &outPathN); + + if (res != NFD_OKAY) { + return res; + } + + if (outPathN) { + // convert the outPath to UTF-8 + res = CopyWCharToNFDChar(outPathN, *outPath); + + // free the native out path, and return the result + NFD_FreePathN(outPathN); + } else { + *outPath = nullptr; + res = NFD_OKAY; + } + + return res; +}