nfd_win.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. /*
  2. Native File Dialog
  3. http://www.frogtoss.com/labs
  4. */
  5. /* only locally define UNICODE in this compilation unit */
  6. #ifndef UNICODE
  7. #define UNICODE
  8. #endif
  9. #include <wchar.h>
  10. #include <stdio.h>
  11. #include <assert.h>
  12. #include <windows.h>
  13. #include <ShObjIdl.h>
  14. #include "nfd_common.h"
  15. // allocs the space in outPath -- call free()
  16. static void CopyWCharToNFDChar( const wchar_t *inStr, nfdchar_t **outStr )
  17. {
  18. int inStrCharacterCount = static_cast<int>(wcslen(inStr));
  19. int bytesNeeded = WideCharToMultiByte( CP_UTF8, 0,
  20. inStr, inStrCharacterCount,
  21. NULL, 0, NULL, NULL );
  22. assert( bytesNeeded );
  23. bytesNeeded += 1;
  24. *outStr = (nfdchar_t*)NFDi_Malloc( bytesNeeded );
  25. if ( !*outStr )
  26. return;
  27. int bytesWritten = WideCharToMultiByte( CP_UTF8, 0,
  28. inStr, -1,
  29. *outStr, bytesNeeded,
  30. NULL, NULL );
  31. assert( bytesWritten ); _NFD_UNUSED( bytesWritten );
  32. }
  33. /* includes NULL terminator byte in return */
  34. static size_t GetUTF8ByteCountForWChar( const wchar_t *str )
  35. {
  36. int bytesNeeded = WideCharToMultiByte( CP_UTF8, 0,
  37. str, -1,
  38. NULL, 0, NULL, NULL );
  39. assert( bytesNeeded );
  40. return bytesNeeded+1;
  41. }
  42. // write to outPtr -- no free() necessary. No memory stomp tests are done -- they must be done
  43. // before entering this function.
  44. static int CopyWCharToExistingNFDCharBuffer( const wchar_t *inStr, nfdchar_t *outPtr )
  45. {
  46. int inStrCharacterCount = static_cast<int>(wcslen(inStr));
  47. int bytesNeeded = static_cast<int>(GetUTF8ByteCountForWChar( inStr ));
  48. /* invocation copies null term */
  49. int bytesWritten = WideCharToMultiByte( CP_UTF8, 0,
  50. inStr, -1,
  51. outPtr, bytesNeeded,
  52. NULL, 0 );
  53. assert( bytesWritten );
  54. return bytesWritten;
  55. }
  56. // allocs the space in outStr -- call free()
  57. static void CopyNFDCharToWChar( const nfdchar_t *inStr, wchar_t **outStr )
  58. {
  59. int inStrByteCount = static_cast<int>(strlen(inStr));
  60. int charsNeeded = MultiByteToWideChar(CP_UTF8, 0,
  61. inStr, inStrByteCount,
  62. NULL, 0 );
  63. assert( charsNeeded );
  64. assert( !*outStr );
  65. charsNeeded += 1; // terminator
  66. *outStr = (wchar_t*)NFDi_Malloc( charsNeeded * sizeof(wchar_t) );
  67. if ( !*outStr )
  68. return;
  69. int ret = MultiByteToWideChar(CP_UTF8, 0,
  70. inStr, inStrByteCount,
  71. *outStr, charsNeeded);
  72. (*outStr)[charsNeeded-1] = '\0';
  73. #ifdef _DEBUG
  74. int inStrCharacterCount = static_cast<int>(NFDi_UTF8_Strlen(inStr));
  75. assert( ret == inStrCharacterCount );
  76. #else
  77. _NFD_UNUSED(ret);
  78. #endif
  79. }
  80. /* ext is in format "jpg", no wildcards or separators */
  81. static int AppendExtensionToSpecBuf( const char *ext, char *specBuf, size_t specBufLen )
  82. {
  83. const char SEP[] = ";";
  84. assert( specBufLen > strlen(ext)+3 );
  85. if ( strlen(specBuf) > 0 )
  86. {
  87. strncat( specBuf, SEP, specBufLen - strlen(specBuf) - 1 );
  88. specBufLen += strlen(SEP);
  89. }
  90. char extWildcard[NFD_MAX_STRLEN];
  91. int bytesWritten = sprintf_s( extWildcard, NFD_MAX_STRLEN, "*.%s", ext );
  92. assert( bytesWritten == strlen(ext)+2 );
  93. strncat( specBuf, extWildcard, specBufLen - strlen(specBuf) - 1 );
  94. return NFD_OKAY;
  95. }
  96. static nfdresult_t AddFiltersToDialog( ::IFileDialog *fileOpenDialog, const char *filterList )
  97. {
  98. const wchar_t EMPTY_WSTR[] = L"";
  99. const wchar_t WILDCARD[] = L"*.*";
  100. if ( !filterList || strlen(filterList) == 0 )
  101. return NFD_OKAY;
  102. // Count rows to alloc
  103. UINT filterCount = 1; /* guaranteed to have one filter on a correct, non-empty parse */
  104. const char *p_filterList;
  105. for ( p_filterList = filterList; *p_filterList; ++p_filterList )
  106. {
  107. if ( *p_filterList == ';' )
  108. ++filterCount;
  109. }
  110. assert(filterCount);
  111. if ( !filterCount )
  112. {
  113. NFDi_SetError("Error parsing filters.");
  114. return NFD_ERROR;
  115. }
  116. /* filterCount plus 1 because we hardcode the *.* wildcard after the while loop */
  117. COMDLG_FILTERSPEC *specList = (COMDLG_FILTERSPEC*)NFDi_Malloc( sizeof(COMDLG_FILTERSPEC) * (filterCount + 1) );
  118. if ( !specList )
  119. {
  120. return NFD_ERROR;
  121. }
  122. for (size_t i = 0; i < filterCount+1; ++i )
  123. {
  124. specList[i].pszName = NULL;
  125. specList[i].pszSpec = NULL;
  126. }
  127. size_t specIdx = 0;
  128. p_filterList = filterList;
  129. char typebuf[NFD_MAX_STRLEN] = {0}; /* one per comma or semicolon */
  130. char *p_typebuf = typebuf;
  131. char filterName[NFD_MAX_STRLEN] = {0};
  132. char specbuf[NFD_MAX_STRLEN] = {0}; /* one per semicolon */
  133. while ( 1 )
  134. {
  135. if ( NFDi_IsFilterSegmentChar(*p_filterList) )
  136. {
  137. /* append a type to the specbuf (pending filter) */
  138. AppendExtensionToSpecBuf( typebuf, specbuf, NFD_MAX_STRLEN );
  139. p_typebuf = typebuf;
  140. memset( typebuf, 0, sizeof(char)*NFD_MAX_STRLEN );
  141. }
  142. if ( *p_filterList == ';' || *p_filterList == '\0' )
  143. {
  144. /* end of filter -- add it to specList */
  145. // Empty filter name -- Windows describes them by extension.
  146. specList[specIdx].pszName = EMPTY_WSTR;
  147. CopyNFDCharToWChar( specbuf, (wchar_t**)&specList[specIdx].pszSpec );
  148. memset( specbuf, 0, sizeof(char)*NFD_MAX_STRLEN );
  149. ++specIdx;
  150. if ( specIdx == filterCount )
  151. break;
  152. }
  153. if ( !NFDi_IsFilterSegmentChar( *p_filterList ))
  154. {
  155. *p_typebuf = *p_filterList;
  156. ++p_typebuf;
  157. }
  158. ++p_filterList;
  159. }
  160. /* Add wildcard */
  161. specList[specIdx].pszSpec = WILDCARD;
  162. specList[specIdx].pszName = EMPTY_WSTR;
  163. fileOpenDialog->SetFileTypes( filterCount+1, specList );
  164. /* free speclist */
  165. for ( size_t i = 0; i < filterCount; ++i )
  166. {
  167. NFDi_Free( (void*)specList[i].pszSpec );
  168. }
  169. NFDi_Free( specList );
  170. return NFD_OKAY;
  171. }
  172. static nfdresult_t AllocPathSet( IShellItemArray *shellItems, nfdpathset_t *pathSet )
  173. {
  174. const char ERRORMSG[] = "Error allocating pathset.";
  175. assert(shellItems);
  176. assert(pathSet);
  177. // How many items in shellItems?
  178. DWORD numShellItems;
  179. HRESULT result = shellItems->GetCount(&numShellItems);
  180. if ( !SUCCEEDED(result) )
  181. {
  182. NFDi_SetError(ERRORMSG);
  183. return NFD_ERROR;
  184. }
  185. pathSet->count = static_cast<size_t>(numShellItems);
  186. assert( pathSet->count > 0 );
  187. pathSet->indices = (size_t*)NFDi_Malloc( sizeof(size_t)*pathSet->count );
  188. if ( !pathSet->indices )
  189. {
  190. return NFD_ERROR;
  191. }
  192. /* count the total bytes needed for buf */
  193. size_t bufSize = 0;
  194. for ( DWORD i = 0; i < numShellItems; ++i )
  195. {
  196. ::IShellItem *shellItem;
  197. result = shellItems->GetItemAt(i, &shellItem);
  198. if ( !SUCCEEDED(result) )
  199. {
  200. NFDi_SetError(ERRORMSG);
  201. return NFD_ERROR;
  202. }
  203. // Confirm SFGAO_FILESYSTEM is true for this shellitem, or ignore it.
  204. SFGAOF attribs;
  205. result = shellItem->GetAttributes( SFGAO_FILESYSTEM, &attribs );
  206. if ( !SUCCEEDED(result) )
  207. {
  208. NFDi_SetError(ERRORMSG);
  209. return NFD_ERROR;
  210. }
  211. if ( !(attribs & SFGAO_FILESYSTEM) )
  212. continue;
  213. LPWSTR name;
  214. shellItem->GetDisplayName(SIGDN_FILESYSPATH, &name);
  215. // Calculate length of name with UTF-8 encoding
  216. bufSize += GetUTF8ByteCountForWChar( name );
  217. }
  218. assert(bufSize);
  219. pathSet->buf = (nfdchar_t*)NFDi_Malloc( sizeof(nfdchar_t) * bufSize );
  220. memset( pathSet->buf, 0, sizeof(nfdchar_t) * bufSize );
  221. /* fill buf */
  222. nfdchar_t *p_buf = pathSet->buf;
  223. for (DWORD i = 0; i < numShellItems; ++i )
  224. {
  225. ::IShellItem *shellItem;
  226. result = shellItems->GetItemAt(i, &shellItem);
  227. if ( !SUCCEEDED(result) )
  228. {
  229. NFDi_SetError(ERRORMSG);
  230. return NFD_ERROR;
  231. }
  232. // Confirm SFGAO_FILESYSTEM is true for this shellitem, or ignore it.
  233. SFGAOF attribs;
  234. result = shellItem->GetAttributes( SFGAO_FILESYSTEM, &attribs );
  235. if ( !SUCCEEDED(result) )
  236. {
  237. NFDi_SetError(ERRORMSG);
  238. return NFD_ERROR;
  239. }
  240. if ( !(attribs & SFGAO_FILESYSTEM) )
  241. continue;
  242. LPWSTR name;
  243. shellItem->GetDisplayName(SIGDN_FILESYSPATH, &name);
  244. int bytesWritten = CopyWCharToExistingNFDCharBuffer(name, p_buf);
  245. ptrdiff_t index = p_buf - pathSet->buf;
  246. assert( index >= 0 );
  247. pathSet->indices[i] = static_cast<size_t>(index);
  248. p_buf += bytesWritten;
  249. }
  250. return NFD_OKAY;
  251. }
  252. static nfdresult_t SetDefaultPath( IFileDialog *dialog, const char *defaultPath )
  253. {
  254. if ( !defaultPath || strlen(defaultPath) == 0 )
  255. return NFD_OKAY;
  256. wchar_t *defaultPathW = {0};
  257. CopyNFDCharToWChar( defaultPath, &defaultPathW );
  258. IShellItem *folder;
  259. HRESULT result = SHCreateItemFromParsingName( defaultPathW, NULL, IID_PPV_ARGS(&folder) );
  260. // Valid non results.
  261. if ( result == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || result == HRESULT_FROM_WIN32(ERROR_INVALID_DRIVE) )
  262. {
  263. NFDi_Free( defaultPathW );
  264. return NFD_OKAY;
  265. }
  266. if ( !SUCCEEDED(result) )
  267. {
  268. NFDi_SetError("Error creating ShellItem");
  269. NFDi_Free( defaultPathW );
  270. return NFD_ERROR;
  271. }
  272. // Could also call SetDefaultFolder(), but this guarantees defaultPath -- more consistency across API.
  273. dialog->SetFolder( folder );
  274. NFDi_Free( defaultPathW );
  275. folder->Release();
  276. return NFD_OKAY;
  277. }
  278. /* public */
  279. nfdresult_t NFD_OpenDialog( const char *filterList,
  280. const nfdchar_t *defaultPath,
  281. nfdchar_t **outPath )
  282. {
  283. nfdresult_t nfdResult = NFD_ERROR;
  284. // Init COM library.
  285. HRESULT result = ::CoInitializeEx(NULL,
  286. ::COINIT_APARTMENTTHREADED |
  287. ::COINIT_DISABLE_OLE1DDE );
  288. if ( !SUCCEEDED(result))
  289. {
  290. NFDi_SetError("Could not initialize COM.");
  291. goto end;
  292. }
  293. ::IFileOpenDialog *fileOpenDialog(NULL);
  294. // Create dialog
  295. result = ::CoCreateInstance(::CLSID_FileOpenDialog, NULL,
  296. CLSCTX_ALL, ::IID_IFileOpenDialog,
  297. reinterpret_cast<void**>(&fileOpenDialog) );
  298. if ( !SUCCEEDED(result) )
  299. {
  300. NFDi_SetError("Could not create dialog.");
  301. goto end;
  302. }
  303. // Build the filter list
  304. if ( !AddFiltersToDialog( fileOpenDialog, filterList ) )
  305. {
  306. goto end;
  307. }
  308. // Set the default path
  309. if ( !SetDefaultPath( fileOpenDialog, defaultPath ) )
  310. {
  311. goto end;
  312. }
  313. // Show the dialog.
  314. result = fileOpenDialog->Show(NULL);
  315. if ( SUCCEEDED(result) )
  316. {
  317. // Get the file name
  318. ::IShellItem *shellItem(NULL);
  319. result = fileOpenDialog->GetResult(&shellItem);
  320. if ( !SUCCEEDED(result) )
  321. {
  322. NFDi_SetError("Could not get shell item from dialog.");
  323. goto end;
  324. }
  325. wchar_t *filePath(NULL);
  326. result = shellItem->GetDisplayName(::SIGDN_FILESYSPATH, &filePath);
  327. if ( !SUCCEEDED(result) )
  328. {
  329. NFDi_SetError("Could not get file path for selected.");
  330. goto end;
  331. }
  332. CopyWCharToNFDChar( filePath, outPath );
  333. CoTaskMemFree(filePath);
  334. if ( !*outPath )
  335. {
  336. /* error is malloc-based, error message would be redundant */
  337. goto end;
  338. }
  339. nfdResult = NFD_OKAY;
  340. shellItem->Release();
  341. }
  342. else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED) )
  343. {
  344. nfdResult = NFD_CANCEL;
  345. }
  346. else
  347. {
  348. NFDi_SetError("File dialog box show failed.");
  349. nfdResult = NFD_ERROR;
  350. }
  351. end:
  352. ::CoUninitialize();
  353. return nfdResult;
  354. }
  355. nfdresult_t NFD_OpenDialogMultiple( const nfdchar_t *filterList,
  356. const nfdchar_t *defaultPath,
  357. nfdpathset_t *outPaths )
  358. {
  359. nfdresult_t nfdResult = NFD_ERROR;
  360. // Init COM library.
  361. HRESULT result = ::CoInitializeEx(NULL,
  362. ::COINIT_APARTMENTTHREADED |
  363. ::COINIT_DISABLE_OLE1DDE );
  364. if ( !SUCCEEDED(result))
  365. {
  366. NFDi_SetError("Could not initialize COM.");
  367. return NFD_ERROR;
  368. }
  369. ::IFileOpenDialog *fileOpenDialog(NULL);
  370. // Create dialog
  371. result = ::CoCreateInstance(::CLSID_FileOpenDialog, NULL,
  372. CLSCTX_ALL, ::IID_IFileOpenDialog,
  373. reinterpret_cast<void**>(&fileOpenDialog) );
  374. if ( !SUCCEEDED(result) )
  375. {
  376. NFDi_SetError("Could not create dialog.");
  377. goto end;
  378. }
  379. // Build the filter list
  380. if ( !AddFiltersToDialog( fileOpenDialog, filterList ) )
  381. {
  382. goto end;
  383. }
  384. // Set the default path
  385. if ( !SetDefaultPath( fileOpenDialog, defaultPath ) )
  386. {
  387. goto end;
  388. }
  389. // Set a flag for multiple options
  390. DWORD dwFlags;
  391. result = fileOpenDialog->GetOptions(&dwFlags);
  392. if ( !SUCCEEDED(result) )
  393. {
  394. NFDi_SetError("Could not get options.");
  395. goto end;
  396. }
  397. result = fileOpenDialog->SetOptions(dwFlags | FOS_ALLOWMULTISELECT);
  398. if ( !SUCCEEDED(result) )
  399. {
  400. NFDi_SetError("Could not set options.");
  401. goto end;
  402. }
  403. // Show the dialog.
  404. result = fileOpenDialog->Show(NULL);
  405. if ( SUCCEEDED(result) )
  406. {
  407. IShellItemArray *shellItems;
  408. result = fileOpenDialog->GetResults( &shellItems );
  409. if ( !SUCCEEDED(result) )
  410. {
  411. NFDi_SetError("Could not get shell items.");
  412. goto end;
  413. }
  414. if ( AllocPathSet( shellItems, outPaths ) == NFD_ERROR )
  415. {
  416. goto end;
  417. }
  418. shellItems->Release();
  419. nfdResult = NFD_OKAY;
  420. }
  421. else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED) )
  422. {
  423. nfdResult = NFD_CANCEL;
  424. }
  425. else
  426. {
  427. NFDi_SetError("File dialog box show failed.");
  428. nfdResult = NFD_ERROR;
  429. }
  430. end:
  431. ::CoUninitialize();
  432. return nfdResult;
  433. }
  434. nfdresult_t NFD_SaveDialog( const nfdchar_t *filterList,
  435. const nfdchar_t *defaultPath,
  436. nfdchar_t **outPath )
  437. {
  438. nfdresult_t nfdResult = NFD_ERROR;
  439. // Init COM library.
  440. HRESULT result = ::CoInitializeEx(NULL,
  441. ::COINIT_APARTMENTTHREADED |
  442. ::COINIT_DISABLE_OLE1DDE );
  443. if ( !SUCCEEDED(result))
  444. {
  445. NFDi_SetError("Could not initialize COM.");
  446. return NFD_ERROR;
  447. }
  448. ::IFileSaveDialog *fileSaveDialog(NULL);
  449. // Create dialog
  450. result = ::CoCreateInstance(::CLSID_FileSaveDialog, NULL,
  451. CLSCTX_ALL, ::IID_IFileSaveDialog,
  452. reinterpret_cast<void**>(&fileSaveDialog) );
  453. if ( !SUCCEEDED(result) )
  454. {
  455. NFDi_SetError("Could not create dialog.");
  456. goto end;
  457. }
  458. // Build the filter list
  459. if ( !AddFiltersToDialog( fileSaveDialog, filterList ) )
  460. {
  461. goto end;
  462. }
  463. // Set the default path
  464. if ( !SetDefaultPath( fileSaveDialog, defaultPath ) )
  465. {
  466. goto end;
  467. }
  468. // Show the dialog.
  469. result = fileSaveDialog->Show(NULL);
  470. if ( SUCCEEDED(result) )
  471. {
  472. // Get the file name
  473. ::IShellItem *shellItem;
  474. result = fileSaveDialog->GetResult(&shellItem);
  475. if ( !SUCCEEDED(result) )
  476. {
  477. NFDi_SetError("Could not get shell item from dialog.");
  478. goto end;
  479. }
  480. wchar_t *filePath(NULL);
  481. result = shellItem->GetDisplayName(::SIGDN_FILESYSPATH, &filePath);
  482. if ( !SUCCEEDED(result) )
  483. {
  484. NFDi_SetError("Could not get file path for selected.");
  485. goto end;
  486. }
  487. CopyWCharToNFDChar( filePath, outPath );
  488. CoTaskMemFree(filePath);
  489. if ( !*outPath )
  490. {
  491. /* error is malloc-based, error message would be redundant */
  492. goto end;
  493. }
  494. nfdResult = NFD_OKAY;
  495. shellItem->Release();
  496. }
  497. else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED) )
  498. {
  499. nfdResult = NFD_CANCEL;
  500. }
  501. else
  502. {
  503. NFDi_SetError("File dialog box show failed.");
  504. nfdResult = NFD_ERROR;
  505. }
  506. end:
  507. ::CoUninitialize();
  508. return nfdResult;
  509. }