| @@ -23,6 +23,14 @@ | |||
| ============================================================================== | |||
| */ | |||
| @interface FileChooserControllerClass : UIDocumentPickerViewController | |||
| - (void) setParent: (FileChooser::Native*) ptr; | |||
| @end | |||
| @interface FileChooserDelegateClass : NSObject<UIDocumentPickerDelegate> | |||
| - (id) initWithOwner: (FileChooser::Native*) owner; | |||
| @end | |||
| namespace juce | |||
| { | |||
| @@ -33,18 +41,160 @@ namespace juce | |||
| class FileChooser::Native : public FileChooser::Pimpl, | |||
| public Component, | |||
| private AsyncUpdater | |||
| public AsyncUpdater, | |||
| public std::enable_shared_from_this<Native> | |||
| { | |||
| public: | |||
| static std::shared_ptr<Native> make (FileChooser& fileChooser, int flags) | |||
| { | |||
| std::shared_ptr<Native> result { new Native (fileChooser, flags) }; | |||
| /* Must be called after forming a shared_ptr to an instance of this class. | |||
| Note that we can't call this directly inside the class constructor, because | |||
| the owning shared_ptr might not yet exist. | |||
| */ | |||
| [result->controller.get() setParent: result.get()]; | |||
| return result; | |||
| } | |||
| ~Native() override | |||
| { | |||
| exitModalState (0); | |||
| } | |||
| void launch() override | |||
| { | |||
| jassert (shared_from_this() != nullptr); | |||
| /* Normally, when deleteWhenDismissed is true, the modal component manger will keep a copy of a raw pointer | |||
| to our component and delete it when the modal state has ended. However, this is incompatible with | |||
| our class being tracked by shared_ptr as it will force delete our class regardless of the current | |||
| reference count. On the other hand, it's important that the modal manager keeps a reference as it can | |||
| sometimes be the only reference to our class. | |||
| To do this, we set deleteWhenDismissed to false so that the modal component manager does not delete | |||
| our class. Instead, we pass in a lambda which captures a shared_ptr to ourselves to increase the | |||
| reference count while the component is modal. | |||
| */ | |||
| enterModalState (true, | |||
| ModalCallbackFunction::create ([_self = shared_from_this()] (int) {}), | |||
| false); | |||
| } | |||
| void runModally() override | |||
| { | |||
| #if JUCE_MODAL_LOOPS_PERMITTED | |||
| launch(); | |||
| runModalLoop(); | |||
| #else | |||
| jassertfalse; | |||
| #endif | |||
| } | |||
| void parentHierarchyChanged() override | |||
| { | |||
| auto* newPeer = dynamic_cast<UIViewComponentPeer*> (getPeer()); | |||
| if (peer != newPeer) | |||
| { | |||
| peer = newPeer; | |||
| if (peer != nullptr) | |||
| { | |||
| if (auto* parentController = peer->controller) | |||
| [parentController showViewController: controller.get() sender: parentController]; | |||
| peer->toFront (false); | |||
| } | |||
| } | |||
| } | |||
| void handleAsyncUpdate() override | |||
| { | |||
| pickerWasCancelled(); | |||
| } | |||
| //============================================================================== | |||
| void didPickDocumentsAtURLs (NSArray<NSURL*>* 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<URL> 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)); | |||
| } | |||
| passResultsToInitiator (std::move (result)); | |||
| }]; | |||
| } | |||
| void didPickDocumentAtURL (NSURL* url) | |||
| { | |||
| didPickDocumentsAtURLs (@[url]); | |||
| } | |||
| void pickerWasCancelled() | |||
| { | |||
| passResultsToInitiator ({}); | |||
| } | |||
| private: | |||
| 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(); | |||
| delegate.reset ([[FileChooserDelegateClass alloc] initWithOwner: this]); | |||
| String firstFileExtension; | |||
| auto utTypeArray = createNSArrayFromStringArray (getUTTypesForWildcards (owner.filters, firstFileExtension)); | |||
| @@ -78,20 +228,17 @@ public: | |||
| auto url = [[NSURL alloc] initFileURLWithPath: juceStringToNS (currentFileOrDirectory.getFullPathName())]; | |||
| controller.reset ([controllerClassInstance initWithURL: url | |||
| inMode: pickerMode]); | |||
| controller.reset ([[FileChooserControllerClass alloc] initWithURL: url inMode: pickerMode]); | |||
| [url release]; | |||
| } | |||
| else | |||
| { | |||
| controller.reset ([controllerClassInstance initWithDocumentTypes: utTypeArray | |||
| inMode: UIDocumentPickerModeOpen]); | |||
| controller.reset ([[FileChooserControllerClass alloc] 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]; | |||
| @@ -128,58 +275,24 @@ public: | |||
| } | |||
| } | |||
| ~Native() override | |||
| void passResultsToInitiator (Array<URL> urls) | |||
| { | |||
| cancelPendingUpdate(); | |||
| 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. | |||
| // If the caller attempts to show a platform-native dialog box inside the results callback (e.g. in the DialogsDemo) | |||
| // then the original peer must already have focus. Otherwise, there's a danger that either the invisible FileChooser | |||
| // components will display the popup, locking the application, or maybe no component will have focus, and the | |||
| // dialog won't show at all. | |||
| 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<UIViewComponentPeer*> (getPeer()); | |||
| if (peer != newPeer) | |||
| { | |||
| peer = newPeer; | |||
| if (peer != nullptr) | |||
| { | |||
| if (auto* parentController = peer->controller) | |||
| [parentController showViewController: controller.get() sender: parentController]; | |||
| if ([view becomeFirstResponder] && [view isFirstResponder]) | |||
| break; | |||
| peer->toFront (false); | |||
| } | |||
| } | |||
| } | |||
| private: | |||
| //============================================================================== | |||
| void handleAsyncUpdate() override | |||
| { | |||
| pickerWasCancelled(); | |||
| // Calling owner.finished will delete this Pimpl instance, so don't call any more member functions here! | |||
| owner.finished (std::move (urls)); | |||
| } | |||
| //============================================================================== | |||
| @@ -235,156 +348,12 @@ private: | |||
| return filename; | |||
| } | |||
| //============================================================================== | |||
| void didPickDocumentsAtURLs (NSArray<NSURL*>* 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<URL> 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<NSObject<UIDocumentPickerDelegate>> | |||
| { | |||
| FileChooserDelegateClass() : ObjCClass<NSObject<UIDocumentPickerDelegate>> ("FileChooserDelegate_") | |||
| { | |||
| addIvar<Native*> ("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<Native*> (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<NSURL*>* 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<UIDocumentPickerViewController> | |||
| { | |||
| FileChooserControllerClass() : ObjCClass<UIDocumentPickerViewController> ("FileChooserController_") | |||
| { | |||
| addIvar<Native*> ("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<Native*> (self, "owner"); } | |||
| //============================================================================== | |||
| static void viewDidDisappear (id self, SEL, BOOL animated) | |||
| { | |||
| sendSuperclassMessage<void> (self, @selector (viewDidDisappear:), animated); | |||
| if (auto* picker = getOwner (self)) | |||
| picker->triggerAsyncUpdate(); | |||
| } | |||
| }; | |||
| //============================================================================== | |||
| FileChooser& owner; | |||
| NSUniquePtr<NSObject<UIDocumentPickerDelegate>> delegate; | |||
| NSUniquePtr<UIDocumentPickerViewController> controller; | |||
| NSUniquePtr<FileChooserControllerClass> controller; | |||
| UIViewComponentPeer* peer = nullptr; | |||
| static FileChooserDelegateClass fileChooserDelegateClass; | |||
| static FileChooserControllerClass fileChooserControllerClass; | |||
| //============================================================================== | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Native) | |||
| }; | |||
| @@ -402,7 +371,7 @@ bool FileChooser::isPlatformDialogAvailable() | |||
| std::shared_ptr<FileChooser::Pimpl> FileChooser::showPlatformDialog (FileChooser& owner, int flags, | |||
| FilePreviewComponent*) | |||
| { | |||
| return std::make_shared<FileChooser::Native> (owner, flags); | |||
| return Native::make (owner, flags); | |||
| } | |||
| #if JUCE_DEPRECATION_IGNORED | |||
| @@ -410,3 +379,57 @@ std::shared_ptr<FileChooser::Pimpl> FileChooser::showPlatformDialog (FileChooser | |||
| #endif | |||
| } // namespace juce | |||
| @implementation FileChooserControllerClass | |||
| { | |||
| std::weak_ptr<FileChooser::Native> ptr; | |||
| } | |||
| - (void) setParent: (FileChooser::Native*) parent | |||
| { | |||
| jassert (parent != nullptr); | |||
| jassert (parent->shared_from_this() != nullptr); | |||
| ptr = parent->weak_from_this(); | |||
| } | |||
| - (void) viewDidDisappear: (BOOL) animated | |||
| { | |||
| [super viewDidDisappear: animated]; | |||
| if (auto nativeParent = ptr.lock()) | |||
| nativeParent->triggerAsyncUpdate(); | |||
| } | |||
| @end | |||
| @implementation FileChooserDelegateClass | |||
| { | |||
| FileChooser::Native* owner; | |||
| } | |||
| - (id) initWithOwner: (FileChooser::Native*) o | |||
| { | |||
| self = [super init]; | |||
| owner = o; | |||
| return self; | |||
| } | |||
| - (void) documentPicker: (UIDocumentPickerViewController*) controller didPickDocumentAtURL: (NSURL*) url | |||
| { | |||
| if (owner != nullptr) | |||
| owner->didPickDocumentAtURL (url); | |||
| } | |||
| - (void) documentPicker: (UIDocumentPickerViewController*) controller didPickDocumentsAtURLs: (NSArray<NSURL*>*) urls | |||
| { | |||
| if (owner != nullptr) | |||
| owner->didPickDocumentsAtURLs (urls); | |||
| } | |||
| - (void) documentPickerWasCancelled: (UIDocumentPickerViewController*) controller | |||
| { | |||
| if (owner != nullptr) | |||
| owner->pickerWasCancelled(); | |||
| } | |||
| @end | |||