1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

ContentSharer: Update interface to return safer ScopedMessageBox instances

This commit is contained in:
reuk 2023-03-20 20:55:26 +00:00
parent 557d690ff4
commit 9d1a6a3b28
No known key found for this signature in database
GPG key ID: FCB43929F012EE5C
22 changed files with 1304 additions and 1033 deletions

View file

@ -4,6 +4,32 @@ JUCE breaking changes
develop develop
======= =======
Change
------
The signatures of the ContentSharer member functions have been updated. The
ContentSharer class itself is no longer a singleton.
Possible Issues
---------------
Projects that use the old signatures will not build until they are updated.
Workaround
----------
Instead of calling content sharer functions through a singleton instance, e.g.
ContentSharer::getInstance()->shareText (...);
call the static member functions directly:
ScopedMessageBox messageBox = ContentSharer::shareTextScoped (...);
The new functions return a ScopedMessageBox instance. On iOS, the content
sharer will only remain open for as long as the ScopedMessageBox remains alive.
On Android, this functionality will be added as/when the native APIs allow.
Rationale
---------
The new signatures are safer and easier to use. The ScopedMessageBox also
allows content sharers to be dismissed programmatically, which wasn't
previously possible.
Change Change
------ ------
The minimum supported AAX library version has been bumped to 2.4.0 and the The minimum supported AAX library version has been bumped to 2.4.0 and the

View file

@ -54,6 +54,7 @@ target_sources(DemoRunner PRIVATE
target_compile_definitions(DemoRunner PRIVATE target_compile_definitions(DemoRunner PRIVATE
PIP_JUCE_EXAMPLES_DIRECTORY_STRING="${JUCE_SOURCE_DIR}/examples" PIP_JUCE_EXAMPLES_DIRECTORY_STRING="${JUCE_SOURCE_DIR}/examples"
JUCE_ALLOW_STATIC_NULL_VARIABLES=0 JUCE_ALLOW_STATIC_NULL_VARIABLES=0
JUCE_CONTENT_SHARING=1
JUCE_DEMO_RUNNER=1 JUCE_DEMO_RUNNER=1
JUCE_PLUGINHOST_LV2=1 JUCE_PLUGINHOST_LV2=1
JUCE_PLUGINHOST_VST3=1 JUCE_PLUGINHOST_VST3=1

View file

@ -314,12 +314,11 @@ private:
SafePointer<CameraDemo> safeThis (this); SafePointer<CameraDemo> safeThis (this);
juce::ContentSharer::getInstance()->shareFiles ({url}, messageBox = ContentSharer::shareFilesScoped ({ url }, [safeThis] (bool success, const String&)
[safeThis] (bool success, const String&) mutable {
{ if (safeThis)
if (safeThis) safeThis->sharingFinished (success, false);
safeThis->sharingFinished (success, false); });
});
#endif #endif
} }
} }
@ -355,12 +354,11 @@ private:
SafePointer<CameraDemo> safeThis (this); SafePointer<CameraDemo> safeThis (this);
juce::ContentSharer::getInstance()->shareFiles ({url}, messageBox = ContentSharer::shareFilesScoped ({ url }, [safeThis] (bool success, const String&)
[safeThis] (bool success, const String&) mutable {
{ if (safeThis)
if (safeThis) safeThis->sharingFinished (success, true);
safeThis->sharingFinished (success, true); });
});
} }
#endif #endif
} }

View file

@ -462,7 +462,7 @@ private:
} }
else if (type == shareText) else if (type == shareText)
{ {
ContentSharer::getInstance()->shareText ("I love JUCE!", [ptr = Component::SafePointer (this)] (bool success, const String& error) messageBox = ContentSharer::shareTextScoped ("I love JUCE!", [ptr = Component::SafePointer (this)] (bool success, const String& error)
{ {
if (ptr == nullptr) if (ptr == nullptr)
return; return;
@ -489,7 +489,7 @@ private:
Array<URL> urls; Array<URL> urls;
urls.add (URL (fileToSave)); urls.add (URL (fileToSave));
ContentSharer::getInstance()->shareFiles (urls, [ptr = Component::SafePointer (this)] (bool success, const String& error) messageBox = ContentSharer::shareFilesScoped (urls, [ptr = Component::SafePointer (this)] (bool success, const String& error)
{ {
if (ptr == nullptr) if (ptr == nullptr)
return; return;
@ -519,7 +519,7 @@ private:
Array<Image> images { myImage, myImage2 }; Array<Image> images { myImage, myImage2 };
ContentSharer::getInstance()->shareImages (images, [ptr = Component::SafePointer (this)] (bool success, const String& error) messageBox = ContentSharer::shareImagesScoped (images, nullptr, [ptr = Component::SafePointer (this)] (bool success, const String& error)
{ {
if (ptr == nullptr) if (ptr == nullptr)
return; return;

View file

@ -48,6 +48,7 @@ juce_add_binary_data(AudioPluginHostData SOURCES
target_compile_definitions(AudioPluginHost PRIVATE target_compile_definitions(AudioPluginHost PRIVATE
JUCE_ALSA=1 JUCE_ALSA=1
JUCE_CONTENT_SHARING=1
JUCE_DIRECTSOUND=1 JUCE_DIRECTSOUND=1
JUCE_DISABLE_CAUTIOUS_PARAMETER_ID_CHECKING=1 JUCE_DISABLE_CAUTIOUS_PARAMETER_ID_CHECKING=1
JUCE_PLUGINHOST_LADSPA=1 JUCE_PLUGINHOST_LADSPA=1

View file

@ -1675,6 +1675,10 @@ private:
if (! isLibrary()) if (! isLibrary())
{ {
auto* receiver = getOrCreateChildWithName (application, "receiver");
setAttributeIfNotPresent (*receiver, "android:name", "com.rmsl.juce.Receiver");
setAttributeIfNotPresent (*receiver, "android:exported", "false");
auto* app = createApplicationElement (*manifest); auto* app = createApplicationElement (*manifest);
auto* act = createActivityElement (*app); auto* act = createActivityElement (*app);

View file

@ -94,8 +94,8 @@ struct SystemJavaClassComparator
if ((! isSysClassA) && (! isSysClassB)) if ((! isSysClassA) && (! isSysClassB))
{ {
return DefaultElementComparator<bool>::compareElements (first != nullptr ? first->byteCode != nullptr : false, return DefaultElementComparator<bool>::compareElements (first != nullptr && first->byteCode != nullptr,
second != nullptr ? second->byteCode != nullptr : false); second != nullptr && second->byteCode != nullptr);
} }
return DefaultElementComparator<bool>::compareElements (isSystemClass (first), return DefaultElementComparator<bool>::compareElements (isSystemClass (first),
@ -631,43 +631,17 @@ jobject FragmentOverlay::getNativeHandle()
} }
//============================================================================== //==============================================================================
class ActivityLauncher : public FragmentOverlay void startAndroidActivityForResult (const LocalRef<jobject>& intent,
int requestCode,
std::function<void (int, int, LocalRef<jobject>)>&& callback)
{ {
public: auto* launcher = new ActivityLauncher (intent, requestCode);
ActivityLauncher (const LocalRef<jobject>& intentToUse, launcher->callback = [launcher, c = std::move (callback)] (auto&&... args)
int requestCodeToUse,
std::function<void (int, int, LocalRef<jobject>)> && callbackToUse)
: intent (intentToUse), requestCode (requestCodeToUse), callback (std::move (callbackToUse))
{}
void onStart() override
{ {
if (! std::exchange (activityHasStarted, true)) NullCheckedInvocation::invoke (c, args...);
getEnv()->CallVoidMethod (getNativeHandle(), AndroidFragment.startActivityForResult, delete launcher;
intent.get(), requestCode); };
} launcher->open();
void onActivityResult (int activityRequestCode, int resultCode, LocalRef<jobject> data) override
{
if (callback)
callback (activityRequestCode, resultCode, std::move (data));
getEnv()->CallVoidMethod (getNativeHandle(), JuceFragmentOverlay.close);
delete this;
}
private:
GlobalRef intent;
int requestCode;
std::function<void (int, int, LocalRef<jobject>)> callback;
bool activityHasStarted = false;
};
void startAndroidActivityForResult (const LocalRef<jobject>& intent, int requestCode,
std::function<void (int, int, LocalRef<jobject>)> && callback)
{
auto* activityLauncher = new ActivityLauncher (intent, requestCode, std::move (callback));
activityLauncher->open();
} }
//============================================================================== //==============================================================================

View file

@ -262,7 +262,8 @@ template <typename T, size_t N> constexpr auto numBytes (const T (&) [N]) { retu
METHOD (getApplicationInfo, "getApplicationInfo", "()Landroid/content/pm/ApplicationInfo;") \ METHOD (getApplicationInfo, "getApplicationInfo", "()Landroid/content/pm/ApplicationInfo;") \
METHOD (checkCallingOrSelfPermission, "checkCallingOrSelfPermission", "(Ljava/lang/String;)I") \ METHOD (checkCallingOrSelfPermission, "checkCallingOrSelfPermission", "(Ljava/lang/String;)I") \
METHOD (checkCallingOrSelfUriPermission, "checkCallingOrSelfUriPermission", "(Landroid/net/Uri;I)I") \ METHOD (checkCallingOrSelfUriPermission, "checkCallingOrSelfUriPermission", "(Landroid/net/Uri;I)I") \
METHOD (getCacheDir, "getCacheDir", "()Ljava/io/File;") METHOD (getCacheDir, "getCacheDir", "()Ljava/io/File;") \
METHOD (registerReceiver, "registerReceiver", "(Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;)Landroid/content/Intent;") \
DECLARE_JNI_CLASS (AndroidContext, "android/content/Context") DECLARE_JNI_CLASS (AndroidContext, "android/content/Context")
#undef JNI_CLASS_MEMBERS #undef JNI_CLASS_MEMBERS
@ -418,6 +419,7 @@ DECLARE_JNI_CLASS (AndroidHandlerThread, "android/os/HandlerThread")
METHOD (putExtraStrings, "putExtra", "(Ljava/lang/String;[Ljava/lang/String;)Landroid/content/Intent;") \ METHOD (putExtraStrings, "putExtra", "(Ljava/lang/String;[Ljava/lang/String;)Landroid/content/Intent;") \
METHOD (putExtraParcelable, "putExtra", "(Ljava/lang/String;Landroid/os/Parcelable;)Landroid/content/Intent;") \ METHOD (putExtraParcelable, "putExtra", "(Ljava/lang/String;Landroid/os/Parcelable;)Landroid/content/Intent;") \
METHOD (putExtraBool, "putExtra", "(Ljava/lang/String;Z)Landroid/content/Intent;") \ METHOD (putExtraBool, "putExtra", "(Ljava/lang/String;Z)Landroid/content/Intent;") \
METHOD (putExtraInt, "putExtra", "(Ljava/lang/String;I)Landroid/content/Intent;") \
METHOD (putParcelableArrayListExtra, "putParcelableArrayListExtra", "(Ljava/lang/String;Ljava/util/ArrayList;)Landroid/content/Intent;") \ METHOD (putParcelableArrayListExtra, "putParcelableArrayListExtra", "(Ljava/lang/String;Ljava/util/ArrayList;)Landroid/content/Intent;") \
METHOD (setAction, "setAction", "(Ljava/lang/String;)Landroid/content/Intent;") \ METHOD (setAction, "setAction", "(Ljava/lang/String;)Landroid/content/Intent;") \
METHOD (setFlags, "setFlags", "(I)Landroid/content/Intent;") \ METHOD (setFlags, "setFlags", "(I)Landroid/content/Intent;") \
@ -427,6 +429,12 @@ DECLARE_JNI_CLASS (AndroidHandlerThread, "android/os/HandlerThread")
DECLARE_JNI_CLASS (AndroidIntent, "android/content/Intent") DECLARE_JNI_CLASS (AndroidIntent, "android/content/Intent")
#undef JNI_CLASS_MEMBERS #undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
STATICMETHOD (createChooser, "createChooser", "(Landroid/content/Intent;Ljava/lang/CharSequence;Landroid/content/IntentSender;)Landroid/content/Intent;") \
DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidIntent22, "android/content/Intent", 22)
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (constructor, "<init>", "()V") \ METHOD (constructor, "<init>", "()V") \
METHOD (postRotate, "postRotate", "(FFF)Z") \ METHOD (postRotate, "postRotate", "(FFF)Z") \
@ -490,7 +498,8 @@ DECLARE_JNI_CLASS (AndroidPaint, "android/graphics/Paint")
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
STATICMETHOD (getActivity, "getActivity", "(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;") \ STATICMETHOD (getActivity, "getActivity", "(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;") \
METHOD (getIntentSender, "getIntentSender", "()Landroid/content/IntentSender;") STATICMETHOD (getBroadcast, "getBroadcast", "(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;") \
METHOD (getIntentSender, "getIntentSender", "()Landroid/content/IntentSender;") \
DECLARE_JNI_CLASS (AndroidPendingIntent, "android/app/PendingIntent") DECLARE_JNI_CLASS (AndroidPendingIntent, "android/app/PendingIntent")
#undef JNI_CLASS_MEMBERS #undef JNI_CLASS_MEMBERS
@ -1026,10 +1035,37 @@ private:
//============================================================================== //==============================================================================
// Allows you to start an activity without requiring to have an activity // Allows you to start an activity without requiring to have an activity
void startAndroidActivityForResult (const LocalRef<jobject>& intent, int requestCode, void startAndroidActivityForResult (const LocalRef<jobject>& intent,
std::function<void (int, int, LocalRef<jobject>)> && callback); int requestCode,
std::function<void (int, int, LocalRef<jobject>)>&& callback);
//============================================================================== class ActivityLauncher : public FragmentOverlay
{
public:
ActivityLauncher (const LocalRef<jobject>& intentToUse, int requestCodeToUse)
: intent (intentToUse), requestCode (requestCodeToUse)
{}
void onStart() override
{
if (! std::exchange (activityHasStarted, true))
getEnv()->CallVoidMethod (getNativeHandle(), AndroidFragment.startActivityForResult, intent.get(), requestCode);
}
void onActivityResult (int activityRequestCode, int resultCode, LocalRef<jobject> data) override
{
NullCheckedInvocation::invoke (callback, activityRequestCode, resultCode, std::move (data));
}
std::function<void (int, int, LocalRef<jobject>)> callback;
private:
GlobalRef intent;
int requestCode;
bool activityHasStarted = false;
};
//==============================================================================
bool androidHasSystemFeature (const String& property); bool androidHasSystemFeature (const String& property);
String audioManagerGetProperty (const String& property); String audioManagerGetProperty (const String& property);

View file

@ -0,0 +1,98 @@
/*
==============================================================================
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::detail
{
class ConcreteScopedContentSharerImpl : public ScopedMessageBoxImpl,
private AsyncUpdater
{
public:
static ScopedMessageBox show (std::unique_ptr<ScopedContentSharerInterface>&& native,
ContentSharer::Callback callback)
{
return ScopedMessageBox (runAsync (std::move (native), std::move (callback)));
}
~ConcreteScopedContentSharerImpl() override
{
cancelPendingUpdate();
}
void close() override
{
cancelPendingUpdate();
nativeImplementation->close();
self.reset();
}
private:
static std::shared_ptr<ConcreteScopedContentSharerImpl> runAsync (std::unique_ptr<ScopedContentSharerInterface>&& p,
ContentSharer::Callback&& c)
{
std::shared_ptr<ConcreteScopedContentSharerImpl> result (new ConcreteScopedContentSharerImpl (std::move (p), std::move (c)));
result->self = result;
result->triggerAsyncUpdate();
return result;
}
ConcreteScopedContentSharerImpl (std::unique_ptr<ScopedContentSharerInterface>&& p,
ContentSharer::Callback&& c)
: callback (std::move (c)), nativeImplementation (std::move (p)) {}
void handleAsyncUpdate() override
{
nativeImplementation->runAsync ([weakRecipient = std::weak_ptr<ConcreteScopedContentSharerImpl> (self)] (bool result, const String& error)
{
const auto notifyRecipient = [result, error, weakRecipient]
{
if (const auto locked = weakRecipient.lock())
{
NullCheckedInvocation::invoke (locked->callback, result, error);
locked->self.reset();
}
};
if (MessageManager::getInstance()->isThisTheMessageThread())
notifyRecipient();
else
MessageManager::callAsync (notifyRecipient);
});
}
ContentSharer::Callback callback;
std::unique_ptr<ScopedContentSharerInterface> nativeImplementation;
/* The 'old' native message box API doesn't have a concept of content sharer owners.
Instead, content sharers have to clean up after themselves, once they're done displaying.
To allow this mode of usage, the implementation keeps an owning reference to itself,
which is cleared once the content sharer is closed or asked to quit. To display a content
sharer box without a scoped lifetime, just create a Pimpl instance without using
the ScopedContentSharer wrapper, and the Pimpl will destroy itself after it is dismissed.
*/
std::shared_ptr<ConcreteScopedContentSharerImpl> self;
};
} // namespace juce::detail

View file

@ -0,0 +1,211 @@
/*
==============================================================================
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::detail
{
/*
Instances of this type can show and dismiss a content sharer.
This is an interface rather than a concrete type so that platforms can pick an implementation at
runtime if necessary.
*/
struct ScopedContentSharerInterface
{
virtual ~ScopedContentSharerInterface() = default;
/* Shows the content sharer.
When the content sharer exits normally, it should send the result to the passed-in function.
The passed-in function is safe to call from any thread at any time.
*/
virtual void runAsync (ContentSharer::Callback callback)
{
jassertfalse;
NullCheckedInvocation::invoke (callback, false, "Content sharing not available on this platform!");
}
/* Forcefully closes the content sharer.
This will be called when the content sharer handle has fallen out of scope.
If the content sharer has already been closed by the user, this shouldn't do anything.
*/
virtual void close() {}
/* Implemented differently for each platform. */
static std::unique_ptr<ScopedContentSharerInterface> shareFiles (const Array<URL>&, Component*);
static std::unique_ptr<ScopedContentSharerInterface> shareText (const String&, Component*);
/* Implemented below. */
static std::unique_ptr<ScopedContentSharerInterface> shareImages (const Array<Image>&, std::unique_ptr<ImageFileFormat>, Component*);
static std::unique_ptr<ScopedContentSharerInterface> shareData (MemoryBlock, Component*);
};
class TemporaryFilesDecorator : public ScopedContentSharerInterface,
private AsyncUpdater
{
public:
explicit TemporaryFilesDecorator (Component* parentIn)
: parent (parentIn) {}
void runAsync (ContentSharer::Callback cb) override
{
callback = std::move (cb);
task = std::async (std::launch::async, [this]
{
std::tie (temporaryFiles, error) = prepareTemporaryFiles();
triggerAsyncUpdate();
});
}
void close() override
{
if (inner != nullptr)
inner->close();
}
private:
virtual std::tuple<Array<URL>, String> prepareTemporaryFiles() const = 0;
void handleAsyncUpdate() override
{
if (error.isNotEmpty())
{
NullCheckedInvocation::invoke (callback, false, error);
return;
}
inner = shareFiles (temporaryFiles, parent);
if (inner == nullptr)
{
NullCheckedInvocation::invoke (callback, false, TRANS ("Failed to create file sharer"));
return;
}
inner->runAsync (callback);
}
Array<URL> temporaryFiles;
String error;
std::unique_ptr<ScopedContentSharerInterface> inner;
ContentSharer::Callback callback;
std::future<void> task;
Component* parent = nullptr;
};
std::unique_ptr<ScopedContentSharerInterface> ScopedContentSharerInterface::shareImages (const Array<Image>& images,
std::unique_ptr<ImageFileFormat> format,
Component* parent)
{
class Decorator : public TemporaryFilesDecorator
{
public:
Decorator (Array<Image> imagesIn, std::unique_ptr<ImageFileFormat> formatIn, Component* parentIn)
: TemporaryFilesDecorator (parentIn), images (std::move (imagesIn)), format (std::move (formatIn)) {}
private:
std::tuple<Array<URL>, String> prepareTemporaryFiles() const override
{
const auto extension = format->getFormatName().toLowerCase();
Array<URL> result;
for (const auto& image : images)
{
File tempFile = File::createTempFile (extension);
if (! tempFile.create().wasOk())
return { Array<URL>{}, TRANS ("Failed to create temporary file") };
std::unique_ptr<FileOutputStream> outputStream (tempFile.createOutputStream());
if (outputStream == nullptr)
return { Array<URL>{}, TRANS ("Failed to open temporary file for writing") };
if (format->writeImageToStream (image, *outputStream))
result.add (URL (tempFile));
}
for (const auto& url : result)
jassertquiet (url.isLocalFile() && url.getLocalFile().existsAsFile());
return { std::move (result), String{} };
}
Array<Image> images;
std::unique_ptr<ImageFileFormat> format;
};
return std::make_unique<Decorator> (images,
format == nullptr ? std::make_unique<PNGImageFormat>() : std::move (format),
parent);
}
std::unique_ptr<ScopedContentSharerInterface> ScopedContentSharerInterface::shareData (MemoryBlock mb, Component* parent)
{
class Decorator : public TemporaryFilesDecorator
{
public:
Decorator (MemoryBlock mbIn, Component* parentIn)
: TemporaryFilesDecorator (parentIn), mb (std::move (mbIn)) {}
private:
std::tuple<Array<URL>, String> prepareTemporaryFiles() const override
{
File tempFile = File::createTempFile ("data");
if (! tempFile.create().wasOk())
return { Array<URL>{}, TRANS ("Failed to create temporary file") };
std::unique_ptr<FileOutputStream> outputStream (tempFile.createOutputStream());
if (outputStream == nullptr)
return { Array<URL>{}, TRANS ("Failed to open temporary file for writing") };
size_t pos = 0;
size_t totalSize = mb.getSize();
while (pos < totalSize)
{
size_t numToWrite = std::min ((size_t) 8192, totalSize - pos);
if (! outputStream->write (mb.begin() + pos, numToWrite))
return { Array<URL>{}, TRANS ("Failed to write to temporary file") };
pos += numToWrite;
}
return { Array<URL> { URL (tempFile) }, String{} };
}
MemoryBlock mb;
};
return std::make_unique<Decorator> (std::move (mb), parent);
}
} // namespace juce::detail

View file

@ -27,7 +27,16 @@ namespace juce::detail
{ {
//============================================================================== //==============================================================================
class ScopedMessageBoxImpl : private AsyncUpdater class ScopedMessageBoxImpl
{
public:
virtual ~ScopedMessageBoxImpl() = default;
virtual void close() = 0;
};
//==============================================================================
class ConcreteScopedMessageBoxImpl : public ScopedMessageBoxImpl,
private AsyncUpdater
{ {
public: public:
static ScopedMessageBox show (std::unique_ptr<ScopedMessageBoxInterface>&& native, static ScopedMessageBox show (std::unique_ptr<ScopedMessageBoxInterface>&& native,
@ -50,12 +59,12 @@ public:
return 0; return 0;
} }
~ScopedMessageBoxImpl() override ~ConcreteScopedMessageBoxImpl() override
{ {
cancelPendingUpdate(); cancelPendingUpdate();
} }
void close() void close() override
{ {
cancelPendingUpdate(); cancelPendingUpdate();
nativeImplementation->close(); nativeImplementation->close();
@ -63,10 +72,10 @@ public:
} }
private: private:
static std::shared_ptr<ScopedMessageBoxImpl> runAsync (std::unique_ptr<ScopedMessageBoxInterface>&& p, static std::shared_ptr<ConcreteScopedMessageBoxImpl> runAsync (std::unique_ptr<ScopedMessageBoxInterface>&& p,
std::unique_ptr<ModalComponentManager::Callback>&& c) std::unique_ptr<ModalComponentManager::Callback>&& c)
{ {
std::shared_ptr<ScopedMessageBoxImpl> result (new ScopedMessageBoxImpl (std::move (p), std::move (c))); std::shared_ptr<ConcreteScopedMessageBoxImpl> result (new ConcreteScopedMessageBoxImpl (std::move (p), std::move (c)));
result->self = result; result->self = result;
result->triggerAsyncUpdate(); result->triggerAsyncUpdate();
return result; return result;
@ -78,16 +87,16 @@ private:
return local != nullptr ? local->runSync() : 0; return local != nullptr ? local->runSync() : 0;
} }
explicit ScopedMessageBoxImpl (std::unique_ptr<ScopedMessageBoxInterface>&& p) explicit ConcreteScopedMessageBoxImpl (std::unique_ptr<ScopedMessageBoxInterface>&& p)
: ScopedMessageBoxImpl (std::move (p), nullptr) {} : ConcreteScopedMessageBoxImpl (std::move (p), nullptr) {}
ScopedMessageBoxImpl (std::unique_ptr<ScopedMessageBoxInterface>&& p, ConcreteScopedMessageBoxImpl (std::unique_ptr<ScopedMessageBoxInterface>&& p,
std::unique_ptr<ModalComponentManager::Callback>&& c) std::unique_ptr<ModalComponentManager::Callback>&& c)
: callback (std::move (c)), nativeImplementation (std::move (p)) {} : callback (std::move (c)), nativeImplementation (std::move (p)) {}
void handleAsyncUpdate() override void handleAsyncUpdate() override
{ {
nativeImplementation->runAsync ([weakRecipient = std::weak_ptr<ScopedMessageBoxImpl> (self)] (int result) nativeImplementation->runAsync ([weakRecipient = std::weak_ptr<ConcreteScopedMessageBoxImpl> (self)] (int result)
{ {
const auto notifyRecipient = [result, weakRecipient] const auto notifyRecipient = [result, weakRecipient]
{ {
@ -117,7 +126,7 @@ private:
message box without a scoped lifetime, just create a Pimpl instance without using message box without a scoped lifetime, just create a Pimpl instance without using
the ScopedMessageBox wrapper, and the Pimpl will destroy itself after it is dismissed. the ScopedMessageBox wrapper, and the Pimpl will destroy itself after it is dismissed.
*/ */
std::shared_ptr<ScopedMessageBoxImpl> self; std::shared_ptr<ConcreteScopedMessageBoxImpl> self;
}; };
} // namespace juce::detail } // namespace juce::detail

View file

@ -26,247 +26,49 @@
namespace juce namespace juce
{ {
#if JUCE_CONTENT_SHARING ScopedMessageBox ContentSharer::shareFilesScoped (const Array<URL>& files,
//============================================================================== Callback callback,
class ContentSharer::PrepareImagesThread : private Thread Component* parent)
{ {
public: auto impl = detail::ScopedContentSharerInterface::shareFiles (files, parent);
PrepareImagesThread (ContentSharer& cs, const Array<Image>& imagesToUse, return detail::ConcreteScopedContentSharerImpl::show (std::move (impl), std::move (callback));
ImageFileFormat* imageFileFormatToUse)
: Thread ("ContentSharer::PrepareImagesThread"),
owner (cs),
images (imagesToUse),
imageFileFormat (imageFileFormatToUse == nullptr ? new PNGImageFormat()
: imageFileFormatToUse),
extension (imageFileFormat->getFormatName().toLowerCase())
{
startThread();
}
~PrepareImagesThread() override
{
signalThreadShouldExit();
waitForThreadToExit (10000);
}
private:
void run() override
{
for (const auto& image : images)
{
if (threadShouldExit())
return;
File tempFile = File::createTempFile (extension);
if (! tempFile.create().wasOk())
break;
std::unique_ptr<FileOutputStream> outputStream (tempFile.createOutputStream());
if (outputStream == nullptr)
break;
if (imageFileFormat->writeImageToStream (image, *outputStream))
owner.temporaryFiles.add (tempFile);
}
finish();
}
void finish()
{
MessageManager::callAsync ([this]() { owner.filesToSharePrepared(); });
}
ContentSharer& owner;
const Array<Image> images;
std::unique_ptr<ImageFileFormat> imageFileFormat;
String extension;
};
//==============================================================================
class ContentSharer::PrepareDataThread : private Thread
{
public:
PrepareDataThread (ContentSharer& cs, const MemoryBlock& mb)
: Thread ("ContentSharer::PrepareDataThread"),
owner (cs),
data (mb)
{
startThread();
}
~PrepareDataThread() override
{
signalThreadShouldExit();
waitForThreadToExit (10000);
}
private:
void run() override
{
File tempFile = File::createTempFile ("data");
if (tempFile.create().wasOk())
{
if (auto outputStream = std::unique_ptr<FileOutputStream> (tempFile.createOutputStream()))
{
size_t pos = 0;
size_t totalSize = data.getSize();
while (pos < totalSize)
{
if (threadShouldExit())
return;
size_t numToWrite = std::min ((size_t) 8192, totalSize - pos);
outputStream->write (data.begin() + pos, numToWrite);
pos += numToWrite;
}
owner.temporaryFiles.add (tempFile);
}
}
finish();
}
void finish()
{
MessageManager::callAsync ([this]() { owner.filesToSharePrepared(); });
}
ContentSharer& owner;
const MemoryBlock data;
};
#endif
//==============================================================================
JUCE_IMPLEMENT_SINGLETON (ContentSharer)
ContentSharer::ContentSharer() {}
ContentSharer::~ContentSharer() { clearSingletonInstance(); }
void ContentSharer::shareFiles ([[maybe_unused]] const Array<URL>& files,
std::function<void (bool, const String&)> callbackToUse)
{
#if JUCE_CONTENT_SHARING
startNewShare (callbackToUse);
pimpl->shareFiles (files);
#else
// Content sharing is not available on this platform!
jassertfalse;
if (callbackToUse)
callbackToUse (false, "Content sharing is not available on this platform!");
#endif
} }
#if JUCE_CONTENT_SHARING ScopedMessageBox ContentSharer::shareTextScoped (const String& text,
void ContentSharer::startNewShare (std::function<void (bool, const String&)> callbackToUse) Callback callback,
Component* parent)
{ {
// You should not start another sharing operation before the previous one is finished. auto impl = detail::ScopedContentSharerInterface::shareText (text, parent);
// Forcibly stopping a previous sharing operation is rarely a good idea! return detail::ConcreteScopedContentSharerImpl::show (std::move (impl), std::move (callback));
jassert (pimpl == nullptr); }
pimpl.reset();
prepareDataThread = nullptr; ScopedMessageBox ContentSharer::shareImagesScoped (const Array<Image>& images,
prepareImagesThread = nullptr; std::unique_ptr<ImageFileFormat> format,
Callback callback,
Component* parent)
{
auto impl = detail::ScopedContentSharerInterface::shareImages (images, std::move (format), parent);
return detail::ConcreteScopedContentSharerImpl::show (std::move (impl), std::move (callback));
}
deleteTemporaryFiles(); ScopedMessageBox ContentSharer::shareDataScoped (const MemoryBlock& mb,
Callback callback,
Component* parent)
{
auto impl = detail::ScopedContentSharerInterface::shareData (mb, parent);
return detail::ConcreteScopedContentSharerImpl::show (std::move (impl), std::move (callback));
}
// You need to pass a valid callback. #if ! (JUCE_CONTENT_SHARING && (JUCE_IOS || JUCE_ANDROID))
jassert (callbackToUse); auto detail::ScopedContentSharerInterface::shareFiles (const Array<URL>&, Component*) -> std::unique_ptr<ScopedContentSharerInterface>
callback = std::move (callbackToUse); {
return std::make_unique<detail::ScopedContentSharerInterface>();
}
pimpl.reset (createPimpl()); auto detail::ScopedContentSharerInterface::shareText (const String&, Component*) -> std::unique_ptr<ScopedContentSharerInterface>
{
return std::make_unique<detail::ScopedContentSharerInterface>();
} }
#endif #endif
void ContentSharer::shareText ([[maybe_unused]] const String& text,
std::function<void (bool, const String&)> callbackToUse)
{
#if JUCE_CONTENT_SHARING
startNewShare (callbackToUse);
pimpl->shareText (text);
#else
// Content sharing is not available on this platform!
jassertfalse;
if (callbackToUse)
callbackToUse (false, "Content sharing is not available on this platform!");
#endif
}
void ContentSharer::shareImages ([[maybe_unused]] const Array<Image>& images,
std::function<void (bool, const String&)> callbackToUse,
[[maybe_unused]] ImageFileFormat* imageFileFormatToUse)
{
#if JUCE_CONTENT_SHARING
startNewShare (callbackToUse);
prepareImagesThread.reset (new PrepareImagesThread (*this, images, imageFileFormatToUse));
#else
// Content sharing is not available on this platform!
jassertfalse;
if (callbackToUse)
callbackToUse (false, "Content sharing is not available on this platform!");
#endif
}
#if JUCE_CONTENT_SHARING
void ContentSharer::filesToSharePrepared()
{
Array<URL> urls;
for (const auto& tempFile : temporaryFiles)
urls.add (URL (tempFile));
prepareImagesThread = nullptr;
prepareDataThread = nullptr;
pimpl->shareFiles (urls);
}
#endif
void ContentSharer::shareData ([[maybe_unused]] const MemoryBlock& mb,
std::function<void (bool, const String&)> callbackToUse)
{
#if JUCE_CONTENT_SHARING
startNewShare (callbackToUse);
prepareDataThread.reset (new PrepareDataThread (*this, mb));
#else
if (callbackToUse)
callbackToUse (false, "Content sharing not available on this platform!");
#endif
}
void ContentSharer::sharingFinished (bool succeeded, const String& errorDescription)
{
deleteTemporaryFiles();
std::function<void (bool, String)> cb;
std::swap (cb, callback);
String error (errorDescription);
#if JUCE_CONTENT_SHARING
pimpl.reset();
#endif
if (cb)
cb (succeeded, error);
}
void ContentSharer::deleteTemporaryFiles()
{
for (auto& f : temporaryFiles)
f.deleteFile();
temporaryFiles.clear();
}
} // namespace juce } // namespace juce

View file

@ -23,21 +23,30 @@
============================================================================== ==============================================================================
*/ */
#pragma once
namespace juce namespace juce
{ {
/** A singleton class responsible for sharing content between apps and devices. //==============================================================================
/**
Functions that allow sharing content between apps and devices.
You can share text, images, files or an arbitrary data block. You can share text, images, files or an arbitrary data block.
@tags{GUI} @tags{GUI}
*/ */
class JUCE_API ContentSharer : public DeletedAtShutdown class JUCE_API ContentSharer
{ {
public: public:
JUCE_DECLARE_SINGLETON (ContentSharer, false) ContentSharer() = delete;
/** A callback of this type is passed when starting a content sharing
session.
When the session ends, the function will receive a flag indicating
whether the session was successful. In the case of failure, the
errorText argument may hold a string describing the problem.
*/
using Callback = std::function<void (bool success, const String& errorText)>;
/** Shares the given files. Each URL should be either a full file path /** Shares the given files. Each URL should be either a full file path
or it should point to a resource within the application bundle. For or it should point to a resource within the application bundle. For
@ -50,9 +59,16 @@ public:
Sadly on Android the returned success flag may be wrong as there is no Sadly on Android the returned success flag may be wrong as there is no
standard way the sharing targets report if the sharing operation standard way the sharing targets report if the sharing operation
succeeded. Also, the optional error message is always empty on Android. succeeded. Also, the optional error message is always empty on Android.
@param files the files to share
@param callback a callback that will be called on the main thread
when the sharing session ends
@param parent the component that should be used to host the
sharing view
*/ */
void shareFiles (const Array<URL>& files, [[nodiscard]] static ScopedMessageBox shareFilesScoped (const Array<URL>& files,
std::function<void (bool /*success*/, const String& /*error*/)> callback); Callback callback,
Component* parent = nullptr);
/** Shares the given text. /** Shares the given text.
@ -60,9 +76,16 @@ public:
Sadly on Android the returned success flag may be wrong as there is no Sadly on Android the returned success flag may be wrong as there is no
standard way the sharing targets report if the sharing operation standard way the sharing targets report if the sharing operation
succeeded. Also, the optional error message is always empty on Android. succeeded. Also, the optional error message is always empty on Android.
@param text the text to share
@param callback a callback that will be called on the main thread
when the sharing session ends
@param parent the component that should be used to host the
sharing view
*/ */
void shareText (const String& text, [[nodiscard]] static ScopedMessageBox shareTextScoped (const String& text,
std::function<void (bool /*success*/, const String& /*error*/)> callback); Callback callback,
Component* parent = nullptr);
/** A convenience function to share an image. This is useful when you have images /** A convenience function to share an image. This is useful when you have images
loaded in memory. The images will be written to temporary files first, so if loaded in memory. The images will be written to temporary files first, so if
@ -84,10 +107,20 @@ public:
Sadly on Android the returned success flag may be wrong as there is no Sadly on Android the returned success flag may be wrong as there is no
standard way the sharing targets report if the sharing operation standard way the sharing targets report if the sharing operation
succeeded. Also, the optional error message is always empty on Android. succeeded. Also, the optional error message is always empty on Android.
@param images the images to share
@param format the file format to use when saving the images.
If no format is provided, a sensible default will
be used.
@param callback a callback that will be called on the main thread
when the sharing session ends
@param parent the component that should be used to host the
sharing view
*/ */
void shareImages (const Array<Image>& images, [[nodiscard]] static ScopedMessageBox shareImagesScoped (const Array<Image>& images,
std::function<void (bool /*success*/, const String& /*error*/)> callback, std::unique_ptr<ImageFileFormat> format,
ImageFileFormat* imageFileFormatToUse = nullptr); Callback callback,
Component* parent = nullptr);
/** A convenience function to share arbitrary data. The data will be written /** A convenience function to share arbitrary data. The data will be written
to a temporary file and then that file will be shared. If you have to a temporary file and then that file will be shared. If you have
@ -97,47 +130,16 @@ public:
Sadly on Android the returned success flag may be wrong as there is no Sadly on Android the returned success flag may be wrong as there is no
standard way the sharing targets report if the sharing operation standard way the sharing targets report if the sharing operation
succeeded. Also, the optional error message is always empty on Android. succeeded. Also, the optional error message is always empty on Android.
@param mb the data to share
@param callback a callback that will be called on the main thread
when the sharing session ends
@param parent the component that should be used to host the
sharing view
*/ */
void shareData (const MemoryBlock& mb, [[nodiscard]] static ScopedMessageBox shareDataScoped (const MemoryBlock& mb,
std::function<void (bool /*success*/, const String& /*error*/)> callback); Callback callback,
Component* parent = nullptr);
private:
ContentSharer();
~ContentSharer();
Array<File> temporaryFiles;
std::function<void (bool, String)> callback;
#if JUCE_CONTENT_SHARING
struct Pimpl
{
virtual ~Pimpl() {}
virtual void shareFiles (const Array<URL>& files) = 0;
virtual void shareText (const String& text) = 0;
};
std::unique_ptr<Pimpl> pimpl;
Pimpl* createPimpl();
void startNewShare (std::function<void (bool, const String&)>);
class ContentSharerNativeImpl;
friend class ContentSharerNativeImpl;
class PrepareImagesThread;
friend class PrepareImagesThread;
std::unique_ptr<PrepareImagesThread> prepareImagesThread;
class PrepareDataThread;
friend class PrepareDataThread;
std::unique_ptr<PrepareDataThread> prepareDataThread;
void filesToSharePrepared();
#endif
void deleteTemporaryFiles();
void sharingFinished (bool, const String&);
}; };
} // namespace juce } // namespace juce

View file

@ -122,6 +122,8 @@
#include "detail/juce_ToolbarItemDragAndDropOverlayComponent.h" #include "detail/juce_ToolbarItemDragAndDropOverlayComponent.h"
#include "detail/juce_ScopedMessageBoxInterface.h" #include "detail/juce_ScopedMessageBoxInterface.h"
#include "detail/juce_ScopedMessageBoxImpl.h" #include "detail/juce_ScopedMessageBoxImpl.h"
#include "detail/juce_ScopedContentSharerInterface.h"
#include "detail/juce_ScopedContentSharerImpl.h"
#include "detail/juce_WindowingHelpers.h" #include "detail/juce_WindowingHelpers.h"
#include "detail/juce_AlertWindowHelpers.h" #include "detail/juce_AlertWindowHelpers.h"
#include "detail/juce_TopLevelWindowManager.h" #include "detail/juce_TopLevelWindowManager.h"
@ -143,6 +145,7 @@
#include "native/accessibility/juce_ios_Accessibility.mm" #include "native/accessibility/juce_ios_Accessibility.mm"
#include "native/juce_ios_Windowing.mm" #include "native/juce_ios_Windowing.mm"
#include "native/juce_ios_NativeMessageBox.mm" #include "native/juce_ios_NativeMessageBox.mm"
#include "native/juce_ios_NativeModalWrapperComponent.h"
#include "native/juce_ios_FileChooser.mm" #include "native/juce_ios_FileChooser.mm"
#if JUCE_CONTENT_SHARING #if JUCE_CONTENT_SHARING

View file

@ -0,0 +1,42 @@
/*
==============================================================================
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.
==============================================================================
*/
package com.rmsl.juce;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
//==============================================================================
public class Receiver extends BroadcastReceiver
{
@Override
public void onReceive (Context context, Intent intent)
{
onBroadcastResultNative (intent.getIntExtra ("com.rmsl.juce.JUCE_REQUEST_CODE", 0));
}
private native void onBroadcastResultNative (int requestCode);
}

File diff suppressed because it is too large Load diff

View file

@ -26,80 +26,23 @@
namespace juce namespace juce
{ {
class ContentSharer::ContentSharerNativeImpl : public ContentSharer::Pimpl, class NativeScopedContentSharerInterface : public detail::ScopedContentSharerInterface,
private Component public detail::NativeModalWrapperComponent
{ {
public: public:
ContentSharerNativeImpl (ContentSharer& cs) NativeScopedContentSharerInterface (Component* parentIn, NSUniquePtr<NSArray> itemsIn)
: owner (cs) : parent (parentIn), items (std::move (itemsIn)) {}
void runAsync (std::function<void (bool, const String&)> callback) override
{ {
static PopoverDelegateClass cls; if ([items.get() count] == 0)
popoverDelegate.reset ([cls.createInstance() init]);
}
~ContentSharerNativeImpl() override
{
exitModalState (0);
}
void shareFiles (const Array<URL>& files) override
{
auto urls = [NSMutableArray arrayWithCapacity: (NSUInteger) files.size()];
for (const auto& f : files)
{
NSString* nativeFilePath = nil;
if (f.isLocalFile())
{
nativeFilePath = juceStringToNS (f.getLocalFile().getFullPathName());
}
else
{
auto filePath = f.toString (false);
auto* fileDirectory = filePath.contains ("/")
? juceStringToNS (filePath.upToLastOccurrenceOf ("/", false, false))
: [NSString string];
auto fileName = juceStringToNS (filePath.fromLastOccurrenceOf ("/", false, false)
.upToLastOccurrenceOf (".", false, false));
auto fileExt = juceStringToNS (filePath.fromLastOccurrenceOf (".", false, false));
if ([fileDirectory length] == NSUInteger (0))
nativeFilePath = [[NSBundle mainBundle] pathForResource: fileName
ofType: fileExt];
else
nativeFilePath = [[NSBundle mainBundle] pathForResource: fileName
ofType: fileExt
inDirectory: fileDirectory];
}
if (nativeFilePath != nil)
[urls addObject: [NSURL fileURLWithPath: nativeFilePath]];
}
share (urls);
}
void shareText (const String& text) override
{
auto array = [NSArray arrayWithObject: juceStringToNS (text)];
share (array);
}
private:
void share (NSArray* items)
{
if ([items count] == 0)
{ {
jassertfalse; jassertfalse;
owner.sharingFinished (false, "No valid items found for sharing."); NullCheckedInvocation::invoke (callback, false, "No valid items found for sharing.");
return; return;
} }
controller.reset ([[UIActivityViewController alloc] initWithActivityItems: items controller.reset ([[UIActivityViewController alloc] initWithActivityItems: items.get()
applicationActivities: nil]); applicationActivities: nil]);
controller.get().excludedActivityTypes = nil; controller.get().excludedActivityTypes = nil;
@ -107,98 +50,76 @@ private:
controller.get().completionWithItemsHandler = ^([[maybe_unused]] UIActivityType type, BOOL completed, controller.get().completionWithItemsHandler = ^([[maybe_unused]] UIActivityType type, BOOL completed,
[[maybe_unused]] NSArray* returnedItems, NSError* error) [[maybe_unused]] NSArray* returnedItems, NSError* error)
{ {
succeeded = completed; const auto errorDescription = error != nil ? nsStringToJuce ([error localizedDescription])
: String();
if (error != nil)
errorDescription = nsStringToJuce ([error localizedDescription]);
exitModalState (0); exitModalState (0);
NullCheckedInvocation::invoke (callback, completed && errorDescription.isEmpty(), errorDescription);
}; };
controller.get().modalTransitionStyle = UIModalTransitionStyleCoverVertical; displayNativeWindowModally (parent);
auto bounds = Desktop::getInstance().getDisplays().getPrimaryDisplay()->userArea; enterModalState (true, nullptr, false);
setBounds (bounds);
setAlwaysOnTop (true);
setVisible (true);
addToDesktop (0);
enterModalState (true,
ModalCallbackFunction::create ([this] (int)
{
owner.sharingFinished (succeeded, errorDescription);
}),
false);
} }
static bool isIPad() void close() override
{ {
return [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad; [controller.get() dismissViewControllerAnimated: YES completion: nil];
} }
//============================================================================== private:
void parentHierarchyChanged() override UIViewController* getViewController() const override { return controller.get(); }
{
auto* newPeer = dynamic_cast<UIViewComponentPeer*> (getPeer());
if (peer != newPeer) Component* parent = nullptr;
{
peer = newPeer;
if (isIPad())
{
controller.get().preferredContentSize = peer->view.frame.size;
auto screenBounds = [UIScreen mainScreen].bounds;
auto* popoverController = controller.get().popoverPresentationController;
popoverController.sourceView = peer->view;
popoverController.sourceRect = CGRectMake (0.f, screenBounds.size.height - 10.f, screenBounds.size.width, 10.f);
popoverController.canOverlapSourceViewRect = YES;
popoverController.delegate = popoverDelegate.get();
}
if (auto* parentController = peer->controller)
[parentController showViewController: controller.get() sender: parentController];
}
}
//==============================================================================
struct PopoverDelegateClass : public ObjCClass<NSObject<UIPopoverPresentationControllerDelegate>>
{
PopoverDelegateClass() : ObjCClass<NSObject<UIPopoverPresentationControllerDelegate>> ("PopoverDelegateClass_")
{
addMethod (@selector (popoverPresentationController:willRepositionPopoverToRect:inView:), willRepositionPopover);
registerClass();
}
//==============================================================================
static void willRepositionPopover (id, SEL, UIPopoverPresentationController*, CGRect* rect, UIView*)
{
auto screenBounds = [UIScreen mainScreen].bounds;
rect->origin.x = 0.f;
rect->origin.y = screenBounds.size.height - 10.f;
rect->size.width = screenBounds.size.width;
rect->size.height = 10.f;
}
};
ContentSharer& owner;
UIViewComponentPeer* peer = nullptr;
NSUniquePtr<UIActivityViewController> controller; NSUniquePtr<UIActivityViewController> controller;
NSUniquePtr<NSObject<UIPopoverPresentationControllerDelegate>> popoverDelegate; NSUniquePtr<NSArray> items;
bool succeeded = false;
String errorDescription;
}; };
//============================================================================== auto detail::ScopedContentSharerInterface::shareFiles (const Array<URL>& files, Component* parent) -> std::unique_ptr<ScopedContentSharerInterface>
ContentSharer::Pimpl* ContentSharer::createPimpl()
{ {
return new ContentSharerNativeImpl (*this); NSUniquePtr<NSMutableArray> urls ([[NSMutableArray arrayWithCapacity: (NSUInteger) files.size()] retain]);
for (const auto& f : files)
{
NSString* nativeFilePath = nil;
if (f.isLocalFile())
{
nativeFilePath = juceStringToNS (f.getLocalFile().getFullPathName());
}
else
{
auto filePath = f.toString (false);
auto* fileDirectory = filePath.contains ("/")
? juceStringToNS (filePath.upToLastOccurrenceOf ("/", false, false))
: [NSString string];
auto fileName = juceStringToNS (filePath.fromLastOccurrenceOf ("/", false, false)
.upToLastOccurrenceOf (".", false, false));
auto fileExt = juceStringToNS (filePath.fromLastOccurrenceOf (".", false, false));
if ([fileDirectory length] == NSUInteger (0))
nativeFilePath = [[NSBundle mainBundle] pathForResource: fileName
ofType: fileExt];
else
nativeFilePath = [[NSBundle mainBundle] pathForResource: fileName
ofType: fileExt
inDirectory: fileDirectory];
}
if (nativeFilePath != nil)
[urls.get() addObject: [NSURL fileURLWithPath: nativeFilePath]];
}
return std::make_unique<NativeScopedContentSharerInterface> (parent, std::move (urls));
}
auto detail::ScopedContentSharerInterface::shareText (const String& text, Component* parent) -> std::unique_ptr<ScopedContentSharerInterface>
{
NSUniquePtr<NSArray> array ([[NSArray arrayWithObject: juceStringToNS (text)] retain]);
return std::make_unique<NativeScopedContentSharerInterface> (parent, std::move (array));
} }
} // namespace juce } // namespace juce

View file

@ -39,8 +39,9 @@ namespace juce
#define JUCE_DEPRECATION_IGNORED 1 #define JUCE_DEPRECATION_IGNORED 1
#endif #endif
//==============================================================================
class FileChooser::Native : public FileChooser::Pimpl, class FileChooser::Native : public FileChooser::Pimpl,
public Component, public detail::NativeModalWrapperComponent,
public AsyncUpdater, public AsyncUpdater,
public std::enable_shared_from_this<Native> public std::enable_shared_from_this<Native>
{ {
@ -56,11 +57,6 @@ public:
return result; return result;
} }
~Native() override
{
exitModalState (0);
}
void launch() override void launch() override
{ {
jassert (shared_from_this() != nullptr); jassert (shared_from_this() != nullptr);
@ -90,24 +86,6 @@ public:
#endif #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 void handleAsyncUpdate() override
{ {
pickerWasCancelled(); pickerWasCancelled();
@ -189,6 +167,8 @@ public:
} }
private: private:
UIViewController* getViewController() const override { return controller.get(); }
Native (FileChooser& fileChooser, int flags) Native (FileChooser& fileChooser, int flags)
: owner (fileChooser) : owner (fileChooser)
{ {
@ -241,40 +221,9 @@ private:
[controller.get() setAllowsMultipleSelection: (flags & FileBrowserComponent::canSelectMultipleItems) != 0]; [controller.get() setAllowsMultipleSelection: (flags & FileBrowserComponent::canSelectMultipleItems) != 0];
} }
[controller.get() setDelegate: delegate.get()]; [controller.get() setDelegate: delegate.get()];
[controller.get() setModalTransitionStyle: UIModalTransitionStyleCrossDissolve];
setOpaque (false); displayNativeWindowModally (fileChooser.parent);
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) void passResultsToInitiator (Array<URL> urls)
@ -363,7 +312,6 @@ private:
FileChooser& owner; FileChooser& owner;
NSUniquePtr<NSObject<UIDocumentPickerDelegate>> delegate; NSUniquePtr<NSObject<UIDocumentPickerDelegate>> delegate;
NSUniquePtr<FileChooserControllerClass> controller; NSUniquePtr<FileChooserControllerClass> controller;
UIViewComponentPeer* peer = nullptr;
//============================================================================== //==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Native) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Native)

View file

@ -0,0 +1,132 @@
/*
==============================================================================
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::detail
{
/**
Sets up a native control to be hosted on top of a JUCE component.
*/
class NativeModalWrapperComponent : public Component
{
public:
void parentHierarchyChanged() final
{
auto* newPeer = dynamic_cast<UIViewComponentPeer*> (getPeer());
if (std::exchange (peer, newPeer) == newPeer)
return;
if (peer == nullptr)
return;
if (isIPad())
{
getViewController().preferredContentSize = peer->view.frame.size;
if (auto* popoverController = getViewController().popoverPresentationController)
{
popoverController.sourceView = peer->view;
popoverController.sourceRect = CGRectMake (0.f, getHeight() - 10.f, getWidth(), 10.f);
popoverController.canOverlapSourceViewRect = YES;
popoverController.delegate = popoverDelegate.get();
}
}
if (auto* parentController = peer->controller)
[parentController showViewController: getViewController() sender: parentController];
peer->toFront (false);
}
void displayNativeWindowModally (Component* parent)
{
setOpaque (false);
if (parent != nullptr)
{
[getViewController() setModalPresentationStyle: UIModalPresentationPageSheet];
setBounds (parent->getLocalBounds());
setAlwaysOnTop (true);
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);
}
}
private:
virtual UIViewController* getViewController() const = 0;
static bool isIPad()
{
return [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad;
}
struct PopoverDelegateClass : public ObjCClass<NSObject<UIPopoverPresentationControllerDelegate>>
{
PopoverDelegateClass()
: ObjCClass ("PopoverDelegateClass_")
{
addMethod (@selector (popoverPresentationController:willRepositionPopoverToRect:inView:), [] (id, SEL, UIPopoverPresentationController*, CGRect* rect, UIView*)
{
auto screenBounds = [UIScreen mainScreen].bounds;
rect->origin.x = 0.f;
rect->origin.y = screenBounds.size.height - 10.f;
rect->size.width = screenBounds.size.width;
rect->size.height = 10.f;
});
registerClass();
}
};
UIViewComponentPeer* peer = nullptr;
NSUniquePtr<NSObject<UIPopoverPresentationControllerDelegate>> popoverDelegate { []
{
static PopoverDelegateClass cls;
return cls.createInstance();
}() };
};
} // namespace juce::detail

View file

@ -37,7 +37,7 @@ static juce_wchar getDefaultPasswordChar() noexcept
static int showAlertWindowUnmanaged (const MessageBoxOptions& opts, ModalComponentManager::Callback* cb) static int showAlertWindowUnmanaged (const MessageBoxOptions& opts, ModalComponentManager::Callback* cb)
{ {
return detail::ScopedMessageBoxImpl::showUnmanaged (detail::AlertWindowHelpers::create (opts), cb); return detail::ConcreteScopedMessageBoxImpl::showUnmanaged (detail::AlertWindowHelpers::create (opts), cb);
} }
//============================================================================== //==============================================================================
@ -693,7 +693,7 @@ ScopedMessageBox AlertWindow::showScopedAsync (const MessageBoxOptions& options,
if (LookAndFeel::getDefaultLookAndFeel().isUsingNativeAlertWindows()) if (LookAndFeel::getDefaultLookAndFeel().isUsingNativeAlertWindows())
return NativeMessageBox::showScopedAsync (options, std::move (callback)); return NativeMessageBox::showScopedAsync (options, std::move (callback));
return detail::ScopedMessageBoxImpl::show (detail::AlertWindowHelpers::create (options), std::move (callback)); return detail::ConcreteScopedMessageBoxImpl::show (detail::AlertWindowHelpers::create (options), std::move (callback));
} }
//============================================================================== //==============================================================================

View file

@ -78,7 +78,7 @@ static int showNativeBoxUnmanaged (const MessageBoxOptions& opts,
ResultCodeMappingMode mode) ResultCodeMappingMode mode)
{ {
auto implementation = makeNativeMessageBoxWithMappedResult (opts, mode); auto implementation = makeNativeMessageBoxWithMappedResult (opts, mode);
return detail::ScopedMessageBoxImpl::showUnmanaged (std::move (implementation), cb); return detail::ConcreteScopedMessageBoxImpl::showUnmanaged (std::move (implementation), cb);
} }
#if JUCE_MODAL_LOOPS_PERMITTED #if JUCE_MODAL_LOOPS_PERMITTED
@ -152,7 +152,7 @@ void JUCE_CALLTYPE NativeMessageBox::showAsync (const MessageBoxOptions& options
ScopedMessageBox NativeMessageBox::showScopedAsync (const MessageBoxOptions& options, std::function<void (int)> callback) ScopedMessageBox NativeMessageBox::showScopedAsync (const MessageBoxOptions& options, std::function<void (int)> callback)
{ {
auto implementation = makeNativeMessageBoxWithMappedResult (options, ResultCodeMappingMode::alertWindow); auto implementation = makeNativeMessageBoxWithMappedResult (options, ResultCodeMappingMode::alertWindow);
return detail::ScopedMessageBoxImpl::show (std::move (implementation), std::move (callback)); return detail::ConcreteScopedMessageBoxImpl::show (std::move (implementation), std::move (callback));
} }
} // namespace juce } // namespace juce

View file

@ -34,6 +34,9 @@ namespace juce
class ScopedMessageBox class ScopedMessageBox
{ {
public: public:
/** @internal */
explicit ScopedMessageBox (std::shared_ptr<detail::ScopedMessageBoxImpl>);
/** Constructor */ /** Constructor */
ScopedMessageBox(); ScopedMessageBox();
@ -56,9 +59,6 @@ public:
void close(); void close();
private: private:
friend detail::ScopedMessageBoxImpl;
explicit ScopedMessageBox (std::shared_ptr<detail::ScopedMessageBoxImpl>);
std::shared_ptr<detail::ScopedMessageBoxImpl> impl; std::shared_ptr<detail::ScopedMessageBoxImpl> impl;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScopedMessageBox) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScopedMessageBox)