From 53de8bdd6585345545dbc59e1d25f4ce0dc8adb6 Mon Sep 17 00:00:00 2001 From: Bernard Teo Date: Sun, 23 Jun 2024 17:56:05 +0800 Subject: [PATCH 01/10] Add versioned API (#128) --- src/include/nfd.h | 178 ++++++++++++++++++++++++++++- src/include/nfd.hpp | 30 +++-- src/nfd_cocoa.m | 91 +++++++++++++-- src/nfd_gtk.cpp | 87 ++++++++++++-- src/nfd_portal.cpp | 87 ++++++++++++-- src/nfd_win.cpp | 165 +++++++++++++++++++++----- test/CMakeLists.txt | 8 +- test/test_opendialog_native_with.c | 52 +++++++++ test/test_opendialog_with.c | 39 +++++++ test/test_pickfolder_native_with.c | 43 +++++++ test/test_pickfolder_with.c | 34 ++++++ test/test_savedialog_native_with.c | 59 ++++++++++ test/test_savedialog_with.c | 40 +++++++ 13 files changed, 846 insertions(+), 67 deletions(-) create mode 100644 test/test_opendialog_native_with.c create mode 100644 test/test_opendialog_with.c create mode 100644 test/test_pickfolder_native_with.c create mode 100644 test/test_pickfolder_with.c create mode 100644 test/test_savedialog_native_with.c create mode 100644 test/test_savedialog_with.c diff --git a/src/include/nfd.h b/src/include/nfd.h index 6ce848ff..4c4b12a5 100644 --- a/src/include/nfd.h +++ b/src/include/nfd.h @@ -29,6 +29,9 @@ #ifdef __cplusplus extern "C" { +#define NFD_INLINE inline +#else +#define NFD_INLINE static inline #endif // __cplusplus #include @@ -93,6 +96,59 @@ typedef struct { typedef nfdu8filteritem_t nfdnfilteritem_t; #endif // _WIN32 +typedef size_t nfdversion_t; + +typedef struct { + const nfdu8filteritem_t* filterList; + nfdfiltersize_t filterCount; + const nfdu8char_t* defaultPath; +} nfdopendialogu8args_t; + +#ifdef _WIN32 +typedef struct { + const nfdnfilteritem_t* filterList; + nfdfiltersize_t filterCount; + const nfdnchar_t* defaultPath; +} nfdopendialognargs_t; +#else +typedef nfdopendialogu8args_t nfdopendialognargs_t; +#endif // _WIN32 + +typedef struct { + const nfdu8filteritem_t* filterList; + nfdfiltersize_t filterCount; + const nfdu8char_t* defaultPath; + const nfdu8char_t* defaultName; +} nfdsavedialogu8args_t; + +#ifdef _WIN32 +typedef struct { + const nfdnfilteritem_t* filterList; + nfdfiltersize_t filterCount; + const nfdnchar_t* defaultPath; + const nfdnchar_t* defaultName; +} nfdsavedialognargs_t; +#else +typedef nfdsavedialogu8args_t nfdsavedialognargs_t; +#endif // _WIN32 + +typedef struct { + const nfdu8char_t* defaultPath; +} nfdpickfolderu8args_t; + +#ifdef _WIN32 +typedef struct { + const nfdnchar_t* defaultPath; +} nfdpickfoldernargs_t; +#else +typedef nfdpickfolderu8args_t nfdpickfoldernargs_t; +#endif // _WIN32 + +// This is a unique identifier tagged to all the NFD_*With() function calls, for backward +// compatibility purposes. There is usually no need to use this directly, unless you want to use +// NFD differently depending on the version you're building with. +#define NFD_INTERFACE_VERSION 1 + /** Free a file path that was returned by the dialogs. * * Note: use NFD_PathSet_FreePathN() to free path from pathset instead of this function. */ @@ -134,6 +190,35 @@ NFD_API nfdresult_t NFD_OpenDialogU8(nfdu8char_t** outPath, nfdfiltersize_t filterCount, const nfdu8char_t* defaultPath); +/** This function is a library implementation detail. Please use NFD_OpenDialogN_With() instead. */ +NFD_API nfdresult_t NFD_OpenDialogN_With_Impl(nfdversion_t version, + nfdnchar_t** outPath, + const nfdopendialognargs_t* args); + +/** Single file open dialog, with additional parameters. + * + * It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function + * returns NFD_OKAY. See documentation of nfdopendialognargs_t for details. */ +NFD_INLINE nfdresult_t NFD_OpenDialogN_With(nfdnchar_t** outPath, + const nfdopendialognargs_t* args) { + return NFD_OpenDialogN_With_Impl(NFD_INTERFACE_VERSION, outPath, args); +} + +/** This function is a library implementation detail. Please use NFD_OpenDialogU8_With() instead. + */ +NFD_API nfdresult_t NFD_OpenDialogU8_With_Impl(nfdversion_t version, + nfdu8char_t** outPath, + const nfdopendialogu8args_t* args); + +/** Single file open dialog, with additional parameters. + * + * It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function + * returns NFD_OKAY. See documentation of nfdopendialogu8args_t for details. */ +NFD_INLINE nfdresult_t NFD_OpenDialogU8_With(nfdu8char_t** outPath, + const nfdopendialogu8args_t* args) { + return NFD_OpenDialogU8_With_Impl(NFD_INTERFACE_VERSION, outPath, args); +} + /** Multiple file open dialog * * It is the caller's responsibility to free `outPaths` via NFD_PathSet_FreeN() if this function @@ -158,6 +243,36 @@ NFD_API nfdresult_t NFD_OpenDialogMultipleU8(const nfdpathset_t** outPaths, nfdfiltersize_t filterCount, const nfdu8char_t* defaultPath); +/** This function is a library implementation detail. Please use NFD_OpenDialogMultipleN_With() + * instead. */ +NFD_API nfdresult_t NFD_OpenDialogMultipleN_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdopendialognargs_t* args); + +/** Multiple file open dialog, with additional parameters. + * + * It is the caller's responsibility to free `outPaths` via NFD_PathSet_FreeN() if this function + * returns NFD_OKAY. See documentation of nfdopendialognargs_t for details. */ +NFD_INLINE nfdresult_t NFD_OpenDialogMultipleN_With(const nfdpathset_t** outPaths, + const nfdopendialognargs_t* args) { + return NFD_OpenDialogMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, args); +} + +/** This function is a library implementation detail. Please use NFD_OpenDialogU8_With() instead. + */ +NFD_API nfdresult_t NFD_OpenDialogMultipleU8_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdopendialogu8args_t* args); + +/** Multiple file open dialog, with additional parameters. + * + * It is the caller's responsibility to free `outPaths` via NFD_PathSet_FreeU8() if this function + * returns NFD_OKAY. See documentation of nfdopendialogu8args_t for details. */ +NFD_INLINE nfdresult_t NFD_OpenDialogMultipleU8_With(const nfdpathset_t** outPaths, + const nfdopendialogu8args_t* args) { + return NFD_OpenDialogMultipleU8_With_Impl(NFD_INTERFACE_VERSION, outPaths, args); +} + /** Save dialog * * It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function returns @@ -184,7 +299,36 @@ NFD_API nfdresult_t NFD_SaveDialogU8(nfdu8char_t** outPath, const nfdu8char_t* defaultPath, const nfdu8char_t* defaultName); -/** Select folder dialog +/** This function is a library implementation detail. Please use NFD_SaveDialogN_With() instead. */ +NFD_API nfdresult_t NFD_SaveDialogN_With_Impl(nfdversion_t version, + nfdnchar_t** outPath, + const nfdsavedialognargs_t* args); + +/** Single file save dialog, with additional parameters. + * + * It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function + * returns NFD_OKAY. See documentation of nfdsavedialognargs_t for details. */ +NFD_INLINE nfdresult_t NFD_SaveDialogN_With(nfdnchar_t** outPath, + const nfdsavedialognargs_t* args) { + return NFD_SaveDialogN_With_Impl(NFD_INTERFACE_VERSION, outPath, args); +} + +/** This function is a library implementation detail. Please use NFD_SaveDialogU8_With() instead. + */ +NFD_API nfdresult_t NFD_SaveDialogU8_With_Impl(nfdversion_t version, + nfdu8char_t** outPath, + const nfdsavedialogu8args_t* args); + +/** Single file save dialog, with additional parameters. + * + * It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function + * returns NFD_OKAY. See documentation of nfdsavedialogu8args_t for details. */ +NFD_INLINE nfdresult_t NFD_SaveDialogU8_With(nfdu8char_t** outPath, + const nfdsavedialogu8args_t* args) { + return NFD_SaveDialogU8_With_Impl(NFD_INTERFACE_VERSION, outPath, args); +} + +/** Select single folder dialog * * It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function returns * NFD_OKAY. @@ -192,7 +336,7 @@ NFD_API nfdresult_t NFD_SaveDialogU8(nfdu8char_t** outPath, * @param defaultPath If null, the operating system will decide. */ NFD_API nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath); -/** Select folder dialog +/** Select single folder dialog * * It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function * returns NFD_OKAY. @@ -200,6 +344,35 @@ NFD_API nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defa * @param defaultPath If null, the operating system will decide. */ NFD_API nfdresult_t NFD_PickFolderU8(nfdu8char_t** outPath, const nfdu8char_t* defaultPath); +/** This function is a library implementation detail. Please use NFD_PickFolderN_With() instead. */ +NFD_API nfdresult_t NFD_PickFolderN_With_Impl(nfdversion_t version, + nfdnchar_t** outPath, + const nfdpickfoldernargs_t* args); + +/** Select single folder dialog, with additional parameters. + * + * It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function + * returns NFD_OKAY. See documentation of nfdpickfoldernargs_t for details. */ +NFD_INLINE nfdresult_t NFD_PickFolderN_With(nfdnchar_t** outPath, + const nfdpickfoldernargs_t* args) { + return NFD_PickFolderN_With_Impl(NFD_INTERFACE_VERSION, outPath, args); +} + +/** This function is a library implementation detail. Please use NFD_PickFolderU8_With() instead. + */ +NFD_API nfdresult_t NFD_PickFolderU8_With_Impl(nfdversion_t version, + nfdu8char_t** outPath, + const nfdpickfolderu8args_t* args); + +/** Select single folder dialog, with additional parameters. + * + * It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function + * returns NFD_OKAY. See documentation of nfdpickfolderu8args_t for details. */ +NFD_INLINE nfdresult_t NFD_PickFolderU8_With(nfdu8char_t** outPath, + const nfdpickfolderu8args_t* args) { + return NFD_PickFolderU8_With_Impl(NFD_INTERFACE_VERSION, outPath, args); +} + /** Get the last error * * This is set when a function returns NFD_ERROR. @@ -308,6 +481,7 @@ typedef nfdu8filteritem_t nfdfilteritem_t; #define NFD_PathSet_EnumNext NFD_PathSet_EnumNextU8 #endif // NFD_NATIVE +#undef NFD_INLINE #ifdef __cplusplus } #endif // __cplusplus diff --git a/src/include/nfd.hpp b/src/include/nfd.hpp index bbce108a..ab915433 100644 --- a/src/include/nfd.hpp +++ b/src/include/nfd.hpp @@ -38,14 +38,16 @@ inline nfdresult_t OpenDialog(nfdnchar_t*& outPath, const nfdnfilteritem_t* filterList = nullptr, nfdfiltersize_t filterCount = 0, const nfdnchar_t* defaultPath = nullptr) noexcept { - return ::NFD_OpenDialogN(&outPath, filterList, filterCount, defaultPath); + const nfdopendialognargs_t args{filterList, filterCount, defaultPath}; + return ::NFD_OpenDialogN_With(&outPath, &args); } inline nfdresult_t OpenDialogMultiple(const nfdpathset_t*& outPaths, const nfdnfilteritem_t* filterList = nullptr, nfdfiltersize_t filterCount = 0, const nfdnchar_t* defaultPath = nullptr) noexcept { - return ::NFD_OpenDialogMultipleN(&outPaths, filterList, filterCount, defaultPath); + const nfdopendialognargs_t args{filterList, filterCount, defaultPath}; + return ::NFD_OpenDialogMultipleN_With(&outPaths, &args); } inline nfdresult_t SaveDialog(nfdnchar_t*& outPath, @@ -53,12 +55,14 @@ inline nfdresult_t SaveDialog(nfdnchar_t*& outPath, nfdfiltersize_t filterCount = 0, const nfdnchar_t* defaultPath = nullptr, const nfdnchar_t* defaultName = nullptr) noexcept { - return ::NFD_SaveDialogN(&outPath, filterList, filterCount, defaultPath, defaultName); + const nfdsavedialognargs_t args{filterList, filterCount, defaultPath, defaultName}; + return ::NFD_SaveDialogN_With(&outPath, &args); } inline nfdresult_t PickFolder(nfdnchar_t*& outPath, const nfdnchar_t* defaultPath = nullptr) noexcept { - return ::NFD_PickFolderN(&outPath, defaultPath); + const nfdpickfoldernargs_t args{defaultPath}; + return ::NFD_PickFolderN_With(&outPath, &args); } inline const char* GetError() noexcept { @@ -99,29 +103,33 @@ inline void FreePath(nfdu8char_t* outPath) noexcept { inline nfdresult_t OpenDialog(nfdu8char_t*& outPath, const nfdu8filteritem_t* filterList = nullptr, - nfdfiltersize_t count = 0, + nfdfiltersize_t filterCount = 0, const nfdu8char_t* defaultPath = nullptr) noexcept { - return ::NFD_OpenDialogU8(&outPath, filterList, count, defaultPath); + const nfdopendialogu8args_t args{filterList, filterCount, defaultPath}; + return ::NFD_OpenDialogU8_With(&outPath, &args); } inline nfdresult_t OpenDialogMultiple(const nfdpathset_t*& outPaths, const nfdu8filteritem_t* filterList = nullptr, - nfdfiltersize_t count = 0, + nfdfiltersize_t filterCount = 0, const nfdu8char_t* defaultPath = nullptr) noexcept { - return ::NFD_OpenDialogMultipleU8(&outPaths, filterList, count, defaultPath); + const nfdopendialogu8args_t args{filterList, filterCount, defaultPath}; + return ::NFD_OpenDialogMultipleU8_With(&outPaths, &args); } inline nfdresult_t SaveDialog(nfdu8char_t*& outPath, const nfdu8filteritem_t* filterList = nullptr, - nfdfiltersize_t count = 0, + nfdfiltersize_t filterCount = 0, const nfdu8char_t* defaultPath = nullptr, const nfdu8char_t* defaultName = nullptr) noexcept { - return ::NFD_SaveDialogU8(&outPath, filterList, count, defaultPath, defaultName); + const nfdsavedialogu8args_t args{filterList, filterCount, defaultPath, defaultName}; + return ::NFD_SaveDialogU8_With(&outPath, &args); } inline nfdresult_t PickFolder(nfdu8char_t*& outPath, const nfdu8char_t* defaultPath = nullptr) noexcept { - return ::NFD_PickFolderU8(&outPath, defaultPath); + const nfdpickfolderu8args_t args{defaultPath}; + return ::NFD_PickFolderU8_With(&outPath, &args); } namespace PathSet { diff --git a/src/nfd_cocoa.m b/src/nfd_cocoa.m index babd6d48..cde6e85d 100644 --- a/src/nfd_cocoa.m +++ b/src/nfd_cocoa.m @@ -215,6 +215,19 @@ nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, const nfdnchar_t* defaultPath) { + nfdopendialognargs_t args = {0}; + args.filterList = filterList; + args.filterCount = filterCount; + args.defaultPath = defaultPath; + return NFD_OpenDialogN_With_Impl(NFD_INTERFACE_VERSION, outPath, &args); +} + +nfdresult_t NFD_OpenDialogN_With_Impl(nfdversion_t version, + nfdnchar_t** outPath, + const nfdopendialognargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + nfdresult_t result = NFD_CANCEL; @autoreleasepool { NSWindow* keyWindow = [[NSApplication sharedApplication] keyWindow]; @@ -223,10 +236,10 @@ nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath, [dialog setAllowsMultipleSelection:NO]; // Build the filter list - AddFilterListToDialog(dialog, filterList, filterCount); + AddFilterListToDialog(dialog, args->filterList, args->filterCount); // Set the starting directory - SetDefaultPath(dialog, defaultPath); + SetDefaultPath(dialog, args->defaultPath); if ([dialog runModal] == NSModalResponseOK) { const NSURL* url = [dialog URL]; @@ -247,10 +260,29 @@ nfdresult_t NFD_OpenDialogU8(nfdu8char_t** outPath, return NFD_OpenDialogN(outPath, filterList, filterCount, defaultPath); } +nfdresult_t NFD_OpenDialogU8_With_Impl(nfdversion_t version, + nfdu8char_t** outPath, + const nfdopendialogu8args_t* args) { + return NFD_OpenDialogN_With_Impl(version, outPath, args); +} + nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, const nfdnchar_t* defaultPath) { + nfdopendialognargs_t args = {0}; + args.filterList = filterList; + args.filterCount = filterCount; + args.defaultPath = defaultPath; + return NFD_OpenDialogMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, &args); +} + +nfdresult_t NFD_OpenDialogMultipleN_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdopendialognargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + nfdresult_t result = NFD_CANCEL; @autoreleasepool { NSWindow* keyWindow = [[NSApplication sharedApplication] keyWindow]; @@ -259,10 +291,10 @@ nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, [dialog setAllowsMultipleSelection:YES]; // Build the filter list - AddFilterListToDialog(dialog, filterList, filterCount); + AddFilterListToDialog(dialog, args->filterList, args->filterCount); // Set the starting directory - SetDefaultPath(dialog, defaultPath); + SetDefaultPath(dialog, args->defaultPath); if ([dialog runModal] == NSModalResponseOK) { const NSArray* urls = [dialog URLs]; @@ -288,11 +320,31 @@ nfdresult_t NFD_OpenDialogMultipleU8(const nfdpathset_t** outPaths, return NFD_OpenDialogMultipleN(outPaths, filterList, filterCount, defaultPath); } +nfdresult_t NFD_OpenDialogMultipleU8_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdopendialogu8args_t* args) { + return NFD_OpenDialogMultipleN_With_Impl(version, outPaths, args); +} + nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, const nfdnchar_t* defaultPath, const nfdnchar_t* defaultName) { + nfdsavedialognargs_t args = {0}; + args.filterList = filterList; + args.filterCount = filterCount; + args.defaultPath = defaultPath; + args.defaultName = defaultName; + return NFD_SaveDialogN_With_Impl(NFD_INTERFACE_VERSION, outPath, &args); +} + +nfdresult_t NFD_SaveDialogN_With_Impl(nfdversion_t version, + nfdnchar_t** outPath, + const nfdsavedialognargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + nfdresult_t result = NFD_CANCEL; @autoreleasepool { NSWindow* keyWindow = [[NSApplication sharedApplication] keyWindow]; @@ -304,13 +356,13 @@ nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, [dialog setAllowsOtherFileTypes:TRUE]; // Build the filter list - AddFilterListToDialog(dialog, filterList, filterCount); + AddFilterListToDialog(dialog, args->filterList, args->filterCount); // Set the starting directory - SetDefaultPath(dialog, defaultPath); + SetDefaultPath(dialog, args->defaultPath); // Set the default file name - SetDefaultName(dialog, defaultName); + SetDefaultName(dialog, args->defaultName); if ([dialog runModal] == NSModalResponseOK) { const NSURL* url = [dialog URL]; @@ -332,7 +384,24 @@ nfdresult_t NFD_SaveDialogU8(nfdu8char_t** outPath, return NFD_SaveDialogN(outPath, filterList, filterCount, defaultPath, defaultName); } +nfdresult_t NFD_SaveDialogU8_With_Impl(nfdversion_t version, + nfdu8char_t** outPath, + const nfdsavedialogu8args_t* args) { + return NFD_SaveDialogN_With_Impl(version, outPath, args); +} + nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) { + nfdpickfoldernargs_t args = {0}; + args.defaultPath = defaultPath; + return NFD_PickFolderN_With_Impl(NFD_INTERFACE_VERSION, outPath, &args); +} + +nfdresult_t NFD_PickFolderN_With_Impl(nfdversion_t version, + nfdnchar_t** outPath, + const nfdpickfoldernargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + nfdresult_t result = NFD_CANCEL; @autoreleasepool { NSWindow* keyWindow = [[NSApplication sharedApplication] keyWindow]; @@ -344,7 +413,7 @@ nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) [dialog setCanChooseFiles:NO]; // Set the starting directory - SetDefaultPath(dialog, defaultPath); + SetDefaultPath(dialog, args->defaultPath); if ([dialog runModal] == NSModalResponseOK) { const NSURL* url = [dialog URL]; @@ -362,6 +431,12 @@ nfdresult_t NFD_PickFolderU8(nfdu8char_t** outPath, const nfdu8char_t* defaultPa return NFD_PickFolderN(outPath, defaultPath); } +nfdresult_t NFD_PickFolderU8_With_Impl(nfdversion_t version, + nfdu8char_t** outPath, + const nfdpickfolderu8args_t* args) { + return NFD_PickFolderN_With_Impl(version, outPath, args); +} + nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count) { const NSArray* urls = (const NSArray*)pathSet; *count = [urls count]; diff --git a/src/nfd_gtk.cpp b/src/nfd_gtk.cpp index cb1696da..b8a8b06d 100644 --- a/src/nfd_gtk.cpp +++ b/src/nfd_gtk.cpp @@ -416,6 +416,19 @@ nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, const nfdnchar_t* defaultPath) { + nfdopendialognargs_t args{}; + args.filterList = filterList; + args.filterCount = filterCount; + args.defaultPath = defaultPath; + return NFD_OpenDialogN_With_Impl(NFD_INTERFACE_VERSION, outPath, &args); +} + +nfdresult_t NFD_OpenDialogN_With_Impl(nfdversion_t version, + nfdnchar_t** outPath, + const nfdopendialognargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + GtkWidget* widget = gtk_file_chooser_dialog_new("Open File", nullptr, GTK_FILE_CHOOSER_ACTION_OPEN, @@ -429,10 +442,10 @@ nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath, Widget_Guard widgetGuard(widget); /* Build the filter list */ - AddFiltersToDialog(GTK_FILE_CHOOSER(widget), filterList, filterCount); + AddFiltersToDialog(GTK_FILE_CHOOSER(widget), args->filterList, args->filterCount); /* Set the default path */ - SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath); + SetDefaultPath(GTK_FILE_CHOOSER(widget), args->defaultPath); if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) { // write out the file name @@ -450,10 +463,28 @@ nfdresult_t NFD_OpenDialogU8(nfdu8char_t** outPath, const nfdu8char_t* defaultPath) __attribute__((alias("NFD_OpenDialogN"))); +nfdresult_t NFD_OpenDialogU8_With_Impl(nfdversion_t version, + nfdu8char_t** outPath, + const nfdopendialogu8args_t* args) + __attribute__((alias("NFD_OpenDialogN_With_Impl"))); + nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, const nfdnchar_t* defaultPath) { + nfdopendialognargs_t args{}; + args.filterList = filterList; + args.filterCount = filterCount; + args.defaultPath = defaultPath; + return NFD_OpenDialogMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, &args); +} + +nfdresult_t NFD_OpenDialogMultipleN_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdopendialognargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + GtkWidget* widget = gtk_file_chooser_dialog_new("Open Files", nullptr, GTK_FILE_CHOOSER_ACTION_OPEN, @@ -470,10 +501,10 @@ nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(widget), TRUE); /* Build the filter list */ - AddFiltersToDialog(GTK_FILE_CHOOSER(widget), filterList, filterCount); + AddFiltersToDialog(GTK_FILE_CHOOSER(widget), args->filterList, args->filterCount); /* Set the default path */ - SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath); + SetDefaultPath(GTK_FILE_CHOOSER(widget), args->defaultPath); if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) { // write out the file name @@ -492,11 +523,30 @@ nfdresult_t NFD_OpenDialogMultipleU8(const nfdpathset_t** outPaths, const nfdu8char_t* defaultPath) __attribute__((alias("NFD_OpenDialogMultipleN"))); +nfdresult_t NFD_OpenDialogMultipleU8_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdopendialogu8args_t* args) + __attribute__((alias("NFD_OpenDialogMultipleN_With_Impl"))); + nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, const nfdnchar_t* defaultPath, const nfdnchar_t* defaultName) { + nfdsavedialognargs_t args{}; + args.filterList = filterList; + args.filterCount = filterCount; + args.defaultPath = defaultPath; + args.defaultName = defaultName; + return NFD_SaveDialogN_With_Impl(NFD_INTERFACE_VERSION, outPath, &args); +} + +nfdresult_t NFD_SaveDialogN_With_Impl(nfdversion_t version, + nfdnchar_t** outPath, + const nfdsavedialognargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + GtkWidget* widget = gtk_file_chooser_dialog_new("Save File", nullptr, GTK_FILE_CHOOSER_ACTION_SAVE, @@ -516,13 +566,13 @@ nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, ButtonClickedArgs buttonClickedArgs; buttonClickedArgs.chooser = GTK_FILE_CHOOSER(widget); buttonClickedArgs.map = - AddFiltersToDialogWithMap(GTK_FILE_CHOOSER(widget), filterList, filterCount); + AddFiltersToDialogWithMap(GTK_FILE_CHOOSER(widget), args->filterList, args->filterCount); /* Set the default path */ - SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath); + SetDefaultPath(GTK_FILE_CHOOSER(widget), args->defaultPath); /* Set the default file name */ - SetDefaultName(GTK_FILE_CHOOSER(widget), defaultName); + SetDefaultName(GTK_FILE_CHOOSER(widget), args->defaultName); /* set the handler to add file extension */ gulong handlerID = g_signal_connect(G_OBJECT(saveButton), @@ -555,7 +605,23 @@ nfdresult_t NFD_SaveDialogU8(nfdu8char_t** outPath, const nfdu8char_t* defaultName) __attribute__((alias("NFD_SaveDialogN"))); +nfdresult_t NFD_SaveDialogU8_With_Impl(nfdversion_t version, + nfdu8char_t** outPath, + const nfdsavedialogu8args_t* args) + __attribute__((alias("NFD_SaveDialogN_With_Impl"))); + nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) { + nfdpickfoldernargs_t args{}; + args.defaultPath = defaultPath; + return NFD_PickFolderN_With_Impl(NFD_INTERFACE_VERSION, outPath, &args); +} + +nfdresult_t NFD_PickFolderN_With_Impl(nfdversion_t version, + nfdnchar_t** outPath, + const nfdpickfoldernargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + GtkWidget* widget = gtk_file_chooser_dialog_new("Select folder", nullptr, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, @@ -569,7 +635,7 @@ nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) Widget_Guard widgetGuard(widget); /* Set the default path */ - SetDefaultPath(GTK_FILE_CHOOSER(widget), defaultPath); + SetDefaultPath(GTK_FILE_CHOOSER(widget), args->defaultPath); if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) { // write out the file name @@ -584,6 +650,11 @@ nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) nfdresult_t NFD_PickFolderU8(nfdu8char_t** outPath, const nfdu8char_t* defaultPath) __attribute__((alias("NFD_PickFolderN"))); +nfdresult_t NFD_PickFolderU8_With_Impl(nfdversion_t version, + nfdu8char_t** outPath, + const nfdpickfolderu8args_t* args) + __attribute__((alias("NFD_PickFolderN_With_Impl"))); + 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 diff --git a/src/nfd_portal.cpp b/src/nfd_portal.cpp index 655dcb87..e5f09811 100644 --- a/src/nfd_portal.cpp +++ b/src/nfd_portal.cpp @@ -1377,10 +1377,23 @@ nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, const nfdnchar_t* defaultPath) { + nfdopendialognargs_t args{}; + args.filterList = filterList; + args.filterCount = filterCount; + args.defaultPath = defaultPath; + return NFD_OpenDialogN_With_Impl(NFD_INTERFACE_VERSION, outPath, &args); +} + +nfdresult_t NFD_OpenDialogN_With_Impl(nfdversion_t version, + nfdnchar_t** outPath, + const nfdopendialognargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + DBusMessage* msg; { - const nfdresult_t res = - NFD_DBus_OpenFile(msg, filterList, filterCount, defaultPath); + const nfdresult_t res = NFD_DBus_OpenFile( + msg, args->filterList, args->filterCount, args->defaultPath); if (res != NFD_OKAY) { return res; } @@ -1404,14 +1417,32 @@ nfdresult_t NFD_OpenDialogU8(nfdu8char_t** outPath, const nfdu8char_t* defaultPath) __attribute__((alias("NFD_OpenDialogN"))); +nfdresult_t NFD_OpenDialogU8_With_Impl(nfdversion_t version, + nfdu8char_t** outPath, + const nfdopendialogu8args_t* args) + __attribute__((alias("NFD_OpenDialogN_With_Impl"))); + nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, const nfdnchar_t* defaultPath) { + nfdopendialognargs_t args{}; + args.filterList = filterList; + args.filterCount = filterCount; + args.defaultPath = defaultPath; + return NFD_OpenDialogMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, &args); +} + +nfdresult_t NFD_OpenDialogMultipleN_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdopendialognargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + DBusMessage* msg; { - const nfdresult_t res = - NFD_DBus_OpenFile(msg, filterList, filterCount, defaultPath); + const nfdresult_t res = NFD_DBus_OpenFile( + msg, args->filterList, args->filterCount, args->defaultPath); if (res != NFD_OKAY) { return res; } @@ -1434,15 +1465,34 @@ nfdresult_t NFD_OpenDialogMultipleU8(const nfdpathset_t** outPaths, const nfdu8char_t* defaultPath) __attribute__((alias("NFD_OpenDialogMultipleN"))); +nfdresult_t NFD_OpenDialogMultipleU8_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdopendialogu8args_t* args) + __attribute__((alias("NFD_OpenDialogMultipleN_With_Impl"))); + nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, const nfdnchar_t* defaultPath, const nfdnchar_t* defaultName) { + nfdsavedialognargs_t args{}; + args.filterList = filterList; + args.filterCount = filterCount; + args.defaultPath = defaultPath; + args.defaultName = defaultName; + return NFD_SaveDialogN_With_Impl(NFD_INTERFACE_VERSION, outPath, &args); +} + +nfdresult_t NFD_SaveDialogN_With_Impl(nfdversion_t version, + nfdnchar_t** outPath, + const nfdsavedialognargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + DBusMessage* msg; { - const nfdresult_t res = - NFD_DBus_SaveFile(msg, filterList, filterCount, defaultPath, defaultName); + const nfdresult_t res = NFD_DBus_SaveFile( + msg, args->filterList, args->filterCount, args->defaultPath, args->defaultName); if (res != NFD_OKAY) { return res; } @@ -1480,8 +1530,24 @@ nfdresult_t NFD_SaveDialogU8(nfdu8char_t** outPath, const nfdu8char_t* defaultName) __attribute__((alias("NFD_SaveDialogN"))); +nfdresult_t NFD_SaveDialogU8_With_Impl(nfdversion_t version, + nfdu8char_t** outPath, + const nfdsavedialogu8args_t* args) + __attribute__((alias("NFD_SaveDialogN_With_Impl"))); + nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) { - (void)defaultPath; // Default path not supported for portal backend + nfdpickfoldernargs_t args{}; + args.defaultPath = defaultPath; + return NFD_PickFolderN_With_Impl(NFD_INTERFACE_VERSION, outPath, &args); +} + +nfdresult_t NFD_PickFolderN_With_Impl(nfdversion_t version, + nfdnchar_t** outPath, + const nfdpickfoldernargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + + (void)args; // Default path not supported for portal backend { dbus_uint32_t version; @@ -1501,7 +1567,7 @@ nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) DBusMessage* msg; { - const nfdresult_t res = NFD_DBus_OpenFile(msg, nullptr, 0, defaultPath); + const nfdresult_t res = NFD_DBus_OpenFile(msg, nullptr, 0, args->defaultPath); if (res != NFD_OKAY) { return res; } @@ -1522,6 +1588,11 @@ nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) nfdresult_t NFD_PickFolderU8(nfdu8char_t** outPath, const nfdu8char_t* defaultPath) __attribute__((alias("NFD_PickFolderN"))); +nfdresult_t NFD_PickFolderU8_With_Impl(nfdversion_t version, + nfdu8char_t** outPath, + const nfdpickfolderu8args_t* args) + __attribute__((alias("NFD_PickFolderN_With_Impl"))); + nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count) { assert(pathSet); DBusMessage* msg = const_cast(static_cast(pathSet)); diff --git a/src/nfd_win.cpp b/src/nfd_win.cpp index 66ef5962..614c1d10 100644 --- a/src/nfd_win.cpp +++ b/src/nfd_win.cpp @@ -334,6 +334,19 @@ nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, const nfdnchar_t* defaultPath) { + nfdopendialognargs_t args{}; + args.filterList = filterList; + args.filterCount = filterCount; + args.defaultPath = defaultPath; + return NFD_OpenDialogN_With_Impl(NFD_INTERFACE_VERSION, outPath, &args); +} + +nfdresult_t NFD_OpenDialogN_With_Impl(nfdversion_t version, + nfdnchar_t** outPath, + const nfdopendialognargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + ::IFileOpenDialog* fileOpenDialog; // Create dialog @@ -352,17 +365,17 @@ nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath, Release_Guard<::IFileOpenDialog> fileOpenDialogGuard(fileOpenDialog); // Build the filter list - if (!AddFiltersToDialog(fileOpenDialog, filterList, filterCount)) { + if (!AddFiltersToDialog(fileOpenDialog, args->filterList, args->filterCount)) { return NFD_ERROR; } // Set auto-completed default extension - if (!SetDefaultExtension(fileOpenDialog, filterList, filterCount)) { + if (!SetDefaultExtension(fileOpenDialog, args->filterList, args->filterCount)) { return NFD_ERROR; } // Set the default path - if (!SetDefaultPath(fileOpenDialog, defaultPath)) { + if (!SetDefaultPath(fileOpenDialog, args->defaultPath)) { return NFD_ERROR; } @@ -405,7 +418,20 @@ nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, const nfdnchar_t* defaultPath) { - ::IFileOpenDialog* fileOpenDialog(nullptr); + nfdopendialognargs_t args{}; + args.filterList = filterList; + args.filterCount = filterCount; + args.defaultPath = defaultPath; + return NFD_OpenDialogMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, &args); +} + +nfdresult_t NFD_OpenDialogMultipleN_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdopendialognargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + + ::IFileOpenDialog* fileOpenDialog; // Create dialog HRESULT result = ::CoCreateInstance(::CLSID_FileOpenDialog, @@ -423,17 +449,17 @@ nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, Release_Guard<::IFileOpenDialog> fileOpenDialogGuard(fileOpenDialog); // Build the filter list - if (!AddFiltersToDialog(fileOpenDialog, filterList, filterCount)) { + if (!AddFiltersToDialog(fileOpenDialog, args->filterList, args->filterCount)) { return NFD_ERROR; } // Set auto-completed default extension - if (!SetDefaultExtension(fileOpenDialog, filterList, filterCount)) { + if (!SetDefaultExtension(fileOpenDialog, args->filterList, args->filterCount)) { return NFD_ERROR; } // Set the default path - if (!SetDefaultPath(fileOpenDialog, defaultPath)) { + if (!SetDefaultPath(fileOpenDialog, args->defaultPath)) { return NFD_ERROR; } @@ -469,6 +495,20 @@ nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, nfdfiltersize_t filterCount, const nfdnchar_t* defaultPath, const nfdnchar_t* defaultName) { + nfdsavedialognargs_t args{}; + args.filterList = filterList; + args.filterCount = filterCount; + args.defaultPath = defaultPath; + args.defaultName = defaultName; + return NFD_SaveDialogN_With_Impl(NFD_INTERFACE_VERSION, outPath, &args); +} + +nfdresult_t NFD_SaveDialogN_With_Impl(nfdversion_t version, + nfdnchar_t** outPath, + const nfdsavedialognargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + ::IFileSaveDialog* fileSaveDialog; // Create dialog @@ -487,22 +527,22 @@ nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, Release_Guard<::IFileSaveDialog> fileSaveDialogGuard(fileSaveDialog); // Build the filter list - if (!AddFiltersToDialog(fileSaveDialog, filterList, filterCount)) { + if (!AddFiltersToDialog(fileSaveDialog, args->filterList, args->filterCount)) { return NFD_ERROR; } // Set default extension - if (!SetDefaultExtension(fileSaveDialog, filterList, filterCount)) { + if (!SetDefaultExtension(fileSaveDialog, args->filterList, args->filterCount)) { return NFD_ERROR; } // Set the default path - if (!SetDefaultPath(fileSaveDialog, defaultPath)) { + if (!SetDefaultPath(fileSaveDialog, args->defaultPath)) { return NFD_ERROR; } // Set the default name - if (!SetDefaultName(fileSaveDialog, defaultName)) { + if (!SetDefaultName(fileSaveDialog, args->defaultName)) { return NFD_ERROR; } @@ -542,6 +582,17 @@ nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, } nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) { + nfdpickfoldernargs_t args{}; + args.defaultPath = defaultPath; + return NFD_PickFolderN_With_Impl(NFD_INTERFACE_VERSION, outPath, &args); +} + +nfdresult_t NFD_PickFolderN_With_Impl(nfdversion_t version, + nfdnchar_t** outPath, + const nfdpickfoldernargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + ::IFileOpenDialog* fileOpenDialog; // Create dialog @@ -557,7 +608,7 @@ nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) Release_Guard<::IFileOpenDialog> fileOpenDialogGuard(fileOpenDialog); // Set the default path - if (!SetDefaultPath(fileOpenDialog, defaultPath)) { + if (!SetDefaultPath(fileOpenDialog, args->defaultPath)) { return NFD_ERROR; } @@ -810,23 +861,37 @@ void NFD_FreePathU8(nfdu8char_t* outPath) { nfdresult_t NFD_OpenDialogU8(nfdu8char_t** outPath, const nfdu8filteritem_t* filterList, - nfdfiltersize_t count, + nfdfiltersize_t filterCount, const nfdu8char_t* defaultPath) { + nfdopendialogu8args_t args{}; + args.filterList = filterList; + args.filterCount = filterCount; + args.defaultPath = defaultPath; + return NFD_OpenDialogU8_With_Impl(NFD_INTERFACE_VERSION, outPath, &args); +} + +nfdresult_t NFD_OpenDialogU8_With_Impl(nfdversion_t version, + nfdu8char_t** outPath, + const nfdopendialogu8args_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + // populate the real nfdnfilteritem_t FilterItem_Guard filterItemsNGuard; - if (!CopyFilterItem(filterList, count, filterItemsNGuard)) { + if (!CopyFilterItem(args->filterList, args->filterCount, filterItemsNGuard)) { return NFD_ERROR; } // convert and normalize the default path, but only if it is not nullptr FreeCheck_Guard defaultPathNGuard; - ConvertU8ToNative(defaultPath, defaultPathNGuard); + ConvertU8ToNative(args->defaultPath, defaultPathNGuard); NormalizePathSeparator(defaultPathNGuard.data); // call the native function nfdnchar_t* outPathN; - nfdresult_t res = - NFD_OpenDialogN(&outPathN, filterItemsNGuard.data, count, defaultPathNGuard.data); + const nfdopendialognargs_t argsN{ + filterItemsNGuard.data, args->filterCount, defaultPathNGuard.data}; + nfdresult_t res = NFD_OpenDialogN_With_Impl(NFD_INTERFACE_VERSION, &outPathN, &argsN); if (res != NFD_OKAY) { return res; @@ -845,21 +910,36 @@ nfdresult_t NFD_OpenDialogU8(nfdu8char_t** outPath, * returns NFD_OKAY */ nfdresult_t NFD_OpenDialogMultipleU8(const nfdpathset_t** outPaths, const nfdu8filteritem_t* filterList, - nfdfiltersize_t count, + nfdfiltersize_t filterCount, const nfdu8char_t* defaultPath) { + nfdopendialogu8args_t args{}; + args.filterList = filterList; + args.filterCount = filterCount; + args.defaultPath = defaultPath; + return NFD_OpenDialogMultipleU8_With_Impl(NFD_INTERFACE_VERSION, outPaths, &args); +} + +nfdresult_t NFD_OpenDialogMultipleU8_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdopendialogu8args_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + // populate the real nfdnfilteritem_t FilterItem_Guard filterItemsNGuard; - if (!CopyFilterItem(filterList, count, filterItemsNGuard)) { + if (!CopyFilterItem(args->filterList, args->filterCount, filterItemsNGuard)) { return NFD_ERROR; } // convert and normalize the default path, but only if it is not nullptr FreeCheck_Guard defaultPathNGuard; - ConvertU8ToNative(defaultPath, defaultPathNGuard); + ConvertU8ToNative(args->defaultPath, defaultPathNGuard); NormalizePathSeparator(defaultPathNGuard.data); // call the native function - return NFD_OpenDialogMultipleN(outPaths, filterItemsNGuard.data, count, defaultPathNGuard.data); + const nfdopendialognargs_t argsN{ + filterItemsNGuard.data, args->filterCount, defaultPathNGuard.data}; + return NFD_OpenDialogMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, &argsN); } /* save dialog */ @@ -867,28 +947,43 @@ nfdresult_t NFD_OpenDialogMultipleU8(const nfdpathset_t** outPaths, * NFD_OKAY */ nfdresult_t NFD_SaveDialogU8(nfdu8char_t** outPath, const nfdu8filteritem_t* filterList, - nfdfiltersize_t count, + nfdfiltersize_t filterCount, const nfdu8char_t* defaultPath, const nfdu8char_t* defaultName) { + nfdsavedialogu8args_t args{}; + args.filterList = filterList; + args.filterCount = filterCount; + args.defaultPath = defaultPath; + args.defaultName = defaultName; + return NFD_SaveDialogU8_With_Impl(NFD_INTERFACE_VERSION, outPath, &args); +} + +nfdresult_t NFD_SaveDialogU8_With_Impl(nfdversion_t version, + nfdu8char_t** outPath, + const nfdsavedialogu8args_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + // populate the real nfdnfilteritem_t FilterItem_Guard filterItemsNGuard; - if (!CopyFilterItem(filterList, count, filterItemsNGuard)) { + if (!CopyFilterItem(args->filterList, args->filterCount, filterItemsNGuard)) { return NFD_ERROR; } // convert and normalize the default path, but only if it is not nullptr FreeCheck_Guard defaultPathNGuard; - ConvertU8ToNative(defaultPath, defaultPathNGuard); + ConvertU8ToNative(args->defaultPath, defaultPathNGuard); NormalizePathSeparator(defaultPathNGuard.data); // convert the default name, but only if it is not nullptr FreeCheck_Guard defaultNameNGuard; - ConvertU8ToNative(defaultName, defaultNameNGuard); + ConvertU8ToNative(args->defaultName, defaultNameNGuard); // call the native function nfdnchar_t* outPathN; - nfdresult_t res = NFD_SaveDialogN( - &outPathN, filterItemsNGuard.data, count, defaultPathNGuard.data, defaultNameNGuard.data); + const nfdsavedialognargs_t argsN{ + filterItemsNGuard.data, args->filterCount, defaultPathNGuard.data, defaultNameNGuard.data}; + nfdresult_t res = NFD_SaveDialogN_With_Impl(NFD_INTERFACE_VERSION, &outPathN, &argsN); if (res != NFD_OKAY) { return res; @@ -906,14 +1001,26 @@ nfdresult_t NFD_SaveDialogU8(nfdu8char_t** outPath, /* 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) { + nfdpickfolderu8args_t args{}; + args.defaultPath = defaultPath; + return NFD_PickFolderU8_With_Impl(NFD_INTERFACE_VERSION, outPath, &args); +} + +nfdresult_t NFD_PickFolderU8_With_Impl(nfdversion_t version, + nfdu8char_t** outPath, + const nfdpickfolderu8args_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + // convert and normalize the default path, but only if it is not nullptr FreeCheck_Guard defaultPathNGuard; - ConvertU8ToNative(defaultPath, defaultPathNGuard); + ConvertU8ToNative(args->defaultPath, defaultPathNGuard); NormalizePathSeparator(defaultPathNGuard.data); // call the native function nfdnchar_t* outPathN; - nfdresult_t res = NFD_PickFolderN(&outPathN, defaultPathNGuard.data); + const nfdpickfoldernargs_t argsN{defaultPathNGuard.data}; + nfdresult_t res = NFD_PickFolderN_With_Impl(NFD_INTERFACE_VERSION, &outPathN, &argsN); if (res != NFD_OKAY) { return res; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f965826a..dfe29f3c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -3,6 +3,8 @@ set(TEST_LIST test_opendialog.c test_opendialog_cpp.cpp test_opendialog_native.c + test_opendialog_with.c + test_opendialog_native_with.c test_opendialogmultiple.c test_opendialogmultiple_cpp.cpp test_opendialogmultiple_native.c @@ -11,8 +13,12 @@ set(TEST_LIST test_pickfolder.c test_pickfolder_cpp.cpp test_pickfolder_native.c + test_pickfolder_with.c + test_pickfolder_native_with.c test_savedialog.c - test_savedialog_native.c) + test_savedialog_native.c + test_savedialog_with.c + test_savedialog_native_with.c) foreach (TEST ${TEST_LIST}) string(REPLACE "." "_" CLEAN_TEST_NAME ${TEST}) diff --git a/test/test_opendialog_native_with.c b/test/test_opendialog_native_with.c new file mode 100644 index 00000000..cb961906 --- /dev/null +++ b/test/test_opendialog_native_with.c @@ -0,0 +1,52 @@ +#define NFD_NATIVE +#include + +#include +#include + +/* this test should compile on all supported platforms */ + +int main(void) { + // initialize NFD + // either call NFD_Init at the start of your program and NFD_Quit at the end of your program, + // or before/after every time you want to show a file dialog. + NFD_Init(); + + nfdchar_t* outPath; + + // prepare filters for the dialog +#ifdef _WIN32 + nfdfilteritem_t filterItem[2] = {{L"Source code", L"c,cpp,cc"}, {L"Headers", L"h,hpp"}}; +#else + nfdfilteritem_t filterItem[2] = {{"Source code", "c,cpp,cc"}, {"Headers", "h,hpp"}}; +#endif + + // show the dialog + nfdopendialognargs_t args = {0}; + args.filterList = filterItem; + args.filterCount = 2; + nfdresult_t result = NFD_OpenDialogN_With(&outPath, &args); + if (result == NFD_OKAY) { + puts("Success!"); +#ifdef _WIN32 +#ifdef _MSC_VER + _putws(outPath); +#else + fputws(outPath, stdin); +#endif +#else + puts(outPath); +#endif + // remember to free the memory (since NFD_OKAY is returned) + NFD_FreePath(outPath); + } else if (result == NFD_CANCEL) { + puts("User pressed cancel."); + } else { + printf("Error: %s\n", NFD_GetError()); + } + + // Quit NFD + NFD_Quit(); + + return 0; +} diff --git a/test/test_opendialog_with.c b/test/test_opendialog_with.c new file mode 100644 index 00000000..f8e0ada6 --- /dev/null +++ b/test/test_opendialog_with.c @@ -0,0 +1,39 @@ +#include + +#include +#include + +/* this test should compile on all supported platforms */ + +int main(void) { + // initialize NFD + // either call NFD_Init at the start of your program and NFD_Quit at the end of your program, + // or before/after every time you want to show a file dialog. + NFD_Init(); + + nfdchar_t* outPath; + + // prepare filters for the dialog + nfdfilteritem_t filterItem[2] = {{"Source code", "c,cpp,cc"}, {"Headers", "h,hpp"}}; + + // show the dialog + nfdopendialogu8args_t args = {0}; + args.filterList = filterItem; + args.filterCount = 2; + nfdresult_t result = NFD_OpenDialogU8_With(&outPath, &args); + if (result == NFD_OKAY) { + puts("Success!"); + puts(outPath); + // remember to free the memory (since NFD_OKAY is returned) + NFD_FreePath(outPath); + } else if (result == NFD_CANCEL) { + puts("User pressed cancel."); + } else { + printf("Error: %s\n", NFD_GetError()); + } + + // Quit NFD + NFD_Quit(); + + return 0; +} diff --git a/test/test_pickfolder_native_with.c b/test/test_pickfolder_native_with.c new file mode 100644 index 00000000..c89502e3 --- /dev/null +++ b/test/test_pickfolder_native_with.c @@ -0,0 +1,43 @@ +#define NFD_NATIVE +#include + +#include +#include + +/* this test should compile on all supported platforms */ + +int main(void) { + // initialize NFD + // either call NFD_Init at the start of your program and NFD_Quit at the end of your program, + // or before/after every time you want to show a file dialog. + NFD_Init(); + + nfdchar_t* outPath; + + // show the dialog + nfdpickfoldernargs_t args = {0}; + nfdresult_t result = NFD_PickFolderN_With(&outPath, &args); + if (result == NFD_OKAY) { + puts("Success!"); +#ifdef _WIN32 +#ifdef _MSC_VER + _putws(outPath); +#else + fputws(outPath, stdin); +#endif +#else + puts(outPath); +#endif + // remember to free the memory (since NFD_OKAY is returned) + NFD_FreePath(outPath); + } else if (result == NFD_CANCEL) { + puts("User pressed cancel."); + } else { + printf("Error: %s\n", NFD_GetError()); + } + + // Quit NFD + NFD_Quit(); + + return 0; +} diff --git a/test/test_pickfolder_with.c b/test/test_pickfolder_with.c new file mode 100644 index 00000000..f6e864bf --- /dev/null +++ b/test/test_pickfolder_with.c @@ -0,0 +1,34 @@ +#include + +#include +#include + +/* this test should compile on all supported platforms */ + +int main(void) { + // initialize NFD + // either call NFD_Init at the start of your program and NFD_Quit at the end of your program, + // or before/after every time you want to show a file dialog. + NFD_Init(); + + nfdchar_t* outPath; + + // show the dialog + nfdpickfolderu8args_t args = {0}; + nfdresult_t result = NFD_PickFolderU8_With(&outPath, &args); + if (result == NFD_OKAY) { + puts("Success!"); + puts(outPath); + // remember to free the memory (since NFD_OKAY is returned) + NFD_FreePath(outPath); + } else if (result == NFD_CANCEL) { + puts("User pressed cancel."); + } else { + printf("Error: %s\n", NFD_GetError()); + } + + // Quit NFD + NFD_Quit(); + + return 0; +} diff --git a/test/test_savedialog_native_with.c b/test/test_savedialog_native_with.c new file mode 100644 index 00000000..8a86f541 --- /dev/null +++ b/test/test_savedialog_native_with.c @@ -0,0 +1,59 @@ +#define NFD_NATIVE +#include + +#include +#include + +/* this test should compile on all supported platforms */ + +int main(void) { + // initialize NFD + // either call NFD_Init at the start of your program and NFD_Quit at the end of your program, + // or before/after every time you want to show a file dialog. + NFD_Init(); + + nfdchar_t* savePath; + + // prepare filters for the dialog +#ifdef _WIN32 + nfdfilteritem_t filterItem[2] = {{L"Source code", L"c,cpp,cc"}, {L"Headers", L"h,hpp"}}; +#else + nfdfilteritem_t filterItem[2] = {{"Source code", "c,cpp,cc"}, {"Headers", "h,hpp"}}; +#endif + +#ifdef _WIN32 + const wchar_t* defaultPath = L"Untitled.c"; +#else + const char* defaultPath = "Untitled.c"; +#endif + + // show the dialog + nfdsavedialognargs_t args = {0}; + args.filterList = filterItem; + args.filterCount = 2; + args.defaultName = defaultPath; + nfdresult_t result = NFD_SaveDialogN_With(&savePath, &args); + if (result == NFD_OKAY) { + puts("Success!"); +#ifdef _WIN32 +#ifdef _MSC_VER + _putws(savePath); +#else + fputws(savePath, stdin); +#endif +#else + puts(savePath); +#endif + // remember to free the memory (since NFD_OKAY is returned) + NFD_FreePath(savePath); + } else if (result == NFD_CANCEL) { + puts("User pressed cancel."); + } else { + printf("Error: %s\n", NFD_GetError()); + } + + // Quit NFD + NFD_Quit(); + + return 0; +} diff --git a/test/test_savedialog_with.c b/test/test_savedialog_with.c new file mode 100644 index 00000000..add5eb26 --- /dev/null +++ b/test/test_savedialog_with.c @@ -0,0 +1,40 @@ +#include + +#include +#include + +/* this test should compile on all supported platforms */ + +int main(void) { + // initialize NFD + // either call NFD_Init at the start of your program and NFD_Quit at the end of your program, + // or before/after every time you want to show a file dialog. + NFD_Init(); + + nfdchar_t* savePath; + + // prepare filters for the dialog + nfdfilteritem_t filterItem[2] = {{"Source code", "c,cpp,cc"}, {"Header", "h,hpp"}}; + + // show the dialog + nfdsavedialogu8args_t args = {0}; + args.filterList = filterItem; + args.filterCount = 2; + args.defaultName = "Untitled.c"; + nfdresult_t result = NFD_SaveDialogU8_With(&savePath, &args); + if (result == NFD_OKAY) { + puts("Success!"); + puts(savePath); + // remember to free the memory (since NFD_OKAY is returned) + NFD_FreePath(savePath); + } else if (result == NFD_CANCEL) { + puts("User pressed cancel."); + } else { + printf("Error: %s\n", NFD_GetError()); + } + + // Quit NFD + NFD_Quit(); + + return 0; +} From ec07834d7dd5a83feacb71b60b8f2d27b6b731bd Mon Sep 17 00:00:00 2001 From: Bernard Teo Date: Sun, 23 Jun 2024 18:06:20 +0800 Subject: [PATCH 02/10] New feature: Implement PickFolderMultiple (#135) We have OpenFile, OpenFileMultiple, and PickFolder. It's only reasonable to also add PickFolderMultiple, since all backends support it. It does the expected thing on all backends. --- src/include/nfd.h | 51 ++++++++++++++++ src/include/nfd.hpp | 32 ++++++++++ src/nfd_cocoa.m | 53 +++++++++++++++++ src/nfd_gtk.cpp | 48 ++++++++++++++- src/nfd_portal.cpp | 62 +++++++++++++++++++- src/nfd_win.cpp | 84 +++++++++++++++++++++++++++ test/CMakeLists.txt | 2 + test/test_pickfoldermultiple.c | 47 +++++++++++++++ test/test_pickfoldermultiple_native.c | 52 +++++++++++++++++ 9 files changed, 428 insertions(+), 3 deletions(-) create mode 100644 test/test_pickfoldermultiple.c create mode 100644 test/test_pickfoldermultiple_native.c diff --git a/src/include/nfd.h b/src/include/nfd.h index 4c4b12a5..7dc2ca0c 100644 --- a/src/include/nfd.h +++ b/src/include/nfd.h @@ -373,6 +373,55 @@ NFD_INLINE nfdresult_t NFD_PickFolderU8_With(nfdu8char_t** outPath, return NFD_PickFolderU8_With_Impl(NFD_INTERFACE_VERSION, outPath, args); } +/** Select multiple folder dialog + * + * It is the caller's responsibility to free `outPaths` via NFD_PathSet_FreeN() if this function + * returns NFD_OKAY. + * @param[out] outPaths + * @param defaultPath If null, the operating system will decide. */ +NFD_API nfdresult_t NFD_PickFolderMultipleN(const nfdpathset_t** outPaths, + const nfdnchar_t* defaultPath); + +/** Select multiple folder dialog + * + * It is the caller's responsibility to free `outPaths` via NFD_PathSet_FreeU8() if this function + * returns NFD_OKAY. + * @param[out] outPaths + * @param defaultPath If null, the operating system will decide. */ +NFD_API nfdresult_t NFD_PickFolderMultipleU8(const nfdpathset_t** outPaths, + const nfdu8char_t* defaultPath); + +/** This function is a library implementation detail. Please use NFD_PickFolderMultipleN_With() + * instead. */ +NFD_API nfdresult_t NFD_PickFolderMultipleN_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdpickfoldernargs_t* args); + +/** Select multiple folder dialog, with additional parameters. + * + * It is the caller's responsibility to free `outPaths` via NFD_PathSet_FreeN() if this function + * returns NFD_OKAY. See documentation of nfdopendialogargs_t for details. */ +NFD_INLINE nfdresult_t NFD_PickFolderMultipleN_With(const nfdpathset_t** outPaths, + const nfdpickfoldernargs_t* args) { + return NFD_PickFolderMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, args); +} + +/** This function is a library implementation detail. Please use NFD_PickFolderMultipleU8_With() + * instead. + */ +NFD_API nfdresult_t NFD_PickFolderMultipleU8_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdpickfolderu8args_t* args); + +/** Select multiple folder dialog, with additional parameters. + * + * It is the caller's responsibility to free `outPaths` via NFD_PathSet_FreeU8() if this function + * returns NFD_OKAY. See documentation of nfdpickfolderargs_t for details. */ +NFD_INLINE nfdresult_t NFD_PickFolderMultipleU8_With(const nfdpathset_t** outPaths, + const nfdpickfolderu8args_t* args) { + return NFD_PickFolderMultipleU8_With_Impl(NFD_INTERFACE_VERSION, outPaths, args); +} + /** Get the last error * * This is set when a function returns NFD_ERROR. @@ -465,6 +514,7 @@ typedef nfdnfilteritem_t nfdfilteritem_t; #define NFD_OpenDialogMultiple NFD_OpenDialogMultipleN #define NFD_SaveDialog NFD_SaveDialogN #define NFD_PickFolder NFD_PickFolderN +#define NFD_PickFolderMultiple NFD_PickFolderMultipleN #define NFD_PathSet_GetPath NFD_PathSet_GetPathN #define NFD_PathSet_FreePath NFD_PathSet_FreePathN #define NFD_PathSet_EnumNext NFD_PathSet_EnumNextN @@ -476,6 +526,7 @@ typedef nfdu8filteritem_t nfdfilteritem_t; #define NFD_OpenDialogMultiple NFD_OpenDialogMultipleU8 #define NFD_SaveDialog NFD_SaveDialogU8 #define NFD_PickFolder NFD_PickFolderU8 +#define NFD_PickFolderMultiple NFD_PickFolderMultipleU8 #define NFD_PathSet_GetPath NFD_PathSet_GetPathU8 #define NFD_PathSet_FreePath NFD_PathSet_FreePathU8 #define NFD_PathSet_EnumNext NFD_PathSet_EnumNextU8 diff --git a/src/include/nfd.hpp b/src/include/nfd.hpp index ab915433..787a1029 100644 --- a/src/include/nfd.hpp +++ b/src/include/nfd.hpp @@ -65,6 +65,12 @@ inline nfdresult_t PickFolder(nfdnchar_t*& outPath, return ::NFD_PickFolderN_With(&outPath, &args); } +inline nfdresult_t PickFolderMultiple(const nfdpathset_t*& outPaths, + const nfdnchar_t* defaultPath = nullptr) noexcept { + const nfdpickfoldernargs_t args{defaultPath}; + return ::NFD_PickFolderMultipleN_With(&outPaths, &args); +} + inline const char* GetError() noexcept { return ::NFD_GetError(); } @@ -132,6 +138,12 @@ inline nfdresult_t PickFolder(nfdu8char_t*& outPath, return ::NFD_PickFolderU8_With(&outPath, &args); } +inline nfdresult_t PickFolderMultiple(const nfdpathset_t*& outPaths, + const nfdu8char_t* defaultPath = nullptr) noexcept { + const nfdpickfolderu8args_t args{defaultPath}; + return ::NFD_PickFolderMultipleU8_With(&outPaths, &args); +} + namespace PathSet { inline nfdresult_t GetPath(const nfdpathset_t* pathSet, nfdpathsetsize_t index, @@ -237,6 +249,16 @@ inline nfdresult_t PickFolder(UniquePathN& outPath, return res; } +inline nfdresult_t PickFolderMultiple(UniquePathSet& outPaths, + const nfdnchar_t* defaultPath = nullptr) noexcept { + const nfdpathset_t* out; + nfdresult_t res = PickFolderMultiple(out, defaultPath); + if (res == NFD_OKAY) { + outPaths.reset(out); + } + return res; +} + #ifdef NFD_DIFFERENT_NATIVE_FUNCTIONS inline nfdresult_t OpenDialog(UniquePathU8& outPath, const nfdu8filteritem_t* filterList = nullptr, @@ -284,6 +306,16 @@ inline nfdresult_t PickFolder(UniquePathU8& outPath, } return res; } + +inline nfdresult_t PickFolderMultiple(UniquePathSet& outPaths, + const nfdu8char_t* defaultPath = nullptr) noexcept { + const nfdpathset_t* out; + nfdresult_t res = PickFolderMultiple(out, defaultPath); + if (res == NFD_OKAY) { + outPaths.reset(out); + } + return res; +} #endif namespace PathSet { diff --git a/src/nfd_cocoa.m b/src/nfd_cocoa.m index cde6e85d..9f1fa3b5 100644 --- a/src/nfd_cocoa.m +++ b/src/nfd_cocoa.m @@ -437,6 +437,59 @@ nfdresult_t NFD_PickFolderU8_With_Impl(nfdversion_t version, return NFD_PickFolderN_With_Impl(version, outPath, args); } +nfdresult_t NFD_PickFolderMultipleN(const nfdpathset_t** outPaths, const nfdnchar_t* defaultPath) { + nfdpickfoldernargs_t args = {0}; + args.defaultPath = defaultPath; + return NFD_PickFolderMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, &args); +} + +nfdresult_t NFD_PickFolderMultipleN_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdpickfoldernargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + + nfdresult_t result = NFD_CANCEL; + @autoreleasepool { + NSWindow* keyWindow = [[NSApplication sharedApplication] keyWindow]; + + NSOpenPanel* dialog = [NSOpenPanel openPanel]; + [dialog setAllowsMultipleSelection:YES]; + [dialog setCanChooseDirectories:YES]; + [dialog setCanCreateDirectories:YES]; + [dialog setCanChooseFiles:NO]; + + // Set the starting directory + SetDefaultPath(dialog, args->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_PickFolderMultipleU8(const nfdpathset_t** outPaths, + const nfdu8char_t* defaultPath) { + return NFD_PickFolderMultipleN(outPaths, defaultPath); +} + +nfdresult_t NFD_PickFolderMultipleU8_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdpickfolderu8args_t* args) { + return NFD_PickFolderMultipleN_With_Impl(version, outPaths, args); +} + nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count) { const NSArray* urls = (const NSArray*)pathSet; *count = [urls count]; diff --git a/src/nfd_gtk.cpp b/src/nfd_gtk.cpp index b8a8b06d..6f519558 100644 --- a/src/nfd_gtk.cpp +++ b/src/nfd_gtk.cpp @@ -622,7 +622,7 @@ nfdresult_t NFD_PickFolderN_With_Impl(nfdversion_t version, // We haven't needed to bump the interface version yet. (void)version; - GtkWidget* widget = gtk_file_chooser_dialog_new("Select folder", + GtkWidget* widget = gtk_file_chooser_dialog_new("Select Folder", nullptr, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, "_Cancel", @@ -655,6 +655,52 @@ nfdresult_t NFD_PickFolderU8_With_Impl(nfdversion_t version, const nfdpickfolderu8args_t* args) __attribute__((alias("NFD_PickFolderN_With_Impl"))); +nfdresult_t NFD_PickFolderMultipleN(const nfdpathset_t** outPaths, const nfdnchar_t* defaultPath) { + nfdpickfoldernargs_t args{}; + args.defaultPath = defaultPath; + return NFD_PickFolderMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, &args); +} + +nfdresult_t NFD_PickFolderMultipleN_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdpickfoldernargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + + GtkWidget* widget = gtk_file_chooser_dialog_new("Select Folders", + 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), args->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_PickFolderMultipleU8(const nfdpathset_t** outPaths, const nfdu8char_t* defaultPath) + __attribute__((alias("NFD_PickFolderMultipleN"))); + +nfdresult_t NFD_PickFolderMultipleU8_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdpickfolderu8args_t* args) + __attribute__((alias("NFD_PickFolderMultipleN_With_Impl"))); + 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 diff --git a/src/nfd_portal.cpp b/src/nfd_portal.cpp index e5f09811..a91b71b2 100644 --- a/src/nfd_portal.cpp +++ b/src/nfd_portal.cpp @@ -120,6 +120,7 @@ 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_SELECT_FOLDERS = "Select Folders"; constexpr const char* STR_HANDLE_TOKEN = "handle_token"; constexpr const char* STR_MULTIPLE = "multiple"; constexpr const char* STR_DIRECTORY = "directory"; @@ -149,6 +150,10 @@ template <> void AppendOpenFileQueryTitle(DBusMessageIter& iter) { dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_SELECT_FOLDER); } +template <> +void AppendOpenFileQueryTitle(DBusMessageIter& iter) { + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_SELECT_FOLDERS); +} void AppendSaveFileQueryTitle(DBusMessageIter& iter) { dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_SAVE_FILE); @@ -1547,8 +1552,6 @@ nfdresult_t NFD_PickFolderN_With_Impl(nfdversion_t version, // We haven't needed to bump the interface version yet. (void)version; - (void)args; // Default path not supported for portal backend - { dbus_uint32_t version; const nfdresult_t res = NFD_DBus_GetVersion(version); @@ -1593,6 +1596,61 @@ nfdresult_t NFD_PickFolderU8_With_Impl(nfdversion_t version, const nfdpickfolderu8args_t* args) __attribute__((alias("NFD_PickFolderN_With_Impl"))); +nfdresult_t NFD_PickFolderMultipleN(const nfdpathset_t** outPaths, const nfdnchar_t* defaultPath) { + nfdpickfoldernargs_t args{}; + args.defaultPath = defaultPath; + return NFD_PickFolderMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, &args); +} + +nfdresult_t NFD_PickFolderMultipleN_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdpickfoldernargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + + { + dbus_uint32_t version; + const nfdresult_t res = NFD_DBus_GetVersion(version); + if (res != NFD_OKAY) { + return res; + } + if (version < 3) { + NFDi_SetFormattedError( + "The xdg-desktop-portal installed on this system does not support a folder picker; " + "at least version 3 of the org.freedesktop.portal.FileChooser interface is " + "required but the installed interface version is %u.", + version); + return NFD_ERROR; + } + } + + DBusMessage* msg; + { + const nfdresult_t res = NFD_DBus_OpenFile(msg, nullptr, 0, args->defaultPath); + 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_PickFolderMultipleU8(const nfdpathset_t** outPaths, const nfdu8char_t* defaultPath) + __attribute__((alias("NFD_PickFolderMultipleN"))); + +nfdresult_t NFD_PickFolderMultipleU8_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdpickfolderu8args_t* args) + __attribute__((alias("NFD_PickFolderMultipleN_With_Impl"))); + nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count) { assert(pathSet); DBusMessage* msg = const_cast(static_cast(pathSet)); diff --git a/src/nfd_win.cpp b/src/nfd_win.cpp index 614c1d10..2095b3fd 100644 --- a/src/nfd_win.cpp +++ b/src/nfd_win.cpp @@ -647,6 +647,64 @@ nfdresult_t NFD_PickFolderN_With_Impl(nfdversion_t version, return NFD_OKAY; } +nfdresult_t NFD_PickFolderMultipleN(const nfdpathset_t** outPaths, const nfdnchar_t* defaultPath) { + nfdpickfoldernargs_t args{}; + args.defaultPath = defaultPath; + return NFD_PickFolderMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, &args); +} + +nfdresult_t NFD_PickFolderMultipleN_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdpickfoldernargs_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + + ::IFileOpenDialog* fileOpenDialog; + + // Create dialog + if (!SUCCEEDED(::CoCreateInstance(::CLSID_FileOpenDialog, + nullptr, + CLSCTX_ALL, + ::IID_IFileOpenDialog, + reinterpret_cast(&fileOpenDialog)))) { + NFDi_SetError("Could not create dialog."); + return NFD_ERROR; + } + + Release_Guard<::IFileOpenDialog> fileOpenDialogGuard(fileOpenDialog); + + // Set the default path + if (!SetDefaultPath(fileOpenDialog, args->defaultPath)) { + return NFD_ERROR; + } + + // Allow multiple selection; only show items that are folders and on the file system + if (!AddOptions(fileOpenDialog, + ::FOS_FORCEFILESYSTEM | ::FOS_PICKFOLDERS | ::FOS_ALLOWMULTISELECT)) { + return NFD_ERROR; + } + + // Show the dialog. + const HRESULT result = fileOpenDialog->Show(nullptr); + if (SUCCEEDED(result)) { + ::IShellItemArray* shellItems; + if (!SUCCEEDED(fileOpenDialog->GetResults(&shellItems))) { + NFDi_SetError("Could not get shell items."); + return NFD_ERROR; + } + + // 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."); + return NFD_ERROR; + } +} + 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 @@ -1034,6 +1092,32 @@ nfdresult_t NFD_PickFolderU8_With_Impl(nfdversion_t version, return res; } +/* select multiple folders dialog */ +/* It is the caller's responsibility to free `outPaths` via NFD_PathSet_FreeU8() if this function + * returns NFD_OKAY. */ +nfdresult_t NFD_PickFolderMultipleU8(const nfdpathset_t** outPaths, + const nfdu8char_t* defaultPath) { + nfdpickfolderu8args_t args{}; + args.defaultPath = defaultPath; + return NFD_PickFolderMultipleU8_With_Impl(NFD_INTERFACE_VERSION, outPaths, &args); +} + +nfdresult_t NFD_PickFolderMultipleU8_With_Impl(nfdversion_t version, + const nfdpathset_t** outPaths, + const nfdpickfolderu8args_t* args) { + // We haven't needed to bump the interface version yet. + (void)version; + + // convert and normalize the default path, but only if it is not nullptr + FreeCheck_Guard defaultPathNGuard; + ConvertU8ToNative(args->defaultPath, defaultPathNGuard); + NormalizePathSeparator(defaultPathNGuard.data); + + // call the native function + const nfdpickfoldernargs_t argsN{defaultPathNGuard.data}; + return NFD_PickFolderMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, &argsN); +} + /* 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 */ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index dfe29f3c..95f42a87 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -15,6 +15,8 @@ set(TEST_LIST test_pickfolder_native.c test_pickfolder_with.c test_pickfolder_native_with.c + test_pickfoldermultiple.c + test_pickfoldermultiple_native.c test_savedialog.c test_savedialog_native.c test_savedialog_with.c diff --git a/test/test_pickfoldermultiple.c b/test/test_pickfoldermultiple.c new file mode 100644 index 00000000..23ad4dbc --- /dev/null +++ b/test/test_pickfoldermultiple.c @@ -0,0 +1,47 @@ +#include + +#include +#include + +/* this test should compile on all supported platforms */ + +int main(void) { + // initialize NFD + // either call NFD_Init at the start of your program and NFD_Quit at the end of your program, + // or before/after every time you want to show a file dialog. + NFD_Init(); + + const nfdpathset_t* outPaths; + + // show the dialog + nfdresult_t result = NFD_PickFolderMultiple(&outPaths, NULL); + + if (result == NFD_OKAY) { + puts("Success!"); + + nfdpathsetsize_t numPaths; + NFD_PathSet_GetCount(outPaths, &numPaths); + + nfdpathsetsize_t i; + for (i = 0; i < numPaths; ++i) { + nfdchar_t* path; + NFD_PathSet_GetPath(outPaths, i, &path); + printf("Path %i: %s\n", (int)i, path); + + // remember to free the pathset path with NFD_PathSet_FreePath (not NFD_FreePath!) + NFD_PathSet_FreePath(path); + } + + // remember to free the pathset memory (since NFD_OKAY is returned) + NFD_PathSet_Free(outPaths); + } else if (result == NFD_CANCEL) { + puts("User pressed cancel."); + } else { + printf("Error: %s\n", NFD_GetError()); + } + + // Quit NFD + NFD_Quit(); + + return 0; +} diff --git a/test/test_pickfoldermultiple_native.c b/test/test_pickfoldermultiple_native.c new file mode 100644 index 00000000..93c14494 --- /dev/null +++ b/test/test_pickfoldermultiple_native.c @@ -0,0 +1,52 @@ +#define NFD_NATIVE +#include + +#include +#include + +/* this test should compile on all supported platforms */ + +int main(void) { + // initialize NFD + // either call NFD_Init at the start of your program and NFD_Quit at the end of your program, + // or before/after every time you want to show a file dialog. + NFD_Init(); + + const nfdpathset_t* outPaths; + + // show the dialog + nfdresult_t result = NFD_PickFolderMultiple(&outPaths, NULL); + + if (result == NFD_OKAY) { + puts("Success!"); + + nfdpathsetsize_t numPaths; + NFD_PathSet_GetCount(outPaths, &numPaths); + + nfdpathsetsize_t i; + for (i = 0; i < numPaths; ++i) { + nfdchar_t* path; + NFD_PathSet_GetPath(outPaths, i, &path); +#ifdef _WIN32 + wprintf(L"Path %i: %s\n", (int)i, path); +#else + printf("Path %i: %s\n", (int)i, path); +#endif + + // remember to free the pathset path with NFD_PathSet_FreePath (not NFD_FreePath!) + NFD_PathSet_FreePath(path); + } + + // remember to free the pathset memory (since NFD_OKAY is returned) + NFD_PathSet_Free(outPaths); + } else if (result == NFD_CANCEL) { + puts("User pressed cancel."); + } else { + printf("Error: %s\n", NFD_GetError()); + } + + // Quit NFD + NFD_Quit(); + + return 0; +} From 1e2020695185127635a158d189abbd5fe8308082 Mon Sep 17 00:00:00 2001 From: Bernard Teo Date: Sun, 23 Jun 2024 18:16:37 +0800 Subject: [PATCH 03/10] New feature: Accept native parent window handle (#136) This is necessary for platforms to present the dialog properly, e.g. ensuring that the dialog never goes behind the parent window. --- .github/workflows/cmake.yml | 76 ++++++- CMakeLists.txt | 3 +- src/CMakeLists.txt | 4 +- src/include/nfd.h | 24 +++ src/include/nfd.hpp | 104 +++++---- src/include/nfd_glfw3.h | 85 ++++++++ src/include/nfd_sdl2.h | 76 +++++++ src/nfd_cocoa.m | 42 +++- src/nfd_gtk.cpp | 162 +++++++++++++- src/nfd_portal.cpp | 116 +++++++--- src/nfd_win.cpp | 32 ++- test/CMakeLists.txt | 75 ++++--- test/test_sdl.c | 410 ++++++++++++++++++++++++++++++++++++ test/test_sdl.manifest | 29 +++ 14 files changed, 1111 insertions(+), 127 deletions(-) create mode 100644 src/include/nfd_glfw3.h create mode 100644 src/include/nfd_sdl2.h create mode 100644 test/test_sdl.c create mode 100644 test/test_sdl.manifest diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 089fc847..e42daa2a 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - - name: Installing Dependencies + - name: Install Dependencies run: sudo apt-get update && sudo apt-get install dos2unix - name: Convert to Unix line endings run: dos2unix */* @@ -72,7 +72,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - - name: Installing Dependencies + - name: Install Dependencies run: sudo apt-get update && sudo apt-get install ${{ matrix.portal.dep }} - name: Configure run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=${{ matrix.compiler.c }} -DCMAKE_CXX_COMPILER=${{ matrix.compiler.cpp }} -DCMAKE_CXX_STANDARD=${{ matrix.cppstd }} -DCMAKE_C_FLAGS="-Wall -Wextra -Werror -pedantic" -DCMAKE_CXX_FLAGS="-Wall -Wextra -Werror -pedantic" -DNFD_PORTAL=${{ matrix.portal.flag }} -DNFD_APPEND_EXTENSION=${{ matrix.autoappend.flag }} -DBUILD_SHARED_LIBS=${{ matrix.shared_lib.flag }} -DNFD_BUILD_TESTS=ON .. @@ -189,3 +189,75 @@ jobs: path: | build/src/* build/test/* + + build-ubuntu-sdl2: + + name: Ubuntu latest - GCC, ${{ matrix.portal.name }}, Static, SDL2 + runs-on: ubuntu-latest + + strategy: + matrix: + portal: [ {flag: OFF, dep: libgtk-3-dev, name: GTK}, {flag: ON, dep: libdbus-1-dev, name: Portal} ] # The NFD_PORTAL setting defaults to OFF (i.e. uses GTK) + + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install Dependencies + run: sudo apt-get update && sudo apt-get install ${{ matrix.portal.dep }} libsdl2-dev libsdl2-ttf-dev + - name: Configure + run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-Wall -Wextra -Werror -pedantic" -DCMAKE_CXX_FLAGS="-Wall -Wextra -Werror -pedantic" -DNFD_PORTAL=${{ matrix.portal.flag }} -DNFD_APPEND_EXTENSION=OFF -DNFD_BUILD_TESTS=OFF -DNFD_BUILD_SDL2_TESTS=ON .. + - name: Build + run: cmake --build build --target install + - name: Upload test binaries + uses: actions/upload-artifact@v2 + with: + name: Ubuntu latest - GCC, ${{ matrix.portal.name }}, Static, SDL2 + path: | + build/src/* + build/test/* + + build-macos-sdl2: + + name: MacOS latest - Clang, Static, SDL2 + runs-on: macos-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install Dependencies + run: brew install sdl2 sdl2_ttf + - name: Configure + run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-Wall -Wextra -Werror -pedantic" -DCMAKE_CXX_FLAGS="-Wall -Wextra -Werror -pedantic" -DNFD_BUILD_TESTS=OFF -DNFD_BUILD_SDL2_TESTS=ON .. + - name: Build + run: cmake --build build --target install + - name: Upload test binaries + uses: actions/upload-artifact@v2 + with: + name: MacOS latest - Clang, Static, SDL2 + path: | + build/src/* + build/test/* + + build-windows-sdl2: + + name: Windows latest - MSVC, Static, SDL2 + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install pkgconfiglite + run: choco install pkgconfiglite + - name: Install Dependencies + run: vcpkg integrate install && vcpkg install sdl2 sdl2-ttf --triplet=x64-windows-release + - name: Configure + run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_TOOLCHAIN_FILE="C:/vcpkg/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET="x64-windows-release" -DNFD_BUILD_TESTS=OFF -DNFD_BUILD_SDL2_TESTS=ON .. + - name: Build + run: cmake --build build --target install --config Release + - name: Upload test binaries + uses: actions/upload-artifact@v2 + with: + name: Windows latest - MSVC, Static, SDL2 + path: | + build/src/Release/* + build/test/Release/* diff --git a/CMakeLists.txt b/CMakeLists.txt index 86873e83..fedef3aa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,7 @@ endif () option(BUILD_SHARED_LIBS "Build a shared library instead of static" OFF) option(NFD_BUILD_TESTS "Build tests for nfd" ${nfd_ROOT_PROJECT}) +option(NFD_BUILD_SDL2_TESTS "Build SDL2 tests for nfd" OFF) option(NFD_INSTALL "Generate install target for nfd" ${nfd_ROOT_PROJECT}) set(nfd_PLATFORM Undefined) @@ -48,6 +49,6 @@ endif() add_subdirectory(src) -if(${NFD_BUILD_TESTS}) +if(${NFD_BUILD_TESTS} OR ${NFD_BUILD_SDL2_TESTS}) add_subdirectory(test) endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f83c583f..83307eaf 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,7 +2,9 @@ set(TARGET_NAME nfd) set(PUBLIC_HEADER_FILES include/nfd.h - include/nfd.hpp) + include/nfd.hpp + include/nfd_sdl2.h + include/nfd_glfw3.h) set(SOURCE_FILES ${PUBLIC_HEADER_FILES}) diff --git a/src/include/nfd.h b/src/include/nfd.h index 7dc2ca0c..adb1c814 100644 --- a/src/include/nfd.h +++ b/src/include/nfd.h @@ -96,12 +96,31 @@ typedef struct { typedef nfdu8filteritem_t nfdnfilteritem_t; #endif // _WIN32 +// The native window handle type. +enum { + NFD_WINDOW_HANDLE_TYPE_UNSET = 0, + // Windows: handle is HWND (the Windows API typedefs this to void*) + NFD_WINDOW_HANDLE_TYPE_WINDOWS = 1, + // Cocoa: handle is NSWindow* + NFD_WINDOW_HANDLE_TYPE_COCOA = 2, + // X11: handle is Window + NFD_WINDOW_HANDLE_TYPE_X11 = 3, + // Wayland support will be implemented separately in the future +}; +// The native window handle. If using a platform abstraction framework (e.g. SDL2), this should be +// obtained using the corresponding NFD glue header (e.g. nfd_sdl2.h). +typedef struct { + size_t type; // this is one of the values of the enum above + void* handle; +} nfdwindowhandle_t; + typedef size_t nfdversion_t; typedef struct { const nfdu8filteritem_t* filterList; nfdfiltersize_t filterCount; const nfdu8char_t* defaultPath; + nfdwindowhandle_t parentWindow; } nfdopendialogu8args_t; #ifdef _WIN32 @@ -109,6 +128,7 @@ typedef struct { const nfdnfilteritem_t* filterList; nfdfiltersize_t filterCount; const nfdnchar_t* defaultPath; + nfdwindowhandle_t parentWindow; } nfdopendialognargs_t; #else typedef nfdopendialogu8args_t nfdopendialognargs_t; @@ -119,6 +139,7 @@ typedef struct { nfdfiltersize_t filterCount; const nfdu8char_t* defaultPath; const nfdu8char_t* defaultName; + nfdwindowhandle_t parentWindow; } nfdsavedialogu8args_t; #ifdef _WIN32 @@ -127,6 +148,7 @@ typedef struct { nfdfiltersize_t filterCount; const nfdnchar_t* defaultPath; const nfdnchar_t* defaultName; + nfdwindowhandle_t parentWindow; } nfdsavedialognargs_t; #else typedef nfdsavedialogu8args_t nfdsavedialognargs_t; @@ -134,11 +156,13 @@ typedef nfdsavedialogu8args_t nfdsavedialognargs_t; typedef struct { const nfdu8char_t* defaultPath; + nfdwindowhandle_t parentWindow; } nfdpickfolderu8args_t; #ifdef _WIN32 typedef struct { const nfdnchar_t* defaultPath; + nfdwindowhandle_t parentWindow; } nfdpickfoldernargs_t; #else typedef nfdpickfolderu8args_t nfdpickfoldernargs_t; diff --git a/src/include/nfd.hpp b/src/include/nfd.hpp index 787a1029..da232ae8 100644 --- a/src/include/nfd.hpp +++ b/src/include/nfd.hpp @@ -37,16 +37,18 @@ inline void FreePath(nfdnchar_t* outPath) noexcept { inline nfdresult_t OpenDialog(nfdnchar_t*& outPath, const nfdnfilteritem_t* filterList = nullptr, nfdfiltersize_t filterCount = 0, - const nfdnchar_t* defaultPath = nullptr) noexcept { - const nfdopendialognargs_t args{filterList, filterCount, defaultPath}; + const nfdnchar_t* defaultPath = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { + const nfdopendialognargs_t args{filterList, filterCount, defaultPath, parentWindow}; return ::NFD_OpenDialogN_With(&outPath, &args); } inline nfdresult_t OpenDialogMultiple(const nfdpathset_t*& outPaths, const nfdnfilteritem_t* filterList = nullptr, nfdfiltersize_t filterCount = 0, - const nfdnchar_t* defaultPath = nullptr) noexcept { - const nfdopendialognargs_t args{filterList, filterCount, defaultPath}; + const nfdnchar_t* defaultPath = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { + const nfdopendialognargs_t args{filterList, filterCount, defaultPath, parentWindow}; return ::NFD_OpenDialogMultipleN_With(&outPaths, &args); } @@ -54,20 +56,24 @@ inline nfdresult_t SaveDialog(nfdnchar_t*& outPath, const nfdnfilteritem_t* filterList = nullptr, nfdfiltersize_t filterCount = 0, const nfdnchar_t* defaultPath = nullptr, - const nfdnchar_t* defaultName = nullptr) noexcept { - const nfdsavedialognargs_t args{filterList, filterCount, defaultPath, defaultName}; + const nfdnchar_t* defaultName = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { + const nfdsavedialognargs_t args{ + filterList, filterCount, defaultPath, defaultName, parentWindow}; return ::NFD_SaveDialogN_With(&outPath, &args); } inline nfdresult_t PickFolder(nfdnchar_t*& outPath, - const nfdnchar_t* defaultPath = nullptr) noexcept { - const nfdpickfoldernargs_t args{defaultPath}; + const nfdnchar_t* defaultPath = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { + const nfdpickfoldernargs_t args{defaultPath, parentWindow}; return ::NFD_PickFolderN_With(&outPath, &args); } inline nfdresult_t PickFolderMultiple(const nfdpathset_t*& outPaths, - const nfdnchar_t* defaultPath = nullptr) noexcept { - const nfdpickfoldernargs_t args{defaultPath}; + const nfdnchar_t* defaultPath = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { + const nfdpickfoldernargs_t args{defaultPath, parentWindow}; return ::NFD_PickFolderMultipleN_With(&outPaths, &args); } @@ -110,16 +116,18 @@ inline void FreePath(nfdu8char_t* outPath) noexcept { inline nfdresult_t OpenDialog(nfdu8char_t*& outPath, const nfdu8filteritem_t* filterList = nullptr, nfdfiltersize_t filterCount = 0, - const nfdu8char_t* defaultPath = nullptr) noexcept { - const nfdopendialogu8args_t args{filterList, filterCount, defaultPath}; + const nfdu8char_t* defaultPath = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { + const nfdopendialogu8args_t args{filterList, filterCount, defaultPath, parentWindow}; return ::NFD_OpenDialogU8_With(&outPath, &args); } inline nfdresult_t OpenDialogMultiple(const nfdpathset_t*& outPaths, const nfdu8filteritem_t* filterList = nullptr, nfdfiltersize_t filterCount = 0, - const nfdu8char_t* defaultPath = nullptr) noexcept { - const nfdopendialogu8args_t args{filterList, filterCount, defaultPath}; + const nfdu8char_t* defaultPath = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { + const nfdopendialogu8args_t args{filterList, filterCount, defaultPath, parentWindow}; return ::NFD_OpenDialogMultipleU8_With(&outPaths, &args); } @@ -127,20 +135,24 @@ inline nfdresult_t SaveDialog(nfdu8char_t*& outPath, const nfdu8filteritem_t* filterList = nullptr, nfdfiltersize_t filterCount = 0, const nfdu8char_t* defaultPath = nullptr, - const nfdu8char_t* defaultName = nullptr) noexcept { - const nfdsavedialogu8args_t args{filterList, filterCount, defaultPath, defaultName}; + const nfdu8char_t* defaultName = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { + const nfdsavedialogu8args_t args{ + filterList, filterCount, defaultPath, defaultName, parentWindow}; return ::NFD_SaveDialogU8_With(&outPath, &args); } inline nfdresult_t PickFolder(nfdu8char_t*& outPath, - const nfdu8char_t* defaultPath = nullptr) noexcept { - const nfdpickfolderu8args_t args{defaultPath}; + const nfdu8char_t* defaultPath = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { + const nfdpickfolderu8args_t args{defaultPath, parentWindow}; return ::NFD_PickFolderU8_With(&outPath, &args); } inline nfdresult_t PickFolderMultiple(const nfdpathset_t*& outPaths, - const nfdu8char_t* defaultPath = nullptr) noexcept { - const nfdpickfolderu8args_t args{defaultPath}; + const nfdu8char_t* defaultPath = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { + const nfdpickfolderu8args_t args{defaultPath, parentWindow}; return ::NFD_PickFolderMultipleU8_With(&outPaths, &args); } @@ -205,9 +217,10 @@ typedef std::unique_ptr> UniquePath inline nfdresult_t OpenDialog(UniquePathN& outPath, const nfdnfilteritem_t* filterList = nullptr, nfdfiltersize_t filterCount = 0, - const nfdnchar_t* defaultPath = nullptr) noexcept { + const nfdnchar_t* defaultPath = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { nfdnchar_t* out; - nfdresult_t res = OpenDialog(out, filterList, filterCount, defaultPath); + nfdresult_t res = OpenDialog(out, filterList, filterCount, defaultPath, parentWindow); if (res == NFD_OKAY) { outPath.reset(out); } @@ -217,9 +230,10 @@ inline nfdresult_t OpenDialog(UniquePathN& outPath, inline nfdresult_t OpenDialogMultiple(UniquePathSet& outPaths, const nfdnfilteritem_t* filterList = nullptr, nfdfiltersize_t filterCount = 0, - const nfdnchar_t* defaultPath = nullptr) noexcept { + const nfdnchar_t* defaultPath = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { const nfdpathset_t* out; - nfdresult_t res = OpenDialogMultiple(out, filterList, filterCount, defaultPath); + nfdresult_t res = OpenDialogMultiple(out, filterList, filterCount, defaultPath, parentWindow); if (res == NFD_OKAY) { outPaths.reset(out); } @@ -230,9 +244,11 @@ inline nfdresult_t SaveDialog(UniquePathN& outPath, const nfdnfilteritem_t* filterList = nullptr, nfdfiltersize_t filterCount = 0, const nfdnchar_t* defaultPath = nullptr, - const nfdnchar_t* defaultName = nullptr) noexcept { + const nfdnchar_t* defaultName = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { nfdnchar_t* out; - nfdresult_t res = SaveDialog(out, filterList, filterCount, defaultPath, defaultName); + nfdresult_t res = + SaveDialog(out, filterList, filterCount, defaultPath, defaultName, parentWindow); if (res == NFD_OKAY) { outPath.reset(out); } @@ -240,9 +256,10 @@ inline nfdresult_t SaveDialog(UniquePathN& outPath, } inline nfdresult_t PickFolder(UniquePathN& outPath, - const nfdnchar_t* defaultPath = nullptr) noexcept { + const nfdnchar_t* defaultPath = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { nfdnchar_t* out; - nfdresult_t res = PickFolder(out, defaultPath); + nfdresult_t res = PickFolder(out, defaultPath, parentWindow); if (res == NFD_OKAY) { outPath.reset(out); } @@ -250,9 +267,10 @@ inline nfdresult_t PickFolder(UniquePathN& outPath, } inline nfdresult_t PickFolderMultiple(UniquePathSet& outPaths, - const nfdnchar_t* defaultPath = nullptr) noexcept { + const nfdnchar_t* defaultPath = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { const nfdpathset_t* out; - nfdresult_t res = PickFolderMultiple(out, defaultPath); + nfdresult_t res = PickFolderMultiple(out, defaultPath, parentWindow); if (res == NFD_OKAY) { outPaths.reset(out); } @@ -263,9 +281,10 @@ inline nfdresult_t PickFolderMultiple(UniquePathSet& outPaths, inline nfdresult_t OpenDialog(UniquePathU8& outPath, const nfdu8filteritem_t* filterList = nullptr, nfdfiltersize_t filterCount = 0, - const nfdu8char_t* defaultPath = nullptr) noexcept { + const nfdu8char_t* defaultPath = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { nfdu8char_t* out; - nfdresult_t res = OpenDialog(out, filterList, filterCount, defaultPath); + nfdresult_t res = OpenDialog(out, filterList, filterCount, defaultPath, parentWindow); if (res == NFD_OKAY) { outPath.reset(out); } @@ -275,9 +294,10 @@ inline nfdresult_t OpenDialog(UniquePathU8& outPath, inline nfdresult_t OpenDialogMultiple(UniquePathSet& outPaths, const nfdu8filteritem_t* filterList = nullptr, nfdfiltersize_t filterCount = 0, - const nfdu8char_t* defaultPath = nullptr) noexcept { + const nfdu8char_t* defaultPath = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { const nfdpathset_t* out; - nfdresult_t res = OpenDialogMultiple(out, filterList, filterCount, defaultPath); + nfdresult_t res = OpenDialogMultiple(out, filterList, filterCount, defaultPath, parentWindow); if (res == NFD_OKAY) { outPaths.reset(out); } @@ -288,9 +308,11 @@ inline nfdresult_t SaveDialog(UniquePathU8& outPath, const nfdu8filteritem_t* filterList = nullptr, nfdfiltersize_t filterCount = 0, const nfdu8char_t* defaultPath = nullptr, - const nfdu8char_t* defaultName = nullptr) noexcept { + const nfdu8char_t* defaultName = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { nfdu8char_t* out; - nfdresult_t res = SaveDialog(out, filterList, filterCount, defaultPath, defaultName); + nfdresult_t res = + SaveDialog(out, filterList, filterCount, defaultPath, defaultName, parentWindow); if (res == NFD_OKAY) { outPath.reset(out); } @@ -298,9 +320,10 @@ inline nfdresult_t SaveDialog(UniquePathU8& outPath, } inline nfdresult_t PickFolder(UniquePathU8& outPath, - const nfdu8char_t* defaultPath = nullptr) noexcept { + const nfdu8char_t* defaultPath = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { nfdu8char_t* out; - nfdresult_t res = PickFolder(out, defaultPath); + nfdresult_t res = PickFolder(out, defaultPath, parentWindow); if (res == NFD_OKAY) { outPath.reset(out); } @@ -308,9 +331,10 @@ inline nfdresult_t PickFolder(UniquePathU8& outPath, } inline nfdresult_t PickFolderMultiple(UniquePathSet& outPaths, - const nfdu8char_t* defaultPath = nullptr) noexcept { + const nfdu8char_t* defaultPath = nullptr, + nfdwindowhandle_t parentWindow = {}) noexcept { const nfdpathset_t* out; - nfdresult_t res = PickFolderMultiple(out, defaultPath); + nfdresult_t res = PickFolderMultiple(out, defaultPath, parentWindow); if (res == NFD_OKAY) { outPaths.reset(out); } diff --git a/src/include/nfd_glfw3.h b/src/include/nfd_glfw3.h new file mode 100644 index 00000000..c4ec5307 --- /dev/null +++ b/src/include/nfd_glfw3.h @@ -0,0 +1,85 @@ +/* + Native File Dialog Extended + Repository: https://github.com/btzy/nativefiledialog-extended + License: Zlib + Authors: Bernard Teo + + This header contains a function to convert a GLFW window handle to a native window handle for + passing to NFDe. + */ + +#ifndef _NFD_GLFW3_H +#define _NFD_GLFW3_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#define NFD_INLINE inline +#else +#define NFD_INLINE static inline +#endif // __cplusplus + +/** + * Converts a GLFW window handle to a native window handle that can be passed to NFDe. + * @param sdlWindow The GLFW window handle. + * @param[out] nativeWindow The output native window handle, populated if and only if this function + * returns true. + * @return Either true to indicate success, or false to indicate failure. It is intended that + * users ignore the error and simply pass a value-initialized nfdwindowhandle_t to NFDe if this + * function fails. */ +NFD_INLINE bool NFD_GetNativeWindowFromGLFWWindow(GLFWwindow* glfwWindow, + nfdwindowhandle_t* nativeWindow) { + GLFWerrorfun oldCallback = glfwSetErrorCallback(NULL); + bool success = false; +#if defined(GLFW_EXPOSE_NATIVE_WIN32) + if (!success) { + const HWND hwnd = glfwGetWin32Window(glfwWindow); + if (hwnd) { + nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_WINDOWS; + nativeWindow->handle = (void*)hwnd; + success = true; + } + } +#endif +#if defined(GLFW_EXPOSE_NATIVE_COCOA) + if (!success) { + const id cocoa_window = glfwGetCocoaWindow(glfwWindow); + if (cocoa_window) { + nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_COCOA; + nativeWindow->handle = (void*)cocoa_window; + success = true; + } + } +#endif +#if defined(GLFW_EXPOSE_NATIVE_X11) + if (!success) { + const Window x11_window = glfwGetX11Window(glfwWindow); + if (x11_window != None) { + nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_X11; + nativeWindow->handle = (void*)x11_window; + success = true; + } + } +#endif +#if defined(GLFW_EXPOSE_NATIVE_WAYLAND) + // For now we don't support Wayland, but we intend to support it eventually. + // Silence the warnings. + { + (void)glfwWindow; + (void)nativeWindow; + } +#endif + glfwSetErrorCallback(oldCallback); + return success; +} + +#undef NFD_INLINE +#ifdef __cplusplus +} +#endif // __cplusplus + +#endif // _NFD_GLFW3_H diff --git a/src/include/nfd_sdl2.h b/src/include/nfd_sdl2.h new file mode 100644 index 00000000..5703762e --- /dev/null +++ b/src/include/nfd_sdl2.h @@ -0,0 +1,76 @@ +/* + Native File Dialog Extended + Repository: https://github.com/btzy/nativefiledialog-extended + License: Zlib + Authors: Bernard Teo + + This header contains a function to convert an SDL window handle to a native window handle for + passing to NFDe. + + This is meant to be used with SDL2, but if there are incompatibilities with future SDL versions, + we can conditionally compile based on SDL_MAJOR_VERSION. + */ + +#ifndef _NFD_SDL2_H +#define _NFD_SDL2_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#define NFD_INLINE inline +#else +#define NFD_INLINE static inline +#endif // __cplusplus + +/** + * Converts an SDL window handle to a native window handle that can be passed to NFDe. + * @param sdlWindow The SDL window handle. + * @param[out] nativeWindow The output native window handle, populated if and only if this function + * returns true. + * @return Either true to indicate success, or false to indicate failure. If false is returned, + * you can call SDL_GetError() for more information. However, it is intended that users ignore the + * error and simply pass a value-initialized nfdwindowhandle_t to NFDe if this function fails. */ +NFD_INLINE bool NFD_GetNativeWindowFromSDLWindow(SDL_Window* sdlWindow, + nfdwindowhandle_t* nativeWindow) { + SDL_SysWMinfo info; + SDL_VERSION(&info.version); + if (!SDL_GetWindowWMInfo(sdlWindow, &info)) { + return false; + } + switch (info.subsystem) { +#if defined(SDL_VIDEO_DRIVER_WINDOWS) + case SDL_SYSWM_WINDOWS: + nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_WINDOWS; + nativeWindow->handle = (void*)info.info.win.window; + return true; +#endif +#if defined(SDL_VIDEO_DRIVER_COCOA) + case SDL_SYSWM_COCOA: + nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_COCOA; + nativeWindow->handle = (void*)info.info.cocoa.window; + return true; +#endif +#if defined(SDL_VIDEO_DRIVER_X11) + case SDL_SYSWM_X11: + nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_X11; + nativeWindow->handle = (void*)info.info.x11.window; + return true; +#endif + default: + // Silence the warning in case we are not using a supported backend. + (void)nativeWindow; + SDL_SetError("Unsupported native window type."); + return false; + } +} + +#undef NFD_INLINE +#ifdef __cplusplus +} +#endif // __cplusplus + +#endif // _NFD_SDL2_H diff --git a/src/nfd_cocoa.m b/src/nfd_cocoa.m index 9f1fa3b5..4bcea220 100644 --- a/src/nfd_cocoa.m +++ b/src/nfd_cocoa.m @@ -174,6 +174,13 @@ static nfdresult_t CopyUtf8String(const char* utf8Str, nfdnchar_t** out) { return NFD_ERROR; } +static NSWindow* GetNativeWindowHandle(const nfdwindowhandle_t* parentWindow) { + if (parentWindow->type != NFD_WINDOW_HANDLE_TYPE_COCOA) { + return NULL; + } + return (NSWindow*)parentWindow->handle; +} + /* public */ const char* NFD_GetError(void) { @@ -230,7 +237,12 @@ nfdresult_t NFD_OpenDialogN_With_Impl(nfdversion_t version, nfdresult_t result = NFD_CANCEL; @autoreleasepool { - NSWindow* keyWindow = [[NSApplication sharedApplication] keyWindow]; + NSWindow* keyWindow = GetNativeWindowHandle(&args->parentWindow); + if (keyWindow) { + [keyWindow makeKeyAndOrderFront:nil]; + } else { + keyWindow = [[NSApplication sharedApplication] keyWindow]; + } NSOpenPanel* dialog = [NSOpenPanel openPanel]; [dialog setAllowsMultipleSelection:NO]; @@ -285,7 +297,12 @@ nfdresult_t NFD_OpenDialogMultipleN_With_Impl(nfdversion_t version, nfdresult_t result = NFD_CANCEL; @autoreleasepool { - NSWindow* keyWindow = [[NSApplication sharedApplication] keyWindow]; + NSWindow* keyWindow = GetNativeWindowHandle(&args->parentWindow); + if (keyWindow) { + [keyWindow makeKeyAndOrderFront:nil]; + } else { + keyWindow = [[NSApplication sharedApplication] keyWindow]; + } NSOpenPanel* dialog = [NSOpenPanel openPanel]; [dialog setAllowsMultipleSelection:YES]; @@ -347,7 +364,12 @@ nfdresult_t NFD_SaveDialogN_With_Impl(nfdversion_t version, nfdresult_t result = NFD_CANCEL; @autoreleasepool { - NSWindow* keyWindow = [[NSApplication sharedApplication] keyWindow]; + NSWindow* keyWindow = GetNativeWindowHandle(&args->parentWindow); + if (keyWindow) { + [keyWindow makeKeyAndOrderFront:nil]; + } else { + keyWindow = [[NSApplication sharedApplication] keyWindow]; + } NSSavePanel* dialog = [NSSavePanel savePanel]; [dialog setExtensionHidden:NO]; @@ -404,7 +426,12 @@ nfdresult_t NFD_PickFolderN_With_Impl(nfdversion_t version, nfdresult_t result = NFD_CANCEL; @autoreleasepool { - NSWindow* keyWindow = [[NSApplication sharedApplication] keyWindow]; + NSWindow* keyWindow = GetNativeWindowHandle(&args->parentWindow); + if (keyWindow) { + [keyWindow makeKeyAndOrderFront:nil]; + } else { + keyWindow = [[NSApplication sharedApplication] keyWindow]; + } NSOpenPanel* dialog = [NSOpenPanel openPanel]; [dialog setAllowsMultipleSelection:NO]; @@ -451,7 +478,12 @@ nfdresult_t NFD_PickFolderMultipleN_With_Impl(nfdversion_t version, nfdresult_t result = NFD_CANCEL; @autoreleasepool { - NSWindow* keyWindow = [[NSApplication sharedApplication] keyWindow]; + NSWindow* keyWindow = GetNativeWindowHandle(&args->parentWindow); + if (keyWindow) { + [keyWindow makeKeyAndOrderFront:nil]; + } else { + keyWindow = [[NSApplication sharedApplication] keyWindow]; + } NSOpenPanel* dialog = [NSOpenPanel openPanel]; [dialog setAllowsMultipleSelection:YES]; diff --git a/src/nfd_gtk.cpp b/src/nfd_gtk.cpp index 6f519558..c70d7dd4 100644 --- a/src/nfd_gtk.cpp +++ b/src/nfd_gtk.cpp @@ -381,6 +381,113 @@ gint RunDialogWithFocus(GtkDialog* dialog) { return gtk_dialog_run(dialog); } +// Gets the GdkWindow from the given window handle. This function might fail even if parentWindow +// is set correctly, since it calls some failable GDK functions. If it fails, it will return +// nullptr. The caller is responsible for freeing ths returned GdkWindow, if not nullptr. +GdkWindow* GetAllocNativeWindowHandle(const nfdwindowhandle_t& parentWindow, + GdkDisplayManager*& outDisplayManager, + GdkDisplay*& outDisplay) { + switch (parentWindow.type) { +#if defined(GDK_WINDOWING_X11) + case NFD_WINDOW_HANDLE_TYPE_X11: { + const Window x11_handle = reinterpret_cast(parentWindow.handle); + // AFAIK, _any_ X11 display will do, because Windows are not associated to a specific + // Display. Supposedly, a Display is just a connection to the X server. + + // This will contain the X11 display we want to use. + GdkDisplay* x11_display = nullptr; + GdkDisplayManager* display_manager = gdk_display_manager_get(); + + // If we can find an existing X11 display, use it. + GSList* gdk_display_list = gdk_display_manager_list_displays(display_manager); + while (gdk_display_list) { + GSList* node = gdk_display_list; + GdkDisplay* display = GDK_DISPLAY(node->data); + if (GDK_IS_X11_DISPLAY(display)) { + g_slist_free(node); + x11_display = display; + break; + } else { + gdk_display_list = node->next; + g_slist_free_1(node); + } + } + + // Otherwise, we have to create our own X11 display. + if (!x11_display) { + // This is not very nice, because we are always resetting the allowed backends + // setting to NULL (which means all backends are allowed), even though we can't be + // sure that the user didn't call gdk_set_allowed_backends() earlier to force a + // specific backend. But well if the user doesn't have an X11 display already open + // and yet is telling us with have an X11 window as parent, they probably don't use + // GTK in their application at all so they probably won't notice this. + // + // There is no way, AFAIK, to get the allowed backends first so we can restore it + // later, and gdk_x11_display_open() is GTK4-only (the GTK3 version is a private + // implementation detail). + // + // Also, we don't close the display we specially opened, since GTK will need it to + // show the dialog. Though it probably doesn't matter very much if we want to free + // up resources and clean it up. + gdk_set_allowed_backends("x11"); + x11_display = gdk_display_manager_open_display(display_manager, NULL); + gdk_set_allowed_backends(NULL); + } + if (!x11_display) return nullptr; + outDisplayManager = display_manager; + outDisplay = x11_display; + GdkWindow* gdk_window = gdk_x11_window_foreign_new_for_display(x11_display, x11_handle); + return gdk_window; + } +#endif + default: + return nullptr; + } +} + +void RealizedSignalHandler(GtkWidget* window, void* userdata) { + GdkWindow* const parentWindow = static_cast(userdata); + gdk_window_set_transient_for(gtk_widget_get_window(window), parentWindow); +} + +struct NativeWindowParenter { + NativeWindowParenter(GtkWidget* widget, const nfdwindowhandle_t& parentWindow) noexcept + : widget(widget), displayManager(nullptr) { + GdkDisplay* new_display = nullptr; + parent = GetAllocNativeWindowHandle(parentWindow, displayManager, new_display); + + if (parent) { + /* set the handler to the realize signal to set the transient GDK parent */ + handlerID = g_signal_connect(G_OBJECT(widget), + "realize", + G_CALLBACK(RealizedSignalHandler), + static_cast(parent)); + + /* Set the default display to a display that we know is X11 (so that realizing the file + * dialog will use it) */ + /* Note: displayManager here must be non-null since parent is non-null */ + originalDisplay = gdk_display_manager_get_default_display(displayManager); + gdk_display_manager_set_default_display(displayManager, new_display); + } + } + ~NativeWindowParenter() { + if (parent) { + /* Set the default display back to whatever it was, to be nice */ + /* Note: displayManager here must be non-null since parent is non-null */ + gdk_display_manager_set_default_display(displayManager, originalDisplay); + + /* unset the handler and delete the parent GdkWindow */ + g_signal_handler_disconnect(G_OBJECT(widget), handlerID); + g_object_unref(parent); + } + } + GtkWidget* const widget; + GdkWindow* parent; + GdkDisplayManager* displayManager; + GdkDisplay* originalDisplay; + gulong handlerID; +}; + } // namespace const char* NFD_GetError(void) { @@ -447,7 +554,16 @@ nfdresult_t NFD_OpenDialogN_With_Impl(nfdversion_t version, /* Set the default path */ SetDefaultPath(GTK_FILE_CHOOSER(widget), args->defaultPath); - if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) { + gint result; + { + /* Parent the window properly */ + NativeWindowParenter nativeWindowParenter(widget, args->parentWindow); + + /* invoke the dialog (blocks until dialog is closed) */ + result = RunDialogWithFocus(GTK_DIALOG(widget)); + } + + if (result == GTK_RESPONSE_ACCEPT) { // write out the file name *outPath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget)); @@ -506,7 +622,16 @@ nfdresult_t NFD_OpenDialogMultipleN_With_Impl(nfdversion_t version, /* Set the default path */ SetDefaultPath(GTK_FILE_CHOOSER(widget), args->defaultPath); - if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) { + gint result; + { + /* Parent the window properly */ + NativeWindowParenter nativeWindowParenter(widget, args->parentWindow); + + /* invoke the dialog (blocks until dialog is closed) */ + result = RunDialogWithFocus(GTK_DIALOG(widget)); + } + + if (result == GTK_RESPONSE_ACCEPT) { // write out the file name GSList* fileList = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(widget)); @@ -580,8 +705,15 @@ nfdresult_t NFD_SaveDialogN_With_Impl(nfdversion_t version, G_CALLBACK(FileActivatedSignalHandler), static_cast(&buttonClickedArgs)); - /* invoke the dialog (blocks until dialog is closed) */ - gint result = RunDialogWithFocus(GTK_DIALOG(widget)); + gint result; + { + /* Parent the window properly */ + NativeWindowParenter nativeWindowParenter(widget, args->parentWindow); + + /* invoke the dialog (blocks until dialog is closed) */ + result = RunDialogWithFocus(GTK_DIALOG(widget)); + } + /* unset the handler */ g_signal_handler_disconnect(G_OBJECT(saveButton), handlerID); @@ -637,7 +769,16 @@ nfdresult_t NFD_PickFolderN_With_Impl(nfdversion_t version, /* Set the default path */ SetDefaultPath(GTK_FILE_CHOOSER(widget), args->defaultPath); - if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) { + gint result; + { + /* Parent the window properly */ + NativeWindowParenter nativeWindowParenter(widget, args->parentWindow); + + /* invoke the dialog (blocks until dialog is closed) */ + result = RunDialogWithFocus(GTK_DIALOG(widget)); + } + + if (result == GTK_RESPONSE_ACCEPT) { // write out the file name *outPath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget)); @@ -682,7 +823,16 @@ nfdresult_t NFD_PickFolderMultipleN_With_Impl(nfdversion_t version, /* Set the default path */ SetDefaultPath(GTK_FILE_CHOOSER(widget), args->defaultPath); - if (RunDialogWithFocus(GTK_DIALOG(widget)) == GTK_RESPONSE_ACCEPT) { + gint result; + { + /* Parent the window properly */ + NativeWindowParenter nativeWindowParenter(widget, args->parentWindow); + + /* invoke the dialog (blocks until dialog is closed) */ + result = RunDialogWithFocus(GTK_DIALOG(widget)); + } + + if (result == GTK_RESPONSE_ACCEPT) { // write out the file name GSList* fileList = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(widget)); diff --git a/src/nfd_portal.cpp b/src/nfd_portal.cpp index a91b71b2..77fd5d54 100644 --- a/src/nfd_portal.cpp +++ b/src/nfd_portal.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -115,6 +116,41 @@ T* transform(const T* begin, const T* end, T* out, Callback callback) { return out; } +template +T* reverse_copy(const T* begin, const T* end, T* out) { + while (begin != end) { + *out++ = *--end; + } + return out; +} + +// Returns true if ch is in [0-9A-Za-z], false otherwise. +bool IsHex(char ch) { + return ('0' <= ch && ch <= '9') || ('A' <= ch && ch <= 'F') || ('a' <= ch && ch <= 'f'); +} + +// Returns the hexadecimal value contained in the char. Precondition: IsHex(ch) +char ParseHexUnchecked(char ch) { + if ('0' <= ch && ch <= '9') return ch - '0'; + if ('A' <= ch && ch <= 'F') return ch - ('A' - 10); + if ('a' <= ch && ch <= 'f') return ch - ('a' - 10); +#if defined(__GNUC__) + __builtin_unreachable(); +#endif +} + +// Writes val as a hex string to out +char* FormatUIntToHexString(char* out, uintptr_t val) { + char tmp[sizeof(uintptr_t) * 2]; + char* tmp_end = tmp; + do { + const uintptr_t digit = val & 15u; + *tmp_end++ = digit < 10 ? '0' + digit : 'A' - 10 + digit; + val >>= 4; + } while (val != 0); + return reverse_copy(tmp, tmp_end, out); +} + constexpr const char* STR_EMPTY = ""; constexpr const char* STR_OPEN_FILE = "Open File"; constexpr const char* STR_OPEN_FILES = "Open Files"; @@ -136,6 +172,31 @@ constexpr const char* DBUS_PATH = "/org/freedesktop/portal/desktop"; constexpr const char* DBUS_FILECHOOSER_IFACE = "org.freedesktop.portal.FileChooser"; constexpr const char* DBUS_REQUEST_IFACE = "org.freedesktop.portal.Request"; +void AppendOpenFileQueryParentWindow(DBusMessageIter& iter, const nfdwindowhandle_t& parentWindow) { + switch (parentWindow.type) { + case NFD_WINDOW_HANDLE_TYPE_X11: { + constexpr size_t maxX11WindowStrLen = + 4 + sizeof(uintptr_t) * 2 + 1; // "x11:" + "" + "\0" + char serializedWindowBuf[maxX11WindowStrLen]; + char* serializedWindow = serializedWindowBuf; + const uintptr_t handle = reinterpret_cast(parentWindow.handle); + char* out = serializedWindowBuf; + *out++ = 'x'; + *out++ = '1'; + *out++ = '1'; + *out++ = ':'; + out = FormatUIntToHexString(out, handle); + *out = '\0'; + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &serializedWindow); + return; + } + default: { + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_EMPTY); + return; + } + } +} + template void AppendOpenFileQueryTitle(DBusMessageIter&); template <> @@ -557,12 +618,12 @@ void AppendOpenFileQueryParams(DBusMessage* query, const char* handle_token, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, - const nfdnchar_t* defaultPath) { + const nfdnchar_t* defaultPath, + const nfdwindowhandle_t& parentWindow) { DBusMessageIter iter; dbus_message_iter_init_append(query, &iter); - dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_EMPTY); - + AppendOpenFileQueryParentWindow(iter, parentWindow); AppendOpenFileQueryTitle(iter); DBusMessageIter sub_iter; @@ -581,12 +642,12 @@ void AppendSaveFileQueryParams(DBusMessage* query, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, const nfdnchar_t* defaultPath, - const nfdnchar_t* defaultName) { + const nfdnchar_t* defaultName, + const nfdwindowhandle_t& parentWindow) { DBusMessageIter iter; dbus_message_iter_init_append(query, &iter); - dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_EMPTY); - + AppendOpenFileQueryParentWindow(iter, parentWindow); AppendSaveFileQueryTitle(iter); DBusMessageIter sub_iter; @@ -963,21 +1024,6 @@ class DBusSignalSubscriptionHandler { } }; -// Returns true if ch is in [0-9A-Za-z], false otherwise. -bool IsHex(char ch) { - return ('0' <= ch && ch <= '9') || ('A' <= ch && ch <= 'F') || ('a' <= ch && ch <= 'f'); -} - -// Returns the hexadecimal value contained in the char. Precondition: IsHex(ch) -char ParseHexUnchecked(char ch) { - if ('0' <= ch && ch <= '9') return ch - '0'; - if ('A' <= ch && ch <= 'F') return ch - ('A' - 10); - if ('a' <= ch && ch <= 'f') return ch - ('a' - 10); -#if defined(__GNUC__) - __builtin_unreachable(); -#endif -} - // Returns true if the given file URI is decodable (i.e. not malformed), and false otherwise. // If this function returns true, then `out` will be populated with the length of the decoded URI // and `fileUriEnd` will point to the trailing null byte of `fileUri`. Otherwise, `out` and @@ -1128,7 +1174,8 @@ template nfdresult_t NFD_DBus_OpenFile(DBusMessage*& outMsg, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, - const nfdnchar_t* defaultPath) { + const nfdnchar_t* defaultPath, + const nfdwindowhandle_t& parentWindow) { const char* handle_token_ptr; char* handle_obj_path = MakeUniqueObjectPath(&handle_token_ptr); Free_Guard handle_obj_path_guard(handle_obj_path); @@ -1149,7 +1196,7 @@ nfdresult_t NFD_DBus_OpenFile(DBusMessage*& outMsg, DBUS_DESTINATION, DBUS_PATH, DBUS_FILECHOOSER_IFACE, "OpenFile"); DBusMessage_Guard query_guard(query); AppendOpenFileQueryParams( - query, handle_token_ptr, filterList, filterCount, defaultPath); + query, handle_token_ptr, filterList, filterCount, defaultPath, parentWindow); DBusMessage* reply = dbus_connection_send_with_reply_and_block(dbus_conn, query, DBUS_TIMEOUT_INFINITE, &err); @@ -1210,7 +1257,8 @@ nfdresult_t NFD_DBus_SaveFile(DBusMessage*& outMsg, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, const nfdnchar_t* defaultPath, - const nfdnchar_t* defaultName) { + const nfdnchar_t* defaultName, + const nfdwindowhandle_t& parentWindow) { const char* handle_token_ptr; char* handle_obj_path = MakeUniqueObjectPath(&handle_token_ptr); Free_Guard handle_obj_path_guard(handle_obj_path); @@ -1231,7 +1279,7 @@ nfdresult_t NFD_DBus_SaveFile(DBusMessage*& outMsg, DBUS_DESTINATION, DBUS_PATH, DBUS_FILECHOOSER_IFACE, "SaveFile"); DBusMessage_Guard query_guard(query); AppendSaveFileQueryParams( - query, handle_token_ptr, filterList, filterCount, defaultPath, defaultName); + query, handle_token_ptr, filterList, filterCount, defaultPath, defaultName, parentWindow); DBusMessage* reply = dbus_connection_send_with_reply_and_block(dbus_conn, query, DBUS_TIMEOUT_INFINITE, &err); @@ -1398,7 +1446,7 @@ nfdresult_t NFD_OpenDialogN_With_Impl(nfdversion_t version, DBusMessage* msg; { const nfdresult_t res = NFD_DBus_OpenFile( - msg, args->filterList, args->filterCount, args->defaultPath); + msg, args->filterList, args->filterCount, args->defaultPath, args->parentWindow); if (res != NFD_OKAY) { return res; } @@ -1447,7 +1495,7 @@ nfdresult_t NFD_OpenDialogMultipleN_With_Impl(nfdversion_t version, DBusMessage* msg; { const nfdresult_t res = NFD_DBus_OpenFile( - msg, args->filterList, args->filterCount, args->defaultPath); + msg, args->filterList, args->filterCount, args->defaultPath, args->parentWindow); if (res != NFD_OKAY) { return res; } @@ -1496,8 +1544,12 @@ nfdresult_t NFD_SaveDialogN_With_Impl(nfdversion_t version, DBusMessage* msg; { - const nfdresult_t res = NFD_DBus_SaveFile( - msg, args->filterList, args->filterCount, args->defaultPath, args->defaultName); + const nfdresult_t res = NFD_DBus_SaveFile(msg, + args->filterList, + args->filterCount, + args->defaultPath, + args->defaultName, + args->parentWindow); if (res != NFD_OKAY) { return res; } @@ -1570,7 +1622,8 @@ nfdresult_t NFD_PickFolderN_With_Impl(nfdversion_t version, DBusMessage* msg; { - const nfdresult_t res = NFD_DBus_OpenFile(msg, nullptr, 0, args->defaultPath); + const nfdresult_t res = + NFD_DBus_OpenFile(msg, nullptr, 0, args->defaultPath, args->parentWindow); if (res != NFD_OKAY) { return res; } @@ -1626,7 +1679,8 @@ nfdresult_t NFD_PickFolderMultipleN_With_Impl(nfdversion_t version, DBusMessage* msg; { - const nfdresult_t res = NFD_DBus_OpenFile(msg, nullptr, 0, args->defaultPath); + const nfdresult_t res = + NFD_DBus_OpenFile(msg, nullptr, 0, args->defaultPath, args->parentWindow); if (res != NFD_OKAY) { return res; } diff --git a/src/nfd_win.cpp b/src/nfd_win.cpp index 2095b3fd..4bf07e21 100644 --- a/src/nfd_win.cpp +++ b/src/nfd_win.cpp @@ -284,6 +284,13 @@ nfdresult_t AddOptions(IFileDialog* dialog, FILEOPENDIALOGOPTIONS options) { } return NFD_OKAY; } + +HWND GetNativeWindowHandle(const nfdwindowhandle_t& parentWindow) { + if (parentWindow.type != NFD_WINDOW_HANDLE_TYPE_WINDOWS) { + return nullptr; + } + return static_cast(parentWindow.handle); +} } // namespace const char* NFD_GetError(void) { @@ -385,7 +392,7 @@ nfdresult_t NFD_OpenDialogN_With_Impl(nfdversion_t version, } // Show the dialog. - result = fileOpenDialog->Show(nullptr); + result = fileOpenDialog->Show(GetNativeWindowHandle(args->parentWindow)); if (SUCCEEDED(result)) { // Get the file name ::IShellItem* psiResult; @@ -469,7 +476,7 @@ nfdresult_t NFD_OpenDialogMultipleN_With_Impl(nfdversion_t version, } // Show the dialog. - result = fileOpenDialog->Show(nullptr); + result = fileOpenDialog->Show(GetNativeWindowHandle(args->parentWindow)); if (SUCCEEDED(result)) { ::IShellItemArray* shellItems; result = fileOpenDialog->GetResults(&shellItems); @@ -552,7 +559,7 @@ nfdresult_t NFD_SaveDialogN_With_Impl(nfdversion_t version, } // Show the dialog. - result = fileSaveDialog->Show(nullptr); + result = fileSaveDialog->Show(GetNativeWindowHandle(args->parentWindow)); if (SUCCEEDED(result)) { // Get the file name ::IShellItem* psiResult; @@ -618,7 +625,7 @@ nfdresult_t NFD_PickFolderN_With_Impl(nfdversion_t version, } // Show the dialog to the user - const HRESULT result = fileOpenDialog->Show(nullptr); + const HRESULT result = fileOpenDialog->Show(GetNativeWindowHandle(args->parentWindow)); if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED)) { return NFD_CANCEL; } else if (!SUCCEEDED(result)) { @@ -685,7 +692,7 @@ nfdresult_t NFD_PickFolderMultipleN_With_Impl(nfdversion_t version, } // Show the dialog. - const HRESULT result = fileOpenDialog->Show(nullptr); + const HRESULT result = fileOpenDialog->Show(GetNativeWindowHandle(args->parentWindow)); if (SUCCEEDED(result)) { ::IShellItemArray* shellItems; if (!SUCCEEDED(fileOpenDialog->GetResults(&shellItems))) { @@ -948,7 +955,7 @@ nfdresult_t NFD_OpenDialogU8_With_Impl(nfdversion_t version, // call the native function nfdnchar_t* outPathN; const nfdopendialognargs_t argsN{ - filterItemsNGuard.data, args->filterCount, defaultPathNGuard.data}; + filterItemsNGuard.data, args->filterCount, defaultPathNGuard.data, args->parentWindow}; nfdresult_t res = NFD_OpenDialogN_With_Impl(NFD_INTERFACE_VERSION, &outPathN, &argsN); if (res != NFD_OKAY) { @@ -996,7 +1003,7 @@ nfdresult_t NFD_OpenDialogMultipleU8_With_Impl(nfdversion_t version, // call the native function const nfdopendialognargs_t argsN{ - filterItemsNGuard.data, args->filterCount, defaultPathNGuard.data}; + filterItemsNGuard.data, args->filterCount, defaultPathNGuard.data, args->parentWindow}; return NFD_OpenDialogMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, &argsN); } @@ -1039,8 +1046,11 @@ nfdresult_t NFD_SaveDialogU8_With_Impl(nfdversion_t version, // call the native function nfdnchar_t* outPathN; - const nfdsavedialognargs_t argsN{ - filterItemsNGuard.data, args->filterCount, defaultPathNGuard.data, defaultNameNGuard.data}; + const nfdsavedialognargs_t argsN{filterItemsNGuard.data, + args->filterCount, + defaultPathNGuard.data, + defaultNameNGuard.data, + args->parentWindow}; nfdresult_t res = NFD_SaveDialogN_With_Impl(NFD_INTERFACE_VERSION, &outPathN, &argsN); if (res != NFD_OKAY) { @@ -1077,7 +1087,7 @@ nfdresult_t NFD_PickFolderU8_With_Impl(nfdversion_t version, // call the native function nfdnchar_t* outPathN; - const nfdpickfoldernargs_t argsN{defaultPathNGuard.data}; + const nfdpickfoldernargs_t argsN{defaultPathNGuard.data, args->parentWindow}; nfdresult_t res = NFD_PickFolderN_With_Impl(NFD_INTERFACE_VERSION, &outPathN, &argsN); if (res != NFD_OKAY) { @@ -1114,7 +1124,7 @@ nfdresult_t NFD_PickFolderMultipleU8_With_Impl(nfdversion_t version, NormalizePathSeparator(defaultPathNGuard.data); // call the native function - const nfdpickfoldernargs_t argsN{defaultPathNGuard.data}; + const nfdpickfoldernargs_t argsN{defaultPathNGuard.data, args->parentWindow}; return NFD_PickFolderMultipleN_With_Impl(NFD_INTERFACE_VERSION, outPaths, &argsN); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 95f42a87..59d66816 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,31 +1,46 @@ +if(${NFD_BUILD_TESTS}) + set(TEST_LIST + test_opendialog.c + test_opendialog_cpp.cpp + test_opendialog_native.c + test_opendialog_with.c + test_opendialog_native_with.c + test_opendialogmultiple.c + test_opendialogmultiple_cpp.cpp + test_opendialogmultiple_native.c + test_opendialogmultiple_enum.c + test_opendialogmultiple_enum_native.c + test_pickfolder.c + test_pickfolder_cpp.cpp + test_pickfolder_native.c + test_pickfolder_with.c + test_pickfolder_native_with.c + test_pickfoldermultiple.c + test_pickfoldermultiple_native.c + test_savedialog.c + test_savedialog_native.c + test_savedialog_with.c + test_savedialog_native_with.c) -set(TEST_LIST - test_opendialog.c - test_opendialog_cpp.cpp - test_opendialog_native.c - test_opendialog_with.c - test_opendialog_native_with.c - test_opendialogmultiple.c - test_opendialogmultiple_cpp.cpp - test_opendialogmultiple_native.c - test_opendialogmultiple_enum.c - test_opendialogmultiple_enum_native.c - test_pickfolder.c - test_pickfolder_cpp.cpp - test_pickfolder_native.c - test_pickfolder_with.c - test_pickfolder_native_with.c - test_pickfoldermultiple.c - test_pickfoldermultiple_native.c - test_savedialog.c - test_savedialog_native.c - test_savedialog_with.c - test_savedialog_native_with.c) - -foreach (TEST ${TEST_LIST}) - string(REPLACE "." "_" CLEAN_TEST_NAME ${TEST}) - add_executable(${CLEAN_TEST_NAME} - ${TEST}) - target_link_libraries(${CLEAN_TEST_NAME} - PUBLIC nfd) -endforeach() + foreach (TEST ${TEST_LIST}) + string(REPLACE "." "_" CLEAN_TEST_NAME ${TEST}) + add_executable(${CLEAN_TEST_NAME} + ${TEST}) + target_link_libraries(${CLEAN_TEST_NAME} + PRIVATE nfd) + endforeach() +endif() + +if(${NFD_BUILD_SDL2_TESTS}) + find_package(PkgConfig REQUIRED) + pkg_check_modules(SDL2 REQUIRED sdl2 SDL2_ttf) + if(WIN32) + add_executable(test_sdl2 WIN32 test_sdl.c test_sdl.manifest) + else() + add_executable(test_sdl2 test_sdl.c) + endif() + target_link_libraries(test_sdl2 PRIVATE nfd) + target_include_directories(test_sdl2 PRIVATE ${SDL2_INCLUDE_DIRS}) + target_link_libraries(test_sdl2 PRIVATE ${SDL2_LINK_LIBRARIES}) + target_compile_options(test_sdl2 PUBLIC ${SDL2_CFLAGS_OTHER}) +endif() diff --git a/test/test_sdl.c b/test/test_sdl.c new file mode 100644 index 00000000..9135b955 --- /dev/null +++ b/test/test_sdl.c @@ -0,0 +1,410 @@ +#define SDL_MAIN_HANDLED +#include +#include +#include +#include +#include +#include +#include + +// Small program meant to demonstrate and test nfd_sdl2.h with SDL2. Note that it quits immediately +// when it encounters an error, without calling the opposite destroy/quit function. A real-world +// application should call destroy/quit appropriately. + +void show_error(const char* message, SDL_Window* window) { + if (SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", message, window) != 0) { + printf("SDL_ShowSimpleMessageBox failed: %s\n", SDL_GetError()); + return; + } +} + +void show_path(const char* path, SDL_Window* window) { + if (SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "Success", path, window) != 0) { + printf("SDL_ShowSimpleMessageBox failed: %s\n", SDL_GetError()); + return; + } +} + +void show_paths(const nfdpathset_t* paths, SDL_Window* window) { + size_t num_chars = 0; + + nfdpathsetsize_t num_paths; + if (NFD_PathSet_GetCount(paths, &num_paths) != NFD_OKAY) { + printf("NFD_PathSet_GetCount failed: %s\n", NFD_GetError()); + return; + } + + nfdpathsetsize_t i; + for (i = 0; i != num_paths; ++i) { + char* path; + if (NFD_PathSet_GetPathU8(paths, i, &path) != NFD_OKAY) { + printf("NFD_PathSet_GetPathU8 failed: %s\n", NFD_GetError()); + return; + } + num_chars += strlen(path) + 1; + NFD_PathSet_FreePathU8(path); + } + + char* message = malloc(num_chars); + message[0] = '\0'; + + for (i = 0; i != num_paths; ++i) { + if (i != 0) { + strcat(message, "\n"); + } + char* path; + if (NFD_PathSet_GetPathU8(paths, i, &path) != NFD_OKAY) { + printf("NFD_PathSet_GetPathU8 failed: %s\n", NFD_GetError()); + free(message); + return; + } + strcat(message, path); + NFD_PathSet_FreePathU8(path); + } + + if (SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "Success", message, window) != 0) { + printf("SDL_ShowSimpleMessageBox failed: %s\n", SDL_GetError()); + free(message); + return; + } + + free(message); +} + +void set_native_window(SDL_Window* sdlWindow, nfdwindowhandle_t* nativeWindow) { + if (!NFD_GetNativeWindowFromSDLWindow(sdlWindow, nativeWindow)) { + printf("NFD_GetNativeWindowFromSDLWindow failed: %s\n", SDL_GetError()); + } +} + +void opendialog_handler(SDL_Window* window) { + char* path; + nfdopendialogu8args_t args = {0}; + set_native_window(window, &args.parentWindow); + const nfdresult_t res = NFD_OpenDialogU8_With(&path, &args); + switch (res) { + case NFD_OKAY: + show_path(path, window); + NFD_FreePathU8(path); + break; + case NFD_ERROR: + show_error(NFD_GetError(), window); + break; + default: + break; + } +} + +void opendialogmultiple_handler(SDL_Window* window) { + const nfdpathset_t* paths; + nfdopendialogu8args_t args = {0}; + set_native_window(window, &args.parentWindow); + const nfdresult_t res = NFD_OpenDialogMultipleU8_With(&paths, &args); + switch (res) { + case NFD_OKAY: + show_paths(paths, window); + NFD_PathSet_Free(paths); + break; + case NFD_ERROR: + show_error(NFD_GetError(), window); + break; + default: + break; + } +} + +void savedialog_handler(SDL_Window* window) { + char* path; + nfdsavedialogu8args_t args = {0}; + set_native_window(window, &args.parentWindow); + const nfdresult_t res = NFD_SaveDialogU8_With(&path, &args); + switch (res) { + case NFD_OKAY: + show_path(path, window); + NFD_FreePathU8(path); + break; + case NFD_ERROR: + show_error(NFD_GetError(), window); + break; + default: + break; + } +} + +void pickfolder_handler(SDL_Window* window) { + char* path; + nfdpickfolderu8args_t args = {0}; + set_native_window(window, &args.parentWindow); + const nfdresult_t res = NFD_PickFolderU8_With(&path, &args); + switch (res) { + case NFD_OKAY: + show_path(path, window); + NFD_FreePathU8(path); + break; + case NFD_ERROR: + show_error(NFD_GetError(), window); + break; + default: + break; + } +} + +void pickfoldermultiple_handler(SDL_Window* window) { + const nfdpathset_t* paths; + nfdpickfolderu8args_t args = {0}; + set_native_window(window, &args.parentWindow); + const nfdresult_t res = NFD_PickFolderMultipleU8_With(&paths, &args); + switch (res) { + case NFD_OKAY: + show_paths(paths, window); + NFD_PathSet_Free(paths); + break; + case NFD_ERROR: + show_error(NFD_GetError(), window); + break; + default: + break; + } +} + +#if defined(_WIN32) +const char font_file[] = "C:\\Windows\\Fonts\\calibri.ttf"; +#elif defined(__APPLE__) +const char font_file[] = "/System/Library/Fonts/SFNS.ttf"; +#else +const char font_file[] = "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf"; +#endif + +#define NUM_STATES 3 +#define NUM_BUTTONS 5 +const char* button_text[NUM_BUTTONS] = {"Open File", + "Open Files", + "Save File", + "Select Folder", + "Select Folders"}; +const int BUTTON_WIDTH = 400; +const int BUTTON_HEIGHT = 40; + +void (*button_handler[NUM_BUTTONS])(SDL_Window*) = {&opendialog_handler, + &opendialogmultiple_handler, + &savedialog_handler, + &pickfolder_handler, + &pickfoldermultiple_handler}; + +#ifdef _WIN32 +int WINAPI WinMain(void) +#else +int main(void) +#endif +{ +#ifdef _WIN32 + // Enable DPI awareness on Windows + SDL_SetHint("SDL_HINT_WINDOWS_DPI_AWARENESS", "permonitorv2"); + SDL_SetHint("SDL_HINT_WINDOWS_DPI_SCALING", "1"); +#endif + + // initialize SDL + if (SDL_Init(SDL_INIT_VIDEO) != 0) { + printf("SDL_Init failed: %s\n", SDL_GetError()); + return 0; + } + + // initialize SDL_ttf + if (TTF_Init() != 0) { + printf("TTF_Init failed: %s\n", TTF_GetError()); + return 0; + } + + // initialize NFD + if (NFD_Init() != NFD_OKAY) { + printf("NFD_Init failed: %s\n", NFD_GetError()); + return 0; + } + + // create window + SDL_Window* const window = SDL_CreateWindow("Welcome", + SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, + BUTTON_WIDTH, + BUTTON_HEIGHT * NUM_BUTTONS, + SDL_WINDOW_ALLOW_HIGHDPI); + if (!window) { + printf("SDL_CreateWindow failed: %s\n", SDL_GetError()); + return 0; + } + + // create renderer + SDL_Renderer* const renderer = + SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); + if (!renderer) { + printf("SDL_CreateRenderer failed: %s\n", SDL_GetError()); + return 0; + } + + // prepare the buttons and handlers + SDL_Texture* textures_normal[NUM_BUTTONS][NUM_STATES]; + + TTF_Font* const font = TTF_OpenFont(font_file, 20); + if (!font) { + printf("TTF_OpenFont failed: %s\n", TTF_GetError()); + return 0; + } + + const SDL_Color back_color[NUM_STATES] = {{0, 0, 0, SDL_ALPHA_OPAQUE}, + {51, 51, 51, SDL_ALPHA_OPAQUE}, + {102, 102, 102, SDL_ALPHA_OPAQUE}}; + const SDL_Color text_color = {255, 255, 255, SDL_ALPHA_OPAQUE}; + const uint8_t text_alpha[NUM_STATES] = {153, 204, 255}; + + for (size_t i = 0; i != NUM_BUTTONS; ++i) { + SDL_Surface* const text_surface = TTF_RenderUTF8_Blended(font, button_text[i], text_color); + if (!text_surface) { + printf("TTF_RenderUTF8_Blended failed: %s\n", TTF_GetError()); + return 0; + } + + if (SDL_SetSurfaceBlendMode(text_surface, SDL_BLENDMODE_BLEND) != 0) { + printf("SDL_SetSurfaceBlendMode failed: %s\n", SDL_GetError()); + return 0; + } + + for (size_t j = 0; j != NUM_STATES; ++j) { + SDL_Surface* button_surface = + SDL_CreateRGBSurface(0, BUTTON_WIDTH, BUTTON_HEIGHT, 32, 0, 0, 0, 0); + if (!button_surface) { + printf("SDL_CreateRGBSurface failed: %s\n", SDL_GetError()); + return 0; + } + + if (SDL_FillRect(button_surface, + NULL, + SDL_MapRGBA(button_surface->format, + back_color[j].r, + back_color[j].g, + back_color[j].b, + back_color[j].a)) != 0) { + printf("SDL_FillRect failed: %s\n", SDL_GetError()); + return 0; + } + + SDL_SetSurfaceAlphaMod(text_surface, text_alpha[j]); + + SDL_Rect dstrect = {(BUTTON_WIDTH - text_surface->w) / 2, + (BUTTON_HEIGHT - text_surface->h) / 2, + text_surface->w, + text_surface->h}; + if (SDL_BlitSurface(text_surface, NULL, button_surface, &dstrect) != 0) { + printf("SDL_BlitSurface failed: %s\n", SDL_GetError()); + return 0; + } + + SDL_Texture* const texture = SDL_CreateTextureFromSurface(renderer, button_surface); + if (!texture) { + printf("SDL_CreateTextureFromSurface failed: %s\n", SDL_GetError()); + return 0; + } + + SDL_FreeSurface(button_surface); + + textures_normal[i][j] = texture; + } + + SDL_FreeSurface(text_surface); + } + + TTF_CloseFont(font); + + // event loop + bool quit = false; + size_t button_index = (size_t)-1; + bool pressed = false; + do { + // render + for (size_t i = 0; i != NUM_BUTTONS; ++i) { + const SDL_Rect rect = {0, (int)i * BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT}; + SDL_RenderCopy( + renderer, textures_normal[i][button_index == i ? pressed ? 2 : 1 : 0], NULL, &rect); + } + SDL_RenderPresent(renderer); + + // process events + SDL_Event event; + if (SDL_WaitEvent(&event) == 0) { + printf("SDL_WaitEvent failed: %s\n", SDL_GetError()); + return 0; + } + do { + switch (event.type) { + case SDL_QUIT: { + quit = true; + break; + } + case SDL_WINDOWEVENT: { + switch (event.window.event) { + case SDL_WINDOWEVENT_CLOSE: + quit = true; + break; + case SDL_WINDOWEVENT_LEAVE: + button_index = (size_t)-1; + break; + } + break; + } + case SDL_MOUSEMOTION: { + if (event.motion.x < 0 || event.motion.x >= BUTTON_WIDTH || + event.motion.y < 0) { + button_index = (size_t)-1; + break; + } + const int index = event.motion.y / BUTTON_HEIGHT; + if (index < 0 || index >= NUM_BUTTONS) { + button_index = (size_t)-1; + break; + } + button_index = index; + pressed = event.motion.state & SDL_BUTTON(1); + break; + } + case SDL_MOUSEBUTTONDOWN: { + if (event.button.button == 1) { + pressed = true; + } + break; + } + case SDL_MOUSEBUTTONUP: { + if (event.button.button == 1) { + pressed = false; + if (button_index != (size_t)-1) { + (*button_handler[button_index])(window); + } + } + break; + } + } + } while (SDL_PollEvent(&event) != 0); + } while (!quit); + + // destroy textures + for (size_t i = 0; i != NUM_BUTTONS; ++i) { + for (size_t j = 0; j != NUM_STATES; ++j) { + SDL_DestroyTexture(textures_normal[i][j]); + } + } + + // destroy renderer + SDL_DestroyRenderer(renderer); + + // destroy window + SDL_DestroyWindow(window); + + // quit NFD + NFD_Quit(); + + // quit SDL_ttf + TTF_Quit(); + + // quit SDL + SDL_Quit(); + + return 0; +} diff --git a/test/test_sdl.manifest b/test/test_sdl.manifest new file mode 100644 index 00000000..05eb6c23 --- /dev/null +++ b/test/test_sdl.manifest @@ -0,0 +1,29 @@ + + + + + + true + PerMonitorV2 + + + Example application for NFDe. + + + + + + From 5552b8d52135024274133e3a7cc185a6d4a7e07a Mon Sep 17 00:00:00 2001 From: Bernard Teo Date: Sun, 23 Jun 2024 21:21:32 +0800 Subject: [PATCH 04/10] CI: Update to actions/checkout@v4 and actions/upload-artifact@v4 (#139) There are warnings in GitHub Actions due to using actions/checkout@v2 and actions/upload-artifact@v2, which are deprecated. This PR updates them to actions/checkout@v4 and actions/upload-artifact@v4. GitHub Actions has also deprecated their macOS 11 runner, so this PR removes it too. Doing these things removes all the warnings. --- .github/workflows/cmake.yml | 43 +++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index e42daa2a..b1a011a0 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Format code # ClangFormat 14 has a bug, which seems to be fixed in ClangFormat 15. Until GitHub-hosted runners support ClangFormat 15, we will stay at ClangFormat 13. run: find src/ test/ -iname '*.c' -or -iname '*.cpp' -or -iname '*.m' -or -iname '*.mm' -or -iname '*.h' -or -iname '*.hpp' | xargs clang-format-13 -i -style=file @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install Dependencies run: sudo apt-get update && sudo apt-get install dos2unix - name: Convert to Unix line endings @@ -71,7 +71,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install Dependencies run: sudo apt-get update && sudo apt-get install ${{ matrix.portal.dep }} - name: Configure @@ -79,7 +79,7 @@ jobs: - name: Build run: cmake --build build --target install - name: Upload test binaries - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Ubuntu ${{ matrix.os.name }} - ${{ matrix.compiler.name }}, ${{ matrix.portal.name }}, ${{ matrix.autoappend.name }}, ${{ matrix.shared_lib.name }}, C++${{ matrix.cppstd }} path: | @@ -93,21 +93,18 @@ jobs: strategy: matrix: - os: [ {label: macos-latest, name: latest}, {label: macos-11, name: 11} ] - shared_lib: [ {flag: OFF, name: Static} ] - include: - - os: {label: macos-latest, name: latest} - shared_lib: {flag: ON, name: Shared} + os: [ {label: macos-latest, name: latest} ] + shared_lib: [ {flag: OFF, name: Static}, {flag: ON, name: Shared} ] steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Configure run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-Wall -Wextra -Werror -pedantic" -DCMAKE_CXX_FLAGS="-Wall -Wextra -Werror -pedantic" -DBUILD_SHARED_LIBS=${{ matrix.shared_lib.flag }} -DNFD_BUILD_TESTS=ON .. - name: Build run: cmake --build build --target install - name: Upload test binaries - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: MacOS ${{ matrix.os.name }} - Clang, ${{ matrix.shared_lib.name }} path: | @@ -125,13 +122,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Configure run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DBUILD_SHARED_LIBS=${{ matrix.shared_lib.flag }} -DNFD_BUILD_TESTS=ON .. - name: Build run: cmake --build build --target install --config Release - name: Upload test binaries - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Windows latest - MSVC, ${{ matrix.shared_lib.name }} path: | @@ -145,13 +142,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Configure run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -T ClangCL -DNFD_BUILD_TESTS=ON .. - name: Build run: cmake --build build --target install --config Release - name: Upload test binaries - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Windows latest - Clang, Static path: | @@ -169,7 +166,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up MinGW-w64 uses: msys2/setup-msys2@v2 with: @@ -183,7 +180,7 @@ jobs: - name: Build run: cmake --build build --target install - name: Upload test binaries - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Windows latest - MinGW, Static path: | @@ -201,7 +198,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install Dependencies run: sudo apt-get update && sudo apt-get install ${{ matrix.portal.dep }} libsdl2-dev libsdl2-ttf-dev - name: Configure @@ -209,7 +206,7 @@ jobs: - name: Build run: cmake --build build --target install - name: Upload test binaries - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Ubuntu latest - GCC, ${{ matrix.portal.name }}, Static, SDL2 path: | @@ -223,7 +220,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install Dependencies run: brew install sdl2 sdl2_ttf - name: Configure @@ -231,7 +228,7 @@ jobs: - name: Build run: cmake --build build --target install - name: Upload test binaries - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: MacOS latest - Clang, Static, SDL2 path: | @@ -245,7 +242,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install pkgconfiglite run: choco install pkgconfiglite - name: Install Dependencies @@ -255,7 +252,7 @@ jobs: - name: Build run: cmake --build build --target install --config Release - name: Upload test binaries - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Windows latest - MSVC, Static, SDL2 path: | From 85f528defe1f50ad75b6455db4440ed90fcdd3d5 Mon Sep 17 00:00:00 2001 From: Bernard Teo Date: Mon, 24 Jun 2024 00:33:03 +0800 Subject: [PATCH 05/10] README: Add versioned API, PickFolderMultiple, and parent handle (#140) --- README.md | 126 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 106 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index ac87ed5e..8dc9428f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This library is based on Michael Labbe's Native File Dialog ([mlabbe/nativefiled Features: - Lean C API, static library — no C++/ObjC runtime needed -- Supports Windows (MSVC, MinGW, Clang), MacOS (Clang), and Linux (GTK, portal) (GCC, Clang) +- Supports Windows (MSVC, MinGW, Clang), macOS (Clang), and Linux (GTK, portal) (GCC, Clang) - Zlib licensed - Friendly names for filters (e.g. `C/C++ Source files (*.c;*.cpp)` instead of `(*.c;*.cpp)`) on platforms that support it - Automatically append file extension on platforms where users expect it @@ -19,7 +19,7 @@ Features: - Consistent UTF-8 support on all platforms - Native character set (UTF-16 `wchar_t`) support on Windows - Initialization and de-initialization of platform library (e.g. COM (Windows) / GTK (Linux GTK) / D-Bus (Linux portal)) decoupled from dialog functions, so applications can choose when to initialize/de-initialize -- Multiple file selection support (for file open dialog) +- Multiple selection support (for file open and folder select dialogs) - Support for Vista's modern `IFileDialog` on Windows - No third party dependencies - Modern CMake build system @@ -37,6 +37,7 @@ Features added in Native File Dialog Extended: - Support for setting a default file name - Native character set (UTF-16 `wchar_t`) support on Windows - xdg-desktop-portal support on Linux that opens the "native" file chooser (see "Usage" section below) +- Multiple folder selection support - Initialization and de-initialization of platform library decoupled from file dialog functions - Modern CMake build system - Optional C++ wrapper with `unique_ptr` auto-freeing semantics and optional parameters @@ -57,14 +58,17 @@ int main(void) NFD_Init(); - nfdchar_t *outPath; - nfdfilteritem_t filterItem[2] = { { "Source code", "c,cpp,cc" }, { "Headers", "h,hpp" } }; - nfdresult_t result = NFD_OpenDialog(&outPath, filterItem, 2, NULL); + nfdu8char_t *outPath; + nfdu8filteritem_t filters[2] = { { "Source code", "c,cpp,cc" }, { "Headers", "h,hpp" } }; + nfdopendialogu8args_t args = {0}; + args.filterList = filters; + args.filterCount = 2; + nfdresult_t result = NFD_OpenDialogU8_With(&outPath, &args); if (result == NFD_OKAY) { puts("Success!"); puts(outPath); - NFD_FreePath(outPath); + NFD_FreePathU8(outPath); } else if (result == NFD_CANCEL) { @@ -80,16 +84,18 @@ int main(void) } ``` -See [NFD.h](src/include/nfd.h) for more options. +The `U8`/`u8` in NFDe refer to the API for UTF-8 characters (`char`), which most consumers probably want. An `N`/`n` version is also available, which uses the native character type (`wchar_t` on Windows and `char` on other platforms). -If you are using a platform abstraction framework such as SDL or GLFW, also see the "Usage" section below. +For the full list of arguments that you can set on the `args` struct, see the "All Options" section below. + +If you are using a platform abstraction framework such as SDL or GLFW, also see the "Usage with a Platform Abstraction Framework" section below. # Screenshots # ![Windows 10](screens/open_win10.png?raw=true#gh-light-mode-only) ![Windows 10](screens/open_win10_dark.png?raw=true#gh-dark-mode-only) -![MacOS 10.13](screens/open_macos_11.0.png?raw=true#gh-light-mode-only) -![MacOS 10.13](screens/open_macos_11.0_dark.png?raw=true#gh-dark-mode-only) +![macOS 10.13](screens/open_macos_11.0.png?raw=true#gh-light-mode-only) +![macOS 10.13](screens/open_macos_11.0_dark.png?raw=true#gh-dark-mode-only) ![GTK3 on Ubuntu 20.04](screens/open_gtk3.png?raw=true#gh-light-mode-only) ![GTK3 on Ubuntu 20.04](screens/open_gtk3_dark.png?raw=true#gh-dark-mode-only) @@ -165,24 +171,78 @@ Make sure `libgtk-3-dev` is installed on your system. #### Portal Make sure `libdbus-1-dev` is installed on your system. -### MacOS -On MacOS, add `AppKit` and `UniformTypeIdentifiers` to the list of frameworks. +### macOS +On macOS, add `AppKit` and `UniformTypeIdentifiers` to the list of frameworks. ### Windows On Windows (both MSVC and MinGW), ensure you are building against `ole32.lib`, `uuid.lib`, and `shell32.lib`. # Usage -See `NFD.h` for API calls. See the `test` directory for example code (both C and C++). +## All Options + +To open a dialog, you set options on a struct and then pass that struct to an NFDe function, e.g.: +```C +nfdopendialogu8args_t args = {0}; +args.filterList = filters; +args.filterCount = 2; +nfdresult_t result = NFD_OpenDialogU8_With(&outPath, &args); +``` + +All options are optional and may be set individually (zero initialization sets all options to reasonable defaults), except for `filterList` and `filterCount` which must be either both set or both left unset. + +**Future versions of NFDe may add additional options to the end of the arguments struct without bumping the major version number, so to ensure backward API compatibility, you should not assume that the struct has a specific length or number of fields.** You may assume that zero-initialization of the struct will continue to set all options to reasonable defaults, so assigning `{0}` to the struct is acceptable. For those building shared libraries of NFDe, backward ABI compatibility is ensured by an internal version index (`NFD_INTERFACE_VERSION`), which is expected to be transparent to consumers. + +**OpenDialog**/**OpenDialogMultiple**: +```C +typedef struct { + const nfdu8filteritem_t* filterList; + nfdfiltersize_t filterCount; + const nfdu8char_t* defaultPath; + nfdwindowhandle_t parentWindow; +} nfdopendialogu8args_t; +``` + +**SaveDialog**: +```C +typedef struct { + const nfdu8filteritem_t* filterList; + nfdfiltersize_t filterCount; + const nfdu8char_t* defaultPath; + const nfdu8char_t* defaultName; + nfdwindowhandle_t parentWindow; +} nfdsavedialogu8args_t; +``` + +**PickFolder**/**PickFolderMultiple**: +```C +typedef struct { + const nfdu8char_t* defaultPath; + nfdwindowhandle_t parentWindow; +} nfdpickfolderu8args_t; +``` + +- `filterList` and `filterCount`: Set these to customize the file filter (it appears as a dropdown menu on Windows and Linux, but simply hides files on macOS). Set `filterList` to a pointer to the start of the array of filter items and `filterCount` to the number of filter items in that array. See the "File Filter Syntax" section below for details. +- `defaultPath`: Set this to the default folder that the dialog should open to (on Windows, if there is a recently used folder, it opens to that folder instead of the folder you pass). +- `defaultName`: (For SaveDialog only) Set this to the file name that should be pre-filled on the dialog. +- `parentWindow`: Set this to the native window handle of the parent of this dialog. See the "Usage with a Platform Abstraction Framework" section for details. It is also possible to pass a handle even if you do not use a platform abstraction framework. + +## Examples + +See the `test` directory for example code (both C and C++). If you turned on the option to build the `test` directory (`-DNFD_BUILD_TESTS=ON`), then `build/bin` will contain the compiled test programs. +There is also an SDL2 example, which needs to be enabled separately with `-DNFD_BUILD_SDL2_TESTS=ON`. It requires SDL2 to be installed on your machine. + +Compiled examples (including the SDL2 example) are also uploaded as artefacts to GitHub Actions, and may be downloaded from there. + ## File Filter Syntax Files can be filtered by file extension groups: ```C -nfdfilteritem_t filterItem[2] = { { "Source code", "c,cpp,cc" }, { "Headers", "h,hpp" } }; +nfdu8filteritem_t filters[2] = { { "Source code", "c,cpp,cc" }, { "Headers", "h,hpp" } }; ``` A file filter is a pair of strings comprising the friendly name and the specification (multiple file extensions are comma-separated). @@ -191,7 +251,7 @@ A list of file filters can be passed as an argument when invoking the library. A wildcard filter is always added to every dialog. -*Note: On MacOS, the file dialogs do not have friendly names and there is no way to switch between filters, so the filter specifications are combined (e.g. "c,cpp,cc,h,hpp"). The filter specification is also never explicitly shown to the user. This is usual MacOS behaviour and users expect it.* +*Note: On macOS, the file dialogs do not have friendly names and there is no way to switch between filters, so the filter specifications are combined (e.g. "c,cpp,cc,h,hpp"). The filter specification is also never explicitly shown to the user. This is usual macOS behaviour and users expect it.* *Note 2: You must ensure that the specification string is non-empty and that every file extension has at least one character. Otherwise, bad things might ensue (i.e. undefined behaviour).* @@ -234,7 +294,33 @@ Macros that might be defined by `nfd.h`: ## Usage with a Platform Abstraction Framework -NFDe is known to work with SDL2 and GLFW, and should also work with other platform abstraction framworks. However, you should initialize NFDe _after_ initializing the framework, and probably should deinitialize NFDe _before_ deinitializing the framework. This is because some frameworks expect to be initialized on a "clean slate", and they may configure the system in a different way from NFDe. `NFD_Init` is generally very careful not to disrupt the existing configuration unless necessary, and `NFD_Quit` restores the configuration back exactly to what it was before initialization. +NFDe is known to work with SDL2 and GLFW, and should also work with other platform abstraction framworks. This section explains how to use NFDe properly with such frameworks. + +### Parent window handle + +The `parentWindow` argument allows the user to give the dialog a parent. + +If using SDL2, include `` and call the following function to set the parent window handle: +```C +NFD_GetNativeWindowFromSDLWindow(sdlWindow /* SDL_Window* */, &args.parentWindow); +``` + +If using GLFW3, define the appropriate `GLFW_EXPOSE_NATIVE_*` macros described on the [GLFW native access page](https://www.glfw.org/docs/latest/group__native.html), and then include `` and call the following function to set the parent window handle: +```C +NFD_GetNativeWindowFromGLFWWindow(glfwWindow /* GLFWwindow* */, &args.parentWindow); +``` + +If you are using another platform abstraction framework, or not using any such framework, you can set `args.parentWindow` manually. + +Win32 (Windows), Cocoa (macOS), and X11 (Linux) windows are supported. Passing a Wayland (Linux) window currently does nothing (i.e. the dialog acts as if it has no parent), but support is likely to be added in the future. + +#### Why pass a parent window handle? + +To make a window (in this case the file dialog) stay above another window, we need to declare the bottom window as the parent of the top window. This keeps the dialog window from disappearing behind the parent window if the user clicks on the parent window while the dialog is open. Keeping the dialog above the window that invoked it is the expected behaviour on all supported operating systems, and so passing the parent window handle is recommended if possible. + +### Initialization order + +You should initialize NFDe _after_ initializing the framework, and probably should deinitialize NFDe _before_ deinitializing the framework. This is because some frameworks expect to be initialized on a "clean slate", and they may configure the system in a different way from NFDe. `NFD_Init` is generally very careful not to disrupt the existing configuration unless necessary, and `NFD_Quit` restores the configuration back exactly to what it was before initialization. An example with SDL2: @@ -270,15 +356,15 @@ To use the portal implementation, add `-DNFD_PORTAL=ON` to the build command. ### What is a portal? -Unlike Windows and MacOS, Linux does not have a file chooser baked into the operating system. Linux applications that want a file chooser usually link with a library that provides one (such as GTK, as in the Linux screenshot above). This is a mostly acceptable solution that many applications use, but may make the file chooser look foreign on non-GTK distros. +Unlike Windows and macOS, Linux does not have a file chooser baked into the operating system. Linux applications that want a file chooser usually link with a library that provides one (such as GTK, as in the Linux screenshot above). This is a mostly acceptable solution that many applications use, but may make the file chooser look foreign on non-GTK distros. Flatpak was introduced in 2015, and with it came a standardized interface to open a file chooser. Applications using this interface did not need to come with a file chooser, and could use the one provided by Flatpak. This interface became known as the desktop portal, and its use expanded to non-Flatpak applications. Now, most major desktop Linux distros come with the desktop portal installed, with file choosers that fit the theme of the distro. Users can also install a different portal backend if desired. There are currently two known backends: GTK and KDE. (XFCE does not currently seem to have a portal backend.) ## Platform-specific Quirks -### MacOS +### macOS -- If the MacOS deployment target is ≥ 11.0, the [allowedContentTypes](https://developer.apple.com/documentation/appkit/nssavepanel/3566857-allowedcontenttypes?language=objc) property of NSSavePanel is used instead of the deprecated [allowedFileTypes](https://developer.apple.com/documentation/appkit/nssavepanel/1534419-allowedfiletypes?language=objc) property for file filters. Thus, if you are filtering by a custom file extension specific to your application, you will need to define the data type in your `Info.plist` file as per the [Apple documentation](https://developer.apple.com/documentation/uniformtypeidentifiers/defining_file_and_data_types_for_your_app). (It is possible to force NFDe to use allowedFileTypes by adding `-DNFD_USE_ALLOWEDCONTENTTYPES_IF_AVAILABLE=OFF` to your CMake build command, but this is not recommended. If you need to support older MacOS versions, you should be setting the correct deployment target instead.) +- If the macOS deployment target is ≥ 11.0, the [allowedContentTypes](https://developer.apple.com/documentation/appkit/nssavepanel/3566857-allowedcontenttypes?language=objc) property of NSSavePanel is used instead of the deprecated [allowedFileTypes](https://developer.apple.com/documentation/appkit/nssavepanel/1534419-allowedfiletypes?language=objc) property for file filters. Thus, if you are filtering by a custom file extension specific to your application, you will need to define the data type in your `Info.plist` file as per the [Apple documentation](https://developer.apple.com/documentation/uniformtypeidentifiers/defining_file_and_data_types_for_your_app). (It is possible to force NFDe to use allowedFileTypes by adding `-DNFD_USE_ALLOWEDCONTENTTYPES_IF_AVAILABLE=OFF` to your CMake build command, but this is not recommended. If you need to support older macOS versions, you should be setting the correct deployment target instead.) # Known Limitations # @@ -287,7 +373,7 @@ Flatpak was introduced in 2015, and with it came a standardized interface to ope - GTK dialogs don't set the existing window as parent, so if users click the existing window while the dialog is open then the dialog will go behind it. GTK writes a warning to stdout or stderr about this. - Portal dialogs (the alternative to GTK on Linux) don't support a default path. Any default path you supply will be ignored. - This library is not compatible with the original Native File Dialog library. Things might break if you use both in the same project. (There are no plans to support this; you have to use one or the other.) - - This library does not explicitly dispatch calls to the UI thread. This may lead to crashes if you call functions from other threads when the platform does not support it (e.g. MacOS). Users are generally expected to call NFDe from an appropriate UI thread (i.e. the thread performing the UI event loop). + - This library does not explicitly dispatch calls to the UI thread. This may lead to crashes if you call functions from other threads when the platform does not support it (e.g. macOS). Users are generally expected to call NFDe from an appropriate UI thread (i.e. the thread performing the UI event loop). # Reporting Bugs # From c099aaee9a24a35ad93e06513b41aeb503d848d0 Mon Sep 17 00:00:00 2001 From: Bernard Teo Date: Mon, 24 Jun 2024 23:30:18 +0800 Subject: [PATCH 06/10] Release: v1.2.0 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fedef3aa..6d3fccf6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.5) -project(nativefiledialog-extended VERSION 1.1.1) +project(nativefiledialog-extended VERSION 1.2.0) set(nfd_ROOT_PROJECT OFF) if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) From 70209357b17db79318348a133aa2f4c68ac31f28 Mon Sep 17 00:00:00 2001 From: Kyle Coffey Date: Fri, 5 Jul 2024 12:54:44 -0500 Subject: [PATCH 07/10] Build: Add alias target for dependent project CMake consumption (#143) This naming convention is common in other CMake projects. The presence of `::` in the identifier results in fail-fast behavior; when a user calls `target_link_libraries` or similar, `nfd::nfd` MUST be an existing target at configure time, but `nfd` might have been a system library. As a result, the latter would appear to work until the linker fails to find a library by that name. This is not a breaking change - plain `nfd` is still accessible. --- src/CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 83307eaf..740b4848 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -61,6 +61,9 @@ endif() # Define the library add_library(${TARGET_NAME} ${SOURCE_FILES}) +# Define alias library to fail early in dependent projects +add_library(${TARGET_NAME}::${TARGET_NAME} ALIAS ${TARGET_NAME}) + if (BUILD_SHARED_LIBS) target_compile_definitions(${TARGET_NAME} PRIVATE NFD_EXPORT INTERFACE NFD_SHARED) endif () From 2d21ecbfaec051c4b32e586c664b3c5743c3097d Mon Sep 17 00:00:00 2001 From: Bernard Teo Date: Sat, 3 Aug 2024 23:09:42 +0800 Subject: [PATCH 08/10] GTK: Open the dialog window on the same GdkScreen as the parent (#145) Setting the default GdkDisplay doesn't actually cause a newly opened dialog window to use it. The more correct way seems to be to make the dialog window use the same GdkScreen as the parent. Each GdkScreen maps to one GdkDisplay, so this guarantees that the dialog window uses the same GdkDisplay as the parent too. --- src/nfd_gtk.cpp | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/src/nfd_gtk.cpp b/src/nfd_gtk.cpp index c70d7dd4..86aa797c 100644 --- a/src/nfd_gtk.cpp +++ b/src/nfd_gtk.cpp @@ -384,9 +384,7 @@ gint RunDialogWithFocus(GtkDialog* dialog) { // Gets the GdkWindow from the given window handle. This function might fail even if parentWindow // is set correctly, since it calls some failable GDK functions. If it fails, it will return // nullptr. The caller is responsible for freeing ths returned GdkWindow, if not nullptr. -GdkWindow* GetAllocNativeWindowHandle(const nfdwindowhandle_t& parentWindow, - GdkDisplayManager*& outDisplayManager, - GdkDisplay*& outDisplay) { +GdkWindow* GetAllocNativeWindowHandle(const nfdwindowhandle_t& parentWindow) { switch (parentWindow.type) { #if defined(GDK_WINDOWING_X11) case NFD_WINDOW_HANDLE_TYPE_X11: { @@ -434,8 +432,6 @@ GdkWindow* GetAllocNativeWindowHandle(const nfdwindowhandle_t& parentWindow, gdk_set_allowed_backends(NULL); } if (!x11_display) return nullptr; - outDisplayManager = display_manager; - outDisplay = x11_display; GdkWindow* gdk_window = gdk_x11_window_foreign_new_for_display(x11_display, x11_handle); return gdk_window; } @@ -452,39 +448,29 @@ void RealizedSignalHandler(GtkWidget* window, void* userdata) { struct NativeWindowParenter { NativeWindowParenter(GtkWidget* widget, const nfdwindowhandle_t& parentWindow) noexcept - : widget(widget), displayManager(nullptr) { - GdkDisplay* new_display = nullptr; - parent = GetAllocNativeWindowHandle(parentWindow, displayManager, new_display); + : widget(widget) { + parent = GetAllocNativeWindowHandle(parentWindow); if (parent) { - /* set the handler to the realize signal to set the transient GDK parent */ + // set the handler to the realize signal to set the transient GDK parent handlerID = g_signal_connect(G_OBJECT(widget), "realize", G_CALLBACK(RealizedSignalHandler), static_cast(parent)); - /* Set the default display to a display that we know is X11 (so that realizing the file - * dialog will use it) */ - /* Note: displayManager here must be non-null since parent is non-null */ - originalDisplay = gdk_display_manager_get_default_display(displayManager); - gdk_display_manager_set_default_display(displayManager, new_display); + // make the dialog window use the same GtkScreen as the parent (so that parenting works) + gtk_window_set_screen(GTK_WINDOW(widget), gdk_window_get_screen(parent)); } } ~NativeWindowParenter() { if (parent) { - /* Set the default display back to whatever it was, to be nice */ - /* Note: displayManager here must be non-null since parent is non-null */ - gdk_display_manager_set_default_display(displayManager, originalDisplay); - - /* unset the handler and delete the parent GdkWindow */ + // unset the handler and delete the parent GdkWindow g_signal_handler_disconnect(G_OBJECT(widget), handlerID); g_object_unref(parent); } } GtkWidget* const widget; GdkWindow* parent; - GdkDisplayManager* displayManager; - GdkDisplay* originalDisplay; gulong handlerID; }; From 86d5f2005fe1c00747348a12070fec493ea2407e Mon Sep 17 00:00:00 2001 From: Bernard Teo Date: Sat, 3 Aug 2024 23:57:13 +0800 Subject: [PATCH 09/10] Release: v1.2.1 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d3fccf6..4a6decea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.5) -project(nativefiledialog-extended VERSION 1.2.0) +project(nativefiledialog-extended VERSION 1.2.1) set(nfd_ROOT_PROJECT OFF) if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) From 2a7440a3a52feba815effbfcadc280de63eb2acb Mon Sep 17 00:00:00 2001 From: rherilier <73692472+rherilier@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:33:31 +0200 Subject: [PATCH 10/10] README: Update information about portal (#147) Remove portal note about the unsupported default path setting and update text about portal backends --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8dc9428f..218eacd9 100644 --- a/README.md +++ b/README.md @@ -350,15 +350,13 @@ On Linux, you can use the portal implementation instead of GTK, which will open To use the portal implementation, add `-DNFD_PORTAL=ON` to the build command. -*Note: Setting a default path is not supported by the portal implementation, and any default path passed to NFDe will be ignored. This is a limitation of the portal API, so there is no way NFDe can work around it. If this feature is something you desire, please show your interest on https://github.com/flatpak/xdg-desktop-portal/pull/874.* - -*Note 2: The folder picker is only supported on org.freedesktop.portal.FileChooser interface version >= 3, which corresponds to xdg-desktop-portal version >= 1.7.1. `NFD_PickFolder()` will query the interface version at runtime, and return `NFD_ERROR` if the version is too low. +*Note: The folder picker is only supported on org.freedesktop.portal.FileChooser interface version >= 3, which corresponds to xdg-desktop-portal version >= 1.7.1. `NFD_PickFolder()` will query the interface version at runtime, and return `NFD_ERROR` if the version is too low. ### What is a portal? Unlike Windows and macOS, Linux does not have a file chooser baked into the operating system. Linux applications that want a file chooser usually link with a library that provides one (such as GTK, as in the Linux screenshot above). This is a mostly acceptable solution that many applications use, but may make the file chooser look foreign on non-GTK distros. -Flatpak was introduced in 2015, and with it came a standardized interface to open a file chooser. Applications using this interface did not need to come with a file chooser, and could use the one provided by Flatpak. This interface became known as the desktop portal, and its use expanded to non-Flatpak applications. Now, most major desktop Linux distros come with the desktop portal installed, with file choosers that fit the theme of the distro. Users can also install a different portal backend if desired. There are currently two known backends: GTK and KDE. (XFCE does not currently seem to have a portal backend.) +Flatpak was introduced in 2015, and with it came a standardized interface to open a file chooser. Applications using this interface did not need to come with a file chooser, and could use the one provided by Flatpak. This interface became known as the desktop portal, and its use expanded to non-Flatpak applications. Now, most major desktop Linux distros come with the desktop portal installed, with file choosers that fit the theme of the distro. Users can also install a different portal backend if desired. There are currently three known backends with file chooser support: GTK, KDE, and LXQt; Gnome and Xapp backends depend on the GTK one for this functionality. The Xapp backend has been designed for Cinnamon, MATE, and XFCE. Other desktop environments do not seem to currently have a portal backend. ## Platform-specific Quirks @@ -371,7 +369,6 @@ Flatpak was introduced in 2015, and with it came a standardized interface to ope - No support for Windows XP's legacy dialogs such as `GetOpenFileName`. (There are no plans to support this; you shouldn't be still using Windows XP anyway.) - No Emscripten (WebAssembly) bindings. (This might get implemented if I decide to port Circuit Sandbox for the web, but I don't think there is any way to implement a web-based folder picker.) - GTK dialogs don't set the existing window as parent, so if users click the existing window while the dialog is open then the dialog will go behind it. GTK writes a warning to stdout or stderr about this. - - Portal dialogs (the alternative to GTK on Linux) don't support a default path. Any default path you supply will be ignored. - This library is not compatible with the original Native File Dialog library. Things might break if you use both in the same project. (There are no plans to support this; you have to use one or the other.) - This library does not explicitly dispatch calls to the UI thread. This may lead to crashes if you call functions from other threads when the platform does not support it (e.g. macOS). Users are generally expected to call NFDe from an appropriate UI thread (i.e. the thread performing the UI event loop).