mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
WebBrowserComponent: Improve native integrations
This commit is contained in:
parent
624fed2c7f
commit
5f638157f7
63 changed files with 5155 additions and 1288 deletions
|
|
@ -115,13 +115,98 @@ static NSMutableURLRequest* getRequestForURL (const String& url, const StringArr
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
static var fromObject (id object)
|
||||
{
|
||||
// An undefined var serialises to 'undefined' i.e. an expression not returning a value
|
||||
if (object == nil)
|
||||
return var::undefined();
|
||||
|
||||
if ([object isKindOfClass:[NSNumber class]])
|
||||
{
|
||||
// The object returned by evaluateJavaScript is a __NSCFBoolean*, which is a private class
|
||||
// to the framework, but a handle to this class can be obtained through @YES. When cast to
|
||||
// an NSNumber this object would have the wrong type encoding, so [number objCType]; would
|
||||
// return 'c' instead of 'B', hence that approach wouldn't work.
|
||||
if ([object isKindOfClass: [@YES class]])
|
||||
return static_cast<NSNumber*> (object).boolValue == YES ? true : false;
|
||||
|
||||
return static_cast<NSNumber*> (object).doubleValue;
|
||||
}
|
||||
|
||||
if ([object isKindOfClass:[NSString class]])
|
||||
return nsStringToJuce (object);
|
||||
|
||||
if ([object isKindOfClass:[NSArray class]])
|
||||
{
|
||||
Array<var> result;
|
||||
|
||||
auto* array = static_cast<NSArray*> (object);
|
||||
|
||||
for (id elem in array)
|
||||
result.add (fromObject (elem));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if ([object isKindOfClass:[NSDictionary class]])
|
||||
{
|
||||
const auto* dict = static_cast<NSDictionary*> (object);
|
||||
|
||||
DynamicObject::Ptr result (new DynamicObject());
|
||||
|
||||
for (id key in dict)
|
||||
result->setProperty (nsStringToJuce (key), fromObject ([dict objectForKey:key]));
|
||||
|
||||
return result.get();
|
||||
}
|
||||
|
||||
if ([object isKindOfClass:[NSDate class]])
|
||||
{
|
||||
JUCE_AUTORELEASEPOOL
|
||||
{
|
||||
auto* date = static_cast<NSDate*> (object);
|
||||
auto* formatter = [[NSDateFormatter alloc] init];
|
||||
const auto javascriptDateFormatString = @"yyyy'-'MM'-'dd'T'HH':'mm':'ss.SSS'Z'";
|
||||
[formatter setDateFormat: javascriptDateFormatString];
|
||||
[formatter setTimeZone: [NSTimeZone timeZoneWithName: @"UTC"]];
|
||||
NSString* dateString = [formatter stringFromDate: date];
|
||||
return nsStringToJuce (dateString);
|
||||
}
|
||||
}
|
||||
|
||||
// Returning a Void var, which serialises to 'null'
|
||||
if ([object isKindOfClass:[NSNull class]])
|
||||
return {};
|
||||
|
||||
jassertfalse;
|
||||
return {};
|
||||
}
|
||||
|
||||
using LastFocusChange = std::optional<Component::FocusChangeDirection>;
|
||||
|
||||
static const char* lastFocusChangeMemberName = "lastFocusChangeHandle";
|
||||
|
||||
[[maybe_unused]] static void setLastFocusChangeHandle (id instance, LastFocusChange* object)
|
||||
{
|
||||
object_setInstanceVariable (instance, lastFocusChangeMemberName, object);
|
||||
}
|
||||
|
||||
[[maybe_unused]] static LastFocusChange* getLastFocusChangeHandle (id instance)
|
||||
{
|
||||
return getIvar<LastFocusChange*> (instance, lastFocusChangeMemberName);
|
||||
}
|
||||
|
||||
#if JUCE_MAC
|
||||
template <class WebViewClass>
|
||||
struct WebViewKeyEquivalentResponder final : public ObjCClass<WebViewClass>
|
||||
{
|
||||
using Base = ObjCClass<WebViewClass>;
|
||||
|
||||
WebViewKeyEquivalentResponder()
|
||||
: ObjCClass<WebViewClass> ("WebViewKeyEquivalentResponder_")
|
||||
: Base ("WebViewKeyEquivalentResponder_")
|
||||
{
|
||||
this->template addIvar<LastFocusChange*> (lastFocusChangeMemberName);
|
||||
|
||||
this->addMethod (@selector (performKeyEquivalent:),
|
||||
[] (id self, SEL selector, NSEvent* event)
|
||||
{
|
||||
|
|
@ -156,8 +241,48 @@ struct WebViewKeyEquivalentResponder final : public ObjCClass<WebViewClass>
|
|||
return sendAction (@selector (selectAll:));
|
||||
}
|
||||
|
||||
return ObjCClass<WebViewClass>::template sendSuperclassMessage<BOOL> (self, selector, event);
|
||||
return Base::template sendSuperclassMessage<BOOL> (self, selector, event);
|
||||
});
|
||||
|
||||
this->addMethod (@selector (resignFirstResponder),
|
||||
[] (id self, SEL selector)
|
||||
{
|
||||
const auto result = Base::template sendSuperclassMessage<BOOL> (self, selector);
|
||||
|
||||
auto* focusChangeTypeHandle = getLastFocusChangeHandle (self);
|
||||
jassert (focusChangeTypeHandle != nullptr); // Forgot to call setLastFocusChangeHandle?
|
||||
focusChangeTypeHandle->emplace (Component::FocusChangeDirection::unknown);
|
||||
|
||||
auto* currentEvent = [NSApp currentEvent];
|
||||
|
||||
if (currentEvent == nullptr)
|
||||
return result;
|
||||
|
||||
const auto eventType = [currentEvent type];
|
||||
|
||||
if ( eventType != NSEventTypeKeyUp
|
||||
&& eventType != NSEventTypeKeyDown
|
||||
&& eventType != NSEventTypeFlagsChanged)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
// Device independent key numbers should be compared with Carbon
|
||||
// constants, but we define the one we need here to avoid importing
|
||||
// Carbon.h
|
||||
static constexpr unsigned short carbonTabKeycode = 0x30;
|
||||
|
||||
if ([currentEvent keyCode] != carbonTabKeycode)
|
||||
return result;
|
||||
|
||||
const auto shiftKeyDown = ([currentEvent modifierFlags] & NSEventModifierFlagShift) != 0;
|
||||
|
||||
focusChangeTypeHandle->emplace (shiftKeyDown ? Component::FocusChangeDirection::backward
|
||||
: Component::FocusChangeDirection::forward);
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
this->registerClass();
|
||||
}
|
||||
};
|
||||
|
|
@ -267,16 +392,56 @@ private:
|
|||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
#endif
|
||||
|
||||
// Connects the delegate to the rest of the implementation without making WebViewDelegateClass
|
||||
// a nested class as well.
|
||||
class DelegateConnector
|
||||
{
|
||||
public:
|
||||
DelegateConnector (WebBrowserComponent& browserIn,
|
||||
std::function<void (const var&)> handleNativeEventFnIn,
|
||||
std::function<std::optional<WebBrowserComponent::Resource> (const String&)> handleResourceRequestFnIn,
|
||||
const WebBrowserComponent::Options& optionsIn)
|
||||
: browser (browserIn),
|
||||
handleNativeEventFn (std::move (handleNativeEventFnIn)),
|
||||
handleResourceRequestFn (std::move (handleResourceRequestFnIn)),
|
||||
options (optionsIn)
|
||||
{
|
||||
}
|
||||
|
||||
auto& getBrowser() { return browser; }
|
||||
|
||||
void handleNativeEvent (const var& message)
|
||||
{
|
||||
handleNativeEventFn (message);
|
||||
}
|
||||
|
||||
auto handleResourceRequest (const String& url)
|
||||
{
|
||||
return handleResourceRequestFn (url);
|
||||
}
|
||||
|
||||
[[nodiscard]] const auto& getOptions() const
|
||||
{
|
||||
return options;
|
||||
}
|
||||
|
||||
private:
|
||||
WebBrowserComponent& browser;
|
||||
std::function<void (const var&)> handleNativeEventFn;
|
||||
std::function<std::optional<WebBrowserComponent::Resource> (const String&)> handleResourceRequestFn;
|
||||
WebBrowserComponent::Options options;
|
||||
};
|
||||
|
||||
struct API_AVAILABLE (macos (10.10)) WebViewDelegateClass final : public ObjCClass<NSObject>
|
||||
{
|
||||
WebViewDelegateClass() : ObjCClass ("JUCEWebViewDelegate_")
|
||||
{
|
||||
addIvar<WebBrowserComponent*> ("owner");
|
||||
addIvar<DelegateConnector*> ("connector");
|
||||
|
||||
addMethod (@selector (webView:decidePolicyForNavigationAction:decisionHandler:),
|
||||
[] (id self, SEL, WKWebView*, WKNavigationAction* navigationAction, void (^decisionHandler) (WKNavigationActionPolicy))
|
||||
{
|
||||
if (getOwner (self)->pageAboutToLoad (nsStringToJuce ([[[navigationAction request] URL] absoluteString])))
|
||||
if (getConnector (self)->getBrowser().pageAboutToLoad (nsStringToJuce ([[[navigationAction request] URL] absoluteString])))
|
||||
decisionHandler (WKNavigationActionPolicyAllow);
|
||||
else
|
||||
decisionHandler (WKNavigationActionPolicyCancel);
|
||||
|
|
@ -285,34 +450,112 @@ struct API_AVAILABLE (macos (10.10)) WebViewDelegateClass final : public ObjCCla
|
|||
addMethod (@selector (webView:didFinishNavigation:),
|
||||
[] (id self, SEL, WKWebView* webview, WKNavigation*)
|
||||
{
|
||||
getOwner (self)->pageFinishedLoading (nsStringToJuce ([[webview URL] absoluteString]));
|
||||
getConnector (self)->getBrowser().pageFinishedLoading (nsStringToJuce ([[webview URL] absoluteString]));
|
||||
});
|
||||
|
||||
addMethod (@selector (webView:didFailNavigation:withError:),
|
||||
[] (id self, SEL, WKWebView*, WKNavigation*, NSError* error)
|
||||
{
|
||||
displayError (getOwner (self), error);
|
||||
displayError (&getConnector (self)->getBrowser(), error);
|
||||
});
|
||||
|
||||
addMethod (@selector (webView:didFailProvisionalNavigation:withError:),
|
||||
[] (id self, SEL, WKWebView*, WKNavigation*, NSError* error)
|
||||
{
|
||||
displayError (getOwner (self), error);
|
||||
displayError (&getConnector (self)->getBrowser(), error);
|
||||
});
|
||||
|
||||
addMethod (@selector (webViewDidClose:),
|
||||
[] (id self, SEL, WKWebView*)
|
||||
{
|
||||
getOwner (self)->windowCloseRequest();
|
||||
getConnector (self)->getBrowser().windowCloseRequest();
|
||||
});
|
||||
|
||||
addMethod (@selector (webView:createWebViewWithConfiguration:forNavigationAction:windowFeatures:),
|
||||
[] (id self, SEL, WKWebView*, WKWebViewConfiguration*, WKNavigationAction* navigationAction, WKWindowFeatures*)
|
||||
{
|
||||
getOwner (self)->newWindowAttemptingToLoad (nsStringToJuce ([[[navigationAction request] URL] absoluteString]));
|
||||
getConnector (self)->getBrowser().newWindowAttemptingToLoad (nsStringToJuce ([[[navigationAction request] URL] absoluteString]));
|
||||
return nil;
|
||||
});
|
||||
|
||||
addMethod (@selector (userContentController:didReceiveScriptMessage:),
|
||||
[] (id self, SEL, id, id message)
|
||||
{
|
||||
const auto object = fromObject ([message body]);
|
||||
|
||||
if (! object.isString())
|
||||
{
|
||||
jassertfalse;
|
||||
return;
|
||||
}
|
||||
|
||||
getConnector (self)->handleNativeEvent (JSON::fromString (object.toString()));
|
||||
});
|
||||
|
||||
addMethod (@selector (webView:startURLSchemeTask:),
|
||||
[] (id self, SEL, id, id urlSchemeTask)
|
||||
{
|
||||
const auto request = [urlSchemeTask request];
|
||||
|
||||
auto* url = [&]
|
||||
{
|
||||
auto r = [request URL];
|
||||
|
||||
return r == nil ? [NSURL URLWithString:@""] : (NSURL* _Nonnull) r;
|
||||
}();
|
||||
|
||||
const auto path = nsStringToJuce ([url path]);
|
||||
|
||||
const auto resource = getConnector (self)->handleResourceRequest (path);
|
||||
|
||||
JUCE_AUTORELEASEPOOL
|
||||
{
|
||||
const auto makeResponse = [&url] (auto responseCode, id headers=nil)
|
||||
{
|
||||
auto response = [[NSHTTPURLResponse alloc] initWithURL:url
|
||||
statusCode:responseCode
|
||||
HTTPVersion:@"HTTP/1.1"
|
||||
headerFields:headers];
|
||||
|
||||
if (response == nil)
|
||||
return [[NSHTTPURLResponse alloc] autorelease];
|
||||
|
||||
return (NSHTTPURLResponse* _Nonnull) [response autorelease];
|
||||
};
|
||||
|
||||
if (resource.has_value())
|
||||
{
|
||||
NSMutableDictionary* headers = [@ {
|
||||
@"Content-Length" : juceStringToNS (String { resource->data.size() }),
|
||||
@"Content-Type" : juceStringToNS (resource->mimeType),
|
||||
} mutableCopy];
|
||||
|
||||
if (auto allowedOrigin = getConnector (self)->getOptions().getAllowedOrigin())
|
||||
{
|
||||
[headers setObject:juceStringToNS (*allowedOrigin)
|
||||
forKey:@"Access-Control-Allow-Origin"];
|
||||
}
|
||||
|
||||
auto response = makeResponse (200, headers);
|
||||
|
||||
[urlSchemeTask didReceiveResponse:response];
|
||||
[urlSchemeTask didReceiveData:[NSData dataWithBytes:resource->data.data()
|
||||
length:resource->data.size()]];
|
||||
}
|
||||
else
|
||||
{
|
||||
[urlSchemeTask didReceiveResponse:makeResponse (404)];
|
||||
}
|
||||
|
||||
[urlSchemeTask didFinish];
|
||||
}
|
||||
});
|
||||
|
||||
addMethod (@selector (webView:stopURLSchemeTask:),
|
||||
[] (id, SEL, id, id)
|
||||
{
|
||||
});
|
||||
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
|
||||
if (@available (macOS 10.12, *))
|
||||
{
|
||||
|
|
@ -384,8 +627,15 @@ struct API_AVAILABLE (macos (10.10)) WebViewDelegateClass final : public ObjCCla
|
|||
registerClass();
|
||||
}
|
||||
|
||||
static void setOwner (id self, WebBrowserComponent* owner) { object_setInstanceVariable (self, "owner", owner); }
|
||||
static WebBrowserComponent* getOwner (id self) { return getIvar<WebBrowserComponent*> (self, "owner"); }
|
||||
static void setConnector (id self, DelegateConnector* connector)
|
||||
{
|
||||
object_setInstanceVariable (self, "connector", connector);
|
||||
}
|
||||
|
||||
static DelegateConnector* getConnector (id self)
|
||||
{
|
||||
return getIvar<DelegateConnector*> (self, "connector");
|
||||
}
|
||||
|
||||
private:
|
||||
static void displayError (WebBrowserComponent* owner, NSError* error)
|
||||
|
|
@ -403,25 +653,32 @@ private:
|
|||
};
|
||||
|
||||
//==============================================================================
|
||||
struct WebViewBase
|
||||
struct WebBrowserComponent::Impl::Platform
|
||||
{
|
||||
virtual ~WebViewBase() = 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 id getWebView() = 0;
|
||||
class WKWebViewImpl;
|
||||
class WebViewImpl;
|
||||
};
|
||||
|
||||
static constexpr const char* platformSpecificIntegrationScript = R"(
|
||||
window.__JUCE__ = {
|
||||
postMessage: function (object) {
|
||||
window.webkit.messageHandlers.__JUCE__.postMessage(object);
|
||||
},
|
||||
};
|
||||
)";
|
||||
|
||||
//==============================================================================
|
||||
#if JUCE_MAC
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
|
||||
class WebViewImpl : public WebViewBase
|
||||
class WebBrowserComponent::Impl::Platform::WebViewImpl : public WebBrowserComponent::Impl::PlatformInterface,
|
||||
#if JUCE_MAC
|
||||
public NSViewComponent
|
||||
#else
|
||||
public UIViewComponent
|
||||
#endif
|
||||
{
|
||||
public:
|
||||
WebViewImpl (WebBrowserComponent* owner, const String& userAgent)
|
||||
WebViewImpl (WebBrowserComponent::Impl& implIn, const String& userAgent) : browser (implIn.owner)
|
||||
{
|
||||
static WebViewKeyEquivalentResponder<WebView> webviewClass;
|
||||
|
||||
|
|
@ -429,24 +686,59 @@ public:
|
|||
frameName: nsEmptyString()
|
||||
groupName: nsEmptyString()]);
|
||||
|
||||
setLastFocusChangeHandle (webView.get(), &lastFocusChange);
|
||||
|
||||
webView.get().customUserAgent = juceStringToNS (userAgent);
|
||||
|
||||
static DownloadClickDetectorClass cls;
|
||||
clickListener.reset ([cls.createInstance() init]);
|
||||
DownloadClickDetectorClass::setOwner (clickListener.get(), owner);
|
||||
DownloadClickDetectorClass::setOwner (clickListener.get(), &browser);
|
||||
|
||||
[webView.get() setPolicyDelegate: clickListener.get()];
|
||||
[webView.get() setFrameLoadDelegate: clickListener.get()];
|
||||
[webView.get() setUIDelegate: clickListener.get()];
|
||||
|
||||
setView (webView.get());
|
||||
browser.addAndMakeVisible (this);
|
||||
}
|
||||
|
||||
~WebViewImpl() override
|
||||
{
|
||||
setView (nil);
|
||||
|
||||
[webView.get() setPolicyDelegate: nil];
|
||||
[webView.get() setFrameLoadDelegate: nil];
|
||||
[webView.get() setUIDelegate: nil];
|
||||
}
|
||||
|
||||
void setWebViewSize (int width, int height) override
|
||||
{
|
||||
setSize (width, height);
|
||||
}
|
||||
|
||||
void checkWindowAssociation() override
|
||||
{
|
||||
if (browser.isShowing())
|
||||
{
|
||||
browser.reloadLastURL();
|
||||
|
||||
if (browser.blankPageShown)
|
||||
browser.goBack();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (browser.unloadPageWhenHidden && ! browser.blankPageShown)
|
||||
{
|
||||
// when the component becomes invisible, some stuff like flash
|
||||
// carries on playing audio, so we need to force it onto a blank
|
||||
// page to avoid this, (and send it back when it's made visible again).
|
||||
|
||||
browser.blankPageShown = true;
|
||||
goToURL ("about:blank", nullptr, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void goToURL (const String& url,
|
||||
const StringArray* headers,
|
||||
const MemoryBlock* postData) override
|
||||
|
|
@ -486,9 +778,7 @@ public:
|
|||
void stop() override { [webView.get() stopLoading: nil]; }
|
||||
void refresh() override { [webView.get() reload: nil]; }
|
||||
|
||||
id getWebView() override { return webView.get(); }
|
||||
|
||||
void mouseMove (const MouseEvent&)
|
||||
void mouseMove (const MouseEvent&) override
|
||||
{
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
|
||||
// WebKit doesn't capture mouse-moves itself, so it seems the only way to make
|
||||
|
|
@ -498,47 +788,185 @@ public:
|
|||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
}
|
||||
|
||||
void evaluateJavascript (const String&, WebBrowserComponent::EvaluationCallback) override
|
||||
{
|
||||
// This feature is only available on MacOS 10.11 and above
|
||||
jassertfalse;
|
||||
}
|
||||
|
||||
private:
|
||||
WebBrowserComponent& browser;
|
||||
LastFocusChange lastFocusChange;
|
||||
ObjCObjectHandle<WebView*> webView;
|
||||
ObjCObjectHandle<id> clickListener;
|
||||
};
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
#endif
|
||||
|
||||
class API_AVAILABLE (macos (10.11)) WKWebViewImpl : public WebViewBase
|
||||
class API_AVAILABLE (macos (10.11)) WebBrowserComponent::Impl::Platform::WKWebViewImpl : public WebBrowserComponent::Impl::PlatformInterface,
|
||||
#if JUCE_MAC
|
||||
public NSViewComponent
|
||||
#else
|
||||
public UIViewComponent
|
||||
#endif
|
||||
{
|
||||
public:
|
||||
WKWebViewImpl (WebBrowserComponent* owner, const String& userAgent)
|
||||
WKWebViewImpl (WebBrowserComponent::Impl& implIn,
|
||||
const WebBrowserComponent::Options& browserOptions,
|
||||
const StringArray& userScripts)
|
||||
: owner (implIn),
|
||||
delegateConnector (implIn.owner,
|
||||
[this] (const auto& m) { owner.handleNativeEvent (m); },
|
||||
[this] (const auto& r) { return owner.handleResourceRequest (r); },
|
||||
browserOptions)
|
||||
{
|
||||
#if JUCE_MAC
|
||||
static WebViewKeyEquivalentResponder<WKWebView> webviewClass;
|
||||
ObjCObjectHandle<WKWebViewConfiguration*> config { [WKWebViewConfiguration new] };
|
||||
id preferences = [config.get() preferences];
|
||||
|
||||
webView.reset ([webviewClass.createInstance() initWithFrame: NSMakeRect (0, 0, 100.0f, 100.0f)]);
|
||||
#else
|
||||
webView.reset ([[WKWebView alloc] initWithFrame: CGRectMake (0, 0, 100.0f, 100.0f)]);
|
||||
#endif
|
||||
|
||||
if (userAgent.isNotEmpty())
|
||||
webView.get().customUserAgent = juceStringToNS (userAgent);
|
||||
[preferences setValue:@(true) forKey:@"fullScreenEnabled"];
|
||||
[preferences setValue:@(true) forKey:@"DOMPasteAllowed"];
|
||||
[preferences setValue:@(true) forKey:@"javaScriptCanAccessClipboard"];
|
||||
|
||||
static WebViewDelegateClass cls;
|
||||
webViewDelegate.reset ([cls.createInstance() init]);
|
||||
WebViewDelegateClass::setOwner (webViewDelegate.get(), owner);
|
||||
WebViewDelegateClass::setConnector (webViewDelegate.get(), &delegateConnector);
|
||||
|
||||
if (browserOptions.getNativeIntegrationsEnabled())
|
||||
{
|
||||
[[config.get() userContentController] addScriptMessageHandler:webViewDelegate.get()
|
||||
name:@"__JUCE__"];
|
||||
}
|
||||
|
||||
// It isn't necessary to concatenate all scripts and add them as one. They will still work
|
||||
// when added separately. But in the latter case sometimes only the first one is visible in
|
||||
// the WebView developer console, so concatenating them helps with debugging.
|
||||
auto allUserScripts = userScripts;
|
||||
allUserScripts.insert (0, platformSpecificIntegrationScript);
|
||||
|
||||
NSUniquePtr<WKUserScript> script { [[WKUserScript alloc]
|
||||
initWithSource:juceStringToNS (allUserScripts.joinIntoString ("\n"))
|
||||
injectionTime:WKUserScriptInjectionTimeAtDocumentStart
|
||||
forMainFrameOnly:YES] };
|
||||
|
||||
[[config.get() userContentController] addUserScript:script.get()];
|
||||
|
||||
if (@available (macOS 10.13, iOS 11.0, *))
|
||||
{
|
||||
if (browserOptions.getResourceProvider() != nullptr)
|
||||
[config.get() setURLSchemeHandler:webViewDelegate.get() forURLScheme:@"juce"];
|
||||
}
|
||||
|
||||
#if JUCE_DEBUG
|
||||
[preferences setValue: @(true) forKey: @"developerExtrasEnabled"];
|
||||
#endif
|
||||
|
||||
#if JUCE_MAC
|
||||
static WebViewKeyEquivalentResponder<WKWebView> webviewClass;
|
||||
|
||||
webView.reset ([webviewClass.createInstance() initWithFrame: NSMakeRect (0, 0, 100.0f, 100.0f)
|
||||
configuration: config.get()]);
|
||||
|
||||
setLastFocusChangeHandle (webView.get(), &lastFocusChange);
|
||||
#else
|
||||
webView.reset ([[WKWebView alloc] initWithFrame: CGRectMake (0, 0, 100.0f, 100.0f)
|
||||
configuration: config.get()]);
|
||||
#endif
|
||||
|
||||
if (const auto userAgent = browserOptions.getUserAgent(); userAgent.isNotEmpty())
|
||||
webView.get().customUserAgent = juceStringToNS (userAgent);
|
||||
|
||||
[webView.get() setNavigationDelegate: webViewDelegate.get()];
|
||||
[webView.get() setUIDelegate: webViewDelegate.get()];
|
||||
|
||||
#if JUCE_DEBUG
|
||||
[[[webView.get() configuration] preferences] setValue: @(true) forKey: @"developerExtrasEnabled"];
|
||||
#endif
|
||||
setView (webView.get());
|
||||
owner.owner.addAndMakeVisible (this);
|
||||
}
|
||||
|
||||
~WKWebViewImpl() override
|
||||
{
|
||||
setView (nil);
|
||||
[webView.get() setNavigationDelegate: nil];
|
||||
[webView.get() setUIDelegate: nil];
|
||||
}
|
||||
|
||||
void setWebViewSize (int width, int height) override
|
||||
{
|
||||
setSize (width, height);
|
||||
}
|
||||
|
||||
void checkWindowAssociation() override
|
||||
{
|
||||
auto& browser = owner.owner;
|
||||
|
||||
if (browser.isShowing())
|
||||
{
|
||||
browser.reloadLastURL();
|
||||
|
||||
if (browser.blankPageShown)
|
||||
browser.goBack();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (browser.unloadPageWhenHidden && ! browser.blankPageShown)
|
||||
{
|
||||
// when the component becomes invisible, some stuff like flash
|
||||
// carries on playing audio, so we need to force it onto a blank
|
||||
// page to avoid this, (and send it back when it's made visible again).
|
||||
|
||||
browser.blankPageShown = true;
|
||||
goToURL ("about:blank", nullptr, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void focusGainedWithDirection (FocusChangeType, FocusChangeDirection) override
|
||||
{
|
||||
const auto webViewFocusLossDirection = std::exchange (lastFocusChange, std::nullopt);
|
||||
|
||||
// We didn't receive the focus from the WebView, so we need to pass it onto it
|
||||
if (! webViewFocusLossDirection.has_value())
|
||||
{
|
||||
#if JUCE_MAC
|
||||
[[webView.get() window] makeFirstResponder: webView.get()];
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
auto* comp = [&]() -> Component*
|
||||
{
|
||||
auto* c = owner.owner.getParentComponent();
|
||||
|
||||
if (c == nullptr)
|
||||
return nullptr;
|
||||
|
||||
const auto traverser = c->createFocusTraverser();
|
||||
|
||||
if (*webViewFocusLossDirection == FocusChangeDirection::forward)
|
||||
{
|
||||
if (auto* next = traverser->getNextComponent (this); next != nullptr)
|
||||
return next;
|
||||
|
||||
return traverser->getDefaultComponent (c);
|
||||
}
|
||||
|
||||
if (*webViewFocusLossDirection == FocusChangeDirection::backward)
|
||||
{
|
||||
if (auto* previous = traverser->getPreviousComponent (&owner.owner); previous != nullptr)
|
||||
return previous;
|
||||
|
||||
if (auto all = traverser->getAllComponents (c); ! all.empty())
|
||||
return all.back();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}();
|
||||
|
||||
if (comp != nullptr)
|
||||
comp->getAccessibilityHandler()->grabFocus();
|
||||
else
|
||||
giveAwayKeyboardFocus();
|
||||
}
|
||||
|
||||
void goToURL (const String& url,
|
||||
const StringArray* headers,
|
||||
const MemoryBlock* postData) override
|
||||
|
|
@ -574,169 +1002,87 @@ public:
|
|||
void stop() override { [webView.get() stopLoading]; }
|
||||
void refresh() override { [webView.get() reload]; }
|
||||
|
||||
id getWebView() override { return webView.get(); }
|
||||
void evaluateJavascript (const String& script, WebBrowserComponent::EvaluationCallback callback) override
|
||||
{
|
||||
[webView.get() evaluateJavaScript: juceStringToNS (script)
|
||||
completionHandler: ^(id obj, NSError* error)
|
||||
{
|
||||
if (callback == nullptr)
|
||||
return;
|
||||
|
||||
if (error != nil)
|
||||
{
|
||||
const auto resultError = [&]() -> WebBrowserComponent::EvaluationResult::Error
|
||||
{
|
||||
const auto errorCode = [error code];
|
||||
|
||||
if (errorCode == 4)
|
||||
{
|
||||
String errorMsgTemplate { "JAVASCRIPT_ERROR at (EVALUATION_SOURCE:LINE_NUMBER:COLUMN_NUMBER)" };
|
||||
|
||||
if (id m = [error.userInfo objectForKey:@"WKJavaScriptExceptionMessage"]; m != nil)
|
||||
errorMsgTemplate = errorMsgTemplate.replace ("JAVASCRIPT_ERROR", nsStringToJuce (m));
|
||||
|
||||
if (id m = [error.userInfo objectForKey:@"WKJavaScriptExceptionSourceURL"]; m != nil)
|
||||
errorMsgTemplate = errorMsgTemplate.replace ("EVALUATION_SOURCE", nsStringToJuce ([m absoluteString]));
|
||||
|
||||
if (id m = [error.userInfo objectForKey:@"WKJavaScriptExceptionLineNumber"]; m != nil)
|
||||
errorMsgTemplate = errorMsgTemplate.replace ("LINE_NUMBER", String { [m intValue] });
|
||||
|
||||
if (id m = [error.userInfo objectForKey:@"WKJavaScriptExceptionColumnNumber"]; m != nil)
|
||||
errorMsgTemplate = errorMsgTemplate.replace ("COLUMN_NUMBER", String { [m intValue] });
|
||||
|
||||
return { WebBrowserComponent::EvaluationResult::Error::Type::javascriptException,
|
||||
errorMsgTemplate };
|
||||
}
|
||||
else if (errorCode == 5)
|
||||
{
|
||||
String errorMessage;
|
||||
|
||||
if (id m = [[error userInfo] objectForKey:@"NSLocalizedDescription"]; m != nil)
|
||||
errorMessage = nsStringToJuce (m);
|
||||
|
||||
return { WebBrowserComponent::EvaluationResult::Error::Type::unsupportedReturnType,
|
||||
errorMessage };
|
||||
}
|
||||
|
||||
return { WebBrowserComponent::EvaluationResult::Error::Type::unknown, "Unknown error" };
|
||||
}();
|
||||
|
||||
callback (EvaluationResult { resultError });
|
||||
}
|
||||
else
|
||||
{
|
||||
callback (EvaluationResult { fromObject (obj) });
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
private:
|
||||
WebBrowserComponent::Impl& owner;
|
||||
DelegateConnector delegateConnector;
|
||||
LastFocusChange lastFocusChange;
|
||||
ObjCObjectHandle<WKWebView*> webView;
|
||||
ObjCObjectHandle<id> webViewDelegate;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class WebBrowserComponent::Pimpl
|
||||
#if JUCE_MAC
|
||||
: public NSViewComponent
|
||||
#else
|
||||
: public UIViewComponent
|
||||
#endif
|
||||
auto WebBrowserComponent::Impl::createAndInitPlatformDependentPart (WebBrowserComponent::Impl& impl,
|
||||
const WebBrowserComponent::Options& options,
|
||||
const StringArray& userScripts)
|
||||
-> std::unique_ptr<PlatformInterface>
|
||||
{
|
||||
public:
|
||||
Pimpl (WebBrowserComponent* owner, const String& userAgent)
|
||||
{
|
||||
if (@available (macOS 10.11, *))
|
||||
webView = std::make_unique<WKWebViewImpl> (owner, userAgent);
|
||||
#if JUCE_MAC
|
||||
else
|
||||
webView = std::make_unique<WebViewImpl> (owner, userAgent);
|
||||
#endif
|
||||
if (@available (macOS 10.11, *))
|
||||
return std::make_unique<Platform::WKWebViewImpl> (impl, options, userScripts);
|
||||
|
||||
setView (webView->getWebView());
|
||||
}
|
||||
#if JUCE_MAC
|
||||
return std::make_unique<Platform::WebViewImpl> (impl, options.getUserAgent());
|
||||
#endif
|
||||
|
||||
~Pimpl() override
|
||||
{
|
||||
webView = nullptr;
|
||||
setView (nil);
|
||||
}
|
||||
|
||||
void goToURL (const String& url,
|
||||
const StringArray* headers,
|
||||
const MemoryBlock* postData)
|
||||
{
|
||||
webView->goToURL (url, headers, postData);
|
||||
}
|
||||
|
||||
void goBack() { webView->goBack(); }
|
||||
void goForward() { webView->goForward(); }
|
||||
|
||||
void stop() { webView->stop(); }
|
||||
void refresh() { webView->refresh(); }
|
||||
|
||||
private:
|
||||
std::unique_ptr<WebViewBase> webView;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
WebBrowserComponent::WebBrowserComponent (const Options& options)
|
||||
: unloadPageWhenHidden (! options.keepsPageLoadedWhenBrowserIsHidden())
|
||||
{
|
||||
setOpaque (true);
|
||||
browser.reset (new Pimpl (this, options.getUserAgent()));
|
||||
addAndMakeVisible (browser.get());
|
||||
}
|
||||
|
||||
WebBrowserComponent::~WebBrowserComponent() = default;
|
||||
|
||||
//==============================================================================
|
||||
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();
|
||||
|
||||
blankPageShown = false;
|
||||
|
||||
browser->goToURL (url, headers, postData);
|
||||
}
|
||||
|
||||
void WebBrowserComponent::stop()
|
||||
{
|
||||
browser->stop();
|
||||
}
|
||||
|
||||
void WebBrowserComponent::goBack()
|
||||
{
|
||||
lastURL.clear();
|
||||
blankPageShown = false;
|
||||
browser->goBack();
|
||||
}
|
||||
|
||||
void WebBrowserComponent::goForward()
|
||||
{
|
||||
lastURL.clear();
|
||||
browser->goForward();
|
||||
}
|
||||
|
||||
void WebBrowserComponent::refresh()
|
||||
{
|
||||
browser->refresh();
|
||||
return {};
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void WebBrowserComponent::paint (Graphics&)
|
||||
{
|
||||
}
|
||||
|
||||
void WebBrowserComponent::checkWindowAssociation()
|
||||
{
|
||||
if (isShowing())
|
||||
{
|
||||
reloadLastURL();
|
||||
|
||||
if (blankPageShown)
|
||||
goBack();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (unloadPageWhenHidden && ! blankPageShown)
|
||||
{
|
||||
// when the component becomes invisible, some stuff like flash
|
||||
// carries on playing audio, so we need to force it onto a blank
|
||||
// page to avoid this, (and send it back when it's made visible again).
|
||||
|
||||
blankPageShown = true;
|
||||
browser->goToURL ("about:blank", nullptr, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WebBrowserComponent::reloadLastURL()
|
||||
{
|
||||
if (lastURL.isNotEmpty())
|
||||
{
|
||||
goToURL (lastURL, &lastHeaders, &lastPostData);
|
||||
lastURL.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void WebBrowserComponent::parentHierarchyChanged()
|
||||
{
|
||||
checkWindowAssociation();
|
||||
}
|
||||
|
||||
void WebBrowserComponent::resized()
|
||||
{
|
||||
browser->setSize (getWidth(), getHeight());
|
||||
}
|
||||
|
||||
void WebBrowserComponent::visibilityChanged()
|
||||
{
|
||||
checkWindowAssociation();
|
||||
}
|
||||
|
||||
void WebBrowserComponent::focusGainedWithDirection (FocusChangeType, FocusChangeDirection)
|
||||
{
|
||||
}
|
||||
|
||||
void WebBrowserComponent::clearCookies()
|
||||
{
|
||||
NSHTTPCookieStorage* storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue