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:
parent
557d690ff4
commit
9d1a6a3b28
22 changed files with 1304 additions and 1033 deletions
|
|
@ -4,6 +4,32 @@ JUCE breaking changes
|
|||
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
|
||||
------
|
||||
The minimum supported AAX library version has been bumped to 2.4.0 and the
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ target_sources(DemoRunner PRIVATE
|
|||
target_compile_definitions(DemoRunner PRIVATE
|
||||
PIP_JUCE_EXAMPLES_DIRECTORY_STRING="${JUCE_SOURCE_DIR}/examples"
|
||||
JUCE_ALLOW_STATIC_NULL_VARIABLES=0
|
||||
JUCE_CONTENT_SHARING=1
|
||||
JUCE_DEMO_RUNNER=1
|
||||
JUCE_PLUGINHOST_LV2=1
|
||||
JUCE_PLUGINHOST_VST3=1
|
||||
|
|
|
|||
|
|
@ -314,8 +314,7 @@ private:
|
|||
|
||||
SafePointer<CameraDemo> safeThis (this);
|
||||
|
||||
juce::ContentSharer::getInstance()->shareFiles ({url},
|
||||
[safeThis] (bool success, const String&) mutable
|
||||
messageBox = ContentSharer::shareFilesScoped ({ url }, [safeThis] (bool success, const String&)
|
||||
{
|
||||
if (safeThis)
|
||||
safeThis->sharingFinished (success, false);
|
||||
|
|
@ -355,8 +354,7 @@ private:
|
|||
|
||||
SafePointer<CameraDemo> safeThis (this);
|
||||
|
||||
juce::ContentSharer::getInstance()->shareFiles ({url},
|
||||
[safeThis] (bool success, const String&) mutable
|
||||
messageBox = ContentSharer::shareFilesScoped ({ url }, [safeThis] (bool success, const String&)
|
||||
{
|
||||
if (safeThis)
|
||||
safeThis->sharingFinished (success, true);
|
||||
|
|
|
|||
|
|
@ -462,7 +462,7 @@ private:
|
|||
}
|
||||
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)
|
||||
return;
|
||||
|
|
@ -489,7 +489,7 @@ private:
|
|||
Array<URL> urls;
|
||||
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)
|
||||
return;
|
||||
|
|
@ -519,7 +519,7 @@ private:
|
|||
|
||||
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)
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ juce_add_binary_data(AudioPluginHostData SOURCES
|
|||
|
||||
target_compile_definitions(AudioPluginHost PRIVATE
|
||||
JUCE_ALSA=1
|
||||
JUCE_CONTENT_SHARING=1
|
||||
JUCE_DIRECTSOUND=1
|
||||
JUCE_DISABLE_CAUTIOUS_PARAMETER_ID_CHECKING=1
|
||||
JUCE_PLUGINHOST_LADSPA=1
|
||||
|
|
|
|||
|
|
@ -1675,6 +1675,10 @@ private:
|
|||
|
||||
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* act = createActivityElement (*app);
|
||||
|
|
|
|||
|
|
@ -94,8 +94,8 @@ struct SystemJavaClassComparator
|
|||
|
||||
if ((! isSysClassA) && (! isSysClassB))
|
||||
{
|
||||
return DefaultElementComparator<bool>::compareElements (first != nullptr ? first->byteCode != nullptr : false,
|
||||
second != nullptr ? second->byteCode != nullptr : false);
|
||||
return DefaultElementComparator<bool>::compareElements (first != nullptr && first->byteCode != nullptr,
|
||||
second != nullptr && second->byteCode != nullptr);
|
||||
}
|
||||
|
||||
return DefaultElementComparator<bool>::compareElements (isSystemClass (first),
|
||||
|
|
@ -631,43 +631,17 @@ jobject FragmentOverlay::getNativeHandle()
|
|||
}
|
||||
|
||||
//==============================================================================
|
||||
class ActivityLauncher : public FragmentOverlay
|
||||
{
|
||||
public:
|
||||
ActivityLauncher (const LocalRef<jobject>& intentToUse,
|
||||
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))
|
||||
getEnv()->CallVoidMethod (getNativeHandle(), AndroidFragment.startActivityForResult,
|
||||
intent.get(), requestCode);
|
||||
}
|
||||
|
||||
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,
|
||||
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();
|
||||
auto* launcher = new ActivityLauncher (intent, requestCode);
|
||||
launcher->callback = [launcher, c = std::move (callback)] (auto&&... args)
|
||||
{
|
||||
NullCheckedInvocation::invoke (c, args...);
|
||||
delete launcher;
|
||||
};
|
||||
launcher->open();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
|
|||
|
|
@ -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 (checkCallingOrSelfPermission, "checkCallingOrSelfPermission", "(Ljava/lang/String;)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")
|
||||
#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 (putExtraParcelable, "putExtra", "(Ljava/lang/String;Landroid/os/Parcelable;)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 (setAction, "setAction", "(Ljava/lang/String;)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")
|
||||
#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) \
|
||||
METHOD (constructor, "<init>", "()V") \
|
||||
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) \
|
||||
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")
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
|
@ -1026,9 +1035,36 @@ private:
|
|||
|
||||
//==============================================================================
|
||||
// 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,
|
||||
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);
|
||||
String audioManagerGetProperty (const String& property);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
static ScopedMessageBox show (std::unique_ptr<ScopedMessageBoxInterface>&& native,
|
||||
|
|
@ -50,12 +59,12 @@ public:
|
|||
return 0;
|
||||
}
|
||||
|
||||
~ScopedMessageBoxImpl() override
|
||||
~ConcreteScopedMessageBoxImpl() override
|
||||
{
|
||||
cancelPendingUpdate();
|
||||
}
|
||||
|
||||
void close()
|
||||
void close() override
|
||||
{
|
||||
cancelPendingUpdate();
|
||||
nativeImplementation->close();
|
||||
|
|
@ -63,10 +72,10 @@ public:
|
|||
}
|
||||
|
||||
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::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->triggerAsyncUpdate();
|
||||
return result;
|
||||
|
|
@ -78,16 +87,16 @@ private:
|
|||
return local != nullptr ? local->runSync() : 0;
|
||||
}
|
||||
|
||||
explicit ScopedMessageBoxImpl (std::unique_ptr<ScopedMessageBoxInterface>&& p)
|
||||
: ScopedMessageBoxImpl (std::move (p), nullptr) {}
|
||||
explicit ConcreteScopedMessageBoxImpl (std::unique_ptr<ScopedMessageBoxInterface>&& p)
|
||||
: ConcreteScopedMessageBoxImpl (std::move (p), nullptr) {}
|
||||
|
||||
ScopedMessageBoxImpl (std::unique_ptr<ScopedMessageBoxInterface>&& p,
|
||||
ConcreteScopedMessageBoxImpl (std::unique_ptr<ScopedMessageBoxInterface>&& p,
|
||||
std::unique_ptr<ModalComponentManager::Callback>&& c)
|
||||
: callback (std::move (c)), nativeImplementation (std::move (p)) {}
|
||||
|
||||
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]
|
||||
{
|
||||
|
|
@ -117,7 +126,7 @@ private:
|
|||
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.
|
||||
*/
|
||||
std::shared_ptr<ScopedMessageBoxImpl> self;
|
||||
std::shared_ptr<ConcreteScopedMessageBoxImpl> self;
|
||||
};
|
||||
|
||||
} // namespace juce::detail
|
||||
|
|
|
|||
|
|
@ -26,247 +26,49 @@
|
|||
namespace juce
|
||||
{
|
||||
|
||||
#if JUCE_CONTENT_SHARING
|
||||
//==============================================================================
|
||||
class ContentSharer::PrepareImagesThread : private Thread
|
||||
ScopedMessageBox ContentSharer::shareFilesScoped (const Array<URL>& files,
|
||||
Callback callback,
|
||||
Component* parent)
|
||||
{
|
||||
public:
|
||||
PrepareImagesThread (ContentSharer& cs, const Array<Image>& imagesToUse,
|
||||
ImageFileFormat* imageFileFormatToUse)
|
||||
: Thread ("ContentSharer::PrepareImagesThread"),
|
||||
owner (cs),
|
||||
images (imagesToUse),
|
||||
imageFileFormat (imageFileFormatToUse == nullptr ? new PNGImageFormat()
|
||||
: imageFileFormatToUse),
|
||||
extension (imageFileFormat->getFormatName().toLowerCase())
|
||||
{
|
||||
startThread();
|
||||
auto impl = detail::ScopedContentSharerInterface::shareFiles (files, parent);
|
||||
return detail::ConcreteScopedContentSharerImpl::show (std::move (impl), std::move (callback));
|
||||
}
|
||||
|
||||
~PrepareImagesThread() override
|
||||
ScopedMessageBox ContentSharer::shareTextScoped (const String& text,
|
||||
Callback callback,
|
||||
Component* parent)
|
||||
{
|
||||
signalThreadShouldExit();
|
||||
waitForThreadToExit (10000);
|
||||
auto impl = detail::ScopedContentSharerInterface::shareText (text, parent);
|
||||
return detail::ConcreteScopedContentSharerImpl::show (std::move (impl), std::move (callback));
|
||||
}
|
||||
|
||||
private:
|
||||
void run() override
|
||||
ScopedMessageBox ContentSharer::shareImagesScoped (const Array<Image>& images,
|
||||
std::unique_ptr<ImageFileFormat> format,
|
||||
Callback callback,
|
||||
Component* parent)
|
||||
{
|
||||
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);
|
||||
auto impl = detail::ScopedContentSharerInterface::shareImages (images, std::move (format), parent);
|
||||
return detail::ConcreteScopedContentSharerImpl::show (std::move (impl), std::move (callback));
|
||||
}
|
||||
|
||||
finish();
|
||||
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));
|
||||
}
|
||||
|
||||
void finish()
|
||||
#if ! (JUCE_CONTENT_SHARING && (JUCE_IOS || JUCE_ANDROID))
|
||||
auto detail::ScopedContentSharerInterface::shareFiles (const Array<URL>&, Component*) -> std::unique_ptr<ScopedContentSharerInterface>
|
||||
{
|
||||
MessageManager::callAsync ([this]() { owner.filesToSharePrepared(); });
|
||||
return std::make_unique<detail::ScopedContentSharerInterface>();
|
||||
}
|
||||
|
||||
ContentSharer& owner;
|
||||
const Array<Image> images;
|
||||
std::unique_ptr<ImageFileFormat> imageFileFormat;
|
||||
String extension;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class ContentSharer::PrepareDataThread : private Thread
|
||||
auto detail::ScopedContentSharerInterface::shareText (const String&, Component*) -> std::unique_ptr<ScopedContentSharerInterface>
|
||||
{
|
||||
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
|
||||
void ContentSharer::startNewShare (std::function<void (bool, const String&)> callbackToUse)
|
||||
{
|
||||
// You should not start another sharing operation before the previous one is finished.
|
||||
// Forcibly stopping a previous sharing operation is rarely a good idea!
|
||||
jassert (pimpl == nullptr);
|
||||
pimpl.reset();
|
||||
|
||||
prepareDataThread = nullptr;
|
||||
prepareImagesThread = nullptr;
|
||||
|
||||
deleteTemporaryFiles();
|
||||
|
||||
// You need to pass a valid callback.
|
||||
jassert (callbackToUse);
|
||||
callback = std::move (callbackToUse);
|
||||
|
||||
pimpl.reset (createPimpl());
|
||||
return std::make_unique<detail::ScopedContentSharerInterface>();
|
||||
}
|
||||
#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
|
||||
|
|
|
|||
|
|
@ -23,21 +23,30 @@
|
|||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
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.
|
||||
|
||||
@tags{GUI}
|
||||
*/
|
||||
class JUCE_API ContentSharer : public DeletedAtShutdown
|
||||
class JUCE_API ContentSharer
|
||||
{
|
||||
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
|
||||
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
|
||||
standard way the sharing targets report if the sharing operation
|
||||
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,
|
||||
std::function<void (bool /*success*/, const String& /*error*/)> callback);
|
||||
[[nodiscard]] static ScopedMessageBox shareFilesScoped (const Array<URL>& files,
|
||||
Callback callback,
|
||||
Component* parent = nullptr);
|
||||
|
||||
/** Shares the given text.
|
||||
|
||||
|
|
@ -60,9 +76,16 @@ public:
|
|||
Sadly on Android the returned success flag may be wrong as there is no
|
||||
standard way the sharing targets report if the sharing operation
|
||||
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,
|
||||
std::function<void (bool /*success*/, const String& /*error*/)> callback);
|
||||
[[nodiscard]] static ScopedMessageBox shareTextScoped (const String& text,
|
||||
Callback callback,
|
||||
Component* parent = nullptr);
|
||||
|
||||
/** 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
|
||||
|
|
@ -84,10 +107,20 @@ public:
|
|||
Sadly on Android the returned success flag may be wrong as there is no
|
||||
standard way the sharing targets report if the sharing operation
|
||||
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,
|
||||
std::function<void (bool /*success*/, const String& /*error*/)> callback,
|
||||
ImageFileFormat* imageFileFormatToUse = nullptr);
|
||||
[[nodiscard]] static ScopedMessageBox shareImagesScoped (const Array<Image>& images,
|
||||
std::unique_ptr<ImageFileFormat> format,
|
||||
Callback callback,
|
||||
Component* parent = nullptr);
|
||||
|
||||
/** 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
|
||||
|
|
@ -97,47 +130,16 @@ public:
|
|||
Sadly on Android the returned success flag may be wrong as there is no
|
||||
standard way the sharing targets report if the sharing operation
|
||||
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,
|
||||
std::function<void (bool /*success*/, const String& /*error*/)> callback);
|
||||
|
||||
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&);
|
||||
[[nodiscard]] static ScopedMessageBox shareDataScoped (const MemoryBlock& mb,
|
||||
Callback callback,
|
||||
Component* parent = nullptr);
|
||||
};
|
||||
|
||||
} // namespace juce
|
||||
|
|
|
|||
|
|
@ -122,6 +122,8 @@
|
|||
#include "detail/juce_ToolbarItemDragAndDropOverlayComponent.h"
|
||||
#include "detail/juce_ScopedMessageBoxInterface.h"
|
||||
#include "detail/juce_ScopedMessageBoxImpl.h"
|
||||
#include "detail/juce_ScopedContentSharerInterface.h"
|
||||
#include "detail/juce_ScopedContentSharerImpl.h"
|
||||
#include "detail/juce_WindowingHelpers.h"
|
||||
#include "detail/juce_AlertWindowHelpers.h"
|
||||
#include "detail/juce_TopLevelWindowManager.h"
|
||||
|
|
@ -143,6 +145,7 @@
|
|||
#include "native/accessibility/juce_ios_Accessibility.mm"
|
||||
#include "native/juce_ios_Windowing.mm"
|
||||
#include "native/juce_ios_NativeMessageBox.mm"
|
||||
#include "native/juce_ios_NativeModalWrapperComponent.h"
|
||||
#include "native/juce_ios_FileChooser.mm"
|
||||
|
||||
#if JUCE_CONTENT_SHARING
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -26,25 +26,58 @@
|
|||
namespace juce
|
||||
{
|
||||
|
||||
class ContentSharer::ContentSharerNativeImpl : public ContentSharer::Pimpl,
|
||||
private Component
|
||||
class NativeScopedContentSharerInterface : public detail::ScopedContentSharerInterface,
|
||||
public detail::NativeModalWrapperComponent
|
||||
{
|
||||
public:
|
||||
ContentSharerNativeImpl (ContentSharer& cs)
|
||||
: owner (cs)
|
||||
NativeScopedContentSharerInterface (Component* parentIn, NSUniquePtr<NSArray> itemsIn)
|
||||
: parent (parentIn), items (std::move (itemsIn)) {}
|
||||
|
||||
void runAsync (std::function<void (bool, const String&)> callback) override
|
||||
{
|
||||
static PopoverDelegateClass cls;
|
||||
popoverDelegate.reset ([cls.createInstance() init]);
|
||||
if ([items.get() count] == 0)
|
||||
{
|
||||
jassertfalse;
|
||||
NullCheckedInvocation::invoke (callback, false, "No valid items found for sharing.");
|
||||
return;
|
||||
}
|
||||
|
||||
~ContentSharerNativeImpl() override
|
||||
controller.reset ([[UIActivityViewController alloc] initWithActivityItems: items.get()
|
||||
applicationActivities: nil]);
|
||||
|
||||
controller.get().excludedActivityTypes = nil;
|
||||
|
||||
controller.get().completionWithItemsHandler = ^([[maybe_unused]] UIActivityType type, BOOL completed,
|
||||
[[maybe_unused]] NSArray* returnedItems, NSError* error)
|
||||
{
|
||||
const auto errorDescription = error != nil ? nsStringToJuce ([error localizedDescription])
|
||||
: String();
|
||||
exitModalState (0);
|
||||
|
||||
NullCheckedInvocation::invoke (callback, completed && errorDescription.isEmpty(), errorDescription);
|
||||
};
|
||||
|
||||
displayNativeWindowModally (parent);
|
||||
|
||||
enterModalState (true, nullptr, false);
|
||||
}
|
||||
|
||||
void shareFiles (const Array<URL>& files) override
|
||||
void close() override
|
||||
{
|
||||
auto urls = [NSMutableArray arrayWithCapacity: (NSUInteger) files.size()];
|
||||
[controller.get() dismissViewControllerAnimated: YES completion: nil];
|
||||
}
|
||||
|
||||
private:
|
||||
UIViewController* getViewController() const override { return controller.get(); }
|
||||
|
||||
Component* parent = nullptr;
|
||||
NSUniquePtr<UIActivityViewController> controller;
|
||||
NSUniquePtr<NSArray> items;
|
||||
};
|
||||
|
||||
auto detail::ScopedContentSharerInterface::shareFiles (const Array<URL>& files, Component* parent) -> std::unique_ptr<ScopedContentSharerInterface>
|
||||
{
|
||||
NSUniquePtr<NSMutableArray> urls ([[NSMutableArray arrayWithCapacity: (NSUInteger) files.size()] retain]);
|
||||
|
||||
for (const auto& f : files)
|
||||
{
|
||||
|
|
@ -77,128 +110,16 @@ public:
|
|||
}
|
||||
|
||||
if (nativeFilePath != nil)
|
||||
[urls addObject: [NSURL fileURLWithPath: nativeFilePath]];
|
||||
[urls.get() addObject: [NSURL fileURLWithPath: nativeFilePath]];
|
||||
}
|
||||
|
||||
share (urls);
|
||||
return std::make_unique<NativeScopedContentSharerInterface> (parent, std::move (urls));
|
||||
}
|
||||
|
||||
void shareText (const String& text) override
|
||||
auto detail::ScopedContentSharerInterface::shareText (const String& text, Component* parent) -> std::unique_ptr<ScopedContentSharerInterface>
|
||||
{
|
||||
auto array = [NSArray arrayWithObject: juceStringToNS (text)];
|
||||
share (array);
|
||||
}
|
||||
|
||||
private:
|
||||
void share (NSArray* items)
|
||||
{
|
||||
if ([items count] == 0)
|
||||
{
|
||||
jassertfalse;
|
||||
owner.sharingFinished (false, "No valid items found for sharing.");
|
||||
return;
|
||||
}
|
||||
|
||||
controller.reset ([[UIActivityViewController alloc] initWithActivityItems: items
|
||||
applicationActivities: nil]);
|
||||
|
||||
controller.get().excludedActivityTypes = nil;
|
||||
|
||||
controller.get().completionWithItemsHandler = ^([[maybe_unused]] UIActivityType type, BOOL completed,
|
||||
[[maybe_unused]] NSArray* returnedItems, NSError* error)
|
||||
{
|
||||
succeeded = completed;
|
||||
|
||||
if (error != nil)
|
||||
errorDescription = nsStringToJuce ([error localizedDescription]);
|
||||
|
||||
exitModalState (0);
|
||||
};
|
||||
|
||||
controller.get().modalTransitionStyle = UIModalTransitionStyleCoverVertical;
|
||||
|
||||
auto bounds = Desktop::getInstance().getDisplays().getPrimaryDisplay()->userArea;
|
||||
setBounds (bounds);
|
||||
|
||||
setAlwaysOnTop (true);
|
||||
setVisible (true);
|
||||
addToDesktop (0);
|
||||
|
||||
enterModalState (true,
|
||||
ModalCallbackFunction::create ([this] (int)
|
||||
{
|
||||
owner.sharingFinished (succeeded, errorDescription);
|
||||
}),
|
||||
false);
|
||||
}
|
||||
|
||||
static bool isIPad()
|
||||
{
|
||||
return [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void parentHierarchyChanged() override
|
||||
{
|
||||
auto* newPeer = dynamic_cast<UIViewComponentPeer*> (getPeer());
|
||||
|
||||
if (peer != newPeer)
|
||||
{
|
||||
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<NSObject<UIPopoverPresentationControllerDelegate>> popoverDelegate;
|
||||
|
||||
bool succeeded = false;
|
||||
String errorDescription;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
ContentSharer::Pimpl* ContentSharer::createPimpl()
|
||||
{
|
||||
return new ContentSharerNativeImpl (*this);
|
||||
NSUniquePtr<NSArray> array ([[NSArray arrayWithObject: juceStringToNS (text)] retain]);
|
||||
return std::make_unique<NativeScopedContentSharerInterface> (parent, std::move (array));
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
|
|
|
|||
|
|
@ -39,8 +39,9 @@ namespace juce
|
|||
#define JUCE_DEPRECATION_IGNORED 1
|
||||
#endif
|
||||
|
||||
//==============================================================================
|
||||
class FileChooser::Native : public FileChooser::Pimpl,
|
||||
public Component,
|
||||
public detail::NativeModalWrapperComponent,
|
||||
public AsyncUpdater,
|
||||
public std::enable_shared_from_this<Native>
|
||||
{
|
||||
|
|
@ -56,11 +57,6 @@ public:
|
|||
return result;
|
||||
}
|
||||
|
||||
~Native() override
|
||||
{
|
||||
exitModalState (0);
|
||||
}
|
||||
|
||||
void launch() override
|
||||
{
|
||||
jassert (shared_from_this() != nullptr);
|
||||
|
|
@ -90,24 +86,6 @@ public:
|
|||
#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();
|
||||
|
|
@ -189,6 +167,8 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
UIViewController* getViewController() const override { return controller.get(); }
|
||||
|
||||
Native (FileChooser& fileChooser, int flags)
|
||||
: owner (fileChooser)
|
||||
{
|
||||
|
|
@ -241,40 +221,9 @@ private:
|
|||
[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);
|
||||
}
|
||||
displayNativeWindowModally (fileChooser.parent);
|
||||
}
|
||||
|
||||
void passResultsToInitiator (Array<URL> urls)
|
||||
|
|
@ -363,7 +312,6 @@ private:
|
|||
FileChooser& owner;
|
||||
NSUniquePtr<NSObject<UIDocumentPickerDelegate>> delegate;
|
||||
NSUniquePtr<FileChooserControllerClass> controller;
|
||||
UIViewComponentPeer* peer = nullptr;
|
||||
|
||||
//==============================================================================
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Native)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -37,7 +37,7 @@ static juce_wchar getDefaultPasswordChar() noexcept
|
|||
|
||||
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())
|
||||
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));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ static int showNativeBoxUnmanaged (const MessageBoxOptions& opts,
|
|||
ResultCodeMappingMode 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
|
||||
|
|
@ -152,7 +152,7 @@ void JUCE_CALLTYPE NativeMessageBox::showAsync (const MessageBoxOptions& options
|
|||
ScopedMessageBox NativeMessageBox::showScopedAsync (const MessageBoxOptions& options, std::function<void (int)> callback)
|
||||
{
|
||||
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
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ namespace juce
|
|||
class ScopedMessageBox
|
||||
{
|
||||
public:
|
||||
/** @internal */
|
||||
explicit ScopedMessageBox (std::shared_ptr<detail::ScopedMessageBoxImpl>);
|
||||
|
||||
/** Constructor */
|
||||
ScopedMessageBox();
|
||||
|
||||
|
|
@ -56,9 +59,6 @@ public:
|
|||
void close();
|
||||
|
||||
private:
|
||||
friend detail::ScopedMessageBoxImpl;
|
||||
explicit ScopedMessageBox (std::shared_ptr<detail::ScopedMessageBoxImpl>);
|
||||
|
||||
std::shared_ptr<detail::ScopedMessageBoxImpl> impl;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScopedMessageBox)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue