diff --git a/nfd/nfd_cocoa.m b/nfd/nfd_cocoa.m index f7024356..aeffb1c4 100644 --- a/nfd/nfd_cocoa.m +++ b/nfd/nfd_cocoa.m @@ -180,6 +180,10 @@ const char* NFD_GetError(void) { return g_errorstr; } +void NFD_ClearError(void) { + NFDi_SetError(NULL); +} + void NFD_FreePathN(nfdnchar_t* filePath) { NFDi_Free((void*)filePath); } diff --git a/nfd/nfd_portal.cpp b/nfd/nfd_portal.cpp index 8ef9158f..f37e0916 100644 --- a/nfd/nfd_portal.cpp +++ b/nfd/nfd_portal.cpp @@ -14,8 +14,16 @@ #include #include #include -#include // for the random token string -#include // for access() +#include // for access() + +#if !defined(__has_include) || !defined(__linux__) +#include // for getrandom() - the random token string +#elif __has_include() +#include +#else // for GLIBC < 2.25 +#include +#define getrandom(buf, sz, flags) syscall(SYS_getrandom, buf, sz, flags) +#endif #include "nfd.h" diff --git a/nfd/nfd_win.cpp b/nfd/nfd_win.cpp index 991e5071..772dfb27 100644 --- a/nfd/nfd_win.cpp +++ b/nfd/nfd_win.cpp @@ -1,969 +1,969 @@ -/* - Native File Dialog Extended - Repository: https://github.com/btzy/nativefiledialog-extended - License: Zlib - Author: Bernard Teo - */ - -/* only locally define UNICODE in this compilation unit */ -#ifndef UNICODE -#define UNICODE -#endif - -#ifdef __MINGW32__ -// Explicitly setting NTDDI version, this is necessary for the MinGW compiler -#define NTDDI_VERSION NTDDI_VISTA -#define _WIN32_WINNT _WIN32_WINNT_VISTA -#endif - -#if _MSC_VER -// see -// https://developercommunity.visualstudio.com/content/problem/185399/error-c2760-in-combaseapih-with-windows-sdk-81-and.html -struct IUnknown; // Workaround for "combaseapi.h(229): error C2187: syntax error: 'identifier' was - // unexpected here" when using /permissive- -#endif - -#include -#include -#include -#include -#include -#include "nfd.h" - -namespace { - -/* current error */ -const char* g_errorstr = nullptr; - -void NFDi_SetError(const char* msg) { - g_errorstr = msg; -} - -template -T* NFDi_Malloc(size_t bytes) { - void* ptr = malloc(bytes); - if (!ptr) NFDi_SetError("NFDi_Malloc failed."); - - return static_cast(ptr); -} - -template -void NFDi_Free(T* ptr) { - assert(ptr); - free(static_cast(ptr)); -} - -/* guard objects */ -template -struct Release_Guard { - T* data; - Release_Guard(T* releasable) noexcept : data(releasable) {} - ~Release_Guard() { data->Release(); } -}; - -template -struct Free_Guard { - T* data; - Free_Guard(T* freeable) noexcept : data(freeable) {} - ~Free_Guard() { NFDi_Free(data); } -}; - -template -struct FreeCheck_Guard { - T* data; - FreeCheck_Guard(T* freeable = nullptr) noexcept : data(freeable) {} - ~FreeCheck_Guard() { - if (data) NFDi_Free(data); - } -}; - -/* helper functions */ -nfdresult_t AddFiltersToDialog(::IFileDialog* fileOpenDialog, - const nfdnfilteritem_t* filterList, - nfdfiltersize_t filterCount) { - /* filterCount plus 1 because we hardcode the *.* wildcard after the while loop */ - COMDLG_FILTERSPEC* specList = - NFDi_Malloc(sizeof(COMDLG_FILTERSPEC) * (filterCount + 1)); - if (!specList) { - return NFD_ERROR; - } - - /* ad-hoc RAII object to free memory when destructing */ - struct COMDLG_FILTERSPEC_Guard { - COMDLG_FILTERSPEC* _specList; - nfdfiltersize_t index; - COMDLG_FILTERSPEC_Guard(COMDLG_FILTERSPEC* specList) noexcept - : _specList(specList), index(0) {} - ~COMDLG_FILTERSPEC_Guard() { - for (--index; index != static_cast(-1); --index) { - NFDi_Free(const_cast(_specList[index].pszSpec)); - } - NFDi_Free(_specList); - } - }; - - COMDLG_FILTERSPEC_Guard specListGuard(specList); - - if (filterCount) { - assert(filterList); - - // we have filters to add ... format and add them - - // use the index that comes from the RAII object (instead of making a copy), so the RAII - // object will know which memory to free - nfdfiltersize_t& index = specListGuard.index; - - for (; index != filterCount; ++index) { - // set the friendly name of this filter - specList[index].pszName = filterList[index].name; - - // set the specification of this filter... - - // count number of file extensions - size_t sep = 1; - for (const nfdnchar_t* p_spec = filterList[index].spec; *p_spec; ++p_spec) { - if (*p_spec == L',') { - ++sep; - } - } - - // calculate space needed (including the trailing '\0') - size_t specSize = sep * 2 + wcslen(filterList[index].spec) + 1; - - // malloc the required memory and populate it - nfdnchar_t* specBuf = NFDi_Malloc(sizeof(nfdnchar_t) * specSize); - - if (!specBuf) { - // automatic freeing of memory via COMDLG_FILTERSPEC_Guard - return NFD_ERROR; - } - - // convert "png,jpg" to "*.png;*.jpg" as required by Windows ... - nfdnchar_t* p_specBuf = specBuf; - *p_specBuf++ = L'*'; - *p_specBuf++ = L'.'; - for (const nfdnchar_t* p_spec = filterList[index].spec; *p_spec; ++p_spec) { - if (*p_spec == L',') { - *p_specBuf++ = L';'; - *p_specBuf++ = L'*'; - *p_specBuf++ = L'.'; - } else { - *p_specBuf++ = *p_spec; - } - } - *p_specBuf++ = L'\0'; - - // assert that we had allocated exactly the correct amount of memory that we used - assert(static_cast(p_specBuf - specBuf) == specSize); - - // save the buffer to the guard object - specList[index].pszSpec = specBuf; - } - } - - /* Add wildcard */ - specList[filterCount].pszName = L"All files"; - specList[filterCount].pszSpec = L"*.*"; - - // add the filter to the dialog - if (!SUCCEEDED(fileOpenDialog->SetFileTypes(filterCount + 1, specList))) { - NFDi_SetError("Failed to set the allowable file types for the drop-down menu."); - return NFD_ERROR; - } - - // automatic freeing of memory via COMDLG_FILTERSPEC_Guard - return NFD_OKAY; -} - -/* call after AddFiltersToDialog */ -nfdresult_t SetDefaultExtension(::IFileDialog* fileOpenDialog, - const nfdnfilteritem_t* filterList, - nfdfiltersize_t filterCount) { - // if there are no filters, then don't set default extensions - if (!filterCount) { - return NFD_OKAY; - } - - assert(filterList); - - // set the first item as the default index, and set the default extension - if (!SUCCEEDED(fileOpenDialog->SetFileTypeIndex(1))) { - NFDi_SetError("Failed to set the selected file type index."); - return NFD_ERROR; - } - - // set the first item as the default file extension - const nfdnchar_t* p_spec = filterList[0].spec; - for (; *p_spec; ++p_spec) { - if (*p_spec == ',') { - break; - } - } - if (*p_spec) { - // multiple file extensions for this type (need to allocate memory) - size_t numChars = p_spec - filterList[0].spec; - // allocate one more char space for the '\0' - nfdnchar_t* extnBuf = NFDi_Malloc(sizeof(nfdnchar_t) * (numChars + 1)); - if (!extnBuf) { - return NFD_ERROR; - } - Free_Guard extnBufGuard(extnBuf); - - // copy the extension - for (size_t i = 0; i != numChars; ++i) { - extnBuf[i] = filterList[0].spec[i]; - } - // pad with trailing '\0' - extnBuf[numChars] = L'\0'; - - if (!SUCCEEDED(fileOpenDialog->SetDefaultExtension(extnBuf))) { - NFDi_SetError("Failed to set default extension."); - return NFD_ERROR; - } - } else { - // single file extension for this type (no need to allocate memory) - if (!SUCCEEDED(fileOpenDialog->SetDefaultExtension(filterList[0].spec))) { - NFDi_SetError("Failed to set default extension."); - return NFD_ERROR; - } - } - - return NFD_OKAY; -} - -nfdresult_t SetDefaultPath(IFileDialog* dialog, const nfdnchar_t* defaultPath) { - if (!defaultPath || !*defaultPath) return NFD_OKAY; - - IShellItem* folder; - HRESULT result = SHCreateItemFromParsingName(defaultPath, nullptr, IID_PPV_ARGS(&folder)); - - // Valid non results. - if (result == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || - result == HRESULT_FROM_WIN32(ERROR_INVALID_DRIVE)) { - return NFD_OKAY; - } - - if (!SUCCEEDED(result)) { - NFDi_SetError("Failed to create ShellItem for setting the default path."); - return NFD_ERROR; - } - - Release_Guard folderGuard(folder); - - // SetDefaultFolder() might use another recently used folder if available, so the user doesn't - // need to keep navigating back to the default folder (recommended by Windows). change to - // SetFolder() if you always want to use the default folder - if (!SUCCEEDED(dialog->SetDefaultFolder(folder))) { - NFDi_SetError("Failed to set default path."); - return NFD_ERROR; - } - - return NFD_OKAY; -} - -nfdresult_t SetDefaultName(IFileDialog* dialog, const nfdnchar_t* defaultName) { - if (!defaultName || !*defaultName) return NFD_OKAY; - - if (!SUCCEEDED(dialog->SetFileName(defaultName))) { - NFDi_SetError("Failed to set default file name."); - return NFD_ERROR; - } - - return NFD_OKAY; -} - -nfdresult_t AddOptions(IFileDialog* dialog, FILEOPENDIALOGOPTIONS options) { - FILEOPENDIALOGOPTIONS existingOptions; - if (!SUCCEEDED(dialog->GetOptions(&existingOptions))) { - NFDi_SetError("Failed to get options."); - return NFD_ERROR; - } - if (!SUCCEEDED(dialog->SetOptions(existingOptions | options))) { - NFDi_SetError("Failed to set options."); - return NFD_ERROR; - } - return NFD_OKAY; -} -} // namespace - -const char* NFD_GetError(void) { - return g_errorstr; -} - -void NFD_ClearError(void) { - NFDi_SetError(nullptr); -} - -/* public */ - -namespace { -// The user might have initialized with COINIT_MULTITHREADED before, -// in which case we will fail to do CoInitializeEx(), but file dialogs will still work. -// See https://github.com/mlabbe/nativefiledialog/issues/72 for more information. -bool needs_uninitialize; -} // namespace - -nfdresult_t NFD_Init(void) { - // Init COM library. - HRESULT result = - ::CoInitializeEx(nullptr, ::COINIT_APARTMENTTHREADED | ::COINIT_DISABLE_OLE1DDE); - - if (SUCCEEDED(result)) { - needs_uninitialize = true; - return NFD_OKAY; - } else if (result == RPC_E_CHANGED_MODE) { - // If this happens, the user already initialized COM using COINIT_MULTITHREADED, - // so COM will still work, but we shouldn't uninitialize it later. - needs_uninitialize = false; - return NFD_OKAY; - } else { - NFDi_SetError("Failed to initialize COM."); - return NFD_ERROR; - } -} -void NFD_Quit(void) { - if (needs_uninitialize) ::CoUninitialize(); -} - -void NFD_FreePathN(nfdnchar_t* filePath) { - assert(filePath); - ::CoTaskMemFree(filePath); -} - -nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath, - const nfdnfilteritem_t* filterList, - nfdfiltersize_t filterCount, - const nfdnchar_t* defaultPath) { - ::IFileOpenDialog* fileOpenDialog; - - // Create dialog - HRESULT result = ::CoCreateInstance(::CLSID_FileOpenDialog, - nullptr, - CLSCTX_ALL, - ::IID_IFileOpenDialog, - reinterpret_cast(&fileOpenDialog)); - - if (!SUCCEEDED(result)) { - NFDi_SetError("Could not create dialog."); - return NFD_ERROR; - } - - // make sure we remember to free the dialog - Release_Guard<::IFileOpenDialog> fileOpenDialogGuard(fileOpenDialog); - - // Build the filter list - if (!AddFiltersToDialog(fileOpenDialog, filterList, filterCount)) { - return NFD_ERROR; - } - - // Set auto-completed default extension - if (!SetDefaultExtension(fileOpenDialog, filterList, filterCount)) { - return NFD_ERROR; - } - - // Set the default path - if (!SetDefaultPath(fileOpenDialog, defaultPath)) { - return NFD_ERROR; - } - - // Only show file system items - if (!AddOptions(fileOpenDialog, ::FOS_FORCEFILESYSTEM)) { - return NFD_ERROR; - } - - // Show the dialog. - result = fileOpenDialog->Show(nullptr); - if (SUCCEEDED(result)) { - // Get the file name - ::IShellItem* psiResult; - result = fileOpenDialog->GetResult(&psiResult); - if (!SUCCEEDED(result)) { - NFDi_SetError("Could not get shell item from dialog."); - return NFD_ERROR; - } - Release_Guard<::IShellItem> psiResultGuard(psiResult); - - nfdnchar_t* filePath; - result = psiResult->GetDisplayName(::SIGDN_FILESYSPATH, &filePath); - if (!SUCCEEDED(result)) { - NFDi_SetError("Could not get file path from shell item returned by dialog."); - return NFD_ERROR; - } - - *outPath = filePath; - - return NFD_OKAY; - } else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED)) { - return NFD_CANCEL; - } else { - NFDi_SetError("File dialog box show failed."); - return NFD_ERROR; - } -} - -nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, - const nfdnfilteritem_t* filterList, - nfdfiltersize_t filterCount, - const nfdnchar_t* defaultPath) { - ::IFileOpenDialog* fileOpenDialog(nullptr); - - // Create dialog - HRESULT result = ::CoCreateInstance(::CLSID_FileOpenDialog, - nullptr, - CLSCTX_ALL, - ::IID_IFileOpenDialog, - reinterpret_cast(&fileOpenDialog)); - - if (!SUCCEEDED(result)) { - NFDi_SetError("Could not create dialog."); - return NFD_ERROR; - } - - // make sure we remember to free the dialog - Release_Guard<::IFileOpenDialog> fileOpenDialogGuard(fileOpenDialog); - - // Build the filter list - if (!AddFiltersToDialog(fileOpenDialog, filterList, filterCount)) { - return NFD_ERROR; - } - - // Set auto-completed default extension - if (!SetDefaultExtension(fileOpenDialog, filterList, filterCount)) { - return NFD_ERROR; - } - - // Set the default path - if (!SetDefaultPath(fileOpenDialog, defaultPath)) { - return NFD_ERROR; - } - - // Set a flag for multiple options and file system items only - if (!AddOptions(fileOpenDialog, ::FOS_FORCEFILESYSTEM | ::FOS_ALLOWMULTISELECT)) { - return NFD_ERROR; - } - - // Show the dialog. - result = fileOpenDialog->Show(nullptr); - if (SUCCEEDED(result)) { - ::IShellItemArray* shellItems; - result = fileOpenDialog->GetResults(&shellItems); - if (!SUCCEEDED(result)) { - NFDi_SetError("Could not get shell items."); - 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_SaveDialogN(nfdnchar_t** outPath, - const nfdnfilteritem_t* filterList, - nfdfiltersize_t filterCount, - const nfdnchar_t* defaultPath, - const nfdnchar_t* defaultName) { - ::IFileSaveDialog* fileSaveDialog; - - // Create dialog - HRESULT result = ::CoCreateInstance(::CLSID_FileSaveDialog, - nullptr, - CLSCTX_ALL, - ::IID_IFileSaveDialog, - reinterpret_cast(&fileSaveDialog)); - - if (!SUCCEEDED(result)) { - NFDi_SetError("Could not create dialog."); - return NFD_ERROR; - } - - // make sure we remember to free the dialog - Release_Guard<::IFileSaveDialog> fileSaveDialogGuard(fileSaveDialog); - - // Build the filter list - if (!AddFiltersToDialog(fileSaveDialog, filterList, filterCount)) { - return NFD_ERROR; - } - - // Set default extension - if (!SetDefaultExtension(fileSaveDialog, filterList, filterCount)) { - return NFD_ERROR; - } - - // Set the default path - if (!SetDefaultPath(fileSaveDialog, defaultPath)) { - return NFD_ERROR; - } - - // Set the default name - if (!SetDefaultName(fileSaveDialog, defaultName)) { - return NFD_ERROR; - } - - // Only show file system items - if (!AddOptions(fileSaveDialog, ::FOS_FORCEFILESYSTEM)) { - return NFD_ERROR; - } - - // Show the dialog. - result = fileSaveDialog->Show(nullptr); - if (SUCCEEDED(result)) { - // Get the file name - ::IShellItem* psiResult; - result = fileSaveDialog->GetResult(&psiResult); - if (!SUCCEEDED(result)) { - NFDi_SetError("Could not get shell item from dialog."); - return NFD_ERROR; - } - Release_Guard<::IShellItem> psiResultGuard(psiResult); - - nfdnchar_t* filePath; - result = psiResult->GetDisplayName(::SIGDN_FILESYSPATH, &filePath); - if (!SUCCEEDED(result)) { - NFDi_SetError("Could not get file path from shell item returned by dialog."); - return NFD_ERROR; - } - - *outPath = filePath; - - return NFD_OKAY; - } else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED)) { - return NFD_CANCEL; - } else { - NFDi_SetError("File dialog box show failed."); - return NFD_ERROR; - } -} - -nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) { - ::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, defaultPath)) { - return NFD_ERROR; - } - - // Only show items that are folders and on the file system - if (!AddOptions(fileOpenDialog, ::FOS_FORCEFILESYSTEM | ::FOS_PICKFOLDERS)) { - return NFD_ERROR; - } - - // Show the dialog to the user - const HRESULT result = fileOpenDialog->Show(nullptr); - if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED)) { - return NFD_CANCEL; - } else if (!SUCCEEDED(result)) { - NFDi_SetError("File dialog box show failed."); - return NFD_ERROR; - } - - // Get the shell item result - ::IShellItem* psiResult; - if (!SUCCEEDED(fileOpenDialog->GetResult(&psiResult))) { - return NFD_ERROR; - } - - Release_Guard<::IShellItem> psiResultGuard(psiResult); - - // Finally get the path - nfdnchar_t* filePath; - // Why are we not using SIGDN_FILESYSPATH? - if (!SUCCEEDED(psiResult->GetDisplayName(::SIGDN_DESKTOPABSOLUTEPARSING, &filePath))) { - NFDi_SetError("Could not get file path from shell item returned by dialog."); - return NFD_ERROR; - } - - *outPath = filePath; - - return NFD_OKAY; -} - -nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count) { - assert(pathSet); - // const_cast because methods on IShellItemArray aren't const, but it should act like const to - // the caller - ::IShellItemArray* psiaPathSet = - const_cast<::IShellItemArray*>(static_cast(pathSet)); - - DWORD numPaths; - if (!SUCCEEDED(psiaPathSet->GetCount(&numPaths))) { - NFDi_SetError("Could not get path count."); - return NFD_ERROR; - } - *count = numPaths; - return NFD_OKAY; -} - -nfdresult_t NFD_PathSet_GetPathN(const nfdpathset_t* pathSet, - nfdpathsetsize_t index, - nfdnchar_t** outPath) { - assert(pathSet); - // const_cast because methods on IShellItemArray aren't const, but it should act like const to - // the caller - ::IShellItemArray* psiaPathSet = - const_cast<::IShellItemArray*>(static_cast(pathSet)); - - ::IShellItem* psiPath; - if (!SUCCEEDED(psiaPathSet->GetItemAt(index, &psiPath))) { - NFDi_SetError("Could not get shell item."); - return NFD_ERROR; - } - - Release_Guard<::IShellItem> psiPathGuard(psiPath); - - nfdnchar_t* name; - if (!SUCCEEDED(psiPath->GetDisplayName(::SIGDN_FILESYSPATH, &name))) { - NFDi_SetError("Could not get file path from shell item."); - return NFD_ERROR; - } - - *outPath = name; - return NFD_OKAY; -} - -nfdresult_t NFD_PathSet_GetEnum(const nfdpathset_t* pathSet, nfdpathsetenum_t* outEnumerator) { - assert(pathSet); - // const_cast because methods on IShellItemArray aren't const, but it should act like const to - // the caller - ::IShellItemArray* psiaPathSet = - const_cast<::IShellItemArray*>(static_cast(pathSet)); - - ::IEnumShellItems* pesiPaths; - if (!SUCCEEDED(psiaPathSet->EnumItems(&pesiPaths))) { - NFDi_SetError("Could not get enumerator."); - return NFD_ERROR; - } - - outEnumerator->ptr = static_cast(pesiPaths); - return NFD_OKAY; -} - -void NFD_PathSet_FreeEnum(nfdpathsetenum_t* enumerator) { - assert(enumerator->ptr); - - ::IEnumShellItems* pesiPaths = static_cast<::IEnumShellItems*>(enumerator->ptr); - - // free the enumerator memory - pesiPaths->Release(); -} - -nfdresult_t NFD_PathSet_EnumNextN(nfdpathsetenum_t* enumerator, nfdnchar_t** outPath) { - assert(enumerator->ptr); - - ::IEnumShellItems* pesiPaths = static_cast<::IEnumShellItems*>(enumerator->ptr); - - ::IShellItem* psiPath; - HRESULT res = pesiPaths->Next(1, &psiPath, NULL); - if (!SUCCEEDED(res)) { - NFDi_SetError("Could not get next item of enumerator."); - return NFD_ERROR; - } - if (res != S_OK) { - *outPath = nullptr; - return NFD_OKAY; - } - - Release_Guard<::IShellItem> psiPathGuard(psiPath); - - nfdnchar_t* name; - if (!SUCCEEDED(psiPath->GetDisplayName(::SIGDN_FILESYSPATH, &name))) { - NFDi_SetError("Could not get file path from shell item."); - return NFD_ERROR; - } - - *outPath = name; - return NFD_OKAY; -} - -void NFD_PathSet_Free(const nfdpathset_t* pathSet) { - assert(pathSet); - // const_cast because methods on IShellItemArray aren't const, but it should act like const to - // the caller - ::IShellItemArray* psiaPathSet = - const_cast<::IShellItemArray*>(static_cast(pathSet)); - - // free the path set memory - psiaPathSet->Release(); -} - -namespace { -// allocs the space in outStr -- call NFDi_Free() -nfdresult_t CopyCharToWChar(const nfdu8char_t* inStr, nfdnchar_t*& outStr) { - int charsNeeded = MultiByteToWideChar(CP_UTF8, 0, inStr, -1, nullptr, 0); - assert(charsNeeded); - - nfdnchar_t* tmp_outStr = NFDi_Malloc(sizeof(nfdnchar_t) * charsNeeded); - if (!tmp_outStr) { - return NFD_ERROR; - } - - int ret = MultiByteToWideChar(CP_UTF8, 0, inStr, -1, tmp_outStr, charsNeeded); - assert(ret && ret == charsNeeded); - (void)ret; // prevent warning in release build - outStr = tmp_outStr; - return NFD_OKAY; -} - -// allocs the space in outPath -- call NFDi_Free() -nfdresult_t CopyWCharToNFDChar(const nfdnchar_t* inStr, nfdu8char_t*& outStr) { - int bytesNeeded = WideCharToMultiByte(CP_UTF8, 0, inStr, -1, nullptr, 0, nullptr, nullptr); - assert(bytesNeeded); - - nfdu8char_t* tmp_outStr = NFDi_Malloc(sizeof(nfdu8char_t) * bytesNeeded); - if (!tmp_outStr) { - return NFD_ERROR; - } - - int ret = WideCharToMultiByte(CP_UTF8, 0, inStr, -1, tmp_outStr, bytesNeeded, nullptr, nullptr); - assert(ret && ret == bytesNeeded); - (void)ret; // prevent warning in release build - outStr = tmp_outStr; - return NFD_OKAY; -} - -struct FilterItem_Guard { - nfdnfilteritem_t* data; - nfdfiltersize_t index; - FilterItem_Guard() noexcept : data(nullptr), index(0) {} - ~FilterItem_Guard() { - assert(data || index == 0); - for (--index; index != static_cast(-1); --index) { - NFDi_Free(const_cast(data[index].spec)); - NFDi_Free(const_cast(data[index].name)); - } - if (data) NFDi_Free(data); - } -}; - -nfdresult_t CopyFilterItem(const nfdu8filteritem_t* filterList, - nfdfiltersize_t count, - FilterItem_Guard& filterItemsNGuard) { - if (count) { - nfdnfilteritem_t*& filterItemsN = filterItemsNGuard.data; - filterItemsN = NFDi_Malloc(sizeof(nfdnfilteritem_t) * count); - if (!filterItemsN) { - return NFD_ERROR; - } - - nfdfiltersize_t& index = filterItemsNGuard.index; - for (; index != count; ++index) { - nfdresult_t res = CopyCharToWChar(filterList[index].name, - const_cast(filterItemsN[index].name)); - if (!res) { - return NFD_ERROR; - } - res = CopyCharToWChar(filterList[index].spec, - const_cast(filterItemsN[index].spec)); - if (!res) { - // remember to free the name, because we also created it (and it won't be protected - // by the guard, because we have not incremented the index) - NFDi_Free(const_cast(filterItemsN[index].name)); - return NFD_ERROR; - } - } - } - return NFD_OKAY; -} -nfdresult_t ConvertU8ToNative(const nfdu8char_t* u8Text, FreeCheck_Guard& nativeText) { - if (u8Text) { - nfdresult_t res = CopyCharToWChar(u8Text, nativeText.data); - if (!res) { - return NFD_ERROR; - } - } - return NFD_OKAY; -} -void NormalizePathSeparator(nfdnchar_t* path) { - if (path) { - for (; *path; ++path) { - if (*path == L'/') *path = L'\\'; - } - } -} -} // namespace - -void NFD_FreePathU8(nfdu8char_t* outPath) { - NFDi_Free(outPath); -} - -nfdresult_t NFD_OpenDialogU8(nfdu8char_t** outPath, - const nfdu8filteritem_t* filterList, - nfdfiltersize_t count, - const nfdu8char_t* defaultPath) { - // populate the real nfdnfilteritem_t - FilterItem_Guard filterItemsNGuard; - if (!CopyFilterItem(filterList, count, filterItemsNGuard)) { - return NFD_ERROR; - } - - // convert and normalize the default path, but only if it is not nullptr - FreeCheck_Guard defaultPathNGuard; - ConvertU8ToNative(defaultPath, defaultPathNGuard); - NormalizePathSeparator(defaultPathNGuard.data); - - // call the native function - nfdnchar_t* outPathN; - nfdresult_t res = - NFD_OpenDialogN(&outPathN, filterItemsNGuard.data, count, defaultPathNGuard.data); - - if (res != NFD_OKAY) { - return res; - } - - // convert the outPath to UTF-8 - res = CopyWCharToNFDChar(outPathN, *outPath); - - // free the native out path, and return the result - NFD_FreePathN(outPathN); - return res; -} - -/* multiple file open dialog */ -/* It is the caller's responsibility to free `outPaths` via NFD_PathSet_Free() if this function - * returns NFD_OKAY */ -nfdresult_t NFD_OpenDialogMultipleU8(const nfdpathset_t** outPaths, - const nfdu8filteritem_t* filterList, - nfdfiltersize_t count, - const nfdu8char_t* defaultPath) { - // populate the real nfdnfilteritem_t - FilterItem_Guard filterItemsNGuard; - if (!CopyFilterItem(filterList, count, filterItemsNGuard)) { - return NFD_ERROR; - } - - // convert and normalize the default path, but only if it is not nullptr - FreeCheck_Guard defaultPathNGuard; - ConvertU8ToNative(defaultPath, defaultPathNGuard); - NormalizePathSeparator(defaultPathNGuard.data); - - // call the native function - return NFD_OpenDialogMultipleN(outPaths, filterItemsNGuard.data, count, defaultPathNGuard.data); -} - -/* save dialog */ -/* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns - * NFD_OKAY */ -nfdresult_t NFD_SaveDialogU8(nfdu8char_t** outPath, - const nfdu8filteritem_t* filterList, - nfdfiltersize_t count, - const nfdu8char_t* defaultPath, - const nfdu8char_t* defaultName) { - // populate the real nfdnfilteritem_t - FilterItem_Guard filterItemsNGuard; - if (!CopyFilterItem(filterList, count, filterItemsNGuard)) { - return NFD_ERROR; - } - - // convert and normalize the default path, but only if it is not nullptr - FreeCheck_Guard defaultPathNGuard; - ConvertU8ToNative(defaultPath, defaultPathNGuard); - NormalizePathSeparator(defaultPathNGuard.data); - - // convert the default name, but only if it is not nullptr - FreeCheck_Guard defaultNameNGuard; - ConvertU8ToNative(defaultName, defaultNameNGuard); - - // call the native function - nfdnchar_t* outPathN; - nfdresult_t res = NFD_SaveDialogN( - &outPathN, filterItemsNGuard.data, count, defaultPathNGuard.data, defaultNameNGuard.data); - - if (res != NFD_OKAY) { - return res; - } - - // convert the outPath to UTF-8 - res = CopyWCharToNFDChar(outPathN, *outPath); - - // free the native out path, and return the result - NFD_FreePathN(outPathN); - return res; -} - -/* select folder dialog */ -/* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns - * NFD_OKAY */ -nfdresult_t NFD_PickFolderU8(nfdu8char_t** outPath, const nfdu8char_t* defaultPath) { - // convert and normalize the default path, but only if it is not nullptr - FreeCheck_Guard defaultPathNGuard; - ConvertU8ToNative(defaultPath, defaultPathNGuard); - NormalizePathSeparator(defaultPathNGuard.data); - - // call the native function - nfdnchar_t* outPathN; - nfdresult_t res = NFD_PickFolderN(&outPathN, defaultPathNGuard.data); - - if (res != NFD_OKAY) { - return res; - } - - // convert the outPath to UTF-8 - res = CopyWCharToNFDChar(outPathN, *outPath); - - // free the native out path, and return the result - NFD_FreePathN(outPathN); - return res; -} - -/* Get the UTF-8 path at offset index */ -/* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns - * NFD_OKAY */ -nfdresult_t NFD_PathSet_GetPathU8(const nfdpathset_t* pathSet, - nfdpathsetsize_t index, - nfdu8char_t** outPath) { - // call the native function - nfdnchar_t* outPathN; - nfdresult_t res = NFD_PathSet_GetPathN(pathSet, index, &outPathN); - - if (res != NFD_OKAY) { - return res; - } - - // convert the outPath to UTF-8 - res = CopyWCharToNFDChar(outPathN, *outPath); - - // free the native out path, and return the result - NFD_FreePathN(outPathN); - return res; -} - -nfdresult_t NFD_PathSet_EnumNextU8(nfdpathsetenum_t* enumerator, nfdu8char_t** outPath) { - // call the native function - nfdnchar_t* outPathN; - nfdresult_t res = NFD_PathSet_EnumNextN(enumerator, &outPathN); - - if (res != NFD_OKAY) { - return res; - } - - if (outPathN) { - // convert the outPath to UTF-8 - res = CopyWCharToNFDChar(outPathN, *outPath); - - // free the native out path, and return the result - NFD_FreePathN(outPathN); - } else { - *outPath = nullptr; - res = NFD_OKAY; - } - - return res; -} +/* + Native File Dialog Extended + Repository: https://github.com/btzy/nativefiledialog-extended + License: Zlib + Author: Bernard Teo + */ + +/* only locally define UNICODE in this compilation unit */ +#ifndef UNICODE +#define UNICODE +#endif + +#ifdef __MINGW32__ +// Explicitly setting NTDDI version, this is necessary for the MinGW compiler +#define NTDDI_VERSION NTDDI_VISTA +#define _WIN32_WINNT _WIN32_WINNT_VISTA +#endif + +#if _MSC_VER +// see +// https://developercommunity.visualstudio.com/content/problem/185399/error-c2760-in-combaseapih-with-windows-sdk-81-and.html +struct IUnknown; // Workaround for "combaseapi.h(229): error C2187: syntax error: 'identifier' was + // unexpected here" when using /permissive- +#endif + +#include +#include +#include +#include +#include +#include "nfd.h" + +namespace { + +/* current error */ +const char* g_errorstr = nullptr; + +void NFDi_SetError(const char* msg) { + g_errorstr = msg; +} + +template +T* NFDi_Malloc(size_t bytes) { + void* ptr = malloc(bytes); + if (!ptr) NFDi_SetError("NFDi_Malloc failed."); + + return static_cast(ptr); +} + +template +void NFDi_Free(T* ptr) { + assert(ptr); + free(static_cast(ptr)); +} + +/* guard objects */ +template +struct Release_Guard { + T* data; + Release_Guard(T* releasable) noexcept : data(releasable) {} + ~Release_Guard() { data->Release(); } +}; + +template +struct Free_Guard { + T* data; + Free_Guard(T* freeable) noexcept : data(freeable) {} + ~Free_Guard() { NFDi_Free(data); } +}; + +template +struct FreeCheck_Guard { + T* data; + FreeCheck_Guard(T* freeable = nullptr) noexcept : data(freeable) {} + ~FreeCheck_Guard() { + if (data) NFDi_Free(data); + } +}; + +/* helper functions */ +nfdresult_t AddFiltersToDialog(::IFileDialog* fileOpenDialog, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount) { + /* filterCount plus 1 because we hardcode the *.* wildcard after the while loop */ + COMDLG_FILTERSPEC* specList = + NFDi_Malloc(sizeof(COMDLG_FILTERSPEC) * (filterCount + 1)); + if (!specList) { + return NFD_ERROR; + } + + /* ad-hoc RAII object to free memory when destructing */ + struct COMDLG_FILTERSPEC_Guard { + COMDLG_FILTERSPEC* _specList; + nfdfiltersize_t index; + COMDLG_FILTERSPEC_Guard(COMDLG_FILTERSPEC* specList) noexcept + : _specList(specList), index(0) {} + ~COMDLG_FILTERSPEC_Guard() { + for (--index; index != static_cast(-1); --index) { + NFDi_Free(const_cast(_specList[index].pszSpec)); + } + NFDi_Free(_specList); + } + }; + + COMDLG_FILTERSPEC_Guard specListGuard(specList); + + if (filterCount) { + assert(filterList); + + // we have filters to add ... format and add them + + // use the index that comes from the RAII object (instead of making a copy), so the RAII + // object will know which memory to free + nfdfiltersize_t& index = specListGuard.index; + + for (; index != filterCount; ++index) { + // set the friendly name of this filter + specList[index].pszName = filterList[index].name; + + // set the specification of this filter... + + // count number of file extensions + size_t sep = 1; + for (const nfdnchar_t* p_spec = filterList[index].spec; *p_spec; ++p_spec) { + if (*p_spec == L',') { + ++sep; + } + } + + // calculate space needed (including the trailing '\0') + size_t specSize = sep * 2 + wcslen(filterList[index].spec) + 1; + + // malloc the required memory and populate it + nfdnchar_t* specBuf = NFDi_Malloc(sizeof(nfdnchar_t) * specSize); + + if (!specBuf) { + // automatic freeing of memory via COMDLG_FILTERSPEC_Guard + return NFD_ERROR; + } + + // convert "png,jpg" to "*.png;*.jpg" as required by Windows ... + nfdnchar_t* p_specBuf = specBuf; + *p_specBuf++ = L'*'; + *p_specBuf++ = L'.'; + for (const nfdnchar_t* p_spec = filterList[index].spec; *p_spec; ++p_spec) { + if (*p_spec == L',') { + *p_specBuf++ = L';'; + *p_specBuf++ = L'*'; + *p_specBuf++ = L'.'; + } else { + *p_specBuf++ = *p_spec; + } + } + *p_specBuf++ = L'\0'; + + // assert that we had allocated exactly the correct amount of memory that we used + assert(static_cast(p_specBuf - specBuf) == specSize); + + // save the buffer to the guard object + specList[index].pszSpec = specBuf; + } + } + + /* Add wildcard */ + specList[filterCount].pszName = L"All files"; + specList[filterCount].pszSpec = L"*.*"; + + // add the filter to the dialog + if (!SUCCEEDED(fileOpenDialog->SetFileTypes(filterCount + 1, specList))) { + NFDi_SetError("Failed to set the allowable file types for the drop-down menu."); + return NFD_ERROR; + } + + // automatic freeing of memory via COMDLG_FILTERSPEC_Guard + return NFD_OKAY; +} + +/* call after AddFiltersToDialog */ +nfdresult_t SetDefaultExtension(::IFileDialog* fileOpenDialog, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount) { + // if there are no filters, then don't set default extensions + if (!filterCount) { + return NFD_OKAY; + } + + assert(filterList); + + // set the first item as the default index, and set the default extension + if (!SUCCEEDED(fileOpenDialog->SetFileTypeIndex(1))) { + NFDi_SetError("Failed to set the selected file type index."); + return NFD_ERROR; + } + + // set the first item as the default file extension + const nfdnchar_t* p_spec = filterList[0].spec; + for (; *p_spec; ++p_spec) { + if (*p_spec == ',') { + break; + } + } + if (*p_spec) { + // multiple file extensions for this type (need to allocate memory) + size_t numChars = p_spec - filterList[0].spec; + // allocate one more char space for the '\0' + nfdnchar_t* extnBuf = NFDi_Malloc(sizeof(nfdnchar_t) * (numChars + 1)); + if (!extnBuf) { + return NFD_ERROR; + } + Free_Guard extnBufGuard(extnBuf); + + // copy the extension + for (size_t i = 0; i != numChars; ++i) { + extnBuf[i] = filterList[0].spec[i]; + } + // pad with trailing '\0' + extnBuf[numChars] = L'\0'; + + if (!SUCCEEDED(fileOpenDialog->SetDefaultExtension(extnBuf))) { + NFDi_SetError("Failed to set default extension."); + return NFD_ERROR; + } + } else { + // single file extension for this type (no need to allocate memory) + if (!SUCCEEDED(fileOpenDialog->SetDefaultExtension(filterList[0].spec))) { + NFDi_SetError("Failed to set default extension."); + return NFD_ERROR; + } + } + + return NFD_OKAY; +} + +nfdresult_t SetDefaultPath(IFileDialog* dialog, const nfdnchar_t* defaultPath) { + if (!defaultPath || !*defaultPath) return NFD_OKAY; + + IShellItem* folder; + HRESULT result = SHCreateItemFromParsingName(defaultPath, nullptr, IID_PPV_ARGS(&folder)); + + // Valid non results. + if (result == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || + result == HRESULT_FROM_WIN32(ERROR_INVALID_DRIVE)) { + return NFD_OKAY; + } + + if (!SUCCEEDED(result)) { + NFDi_SetError("Failed to create ShellItem for setting the default path."); + return NFD_ERROR; + } + + Release_Guard folderGuard(folder); + + // SetDefaultFolder() might use another recently used folder if available, so the user doesn't + // need to keep navigating back to the default folder (recommended by Windows). change to + // SetFolder() if you always want to use the default folder + if (!SUCCEEDED(dialog->SetDefaultFolder(folder))) { + NFDi_SetError("Failed to set default path."); + return NFD_ERROR; + } + + return NFD_OKAY; +} + +nfdresult_t SetDefaultName(IFileDialog* dialog, const nfdnchar_t* defaultName) { + if (!defaultName || !*defaultName) return NFD_OKAY; + + if (!SUCCEEDED(dialog->SetFileName(defaultName))) { + NFDi_SetError("Failed to set default file name."); + return NFD_ERROR; + } + + return NFD_OKAY; +} + +nfdresult_t AddOptions(IFileDialog* dialog, FILEOPENDIALOGOPTIONS options) { + FILEOPENDIALOGOPTIONS existingOptions; + if (!SUCCEEDED(dialog->GetOptions(&existingOptions))) { + NFDi_SetError("Failed to get options."); + return NFD_ERROR; + } + if (!SUCCEEDED(dialog->SetOptions(existingOptions | options))) { + NFDi_SetError("Failed to set options."); + return NFD_ERROR; + } + return NFD_OKAY; +} +} // namespace + +const char* NFD_GetError(void) { + return g_errorstr; +} + +void NFD_ClearError(void) { + NFDi_SetError(nullptr); +} + +/* public */ + +namespace { +// The user might have initialized with COINIT_MULTITHREADED before, +// in which case we will fail to do CoInitializeEx(), but file dialogs will still work. +// See https://github.com/mlabbe/nativefiledialog/issues/72 for more information. +bool needs_uninitialize; +} // namespace + +nfdresult_t NFD_Init(void) { + // Init COM library. + HRESULT result = + ::CoInitializeEx(nullptr, ::COINIT_APARTMENTTHREADED | ::COINIT_DISABLE_OLE1DDE); + + if (SUCCEEDED(result)) { + needs_uninitialize = true; + return NFD_OKAY; + } else if (result == RPC_E_CHANGED_MODE) { + // If this happens, the user already initialized COM using COINIT_MULTITHREADED, + // so COM will still work, but we shouldn't uninitialize it later. + needs_uninitialize = false; + return NFD_OKAY; + } else { + NFDi_SetError("Failed to initialize COM."); + return NFD_ERROR; + } +} +void NFD_Quit(void) { + if (needs_uninitialize) ::CoUninitialize(); +} + +void NFD_FreePathN(nfdnchar_t* filePath) { + assert(filePath); + ::CoTaskMemFree(filePath); +} + +nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath) { + ::IFileOpenDialog* fileOpenDialog; + + // Create dialog + HRESULT result = ::CoCreateInstance(::CLSID_FileOpenDialog, + nullptr, + CLSCTX_ALL, + ::IID_IFileOpenDialog, + reinterpret_cast(&fileOpenDialog)); + + if (!SUCCEEDED(result)) { + NFDi_SetError("Could not create dialog."); + return NFD_ERROR; + } + + // make sure we remember to free the dialog + Release_Guard<::IFileOpenDialog> fileOpenDialogGuard(fileOpenDialog); + + // Build the filter list + if (!AddFiltersToDialog(fileOpenDialog, filterList, filterCount)) { + return NFD_ERROR; + } + + // Set auto-completed default extension + if (!SetDefaultExtension(fileOpenDialog, filterList, filterCount)) { + return NFD_ERROR; + } + + // Set the default path + if (!SetDefaultPath(fileOpenDialog, defaultPath)) { + return NFD_ERROR; + } + + // Only show file system items + if (!AddOptions(fileOpenDialog, ::FOS_FORCEFILESYSTEM)) { + return NFD_ERROR; + } + + // Show the dialog. + result = fileOpenDialog->Show(nullptr); + if (SUCCEEDED(result)) { + // Get the file name + ::IShellItem* psiResult; + result = fileOpenDialog->GetResult(&psiResult); + if (!SUCCEEDED(result)) { + NFDi_SetError("Could not get shell item from dialog."); + return NFD_ERROR; + } + Release_Guard<::IShellItem> psiResultGuard(psiResult); + + nfdnchar_t* filePath; + result = psiResult->GetDisplayName(::SIGDN_FILESYSPATH, &filePath); + if (!SUCCEEDED(result)) { + NFDi_SetError("Could not get file path from shell item returned by dialog."); + return NFD_ERROR; + } + + *outPath = filePath; + + return NFD_OKAY; + } else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED)) { + return NFD_CANCEL; + } else { + NFDi_SetError("File dialog box show failed."); + return NFD_ERROR; + } +} + +nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath) { + ::IFileOpenDialog* fileOpenDialog(nullptr); + + // Create dialog + HRESULT result = ::CoCreateInstance(::CLSID_FileOpenDialog, + nullptr, + CLSCTX_ALL, + ::IID_IFileOpenDialog, + reinterpret_cast(&fileOpenDialog)); + + if (!SUCCEEDED(result)) { + NFDi_SetError("Could not create dialog."); + return NFD_ERROR; + } + + // make sure we remember to free the dialog + Release_Guard<::IFileOpenDialog> fileOpenDialogGuard(fileOpenDialog); + + // Build the filter list + if (!AddFiltersToDialog(fileOpenDialog, filterList, filterCount)) { + return NFD_ERROR; + } + + // Set auto-completed default extension + if (!SetDefaultExtension(fileOpenDialog, filterList, filterCount)) { + return NFD_ERROR; + } + + // Set the default path + if (!SetDefaultPath(fileOpenDialog, defaultPath)) { + return NFD_ERROR; + } + + // Set a flag for multiple options and file system items only + if (!AddOptions(fileOpenDialog, ::FOS_FORCEFILESYSTEM | ::FOS_ALLOWMULTISELECT)) { + return NFD_ERROR; + } + + // Show the dialog. + result = fileOpenDialog->Show(nullptr); + if (SUCCEEDED(result)) { + ::IShellItemArray* shellItems; + result = fileOpenDialog->GetResults(&shellItems); + if (!SUCCEEDED(result)) { + NFDi_SetError("Could not get shell items."); + 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_SaveDialogN(nfdnchar_t** outPath, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath, + const nfdnchar_t* defaultName) { + ::IFileSaveDialog* fileSaveDialog; + + // Create dialog + HRESULT result = ::CoCreateInstance(::CLSID_FileSaveDialog, + nullptr, + CLSCTX_ALL, + ::IID_IFileSaveDialog, + reinterpret_cast(&fileSaveDialog)); + + if (!SUCCEEDED(result)) { + NFDi_SetError("Could not create dialog."); + return NFD_ERROR; + } + + // make sure we remember to free the dialog + Release_Guard<::IFileSaveDialog> fileSaveDialogGuard(fileSaveDialog); + + // Build the filter list + if (!AddFiltersToDialog(fileSaveDialog, filterList, filterCount)) { + return NFD_ERROR; + } + + // Set default extension + if (!SetDefaultExtension(fileSaveDialog, filterList, filterCount)) { + return NFD_ERROR; + } + + // Set the default path + if (!SetDefaultPath(fileSaveDialog, defaultPath)) { + return NFD_ERROR; + } + + // Set the default name + if (!SetDefaultName(fileSaveDialog, defaultName)) { + return NFD_ERROR; + } + + // Only show file system items + if (!AddOptions(fileSaveDialog, ::FOS_FORCEFILESYSTEM)) { + return NFD_ERROR; + } + + // Show the dialog. + result = fileSaveDialog->Show(nullptr); + if (SUCCEEDED(result)) { + // Get the file name + ::IShellItem* psiResult; + result = fileSaveDialog->GetResult(&psiResult); + if (!SUCCEEDED(result)) { + NFDi_SetError("Could not get shell item from dialog."); + return NFD_ERROR; + } + Release_Guard<::IShellItem> psiResultGuard(psiResult); + + nfdnchar_t* filePath; + result = psiResult->GetDisplayName(::SIGDN_FILESYSPATH, &filePath); + if (!SUCCEEDED(result)) { + NFDi_SetError("Could not get file path from shell item returned by dialog."); + return NFD_ERROR; + } + + *outPath = filePath; + + return NFD_OKAY; + } else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED)) { + return NFD_CANCEL; + } else { + NFDi_SetError("File dialog box show failed."); + return NFD_ERROR; + } +} + +nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) { + ::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, defaultPath)) { + return NFD_ERROR; + } + + // Only show items that are folders and on the file system + if (!AddOptions(fileOpenDialog, ::FOS_FORCEFILESYSTEM | ::FOS_PICKFOLDERS)) { + return NFD_ERROR; + } + + // Show the dialog to the user + const HRESULT result = fileOpenDialog->Show(nullptr); + if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED)) { + return NFD_CANCEL; + } else if (!SUCCEEDED(result)) { + NFDi_SetError("File dialog box show failed."); + return NFD_ERROR; + } + + // Get the shell item result + ::IShellItem* psiResult; + if (!SUCCEEDED(fileOpenDialog->GetResult(&psiResult))) { + return NFD_ERROR; + } + + Release_Guard<::IShellItem> psiResultGuard(psiResult); + + // Finally get the path + nfdnchar_t* filePath; + // Why are we not using SIGDN_FILESYSPATH? + if (!SUCCEEDED(psiResult->GetDisplayName(::SIGDN_DESKTOPABSOLUTEPARSING, &filePath))) { + NFDi_SetError("Could not get file path from shell item returned by dialog."); + return NFD_ERROR; + } + + *outPath = filePath; + + return NFD_OKAY; +} + +nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count) { + assert(pathSet); + // const_cast because methods on IShellItemArray aren't const, but it should act like const to + // the caller + ::IShellItemArray* psiaPathSet = + const_cast<::IShellItemArray*>(static_cast(pathSet)); + + DWORD numPaths; + if (!SUCCEEDED(psiaPathSet->GetCount(&numPaths))) { + NFDi_SetError("Could not get path count."); + return NFD_ERROR; + } + *count = numPaths; + return NFD_OKAY; +} + +nfdresult_t NFD_PathSet_GetPathN(const nfdpathset_t* pathSet, + nfdpathsetsize_t index, + nfdnchar_t** outPath) { + assert(pathSet); + // const_cast because methods on IShellItemArray aren't const, but it should act like const to + // the caller + ::IShellItemArray* psiaPathSet = + const_cast<::IShellItemArray*>(static_cast(pathSet)); + + ::IShellItem* psiPath; + if (!SUCCEEDED(psiaPathSet->GetItemAt(index, &psiPath))) { + NFDi_SetError("Could not get shell item."); + return NFD_ERROR; + } + + Release_Guard<::IShellItem> psiPathGuard(psiPath); + + nfdnchar_t* name; + if (!SUCCEEDED(psiPath->GetDisplayName(::SIGDN_FILESYSPATH, &name))) { + NFDi_SetError("Could not get file path from shell item."); + return NFD_ERROR; + } + + *outPath = name; + return NFD_OKAY; +} + +nfdresult_t NFD_PathSet_GetEnum(const nfdpathset_t* pathSet, nfdpathsetenum_t* outEnumerator) { + assert(pathSet); + // const_cast because methods on IShellItemArray aren't const, but it should act like const to + // the caller + ::IShellItemArray* psiaPathSet = + const_cast<::IShellItemArray*>(static_cast(pathSet)); + + ::IEnumShellItems* pesiPaths; + if (!SUCCEEDED(psiaPathSet->EnumItems(&pesiPaths))) { + NFDi_SetError("Could not get enumerator."); + return NFD_ERROR; + } + + outEnumerator->ptr = static_cast(pesiPaths); + return NFD_OKAY; +} + +void NFD_PathSet_FreeEnum(nfdpathsetenum_t* enumerator) { + assert(enumerator->ptr); + + ::IEnumShellItems* pesiPaths = static_cast<::IEnumShellItems*>(enumerator->ptr); + + // free the enumerator memory + pesiPaths->Release(); +} + +nfdresult_t NFD_PathSet_EnumNextN(nfdpathsetenum_t* enumerator, nfdnchar_t** outPath) { + assert(enumerator->ptr); + + ::IEnumShellItems* pesiPaths = static_cast<::IEnumShellItems*>(enumerator->ptr); + + ::IShellItem* psiPath; + HRESULT res = pesiPaths->Next(1, &psiPath, NULL); + if (!SUCCEEDED(res)) { + NFDi_SetError("Could not get next item of enumerator."); + return NFD_ERROR; + } + if (res != S_OK) { + *outPath = nullptr; + return NFD_OKAY; + } + + Release_Guard<::IShellItem> psiPathGuard(psiPath); + + nfdnchar_t* name; + if (!SUCCEEDED(psiPath->GetDisplayName(::SIGDN_FILESYSPATH, &name))) { + NFDi_SetError("Could not get file path from shell item."); + return NFD_ERROR; + } + + *outPath = name; + return NFD_OKAY; +} + +void NFD_PathSet_Free(const nfdpathset_t* pathSet) { + assert(pathSet); + // const_cast because methods on IShellItemArray aren't const, but it should act like const to + // the caller + ::IShellItemArray* psiaPathSet = + const_cast<::IShellItemArray*>(static_cast(pathSet)); + + // free the path set memory + psiaPathSet->Release(); +} + +namespace { +// allocs the space in outStr -- call NFDi_Free() +nfdresult_t CopyCharToWChar(const nfdu8char_t* inStr, nfdnchar_t*& outStr) { + int charsNeeded = MultiByteToWideChar(CP_UTF8, 0, inStr, -1, nullptr, 0); + assert(charsNeeded); + + nfdnchar_t* tmp_outStr = NFDi_Malloc(sizeof(nfdnchar_t) * charsNeeded); + if (!tmp_outStr) { + return NFD_ERROR; + } + + int ret = MultiByteToWideChar(CP_UTF8, 0, inStr, -1, tmp_outStr, charsNeeded); + assert(ret && ret == charsNeeded); + (void)ret; // prevent warning in release build + outStr = tmp_outStr; + return NFD_OKAY; +} + +// allocs the space in outPath -- call NFDi_Free() +nfdresult_t CopyWCharToNFDChar(const nfdnchar_t* inStr, nfdu8char_t*& outStr) { + int bytesNeeded = WideCharToMultiByte(CP_UTF8, 0, inStr, -1, nullptr, 0, nullptr, nullptr); + assert(bytesNeeded); + + nfdu8char_t* tmp_outStr = NFDi_Malloc(sizeof(nfdu8char_t) * bytesNeeded); + if (!tmp_outStr) { + return NFD_ERROR; + } + + int ret = WideCharToMultiByte(CP_UTF8, 0, inStr, -1, tmp_outStr, bytesNeeded, nullptr, nullptr); + assert(ret && ret == bytesNeeded); + (void)ret; // prevent warning in release build + outStr = tmp_outStr; + return NFD_OKAY; +} + +struct FilterItem_Guard { + nfdnfilteritem_t* data; + nfdfiltersize_t index; + FilterItem_Guard() noexcept : data(nullptr), index(0) {} + ~FilterItem_Guard() { + assert(data || index == 0); + for (--index; index != static_cast(-1); --index) { + NFDi_Free(const_cast(data[index].spec)); + NFDi_Free(const_cast(data[index].name)); + } + if (data) NFDi_Free(data); + } +}; + +nfdresult_t CopyFilterItem(const nfdu8filteritem_t* filterList, + nfdfiltersize_t count, + FilterItem_Guard& filterItemsNGuard) { + if (count) { + nfdnfilteritem_t*& filterItemsN = filterItemsNGuard.data; + filterItemsN = NFDi_Malloc(sizeof(nfdnfilteritem_t) * count); + if (!filterItemsN) { + return NFD_ERROR; + } + + nfdfiltersize_t& index = filterItemsNGuard.index; + for (; index != count; ++index) { + nfdresult_t res = CopyCharToWChar(filterList[index].name, + const_cast(filterItemsN[index].name)); + if (!res) { + return NFD_ERROR; + } + res = CopyCharToWChar(filterList[index].spec, + const_cast(filterItemsN[index].spec)); + if (!res) { + // remember to free the name, because we also created it (and it won't be protected + // by the guard, because we have not incremented the index) + NFDi_Free(const_cast(filterItemsN[index].name)); + return NFD_ERROR; + } + } + } + return NFD_OKAY; +} +nfdresult_t ConvertU8ToNative(const nfdu8char_t* u8Text, FreeCheck_Guard& nativeText) { + if (u8Text) { + nfdresult_t res = CopyCharToWChar(u8Text, nativeText.data); + if (!res) { + return NFD_ERROR; + } + } + return NFD_OKAY; +} +void NormalizePathSeparator(nfdnchar_t* path) { + if (path) { + for (; *path; ++path) { + if (*path == L'/') *path = L'\\'; + } + } +} +} // namespace + +void NFD_FreePathU8(nfdu8char_t* outPath) { + NFDi_Free(outPath); +} + +nfdresult_t NFD_OpenDialogU8(nfdu8char_t** outPath, + const nfdu8filteritem_t* filterList, + nfdfiltersize_t count, + const nfdu8char_t* defaultPath) { + // populate the real nfdnfilteritem_t + FilterItem_Guard filterItemsNGuard; + if (!CopyFilterItem(filterList, count, filterItemsNGuard)) { + return NFD_ERROR; + } + + // convert and normalize the default path, but only if it is not nullptr + FreeCheck_Guard defaultPathNGuard; + ConvertU8ToNative(defaultPath, defaultPathNGuard); + NormalizePathSeparator(defaultPathNGuard.data); + + // call the native function + nfdnchar_t* outPathN; + nfdresult_t res = + NFD_OpenDialogN(&outPathN, filterItemsNGuard.data, count, defaultPathNGuard.data); + + if (res != NFD_OKAY) { + return res; + } + + // convert the outPath to UTF-8 + res = CopyWCharToNFDChar(outPathN, *outPath); + + // free the native out path, and return the result + NFD_FreePathN(outPathN); + return res; +} + +/* multiple file open dialog */ +/* It is the caller's responsibility to free `outPaths` via NFD_PathSet_Free() if this function + * returns NFD_OKAY */ +nfdresult_t NFD_OpenDialogMultipleU8(const nfdpathset_t** outPaths, + const nfdu8filteritem_t* filterList, + nfdfiltersize_t count, + const nfdu8char_t* defaultPath) { + // populate the real nfdnfilteritem_t + FilterItem_Guard filterItemsNGuard; + if (!CopyFilterItem(filterList, count, filterItemsNGuard)) { + return NFD_ERROR; + } + + // convert and normalize the default path, but only if it is not nullptr + FreeCheck_Guard defaultPathNGuard; + ConvertU8ToNative(defaultPath, defaultPathNGuard); + NormalizePathSeparator(defaultPathNGuard.data); + + // call the native function + return NFD_OpenDialogMultipleN(outPaths, filterItemsNGuard.data, count, defaultPathNGuard.data); +} + +/* save dialog */ +/* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns + * NFD_OKAY */ +nfdresult_t NFD_SaveDialogU8(nfdu8char_t** outPath, + const nfdu8filteritem_t* filterList, + nfdfiltersize_t count, + const nfdu8char_t* defaultPath, + const nfdu8char_t* defaultName) { + // populate the real nfdnfilteritem_t + FilterItem_Guard filterItemsNGuard; + if (!CopyFilterItem(filterList, count, filterItemsNGuard)) { + return NFD_ERROR; + } + + // convert and normalize the default path, but only if it is not nullptr + FreeCheck_Guard defaultPathNGuard; + ConvertU8ToNative(defaultPath, defaultPathNGuard); + NormalizePathSeparator(defaultPathNGuard.data); + + // convert the default name, but only if it is not nullptr + FreeCheck_Guard defaultNameNGuard; + ConvertU8ToNative(defaultName, defaultNameNGuard); + + // call the native function + nfdnchar_t* outPathN; + nfdresult_t res = NFD_SaveDialogN( + &outPathN, filterItemsNGuard.data, count, defaultPathNGuard.data, defaultNameNGuard.data); + + if (res != NFD_OKAY) { + return res; + } + + // convert the outPath to UTF-8 + res = CopyWCharToNFDChar(outPathN, *outPath); + + // free the native out path, and return the result + NFD_FreePathN(outPathN); + return res; +} + +/* select folder dialog */ +/* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns + * NFD_OKAY */ +nfdresult_t NFD_PickFolderU8(nfdu8char_t** outPath, const nfdu8char_t* defaultPath) { + // convert and normalize the default path, but only if it is not nullptr + FreeCheck_Guard defaultPathNGuard; + ConvertU8ToNative(defaultPath, defaultPathNGuard); + NormalizePathSeparator(defaultPathNGuard.data); + + // call the native function + nfdnchar_t* outPathN; + nfdresult_t res = NFD_PickFolderN(&outPathN, defaultPathNGuard.data); + + if (res != NFD_OKAY) { + return res; + } + + // convert the outPath to UTF-8 + res = CopyWCharToNFDChar(outPathN, *outPath); + + // free the native out path, and return the result + NFD_FreePathN(outPathN); + return res; +} + +/* Get the UTF-8 path at offset index */ +/* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns + * NFD_OKAY */ +nfdresult_t NFD_PathSet_GetPathU8(const nfdpathset_t* pathSet, + nfdpathsetsize_t index, + nfdu8char_t** outPath) { + // call the native function + nfdnchar_t* outPathN; + nfdresult_t res = NFD_PathSet_GetPathN(pathSet, index, &outPathN); + + if (res != NFD_OKAY) { + return res; + } + + // convert the outPath to UTF-8 + res = CopyWCharToNFDChar(outPathN, *outPath); + + // free the native out path, and return the result + NFD_FreePathN(outPathN); + return res; +} + +nfdresult_t NFD_PathSet_EnumNextU8(nfdpathsetenum_t* enumerator, nfdu8char_t** outPath) { + // call the native function + nfdnchar_t* outPathN; + nfdresult_t res = NFD_PathSet_EnumNextN(enumerator, &outPathN); + + if (res != NFD_OKAY) { + return res; + } + + if (outPathN) { + // convert the outPath to UTF-8 + res = CopyWCharToNFDChar(outPathN, *outPath); + + // free the native out path, and return the result + NFD_FreePathN(outPathN); + } else { + *outPath = nullptr; + res = NFD_OKAY; + } + + return res; +}