From 982a2262a87a7e953309b62da05cef89a5077518 Mon Sep 17 00:00:00 2001 From: Bernard Teo Date: Tue, 23 Apr 2024 15:28:29 +0200 Subject: [PATCH] New feature: Implement PickFolderMultiple --- 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; +}