/* Native File Dialog http://www.frogtoss.com/labs */ #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 #define _CRTDBG_MAP_ALLOC #include #include /* only locally define UNICODE in this compilation unit */ #ifndef UNICODE #define UNICODE #endif #include #include #include #include #include #include "nfd_common.h" // hack I know #include "../../../src/utfutils.h" // hack 2... #include "../../../src/ta-log.h" class NFDWinEvents: public IFileDialogEvents { nfdselcallback_t selCallback; size_t refCount; virtual ~NFDWinEvents() { } public: IFACEMETHODIMP QueryInterface(REFIID riid, void** ppv) { logV("%p: QueryInterface called DAMN IT",(const void*)this); *ppv=NULL; return E_NOTIMPL; } IFACEMETHODIMP_(ULONG) AddRef() { logV("%p: AddRef() called",(const void*)this); return InterlockedIncrement(&refCount); } IFACEMETHODIMP_(ULONG) Release() { logV("%p: Release() called",(const void*)this); LONG ret=InterlockedDecrement(&refCount); if (ret==0) { logV("%p: Destroying the final object.",(const void*)this); delete this; } return ret; } IFACEMETHODIMP OnFileOk(IFileDialog*) { return S_OK; } IFACEMETHODIMP OnFolderChange(IFileDialog*) { return E_NOTIMPL; } IFACEMETHODIMP OnFolderChanging(IFileDialog*, IShellItem*) { return E_NOTIMPL; } IFACEMETHODIMP OnOverwrite(IFileDialog*, IShellItem*, FDE_OVERWRITE_RESPONSE*) { return E_NOTIMPL; } IFACEMETHODIMP OnShareViolation(IFileDialog*, IShellItem*, FDE_SHAREVIOLATION_RESPONSE*) { return E_NOTIMPL; } IFACEMETHODIMP OnTypeChange(IFileDialog*) { return E_NOTIMPL; } IFACEMETHODIMP OnSelectionChange(IFileDialog* dialog) { // Get the file name logV("%p: OnSelectionChange() called",(const void*)this); ::IShellItem *shellItem(NULL); logV("%p: GetCurrentSelection",(const void*)this); HRESULT result = dialog->GetCurrentSelection(&shellItem); if ( !SUCCEEDED(result) ) { logV("%p: failure!",(const void*)this); return S_OK; } wchar_t *filePath(NULL); result = shellItem->GetDisplayName(::SIGDN_FILESYSPATH, &filePath); if ( !SUCCEEDED(result) ) { logV("%p: GDN failure!",(const void*)this); shellItem->Release(); return S_OK; } std::string utf8FilePath=utf16To8(filePath); if (selCallback!=NULL) { logV("%p: calling back.",(const void*)this); selCallback(utf8FilePath.c_str()); logV("%p: end of callback",(const void*)this); } else { logV("%p: no callback.",(const void*)this); } logV("%p: I got you for a value of %s",(const void*)this,utf8FilePath.c_str()); shellItem->Release(); logV("%p: shellItem->Release()",(const void*)this); return S_OK; } NFDWinEvents(nfdselcallback_t callback): selCallback(callback), refCount(1) { logV("%p: CONSTRUCT!",(const void*)this); } }; #define COM_INITFLAGS ::COINIT_APARTMENTTHREADED | ::COINIT_DISABLE_OLE1DDE static BOOL COMIsInitialized(HRESULT coResult) { if (coResult == RPC_E_CHANGED_MODE) { // If COM was previously initialized with different init flags, // NFD still needs to operate. Eat this warning. return TRUE; } return SUCCEEDED(coResult); } static HRESULT COMInit(void) { return ::CoInitializeEx(NULL, COM_INITFLAGS); } static void COMUninit(HRESULT coResult) { // do not uninitialize if RPC_E_CHANGED_MODE occurred -- this // case does not refcount COM. if (SUCCEEDED(coResult)) ::CoUninitialize(); } // allocs the space in outPath -- call free() static void CopyWCharToNFDChar( const wchar_t *inStr, nfdchar_t **outStr ) { int inStrCharacterCount = static_cast(wcslen(inStr)); int bytesNeeded = WideCharToMultiByte( CP_UTF8, 0, inStr, inStrCharacterCount, NULL, 0, NULL, NULL ); assert( bytesNeeded ); bytesNeeded += 1; *outStr = (nfdchar_t*)NFDi_Malloc( bytesNeeded ); if ( !*outStr ) return; int bytesWritten = WideCharToMultiByte( CP_UTF8, 0, inStr, -1, *outStr, bytesNeeded, NULL, NULL ); assert( bytesWritten ); _NFD_UNUSED( bytesWritten ); } /* includes NULL terminator byte in return */ static size_t GetUTF8ByteCountForWChar( const wchar_t *str ) { size_t bytesNeeded = WideCharToMultiByte( CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL ); assert( bytesNeeded ); return bytesNeeded+1; } // write to outPtr -- no free() necessary. static int CopyWCharToExistingNFDCharBuffer( const wchar_t *inStr, nfdchar_t *outPtr ) { int bytesNeeded = static_cast(GetUTF8ByteCountForWChar( inStr )); /* invocation copies null term */ int bytesWritten = WideCharToMultiByte( CP_UTF8, 0, inStr, -1, outPtr, bytesNeeded, NULL, 0 ); assert( bytesWritten ); return bytesWritten; } // allocs the space in outStr -- call free() static void CopyNFDCharToWChar( const nfdchar_t *inStr, wchar_t **outStr ) { int inStrByteCount = static_cast(strlen(inStr)); int charsNeeded = MultiByteToWideChar(CP_UTF8, 0, inStr, inStrByteCount, NULL, 0 ); assert( charsNeeded ); assert( !*outStr ); charsNeeded += 1; // terminator *outStr = (wchar_t*)NFDi_Malloc( charsNeeded * sizeof(wchar_t) ); if ( !*outStr ) return; int ret = MultiByteToWideChar(CP_UTF8, 0, inStr, inStrByteCount, *outStr, charsNeeded); (*outStr)[charsNeeded-1] = '\0'; #ifdef _DEBUG int inStrCharacterCount = static_cast(NFDi_UTF8_Strlen(inStr)); if (ret!=inStrCharacterCount) { logW("length does not match! %d != %d",ret,inStrCharacterCount); } #else _NFD_UNUSED(ret); #endif } static nfdresult_t AddFiltersToDialog( ::IFileDialog *fileOpenDialog, const std::vector& filterList ) { const wchar_t WILDCARD[] = L"*.*"; if (filterList.empty()) return NFD_OKAY; // list size has to be an even number (name/filter) if (filterList.size()&1) return NFD_ERROR; // Count rows to alloc UINT filterCount = filterList.size()>>1; /* guaranteed to have one filter on a correct, non-empty parse */ if (filterCount==0) filterCount=1; if ( !filterCount ) { NFDi_SetError("Error parsing filters."); return NFD_ERROR; } /* filterCount plus 1 because we hardcode the *.* wildcard after the while loop */ COMDLG_FILTERSPEC *specList = (COMDLG_FILTERSPEC*)NFDi_Malloc( sizeof(COMDLG_FILTERSPEC) * ((size_t)filterCount) ); if ( !specList ) { return NFD_ERROR; } for (UINT i = 0; i < filterCount; ++i ) { specList[i].pszName = NULL; specList[i].pszSpec = NULL; } size_t specIdx = 0; for (size_t i=0; iSetFileTypes( filterCount, specList ); /* free speclist */ for ( size_t i = 0; i < filterCount; ++i ) { NFDi_Free( (void*)specList[i].pszSpec ); } NFDi_Free( specList ); return NFD_OKAY; } static nfdresult_t AllocPathSet( IShellItemArray *shellItems, nfdpathset_t *pathSet ) { const char ERRORMSG[] = "Error allocating pathset."; assert(shellItems); assert(pathSet); // How many items in shellItems? DWORD numShellItems; HRESULT result = shellItems->GetCount(&numShellItems); if ( !SUCCEEDED(result) ) { NFDi_SetError(ERRORMSG); return NFD_ERROR; } pathSet->count = static_cast(numShellItems); assert( pathSet->count > 0 ); pathSet->indices = (size_t*)NFDi_Malloc( sizeof(size_t)*pathSet->count ); if ( !pathSet->indices ) { return NFD_ERROR; } /* count the total bytes needed for buf */ size_t bufSize = 0; for ( DWORD i = 0; i < numShellItems; ++i ) { ::IShellItem *shellItem; result = shellItems->GetItemAt(i, &shellItem); if ( !SUCCEEDED(result) ) { NFDi_SetError(ERRORMSG); return NFD_ERROR; } // Confirm SFGAO_FILESYSTEM is true for this shellitem, or ignore it. SFGAOF attribs; result = shellItem->GetAttributes( SFGAO_FILESYSTEM, &attribs ); if ( !SUCCEEDED(result) ) { NFDi_SetError(ERRORMSG); return NFD_ERROR; } if ( !(attribs & SFGAO_FILESYSTEM) ) continue; LPWSTR name; shellItem->GetDisplayName(SIGDN_FILESYSPATH, &name); // Calculate length of name with UTF-8 encoding bufSize += GetUTF8ByteCountForWChar( name ); CoTaskMemFree(name); } assert(bufSize); pathSet->buf = (nfdchar_t*)NFDi_Malloc( sizeof(nfdchar_t) * bufSize ); memset( pathSet->buf, 0, sizeof(nfdchar_t) * bufSize ); /* fill buf */ nfdchar_t *p_buf = pathSet->buf; for (DWORD i = 0; i < numShellItems; ++i ) { ::IShellItem *shellItem; result = shellItems->GetItemAt(i, &shellItem); if ( !SUCCEEDED(result) ) { NFDi_SetError(ERRORMSG); return NFD_ERROR; } // Confirm SFGAO_FILESYSTEM is true for this shellitem, or ignore it. SFGAOF attribs; result = shellItem->GetAttributes( SFGAO_FILESYSTEM, &attribs ); if ( !SUCCEEDED(result) ) { NFDi_SetError(ERRORMSG); return NFD_ERROR; } if ( !(attribs & SFGAO_FILESYSTEM) ) continue; LPWSTR name; shellItem->GetDisplayName(SIGDN_FILESYSPATH, &name); int bytesWritten = CopyWCharToExistingNFDCharBuffer(name, p_buf); CoTaskMemFree(name); ptrdiff_t index = p_buf - pathSet->buf; assert( index >= 0 ); pathSet->indices[i] = static_cast(index); p_buf += bytesWritten; } return NFD_OKAY; } static nfdresult_t SetDefaultPath( IFileDialog *dialog, const char *defaultPath ) { if ( !defaultPath || strlen(defaultPath) == 0 || strcmp(defaultPath,"\\")==0 ) return NFD_OKAY; wchar_t *defaultPathW = {0}; CopyNFDCharToWChar( defaultPath, &defaultPathW ); IShellItem *folder; HRESULT result = SHCreateItemFromParsingName( defaultPathW, NULL, IID_PPV_ARGS(&folder) ); // Valid non results. if ( result == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || result == HRESULT_FROM_WIN32(ERROR_INVALID_DRIVE) ) { NFDi_Free( defaultPathW ); return NFD_OKAY; } if ( !SUCCEEDED(result) ) { NFDi_SetError("Error creating ShellItem"); NFDi_Free( defaultPathW ); return NFD_ERROR; } // Could also call SetDefaultFolder(), but this guarantees defaultPath -- more consistency across API. dialog->SetFolder( folder ); NFDi_Free( defaultPathW ); folder->Release(); return NFD_OKAY; } /* public */ nfdresult_t NFD_OpenDialog( const std::vector& filterList, const nfdchar_t *defaultPath, nfdchar_t **outPath, nfdselcallback_t selCallback ) { nfdresult_t nfdResult = NFD_ERROR; NFDWinEvents* winEvents; bool hasEvents=true; DWORD eventID=0; HRESULT coResult = COMInit(); if (!COMIsInitialized(coResult)) { NFDi_SetError("Could not initialize COM."); return nfdResult; } // Create dialog ::IFileOpenDialog *fileOpenDialog(NULL); HRESULT result = ::CoCreateInstance(::CLSID_FileOpenDialog, NULL, CLSCTX_ALL, ::IID_IFileOpenDialog, reinterpret_cast(&fileOpenDialog) ); if ( !SUCCEEDED(result) ) { NFDi_SetError("Could not create dialog."); goto end; } // Build the filter list if ( !AddFiltersToDialog( fileOpenDialog, filterList ) ) { goto end; } // Set the default path if ( !SetDefaultPath( fileOpenDialog, defaultPath ) ) { goto end; } // Pass the callback winEvents=new NFDWinEvents(selCallback); if ( !SUCCEEDED(fileOpenDialog->Advise(winEvents,&eventID)) ) { // error... ignore hasEvents=false; winEvents->Release(); } else { winEvents->Release(); } // Show the dialog. // TODO: pass the Furnace window here result = fileOpenDialog->Show(NULL); if ( SUCCEEDED(result) ) { // Get the file name ::IShellItem *shellItem(NULL); result = fileOpenDialog->GetResult(&shellItem); if ( !SUCCEEDED(result) ) { NFDi_SetError("Could not get shell item from dialog."); goto end; } wchar_t *filePath(NULL); result = shellItem->GetDisplayName(::SIGDN_FILESYSPATH, &filePath); if ( !SUCCEEDED(result) ) { NFDi_SetError("Could not get file path for selected."); shellItem->Release(); goto end; } CopyWCharToNFDChar( filePath, outPath ); CoTaskMemFree(filePath); if ( !*outPath ) { /* error is malloc-based, error message would be redundant */ shellItem->Release(); goto end; } nfdResult = NFD_OKAY; shellItem->Release(); } else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED) ) { nfdResult = NFD_CANCEL; } else { NFDi_SetError("File dialog box show failed."); nfdResult = NFD_ERROR; } end: if (fileOpenDialog) { if (hasEvents) { fileOpenDialog->Unadvise(eventID); } fileOpenDialog->Release(); } COMUninit(coResult); return nfdResult; } nfdresult_t NFD_OpenDialogMultiple( const std::vector& filterList, const nfdchar_t *defaultPath, nfdpathset_t *outPaths, nfdselcallback_t selCallback ) { nfdresult_t nfdResult = NFD_ERROR; NFDWinEvents* winEvents; bool hasEvents=true; DWORD eventID=0; HRESULT coResult = COMInit(); if (!COMIsInitialized(coResult)) { NFDi_SetError("Could not initialize COM."); return nfdResult; } // Create dialog ::IFileOpenDialog *fileOpenDialog(NULL); HRESULT result = ::CoCreateInstance(::CLSID_FileOpenDialog, NULL, CLSCTX_ALL, ::IID_IFileOpenDialog, reinterpret_cast(&fileOpenDialog) ); if ( !SUCCEEDED(result) ) { fileOpenDialog = NULL; NFDi_SetError("Could not create dialog."); goto end; } // Build the filter list if ( !AddFiltersToDialog( fileOpenDialog, filterList ) ) { goto end; } // Set the default path if ( !SetDefaultPath( fileOpenDialog, defaultPath ) ) { goto end; } // Pass the callback winEvents=new NFDWinEvents(selCallback); if ( !SUCCEEDED(fileOpenDialog->Advise(winEvents,&eventID)) ) { // error... ignore hasEvents=false; winEvents->Release(); } else { winEvents->Release(); } // Set a flag for multiple options DWORD dwFlags; result = fileOpenDialog->GetOptions(&dwFlags); if ( !SUCCEEDED(result) ) { NFDi_SetError("Could not get options."); goto end; } result = fileOpenDialog->SetOptions(dwFlags | FOS_ALLOWMULTISELECT); if ( !SUCCEEDED(result) ) { NFDi_SetError("Could not set options."); goto end; } // Show the dialog. result = fileOpenDialog->Show(NULL); if ( SUCCEEDED(result) ) { IShellItemArray *shellItems; result = fileOpenDialog->GetResults( &shellItems ); if ( !SUCCEEDED(result) ) { NFDi_SetError("Could not get shell items."); goto end; } if ( AllocPathSet( shellItems, outPaths ) == NFD_ERROR ) { shellItems->Release(); goto end; } shellItems->Release(); nfdResult = NFD_OKAY; } else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED) ) { nfdResult = NFD_CANCEL; } else { NFDi_SetError("File dialog box show failed."); nfdResult = NFD_ERROR; } end: if (fileOpenDialog) { if (hasEvents) { fileOpenDialog->Unadvise(eventID); } fileOpenDialog->Release(); } COMUninit(coResult); return nfdResult; } nfdresult_t NFD_SaveDialog( const std::vector& filterList, const nfdchar_t *defaultPath, nfdchar_t **outPath, nfdselcallback_t selCallback ) { nfdresult_t nfdResult = NFD_ERROR; HRESULT coResult = COMInit(); if (!COMIsInitialized(coResult)) { NFDi_SetError("Could not initialize COM."); return nfdResult; } // Create dialog ::IFileSaveDialog *fileSaveDialog(NULL); HRESULT result = ::CoCreateInstance(::CLSID_FileSaveDialog, NULL, CLSCTX_ALL, ::IID_IFileSaveDialog, reinterpret_cast(&fileSaveDialog) ); if ( !SUCCEEDED(result) ) { fileSaveDialog = NULL; NFDi_SetError("Could not create dialog."); goto end; } // Build the filter list if ( !AddFiltersToDialog( fileSaveDialog, filterList ) ) { goto end; } // Set the default path if ( !SetDefaultPath( fileSaveDialog, defaultPath ) ) { goto end; } // Set a flag for no history DWORD dwFlags; result = fileSaveDialog->GetOptions(&dwFlags); if ( !SUCCEEDED(result) ) { NFDi_SetError("Could not get options."); goto end; } result = fileSaveDialog->SetOptions(dwFlags | FOS_DONTADDTORECENT); if ( !SUCCEEDED(result) ) { NFDi_SetError("Could not set options."); goto end; } // Show the dialog. result = fileSaveDialog->Show(NULL); if ( SUCCEEDED(result) ) { // Get the file name ::IShellItem *shellItem; result = fileSaveDialog->GetResult(&shellItem); if ( !SUCCEEDED(result) ) { NFDi_SetError("Could not get shell item from dialog."); goto end; } wchar_t *filePath(NULL); result = shellItem->GetDisplayName(::SIGDN_FILESYSPATH, &filePath); if ( !SUCCEEDED(result) ) { shellItem->Release(); NFDi_SetError("Could not get file path for selected."); goto end; } CopyWCharToNFDChar( filePath, outPath ); CoTaskMemFree(filePath); if ( !*outPath ) { /* error is malloc-based, error message would be redundant */ shellItem->Release(); goto end; } nfdResult = NFD_OKAY; shellItem->Release(); } else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED) ) { nfdResult = NFD_CANCEL; } else { NFDi_SetError("File dialog box show failed."); nfdResult = NFD_ERROR; } end: if ( fileSaveDialog ) fileSaveDialog->Release(); COMUninit(coResult); return nfdResult; } nfdresult_t NFD_PickFolder(const nfdchar_t *defaultPath, nfdchar_t **outPath) { nfdresult_t nfdResult = NFD_ERROR; DWORD dwOptions = 0; HRESULT coResult = COMInit(); if (!COMIsInitialized(coResult)) { NFDi_SetError("CoInitializeEx failed."); return nfdResult; } // Create dialog ::IFileOpenDialog *fileDialog(NULL); HRESULT result = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_ALL, IID_PPV_ARGS(&fileDialog)); if ( !SUCCEEDED(result) ) { NFDi_SetError("CoCreateInstance for CLSID_FileOpenDialog failed."); goto end; } // Set the default path if (SetDefaultPath(fileDialog, defaultPath) != NFD_OKAY) { NFDi_SetError("SetDefaultPath failed."); goto end; } // Get the dialogs options if (!SUCCEEDED(fileDialog->GetOptions(&dwOptions))) { NFDi_SetError("GetOptions for IFileDialog failed."); goto end; } // Add in FOS_PICKFOLDERS which hides files and only allows selection of folders if (!SUCCEEDED(fileDialog->SetOptions(dwOptions | FOS_PICKFOLDERS))) { NFDi_SetError("SetOptions for IFileDialog failed."); goto end; } // Show the dialog to the user result = fileDialog->Show(NULL); if ( SUCCEEDED(result) ) { // Get the folder name ::IShellItem *shellItem(NULL); result = fileDialog->GetResult(&shellItem); if ( !SUCCEEDED(result) ) { NFDi_SetError("Could not get file path for selected."); shellItem->Release(); goto end; } wchar_t *path = NULL; result = shellItem->GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, &path); if ( !SUCCEEDED(result) ) { NFDi_SetError("GetDisplayName for IShellItem failed."); shellItem->Release(); goto end; } CopyWCharToNFDChar(path, outPath); CoTaskMemFree(path); if ( !*outPath ) { shellItem->Release(); goto end; } nfdResult = NFD_OKAY; shellItem->Release(); } else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED) ) { nfdResult = NFD_CANCEL; } else { NFDi_SetError("Show for IFileDialog failed."); nfdResult = NFD_ERROR; } end: if (fileDialog) fileDialog->Release(); COMUninit(coResult); return nfdResult; }