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.

398 lines
13KB

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