nfd_win.cpp 17 KB

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