1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-09 23:34:20 +00:00
JUCE/modules/juce_gui_extra/misc/juce_WebBrowserComponent.cpp

729 lines
22 KiB
C++

/*
==============================================================================
This file is part of the JUCE framework.
Copyright (c) Raw Material Software Limited
JUCE is an open source framework subject to commercial or open source
licensing.
By downloading, installing, or using the JUCE framework, or combining the
JUCE framework with any other source code, object code, content or any other
copyrightable work, you agree to the terms of the JUCE End User Licence
Agreement, and all incorporated terms including the JUCE Privacy Policy and
the JUCE Website Terms of Service, as applicable, which will bind you. If you
do not agree to the terms of these agreements, we will not license the JUCE
framework to you, and you must discontinue the installation or download
process and cease use of the JUCE framework.
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
Or:
You may also use this code under the terms of the AGPLv3:
https://www.gnu.org/licenses/agpl-3.0.en.html
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
namespace juce
{
#if JUCE_WEB_BROWSER || DOXYGEN
bool WebBrowserComponent::pageAboutToLoad ([[maybe_unused]] const String& newURL) { return true; }
void WebBrowserComponent::pageFinishedLoading ([[maybe_unused]] const String& url) {}
bool WebBrowserComponent::pageLoadHadNetworkError ([[maybe_unused]] const String& errorInfo) { return true; }
void WebBrowserComponent::windowCloseRequest() {}
void WebBrowserComponent::newWindowAttemptingToLoad ([[maybe_unused]] const String& newURL) {}
// At least this much code has to be injected as user script, since the native backend functions
// rely on the presence of the `window.__JUCE__.backend.emitByBackend()` function. The rest can be
// optionally imported as JS.
static constexpr const char* lowLevelIntegrationsScript = R"(
if (
typeof window.__JUCE__ !== "undefined" &&
typeof window.__JUCE__.getAndroidUserScripts !== "undefined" &&
typeof window.inAndroidUserScriptEval === "undefined"
) {
window.inAndroidUserScriptEval = true;
eval(window.__JUCE__.getAndroidUserScripts());
delete window.inAndroidUserScriptEval;
}
{
if (typeof window.__JUCE__ === "undefined") {
console.warn(
"The 'window.__JUCE__' object is undefined." +
" Native integration features will not work." +
" Defining a placeholder 'window.__JUCE__' object."
);
window.__JUCE__ = {
postMessage: function () {},
};
}
if (typeof window.__JUCE__.initialisationData === "undefined") {
window.__JUCE__.initialisationData = {
__juce__platform: [],
__juce__functions: [],
__juce__registeredGlobalEventIds: [],
__juce__sliders: [],
__juce__toggles: [],
__juce__comboBoxes: [],
};
}
class ListenerList {
constructor() {
this.listeners = new Map();
this.listenerId = 0;
}
addListener(fn) {
const newListenerId = this.listenerId++;
this.listeners.set(newListenerId, fn);
return newListenerId;
}
removeListener(id) {
if (this.listeners.has(id)) {
this.listeners.delete(id);
}
}
callListeners(payload) {
for (const [, value] of this.listeners) {
value(payload);
}
}
}
class EventListenerList {
constructor() {
this.eventListeners = new Map();
}
addEventListener(eventId, fn) {
if (!this.eventListeners.has(eventId))
this.eventListeners.set(eventId, new ListenerList());
const id = this.eventListeners.get(eventId).addListener(fn);
return [eventId, id];
}
removeEventListener([eventId, id]) {
if (this.eventListeners.has(eventId)) {
this.eventListeners.get(eventId).removeListener(id);
}
}
emitEvent(eventId, object) {
if (this.eventListeners.has(eventId))
this.eventListeners.get(eventId).callListeners(object);
}
}
class Backend {
constructor() {
this.listeners = new EventListenerList();
}
addEventListener(eventId, fn) {
return this.listeners.addEventListener(eventId, fn);
}
removeEventListener([eventId, id]) {
this.listeners.removeEventListener([eventId, id]);
}
emitEvent(eventId, object) {
window.__JUCE__.postMessage(
JSON.stringify({ eventId: eventId, payload: object })
);
}
emitByBackend(eventId, object) {
this.listeners.emitEvent(eventId, JSON.parse(object));
}
}
if (typeof window.__JUCE__.backend === "undefined")
window.__JUCE__.backend = new Backend();
}
)";
static void evaluationHandler (WebBrowserComponent::EvaluationResult r)
{
if (r.getResult() == nullptr)
{
// The unsupported return type means a successful Javascript evaluation that yielded a
// result that cannot be translated and returned to native code such as a Promise.
jassert (r.getError()->type == WebBrowserComponent::EvaluationResult::Error::Type::unsupportedReturnType);
DBG (r.getError()->message);
}
}
static StringArray getUserScriptsForInitialisationData (const std::vector<std::pair<String, var>>& data)
{
std::map<String, StringArray> sortedData;
for (const auto& [key, value] : data)
sortedData[key].add (JSON::toString (value));
StringArray result;
for (const auto& [key, values] : sortedData)
result.add ("window.__JUCE__.initialisationData." + key + " = ["
+ values.joinIntoString (",") + "];");
return result;
}
template <typename T>
static auto getCommaSeparatedList (const T& mapOfStringable)
{
StringArray keys;
for (auto it : mapOfStringable)
keys.add (it.first.toString().quoted());
return keys.joinIntoString (",");
}
struct NativeEvents
{
NativeEvents() = delete;
struct NativeEvent
{
String eventId;
var payload;
static constexpr std::optional<int> marshallingVersion = std::nullopt;
template <typename Archive, typename Item>
static void serialise (Archive& archive, Item& item)
{
archive (named ("eventId", item.eventId),
named ("payload", item.payload));
}
};
struct Invoke
{
String name;
var params;
int64 resultId;
static constexpr std::optional<int> marshallingVersion = std::nullopt;
template <typename Archive, typename Item>
static void serialise (Archive& archive, Item& item)
{
archive (named ("name", item.name),
named ("params", item.params),
named ("resultId", item.resultId));
}
static inline const Identifier eventId { "__juce__invoke" };
static inline const Identifier completeId { "__juce__complete" };
};
};
class NativeFunctionsProvider : public OptionsBuilder<WebBrowserComponent::Options>,
private AsyncUpdater
{
public:
explicit NativeFunctionsProvider (WebBrowserComponent& ownerIn)
: owner (ownerIn)
{
}
WebBrowserComponent::Options buildOptions (const WebBrowserComponent::Options& initialOptions) override
{
nativeFunctions = initialOptions.getNativeFunctions();
if (nativeFunctions.empty())
return initialOptions;
auto options = initialOptions.withNativeIntegrationEnabled()
.withEventListener (NativeEvents::Invoke::eventId,
[this] (const auto& object)
{
handleNativeFunctionCall (object);
});
for (auto it : nativeFunctions)
options = options.withInitialisationData ("__juce__functions", it.first.toString());
return options;
}
private:
void handleAsyncUpdate() override
{
const auto popFront = [&] (auto&& callback)
{
functionCompletionsMutex.lock();
ErasedScopeGuard unlocker { [&] { functionCompletionsMutex.unlock(); } };
if (! functionCompletions.empty())
{
auto completion = functionCompletions.front();
functionCompletions.pop_front();
unlocker.reset();
callback (completion);
return true;
}
else
{
return false;
}
};
while (popFront ([&] (const auto& c) { emitCompletionEvent (c.first, c.second); }))
;
}
void handleNativeFunctionCall (const var& object)
{
const auto invocation = FromVar::convert<NativeEvents::Invoke> (object);
if (! invocation.has_value())
{
jassertfalse;
return;
}
auto it = nativeFunctions.find (invocation->name);
if (it == nativeFunctions.end())
{
jassertfalse;
return;
}
jassert (invocation->params.isArray());
it->second (*invocation->params.getArray(),
[this, resultId = invocation->resultId] (auto result)
{
completeNativeFunctionCall (resultId, result);
});
}
void completeNativeFunctionCall (int64 resultId, const var& object)
{
if (MessageManager::getInstance()->isThisTheMessageThread())
{
emitCompletionEvent (resultId, object);
}
else
{
const std::lock_guard lock { functionCompletionsMutex };
functionCompletions.push_back (std::make_pair (resultId, object));
triggerAsyncUpdate();
}
}
void emitCompletionEvent (int64 resultId, const var& object)
{
DynamicObject::Ptr eventObject { new DynamicObject() };
eventObject->setProperty ("promiseId", resultId);
eventObject->setProperty ("result", object);
jassert (owner.isVisible());
owner.emitEventIfBrowserIsVisible (NativeEvents::Invoke::completeId, eventObject.get());
}
WebBrowserComponent& owner;
std::map<Identifier, WebBrowserComponent::NativeFunction> nativeFunctions;
std::deque<std::pair<int64, var>> functionCompletions;
std::mutex functionCompletionsMutex;
JUCE_DECLARE_NON_COPYABLE (NativeFunctionsProvider)
JUCE_DECLARE_NON_MOVEABLE (NativeFunctionsProvider)
};
class NativeEventListeners
{
public:
void addListener (const Identifier& eventId, std::function<void (const var&)> handler)
{
listeners.push_back (std::move (handler));
listenerMap[eventId].add (&listeners.back());
}
void emit (const Identifier& eventId, const var& object)
{
if (const auto& it = listenerMap.find (eventId); it != listenerMap.end())
it->second.call ([&object] (auto& l) { l (object); });
}
private:
std::list<WebBrowserComponent::NativeEventListener> listeners;
std::map<Identifier, ListenerList<WebBrowserComponent::NativeEventListener>> listenerMap;
};
class WebBrowserComponent::Impl
{
public:
Impl (WebBrowserComponent& ownerIn, const Options& optionsIn)
: owner (ownerIn),
options ([&]
{
makeFunctionsProviderIfNecessary (nativeFunctionsProvider, *this, optionsIn);
if (nativeFunctionsProvider.has_value())
return getOptions (optionsIn).withOptionsFrom (*nativeFunctionsProvider);
return getOptions (optionsIn);
}())
{
auto userScripts = options.getUserScripts();
// Adding user scripts in reverse order of depending on each other. The most depended on
// comes last.
for (const auto& eventListener : options.getEventListeners())
{
userScripts.insert (0,
"window.__JUCE__.initialisationData.__juce__registeredGlobalEventIds = ["
+ getCommaSeparatedList (options.getEventListeners()) + "];");
addPermanentEventListener (eventListener.first, eventListener.second);
}
for (const auto& script : getUserScriptsForInitialisationData (options.getInitialisationData()))
userScripts.insert (0, script);
userScripts.insert (0, lowLevelIntegrationsScript);
platform = createAndInitPlatformDependentPart (*this, options, userScripts);
}
void emitEvent (const Identifier& eventId, const var& object)
{
// The object parameter is serialised into a string and used as a parameter to a Javascript
// function call. During this JS parameter substitution, control character escape sequences
// would be interpreted as the control characters themselves. So we need to escape anything
// that was escaped.
//
// We also need to escape the ' character since we use this to delimit the parameter string
// to emitByBackend.
const auto objectAsString = JSON::toString (object, true);
const auto escaped = objectAsString.replace ("\\", "\\\\").replace ("'", "\\'");
evaluateJavascript ("window.__JUCE__.backend.emitByBackend(" + eventId.toString().quoted() + ", "
+ escaped.quoted ('\'')
+ ");", evaluationHandler);
}
void goToURL (const String& url, const StringArray* headers, const MemoryBlock* postData)
{
platform->goToURL (url, headers, postData);
}
void stop()
{
platform->stop();
}
void goBack()
{
platform->goBack();
owner.lastURL.clear();
owner.blankPageShown = false;
}
void goForward()
{
owner.lastURL.clear();
platform->goForward();
}
void refresh()
{
platform->refresh();
}
void evaluateJavascript (const String& script, EvaluationCallback callback)
{
platform->evaluateJavascript (script, std::move (callback));
}
void setSize (int width, int height)
{
platform->setWebViewSize (width, height);
}
void checkWindowAssociation()
{
platform->checkWindowAssociation();
}
void fallbackPaint (Graphics& webBrowserComponentContext)
{
platform->fallbackPaint (webBrowserComponentContext);
}
void focusGainedWithDirection (FocusChangeType type, FocusChangeDirection dir)
{
platform->focusGainedWithDirection (type, dir);
}
struct Platform;
private:
class PlatformInterface
{
public:
PlatformInterface() = default;
virtual ~PlatformInterface() = default;
virtual void goToURL (const String&, const StringArray*, const MemoryBlock*) = 0;
virtual void goBack() = 0;
virtual void goForward() = 0;
virtual void stop() = 0;
virtual void refresh() = 0;
virtual void evaluateJavascript (const String&, WebBrowserComponent::EvaluationCallback) = 0;
virtual void setWebViewSize (int, int) = 0;
virtual void checkWindowAssociation() = 0;
virtual void focusGainedWithDirection (FocusChangeType, FocusChangeDirection) {}
virtual void fallbackPaint (Graphics&) {}
};
static void makeFunctionsProviderIfNecessary (std::optional<NativeFunctionsProvider>& provider,
WebBrowserComponent::Impl& impl,
const WebBrowserComponent::Options& options)
{
if (! options.getNativeFunctions().empty())
provider.emplace (impl.owner);
}
static Options getOptions (const Options& optionsIn)
{
const auto os = SystemStats::getOperatingSystemType();
const auto platformString = [&]
{
using OsType = SystemStats::OperatingSystemType;
if ((os & OsType::MacOSX) != 0)
return "macos";
if ((os & OsType::iOS) != 0)
return "ios";
if ((os & OsType::Windows) != 0)
return "windows";
if ((os & OsType::Android) != 0)
return "android";
if ((os & OsType::Linux) != 0)
return "linux";
return "";
}();
return optionsIn.withInitialisationData ("__juce__platform", platformString);
}
void addPermanentEventListener (const Identifier& eventId, NativeEventListener listener)
{
nativeEventListeners.addListener (eventId, std::move (listener));
}
std::optional<Resource> handleResourceRequest (const String& url)
{
if (resourceProvider != nullptr)
return resourceProvider (url);
return std::nullopt;
}
void handleNativeEvent (const var& message)
{
const auto event = FromVar::convert<NativeEvents::NativeEvent> (message);
if (! event.has_value())
{
jassertfalse;
return;
}
nativeEventListeners.emit (event->eventId, event->payload);
}
static std::unique_ptr<PlatformInterface> createAndInitPlatformDependentPart (WebBrowserComponent::Impl&,
const Options&,
const StringArray&);
WebBrowserComponent& owner;
std::optional<NativeFunctionsProvider> nativeFunctionsProvider;
Options options;
ResourceProvider resourceProvider { options.getResourceProvider() };
NativeEventListeners nativeEventListeners;
std::unique_ptr<PlatformInterface> platform;
};
//==============================================================================
WebBrowserComponent::WebBrowserComponent (const Options& options)
: impl (std::make_unique<Impl> (*this, options)),
unloadPageWhenHidden (! options.keepsPageLoadedWhenBrowserIsHidden())
{
setOpaque (true);
#if JUCE_LINUX
ignoreUnused (blankPageShown);
ignoreUnused (unloadPageWhenHidden);
#endif
for (const auto& l : options.getLifetimeListeners())
lifetimeListeners.add (l);
lifetimeListeners.call (&WebViewLifetimeListener::webViewConstructed, this);
}
WebBrowserComponent::~WebBrowserComponent()
{
lifetimeListeners.call (&WebViewLifetimeListener::webViewDestructed, this);
}
void WebBrowserComponent::emitEventIfBrowserIsVisible (const Identifier& eventId, const var& object)
{
if (isVisible())
impl->emitEvent (eventId, object);
}
const String& WebBrowserComponent::getResourceProviderRoot()
{
const auto os = SystemStats::getOperatingSystemType();
using OsType = SystemStats::OperatingSystemType;
if ( (os & OsType::MacOSX) != 0
|| (os & OsType::iOS) != 0
|| (os & OsType::Linux) != 0)
{
static String backendUrl { "juce://juce.backend/" };
return backendUrl;
}
if ( (os & OsType::Windows) != 0
|| (os & OsType::Android) != 0)
{
static String backendUrl { "https://juce.backend/" };
return backendUrl;
}
static String emptyUrl { "" };
return emptyUrl;
}
void WebBrowserComponent::goToURL (const String& url,
const StringArray* headers,
const MemoryBlock* postData)
{
lastURL = url;
if (headers != nullptr)
lastHeaders = *headers;
else
lastHeaders.clear();
if (postData != nullptr)
lastPostData = *postData;
else
lastPostData.reset();
impl->goToURL (url, headers, postData);
blankPageShown = false;
}
void WebBrowserComponent::stop()
{
impl->stop();
}
void WebBrowserComponent::goBack()
{
impl->goBack();
lastURL.clear();
blankPageShown = false;
}
void WebBrowserComponent::goForward()
{
lastURL.clear();
impl->goForward();
}
void WebBrowserComponent::refresh()
{
impl->refresh();
}
void WebBrowserComponent::paint (Graphics& g)
{
impl->fallbackPaint (g);
}
void WebBrowserComponent::parentHierarchyChanged()
{
impl->checkWindowAssociation();
}
void WebBrowserComponent::visibilityChanged()
{
impl->checkWindowAssociation();
}
void WebBrowserComponent::resized()
{
impl->setSize (getWidth(), getHeight());
}
void WebBrowserComponent::reloadLastURL()
{
const auto ptrOrNullIfEmpty = [] (auto& value)
{
return ! value.isEmpty() ? &value : nullptr;
};
if (lastURL.isNotEmpty())
{
goToURL (lastURL, ptrOrNullIfEmpty (lastHeaders), ptrOrNullIfEmpty (lastPostData));
lastURL.clear();
}
}
void WebBrowserComponent::evaluateJavascript (const String& script, EvaluationCallback callback)
{
impl->evaluateJavascript (script, std::move (callback));
}
void WebBrowserComponent::focusGainedWithDirection (FocusChangeType type,
FocusChangeDirection direction)
{
impl->focusGainedWithDirection (type, direction);
}
#endif
} // namespace juce