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.

419 lines
14KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2022 - 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 7 End-User License
  8. Agreement and JUCE Privacy Policy.
  9. End User License Agreement: www.juce.com/juce-7-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. //==============================================================================
  21. static NSMutableArray* createAllowedTypesArray (const StringArray& filters)
  22. {
  23. if (filters.size() == 0)
  24. return nil;
  25. NSMutableArray* filterArray = [[[NSMutableArray alloc] init] autorelease];
  26. for (int i = 0; i < filters.size(); ++i)
  27. {
  28. // From OS X 10.6 you can only specify allowed extensions, so any filters containing wildcards
  29. // must be of the form "*.extension"
  30. jassert (filters[i] == "*"
  31. || (filters[i].startsWith ("*.") && filters[i].lastIndexOfChar ('*') == 0));
  32. const String f (filters[i].replace ("*.", ""));
  33. if (f == "*")
  34. return nil;
  35. [filterArray addObject: juceStringToNS (f)];
  36. }
  37. return filterArray;
  38. }
  39. //==============================================================================
  40. class FileChooser::Native final : public Component,
  41. public FileChooser::Pimpl
  42. {
  43. public:
  44. Native (FileChooser& fileChooser, int flags, FilePreviewComponent* previewComponent)
  45. : owner (fileChooser), preview (previewComponent),
  46. selectsDirectories ((flags & FileBrowserComponent::canSelectDirectories) != 0),
  47. selectsFiles ((flags & FileBrowserComponent::canSelectFiles) != 0),
  48. isSave ((flags & FileBrowserComponent::saveMode) != 0),
  49. selectMultiple ((flags & FileBrowserComponent::canSelectMultipleItems) != 0)
  50. {
  51. setBounds (0, 0, 0, 0);
  52. setOpaque (true);
  53. panel = [&]
  54. {
  55. if (SystemStats::isAppSandboxEnabled())
  56. return isSave ? [[NSSavePanel alloc] init]
  57. : [[NSOpenPanel alloc] init];
  58. static SafeSavePanel safeSavePanel;
  59. static SafeOpenPanel safeOpenPanel;
  60. return isSave ? [safeSavePanel.createInstance() init]
  61. : [safeOpenPanel.createInstance() init];
  62. }();
  63. static DelegateClass delegateClass;
  64. delegate = [delegateClass.createInstance() init];
  65. object_setInstanceVariable (delegate, "cppObject", this);
  66. [panel setDelegate: delegate];
  67. filters.addTokens (owner.filters.replaceCharacters (",:", ";;"), ";", String());
  68. filters.trim();
  69. filters.removeEmptyStrings();
  70. auto* nsTitle = juceStringToNS (owner.title);
  71. [panel setTitle: nsTitle];
  72. [panel setReleasedWhenClosed: YES];
  73. JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
  74. [panel setAllowedFileTypes: createAllowedTypesArray (filters)];
  75. JUCE_END_IGNORE_WARNINGS_GCC_LIKE
  76. if (! isSave)
  77. {
  78. auto* openPanel = static_cast<NSOpenPanel*> (panel);
  79. [openPanel setCanChooseDirectories: selectsDirectories];
  80. [openPanel setCanChooseFiles: selectsFiles];
  81. [openPanel setAllowsMultipleSelection: selectMultiple];
  82. [openPanel setResolvesAliases: YES];
  83. [openPanel setMessage: nsTitle]; // equivalent to the title bar since 10.11
  84. if (owner.treatFilePackagesAsDirs)
  85. [openPanel setTreatsFilePackagesAsDirectories: YES];
  86. }
  87. if (preview != nullptr)
  88. {
  89. nsViewPreview = [[NSView alloc] initWithFrame: makeNSRect (preview->getLocalBounds())];
  90. [panel setAccessoryView: nsViewPreview];
  91. preview->addToDesktop (0, (void*) nsViewPreview);
  92. preview->setVisible (true);
  93. if (@available (macOS 10.11, *))
  94. {
  95. if (! isSave)
  96. {
  97. auto* openPanel = static_cast<NSOpenPanel*> (panel);
  98. [openPanel setAccessoryViewDisclosed: YES];
  99. }
  100. }
  101. }
  102. if (isSave || selectsDirectories)
  103. [panel setCanCreateDirectories: YES];
  104. [panel setLevel: NSModalPanelWindowLevel];
  105. if (owner.startingFile.isDirectory())
  106. {
  107. startingDirectory = owner.startingFile.getFullPathName();
  108. }
  109. else
  110. {
  111. startingDirectory = owner.startingFile.getParentDirectory().getFullPathName();
  112. filename = owner.startingFile.getFileName();
  113. }
  114. [panel setDirectoryURL: createNSURLFromFile (startingDirectory)];
  115. [panel setNameFieldStringValue: juceStringToNS (filename)];
  116. }
  117. ~Native() override
  118. {
  119. exitModalState (0);
  120. if (preview != nullptr)
  121. preview->removeFromDesktop();
  122. removeFromDesktop();
  123. if (panel != nil)
  124. {
  125. [panel setDelegate: nil];
  126. if (nsViewPreview != nil)
  127. {
  128. [panel setAccessoryView: nil];
  129. [nsViewPreview release];
  130. }
  131. [panel close];
  132. }
  133. if (delegate != nil)
  134. [delegate release];
  135. }
  136. void launch() override
  137. {
  138. if (panel != nil)
  139. {
  140. setAlwaysOnTop (WindowUtils::areThereAnyAlwaysOnTopWindows());
  141. addToDesktop (0);
  142. enterModalState (true);
  143. MessageManager::callAsync ([ref = SafePointer<Native> (this)]
  144. {
  145. if (ref == nullptr)
  146. return;
  147. [ref->panel beginWithCompletionHandler: ^(NSInteger result)
  148. {
  149. if (auto* ptr = ref.getComponent())
  150. ptr->finished (result);
  151. }];
  152. if (ref->preview != nullptr)
  153. ref->preview->toFront (true);
  154. });
  155. }
  156. }
  157. void runModally() override
  158. {
  159. #if JUCE_MODAL_LOOPS_PERMITTED
  160. ensurePanelSafe();
  161. std::unique_ptr<TemporaryMainMenuWithStandardCommands> tempMenu;
  162. if (JUCEApplicationBase::isStandaloneApp())
  163. tempMenu = std::make_unique<TemporaryMainMenuWithStandardCommands> (preview);
  164. jassert (panel != nil);
  165. auto result = [panel runModal];
  166. finished (result);
  167. #else
  168. jassertfalse;
  169. #endif
  170. }
  171. bool canModalEventBeSentToComponent (const Component* targetComponent) override
  172. {
  173. return TemporaryMainMenuWithStandardCommands::checkModalEvent (preview, targetComponent);
  174. }
  175. private:
  176. //==============================================================================
  177. typedef NSObject<NSOpenSavePanelDelegate> DelegateType;
  178. static URL urlFromNSURL (NSURL* url)
  179. {
  180. const auto scheme = nsStringToJuce ([url scheme]);
  181. auto pathComponents = StringArray::fromTokens (nsStringToJuce ([url path]), "/", {});
  182. for (auto& component : pathComponents)
  183. component = URL::addEscapeChars (component, false);
  184. return { scheme + "://" + pathComponents.joinIntoString ("/") };
  185. }
  186. void finished (NSInteger result)
  187. {
  188. Array<URL> chooserResults;
  189. exitModalState (0);
  190. const auto okResult = []() -> NSInteger
  191. {
  192. if (@available (macOS 10.9, *))
  193. return NSModalResponseOK;
  194. JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
  195. return NSFileHandlingPanelOKButton;
  196. JUCE_END_IGNORE_WARNINGS_GCC_LIKE
  197. }();
  198. if (panel != nil && result == okResult)
  199. {
  200. auto addURLResult = [&chooserResults] (NSURL* urlToAdd)
  201. {
  202. chooserResults.add (urlFromNSURL (urlToAdd));
  203. };
  204. if (isSave)
  205. {
  206. addURLResult ([panel URL]);
  207. }
  208. else
  209. {
  210. auto* openPanel = static_cast<NSOpenPanel*> (panel);
  211. auto urls = [openPanel URLs];
  212. for (unsigned int i = 0; i < [urls count]; ++i)
  213. addURLResult ([urls objectAtIndex: i]);
  214. }
  215. }
  216. owner.finished (chooserResults);
  217. }
  218. BOOL shouldShowURL (const URL& urlToTest)
  219. {
  220. for (int i = filters.size(); --i >= 0;)
  221. if (urlToTest.getFileName().matchesWildcard (filters[i], true))
  222. return YES;
  223. const auto f = urlToTest.getLocalFile();
  224. return f.isDirectory()
  225. && ! [[NSWorkspace sharedWorkspace] isFilePackageAtPath: juceStringToNS (f.getFullPathName())];
  226. }
  227. void panelSelectionDidChange ([[maybe_unused]] id sender)
  228. {
  229. jassert (sender == panel);
  230. // NB: would need to extend FilePreviewComponent to handle the full list rather than just the first one
  231. if (preview != nullptr)
  232. preview->selectedFileChanged (File (getSelectedPaths()[0]));
  233. }
  234. StringArray getSelectedPaths() const
  235. {
  236. if (panel == nullptr)
  237. return {};
  238. StringArray paths;
  239. if (isSave)
  240. {
  241. paths.add (nsStringToJuce ([[panel URL] path]));
  242. }
  243. else
  244. {
  245. auto* urls = [static_cast<NSOpenPanel*> (panel) URLs];
  246. for (NSUInteger i = 0; i < [urls count]; ++i)
  247. paths.add (nsStringToJuce ([[urls objectAtIndex: i] path]));
  248. }
  249. return paths;
  250. }
  251. //==============================================================================
  252. FileChooser& owner;
  253. FilePreviewComponent* preview;
  254. NSView* nsViewPreview = nullptr;
  255. bool selectsDirectories, selectsFiles, isSave, selectMultiple;
  256. NSSavePanel* panel;
  257. DelegateType* delegate;
  258. StringArray filters;
  259. String startingDirectory, filename;
  260. void ensurePanelSafe()
  261. {
  262. // If you hit this, something (probably the plugin host) has modified the panel,
  263. // allowing the application to terminate while the panel's modal loop is running.
  264. // This is a very bad idea! Quitting from within the panel's modal loop may cause
  265. // your plugin/app destructor to run directly from within `runModally`, which will
  266. // dispose all app resources while they're still in use.
  267. // A safer alternative is to invoke the FileChooser with `launchAsync`, rather than
  268. // using the modal launchers.
  269. jassert ([panel preventsApplicationTerminationWhenModal]);
  270. }
  271. static BOOL preventsApplicationTerminationWhenModal (id, SEL) { return YES; }
  272. template <typename Base>
  273. struct SafeModalPanel : public ObjCClass<Base>
  274. {
  275. explicit SafeModalPanel (const char* name) : ObjCClass<Base> (name)
  276. {
  277. this->addMethod (@selector (preventsApplicationTerminationWhenModal),
  278. preventsApplicationTerminationWhenModal);
  279. this->registerClass();
  280. }
  281. };
  282. struct SafeSavePanel : SafeModalPanel<NSSavePanel>
  283. {
  284. SafeSavePanel() : SafeModalPanel ("SafeSavePanel_") {}
  285. };
  286. struct SafeOpenPanel : SafeModalPanel<NSOpenPanel>
  287. {
  288. SafeOpenPanel() : SafeModalPanel ("SafeOpenPanel_") {}
  289. };
  290. //==============================================================================
  291. struct DelegateClass final : public ObjCClass<DelegateType>
  292. {
  293. DelegateClass() : ObjCClass<DelegateType> ("JUCEFileChooser_")
  294. {
  295. addIvar<Native*> ("cppObject");
  296. addMethod (@selector (panel:shouldEnableURL:), shouldEnableURL);
  297. addMethod (@selector (panelSelectionDidChange:), panelSelectionDidChange);
  298. addProtocol (@protocol (NSOpenSavePanelDelegate));
  299. registerClass();
  300. }
  301. private:
  302. static BOOL shouldEnableURL (id self, SEL, id /*sender*/, NSURL* url)
  303. {
  304. return getIvar<Native*> (self, "cppObject")->shouldShowURL (urlFromNSURL (url));
  305. }
  306. static void panelSelectionDidChange (id self, SEL, id sender)
  307. {
  308. getIvar<Native*> (self, "cppObject")->panelSelectionDidChange (sender);
  309. }
  310. };
  311. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Native)
  312. };
  313. std::shared_ptr<FileChooser::Pimpl> FileChooser::showPlatformDialog (FileChooser& owner, int flags,
  314. FilePreviewComponent* preview)
  315. {
  316. return std::make_shared<FileChooser::Native> (owner, flags, preview);
  317. }
  318. bool FileChooser::isPlatformDialogAvailable()
  319. {
  320. #if JUCE_DISABLE_NATIVE_FILECHOOSERS
  321. return false;
  322. #else
  323. return true;
  324. #endif
  325. }
  326. } // namespace juce