1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-11 23:54:18 +00:00
JUCE/modules/juce_gui_extra/native/juce_win32_WebBrowserComponent.cpp

885 lines
29 KiB
C++

/*
==============================================================================
This file is part of the JUCE 6 technical preview.
Copyright (c) 2020 - Raw Material Software Limited
You may use this code under the terms of the GPL v3
(see www.gnu.org/licenses).
For this technical preview, this file is not subject to commercial licensing.
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
{
struct InternalWebViewType
{
InternalWebViewType() {}
virtual ~InternalWebViewType() {}
virtual void createBrowser() = 0;
virtual bool hasBrowserBeenCreated() = 0;
virtual void goToURL (const String&, const StringArray*, const MemoryBlock*) = 0;
virtual void stop() = 0;
virtual void goBack() = 0;
virtual void goForward() = 0;
virtual void refresh() = 0;
virtual void focusGained() {}
virtual void setWebViewSize (int, int) = 0;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InternalWebViewType)
};
#if JUCE_MINGW
JUCE_DECLARE_UUID_GETTER (IOleClientSite, "00000118-0000-0000-c000-000000000046")
JUCE_DECLARE_UUID_GETTER (IDispatch, "00020400-0000-0000-c000-000000000046")
#ifndef WebBrowser
class WebBrowser;
#endif
#endif
JUCE_DECLARE_UUID_GETTER (DWebBrowserEvents2, "34A715A0-6587-11D0-924A-0020AFC7AC4D")
JUCE_DECLARE_UUID_GETTER (IConnectionPointContainer, "B196B284-BAB4-101A-B69C-00AA00341D07")
JUCE_DECLARE_UUID_GETTER (IWebBrowser2, "D30C1661-CDAF-11D0-8A3E-00C04FC9E26E")
JUCE_DECLARE_UUID_GETTER (WebBrowser, "8856F961-340A-11D0-A96B-00C04FD705A2")
//==============================================================================
class Win32WebView : public InternalWebViewType,
public ActiveXControlComponent
{
public:
Win32WebView (WebBrowserComponent& owner)
{
owner.addAndMakeVisible (this);
}
~Win32WebView() override
{
if (connectionPoint != nullptr)
connectionPoint->Unadvise (adviseCookie);
if (browser != nullptr)
browser->Release();
}
void createBrowser() override
{
auto webCLSID = __uuidof (WebBrowser);
createControl (&webCLSID);
auto iidWebBrowser2 = __uuidof (IWebBrowser2);
auto iidConnectionPointContainer = __uuidof (IConnectionPointContainer);
browser = (IWebBrowser2*) queryInterface (&iidWebBrowser2);
if (auto connectionPointContainer = (IConnectionPointContainer*) queryInterface (&iidConnectionPointContainer))
{
connectionPointContainer->FindConnectionPoint (__uuidof (DWebBrowserEvents2), &connectionPoint);
if (connectionPoint != nullptr)
{
auto* owner = dynamic_cast<WebBrowserComponent*> (Component::getParentComponent());
jassert (owner != nullptr);
auto handler = new EventHandler (*owner);
connectionPoint->Advise (handler, &adviseCookie);
handler->Release();
}
}
}
bool hasBrowserBeenCreated() override
{
return browser != nullptr;
}
void goToURL (const String& url, const StringArray* headers, const MemoryBlock* postData) override
{
if (browser != nullptr)
{
VARIANT headerFlags, frame, postDataVar, headersVar; // (_variant_t isn't available in all compilers)
VariantInit (&headerFlags);
VariantInit (&frame);
VariantInit (&postDataVar);
VariantInit (&headersVar);
if (headers != nullptr)
{
V_VT (&headersVar) = VT_BSTR;
V_BSTR (&headersVar) = SysAllocString ((const OLECHAR*) headers->joinIntoString ("\r\n").toWideCharPointer());
}
if (postData != nullptr && postData->getSize() > 0)
{
auto sa = SafeArrayCreateVector (VT_UI1, 0, (ULONG) postData->getSize());
if (sa != nullptr)
{
void* data = nullptr;
SafeArrayAccessData (sa, &data);
jassert (data != nullptr);
if (data != nullptr)
{
postData->copyTo (data, 0, postData->getSize());
SafeArrayUnaccessData (sa);
VARIANT postDataVar2;
VariantInit (&postDataVar2);
V_VT (&postDataVar2) = VT_ARRAY | VT_UI1;
V_ARRAY (&postDataVar2) = sa;
sa = nullptr;
postDataVar = postDataVar2;
}
else
{
SafeArrayDestroy (sa);
}
}
}
auto urlBSTR = SysAllocString ((const OLECHAR*) url.toWideCharPointer());
browser->Navigate (urlBSTR, &headerFlags, &frame, &postDataVar, &headersVar);
SysFreeString (urlBSTR);
VariantClear (&headerFlags);
VariantClear (&frame);
VariantClear (&postDataVar);
VariantClear (&headersVar);
}
}
void stop() override
{
if (browser != nullptr)
browser->Stop();
}
void goBack() override
{
if (browser != nullptr)
browser->GoBack();
}
void goForward() override
{
if (browser != nullptr)
browser->GoForward();
}
void refresh() override
{
if (browser != nullptr)
browser->Refresh();
}
void focusGained() override
{
auto iidOleObject = __uuidof (IOleObject);
auto iidOleWindow = __uuidof (IOleWindow);
if (auto oleObject = (IOleObject*) queryInterface (&iidOleObject))
{
if (auto oleWindow = (IOleWindow*) queryInterface (&iidOleWindow))
{
IOleClientSite* oleClientSite = nullptr;
if (SUCCEEDED (oleObject->GetClientSite (&oleClientSite)))
{
HWND hwnd;
oleWindow->GetWindow (&hwnd);
oleObject->DoVerb (OLEIVERB_UIACTIVATE, nullptr, oleClientSite, 0, hwnd, nullptr);
oleClientSite->Release();
}
oleWindow->Release();
}
oleObject->Release();
}
}
using ActiveXControlComponent::focusGained;
void setWebViewSize (int width, int height) override
{
setSize (width, height);
}
private:
IWebBrowser2* browser = nullptr;
IConnectionPoint* connectionPoint = nullptr;
DWORD adviseCookie = 0;
//==============================================================================
struct EventHandler : public ComBaseClassHelper<IDispatch>,
public ComponentMovementWatcher
{
EventHandler (WebBrowserComponent& w) : ComponentMovementWatcher (&w), owner (w) {}
JUCE_COMRESULT GetTypeInfoCount (UINT*) override { return E_NOTIMPL; }
JUCE_COMRESULT GetTypeInfo (UINT, LCID, ITypeInfo**) override { return E_NOTIMPL; }
JUCE_COMRESULT GetIDsOfNames (REFIID, LPOLESTR*, UINT, LCID, DISPID*) override { return E_NOTIMPL; }
JUCE_COMRESULT Invoke (DISPID dispIdMember, REFIID /*riid*/, LCID /*lcid*/, WORD /*wFlags*/, DISPPARAMS* pDispParams,
VARIANT* /*pVarResult*/, EXCEPINFO* /*pExcepInfo*/, UINT* /*puArgErr*/) override
{
if (dispIdMember == DISPID_BEFORENAVIGATE2)
{
*pDispParams->rgvarg->pboolVal
= owner.pageAboutToLoad (getStringFromVariant (pDispParams->rgvarg[5].pvarVal)) ? VARIANT_FALSE
: VARIANT_TRUE;
return S_OK;
}
if (dispIdMember == 273 /*DISPID_NEWWINDOW3*/)
{
owner.newWindowAttemptingToLoad (pDispParams->rgvarg[0].bstrVal);
*pDispParams->rgvarg[3].pboolVal = VARIANT_TRUE;
return S_OK;
}
if (dispIdMember == DISPID_DOCUMENTCOMPLETE)
{
owner.pageFinishedLoading (getStringFromVariant (pDispParams->rgvarg[0].pvarVal));
return S_OK;
}
if (dispIdMember == 271 /*DISPID_NAVIGATEERROR*/)
{
int statusCode = pDispParams->rgvarg[1].pvarVal->intVal;
*pDispParams->rgvarg[0].pboolVal = VARIANT_FALSE;
// IWebBrowser2 also reports http status codes here, we need
// report only network errors
if (statusCode < 0)
{
LPTSTR messageBuffer = nullptr;
auto size = FormatMessage (FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, statusCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR) &messageBuffer, 0, nullptr);
String message (messageBuffer, size);
LocalFree (messageBuffer);
if (! owner.pageLoadHadNetworkError (message))
*pDispParams->rgvarg[0].pboolVal = VARIANT_TRUE;
}
return S_OK;
}
if (dispIdMember == 263 /*DISPID_WINDOWCLOSING*/)
{
owner.windowCloseRequest();
// setting this bool tells the browser to ignore the event - we'll handle it.
if (pDispParams->cArgs > 0 && pDispParams->rgvarg[0].vt == (VT_BYREF | VT_BOOL))
*pDispParams->rgvarg[0].pboolVal = VARIANT_TRUE;
return S_OK;
}
return E_NOTIMPL;
}
void componentMovedOrResized (bool, bool) override {}
void componentPeerChanged() override {}
void componentVisibilityChanged() override { owner.visibilityChanged(); }
private:
WebBrowserComponent& owner;
static String getStringFromVariant (VARIANT* v)
{
return (v->vt & VT_BYREF) != 0 ? *v->pbstrVal
: v->bstrVal;
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EventHandler)
};
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Win32WebView)
};
#if JUCE_USE_WIN_WEBVIEW2
using namespace Microsoft::WRL;
class WebView2 : public InternalWebViewType,
public Component,
public ComponentMovementWatcher
{
public:
WebView2 (WebBrowserComponent& o)
: ComponentMovementWatcher (&o),
owner (o)
{
if (! WinRTWrapper::getInstance()->isInitialised())
throw std::runtime_error ("Failed to initialise the WinRT wrapper");
if (! createWebViewEnvironment())
throw std::runtime_error ("Failed to create the CoreWebView2Environemnt");
owner.addAndMakeVisible (this);
}
~WebView2() override
{
removeEventHandlers();
closeWebView();
}
void createBrowser() override
{
if (webView == nullptr)
{
jassert (webViewEnvironment != nullptr);
createWebView();
}
}
bool hasBrowserBeenCreated() override
{
return webView != nullptr || isCreating;
}
void goToURL (const String& url, const StringArray* headers, const MemoryBlock* postData) override
{
urlRequest = { url,
headers != nullptr ? *headers : StringArray(),
postData != nullptr && postData->getSize() > 0 ? *postData : MemoryBlock() };
if (webView != nullptr)
webView->Navigate (urlRequest.url.toWideCharPointer());
}
void stop() override
{
if (webView != nullptr)
webView->Stop();
}
void goBack() override
{
if (webView != nullptr)
{
BOOL canGoBack = false;
webView->get_CanGoBack (&canGoBack);
if (canGoBack)
webView->GoBack();
}
}
void goForward() override
{
if (webView != nullptr)
{
BOOL canGoForward = false;
webView->get_CanGoForward (&canGoForward);
if (canGoForward)
webView->GoForward();
}
}
void refresh() override
{
if (webView != nullptr)
webView->Reload();
}
void setWebViewSize (int width, int height) override
{
setSize (width, height);
}
void componentMovedOrResized (bool /*wasMoved*/, bool /*wasResized*/) override
{
if (auto* peer = owner.getTopLevelComponent()->getPeer())
setControlBounds (peer->getAreaCoveredBy (owner));
}
void componentPeerChanged() override
{
componentMovedOrResized (true, true);
}
void componentVisibilityChanged() override
{
setControlVisible (owner.isShowing());
componentPeerChanged();
owner.visibilityChanged();
}
private:
//==============================================================================
template<class ArgType>
static String getUriStringFromArgs (ArgType* args)
{
if (args != nullptr)
{
LPWSTR uri;
args->get_Uri (&uri);
return uri;
}
return {};
}
//==============================================================================
void addEventHandlers()
{
if (webView != nullptr)
{
webView->add_NavigationStarting (Callback<ICoreWebView2NavigationStartingEventHandler> (
[this] (ICoreWebView2*, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT
{
auto uriString = getUriStringFromArgs (args);
if (uriString.isNotEmpty() && ! owner.pageAboutToLoad (uriString))
args->put_Cancel (true);
return S_OK;
}).Get(), &navigationStartingToken);
webView->add_NewWindowRequested (Callback<ICoreWebView2NewWindowRequestedEventHandler> (
[this] (ICoreWebView2*, ICoreWebView2NewWindowRequestedEventArgs* args) -> HRESULT
{
auto uriString = getUriStringFromArgs (args);
if (uriString.isNotEmpty())
{
owner.newWindowAttemptingToLoad (uriString);
args->put_Handled (true);
}
return S_OK;
}).Get(), &newWindowRequestedToken);
webView->add_WindowCloseRequested (Callback<ICoreWebView2WindowCloseRequestedEventHandler> (
[this] (ICoreWebView2*, IUnknown*) -> HRESULT
{
owner.windowCloseRequest();
return S_OK;
}).Get(), &windowCloseRequestedToken);
webView->add_NavigationCompleted (Callback<ICoreWebView2NavigationCompletedEventHandler> (
[this] (ICoreWebView2* sender, ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT
{
LPWSTR uri;
sender->get_Source (&uri);
String uriString (uri);
if (uriString.isNotEmpty())
{
BOOL success = false;
args->get_IsSuccess (&success);
COREWEBVIEW2_WEB_ERROR_STATUS errorStatus;
args->get_WebErrorStatus (&errorStatus);
if (success
|| errorStatus == COREWEBVIEW2_WEB_ERROR_STATUS_OPERATION_CANCELED) // this error seems to happen erroneously so ignore
{
owner.pageFinishedLoading (uriString);
}
else
{
auto errorString = "Error code: " + String (errorStatus);
if (owner.pageLoadHadNetworkError (errorString))
owner.goToURL ("data:text/plain;charset=UTF-8," + errorString);
}
}
return S_OK;
}).Get(), &navigationCompletedToken);
webView->AddWebResourceRequestedFilter (L"*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_DOCUMENT);
webView->add_WebResourceRequested (Callback<ICoreWebView2WebResourceRequestedEventHandler> (
[this] (ICoreWebView2*, ICoreWebView2WebResourceRequestedEventArgs* args) -> HRESULT
{
if (urlRequest.url.isEmpty())
return S_OK;
WinRTWrapper::ComPtr<ICoreWebView2WebResourceRequest> request;
args->get_Request (request.resetAndGetPointerAddress());
auto uriString = getUriStringFromArgs (request.get());
if (uriString == urlRequest.url
|| (uriString.endsWith ("/") && uriString.upToLastOccurrenceOf ("/", false, false) == urlRequest.url))
{
String method ("GET");
if (urlRequest.postData.getSize() > 0)
{
method = "POST";
WinRTWrapper::ComPtr<IStream> content (SHCreateMemStream ((BYTE*) urlRequest.postData.getData(),
(UINT) urlRequest.postData.getSize()));
request->put_Content (content.get());
}
if (! urlRequest.headers.isEmpty())
{
WinRTWrapper::ComPtr<ICoreWebView2HttpRequestHeaders> headers;
request->get_Headers (headers.resetAndGetPointerAddress());
for (auto& header : urlRequest.headers)
{
headers->SetHeader (header.upToFirstOccurrenceOf (":", false, false).trim().toWideCharPointer(),
header.fromFirstOccurrenceOf (":", false, false).trim().toWideCharPointer());
}
}
request->put_Method (method.toWideCharPointer());
urlRequest = {};
}
return S_OK;
}).Get(), &webResourceRequestedToken);
}
}
void removeEventHandlers()
{
if (webView != nullptr)
{
if (navigationStartingToken.value != 0)
webView->remove_NavigationStarting (navigationStartingToken);
if (newWindowRequestedToken.value != 0)
webView->remove_NewWindowRequested (newWindowRequestedToken);
if (windowCloseRequestedToken.value != 0)
webView->remove_WindowCloseRequested (windowCloseRequestedToken);
if (navigationCompletedToken.value != 0)
webView->remove_NavigationCompleted (navigationCompletedToken);
if (webResourceRequestedToken.value != 0)
{
webView->RemoveWebResourceRequestedFilter (L"*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_DOCUMENT);
webView->remove_WebResourceRequested (webResourceRequestedToken);
}
}
}
bool createWebViewEnvironment()
{
auto options = Microsoft::WRL::Make<CoreWebView2EnvironmentOptions>();
auto hr = CreateCoreWebView2EnvironmentWithOptions (nullptr, nullptr, options.Get(),
Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
[this] (HRESULT, ICoreWebView2Environment* env) -> HRESULT
{
webViewEnvironment = env;
return S_OK;
}).Get());
return SUCCEEDED (hr);
}
void createWebView()
{
isCreating = true;
if (auto* peer = getPeer())
{
webViewEnvironment->CreateCoreWebView2Controller ((HWND) peer->getNativeHandle(),
Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler> (
[this] (HRESULT, ICoreWebView2Controller* controller) -> HRESULT
{
webViewController = controller;
controller->get_CoreWebView2 (webView.resetAndGetPointerAddress());
isCreating = false;
addEventHandlers();
componentMovedOrResized (true, true);
if (webView != nullptr && urlRequest.url.isNotEmpty())
webView->Navigate (urlRequest.url.toWideCharPointer());
return S_OK;
}).Get());
}
}
void closeWebView()
{
if (webViewController.get() != nullptr)
{
webViewController->Close();
webViewController = nullptr;
webView = nullptr;
}
webViewEnvironment = nullptr;
}
//==============================================================================
void setControlBounds (Rectangle<int> newBounds) const
{
if (webViewController != nullptr)
{
#if JUCE_WIN_PER_MONITOR_DPI_AWARE
if (auto* peer = owner.getTopLevelComponent()->getPeer())
newBounds = (newBounds.toDouble() * peer->getPlatformScaleFactor()).toNearestInt();
#endif
webViewController->put_Bounds({ newBounds.getX(), newBounds.getY(),
newBounds.getRight(), newBounds.getBottom() });
}
}
void setControlVisible (bool shouldBeVisible) const
{
if (webViewController != nullptr)
webViewController->put_IsVisible (shouldBeVisible);
}
//==============================================================================
WebBrowserComponent& owner;
WinRTWrapper::ComPtr<ICoreWebView2Environment> webViewEnvironment;
WinRTWrapper::ComPtr<ICoreWebView2Controller> webViewController;
WinRTWrapper::ComPtr<ICoreWebView2> webView;
EventRegistrationToken navigationStartingToken { 0 },
newWindowRequestedToken { 0 },
windowCloseRequestedToken { 0 },
navigationCompletedToken { 0 },
webResourceRequestedToken { 0 };
struct URLRequest
{
String url;
StringArray headers;
MemoryBlock postData;
};
URLRequest urlRequest;
bool isCreating = false;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WebView2)
};
#endif
//==============================================================================
class WebBrowserComponent::Pimpl
{
public:
Pimpl (WebBrowserComponent& owner)
{
#if JUCE_USE_WIN_WEBVIEW2
try
{
internal.reset (new WebView2 (owner));
}
catch (std::runtime_error&) {}
#endif
if (internal == nullptr)
internal.reset (new Win32WebView (owner));
}
InternalWebViewType& getInternalWebView()
{
return *internal;
}
private:
std::unique_ptr<InternalWebViewType> internal;
};
//==============================================================================
WebBrowserComponent::WebBrowserComponent (bool unloadWhenHidden)
: browser (new Pimpl (*this)),
unloadPageWhenBrowserIsHidden (unloadWhenHidden)
{
setOpaque (true);
}
WebBrowserComponent::~WebBrowserComponent()
{
}
//==============================================================================
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;
if (! browser->getInternalWebView().hasBrowserBeenCreated())
checkWindowAssociation();
browser->getInternalWebView().goToURL (url, headers, postData);
}
void WebBrowserComponent::stop()
{
browser->getInternalWebView().stop();
}
void WebBrowserComponent::goBack()
{
lastURL.clear();
blankPageShown = false;
browser->getInternalWebView().goBack();
}
void WebBrowserComponent::goForward()
{
lastURL.clear();
browser->getInternalWebView().goForward();
}
void WebBrowserComponent::refresh()
{
browser->getInternalWebView().refresh();
}
//==============================================================================
void WebBrowserComponent::paint (Graphics& g)
{
if (! browser->getInternalWebView().hasBrowserBeenCreated())
{
g.fillAll (Colours::white);
checkWindowAssociation();
}
}
void WebBrowserComponent::checkWindowAssociation()
{
if (isShowing())
{
if (! browser->getInternalWebView().hasBrowserBeenCreated() && getPeer() != nullptr)
{
browser->getInternalWebView().createBrowser();
reloadLastURL();
}
else
{
if (blankPageShown)
goBack();
}
}
else
{
if (browser != nullptr && unloadPageWhenBrowserIsHidden && ! 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..
blankPageShown = true;
browser->getInternalWebView().goToURL ("about:blank", 0, 0);
}
}
}
void WebBrowserComponent::reloadLastURL()
{
if (lastURL.isNotEmpty())
{
goToURL (lastURL, &lastHeaders, &lastPostData);
lastURL.clear();
}
}
void WebBrowserComponent::parentHierarchyChanged()
{
checkWindowAssociation();
}
void WebBrowserComponent::resized()
{
browser->getInternalWebView().setWebViewSize (getWidth(), getHeight());
}
void WebBrowserComponent::visibilityChanged()
{
checkWindowAssociation();
}
void WebBrowserComponent::focusGained (FocusChangeType)
{
browser->getInternalWebView().focusGained();
}
void WebBrowserComponent::clearCookies()
{
HeapBlock<::INTERNET_CACHE_ENTRY_INFOA> entry;
::DWORD entrySize = sizeof (::INTERNET_CACHE_ENTRY_INFOA);
::HANDLE urlCacheHandle = ::FindFirstUrlCacheEntryA ("cookie:", entry.getData(), &entrySize);
if (urlCacheHandle == nullptr && GetLastError() == ERROR_INSUFFICIENT_BUFFER)
{
entry.realloc (1, entrySize);
urlCacheHandle = ::FindFirstUrlCacheEntryA ("cookie:", entry.getData(), &entrySize);
}
if (urlCacheHandle != nullptr)
{
for (;;)
{
::DeleteUrlCacheEntryA (entry.getData()->lpszSourceUrlName);
if (::FindNextUrlCacheEntryA (urlCacheHandle, entry.getData(), &entrySize) == 0)
{
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
{
entry.realloc (1, entrySize);
if (::FindNextUrlCacheEntryA (urlCacheHandle, entry.getData(), &entrySize) != 0)
continue;
}
break;
}
}
FindCloseUrlCache (urlCacheHandle);
}
}
} // namespace juce