The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
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.

576 lines
19KB

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