/* ============================================================================== This file is part of the JUCE 7 technical preview. Copyright (c) 2022 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For the technical preview this file cannot be licensed commercially. 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() = default; virtual ~InternalWebViewType() = default; 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) }; //============================================================================== 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 { JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token") 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) { if (auto* owner = dynamic_cast (Component::getParentComponent())) { auto handler = new EventHandler (*owner); connectionPoint->Advise (handler, &adviseCookie); handler->Release(); } } } JUCE_END_IGNORE_WARNINGS_GCC_LIKE } 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 { JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token") 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(); } JUCE_END_IGNORE_WARNINGS_GCC_LIKE } 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, 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, (DWORD) 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(); } using ComponentMovementWatcher::componentVisibilityChanged; using ComponentMovementWatcher::componentMovedOrResized; 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, const WebView2Preferences& prefs) : ComponentMovementWatcher (&o), owner (o), preferences (prefs) { if (! createWebViewEnvironment()) throw std::runtime_error ("Failed to create the CoreWebView2Environemnt"); owner.addAndMakeVisible (this); } ~WebView2() override { removeEventHandlers(); closeWebView(); if (webView2LoaderHandle != nullptr) ::FreeLibrary (webView2LoaderHandle); } 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 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 ( [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 ( [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 ( [this] (ICoreWebView2*, IUnknown*) -> HRESULT { owner.windowCloseRequest(); return S_OK; }).Get(), &windowCloseRequestedToken); webView->add_NavigationCompleted (Callback ( [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 ( [this] (ICoreWebView2*, ICoreWebView2WebResourceRequestedEventArgs* args) -> HRESULT { if (urlRequest.url.isEmpty()) return S_OK; ComSmartPtr request; args->get_Request (request.resetAndGetPointerAddress()); auto uriString = getUriStringFromArgs (request); if (uriString == urlRequest.url || (uriString.endsWith ("/") && uriString.upToLastOccurrenceOf ("/", false, false) == urlRequest.url)) { String method ("GET"); if (! urlRequest.postData.isEmpty()) { method = "POST"; ComSmartPtr content (SHCreateMemStream ((BYTE*) urlRequest.postData.getData(), (UINT) urlRequest.postData.getSize())); request->put_Content (content); } if (! urlRequest.headers.isEmpty()) { ComSmartPtr 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); } } } void setWebViewPreferences() { ComSmartPtr controller2; webViewController->QueryInterface (controller2.resetAndGetPointerAddress()); if (controller2 != nullptr) { const auto bgColour = preferences.getBackgroundColour(); controller2->put_DefaultBackgroundColor ({ (BYTE) bgColour.getAlpha(), (BYTE) bgColour.getRed(), (BYTE) bgColour.getGreen(), (BYTE) bgColour.getBlue() }); } ComSmartPtr settings; webView->get_Settings (settings.resetAndGetPointerAddress()); if (settings != nullptr) { settings->put_IsStatusBarEnabled (! preferences.getIsStatusBarDisabled()); settings->put_IsBuiltInErrorPageEnabled (! preferences.getIsBuiltInErrorPageDisabled()); } } bool createWebViewEnvironment() { using CreateWebViewEnvironmentWithOptionsFunc = HRESULT (*) (PCWSTR, PCWSTR, ICoreWebView2EnvironmentOptions*, ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler*); auto dllPath = preferences.getDLLLocation().getFullPathName(); if (dllPath.isEmpty()) dllPath = "WebView2Loader.dll"; webView2LoaderHandle = LoadLibraryA (dllPath.toUTF8()); if (webView2LoaderHandle == nullptr) return false; auto* createWebViewEnvironmentWithOptions = (CreateWebViewEnvironmentWithOptionsFunc) GetProcAddress (webView2LoaderHandle, "CreateCoreWebView2EnvironmentWithOptions"); if (createWebViewEnvironmentWithOptions == nullptr) { // failed to load WebView2Loader.dll jassertfalse; return false; } auto options = Microsoft::WRL::Make(); const auto userDataFolder = preferences.getUserDataFolder().getFullPathName(); auto hr = createWebViewEnvironmentWithOptions (nullptr, userDataFolder.isNotEmpty() ? userDataFolder.toWideCharPointer() : nullptr, options.Get(), Callback( [weakThis = WeakReference { this }] (HRESULT, ICoreWebView2Environment* env) -> HRESULT { if (weakThis != nullptr) weakThis->webViewEnvironment = env; return S_OK; }).Get()); return SUCCEEDED (hr); } void createWebView() { if (auto* peer = getPeer()) { isCreating = true; WeakReference weakThis (this); webViewEnvironment->CreateCoreWebView2Controller ((HWND) peer->getNativeHandle(), Callback ( [weakThis = WeakReference { this }] (HRESULT, ICoreWebView2Controller* controller) -> HRESULT { if (weakThis != nullptr) { weakThis->isCreating = false; if (controller != nullptr) { weakThis->webViewController = controller; controller->get_CoreWebView2 (weakThis->webView.resetAndGetPointerAddress()); if (weakThis->webView != nullptr) { weakThis->addEventHandlers(); weakThis->setWebViewPreferences(); weakThis->componentMovedOrResized (true, true); if (weakThis->urlRequest.url.isNotEmpty()) weakThis->webView->Navigate (weakThis->urlRequest.url.toWideCharPointer()); } } } return S_OK; }).Get()); } } void closeWebView() { if (webViewController != nullptr) { webViewController->Close(); webViewController = nullptr; webView = nullptr; } webViewEnvironment = nullptr; } //============================================================================== void setControlBounds (Rectangle 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; WebView2Preferences preferences; HMODULE webView2LoaderHandle = nullptr; ComSmartPtr webViewEnvironment; ComSmartPtr webViewController; ComSmartPtr 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_WEAK_REFERENCEABLE (WebView2) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WebView2) }; #endif //============================================================================== class WebBrowserComponent::Pimpl { public: Pimpl (WebBrowserComponent& owner, const WebView2Preferences& preferences, bool useWebView2) { if (useWebView2) { #if JUCE_USE_WIN_WEBVIEW2 try { internal.reset (new WebView2 (owner, preferences)); } catch (const std::runtime_error&) {} #endif } ignoreUnused (preferences); if (internal == nullptr) internal.reset (new Win32WebView (owner)); } InternalWebViewType& getInternalWebView() { return *internal; } private: std::unique_ptr internal; }; //============================================================================== WebBrowserComponent::WebBrowserComponent (bool unloadWhenHidden) : browser (new Pimpl (*this, {}, false)), unloadPageWhenHidden (unloadWhenHidden) { setOpaque (true); } WebBrowserComponent::WebBrowserComponent (ConstructWithoutPimpl args) : unloadPageWhenHidden (args.unloadWhenHidden) { setOpaque (true); } WebBrowserComponent::~WebBrowserComponent() { } WindowsWebView2WebBrowserComponent::WindowsWebView2WebBrowserComponent (bool unloadWhenHidden, const WebView2Preferences& preferences) : WebBrowserComponent (ConstructWithoutPimpl { unloadWhenHidden }) { browser = std::make_unique (*this, preferences, true); } //============================================================================== 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 && 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.. blankPageShown = true; browser->getInternalWebView().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->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