/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { #if ! (defined (__IPHONE_16_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_16_0) JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") #define JUCE_DEPRECATION_IGNORED 1 #endif class FileChooser::Native : public FileChooser::Pimpl, public Component, private AsyncUpdater { public: Native (FileChooser& fileChooser, int flags) : owner (fileChooser) { static FileChooserDelegateClass delegateClass; delegate.reset ([delegateClass.createInstance() init]); FileChooserDelegateClass::setOwner (delegate.get(), this); static FileChooserControllerClass controllerClass; auto* controllerClassInstance = controllerClass.createInstance(); String firstFileExtension; auto utTypeArray = createNSArrayFromStringArray (getUTTypesForWildcards (owner.filters, firstFileExtension)); if ((flags & FileBrowserComponent::saveMode) != 0) { auto currentFileOrDirectory = owner.startingFile; UIDocumentPickerMode pickerMode = currentFileOrDirectory.existsAsFile() ? UIDocumentPickerModeExportToService : UIDocumentPickerModeMoveToService; if (! currentFileOrDirectory.existsAsFile()) { auto filename = getFilename (currentFileOrDirectory, firstFileExtension); auto tmpDirectory = File::createTempFile ("JUCE-filepath"); if (tmpDirectory.createDirectory().wasOk()) { currentFileOrDirectory = tmpDirectory.getChildFile (filename); currentFileOrDirectory.replaceWithText (""); } else { // Temporary directory creation failed! You need to specify a // path you have write access to. Saving will not work for // current path. jassertfalse; } } auto url = [[NSURL alloc] initFileURLWithPath: juceStringToNS (currentFileOrDirectory.getFullPathName())]; controller.reset ([controllerClassInstance initWithURL: url inMode: pickerMode]); [url release]; } else { controller.reset ([controllerClassInstance initWithDocumentTypes: utTypeArray inMode: UIDocumentPickerModeOpen]); if (@available (iOS 11.0, *)) [controller.get() setAllowsMultipleSelection: (flags & FileBrowserComponent::canSelectMultipleItems) != 0]; } FileChooserControllerClass::setOwner (controller.get(), this); [controller.get() setDelegate: delegate.get()]; [controller.get() setModalTransitionStyle: UIModalTransitionStyleCrossDissolve]; setOpaque (false); if (fileChooser.parent != nullptr) { [controller.get() setModalPresentationStyle: UIModalPresentationFullScreen]; auto chooserBounds = fileChooser.parent->getBounds(); setBounds (chooserBounds); setAlwaysOnTop (true); fileChooser.parent->addAndMakeVisible (this); } else { if (SystemStats::isRunningInAppExtensionSandbox()) { // Opening a native top-level window in an AUv3 is not allowed (sandboxing). You need to specify a // parent component (for example your editor) to parent the native file chooser window. To do this // specify a parent component in the FileChooser's constructor! jassertfalse; return; } auto chooserBounds = Desktop::getInstance().getDisplays().getPrimaryDisplay()->userArea; setBounds (chooserBounds); setAlwaysOnTop (true); setVisible (true); addToDesktop (0); } } ~Native() override { exitModalState (0); // Our old peer may not have received a becomeFirstResponder call at this point, // so the static currentlyFocusedPeer may be null. // We'll try to find an appropriate peer to focus. for (auto i = 0; i < ComponentPeer::getNumPeers(); ++i) if (auto* p = ComponentPeer::getPeer (i)) if (p != getPeer()) if (auto* view = (UIView*) p->getNativeHandle()) [view becomeFirstResponder]; } void launch() override { enterModalState (true, nullptr, true); } void runModally() override { #if JUCE_MODAL_LOOPS_PERMITTED runModalLoop(); #else jassertfalse; #endif } void parentHierarchyChanged() override { auto* newPeer = dynamic_cast (getPeer()); if (peer != newPeer) { peer = newPeer; if (peer != nullptr) { if (auto* parentController = peer->controller) [parentController showViewController: controller.get() sender: parentController]; peer->toFront (false); } } } private: //============================================================================== void handleAsyncUpdate() override { pickerWasCancelled(); } //============================================================================== static StringArray getUTTypesForWildcards (const String& filterWildcards, String& firstExtension) { auto filters = StringArray::fromTokens (filterWildcards, ";", ""); StringArray result; firstExtension = {}; if (! filters.contains ("*") && filters.size() > 0) { for (auto filter : filters) { if (filter.isEmpty()) continue; // iOS only supports file extension wild cards jassert (filter.upToLastOccurrenceOf (".", true, false) == "*."); auto fileExtension = filter.fromLastOccurrenceOf (".", false, false); CFUniquePtr fileExtensionCF (fileExtension.toCFString()); if (firstExtension.isEmpty()) firstExtension = fileExtension; if (auto tag = CFUniquePtr (UTTypeCreatePreferredIdentifierForTag (kUTTagClassFilenameExtension, fileExtensionCF.get(), nullptr))) result.add (String::fromCFString (tag.get())); } } else { result.add ("public.data"); } return result; } static String getFilename (const File& path, const String& fallbackExtension) { auto filename = path.getFileNameWithoutExtension(); auto extension = path.getFileExtension().substring (1); if (filename.isEmpty()) filename = "Untitled"; if (extension.isEmpty()) extension = fallbackExtension; if (extension.isNotEmpty()) filename += "." + extension; return filename; } //============================================================================== void didPickDocumentsAtURLs (NSArray* urls) { cancelPendingUpdate(); const auto isWriting = controller.get().documentPickerMode == UIDocumentPickerModeExportToService || controller.get().documentPickerMode == UIDocumentPickerModeMoveToService; const auto accessOptions = isWriting ? 0 : NSFileCoordinatorReadingWithoutChanges; auto* fileCoordinator = [[[NSFileCoordinator alloc] initWithFilePresenter: nil] autorelease]; auto* intents = [[[NSMutableArray alloc] init] autorelease]; for (NSURL* url in urls) { auto* fileAccessIntent = isWriting ? [NSFileAccessIntent writingIntentWithURL: url options: accessOptions] : [NSFileAccessIntent readingIntentWithURL: url options: accessOptions]; [intents addObject: fileAccessIntent]; } [fileCoordinator coordinateAccessWithIntents: intents queue: [NSOperationQueue mainQueue] byAccessor: ^(NSError* err) { if (err != nil) { auto desc = [err localizedDescription]; ignoreUnused (desc); jassertfalse; return; } Array result; for (NSURL* url in urls) { [url startAccessingSecurityScopedResource]; NSError* error = nil; auto* bookmark = [url bookmarkDataWithOptions: 0 includingResourceValuesForKeys: nil relativeToURL: nil error: &error]; [bookmark retain]; [url stopAccessingSecurityScopedResource]; URL juceUrl (nsStringToJuce ([url absoluteString])); if (error == nil) { setURLBookmark (juceUrl, (void*) bookmark); } else { auto desc = [error localizedDescription]; ignoreUnused (desc); jassertfalse; } result.add (std::move (juceUrl)); } owner.finished (std::move (result)); }]; } void didPickDocumentAtURL (NSURL* url) { didPickDocumentsAtURLs (@[url]); } void pickerWasCancelled() { cancelPendingUpdate(); owner.finished ({}); // Calling owner.finished will delete this Pimpl instance, so don't call any more member functions here! } //============================================================================== struct FileChooserDelegateClass : public ObjCClass> { FileChooserDelegateClass() : ObjCClass> ("FileChooserDelegate_") { addIvar ("owner"); addMethod (@selector (documentPicker:didPickDocumentAtURL:), didPickDocumentAtURL); addMethod (@selector (documentPicker:didPickDocumentsAtURLs:), didPickDocumentsAtURLs); addMethod (@selector (documentPickerWasCancelled:), documentPickerWasCancelled); addProtocol (@protocol (UIDocumentPickerDelegate)); registerClass(); } static void setOwner (id self, Native* owner) { object_setInstanceVariable (self, "owner", owner); } static Native* getOwner (id self) { return getIvar (self, "owner"); } //============================================================================== static void didPickDocumentAtURL (id self, SEL, UIDocumentPickerViewController*, NSURL* url) { if (auto* picker = getOwner (self)) picker->didPickDocumentAtURL (url); } static void didPickDocumentsAtURLs (id self, SEL, UIDocumentPickerViewController*, NSArray* urls) { if (auto* picker = getOwner (self)) picker->didPickDocumentsAtURLs (urls); } static void documentPickerWasCancelled (id self, SEL, UIDocumentPickerViewController*) { if (auto* picker = getOwner (self)) picker->pickerWasCancelled(); } }; struct FileChooserControllerClass : public ObjCClass { FileChooserControllerClass() : ObjCClass ("FileChooserController_") { addIvar ("owner"); addMethod (@selector (viewDidDisappear:), viewDidDisappear); registerClass(); } static void setOwner (id self, Native* owner) { object_setInstanceVariable (self, "owner", owner); } static Native* getOwner (id self) { return getIvar (self, "owner"); } //============================================================================== static void viewDidDisappear (id self, SEL, BOOL animated) { sendSuperclassMessage (self, @selector (viewDidDisappear:), animated); if (auto* picker = getOwner (self)) picker->triggerAsyncUpdate(); } }; //============================================================================== FileChooser& owner; NSUniquePtr> delegate; NSUniquePtr controller; UIViewComponentPeer* peer = nullptr; static FileChooserDelegateClass fileChooserDelegateClass; static FileChooserControllerClass fileChooserControllerClass; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Native) }; //============================================================================== bool FileChooser::isPlatformDialogAvailable() { #if JUCE_DISABLE_NATIVE_FILECHOOSERS return false; #else return true; #endif } std::shared_ptr FileChooser::showPlatformDialog (FileChooser& owner, int flags, FilePreviewComponent*) { return std::make_shared (owner, flags); } #if JUCE_DEPRECATION_IGNORED JUCE_END_IGNORE_WARNINGS_GCC_LIKE #endif } // namespace juce