diff --git a/modules/juce_gui_basics/native/juce_ios_FileChooser.mm b/modules/juce_gui_basics/native/juce_ios_FileChooser.mm index 5da0b68eac..42e95908c4 100644 --- a/modules/juce_gui_basics/native/juce_ios_FileChooser.mm +++ b/modules/juce_gui_basics/native/juce_ios_FileChooser.mm @@ -23,6 +23,14 @@ ============================================================================== */ +@interface FileChooserControllerClass : UIDocumentPickerViewController +- (void) setParent: (FileChooser::Native*) ptr; +@end + +@interface FileChooserDelegateClass : NSObject +- (id) initWithOwner: (FileChooser::Native*) owner; +@end + namespace juce { @@ -33,124 +41,49 @@ namespace juce class FileChooser::Native : public FileChooser::Pimpl, public Component, - private AsyncUpdater + public AsyncUpdater, + public std::enable_shared_from_this { public: - Native (FileChooser& fileChooser, int flags) - : owner (fileChooser) + static std::shared_ptr make (FileChooser& fileChooser, int flags) { - 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); - } + std::shared_ptr 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); - - // 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); + 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; @@ -175,66 +108,11 @@ public: } } -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) { @@ -298,7 +176,7 @@ private: result.add (std::move (juceUrl)); } - owner.finished (std::move (result)); + passResultsToInitiator (std::move (result)); }]; } @@ -308,83 +186,174 @@ private: } void pickerWasCancelled() + { + passResultsToInitiator ({}); + } + +private: + Native (FileChooser& fileChooser, int flags) + : owner (fileChooser) + { + delegate.reset ([[FileChooserDelegateClass alloc] initWithOwner: this]); + + 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 ([[FileChooserControllerClass alloc] initWithURL: url inMode: pickerMode]); + [url release]; + } + else + { + controller.reset ([[FileChooserControllerClass alloc] initWithDocumentTypes: utTypeArray inMode: UIDocumentPickerModeOpen]); + + if (@available (iOS 11.0, *)) + [controller.get() setAllowsMultipleSelection: (flags & FileBrowserComponent::canSelectMultipleItems) != 0]; + } + + + [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); + } + } + + void passResultsToInitiator (Array urls) { cancelPendingUpdate(); - owner.finished ({}); + exitModalState (0); + + // 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()) + if ([view becomeFirstResponder] && [view isFirstResponder]) + break; + // Calling owner.finished will delete this Pimpl instance, so don't call any more member functions here! + owner.finished (std::move (urls)); } //============================================================================== - struct FileChooserDelegateClass : public ObjCClass> + static StringArray getUTTypesForWildcards (const String& filterWildcards, String& firstExtension) { - FileChooserDelegateClass() : ObjCClass> ("FileChooserDelegate_") + auto filters = StringArray::fromTokens (filterWildcards, ";", ""); + StringArray result; + + firstExtension = {}; + + if (! filters.contains ("*") && filters.size() > 0) { - addIvar ("owner"); + for (auto filter : filters) + { + if (filter.isEmpty()) + continue; - addMethod (@selector (documentPicker:didPickDocumentAtURL:), didPickDocumentAtURL); - addMethod (@selector (documentPicker:didPickDocumentsAtURLs:), didPickDocumentsAtURLs); - addMethod (@selector (documentPickerWasCancelled:), documentPickerWasCancelled); + // iOS only supports file extension wild cards + jassert (filter.upToLastOccurrenceOf (".", true, false) == "*."); - addProtocol (@protocol (UIDocumentPickerDelegate)); + auto fileExtension = filter.fromLastOccurrenceOf (".", false, false); + CFUniquePtr fileExtensionCF (fileExtension.toCFString()); - registerClass(); + 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"); } - static void setOwner (id self, Native* owner) { object_setInstanceVariable (self, "owner", owner); } - static Native* getOwner (id self) { return getIvar (self, "owner"); } + return result; + } - //============================================================================== - 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 + static String getFilename (const File& path, const String& fallbackExtension) { - FileChooserControllerClass() : ObjCClass ("FileChooserController_") - { - addIvar ("owner"); - addMethod (@selector (viewDidDisappear:), viewDidDisappear); + auto filename = path.getFileNameWithoutExtension(); + auto extension = path.getFileExtension().substring (1); - registerClass(); - } + if (filename.isEmpty()) + filename = "Untitled"; - static void setOwner (id self, Native* owner) { object_setInstanceVariable (self, "owner", owner); } - static Native* getOwner (id self) { return getIvar (self, "owner"); } + if (extension.isEmpty()) + extension = fallbackExtension; - //============================================================================== - static void viewDidDisappear (id self, SEL, BOOL animated) - { - sendSuperclassMessage (self, @selector (viewDidDisappear:), animated); + if (extension.isNotEmpty()) + filename += "." + extension; - if (auto* picker = getOwner (self)) - picker->triggerAsyncUpdate(); - } - }; + return filename; + } //============================================================================== FileChooser& owner; NSUniquePtr> delegate; - NSUniquePtr controller; + NSUniquePtr 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::showPlatformDialog (FileChooser& owner, int flags, FilePreviewComponent*) { - return std::make_shared (owner, flags); + return Native::make (owner, flags); } #if JUCE_DEPRECATION_IGNORED @@ -410,3 +379,57 @@ std::shared_ptr FileChooser::showPlatformDialog (FileChooser #endif } // namespace juce + +@implementation FileChooserControllerClass +{ + std::weak_ptr 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*) urls +{ + if (owner != nullptr) + owner->didPickDocumentsAtURLs (urls); +} + +- (void) documentPickerWasCancelled: (UIDocumentPickerViewController*) controller +{ + if (owner != nullptr) + owner->pickerWasCancelled(); +} + +@end