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.

599 lines
20KB

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