juce_ios_FileChooser.mm 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2017 - ROLI Ltd.
  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 5 End-User License
  8. Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
  9. 27th April 2017).
  10. End User License Agreement: www.juce.com/juce-5-licence
  11. Privacy Policy: www.juce.com/juce-5-privacy-policy
  12. Or: You may also use this code under the terms of the GPL v3 (see
  13. www.gnu.org/licenses).
  14. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  15. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  16. DISCLAIMED.
  17. ==============================================================================
  18. */
  19. namespace juce
  20. {
  21. class FileChooser::Native : private Component,
  22. public FileChooser::Pimpl
  23. {
  24. public:
  25. Native (FileChooser& fileChooser, int flags)
  26. : owner (fileChooser)
  27. {
  28. String firstFileExtension;
  29. static FileChooserDelegateClass cls;
  30. delegate.reset ([cls.createInstance() init]);
  31. FileChooserDelegateClass::setOwner (delegate.get(), this);
  32. auto utTypeArray = createNSArrayFromStringArray (getUTTypesForWildcards (owner.filters, firstFileExtension));
  33. if ((flags & FileBrowserComponent::saveMode) != 0)
  34. {
  35. auto currentFileOrDirectory = owner.startingFile;
  36. UIDocumentPickerMode pickerMode = currentFileOrDirectory.existsAsFile()
  37. ? UIDocumentPickerModeExportToService
  38. : UIDocumentPickerModeMoveToService;
  39. if (! currentFileOrDirectory.existsAsFile())
  40. {
  41. auto filename = getFilename (currentFileOrDirectory, firstFileExtension);
  42. auto tmpDirectory = File::createTempFile ("JUCE-filepath");
  43. if (tmpDirectory.createDirectory().wasOk())
  44. {
  45. currentFileOrDirectory = tmpDirectory.getChildFile (filename);
  46. currentFileOrDirectory.replaceWithText ("");
  47. }
  48. else
  49. {
  50. // Temporary directory creation failed! You need to specify a
  51. // path you have write access to. Saving will not work for
  52. // current path.
  53. jassertfalse;
  54. }
  55. }
  56. auto url = [[NSURL alloc] initFileURLWithPath: juceStringToNS (currentFileOrDirectory.getFullPathName())];
  57. controller.reset ([[UIDocumentPickerViewController alloc] initWithURL: url
  58. inMode: pickerMode]);
  59. [url release];
  60. }
  61. else
  62. {
  63. controller.reset ([[UIDocumentPickerViewController alloc] initWithDocumentTypes: utTypeArray
  64. inMode: UIDocumentPickerModeOpen]);
  65. }
  66. [controller.get() setDelegate: delegate.get()];
  67. [controller.get() setModalTransitionStyle: UIModalTransitionStyleCrossDissolve];
  68. setOpaque (false);
  69. if (SystemStats::isRunningInAppExtensionSandbox())
  70. {
  71. if (fileChooser.parent != nullptr)
  72. {
  73. [controller.get() setModalPresentationStyle:UIModalPresentationFullScreen];
  74. auto chooserBounds = fileChooser.parent->getBounds();
  75. setBounds (chooserBounds);
  76. setAlwaysOnTop (true);
  77. fileChooser.parent->addAndMakeVisible (this);
  78. }
  79. else
  80. {
  81. // Opening a native top-level window in an AUv3 is not allowed (sandboxing). You need to specify a
  82. // parent component (for example your editor) to parent the native file chooser window. To do this
  83. // specify a parent component in the FileChooser's constructor!
  84. jassert (fileChooser.parent != nullptr);
  85. }
  86. }
  87. else
  88. {
  89. auto chooserBounds = Desktop::getInstance().getDisplays().getMainDisplay().userArea;
  90. setBounds (chooserBounds);
  91. setAlwaysOnTop (true);
  92. addToDesktop (0);
  93. }
  94. }
  95. ~Native() override
  96. {
  97. exitModalState (0);
  98. }
  99. void launch() override
  100. {
  101. enterModalState (true, nullptr, true);
  102. }
  103. void runModally() override
  104. {
  105. #if JUCE_MODAL_LOOPS_PERMITTED
  106. runModalLoop();
  107. #endif
  108. }
  109. private:
  110. //==============================================================================
  111. void parentHierarchyChanged() override
  112. {
  113. auto* newPeer = dynamic_cast<UIViewComponentPeer*> (getPeer());
  114. if (peer != newPeer)
  115. {
  116. peer = newPeer;
  117. if (auto* parentController = peer->controller)
  118. [parentController showViewController: controller.get() sender: parentController];
  119. }
  120. }
  121. //==============================================================================
  122. static StringArray getUTTypesForWildcards (const String& filterWildcards, String& firstExtension)
  123. {
  124. auto filters = StringArray::fromTokens (filterWildcards, ";", "");
  125. StringArray result;
  126. firstExtension = {};
  127. if (! filters.contains ("*") && filters.size() > 0)
  128. {
  129. for (auto filter : filters)
  130. {
  131. if (filter.isEmpty())
  132. continue;
  133. // iOS only supports file extension wild cards
  134. jassert (filter.upToLastOccurrenceOf (".", true, false) == "*.");
  135. auto fileExtension = filter.fromLastOccurrenceOf (".", false, false);
  136. auto fileExtensionCF = fileExtension.toCFString();
  137. if (firstExtension.isEmpty())
  138. firstExtension = fileExtension;
  139. auto tag = UTTypeCreatePreferredIdentifierForTag (kUTTagClassFilenameExtension, fileExtensionCF, nullptr);
  140. if (tag != nullptr)
  141. {
  142. result.add (String::fromCFString (tag));
  143. CFRelease (tag);
  144. }
  145. CFRelease (fileExtensionCF);
  146. }
  147. }
  148. else
  149. result.add ("public.data");
  150. return result;
  151. }
  152. static String getFilename (const File& path, const String& fallbackExtension)
  153. {
  154. auto filename = path.getFileNameWithoutExtension();
  155. auto extension = path.getFileExtension().substring (1);
  156. if (filename.isEmpty())
  157. filename = "Untitled";
  158. if (extension.isEmpty())
  159. extension = fallbackExtension;
  160. if (extension.isNotEmpty())
  161. filename += "." + extension;
  162. return filename;
  163. }
  164. //==============================================================================
  165. void didPickDocumentAtURL (NSURL* url)
  166. {
  167. bool isWriting = controller.get().documentPickerMode == UIDocumentPickerModeExportToService
  168. | controller.get().documentPickerMode == UIDocumentPickerModeMoveToService;
  169. NSUInteger accessOptions = isWriting ? 0 : NSFileCoordinatorReadingWithoutChanges;
  170. auto* fileAccessIntent = isWriting
  171. ? [NSFileAccessIntent writingIntentWithURL: url options: accessOptions]
  172. : [NSFileAccessIntent readingIntentWithURL: url options: accessOptions];
  173. NSArray<NSFileAccessIntent*>* intents = @[fileAccessIntent];
  174. auto fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter: nil];
  175. [fileCoordinator coordinateAccessWithIntents: intents queue: [NSOperationQueue mainQueue] byAccessor: ^(NSError* err)
  176. {
  177. Array<URL> chooserResults;
  178. if (err == nil)
  179. {
  180. [url startAccessingSecurityScopedResource];
  181. NSError* error = nil;
  182. NSData* bookmark = [url bookmarkDataWithOptions: 0
  183. includingResourceValuesForKeys: nil
  184. relativeToURL: nil
  185. error: &error];
  186. [bookmark retain];
  187. [url stopAccessingSecurityScopedResource];
  188. URL juceUrl (nsStringToJuce ([url absoluteString]));
  189. if (error == nil)
  190. {
  191. setURLBookmark (juceUrl, (void*) bookmark);
  192. }
  193. else
  194. {
  195. auto desc = [error localizedDescription];
  196. ignoreUnused (desc);
  197. jassertfalse;
  198. }
  199. chooserResults.add (juceUrl);
  200. }
  201. else
  202. {
  203. auto desc = [err localizedDescription];
  204. ignoreUnused (desc);
  205. jassertfalse;
  206. }
  207. owner.finished (chooserResults);
  208. }];
  209. }
  210. void pickerWasCancelled()
  211. {
  212. Array<URL> chooserResults;
  213. owner.finished (chooserResults);
  214. exitModalState (0);
  215. }
  216. //==============================================================================
  217. struct FileChooserDelegateClass : public ObjCClass<NSObject<UIDocumentPickerDelegate>>
  218. {
  219. FileChooserDelegateClass() : ObjCClass<NSObject<UIDocumentPickerDelegate>> ("FileChooserDelegate_")
  220. {
  221. addIvar<Native*> ("owner");
  222. addMethod (@selector (documentPicker:didPickDocumentAtURL:), didPickDocumentAtURL, "v@:@@");
  223. addMethod (@selector (documentPickerWasCancelled:), documentPickerWasCancelled, "v@:@");
  224. addProtocol (@protocol (UIDocumentPickerDelegate));
  225. registerClass();
  226. }
  227. static void setOwner (id self, Native* owner) { object_setInstanceVariable (self, "owner", owner); }
  228. static Native* getOwner (id self) { return getIvar<Native*> (self, "owner"); }
  229. //==============================================================================
  230. static void didPickDocumentAtURL (id self, SEL, UIDocumentPickerViewController*, NSURL* url)
  231. {
  232. auto picker = getOwner (self);
  233. if (picker != nullptr)
  234. picker->didPickDocumentAtURL (url);
  235. }
  236. static void documentPickerWasCancelled (id self, SEL, UIDocumentPickerViewController*)
  237. {
  238. auto picker = getOwner (self);
  239. if (picker != nullptr)
  240. picker->pickerWasCancelled();
  241. }
  242. };
  243. //==============================================================================
  244. FileChooser& owner;
  245. std::unique_ptr<NSObject<UIDocumentPickerDelegate>, NSObjectDeleter> delegate;
  246. std::unique_ptr<UIDocumentPickerViewController, NSObjectDeleter> controller;
  247. UIViewComponentPeer* peer = nullptr;
  248. static FileChooserDelegateClass fileChooserDelegateClass;
  249. //==============================================================================
  250. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Native)
  251. };
  252. //==============================================================================
  253. bool FileChooser::isPlatformDialogAvailable()
  254. {
  255. #if JUCE_DISABLE_NATIVE_FILECHOOSERS
  256. return false;
  257. #else
  258. return true;
  259. #endif
  260. }
  261. FileChooser::Pimpl* FileChooser::showPlatformDialog (FileChooser& owner, int flags,
  262. FilePreviewComponent*)
  263. {
  264. return new FileChooser::Native (owner, flags);
  265. }
  266. } // namespace juce