Audio plugin host https://kx.studio/carla
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

juce_win32_FileChooser.cpp 20KB

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