Browse Source

iOS: Fix a crash when an AUv3 editor is dismissed while showing a native iOS file chooser

v7.0.9
hogliux 3 years ago
parent
commit
b01d8c3619
1 changed files with 226 additions and 203 deletions
  1. +226
    -203
      modules/juce_gui_basics/native/juce_ios_FileChooser.mm

+ 226
- 203
modules/juce_gui_basics/native/juce_ios_FileChooser.mm View File

@@ -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

Loading…
Cancel
Save