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
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue