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.

413 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. namespace juce
  19. {
  20. #if ! (defined (__IPHONE_16_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_16_0)
  21. JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
  22. #define JUCE_DEPRECATION_IGNORED 1
  23. #endif
  24. class FileChooser::Native : public FileChooser::Pimpl,
  25. public Component,
  26. private AsyncUpdater
  27. {
  28. public:
  29. Native (FileChooser& fileChooser, int flags)
  30. : owner (fileChooser)
  31. {
  32. static FileChooserDelegateClass delegateClass;
  33. delegate.reset ([delegateClass.createInstance() init]);
  34. FileChooserDelegateClass::setOwner (delegate.get(), this);
  35. static FileChooserControllerClass controllerClass;
  36. auto* controllerClassInstance = controllerClass.createInstance();
  37. String firstFileExtension;
  38. auto utTypeArray = createNSArrayFromStringArray (getUTTypesForWildcards (owner.filters, firstFileExtension));
  39. if ((flags & FileBrowserComponent::saveMode) != 0)
  40. {
  41. auto currentFileOrDirectory = owner.startingFile;
  42. UIDocumentPickerMode pickerMode = currentFileOrDirectory.existsAsFile()
  43. ? UIDocumentPickerModeExportToService
  44. : UIDocumentPickerModeMoveToService;
  45. if (! currentFileOrDirectory.existsAsFile())
  46. {
  47. auto filename = getFilename (currentFileOrDirectory, firstFileExtension);
  48. auto tmpDirectory = File::createTempFile ("JUCE-filepath");
  49. if (tmpDirectory.createDirectory().wasOk())
  50. {
  51. currentFileOrDirectory = tmpDirectory.getChildFile (filename);
  52. currentFileOrDirectory.replaceWithText ("");
  53. }
  54. else
  55. {
  56. // Temporary directory creation failed! You need to specify a
  57. // path you have write access to. Saving will not work for
  58. // current path.
  59. jassertfalse;
  60. }
  61. }
  62. auto url = [[NSURL alloc] initFileURLWithPath: juceStringToNS (currentFileOrDirectory.getFullPathName())];
  63. controller.reset ([controllerClassInstance initWithURL: url
  64. inMode: pickerMode]);
  65. [url release];
  66. }
  67. else
  68. {
  69. controller.reset ([controllerClassInstance initWithDocumentTypes: utTypeArray
  70. inMode: UIDocumentPickerModeOpen]);
  71. if (@available (iOS 11.0, *))
  72. [controller.get() setAllowsMultipleSelection: (flags & FileBrowserComponent::canSelectMultipleItems) != 0];
  73. }
  74. FileChooserControllerClass::setOwner (controller.get(), this);
  75. [controller.get() setDelegate: delegate.get()];
  76. [controller.get() setModalTransitionStyle: UIModalTransitionStyleCrossDissolve];
  77. setOpaque (false);
  78. if (fileChooser.parent != nullptr)
  79. {
  80. [controller.get() setModalPresentationStyle: UIModalPresentationFullScreen];
  81. auto chooserBounds = fileChooser.parent->getBounds();
  82. setBounds (chooserBounds);
  83. setAlwaysOnTop (true);
  84. fileChooser.parent->addAndMakeVisible (this);
  85. }
  86. else
  87. {
  88. if (SystemStats::isRunningInAppExtensionSandbox())
  89. {
  90. // Opening a native top-level window in an AUv3 is not allowed (sandboxing). You need to specify a
  91. // parent component (for example your editor) to parent the native file chooser window. To do this
  92. // specify a parent component in the FileChooser's constructor!
  93. jassertfalse;
  94. return;
  95. }
  96. auto chooserBounds = Desktop::getInstance().getDisplays().getPrimaryDisplay()->userArea;
  97. setBounds (chooserBounds);
  98. setAlwaysOnTop (true);
  99. setVisible (true);
  100. addToDesktop (0);
  101. }
  102. }
  103. ~Native() override
  104. {
  105. exitModalState (0);
  106. // Our old peer may not have received a becomeFirstResponder call at this point,
  107. // so the static currentlyFocusedPeer may be null.
  108. // We'll try to find an appropriate peer to focus.
  109. for (auto i = 0; i < ComponentPeer::getNumPeers(); ++i)
  110. if (auto* p = ComponentPeer::getPeer (i))
  111. if (p != getPeer())
  112. if (auto* view = (UIView*) p->getNativeHandle())
  113. [view becomeFirstResponder];
  114. }
  115. void launch() override
  116. {
  117. enterModalState (true, nullptr, true);
  118. }
  119. void runModally() override
  120. {
  121. #if JUCE_MODAL_LOOPS_PERMITTED
  122. runModalLoop();
  123. #else
  124. jassertfalse;
  125. #endif
  126. }
  127. void parentHierarchyChanged() override
  128. {
  129. auto* newPeer = dynamic_cast<UIViewComponentPeer*> (getPeer());
  130. if (peer != newPeer)
  131. {
  132. peer = newPeer;
  133. if (peer != nullptr)
  134. {
  135. if (auto* parentController = peer->controller)
  136. [parentController showViewController: controller.get() sender: parentController];
  137. peer->toFront (false);
  138. }
  139. }
  140. }
  141. private:
  142. //==============================================================================
  143. void handleAsyncUpdate() override
  144. {
  145. pickerWasCancelled();
  146. }
  147. //==============================================================================
  148. static StringArray getUTTypesForWildcards (const String& filterWildcards, String& firstExtension)
  149. {
  150. auto filters = StringArray::fromTokens (filterWildcards, ";", "");
  151. StringArray result;
  152. firstExtension = {};
  153. if (! filters.contains ("*") && filters.size() > 0)
  154. {
  155. for (auto filter : filters)
  156. {
  157. if (filter.isEmpty())
  158. continue;
  159. // iOS only supports file extension wild cards
  160. jassert (filter.upToLastOccurrenceOf (".", true, false) == "*.");
  161. auto fileExtension = filter.fromLastOccurrenceOf (".", false, false);
  162. CFUniquePtr<CFStringRef> fileExtensionCF (fileExtension.toCFString());
  163. if (firstExtension.isEmpty())
  164. firstExtension = fileExtension;
  165. if (auto tag = CFUniquePtr<CFStringRef> (UTTypeCreatePreferredIdentifierForTag (kUTTagClassFilenameExtension, fileExtensionCF.get(), nullptr)))
  166. result.add (String::fromCFString (tag.get()));
  167. }
  168. }
  169. else
  170. {
  171. result.add ("public.data");
  172. }
  173. return result;
  174. }
  175. static String getFilename (const File& path, const String& fallbackExtension)
  176. {
  177. auto filename = path.getFileNameWithoutExtension();
  178. auto extension = path.getFileExtension().substring (1);
  179. if (filename.isEmpty())
  180. filename = "Untitled";
  181. if (extension.isEmpty())
  182. extension = fallbackExtension;
  183. if (extension.isNotEmpty())
  184. filename += "." + extension;
  185. return filename;
  186. }
  187. //==============================================================================
  188. void didPickDocumentsAtURLs (NSArray<NSURL*>* urls)
  189. {
  190. cancelPendingUpdate();
  191. const auto isWriting = controller.get().documentPickerMode == UIDocumentPickerModeExportToService
  192. || controller.get().documentPickerMode == UIDocumentPickerModeMoveToService;
  193. const auto accessOptions = isWriting ? 0 : NSFileCoordinatorReadingWithoutChanges;
  194. auto* fileCoordinator = [[[NSFileCoordinator alloc] initWithFilePresenter: nil] autorelease];
  195. auto* intents = [[[NSMutableArray alloc] init] autorelease];
  196. for (NSURL* url in urls)
  197. {
  198. auto* fileAccessIntent = isWriting
  199. ? [NSFileAccessIntent writingIntentWithURL: url options: accessOptions]
  200. : [NSFileAccessIntent readingIntentWithURL: url options: accessOptions];
  201. [intents addObject: fileAccessIntent];
  202. }
  203. [fileCoordinator coordinateAccessWithIntents: intents queue: [NSOperationQueue mainQueue] byAccessor: ^(NSError* err)
  204. {
  205. if (err != nil)
  206. {
  207. auto desc = [err localizedDescription];
  208. ignoreUnused (desc);
  209. jassertfalse;
  210. return;
  211. }
  212. Array<URL> result;
  213. for (NSURL* url in urls)
  214. {
  215. [url startAccessingSecurityScopedResource];
  216. NSError* error = nil;
  217. auto* bookmark = [url bookmarkDataWithOptions: 0
  218. includingResourceValuesForKeys: nil
  219. relativeToURL: nil
  220. error: &error];
  221. [bookmark retain];
  222. [url stopAccessingSecurityScopedResource];
  223. URL juceUrl (nsStringToJuce ([url absoluteString]));
  224. if (error == nil)
  225. {
  226. setURLBookmark (juceUrl, (void*) bookmark);
  227. }
  228. else
  229. {
  230. auto desc = [error localizedDescription];
  231. ignoreUnused (desc);
  232. jassertfalse;
  233. }
  234. result.add (std::move (juceUrl));
  235. }
  236. owner.finished (std::move (result));
  237. }];
  238. }
  239. void didPickDocumentAtURL (NSURL* url)
  240. {
  241. didPickDocumentsAtURLs (@[url]);
  242. }
  243. void pickerWasCancelled()
  244. {
  245. cancelPendingUpdate();
  246. owner.finished ({});
  247. // Calling owner.finished will delete this Pimpl instance, so don't call any more member functions here!
  248. }
  249. //==============================================================================
  250. struct FileChooserDelegateClass : public ObjCClass<NSObject<UIDocumentPickerDelegate>>
  251. {
  252. FileChooserDelegateClass() : ObjCClass<NSObject<UIDocumentPickerDelegate>> ("FileChooserDelegate_")
  253. {
  254. addIvar<Native*> ("owner");
  255. addMethod (@selector (documentPicker:didPickDocumentAtURL:), didPickDocumentAtURL);
  256. addMethod (@selector (documentPicker:didPickDocumentsAtURLs:), didPickDocumentsAtURLs);
  257. addMethod (@selector (documentPickerWasCancelled:), documentPickerWasCancelled);
  258. addProtocol (@protocol (UIDocumentPickerDelegate));
  259. registerClass();
  260. }
  261. static void setOwner (id self, Native* owner) { object_setInstanceVariable (self, "owner", owner); }
  262. static Native* getOwner (id self) { return getIvar<Native*> (self, "owner"); }
  263. //==============================================================================
  264. static void didPickDocumentAtURL (id self, SEL, UIDocumentPickerViewController*, NSURL* url)
  265. {
  266. if (auto* picker = getOwner (self))
  267. picker->didPickDocumentAtURL (url);
  268. }
  269. static void didPickDocumentsAtURLs (id self, SEL, UIDocumentPickerViewController*, NSArray<NSURL*>* urls)
  270. {
  271. if (auto* picker = getOwner (self))
  272. picker->didPickDocumentsAtURLs (urls);
  273. }
  274. static void documentPickerWasCancelled (id self, SEL, UIDocumentPickerViewController*)
  275. {
  276. if (auto* picker = getOwner (self))
  277. picker->pickerWasCancelled();
  278. }
  279. };
  280. struct FileChooserControllerClass : public ObjCClass<UIDocumentPickerViewController>
  281. {
  282. FileChooserControllerClass() : ObjCClass<UIDocumentPickerViewController> ("FileChooserController_")
  283. {
  284. addIvar<Native*> ("owner");
  285. addMethod (@selector (viewDidDisappear:), viewDidDisappear);
  286. registerClass();
  287. }
  288. static void setOwner (id self, Native* owner) { object_setInstanceVariable (self, "owner", owner); }
  289. static Native* getOwner (id self) { return getIvar<Native*> (self, "owner"); }
  290. //==============================================================================
  291. static void viewDidDisappear (id self, SEL, BOOL animated)
  292. {
  293. sendSuperclassMessage<void> (self, @selector (viewDidDisappear:), animated);
  294. if (auto* picker = getOwner (self))
  295. picker->triggerAsyncUpdate();
  296. }
  297. };
  298. //==============================================================================
  299. FileChooser& owner;
  300. NSUniquePtr<NSObject<UIDocumentPickerDelegate>> delegate;
  301. NSUniquePtr<UIDocumentPickerViewController> controller;
  302. UIViewComponentPeer* peer = nullptr;
  303. static FileChooserDelegateClass fileChooserDelegateClass;
  304. static FileChooserControllerClass fileChooserControllerClass;
  305. //==============================================================================
  306. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Native)
  307. };
  308. //==============================================================================
  309. bool FileChooser::isPlatformDialogAvailable()
  310. {
  311. #if JUCE_DISABLE_NATIVE_FILECHOOSERS
  312. return false;
  313. #else
  314. return true;
  315. #endif
  316. }
  317. std::shared_ptr<FileChooser::Pimpl> FileChooser::showPlatformDialog (FileChooser& owner, int flags,
  318. FilePreviewComponent*)
  319. {
  320. return std::make_shared<FileChooser::Native> (owner, flags);
  321. }
  322. #if JUCE_DEPRECATION_IGNORED
  323. JUCE_END_IGNORE_WARNINGS_GCC_LIKE
  324. #endif
  325. } // namespace juce