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.

438 lines
15KB

  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. @interface FileChooserControllerClass : UIDocumentPickerViewController
  19. - (void) setParent: (FileChooser::Native*) ptr;
  20. @end
  21. @interface FileChooserDelegateClass : NSObject<UIDocumentPickerDelegate>
  22. - (id) initWithOwner: (FileChooser::Native*) owner;
  23. @end
  24. namespace juce
  25. {
  26. #if ! (defined (__IPHONE_16_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_16_0)
  27. JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
  28. #define JUCE_DEPRECATION_IGNORED 1
  29. #endif
  30. class FileChooser::Native : public FileChooser::Pimpl,
  31. public Component,
  32. public AsyncUpdater,
  33. public std::enable_shared_from_this<Native>
  34. {
  35. public:
  36. static std::shared_ptr<Native> make (FileChooser& fileChooser, int flags)
  37. {
  38. std::shared_ptr<Native> result { new Native (fileChooser, flags) };
  39. /* Must be called after forming a shared_ptr to an instance of this class.
  40. Note that we can't call this directly inside the class constructor, because
  41. the owning shared_ptr might not yet exist.
  42. */
  43. [result->controller.get() setParent: result.get()];
  44. return result;
  45. }
  46. ~Native() override
  47. {
  48. exitModalState (0);
  49. }
  50. void launch() override
  51. {
  52. jassert (shared_from_this() != nullptr);
  53. /* Normally, when deleteWhenDismissed is true, the modal component manger will keep a copy of a raw pointer
  54. to our component and delete it when the modal state has ended. However, this is incompatible with
  55. our class being tracked by shared_ptr as it will force delete our class regardless of the current
  56. reference count. On the other hand, it's important that the modal manager keeps a reference as it can
  57. sometimes be the only reference to our class.
  58. To do this, we set deleteWhenDismissed to false so that the modal component manager does not delete
  59. our class. Instead, we pass in a lambda which captures a shared_ptr to ourselves to increase the
  60. reference count while the component is modal.
  61. */
  62. enterModalState (true,
  63. ModalCallbackFunction::create ([_self = shared_from_this()] (int) {}),
  64. false);
  65. }
  66. void runModally() override
  67. {
  68. #if JUCE_MODAL_LOOPS_PERMITTED
  69. launch();
  70. runModalLoop();
  71. #else
  72. jassertfalse;
  73. #endif
  74. }
  75. void parentHierarchyChanged() override
  76. {
  77. auto* newPeer = dynamic_cast<UIViewComponentPeer*> (getPeer());
  78. if (peer != newPeer)
  79. {
  80. peer = newPeer;
  81. if (peer != nullptr)
  82. {
  83. if (auto* parentController = peer->controller)
  84. [parentController showViewController: controller.get() sender: parentController];
  85. peer->toFront (false);
  86. }
  87. }
  88. }
  89. void handleAsyncUpdate() override
  90. {
  91. pickerWasCancelled();
  92. }
  93. //==============================================================================
  94. void didPickDocumentsAtURLs (NSArray<NSURL*>* urls)
  95. {
  96. cancelPendingUpdate();
  97. const auto isWriting = controller.get().documentPickerMode == UIDocumentPickerModeExportToService
  98. || controller.get().documentPickerMode == UIDocumentPickerModeMoveToService;
  99. const auto accessOptions = isWriting ? 0 : NSFileCoordinatorReadingWithoutChanges;
  100. auto* fileCoordinator = [[[NSFileCoordinator alloc] initWithFilePresenter: nil] autorelease];
  101. auto* intents = [[[NSMutableArray alloc] init] autorelease];
  102. for (NSURL* url in urls)
  103. {
  104. auto* fileAccessIntent = isWriting
  105. ? [NSFileAccessIntent writingIntentWithURL: url options: accessOptions]
  106. : [NSFileAccessIntent readingIntentWithURL: url options: accessOptions];
  107. [intents addObject: fileAccessIntent];
  108. }
  109. [fileCoordinator coordinateAccessWithIntents: intents queue: [NSOperationQueue mainQueue] byAccessor: ^(NSError* err)
  110. {
  111. if (err != nil)
  112. {
  113. auto desc = [err localizedDescription];
  114. ignoreUnused (desc);
  115. jassertfalse;
  116. return;
  117. }
  118. Array<URL> result;
  119. for (NSURL* url in urls)
  120. {
  121. [url startAccessingSecurityScopedResource];
  122. NSError* error = nil;
  123. auto* bookmark = [url bookmarkDataWithOptions: 0
  124. includingResourceValuesForKeys: nil
  125. relativeToURL: nil
  126. error: &error];
  127. [bookmark retain];
  128. [url stopAccessingSecurityScopedResource];
  129. URL juceUrl (nsStringToJuce ([url absoluteString]));
  130. if (error == nil)
  131. {
  132. setURLBookmark (juceUrl, (void*) bookmark);
  133. }
  134. else
  135. {
  136. auto desc = [error localizedDescription];
  137. ignoreUnused (desc);
  138. jassertfalse;
  139. }
  140. result.add (std::move (juceUrl));
  141. }
  142. passResultsToInitiator (std::move (result));
  143. }];
  144. }
  145. void didPickDocumentAtURL (NSURL* url)
  146. {
  147. didPickDocumentsAtURLs (@[url]);
  148. }
  149. void pickerWasCancelled()
  150. {
  151. passResultsToInitiator ({});
  152. }
  153. private:
  154. Native (FileChooser& fileChooser, int flags)
  155. : owner (fileChooser)
  156. {
  157. delegate.reset ([[FileChooserDelegateClass alloc] initWithOwner: this]);
  158. String firstFileExtension;
  159. auto utTypeArray = createNSArrayFromStringArray (getUTTypesForWildcards (owner.filters, firstFileExtension));
  160. if ((flags & FileBrowserComponent::saveMode) != 0)
  161. {
  162. auto currentFileOrDirectory = owner.startingFile;
  163. UIDocumentPickerMode pickerMode = currentFileOrDirectory.existsAsFile()
  164. ? UIDocumentPickerModeExportToService
  165. : UIDocumentPickerModeMoveToService;
  166. if (! currentFileOrDirectory.existsAsFile())
  167. {
  168. auto filename = getFilename (currentFileOrDirectory, firstFileExtension);
  169. auto tmpDirectory = File::createTempFile ("JUCE-filepath");
  170. if (tmpDirectory.createDirectory().wasOk())
  171. {
  172. currentFileOrDirectory = tmpDirectory.getChildFile (filename);
  173. currentFileOrDirectory.replaceWithText ("");
  174. }
  175. else
  176. {
  177. // Temporary directory creation failed! You need to specify a
  178. // path you have write access to. Saving will not work for
  179. // current path.
  180. jassertfalse;
  181. }
  182. }
  183. auto url = [[NSURL alloc] initFileURLWithPath: juceStringToNS (currentFileOrDirectory.getFullPathName())];
  184. controller.reset ([[FileChooserControllerClass alloc] initWithURL: url inMode: pickerMode]);
  185. [url release];
  186. }
  187. else
  188. {
  189. controller.reset ([[FileChooserControllerClass alloc] initWithDocumentTypes: utTypeArray inMode: UIDocumentPickerModeOpen]);
  190. if (@available (iOS 11.0, *))
  191. [controller.get() setAllowsMultipleSelection: (flags & FileBrowserComponent::canSelectMultipleItems) != 0];
  192. }
  193. [controller.get() setDelegate: delegate.get()];
  194. [controller.get() setModalTransitionStyle: UIModalTransitionStyleCrossDissolve];
  195. setOpaque (false);
  196. if (fileChooser.parent != nullptr)
  197. {
  198. [controller.get() setModalPresentationStyle: UIModalPresentationFullScreen];
  199. auto chooserBounds = fileChooser.parent->getBounds();
  200. setBounds (chooserBounds);
  201. setAlwaysOnTop (true);
  202. fileChooser.parent->addAndMakeVisible (this);
  203. }
  204. else
  205. {
  206. if (SystemStats::isRunningInAppExtensionSandbox())
  207. {
  208. // Opening a native top-level window in an AUv3 is not allowed (sandboxing). You need to specify a
  209. // parent component (for example your editor) to parent the native file chooser window. To do this
  210. // specify a parent component in the FileChooser's constructor!
  211. jassertfalse;
  212. return;
  213. }
  214. auto chooserBounds = Desktop::getInstance().getDisplays().getPrimaryDisplay()->userArea;
  215. setBounds (chooserBounds);
  216. setAlwaysOnTop (true);
  217. setVisible (true);
  218. addToDesktop (0);
  219. }
  220. }
  221. void passResultsToInitiator (Array<URL> urls)
  222. {
  223. cancelPendingUpdate();
  224. exitModalState (0);
  225. // If the caller attempts to show a platform-native dialog box inside the results callback (e.g. in the DialogsDemo)
  226. // then the original peer must already have focus. Otherwise, there's a danger that either the invisible FileChooser
  227. // components will display the popup, locking the application, or maybe no component will have focus, and the
  228. // dialog won't show at all.
  229. for (auto i = 0; i < ComponentPeer::getNumPeers(); ++i)
  230. if (auto* p = ComponentPeer::getPeer (i))
  231. if (p != getPeer())
  232. if (auto* view = (UIView*) p->getNativeHandle())
  233. if ([view becomeFirstResponder] && [view isFirstResponder])
  234. break;
  235. // Calling owner.finished will delete this Pimpl instance, so don't call any more member functions here!
  236. owner.finished (std::move (urls));
  237. }
  238. //==============================================================================
  239. static StringArray getUTTypesForWildcards (const String& filterWildcards, String& firstExtension)
  240. {
  241. auto filters = StringArray::fromTokens (filterWildcards, ";", "");
  242. StringArray result;
  243. firstExtension = {};
  244. if (! filters.contains ("*") && filters.size() > 0)
  245. {
  246. for (auto filter : filters)
  247. {
  248. if (filter.isEmpty())
  249. continue;
  250. // iOS only supports file extension wild cards
  251. jassert (filter.upToLastOccurrenceOf (".", true, false) == "*.");
  252. auto fileExtension = filter.fromLastOccurrenceOf (".", false, false);
  253. CFUniquePtr<CFStringRef> fileExtensionCF (fileExtension.toCFString());
  254. if (firstExtension.isEmpty())
  255. firstExtension = fileExtension;
  256. if (auto tag = CFUniquePtr<CFStringRef> (UTTypeCreatePreferredIdentifierForTag (kUTTagClassFilenameExtension, fileExtensionCF.get(), nullptr)))
  257. result.add (String::fromCFString (tag.get()));
  258. }
  259. }
  260. else
  261. {
  262. result.add ("public.data");
  263. }
  264. return result;
  265. }
  266. static String getFilename (const File& path, const String& fallbackExtension)
  267. {
  268. auto filename = path.getFileNameWithoutExtension();
  269. auto extension = path.getFileExtension().substring (1);
  270. if (filename.isEmpty())
  271. filename = "Untitled";
  272. if (extension.isEmpty())
  273. extension = fallbackExtension;
  274. if (extension.isNotEmpty())
  275. filename += "." + extension;
  276. return filename;
  277. }
  278. //==============================================================================
  279. FileChooser& owner;
  280. NSUniquePtr<NSObject<UIDocumentPickerDelegate>> delegate;
  281. NSUniquePtr<FileChooserControllerClass> controller;
  282. UIViewComponentPeer* peer = nullptr;
  283. //==============================================================================
  284. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Native)
  285. };
  286. //==============================================================================
  287. bool FileChooser::isPlatformDialogAvailable()
  288. {
  289. #if JUCE_DISABLE_NATIVE_FILECHOOSERS
  290. return false;
  291. #else
  292. return true;
  293. #endif
  294. }
  295. std::shared_ptr<FileChooser::Pimpl> FileChooser::showPlatformDialog (FileChooser& owner, int flags,
  296. FilePreviewComponent*)
  297. {
  298. return Native::make (owner, flags);
  299. }
  300. #if JUCE_DEPRECATION_IGNORED
  301. JUCE_END_IGNORE_WARNINGS_GCC_LIKE
  302. #endif
  303. } // namespace juce
  304. @implementation FileChooserControllerClass
  305. {
  306. std::weak_ptr<FileChooser::Native> ptr;
  307. }
  308. - (void) setParent: (FileChooser::Native*) parent
  309. {
  310. jassert (parent != nullptr);
  311. jassert (parent->shared_from_this() != nullptr);
  312. ptr = parent->weak_from_this();
  313. }
  314. - (void) viewDidDisappear: (BOOL) animated
  315. {
  316. [super viewDidDisappear: animated];
  317. if (auto nativeParent = ptr.lock())
  318. nativeParent->triggerAsyncUpdate();
  319. }
  320. @end
  321. @implementation FileChooserDelegateClass
  322. {
  323. FileChooser::Native* owner;
  324. }
  325. - (id) initWithOwner: (FileChooser::Native*) o
  326. {
  327. self = [super init];
  328. owner = o;
  329. return self;
  330. }
  331. JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-implementations")
  332. - (void) documentPicker: (UIDocumentPickerViewController*) controller didPickDocumentAtURL: (NSURL*) url
  333. {
  334. if (owner != nullptr)
  335. owner->didPickDocumentAtURL (url);
  336. }
  337. JUCE_END_IGNORE_WARNINGS_GCC_LIKE
  338. - (void) documentPicker: (UIDocumentPickerViewController*) controller didPickDocumentsAtURLs: (NSArray<NSURL*>*) urls
  339. {
  340. if (owner != nullptr)
  341. owner->didPickDocumentsAtURLs (urls);
  342. }
  343. - (void) documentPickerWasCancelled: (UIDocumentPickerViewController*) controller
  344. {
  345. if (owner != nullptr)
  346. owner->pickerWasCancelled();
  347. }
  348. @end