1 /*
2   Native File Dialog
3 
4   http://www.frogtoss.com/labs
5  */
6 
7 /* only locally define UNICODE in this compilation unit */
8 #ifndef UNICODE
9 #define UNICODE
10 #endif
11 
12 #ifdef __MINGW32__
13 // Explicitly setting NTDDI version, this is necessary for the MinGW compiler
14 #define NTDDI_VERSION NTDDI_VISTA
15 #define _WIN32_WINNT _WIN32_WINNT_VISTA
16 #endif
17 
18 #include <wchar.h>
19 #include <stdio.h>
20 #include <assert.h>
21 #include <windows.h>
22 #include <ShObjIdl.h>
23 #include "nfd_common.h"
24 
25 
26 // allocs the space in outPath -- call free()
CopyWCharToNFDChar(const wchar_t * inStr,nfdchar_t ** outStr)27 static void CopyWCharToNFDChar( const wchar_t *inStr, nfdchar_t **outStr )
28 {
29     int inStrCharacterCount = static_cast<int>(wcslen(inStr));
30     int bytesNeeded = WideCharToMultiByte( CP_UTF8, 0,
31                                            inStr, inStrCharacterCount,
32                                            NULL, 0, NULL, NULL );
33     assert( bytesNeeded );
34     bytesNeeded += 1;
35 
36     *outStr = (nfdchar_t*)NFDi_Malloc( bytesNeeded );
37     if ( !*outStr )
38         return;
39 
40     int bytesWritten = WideCharToMultiByte( CP_UTF8, 0,
41                                             inStr, -1,
42                                             *outStr, bytesNeeded,
43                                             NULL, NULL );
44     assert( bytesWritten ); _NFD_UNUSED( bytesWritten );
45 }
46 
47 /* includes NULL terminator byte in return */
GetUTF8ByteCountForWChar(const wchar_t * str)48 static size_t GetUTF8ByteCountForWChar( const wchar_t *str )
49 {
50     int bytesNeeded = WideCharToMultiByte( CP_UTF8, 0,
51                                            str, -1,
52                                            NULL, 0, NULL, NULL );
53     assert( bytesNeeded );
54     return bytesNeeded+1;
55 }
56 
57 // write to outPtr -- no free() necessary.  No memory stomp tests are done -- they must be done
58 // before entering this function.
CopyWCharToExistingNFDCharBuffer(const wchar_t * inStr,nfdchar_t * outPtr)59 static int CopyWCharToExistingNFDCharBuffer( const wchar_t *inStr, nfdchar_t *outPtr )
60 {
61     int inStrCharacterCount = static_cast<int>(wcslen(inStr));
62     (void)inStrCharacterCount;
63     int bytesNeeded = static_cast<int>(GetUTF8ByteCountForWChar( inStr ));
64 
65     /* invocation copies null term */
66     int bytesWritten = WideCharToMultiByte( CP_UTF8, 0,
67                                             inStr, -1,
68                                             outPtr, bytesNeeded,
69                                             NULL, 0 );
70     assert( bytesWritten );
71 
72     return bytesWritten;
73 
74 }
75 
76 
77 // allocs the space in outStr -- call free()
CopyNFDCharToWChar(const nfdchar_t * inStr,wchar_t ** outStr)78 static void CopyNFDCharToWChar( const nfdchar_t *inStr, wchar_t **outStr )
79 {
80     int inStrByteCount = static_cast<int>(strlen(inStr));
81     int charsNeeded = MultiByteToWideChar(CP_UTF8, 0,
82                                           inStr, inStrByteCount,
83                                           NULL, 0 );
84     assert( charsNeeded );
85     assert( !*outStr );
86     charsNeeded += 1; // terminator
87 
88     *outStr = (wchar_t*)NFDi_Malloc( charsNeeded * sizeof(wchar_t) );
89     if ( !*outStr )
90         return;
91 
92     int ret = MultiByteToWideChar(CP_UTF8, 0,
93                                   inStr, inStrByteCount,
94                                   *outStr, charsNeeded);
95     (*outStr)[charsNeeded-1] = '\0';
96 
97 #ifdef _DEBUG
98     int inStrCharacterCount = static_cast<int>(NFDi_UTF8_Strlen(inStr));
99     assert( ret == inStrCharacterCount );
100 #else
101     _NFD_UNUSED(ret);
102 #endif
103 }
104 
105 
106 /* ext is in format "jpg", no wildcards or separators */
AppendExtensionToSpecBuf(const char * ext,char * specBuf,size_t specBufLen)107 static int AppendExtensionToSpecBuf( const char *ext, char *specBuf, size_t specBufLen )
108 {
109     const char SEP[] = ";";
110     assert( specBufLen > strlen(ext)+3 );
111 
112     if ( strlen(specBuf) > 0 )
113     {
114         strncat( specBuf, SEP, specBufLen - strlen(specBuf) - 1 );
115         specBufLen += strlen(SEP);
116     }
117 
118     char extWildcard[NFD_MAX_STRLEN];
119     int bytesWritten = sprintf_s( extWildcard, NFD_MAX_STRLEN, "*.%s", ext );
120     assert( size_t(bytesWritten) == strlen(ext)+2 );
121 
122     strncat( specBuf, extWildcard, specBufLen - strlen(specBuf) - 1 );
123 
124     return NFD_OKAY;
125 }
126 
AddFiltersToDialog(::IFileDialog * fileOpenDialog,const char * filterList)127 static nfdresult_t AddFiltersToDialog( ::IFileDialog *fileOpenDialog, const char *filterList )
128 {
129     const wchar_t EMPTY_WSTR[] = L"";
130     const wchar_t WILDCARD[] = L"*.*";
131 
132     if ( !filterList || strlen(filterList) == 0 )
133         return NFD_OKAY;
134 
135     // Count rows to alloc
136     UINT filterCount = 1; /* guaranteed to have one filter on a correct, non-empty parse */
137     const char *p_filterList;
138     for ( p_filterList = filterList; *p_filterList; ++p_filterList )
139     {
140         if ( *p_filterList == ';' )
141             ++filterCount;
142     }
143 
144     assert(filterCount);
145     if ( !filterCount )
146     {
147         NFDi_SetError("Error parsing filters.");
148         return NFD_ERROR;
149     }
150 
151     /* filterCount plus 1 because we hardcode the *.* wildcard after the while loop */
152     COMDLG_FILTERSPEC *specList = (COMDLG_FILTERSPEC*)NFDi_Malloc( sizeof(COMDLG_FILTERSPEC) * (filterCount + 1) );
153     if ( !specList )
154     {
155         return NFD_ERROR;
156     }
157     for (size_t i = 0; i < filterCount+1; ++i )
158     {
159         specList[i].pszName = NULL;
160         specList[i].pszSpec = NULL;
161     }
162 
163     size_t specIdx = 0;
164     p_filterList = filterList;
165     char typebuf[NFD_MAX_STRLEN] = {0};  /* one per comma or semicolon */
166     char *p_typebuf = typebuf;
167     char filterName[NFD_MAX_STRLEN] = {0};
168 
169     char specbuf[NFD_MAX_STRLEN] = {0}; /* one per semicolon */
170 
171     while ( 1 )
172     {
173         if ( NFDi_IsFilterSegmentChar(*p_filterList) )
174         {
175             /* append a type to the specbuf (pending filter) */
176             AppendExtensionToSpecBuf( typebuf, specbuf, NFD_MAX_STRLEN );
177 
178             p_typebuf = typebuf;
179             memset( typebuf, 0, sizeof(char)*NFD_MAX_STRLEN );
180         }
181 
182         if ( *p_filterList == ';' || *p_filterList == '\0' )
183         {
184             /* end of filter -- add it to specList */
185 
186             // Empty filter name -- Windows describes them by extension.
187             specList[specIdx].pszName = EMPTY_WSTR;
188             CopyNFDCharToWChar( specbuf, (wchar_t**)&specList[specIdx].pszSpec );
189 
190             memset( specbuf, 0, sizeof(char)*NFD_MAX_STRLEN );
191             ++specIdx;
192             if ( specIdx == filterCount )
193                 break;
194         }
195 
196         if ( !NFDi_IsFilterSegmentChar( *p_filterList ))
197         {
198             *p_typebuf = *p_filterList;
199             ++p_typebuf;
200         }
201 
202         ++p_filterList;
203     }
204 
205     /* Add wildcard */
206     specList[specIdx].pszSpec = WILDCARD;
207     specList[specIdx].pszName = EMPTY_WSTR;
208 
209     fileOpenDialog->SetFileTypes( filterCount+1, specList );
210 
211     /* free speclist */
212     for ( size_t i = 0; i < filterCount; ++i )
213     {
214         NFDi_Free( (void*)specList[i].pszSpec );
215     }
216     NFDi_Free( specList );
217 
218     return NFD_OKAY;
219 }
220 
AllocPathSet(IShellItemArray * shellItems,nfdpathset_t * pathSet)221 static nfdresult_t AllocPathSet( IShellItemArray *shellItems, nfdpathset_t *pathSet )
222 {
223     const char ERRORMSG[] = "Error allocating pathset.";
224 
225     assert(shellItems);
226     assert(pathSet);
227 
228     // How many items in shellItems?
229     DWORD numShellItems;
230     HRESULT result = shellItems->GetCount(&numShellItems);
231     if ( !SUCCEEDED(result) )
232     {
233         NFDi_SetError(ERRORMSG);
234         return NFD_ERROR;
235     }
236 
237     pathSet->count = static_cast<size_t>(numShellItems);
238     assert( pathSet->count > 0 );
239 
240     pathSet->indices = (size_t*)NFDi_Malloc( sizeof(size_t)*pathSet->count );
241     if ( !pathSet->indices )
242     {
243         return NFD_ERROR;
244     }
245 
246     /* count the total bytes needed for buf */
247     size_t bufSize = 0;
248     for ( DWORD i = 0; i < numShellItems; ++i )
249     {
250         ::IShellItem *shellItem;
251         result = shellItems->GetItemAt(i, &shellItem);
252         if ( !SUCCEEDED(result) )
253         {
254             NFDi_SetError(ERRORMSG);
255             return NFD_ERROR;
256         }
257 
258         // Confirm SFGAO_FILESYSTEM is true for this shellitem, or ignore it.
259         SFGAOF attribs;
260         result = shellItem->GetAttributes( SFGAO_FILESYSTEM, &attribs );
261         if ( !SUCCEEDED(result) )
262         {
263             NFDi_SetError(ERRORMSG);
264             return NFD_ERROR;
265         }
266         if ( !(attribs & SFGAO_FILESYSTEM) )
267             continue;
268 
269         LPWSTR name;
270         shellItem->GetDisplayName(SIGDN_FILESYSPATH, &name);
271 
272         // Calculate length of name with UTF-8 encoding
273         bufSize += GetUTF8ByteCountForWChar( name );
274     }
275 
276     assert(bufSize);
277 
278     pathSet->buf = (nfdchar_t*)NFDi_Malloc( sizeof(nfdchar_t) * bufSize );
279     memset( pathSet->buf, 0, sizeof(nfdchar_t) * bufSize );
280 
281     /* fill buf */
282     nfdchar_t *p_buf = pathSet->buf;
283     for (DWORD i = 0; i < numShellItems; ++i )
284     {
285         ::IShellItem *shellItem;
286         result = shellItems->GetItemAt(i, &shellItem);
287         if ( !SUCCEEDED(result) )
288         {
289             NFDi_SetError(ERRORMSG);
290             return NFD_ERROR;
291         }
292 
293         // Confirm SFGAO_FILESYSTEM is true for this shellitem, or ignore it.
294         SFGAOF attribs;
295         result = shellItem->GetAttributes( SFGAO_FILESYSTEM, &attribs );
296         if ( !SUCCEEDED(result) )
297         {
298             NFDi_SetError(ERRORMSG);
299             return NFD_ERROR;
300         }
301         if ( !(attribs & SFGAO_FILESYSTEM) )
302             continue;
303 
304         LPWSTR name;
305         shellItem->GetDisplayName(SIGDN_FILESYSPATH, &name);
306 
307         int bytesWritten = CopyWCharToExistingNFDCharBuffer(name, p_buf);
308 
309         ptrdiff_t index = p_buf - pathSet->buf;
310         assert( index >= 0 );
311         pathSet->indices[i] = static_cast<size_t>(index);
312 
313         p_buf += bytesWritten;
314     }
315 
316     return NFD_OKAY;
317 }
318 
319 
SetDefaultPath(IFileDialog * dialog,const char * defaultPath)320 static nfdresult_t SetDefaultPath( IFileDialog *dialog, const char *defaultPath )
321 {
322     if ( !defaultPath || strlen(defaultPath) == 0 )
323         return NFD_OKAY;
324 
325     wchar_t *defaultPathW = {0};
326     CopyNFDCharToWChar( defaultPath, &defaultPathW );
327 
328     IShellItem *folder;
329     HRESULT result = SHCreateItemFromParsingName( defaultPathW, NULL, IID_PPV_ARGS(&folder) );
330 
331     // Valid non results.
332     if ( result == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || result == HRESULT_FROM_WIN32(ERROR_INVALID_DRIVE) )
333     {
334         NFDi_Free( defaultPathW );
335         return NFD_OKAY;
336     }
337 
338     if ( !SUCCEEDED(result) )
339     {
340         NFDi_SetError("Error creating ShellItem");
341         NFDi_Free( defaultPathW );
342         return NFD_ERROR;
343     }
344 
345     // Could also call SetDefaultFolder(), but this guarantees defaultPath -- more consistency across API.
346     dialog->SetFolder( folder );
347 
348     NFDi_Free( defaultPathW );
349     folder->Release();
350 
351     return NFD_OKAY;
352 }
353 
354 /* public */
355 
356 
NFD_OpenDialog(const char * filterList,const nfdchar_t * defaultPath,nfdchar_t ** outPath)357 nfdresult_t NFD_OpenDialog( const char *filterList,
358                             const nfdchar_t *defaultPath,
359                             nfdchar_t **outPath )
360 {
361     nfdresult_t nfdResult = NFD_ERROR;
362 
363     // Init COM library.
364     HRESULT result = ::CoInitializeEx(NULL,
365                                       ::COINIT_APARTMENTTHREADED |
366                                       ::COINIT_DISABLE_OLE1DDE );
367 
368     ::IFileOpenDialog *fileOpenDialog(NULL);
369 
370     if ( !SUCCEEDED(result))
371     {
372         NFDi_SetError("Could not initialize COM.");
373         goto end;
374     }
375 
376     // Create dialog
377     result = ::CoCreateInstance(::CLSID_FileOpenDialog, NULL,
378                                 CLSCTX_ALL, ::IID_IFileOpenDialog,
379                                 reinterpret_cast<void**>(&fileOpenDialog) );
380 
381     if ( !SUCCEEDED(result) )
382     {
383         NFDi_SetError("Could not create dialog.");
384         goto end;
385     }
386 
387     // Build the filter list
388     if ( !AddFiltersToDialog( fileOpenDialog, filterList ) )
389     {
390         goto end;
391     }
392 
393     // Set the default path
394     if ( !SetDefaultPath( fileOpenDialog, defaultPath ) )
395     {
396         goto end;
397     }
398 
399     // Show the dialog.
400     result = fileOpenDialog->Show(NULL);
401     if ( SUCCEEDED(result) )
402     {
403         // Get the file name
404         ::IShellItem *shellItem(NULL);
405         result = fileOpenDialog->GetResult(&shellItem);
406         if ( !SUCCEEDED(result) )
407         {
408             NFDi_SetError("Could not get shell item from dialog.");
409             goto end;
410         }
411         wchar_t *filePath(NULL);
412         result = shellItem->GetDisplayName(::SIGDN_FILESYSPATH, &filePath);
413         if ( !SUCCEEDED(result) )
414         {
415             NFDi_SetError("Could not get file path for selected.");
416             goto end;
417         }
418 
419         CopyWCharToNFDChar( filePath, outPath );
420         CoTaskMemFree(filePath);
421         if ( !*outPath )
422         {
423             /* error is malloc-based, error message would be redundant */
424             goto end;
425         }
426 
427         nfdResult = NFD_OKAY;
428         shellItem->Release();
429     }
430     else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED) )
431     {
432         nfdResult = NFD_CANCEL;
433     }
434     else
435     {
436         NFDi_SetError("File dialog box show failed.");
437         nfdResult = NFD_ERROR;
438     }
439 
440  end:
441     ::CoUninitialize();
442 
443     return nfdResult;
444 }
445 
NFD_OpenDialogMultiple(const nfdchar_t * filterList,const nfdchar_t * defaultPath,nfdpathset_t * outPaths)446 nfdresult_t NFD_OpenDialogMultiple( const nfdchar_t *filterList,
447                                     const nfdchar_t *defaultPath,
448                                     nfdpathset_t *outPaths )
449 {
450     nfdresult_t nfdResult = NFD_ERROR;
451 
452     // Init COM library.
453     HRESULT result = ::CoInitializeEx(NULL,
454                                       ::COINIT_APARTMENTTHREADED |
455                                       ::COINIT_DISABLE_OLE1DDE );
456     if ( !SUCCEEDED(result))
457     {
458         NFDi_SetError("Could not initialize COM.");
459         return NFD_ERROR;
460     }
461 
462     ::IFileOpenDialog *fileOpenDialog(NULL);
463 
464     // Create dialog
465     result = ::CoCreateInstance(::CLSID_FileOpenDialog, NULL,
466                                 CLSCTX_ALL, ::IID_IFileOpenDialog,
467                                 reinterpret_cast<void**>(&fileOpenDialog) );
468 
469     if ( !SUCCEEDED(result) )
470     {
471         NFDi_SetError("Could not create dialog.");
472         goto end;
473     }
474 
475     // Build the filter list
476     if ( !AddFiltersToDialog( fileOpenDialog, filterList ) )
477     {
478         goto end;
479     }
480 
481     // Set the default path
482     if ( !SetDefaultPath( fileOpenDialog, defaultPath ) )
483     {
484         goto end;
485     }
486 
487     // Set a flag for multiple options
488     DWORD dwFlags;
489     result = fileOpenDialog->GetOptions(&dwFlags);
490     if ( !SUCCEEDED(result) )
491     {
492         NFDi_SetError("Could not get options.");
493         goto end;
494     }
495     result = fileOpenDialog->SetOptions(dwFlags | FOS_ALLOWMULTISELECT);
496     if ( !SUCCEEDED(result) )
497     {
498         NFDi_SetError("Could not set options.");
499         goto end;
500     }
501 
502     // Show the dialog.
503     result = fileOpenDialog->Show(NULL);
504     if ( SUCCEEDED(result) )
505     {
506         IShellItemArray *shellItems;
507         result = fileOpenDialog->GetResults( &shellItems );
508         if ( !SUCCEEDED(result) )
509         {
510             NFDi_SetError("Could not get shell items.");
511             goto end;
512         }
513 
514         if ( AllocPathSet( shellItems, outPaths ) == NFD_ERROR )
515         {
516             goto end;
517         }
518 
519         shellItems->Release();
520         nfdResult = NFD_OKAY;
521     }
522     else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED) )
523     {
524         nfdResult = NFD_CANCEL;
525     }
526     else
527     {
528         NFDi_SetError("File dialog box show failed.");
529         nfdResult = NFD_ERROR;
530     }
531 
532  end:
533     ::CoUninitialize();
534 
535     return nfdResult;
536 }
537 
NFD_SaveDialog(const nfdchar_t * filterList,const nfdchar_t * defaultPath,nfdchar_t ** outPath)538 nfdresult_t NFD_SaveDialog( const nfdchar_t *filterList,
539                             const nfdchar_t *defaultPath,
540                             nfdchar_t **outPath )
541 {
542     nfdresult_t nfdResult = NFD_ERROR;
543 
544     // Init COM library.
545     HRESULT result = ::CoInitializeEx(NULL,
546                                       ::COINIT_APARTMENTTHREADED |
547                                       ::COINIT_DISABLE_OLE1DDE );
548     if ( !SUCCEEDED(result))
549     {
550         NFDi_SetError("Could not initialize COM.");
551         return NFD_ERROR;
552     }
553 
554     ::IFileSaveDialog *fileSaveDialog(NULL);
555 
556     // Create dialog
557     result = ::CoCreateInstance(::CLSID_FileSaveDialog, NULL,
558                                 CLSCTX_ALL, ::IID_IFileSaveDialog,
559                                 reinterpret_cast<void**>(&fileSaveDialog) );
560 
561     if ( !SUCCEEDED(result) )
562     {
563         NFDi_SetError("Could not create dialog.");
564         goto end;
565     }
566 
567     // Build the filter list
568     if ( !AddFiltersToDialog( fileSaveDialog, filterList ) )
569     {
570         goto end;
571     }
572 
573     // Set the default path
574     if ( !SetDefaultPath( fileSaveDialog, defaultPath ) )
575     {
576         goto end;
577     }
578 
579     // Show the dialog.
580     result = fileSaveDialog->Show(NULL);
581     if ( SUCCEEDED(result) )
582     {
583         // Get the file name
584         ::IShellItem *shellItem;
585         result = fileSaveDialog->GetResult(&shellItem);
586         if ( !SUCCEEDED(result) )
587         {
588             NFDi_SetError("Could not get shell item from dialog.");
589             goto end;
590         }
591         wchar_t *filePath(NULL);
592         result = shellItem->GetDisplayName(::SIGDN_FILESYSPATH, &filePath);
593         if ( !SUCCEEDED(result) )
594         {
595             NFDi_SetError("Could not get file path for selected.");
596             goto end;
597         }
598 
599         CopyWCharToNFDChar( filePath, outPath );
600         CoTaskMemFree(filePath);
601         if ( !*outPath )
602         {
603             /* error is malloc-based, error message would be redundant */
604             goto end;
605         }
606 
607         nfdResult = NFD_OKAY;
608         shellItem->Release();
609     }
610     else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED) )
611     {
612         nfdResult = NFD_CANCEL;
613     }
614     else
615     {
616         NFDi_SetError("File dialog box show failed.");
617         nfdResult = NFD_ERROR;
618     }
619 
620  end:
621     ::CoUninitialize();
622 
623     return nfdResult;
624 }
625 
626 class AutoCoInit
627 {
628 public:
AutoCoInit()629     AutoCoInit()
630     {
631         mResult = ::CoInitializeEx(NULL,
632             ::COINIT_APARTMENTTHREADED |
633             ::COINIT_DISABLE_OLE1DDE);
634     }
635 
~AutoCoInit()636     ~AutoCoInit()
637     {
638         if (SUCCEEDED(mResult))
639         {
640             ::CoUninitialize();
641         }
642     }
643 
Result() const644     HRESULT Result() const { return mResult; }
645 private:
646     HRESULT mResult;
647 };
648 
649 // VS2010 hasn't got a copy of CComPtr - this was first added in the 2003 SDK, so we make our own small CComPtr instead
650 template<class T>
651 class ComPtr
652 {
653 public:
ComPtr()654     ComPtr() : mPtr(NULL) { }
~ComPtr()655     ~ComPtr()
656     {
657         if (mPtr)
658         {
659             mPtr->Release();
660         }
661     }
662 
Ptr() const663     T* Ptr() const { return mPtr; }
operator &()664     T** operator&() { return &mPtr; }
operator ->() const665     T* operator->() const { return mPtr; }
666 private:
667     // Don't allow copy or assignment
668     ComPtr(const ComPtr&);
669     ComPtr& operator = (const ComPtr&) const;
670     T* mPtr;
671 };
672 
NFD_PickFolder(const nfdchar_t * defaultPath,nfdchar_t ** outPath)673 nfdresult_t NFD_PickFolder(const nfdchar_t *defaultPath,
674     nfdchar_t **outPath)
675 {
676     // Init COM
677     AutoCoInit autoCoInit;
678     if (!SUCCEEDED(autoCoInit.Result()))
679     {
680         NFDi_SetError("CoInitializeEx failed.");
681         return NFD_ERROR;
682     }
683 
684     // Create the file dialog COM object
685     ComPtr<IFileDialog> pFileDialog;
686     if (!SUCCEEDED(CoCreateInstance(CLSID_FileOpenDialog,
687                                     NULL,
688                                     CLSCTX_ALL,
689                                     IID_PPV_ARGS(&pFileDialog))))
690     {
691         NFDi_SetError("CoCreateInstance for CLSID_FileOpenDialog failed.");
692         return NFD_ERROR;
693     }
694 
695     // Set the default path
696     if (SetDefaultPath(pFileDialog.Ptr(), defaultPath) != NFD_OKAY)
697     {
698         NFDi_SetError("SetDefaultPath failed.");
699         return NFD_ERROR;
700     }
701 
702     // Get the dialogs options
703     DWORD dwOptions = 0;
704     if (!SUCCEEDED(pFileDialog->GetOptions(&dwOptions)))
705     {
706         NFDi_SetError("GetOptions for IFileDialog failed.");
707         return NFD_ERROR;
708     }
709 
710     // Add in FOS_PICKFOLDERS which hides files and only allows selection of folders
711     if (!SUCCEEDED(pFileDialog->SetOptions(dwOptions | FOS_PICKFOLDERS)))
712     {
713         NFDi_SetError("SetOptions for IFileDialog failed.");
714         return NFD_ERROR;
715     }
716 
717     // Show the dialog to the user
718     const HRESULT result = pFileDialog->Show(NULL);
719     if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED))
720     {
721         return NFD_CANCEL;
722     }
723     else if (!SUCCEEDED(result))
724     {
725         NFDi_SetError("Show for IFileDialog failed.");
726         return NFD_ERROR;
727     }
728 
729     // Get the shell item result
730     ComPtr<IShellItem> pShellItem;
731     if (!SUCCEEDED(pFileDialog->GetResult(&pShellItem)))
732     {
733         return NFD_OKAY;
734     }
735 
736     // Finally get the path
737     wchar_t *path = NULL;
738     if (!SUCCEEDED(pShellItem->GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, &path)))
739     {
740         NFDi_SetError("GetDisplayName for IShellItem failed.");
741         return NFD_ERROR;
742     }
743 
744     // Convert string
745     CopyWCharToNFDChar(path, outPath);
746     CoTaskMemFree(path);
747     if (!*outPath)
748     {
749         // error is malloc-based, error message would be redundant
750         return NFD_ERROR;
751     }
752 
753     return NFD_OKAY;
754 }
755