From 74498673378f0ca30012d487f6fd0e02295ec0f5 Mon Sep 17 00:00:00 2001 From: attila Date: Mon, 6 Oct 2025 18:59:59 +0200 Subject: [PATCH] MacOS: Fix WebBrowserComponent going blank in FL Studio The issue could be triggered by opening the plugin in FL Studio, and then using the TAB button to switch between FL Studio UI elements, until the plugin became invisible and then it became visible again. This would cause the WebBrowserComponent to navigate to about:blank permanently. This was caused by the component becoming invisible and visible again in rapid succession. This triggered a navigation to about:blank. To understand the root cause of this, some undocumented behaviour of WkWebView had to be uncovered. To understand this, see the following test code, where the test1, test2 and test3 functions are called with ample time in between one after the other. void test1() { goToURL ("A"); } void test2() { goToURL ("B"); goToURL ("C"); // B, C ignored completely, only D inserted into back-forward navigation queue goToURL ("D"); } void test3() { goToURL ("E"); goToURL ("F"); // E, F ignored completely, back navigation executed from D to A goBack(); } --- .../native/juce_WebBrowserComponent_mac.mm | 63 +++++++++++++++++-- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/modules/juce_gui_extra/native/juce_WebBrowserComponent_mac.mm b/modules/juce_gui_extra/native/juce_WebBrowserComponent_mac.mm index 31684c6865..782428ab8c 100644 --- a/modules/juce_gui_extra/native/juce_WebBrowserComponent_mac.mm +++ b/modules/juce_gui_extra/native/juce_WebBrowserComponent_mac.mm @@ -383,10 +383,12 @@ public: DelegateConnector (WebBrowserComponent& browserIn, std::function handleNativeEventFnIn, std::function (const String&)> handleResourceRequestFnIn, + std::function didFinishNavigationCallbackIn, const WebBrowserComponent::Options& optionsIn) : browser (browserIn), handleNativeEventFn (std::move (handleNativeEventFnIn)), handleResourceRequestFn (std::move (handleResourceRequestFnIn)), + didFinishNavigationCallback (std::move (didFinishNavigationCallbackIn)), options (optionsIn) { } @@ -408,10 +410,16 @@ public: return options; } + void didFinishNavigation (const String& url) + { + didFinishNavigationCallback (url); + } + private: WebBrowserComponent& browser; std::function handleNativeEventFn; std::function (const String&)> handleResourceRequestFn; + std::function didFinishNavigationCallback; WebBrowserComponent::Options options; }; @@ -437,7 +445,7 @@ struct WebViewDelegateClass final : public ObjCClass [] (id self, SEL, WKWebView* webview, WKNavigation*) { if (auto* connector = getConnector (self)) - connector->getBrowser().pageFinishedLoading (nsStringToJuce ([[webview URL] absoluteString])); + connector->didFinishNavigation (nsStringToJuce ([[webview URL] absoluteString])); }); addMethod (@selector (webView:didFailNavigation:withError:), @@ -811,6 +819,7 @@ JUCE_END_IGNORE_DEPRECATION_WARNINGS #endif class WebBrowserComponent::Impl::Platform::WKWebViewImpl : public WebBrowserComponent::Impl::PlatformInterface, + private AsyncUpdater, #if JUCE_MAC public NSViewComponent #else @@ -825,6 +834,10 @@ public: delegateConnector (implIn.owner, [this] (const auto& m) { owner.handleNativeEvent (m); }, [this] (const auto& r) { return owner.handleResourceRequest (r); }, + [this] (const auto& url) { + lastLoadedUrl = url; + owner.owner.pageFinishedLoading (url); + }, browserOptions), allowAccessToEnclosingDirectory (browserOptions.getAppleWkWebViewOptions() .getAllowAccessToEnclosingDirectory()) @@ -917,6 +930,39 @@ public: setSize (width, height); } + void handleAsyncUpdate() override + { + auto& browser = owner.owner; + + if (! browser.blankPageShown) + return; + + if (lastRequestedUrl != blankPageUrl) + return; + + // According to our logic, a blank page was shown, and now we are trying to go back to the + // page before that. + // + // But WkWebView seems to be doing some asynchronous batching, and if you send loadRequest: + // and goBack in quick succession, loadRequest: will be ignored entirely and goBack will be + // executed on the backForwardList as if it never happened. + // + // Although none of this is documented, it seems we can reliably query the current contents + // of the backForwardList to see, if we would be navigating away from the URL with the + // actual contents if we executed goBack now, and we can wait until loadRequest: has taken + // effect. + // + // This behaviour initially caused a bug in FL Studio, where the plugin window can become + // invisible and visible again in very rapid succession, when using the TAB button. + if (lastLoadedUrl != blankPageUrl) + { + triggerAsyncUpdate(); + return; + } + + browser.goBack(); + } + void checkWindowAssociation() override { auto& browser = owner.owner; @@ -924,20 +970,21 @@ public: if (browser.isShowing()) { browser.reloadLastURL(); - - if (browser.blankPageShown) - browser.goBack(); + handleAsyncUpdate(); } else { - if (browser.unloadPageWhenHidden && ! browser.blankPageShown) + if ( browser.unloadPageWhenHidden + && ! browser.blankPageShown + && lastLoadedUrl.isNotEmpty() + && lastLoadedUrl != blankPageUrl) { // 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); + goToURL (blankPageUrl, nullptr, nullptr); } } } @@ -1030,6 +1077,7 @@ public: } else if (NSMutableURLRequest* request = getRequestForURL (url, headers, postData)) { + lastRequestedUrl = url; [webView.get() loadRequest: request]; } } @@ -1097,12 +1145,15 @@ public: } private: + static inline auto blankPageUrl = "about:blank"; + WebBrowserComponent::Impl& owner; DelegateConnector delegateConnector; bool allowAccessToEnclosingDirectory = false; LastFocusChange lastFocusChange; ObjCObjectHandle webView; ObjCObjectHandle webViewDelegate; + String lastRequestedUrl, lastLoadedUrl; }; //==============================================================================