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_mac_FileChooser.mm 13KB

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