Merge branch 'webview_script_message' of https://github.com/TcT2k/wxWidgets

Add WebView script message and user scripts.

See https://github.com/wxWidgets/wxWidgets/pull/2237
This commit is contained in:
Vadim Zeitlin
2021-03-05 18:44:46 +01:00
11 changed files with 550 additions and 54 deletions

View File

@@ -49,6 +49,7 @@ wxDEFINE_EVENT( wxEVT_WEBVIEW_ERROR, wxWebViewEvent );
wxDEFINE_EVENT( wxEVT_WEBVIEW_NEWWINDOW, wxWebViewEvent );
wxDEFINE_EVENT( wxEVT_WEBVIEW_TITLE_CHANGED, wxWebViewEvent );
wxDEFINE_EVENT( wxEVT_WEBVIEW_FULLSCREEN_CHANGED, wxWebViewEvent);
wxDEFINE_EVENT( wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, wxWebViewEvent);
wxStringWebViewFactoryMap wxWebView::m_factoryMap;

View File

@@ -31,6 +31,37 @@
#include <JavaScriptCore/JSValueRef.h>
#include <JavaScriptCore/JSStringRef.h>
// Helper function to get string from Webkit JS result
bool wxGetStringFromJSResult(WebKitJavascriptResult* js_result, wxString* output)
{
JSGlobalContextRef context = webkit_javascript_result_get_global_context(js_result);
JSValueRef value = webkit_javascript_result_get_value(js_result);
JSValueRef exception = NULL;
wxJSStringRef js_value
(
JSValueIsObject(context, value)
? JSValueCreateJSONString(context, value, 0, &exception)
: JSValueToStringCopy(context, value, &exception)
);
if ( exception )
{
if ( output )
{
wxJSStringRef ex_value(JSValueToStringCopy(context, exception, NULL));
*output = ex_value.ToWxString();
}
return false;
}
if ( output != NULL )
*output = js_value.ToWxString();
return true;
}
// ----------------------------------------------------------------------------
// GTK callbacks
// ----------------------------------------------------------------------------
@@ -290,6 +321,21 @@ wxgtk_webview_webkit_leave_fullscreen(WebKitWebView *WXUNUSED(web_view),
return FALSE;
}
static void
wxgtk_webview_webkit_script_message_received(WebKitUserContentManager *WXUNUSED(content_manager),
WebKitJavascriptResult *js_result,
wxWebViewWebKit *webKitCtrl)
{
wxWebViewEvent event(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED,
webKitCtrl->GetId(),
webKitCtrl->GetCurrentURL(),
"");
wxString msgStr;
if (wxGetStringFromJSResult(js_result, &msgStr))
event.SetString(msgStr);
webKitCtrl->HandleWindowEvent(event);
}
static gboolean
wxgtk_webview_webkit_decide_policy(WebKitWebView *web_view,
WebKitPolicyDecision *decision,
@@ -698,6 +744,18 @@ float wxWebViewWebKit::GetWebkitZoom() const
return webkit_web_view_get_zoom_level(m_web_view);
}
void wxWebViewWebKit::EnableAccessToDevTools(bool enable)
{
WebKitSettings* settings = webkit_web_view_get_settings(m_web_view);
webkit_settings_set_enable_developer_extras(settings, enable);
}
bool wxWebViewWebKit::IsAccessToDevToolsEnabled() const
{
WebKitSettings* settings = webkit_web_view_get_settings(m_web_view);
return webkit_settings_get_enable_developer_extras(settings);
}
void wxWebViewWebKit::Stop()
{
webkit_web_view_stop_loading(m_web_view);
@@ -1209,32 +1267,7 @@ bool wxWebViewWebKit::RunScriptSync(const wxString& javascript, wxString* output
return false;
}
JSGlobalContextRef context = webkit_javascript_result_get_global_context(js_result);
JSValueRef value = webkit_javascript_result_get_value(js_result);
JSValueRef exception = NULL;
wxJSStringRef js_value
(
JSValueIsObject(context, value)
? JSValueCreateJSONString(context, value, 0, &exception)
: JSValueToStringCopy(context, value, &exception)
);
if ( exception )
{
if ( output )
{
wxJSStringRef ex_value(JSValueToStringCopy(context, exception, NULL));
*output = ex_value.ToWxString();
}
return false;
}
if ( output != NULL )
*output = js_value.ToWxString();
return true;
return wxGetStringFromJSResult(js_result, output);
}
bool wxWebViewWebKit::RunScript(const wxString& javascript, wxString* output) const
@@ -1266,6 +1299,57 @@ bool wxWebViewWebKit::RunScript(const wxString& javascript, wxString* output) co
return true;
}
bool wxWebViewWebKit::AddScriptMessageHandler(const wxString& name)
{
if (!m_web_view)
return false;
WebKitUserContentManager *ucm = webkit_web_view_get_user_content_manager(m_web_view);
g_signal_connect(ucm, wxString::Format("script-message-received::%s", name).utf8_str(),
G_CALLBACK(wxgtk_webview_webkit_script_message_received), this);
bool res = webkit_user_content_manager_register_script_message_handler(ucm, name.utf8_str());
if (res)
{
// Make webkit message handler available under common name
wxString js = wxString::Format("window.%s = window.webkit.messageHandlers.%s;",
name, name);
AddUserScript(js);
RunScript(js);
}
return res;
}
bool wxWebViewWebKit::RemoveScriptMessageHandler(const wxString& name)
{
WebKitUserContentManager *ucm = webkit_web_view_get_user_content_manager(m_web_view);
webkit_user_content_manager_unregister_script_message_handler(ucm, name.utf8_str());
return true;
}
bool wxWebViewWebKit::AddUserScript(const wxString& javascript,
wxWebViewUserScriptInjectionTime injectionTime)
{
WebKitUserScript* userScript = webkit_user_script_new(
javascript.utf8_str(),
WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
(injectionTime == wxWEBVIEW_INJECT_AT_DOCUMENT_START) ?
WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START : WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END,
NULL, NULL
);
WebKitUserContentManager *ucm = webkit_web_view_get_user_content_manager(m_web_view);
webkit_user_content_manager_add_script(ucm, userScript);
webkit_user_script_unref(userScript);
return true;
}
void wxWebViewWebKit::RemoveAllUserScripts()
{
WebKitUserContentManager *ucm = webkit_web_view_get_user_content_manager(m_web_view);
webkit_user_content_manager_remove_all_scripts(ucm);
}
void wxWebViewWebKit::RegisterHandler(wxSharedPtr<wxWebViewHandler> handler)
{
m_handlerList.push_back(handler);

View File

@@ -75,6 +75,7 @@ wxWebViewEdgeImpl::~wxWebViewEdgeImpl()
m_webView->remove_DocumentTitleChanged(m_documentTitleChangedToken);
m_webView->remove_ContentLoading(m_contentLoadingToken);
m_webView->remove_ContainsFullScreenElementChanged(m_containsFullScreenElementChangedToken);
m_webView->remove_WebMessageReceived(m_webMessageReceivedToken);
}
}
@@ -83,7 +84,7 @@ bool wxWebViewEdgeImpl::Create()
m_initialized = false;
m_isBusy = false;
m_pendingContextMenuEnabled = -1;
m_pendingAccessToDevToolsEnabled = -1;
m_pendingAccessToDevToolsEnabled = 0;
m_historyLoadingFromList = false;
m_historyEnabled = true;
@@ -320,6 +321,37 @@ HRESULT wxWebViewEdgeImpl::OnContainsFullScreenElementChanged(ICoreWebView2* WXU
return S_OK;
}
HRESULT
wxWebViewEdgeImpl::OnWebMessageReceived(ICoreWebView2* WXUNUSED(sender),
ICoreWebView2WebMessageReceivedEventArgs* args)
{
wxCoTaskMemPtr<wchar_t> msgContent;
HRESULT hr = args->get_WebMessageAsJson(&msgContent);
if (FAILED(hr))
{
wxLogApiError("get_WebMessageAsJson", hr);
return hr;
}
wxWebViewEvent event(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, m_ctrl->GetId(),
m_ctrl->GetCurrentURL(), wxString(),
wxWEBVIEW_NAV_ACTION_NONE, m_scriptMsgHandlerName);
event.SetEventObject(m_ctrl);
// Try to decode JSON string or return original
// result if it's not a valid JSON string
wxString msgStr;
wxString msgJson(msgContent);
if (!wxJSON::DecodeString(msgJson, &msgStr))
msgStr = msgJson;
event.SetString(msgStr);
m_ctrl->HandleWindowEvent(event);
return S_OK;
}
HRESULT wxWebViewEdgeImpl::OnWebViewCreated(HRESULT result, ICoreWebView2Controller* webViewController)
{
if (FAILED(result))
@@ -369,6 +401,10 @@ HRESULT wxWebViewEdgeImpl::OnWebViewCreated(HRESULT result, ICoreWebView2Control
Callback<ICoreWebView2ContainsFullScreenElementChangedEventHandler>(
this, &wxWebViewEdgeImpl::OnContainsFullScreenElementChanged).Get(),
&m_containsFullScreenElementChangedToken);
m_webView->add_WebMessageReceived(
Callback<ICoreWebView2WebMessageReceivedEventHandler>(
this, &wxWebViewEdgeImpl::OnWebMessageReceived).Get(),
&m_webMessageReceivedToken);
if (m_pendingContextMenuEnabled != -1)
{
@@ -384,7 +420,18 @@ HRESULT wxWebViewEdgeImpl::OnWebViewCreated(HRESULT result, ICoreWebView2Control
wxCOMPtr<ICoreWebView2Settings> settings(GetSettings());
if (settings)
{
settings->put_IsStatusBarEnabled(false);
}
UpdateWebMessageHandler();
if (!m_pendingUserScripts.empty())
{
for (wxVector<wxString>::iterator it = m_pendingUserScripts.begin();
it != m_pendingUserScripts.end(); ++it)
m_ctrl->AddUserScript(*it);
m_pendingUserScripts.clear();
}
if (!m_pendingURL.empty())
{
@@ -395,6 +442,24 @@ HRESULT wxWebViewEdgeImpl::OnWebViewCreated(HRESULT result, ICoreWebView2Control
return S_OK;
}
void wxWebViewEdgeImpl::UpdateWebMessageHandler()
{
wxCOMPtr<ICoreWebView2Settings> settings(GetSettings());
if (!settings)
return;
settings->put_IsWebMessageEnabled(!m_scriptMsgHandlerName.empty());
if (!m_scriptMsgHandlerName.empty())
{
// Make edge message handler available under common name
wxString js = wxString::Format("window.%s = window.chrome.webview;",
m_scriptMsgHandlerName);
m_ctrl->AddUserScript(js);
m_webView->ExecuteScript(js.wc_str(), NULL);
}
}
ICoreWebView2Settings* wxWebViewEdgeImpl::GetSettings()
{
if (!m_webView)
@@ -774,6 +839,65 @@ bool wxWebViewEdge::RunScript(const wxString& javascript, wxString* output) cons
return true;
}
bool wxWebViewEdge::AddScriptMessageHandler(const wxString& name)
{
// Edge only supports a single message handler
if (!m_impl->m_scriptMsgHandlerName.empty())
return false;
m_impl->m_scriptMsgHandlerName = name;
m_impl->UpdateWebMessageHandler();
return true;
}
bool wxWebViewEdge::RemoveScriptMessageHandler(const wxString& WXUNUSED(name))
{
m_impl->m_scriptMsgHandlerName.clear();
m_impl->UpdateWebMessageHandler();
return true;
}
HRESULT wxWebViewEdgeImpl::OnAddScriptToExecuteOnDocumentedCreatedCompleted(HRESULT errorCode, LPCWSTR id)
{
if (SUCCEEDED(errorCode))
m_userScriptIds.push_back(id);
return S_OK;
}
bool wxWebViewEdge::AddUserScript(const wxString& javascript,
wxWebViewUserScriptInjectionTime injectionTime)
{
// Currently only AT_DOCUMENT_START is supported
if (injectionTime != wxWEBVIEW_INJECT_AT_DOCUMENT_START)
return false;
if (m_impl->m_webView)
{
HRESULT hr = m_impl->m_webView->AddScriptToExecuteOnDocumentCreated(javascript.wc_str(),
Callback<ICoreWebView2AddScriptToExecuteOnDocumentCreatedCompletedHandler>(m_impl,
&wxWebViewEdgeImpl::OnAddScriptToExecuteOnDocumentedCreatedCompleted).Get());
if (FAILED(hr))
return false;
}
else
m_impl->m_pendingUserScripts.push_back(javascript);
return true;
}
void wxWebViewEdge::RemoveAllUserScripts()
{
m_impl->m_pendingUserScripts.clear();
for (auto& scriptId : m_impl->m_userScriptIds)
{
HRESULT hr = m_impl->m_webView->RemoveScriptToExecuteOnDocumentCreated(scriptId.wc_str());
if (FAILED(hr))
wxLogApiError("RemoveScriptToExecuteOnDocumentCreated", hr);
}
m_impl->m_userScriptIds.clear();
}
void wxWebViewEdge::RegisterHandler(wxSharedPtr<wxWebViewHandler> WXUNUSED(handler))
{
// TODO: could maybe be implemented via IWebView2WebView5::add_WebResourceRequested

View File

@@ -87,6 +87,15 @@ wxEND_EVENT_TABLE()
@end
#endif // macOS 10.13+
@interface WebViewScriptMessageHandler: NSObject<WKScriptMessageHandler>
{
wxWebViewWebKit* webKitWindow;
}
- (id)initWithWxWindow: (wxWebViewWebKit*)inWindow;
@end
//-----------------------------------------------------------------------------
// wxWebViewFactoryWebKit
//-----------------------------------------------------------------------------
@@ -385,6 +394,41 @@ bool wxWebViewWebKit::RunScript(const wxString& javascript, wxString* output) co
return true;
}
bool wxWebViewWebKit::AddScriptMessageHandler(const wxString& name)
{
[m_webView.configuration.userContentController addScriptMessageHandler:
[[WebViewScriptMessageHandler alloc] initWithWxWindow:this] name:wxCFStringRef(name).AsNSString()];
// Make webkit message handler available under common name
wxString js = wxString::Format("window.%s = window.webkit.messageHandlers.%s;",
name, name);
AddUserScript(js);
RunScript(js);
return true;
}
bool wxWebViewWebKit::RemoveScriptMessageHandler(const wxString& name)
{
[m_webView.configuration.userContentController removeScriptMessageHandlerForName:wxCFStringRef(name).AsNSString()];
return true;
}
bool wxWebViewWebKit::AddUserScript(const wxString& javascript,
wxWebViewUserScriptInjectionTime injectionTime)
{
WKUserScript* userScript =
[[WKUserScript alloc] initWithSource:wxCFStringRef(javascript).AsNSString()
injectionTime:(injectionTime == wxWEBVIEW_INJECT_AT_DOCUMENT_START) ?
WKUserScriptInjectionTimeAtDocumentStart : WKUserScriptInjectionTimeAtDocumentEnd
forMainFrameOnly:NO];
[m_webView.configuration.userContentController addUserScript:userScript];
return true;
}
void wxWebViewWebKit::RemoveAllUserScripts()
{
[m_webView.configuration.userContentController removeAllUserScripts];
}
void wxWebViewWebKit::LoadURL(const wxString& url)
{
[m_webView loadRequest:[NSURLRequest requestWithURL:
@@ -891,4 +935,45 @@ WX_API_AVAILABLE_MACOS(10, 12)
@end
@implementation WebViewScriptMessageHandler
- (id)initWithWxWindow: (wxWebViewWebKit*)inWindow
{
if (self = [super init])
{
webKitWindow = inWindow; // non retained
}
return self;
}
- (void)userContentController:(nonnull WKUserContentController *)userContentController
didReceiveScriptMessage:(nonnull WKScriptMessage *)message
{
wxWebViewEvent event(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED,
webKitWindow->GetId(),
webKitWindow->GetCurrentURL(),
"",
wxWEBVIEW_NAV_ACTION_NONE,
wxCFStringRef::AsString(message.name));
if ([message.body isKindOfClass:NSString.class])
event.SetString(wxCFStringRef::AsString(message.body));
else if ([message.body isKindOfClass:NSNumber.class])
event.SetString(wxCFStringRef::AsString(((NSNumber*)message.body).stringValue));
else if ([message.body isKindOfClass:NSDate.class])
event.SetString(wxCFStringRef::AsString(((NSDate*)message.body).description));
else if ([message.body isKindOfClass:NSNull.class])
event.SetString("null");
else if ([message.body isKindOfClass:NSDictionary.class] || [message.body isKindOfClass:NSArray.class])
{
NSError* error = nil;
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:message.body options:0 error:&error];
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
event.SetString(wxCFStringRef::AsString(jsonString));
}
webKitWindow->ProcessWindowEvent(event);
}
@end
#endif //wxUSE_WEBVIEW && wxUSE_WEBVIEW_WEBKIT