mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
iOS: Fix a crash when an AUv3 editor is dismissed while showing a native iOS file chooser
This commit is contained in:
parent
b0bd31b3c2
commit
b01d8c3619
1 changed files with 233 additions and 210 deletions
|
|
@ -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,124 +41,49 @@ namespace juce
|
|||
|
||||
class FileChooser::Native : public FileChooser::Pimpl,
|
||||
public Component,
|
||||
private AsyncUpdater
|
||||
public AsyncUpdater,
|
||||
public std::enable_shared_from_this<Native>
|
||||
{
|
||||
public:
|
||||
Native (FileChooser& fileChooser, int flags)
|
||||
: owner (fileChooser)
|
||||
static std::shared_ptr<Native> 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<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);
|
||||
|
||||
// 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<CFStringRef> fileExtensionCF (fileExtension.toCFString());
|
||||
|
||||
if (firstExtension.isEmpty())
|
||||
firstExtension = fileExtension;
|
||||
|
||||
if (auto tag = CFUniquePtr<CFStringRef> (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<NSURL*>* 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<URL> 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<NSObject<UIDocumentPickerDelegate>>
|
||||
static StringArray getUTTypesForWildcards (const String& filterWildcards, String& firstExtension)
|
||||
{
|
||||
FileChooserDelegateClass() : ObjCClass<NSObject<UIDocumentPickerDelegate>> ("FileChooserDelegate_")
|
||||
auto filters = StringArray::fromTokens (filterWildcards, ";", "");
|
||||
StringArray result;
|
||||
|
||||
firstExtension = {};
|
||||
|
||||
if (! filters.contains ("*") && filters.size() > 0)
|
||||
{
|
||||
addIvar<Native*> ("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<CFStringRef> fileExtensionCF (fileExtension.toCFString());
|
||||
|
||||
registerClass();
|
||||
if (firstExtension.isEmpty())
|
||||
firstExtension = fileExtension;
|
||||
|
||||
if (auto tag = CFUniquePtr<CFStringRef> (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<Native*> (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<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>
|
||||
static String getFilename (const File& path, const String& fallbackExtension)
|
||||
{
|
||||
FileChooserControllerClass() : ObjCClass<UIDocumentPickerViewController> ("FileChooserController_")
|
||||
{
|
||||
addIvar<Native*> ("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<Native*> (self, "owner"); }
|
||||
if (extension.isEmpty())
|
||||
extension = fallbackExtension;
|
||||
|
||||
//==============================================================================
|
||||
static void viewDidDisappear (id self, SEL, BOOL animated)
|
||||
{
|
||||
sendSuperclassMessage<void> (self, @selector (viewDidDisappear:), animated);
|
||||
if (extension.isNotEmpty())
|
||||
filename += "." + extension;
|
||||
|
||||
if (auto* picker = getOwner (self))
|
||||
picker->triggerAsyncUpdate();
|
||||
}
|
||||
};
|
||||
return filename;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
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…
Add table
Add a link
Reference in a new issue