/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). 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 final : public InternalWebViewType, public ActiveXControlComponent { public: Win32WebView (WebBrowserComponent& parent, const String& userAgentToUse) : owner (parent), userAgent (userAgentToUse) { owner.addAndMakeVisible (this); } ~Win32WebView() override { if (connectionPoint != nullptr) { connectionPoint->Unadvise (adviseCookie); connectionPoint->Release(); } 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); auto iidOleControl = __uuidof (IOleControl); browser = (IWebBrowser2*) queryInterface (&iidWebBrowser2); if (auto connectionPointContainer = (IConnectionPointContainer*) queryInterface (&iidConnectionPointContainer)) { connectionPointContainer->FindConnectionPoint (__uuidof (DWebBrowserEvents2), &connectionPoint); if (connectionPoint != nullptr) { auto handler = new EventHandler (*this); connectionPoint->Advise (handler, &adviseCookie); setEventHandler (handler); handler->Release(); } connectionPointContainer->Release(); } if (auto oleControl = (IOleControl*) queryInterface (&iidOleControl)) { oleControl->OnAmbientPropertyChange (/*DISPID_AMBIENT_USERAGENT*/-5513); oleControl->Release(); } JUCE_END_IGNORE_WARNINGS_GCC_LIKE } bool hasBrowserBeenCreated() override { return browser != nullptr; } void goToURL (const String& url, const StringArray* requestedHeaders, 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); StringArray headers; if (userAgent.isNotEmpty()) headers.add ("User-Agent: " + userAgent); if (requestedHeaders != nullptr) headers.addArray (*requestedHeaders); if (headers.size() > 0) { 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: WebBrowserComponent& owner; IWebBrowser2* browser = nullptr; IConnectionPoint* connectionPoint = nullptr; DWORD adviseCookie = 0; String userAgent; //============================================================================== struct EventHandler final : public ComBaseClassHelper, public ComponentMovementWatcher { EventHandler (Win32WebView& w) : ComponentMovementWatcher (&w.owner), 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_AMBIENT_USERAGENT*/-5513) { V_VT ( pVarResult ) = VT_BSTR; V_BSTR ( pVarResult ) = SysAllocString ((const OLECHAR*) String (owner.userAgent).toWideCharPointer());; return S_OK; } if (dispIdMember == DISPID_BEFORENAVIGATE2) { *pDispParams->rgvarg->pboolVal = owner.owner.pageAboutToLoad (getStringFromVariant (pDispParams->rgvarg[5].pvarVal)) ? VARIANT_FALSE : VARIANT_TRUE; return S_OK; } if (dispIdMember == 273 /*DISPID_NEWWINDOW3*/) { owner.owner.newWindowAttemptingToLoad (pDispParams->rgvarg[0].bstrVal); *pDispParams->rgvarg[3].pboolVal = VARIANT_TRUE; return S_OK; } if (dispIdMember == DISPID_DOCUMENTCOMPLETE) { owner.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.owner.pageLoadHadNetworkError (message)) *pDispParams->rgvarg[0].pboolVal = VARIANT_TRUE; } return S_OK; } if (dispIdMember == 263 /*DISPID_WINDOWCLOSING*/) { owner.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: Win32WebView& 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; static std::vector getDirectChildWindows (HWND hwnd) { std::vector result; const auto getNextChildWindow = [hwnd, &result] { return FindWindowExA (hwnd, result.empty() ? nullptr : result.back(), nullptr, nullptr); }; for (auto* next = getNextChildWindow(); next != nullptr; next = getNextChildWindow()) result.push_back (next); return result; } static void forEachChildWindowRecursive (HWND hwnd, std::function callback) { // EnumChildWindows itself provides the recursion EnumChildWindows (hwnd, [] (HWND hwnd, LPARAM lParam) { auto* callbackPtr = reinterpret_cast*> (lParam); return (*callbackPtr) (hwnd) ? TRUE : FALSE; }, reinterpret_cast (&callback)); } static bool anyChildWindow (HWND hwnd, std::function predicate) { auto result = false; forEachChildWindowRecursive (hwnd, [&predicate, &result] (auto* child) { result = predicate (child); const auto keepGoing = ! result; return keepGoing; }); return result; } class WebView2 final : public InternalWebViewType, public Component, public ComponentMovementWatcher, private AsyncUpdater { public: WebView2 (WebBrowserComponent& o, const WebBrowserComponent::Options& prefs) : ComponentMovementWatcher (&o), owner (o), preferences (prefs.getWinWebView2BackendOptions()), userAgent (prefs.getUserAgent()) { if (auto handle = createWebViewHandle (preferences)) webViewHandle = std::move (*handle); else throw std::runtime_error ("Failed to create the CoreWebView2Environment"); owner.addAndMakeVisible (this); } void focusGainedWithDirection (FocusChangeType, FocusChangeDirection direction) override { if (inMoveFocusRequested) return; const auto moveFocusReason = [&] { if (direction == FocusChangeDirection::backward) return COREWEBVIEW2_MOVE_FOCUS_REASON_PREVIOUS; if (direction == FocusChangeDirection::forward) return COREWEBVIEW2_MOVE_FOCUS_REASON_NEXT; return COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC; }(); if (webViewController != nullptr) webViewController->MoveFocus (moveFocusReason); } ~WebView2() override { removeEventHandlers(); closeWebView(); } void createBrowser() override { if (webView == nullptr) { jassert (webViewHandle.environment != nullptr); createWebView(); } } bool hasBrowserBeenCreated() override { return webView != nullptr || webView2ConstructionHelper.webView2BeingCreated == this || webView2ConstructionHelper.viewsWaitingForCreation.count (this) > 0; } 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(); } std::unique_ptr createAccessibilityHandler() override { return std::make_unique (*this, AccessibilityRole::group); } //============================================================================== struct WebViewHandle { using LibraryRef = std::unique_ptr::element_type, decltype (&::FreeLibrary)>; LibraryRef loaderHandle {nullptr, &::FreeLibrary}; ComSmartPtr environment; }; static std::optional createWebViewHandle (const WebBrowserComponent::Options::WinWebView2& options) { using CreateWebViewEnvironmentWithOptionsFunc = HRESULT (*) (PCWSTR, PCWSTR, ICoreWebView2EnvironmentOptions*, ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler*); auto dllPath = options.getDLLLocation().getFullPathName(); if (dllPath.isEmpty()) dllPath = "WebView2Loader.dll"; WebViewHandle result; result.loaderHandle = WebViewHandle::LibraryRef (LoadLibraryA (dllPath.toUTF8()), &::FreeLibrary); if (result.loaderHandle == nullptr) return {}; auto* createWebViewEnvironmentWithOptions = (CreateWebViewEnvironmentWithOptionsFunc) GetProcAddress (result.loaderHandle.get(), "CreateCoreWebView2EnvironmentWithOptions"); if (createWebViewEnvironmentWithOptions == nullptr) return {}; auto webViewOptions = Microsoft::WRL::Make(); const auto userDataFolder = options.getUserDataFolder().getFullPathName(); auto hr = createWebViewEnvironmentWithOptions (nullptr, userDataFolder.isNotEmpty() ? userDataFolder.toWideCharPointer() : nullptr, webViewOptions.Get(), Callback( [&result] (HRESULT, ICoreWebView2Environment* env) -> HRESULT { result.environment = env; return S_OK; }).Get()); if (! SUCCEEDED (hr)) return {}; return result; } 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); } if (webViewController != nullptr) { webViewController->add_MoveFocusRequested (Callback ( [this] (ICoreWebView2Controller*, ICoreWebView2MoveFocusRequestedEventArgs* args) -> HRESULT { ScopedValueSetter scope { inMoveFocusRequested, true }; auto* comp = [&]() -> Component* { auto* c = owner.getParentComponent(); if (c == nullptr) return nullptr; const auto traverser = c->createFocusTraverser(); if (COREWEBVIEW2_MOVE_FOCUS_REASON reason; args->get_Reason (&reason) == S_OK && reason == COREWEBVIEW2_MOVE_FOCUS_REASON_PREVIOUS) { // The previous Component to the embedded WebView2 Component is the // WebBrowserComponent. Here we want to skip that and jump to the // Component that comes before it. return traverser->getPreviousComponent (&owner); } // The Component that comes immediately after the WebBrowserComponent is the // embedded WebView2. We want to jump to the Component that comes after that. return traverser->getNextComponent (this); }(); if (comp != nullptr) comp->getAccessibilityHandler()->grabFocus(); else giveAwayKeyboardFocus(); return S_OK; }).Get(), &moveFocusRequestedToken); } } 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); } } if (webViewController != nullptr) { if (moveFocusRequestedToken.value != 0) webViewController->remove_MoveFocusRequested (moveFocusRequestedToken); } } 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()); if (userAgent.isNotEmpty()) { ComSmartPtr settings2; settings.QueryInterface (settings2); if (settings2 != nullptr) settings2->put_UserAgent (userAgent.toWideCharPointer()); } } } void createWebView() { if (auto* peer = getPeer()) { // We enforce the serial creation of WebView2 instances so that our HWND association // logic can work. Multiple HWNDs can belong to the same browser process, so the only // way to identify which belongs to which WebView2 is to associate them with each other // in the order of creation. if (webView2ConstructionHelper.webView2BeingCreated != nullptr) { webView2ConstructionHelper.viewsWaitingForCreation.insert (this); return; } webView2ConstructionHelper.viewsWaitingForCreation.erase (this); webView2ConstructionHelper.webView2BeingCreated = this; WeakReference weakThis (this); webViewHandle.environment->CreateCoreWebView2Controller ((HWND) peer->getNativeHandle(), Callback ( [weakThis = WeakReference { this }] (HRESULT, ICoreWebView2Controller* controller) -> HRESULT { if (weakThis != nullptr) { webView2ConstructionHelper.webView2BeingCreated = nullptr; if (controller != nullptr) { weakThis->webViewController = controller; controller->get_CoreWebView2 (weakThis->webView.resetAndGetPointerAddress()); if (weakThis->webView != nullptr) { if (UINT32 browserProcessId; weakThis->webView->get_BrowserProcessId (&browserProcessId) == S_OK) { auto* self = weakThis.get(); auto* webView2WindowHandle = static_cast (self->getWindowHandle()); // There is no WebView2 API for getting the HWND hosting // the WebView2 content. So we iterate over all child // windows of the JUCE peer HWND, and try to figure out // which one belongs to a WebView2. What we are looking for // is a window that has a child window that belongs to the // browserProcessId. const auto directChildWindows = getDirectChildWindows (webView2WindowHandle); for (auto* childWindow : directChildWindows) { if (self->webView2ConstructionHelper.associatedWebViewNativeWindows.count (childWindow) == 0) { if (anyChildWindow (childWindow, [browserProcessId] (auto* childOfChild) { if (DWORD procId; GetWindowThreadProcessId (childOfChild, &procId) != 0) return (UINT32) procId == browserProcessId; return false; })) { webView2ConstructionHelper.associatedWebViewNativeWindows.insert (childWindow); AccessibilityHandler::setNativeChildForComponent (*self, childWindow); } } } } weakThis->addEventHandlers(); weakThis->setWebViewPreferences(); weakThis->componentMovedOrResized (true, true); if (weakThis->urlRequest.url.isNotEmpty()) weakThis->webView->Navigate (weakThis->urlRequest.url.toWideCharPointer()); } } if (! weakThis->webView2ConstructionHelper.viewsWaitingForCreation.empty()) (*weakThis->webView2ConstructionHelper.viewsWaitingForCreation.begin())->triggerAsyncUpdate(); } return S_OK; }).Get()); } } void closeWebView() { if (auto* webViewNativeWindow = AccessibilityHandler::getNativeChildForComponent (*this)) webView2ConstructionHelper.associatedWebViewNativeWindows.erase (webViewNativeWindow); AccessibilityHandler::setNativeChildForComponent (*this, nullptr); if (webViewController != nullptr) { webViewController->Close(); webViewController = nullptr; webView = nullptr; } webViewHandle.environment = nullptr; } //============================================================================== void handleAsyncUpdate() override { createWebView(); } //============================================================================== 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; WebBrowserComponent::Options::WinWebView2 preferences; String userAgent; WebViewHandle webViewHandle; ComSmartPtr webViewController; ComSmartPtr webView; EventRegistrationToken navigationStartingToken { 0 }, newWindowRequestedToken { 0 }, windowCloseRequestedToken { 0 }, navigationCompletedToken { 0 }, webResourceRequestedToken { 0 }, moveFocusRequestedToken { 0 }; bool inMoveFocusRequested = false; struct URLRequest { String url; StringArray headers; MemoryBlock postData; }; URLRequest urlRequest; struct WebView2ConstructionHelper { WebView2* webView2BeingCreated; std::set viewsWaitingForCreation; std::set associatedWebViewNativeWindows; }; inline static WebView2ConstructionHelper webView2ConstructionHelper; NativeScaleFactorNotifier scaleFactorNotifier { this, [this] (auto) { componentMovedOrResized (true, true); } }; //============================================================================== JUCE_DECLARE_WEAK_REFERENCEABLE (WebView2) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WebView2) }; #endif //============================================================================== class WebBrowserComponent::Pimpl { public: Pimpl (WebBrowserComponent& owner, [[maybe_unused]] const Options& preferences, bool useWebView2, const String& userAgent) { if (useWebView2) { #if JUCE_USE_WIN_WEBVIEW2 try { internal.reset (new WebView2 (owner, preferences)); } catch (const std::runtime_error&) {} #endif } if (internal == nullptr) internal.reset (new Win32WebView (owner, userAgent)); } InternalWebViewType& getInternalWebView() { return *internal; } private: std::unique_ptr internal; }; //============================================================================== WebBrowserComponent::WebBrowserComponent (const Options& options) : browser (new Pimpl (*this, options, options.getBackend() == Options::Backend::webview2, options.getUserAgent())), unloadPageWhenHidden (! options.keepsPageLoadedWhenBrowserIsHidden()) { 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 && 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::focusGainedWithDirection (FocusChangeType type, FocusChangeDirection dir) { ignoreUnused (type, dir); #if JUCE_USE_WIN_WEBVIEW2 if (auto* webView2 = dynamic_cast (&browser->getInternalWebView())) webView2->focusGainedWithDirection (type, dir); else #endif 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); } } //============================================================================== bool WebBrowserComponent::areOptionsSupported (const Options& options) { if (options.getBackend() == Options::Backend::defaultBackend || options.getBackend() == Options::Backend::ie) return true; #if JUCE_USE_WIN_WEBVIEW2 if (options.getBackend() != Options::Backend::webview2) return false; if (auto webView = WebView2::createWebViewHandle (options.getWinWebView2BackendOptions())) return true; #endif return false; } } // namespace juce