juce_win32_FileChooser.cpp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2017 - ROLI Ltd.
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 5 End-User License
  8. Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
  9. 27th April 2017).
  10. End User License Agreement: www.juce.com/juce-5-licence
  11. Privacy Policy: www.juce.com/juce-5-privacy-policy
  12. Or: You may also use this code under the terms of the GPL v3 (see
  13. www.gnu.org/licenses).
  14. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  15. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  16. DISCLAIMED.
  17. ==============================================================================
  18. */
  19. namespace juce
  20. {
  21. // Win32NativeFileChooser needs to be a reference counted object as there
  22. // is no way for the parent to know when the dialog HWND has actually been
  23. // created without pumping the message thread (which is forbidden when modal
  24. // loops are disabled). However, the HWND pointer is the only way to cancel
  25. // the dialog box. This means that the actual native FileChooser HWND may
  26. // not have been created yet when the user deletes JUCE's FileChooser class. If this
  27. // occurs the Win32NativeFileChooser will still have a reference count of 1 and will
  28. // simply delete itself immediately once the HWND will have been created a while later.
  29. class Win32NativeFileChooser : public ReferenceCountedObject,
  30. private Thread
  31. {
  32. public:
  33. using Ptr = ReferenceCountedObjectPtr<Win32NativeFileChooser>;
  34. enum { charsAvailableForResult = 32768 };
  35. Win32NativeFileChooser (Component* parent, int flags, FilePreviewComponent* previewComp,
  36. const File& startingFile, const String& titleToUse,
  37. const String& filtersToUse)
  38. : Thread ("Native Win32 FileChooser"),
  39. owner (parent), title (titleToUse), filtersString (filtersToUse),
  40. selectsDirectories ((flags & FileBrowserComponent::canSelectDirectories) != 0),
  41. selectsFiles ((flags & FileBrowserComponent::canSelectFiles) != 0),
  42. isSave ((flags & FileBrowserComponent::saveMode) != 0),
  43. warnAboutOverwrite ((flags & FileBrowserComponent::warnAboutOverwriting) != 0),
  44. selectMultiple ((flags & FileBrowserComponent::canSelectMultipleItems) != 0),
  45. nativeDialogRef (nullptr), shouldCancel (0)
  46. {
  47. auto parentDirectory = startingFile.getParentDirectory();
  48. // Handle nonexistent root directories in the same way as existing ones
  49. files.calloc (static_cast<size_t> (charsAvailableForResult) + 1);
  50. if (startingFile.isDirectory() ||startingFile.isRoot())
  51. {
  52. initialPath = startingFile.getFullPathName();
  53. }
  54. else
  55. {
  56. startingFile.getFileName().copyToUTF16 (files,
  57. static_cast<size_t> (charsAvailableForResult) * sizeof (WCHAR));
  58. initialPath = parentDirectory.getFullPathName();
  59. }
  60. if (! selectsDirectories)
  61. {
  62. if (previewComp != nullptr)
  63. customComponent.reset (new CustomComponentHolder (previewComp));
  64. setupFilters();
  65. }
  66. }
  67. ~Win32NativeFileChooser()
  68. {
  69. signalThreadShouldExit();
  70. waitForThreadToExit (-1);
  71. }
  72. void open (bool async)
  73. {
  74. results.clear();
  75. // the thread should not be running
  76. nativeDialogRef.set (nullptr);
  77. if (async)
  78. {
  79. jassert (! isThreadRunning());
  80. threadHasReference.reset();
  81. startThread();
  82. threadHasReference.wait (-1);
  83. }
  84. else
  85. {
  86. results = openDialog (false);
  87. owner->exitModalState (results.size() > 0 ? 1 : 0);
  88. }
  89. }
  90. void cancel()
  91. {
  92. ScopedLock lock (deletingDialog);
  93. customComponent = nullptr;
  94. shouldCancel.set (1);
  95. if (auto hwnd = nativeDialogRef.get())
  96. EndDialog (hwnd, 0);
  97. }
  98. Component* getCustomComponent() { return customComponent.get(); }
  99. Array<URL> results;
  100. private:
  101. //==============================================================================
  102. class CustomComponentHolder : public Component
  103. {
  104. public:
  105. CustomComponentHolder (Component* const customComp)
  106. {
  107. setVisible (true);
  108. setOpaque (true);
  109. addAndMakeVisible (customComp);
  110. setSize (jlimit (20, 800, customComp->getWidth()), customComp->getHeight());
  111. }
  112. void paint (Graphics& g) override
  113. {
  114. g.fillAll (Colours::lightgrey);
  115. }
  116. void resized() override
  117. {
  118. if (Component* const c = getChildComponent(0))
  119. c->setBounds (getLocalBounds());
  120. }
  121. private:
  122. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomComponentHolder)
  123. };
  124. //==============================================================================
  125. Component::SafePointer<Component> owner;
  126. String title, filtersString;
  127. std::unique_ptr<CustomComponentHolder> customComponent;
  128. String initialPath, returnedString, defaultExtension;
  129. WaitableEvent threadHasReference;
  130. CriticalSection deletingDialog;
  131. bool selectsDirectories, selectsFiles, isSave, warnAboutOverwrite, selectMultiple;
  132. HeapBlock<WCHAR> files;
  133. HeapBlock<WCHAR> filters;
  134. Atomic<HWND> nativeDialogRef;
  135. Atomic<int> shouldCancel;
  136. //==============================================================================
  137. Array<URL> openDialog (bool async)
  138. {
  139. Array<URL> selections;
  140. if (selectsDirectories)
  141. {
  142. BROWSEINFO bi = { 0 };
  143. bi.hwndOwner = (HWND) (async ? nullptr : owner->getWindowHandle());
  144. bi.pszDisplayName = files;
  145. bi.lpszTitle = title.toWideCharPointer();
  146. bi.lParam = (LPARAM) this;
  147. bi.lpfn = browseCallbackProc;
  148. #ifdef BIF_USENEWUI
  149. bi.ulFlags = BIF_USENEWUI | BIF_VALIDATE;
  150. #else
  151. bi.ulFlags = 0x50;
  152. #endif
  153. LPITEMIDLIST list = SHBrowseForFolder (&bi);
  154. if (! SHGetPathFromIDListW (list, files))
  155. {
  156. files[0] = 0;
  157. returnedString.clear();
  158. }
  159. LPMALLOC al;
  160. if (list != nullptr && SUCCEEDED (SHGetMalloc (&al)))
  161. al->Free (list);
  162. if (files[0] != 0)
  163. {
  164. File result (String (files.get()));
  165. if (returnedString.isNotEmpty())
  166. result = result.getSiblingFile (returnedString);
  167. selections.add (URL (result));
  168. }
  169. }
  170. else
  171. {
  172. OPENFILENAMEW of = { 0 };
  173. #ifdef OPENFILENAME_SIZE_VERSION_400W
  174. of.lStructSize = OPENFILENAME_SIZE_VERSION_400W;
  175. #else
  176. of.lStructSize = sizeof (of);
  177. #endif
  178. of.hwndOwner = (HWND) (async ? nullptr : owner->getWindowHandle());
  179. of.lpstrFilter = filters.getData();
  180. of.nFilterIndex = 1;
  181. of.lpstrFile = files;
  182. of.nMaxFile = (DWORD) charsAvailableForResult;
  183. of.lpstrInitialDir = initialPath.toWideCharPointer();
  184. of.lpstrTitle = title.toWideCharPointer();
  185. of.Flags = getOpenFilenameFlags (async);
  186. of.lCustData = (LPARAM) this;
  187. of.lpfnHook = &openCallback;
  188. if (isSave)
  189. {
  190. StringArray tokens;
  191. tokens.addTokens (filtersString, ";,", "\"'");
  192. tokens.trim();
  193. tokens.removeEmptyStrings();
  194. if (tokens.size() == 1 && tokens[0].removeCharacters ("*.").isNotEmpty())
  195. {
  196. defaultExtension = tokens[0].fromFirstOccurrenceOf (".", false, false);
  197. of.lpstrDefExt = defaultExtension.toWideCharPointer();
  198. }
  199. if (! GetSaveFileName (&of))
  200. return {};
  201. }
  202. else
  203. {
  204. if (! GetOpenFileName (&of))
  205. return {};
  206. }
  207. if (selectMultiple && of.nFileOffset > 0 && files [of.nFileOffset - 1] == 0)
  208. {
  209. const WCHAR* filename = files + of.nFileOffset;
  210. while (*filename != 0)
  211. {
  212. selections.add (URL (File (String (files.get())).getChildFile (String (filename))));
  213. filename += wcslen (filename) + 1;
  214. }
  215. }
  216. else if (files[0] != 0)
  217. {
  218. selections.add (URL (File (String (files.get()))));
  219. }
  220. }
  221. getNativeDialogList().removeValue (this);
  222. return selections;
  223. }
  224. void run() override
  225. {
  226. // as long as the thread is running, don't delete this class
  227. Ptr safeThis (this);
  228. threadHasReference.signal();
  229. Array<URL> r = openDialog (true);
  230. MessageManager::callAsync ([safeThis, r]
  231. {
  232. safeThis->results = r;
  233. if (safeThis->owner != nullptr)
  234. safeThis->owner->exitModalState (r.size() > 0 ? 1 : 0);
  235. });
  236. }
  237. static HashMap<HWND, Win32NativeFileChooser*>& getNativeDialogList()
  238. {
  239. static HashMap<HWND, Win32NativeFileChooser*> dialogs;
  240. return dialogs;
  241. }
  242. static Win32NativeFileChooser* getNativePointerForDialog (HWND hWnd)
  243. {
  244. return getNativeDialogList()[hWnd];
  245. }
  246. //==============================================================================
  247. void setupFilters()
  248. {
  249. const size_t filterSpaceNumChars = 2048;
  250. filters.calloc (filterSpaceNumChars);
  251. const size_t bytesWritten = filtersString.copyToUTF16 (filters.getData(), filterSpaceNumChars * sizeof (WCHAR));
  252. filtersString.copyToUTF16 (filters + (bytesWritten / sizeof (WCHAR)),
  253. ((filterSpaceNumChars - 1) * sizeof (WCHAR) - bytesWritten));
  254. for (size_t i = 0; i < filterSpaceNumChars; ++i)
  255. if (filters[i] == '|')
  256. filters[i] = 0;
  257. }
  258. DWORD getOpenFilenameFlags (bool async)
  259. {
  260. DWORD ofFlags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR | OFN_HIDEREADONLY | OFN_ENABLESIZING;
  261. if (warnAboutOverwrite)
  262. ofFlags |= OFN_OVERWRITEPROMPT;
  263. if (selectMultiple)
  264. ofFlags |= OFN_ALLOWMULTISELECT;
  265. if (async || customComponent != nullptr)
  266. ofFlags |= OFN_ENABLEHOOK;
  267. return ofFlags;
  268. }
  269. //==============================================================================
  270. void initialised (HWND hWnd)
  271. {
  272. SendMessage (hWnd, BFFM_SETSELECTIONW, TRUE, (LPARAM) initialPath.toWideCharPointer());
  273. initDialog (hWnd);
  274. }
  275. void validateFailed (const String& path)
  276. {
  277. returnedString = path;
  278. }
  279. void initDialog (HWND hdlg)
  280. {
  281. ScopedLock lock (deletingDialog);
  282. getNativeDialogList().set (hdlg, this);
  283. if (shouldCancel.get() != 0)
  284. {
  285. EndDialog (hdlg, 0);
  286. }
  287. else
  288. {
  289. nativeDialogRef.set (hdlg);
  290. if (customComponent != nullptr)
  291. {
  292. Component::SafePointer<Component> safeCustomComponent (customComponent.get());
  293. RECT dialogScreenRect, dialogClientRect;
  294. GetWindowRect (hdlg, &dialogScreenRect);
  295. GetClientRect (hdlg, &dialogClientRect);
  296. auto screenRectangle = Rectangle<int>::leftTopRightBottom (dialogScreenRect.left, dialogScreenRect.top,
  297. dialogScreenRect.right, dialogScreenRect.bottom);
  298. auto scale = Desktop::getInstance().getDisplays().findDisplayForRect (screenRectangle, true).scale;
  299. auto physicalComponentWidth = roundToInt (safeCustomComponent->getWidth() * scale);
  300. SetWindowPos (hdlg, 0, screenRectangle.getX(), screenRectangle.getY(),
  301. physicalComponentWidth + jmax (150, screenRectangle.getWidth()),
  302. jmax (150, screenRectangle.getHeight()),
  303. SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOZORDER);
  304. auto appendCustomComponent = [safeCustomComponent, dialogClientRect, scale, hdlg]() mutable
  305. {
  306. if (safeCustomComponent != nullptr)
  307. {
  308. auto scaledClientRectangle = Rectangle<int>::leftTopRightBottom (dialogClientRect.left, dialogClientRect.top,
  309. dialogClientRect.right, dialogClientRect.bottom) / scale;
  310. safeCustomComponent->setBounds (scaledClientRectangle.getRight(), scaledClientRectangle.getY(),
  311. safeCustomComponent->getWidth(), scaledClientRectangle.getHeight());
  312. safeCustomComponent->addToDesktop (0, hdlg);
  313. }
  314. };
  315. if (MessageManager::getInstance()->isThisTheMessageThread())
  316. appendCustomComponent();
  317. else
  318. MessageManager::callAsync (appendCustomComponent);
  319. }
  320. }
  321. }
  322. void destroyDialog (HWND hdlg)
  323. {
  324. ScopedLock exiting (deletingDialog);
  325. getNativeDialogList().remove (hdlg);
  326. nativeDialogRef.set (nullptr);
  327. if (MessageManager::getInstance()->isThisTheMessageThread())
  328. customComponent = nullptr;
  329. else
  330. MessageManager::callAsync ([this] { customComponent = nullptr; });
  331. }
  332. void selectionChanged (HWND hdlg)
  333. {
  334. ScopedLock lock (deletingDialog);
  335. if (customComponent != nullptr && shouldCancel.get() == 0)
  336. {
  337. if (FilePreviewComponent* comp = dynamic_cast<FilePreviewComponent*> (customComponent->getChildComponent(0)))
  338. {
  339. WCHAR path [MAX_PATH * 2] = { 0 };
  340. CommDlg_OpenSave_GetFilePath (hdlg, (LPARAM) &path, MAX_PATH);
  341. if (MessageManager::getInstance()->isThisTheMessageThread())
  342. {
  343. comp->selectedFileChanged (File (path));
  344. }
  345. else
  346. {
  347. Component::SafePointer<FilePreviewComponent> safeComp (comp);
  348. File selectedFile (path);
  349. MessageManager::callAsync ([safeComp, selectedFile]() mutable
  350. {
  351. safeComp->selectedFileChanged (selectedFile);
  352. });
  353. }
  354. }
  355. }
  356. }
  357. //==============================================================================
  358. static int CALLBACK browseCallbackProc (HWND hWnd, UINT msg, LPARAM lParam, LPARAM lpData)
  359. {
  360. auto* self = reinterpret_cast<Win32NativeFileChooser*> (lpData);
  361. switch (msg)
  362. {
  363. case BFFM_INITIALIZED: self->initialised (hWnd); break;
  364. case BFFM_VALIDATEFAILEDW: self->validateFailed (String ((LPCWSTR) lParam)); break;
  365. case BFFM_VALIDATEFAILEDA: self->validateFailed (String ((const char*) lParam)); break;
  366. default: break;
  367. }
  368. return 0;
  369. }
  370. static UINT_PTR CALLBACK openCallback (HWND hwnd, UINT uiMsg, WPARAM /*wParam*/, LPARAM lParam)
  371. {
  372. auto hdlg = getDialogFromHWND (hwnd);
  373. switch (uiMsg)
  374. {
  375. case WM_INITDIALOG:
  376. {
  377. if (auto* self = reinterpret_cast<Win32NativeFileChooser*> (((OPENFILENAMEW*) lParam)->lCustData))
  378. self->initDialog (hdlg);
  379. break;
  380. }
  381. case WM_DESTROY:
  382. {
  383. if (auto* self = getNativeDialogList()[hdlg])
  384. self->destroyDialog (hdlg);
  385. break;
  386. }
  387. case WM_NOTIFY:
  388. {
  389. auto ofn = reinterpret_cast<LPOFNOTIFY> (lParam);
  390. if (ofn->hdr.code == CDN_SELCHANGE)
  391. if (auto* self = reinterpret_cast<Win32NativeFileChooser*> (ofn->lpOFN->lCustData))
  392. self->selectionChanged (hdlg);
  393. break;
  394. }
  395. default:
  396. break;
  397. }
  398. return 0;
  399. }
  400. static HWND getDialogFromHWND (HWND hwnd)
  401. {
  402. if (hwnd == nullptr)
  403. return nullptr;
  404. HWND dialogH = GetParent (hwnd);
  405. if (dialogH == 0)
  406. dialogH = hwnd;
  407. return dialogH;
  408. }
  409. //==============================================================================
  410. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Win32NativeFileChooser)
  411. };
  412. class FileChooser::Native : public Component,
  413. public FileChooser::Pimpl
  414. {
  415. public:
  416. Native (FileChooser& fileChooser, int flags, FilePreviewComponent* previewComp)
  417. : owner (fileChooser),
  418. nativeFileChooser (new Win32NativeFileChooser (this, flags, previewComp, fileChooser.startingFile,
  419. fileChooser.title, fileChooser.filters))
  420. {
  421. auto mainMon = Desktop::getInstance().getDisplays().getMainDisplay().userArea;
  422. setBounds (mainMon.getX() + mainMon.getWidth() / 4,
  423. mainMon.getY() + mainMon.getHeight() / 4,
  424. 0, 0);
  425. setOpaque (true);
  426. setAlwaysOnTop (juce_areThereAnyAlwaysOnTopWindows());
  427. addToDesktop (0);
  428. }
  429. ~Native()
  430. {
  431. exitModalState (0);
  432. nativeFileChooser->cancel();
  433. nativeFileChooser = nullptr;
  434. }
  435. void launch() override
  436. {
  437. SafePointer<Native> safeThis (this);
  438. enterModalState (true, ModalCallbackFunction::create (
  439. [safeThis] (int)
  440. {
  441. if (safeThis != nullptr)
  442. safeThis->owner.finished (safeThis->nativeFileChooser->results);
  443. }));
  444. nativeFileChooser->open (true);
  445. }
  446. void runModally() override
  447. {
  448. enterModalState (true);
  449. nativeFileChooser->open (false);
  450. exitModalState (nativeFileChooser->results.size() > 0 ? 1 : 0);
  451. nativeFileChooser->cancel();
  452. owner.finished (nativeFileChooser->results);
  453. }
  454. bool canModalEventBeSentToComponent (const Component* targetComponent) override
  455. {
  456. if (targetComponent == nullptr)
  457. return false;
  458. if (targetComponent == nativeFileChooser->getCustomComponent())
  459. return true;
  460. return targetComponent->findParentComponentOfClass<FilePreviewComponent>() != nullptr;
  461. }
  462. private:
  463. FileChooser& owner;
  464. Win32NativeFileChooser::Ptr nativeFileChooser;
  465. };
  466. //==============================================================================
  467. bool FileChooser::isPlatformDialogAvailable()
  468. {
  469. #if JUCE_DISABLE_NATIVE_FILECHOOSERS
  470. return false;
  471. #else
  472. return true;
  473. #endif
  474. }
  475. FileChooser::Pimpl* FileChooser::showPlatformDialog (FileChooser& owner, int flags,
  476. FilePreviewComponent* preview)
  477. {
  478. return new FileChooser::Native (owner, flags, preview);
  479. }
  480. } // namespace juce