diff --git a/bundles/org.eclipse.swt/Eclipse SWT Browser/win32/org/eclipse/swt/browser/Edge.java b/bundles/org.eclipse.swt/Eclipse SWT Browser/win32/org/eclipse/swt/browser/Edge.java index d28d5d26b5..4246b8f30f 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Browser/win32/org/eclipse/swt/browser/Edge.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Browser/win32/org/eclipse/swt/browser/Edge.java @@ -74,8 +74,8 @@ public WebViewEnvironment(ICoreWebView2Environment environment) { private static Map webViewEnvironments = new HashMap<>(); - ICoreWebView2 webView; - ICoreWebView2_2 webView_2; + CompletableFuture lastWebViewTask = new CompletableFuture<>(); + CompletableFuture lastWebView_2Task = new CompletableFuture<>(); ICoreWebView2Controller controller; ICoreWebView2Settings settings; ICoreWebView2Environment2 environment2; @@ -310,12 +310,13 @@ static ICoreWebView2CookieManager getCookieManager() { SWT.error(SWT.ERROR_NOT_IMPLEMENTED, null, " [WebView2: cookie access requires a Browser instance]"); } Edge instance = environmentWrapper.instances().get(0); - if (instance.webView_2 == null) { + if (instance.lastWebView_2Task == null) { SWT.error(SWT.ERROR_NOT_IMPLEMENTED, null, " [WebView2 version 88+ is required to access cookies]"); } long[] ppv = new long[1]; - int hr = instance.webView_2.get_CookieManager(ppv); + instance.waitForWebviewInitialization(); + int hr = instance.lastWebView_2Task.join().get_CookieManager(ppv); if (hr != COM.S_OK) error(SWT.ERROR_NO_HANDLES, hr); return new ICoreWebView2CookieManager(ppv[0]); } @@ -325,15 +326,11 @@ void checkDeadlock() { // and JavaScript callbacks are serialized. An event handler waiting // for a completion of another handler will deadlock. Detect this // situation and throw an exception instead. - if (leadsToDeadlock()) { + if (inCallback || inNewWindow) { SWT.error(SWT.ERROR_FAILED_EVALUATE, null, " [WebView2: deadlock detected]"); } } -boolean leadsToDeadlock() { - return inCallback || inNewWindow; -} - WebViewEnvironment createEnvironment() { Display display = Display.getCurrent(); WebViewEnvironment existingEnvironment = webViewEnvironments.get(display); @@ -401,18 +398,26 @@ WebViewEnvironment createEnvironment() { public void create(Composite parent, int style) { containingEnvironment = createEnvironment(); + final CompletableFuture firstWebViewTask = lastWebViewTask; + final CompletableFuture firstWebView_2Task = lastWebView_2Task; long[] ppv = new long[1]; int hr = containingEnvironment.environment().QueryInterface(COM.IID_ICoreWebView2Environment2, ppv); if (hr == COM.S_OK) environment2 = new ICoreWebView2Environment2(ppv[0]); - // If leads to deadlock then execute asynchronously and move on. // The webview calls are queued to be executed when it is done executing the current task. - browser.getDisplay().asyncExec(() -> setupBrowser()); + containingEnvironment.environment().CreateCoreWebView2Controller(browser.handle, + newCallback((result, pv) -> { + if ((int)result == COM.S_OK) { + ppv[0] = pv; + new IUnknown(pv).AddRef(); + } + setupBrowser((int)result, ppv, firstWebViewTask, firstWebView_2Task); + return COM.S_OK; + })); + + addProgressListener(ProgressListener.completedAdapter(__ -> writeToDefaultPathDOM())); } -void setupBrowser() { - int hr; - long[] ppv = new long[1]; - hr = callAndWait(ppv, completion -> containingEnvironment.environment().CreateCoreWebView2Controller(browser.handle, completion)); +void setupBrowser(int hr, long[] ppv, CompletableFuture firstWebViewTask, CompletableFuture firstWebView_2Task) { if(browser.isDisposed()) { browserDispose(new Event()); return; @@ -429,37 +434,38 @@ void setupBrowser() { controller = new ICoreWebView2Controller(ppv[0]); controller.get_CoreWebView2(ppv); - webView = new ICoreWebView2(ppv[0]); - webView.get_Settings(ppv); + firstWebViewTask.complete(new ICoreWebView2(ppv[0])); + final ICoreWebView2 wv = firstWebViewTask.join(); + wv.get_Settings(ppv); settings = new ICoreWebView2Settings(ppv[0]); - hr = webView.QueryInterface(COM.IID_ICoreWebView2_2, ppv); - if (hr == COM.S_OK) webView_2 = new ICoreWebView2_2(ppv[0]); + hr = wv.QueryInterface(COM.IID_ICoreWebView2_2, ppv); + if (hr == COM.S_OK) firstWebView_2Task.complete(new ICoreWebView2_2(ppv[0])); long[] token = new long[1]; IUnknown handler; handler = newCallback(this::handleCloseRequested); - webView.add_WindowCloseRequested(handler, token); + wv.add_WindowCloseRequested(handler, token); handler.Release(); handler = newCallback(this::handleNavigationStarting); - webView.add_NavigationStarting(handler, token); + wv.add_NavigationStarting(handler, token); handler.Release(); handler = newCallback(this::handleFrameNavigationStarting); - webView.add_FrameNavigationStarting(handler, token); + wv.add_FrameNavigationStarting(handler, token); handler.Release(); handler = newCallback(this::handleNavigationCompleted); - webView.add_NavigationCompleted(handler, token); + wv.add_NavigationCompleted(handler, token); handler.Release(); handler = newCallback(this::handleFrameNavigationCompleted); - webView.add_FrameNavigationCompleted(handler, token); + wv.add_FrameNavigationCompleted(handler, token); handler.Release(); handler = newCallback(this::handleDocumentTitleChanged); - webView.add_DocumentTitleChanged(handler, token); + wv.add_DocumentTitleChanged(handler, token); handler.Release(); handler = newCallback(this::handleNewWindowRequested); - webView.add_NewWindowRequested(handler, token); + wv.add_NewWindowRequested(handler, token); handler.Release(); handler = newCallback(this::handleSourceChanged); - webView.add_SourceChanged(handler, token); + wv.add_SourceChanged(handler, token); handler.Release(); handler = newCallback(this::handleMoveFocusRequested); controller.add_MoveFocusRequested(handler, token); @@ -470,17 +476,15 @@ void setupBrowser() { handler = newCallback(this::handleAcceleratorKeyPressed); controller.add_AcceleratorKeyPressed(handler, token); handler.Release(); - if (webView_2 != null) { + if (firstWebView_2Task.isDone()) { handler = newCallback(this::handleDOMContentLoaded); - webView_2.add_DOMContentLoaded(handler, token); + firstWebView_2Task.join().add_DOMContentLoaded(handler, token); handler.Release(); } - addProgressListener(ProgressListener.completedAdapter(__ -> writeToDefaultPathDOM())); - IUnknown hostDisp = newHostObject(this::handleCallJava); long[] hostObj = { COM.VT_DISPATCH, hostDisp.getAddress(), 0 }; // VARIANT - webView.AddHostObjectToScript("swt\0".toCharArray(), hostObj); + wv.AddHostObjectToScript("swt\0".toCharArray(), hostObj); hostDisp.Release(); browser.addListener(SWT.Dispose, this::browserDispose); @@ -497,14 +501,16 @@ void browserDispose(Event event) { containingEnvironment.instances.remove(this); // Check for null before releasing - if (webView_2 != null) webView_2.Release(); if (environment2 != null) environment2.Release(); if (settings != null) settings.Release(); - if (webView != null) webView.Release(); - webView_2 = null; + lastWebViewTask.thenAcceptAsync(it -> { + it.Release(); + if (lastWebView_2Task.isDone()) lastWebView_2Task.join().Release(); + }); + lastWebView_2Task = null; environment2 = null; settings = null; - webView = null; + lastWebViewTask = null; if(controller != null) { // Bug in WebView2. Closing the controller from an event handler results @@ -543,14 +549,14 @@ void browserResize(Event event) { @Override public Object evaluate(String script) throws SWTException { checkDeadlock(); - + waitForWebviewInitialization(); // Feature in WebView2. ExecuteScript works regardless of IsScriptEnabled setting. // Disallow programmatic execution manually. if (!jsEnabled) return null; String script2 = "(function() {try { " + script + " } catch (e) { return '" + ERROR_ID + "' + e.message; } })();\0"; String[] pJson = new String[1]; - int hr = callAndWait(pJson, completion -> webView.ExecuteScript(script2.toCharArray(), completion)); + int hr = callAndWait(pJson, completion -> lastWebViewTask.join().ExecuteScript(script2.toCharArray(), completion)); if (hr != COM.S_OK) error(SWT.ERROR_FAILED_EVALUATE, hr); Object data = JSON.parse(pJson[0]); @@ -566,11 +572,17 @@ public boolean execute(String script) { // Feature in WebView2. ExecuteScript works regardless of IsScriptEnabled setting. // Disallow programmatic execution manually. if (!jsEnabled) return false; - + waitForWebviewInitialization(); IUnknown completion = newCallback((long result, long json) -> COM.S_OK); - int hr = webView.ExecuteScript(stringToWstr(script), completion); + lastWebViewTask.join().ExecuteScript(stringToWstr(script), completion); completion.Release(); - return hr == COM.S_OK; + return true; +} + +private void waitForWebviewInitialization() { + while(!lastWebViewTask.isDone() && !lastWebView_2Task.isDone()) { + processNextOSMessage(); + } } @Override @@ -595,8 +607,9 @@ public String getText() { @Override public String getUrl() { + waitForWebviewInitialization(); long ppsz[] = new long[1]; - webView.get_Source(ppsz); + lastWebViewTask.join().get_Source(ppsz); return wstrToString(ppsz[0], true); } @@ -617,7 +630,7 @@ int handleCloseRequested(long pView, long pArgs) { int handleDocumentTitleChanged(long pView, long pArgs) { long[] ppsz = new long[1]; - webView.get_DocumentTitle(ppsz); + lastWebViewTask.join().get_DocumentTitle(ppsz); String title = wstrToString(ppsz[0], true); browser.getDisplay().asyncExec(() -> { if (browser.isDisposed()) return; @@ -690,6 +703,7 @@ int handleNavigationStarting(long pView, long pArgs, boolean top) { execute(sb.toString()); } } else { + this.html = null; args.put_Cancel(true); } return COM.S_OK; @@ -702,7 +716,7 @@ int handleSourceChanged(long pView, long pArgs) { // is the same between navigations, SourceChanged isn't fired. // TODO: emit missing location changed events long[] ppsz = new long[1]; - int hr = webView.get_Source(ppsz); + int hr = lastWebViewTask.join().get_Source(ppsz); if (hr != COM.S_OK) return hr; String url = wstrToString(ppsz[0], true); browser.getDisplay().asyncExec(() -> { @@ -757,7 +771,7 @@ int handleNavigationCompleted(long pView, long pArgs, boolean top) { long[] pNavId = new long[1]; args.get_NavigationId(pNavId); LocationEvent startEvent = navigations.remove(pNavId[0]); - if (webView_2 == null && startEvent != null && startEvent.top) { + if (lastWebView_2Task == null && startEvent != null && startEvent.top) { // If DOMContentLoaded isn't available, fire // ProgressListener.completed from here. sendProgressCompleted(); @@ -814,8 +828,9 @@ int handleNewWindowRequested(long pView, long pArgs) { if (openEvent.browser != null && !openEvent.browser.isDisposed()) { WebBrowser other = openEvent.browser.webBrowser; args.put_Handled(true); - if (other instanceof Edge) { - args.put_NewWindow(((Edge)other).webView.getAddress()); + if (other instanceof Edge edge) { + edge.waitForWebviewInitialization(); + args.put_NewWindow(edge.lastWebViewTask.join().getAddress()); // Send show event to the other browser. WindowEvent showEvent = new WindowEvent (other.browser); @@ -921,37 +936,39 @@ int handleMoveFocusRequested(long pView, long pArgs) { @Override public boolean isBackEnabled() { int[] pval = new int[1]; - webView.get_CanGoBack(pval); + waitForWebviewInitialization(); + lastWebViewTask.join().get_CanGoBack(pval); return pval[0] != 0; } @Override public boolean isForwardEnabled() { int[] pval = new int[1]; - webView.get_CanGoForward(pval); + waitForWebviewInitialization(); + lastWebViewTask.join().get_CanGoForward(pval); return pval[0] != 0; } @Override public boolean back() { // Feature in WebView2. GoBack returns S_OK even when CanGoBack is FALSE. - return isBackEnabled() && webView.GoBack() == COM.S_OK; + return isBackEnabled() && lastWebViewTask.join().GoBack() == COM.S_OK; } @Override public boolean forward() { // Feature in WebView2. GoForward returns S_OK even when CanGoForward is FALSE. - return isForwardEnabled() && webView.GoForward() == COM.S_OK; + return isForwardEnabled() && lastWebViewTask.join().GoForward() == COM.S_OK; } @Override public void refresh() { - webView.Reload(); + lastWebViewTask = acceptAsyncAndRunSync(lastWebViewTask, ICoreWebView2::Reload); } @Override public void stop() { - webView.Stop(); + lastWebViewTask = acceptAsyncAndRunSync(lastWebViewTask, ICoreWebView2::Stop); } @Override @@ -975,17 +992,6 @@ public boolean setText(String html, boolean trusted) { } private boolean setWebpageData(String url, String postData, String[] headers, String html) { - // If the create() method hasn't finished executing, queue the call to wait for the browser to finish initializing - if (!initializationProcessFinished.isDone()) { - final String fUrl = url; - browser.getDisplay().asyncExec(() -> { - if (waitForInitialization()) { - setWebpageData(fUrl, postData, headers, html); - browserResize(new Event()); - } - }); - return true; - } // Feature in WebView2. Partial URLs like "www.example.com" are not accepted. // Prepend the protocol if it's missing. if (!url.matches("[a-z][a-z0-9+.-]*:.*")) { @@ -995,7 +1001,7 @@ private boolean setWebpageData(String url, String postData, String[] headers, St char[] pszUrl = stringToWstr(url); this.html = html; if (postData != null || headers != null) { - if (environment2 == null || webView_2 == null) { + if (environment2 == null || lastWebView_2Task == null) { SWT.error(SWT.ERROR_NOT_IMPLEMENTED, null, " [WebView2 version 88+ is required to set postData and headers]"); } long[] ppRequest = new long[1]; @@ -1020,23 +1026,25 @@ private boolean setWebpageData(String url, String postData, String[] headers, St if (stream != null) stream.Release(); if (hr != COM.S_OK) error(SWT.ERROR_NO_HANDLES, hr); IUnknown request = new IUnknown(ppRequest[0]); - hr = webView_2.NavigateWithWebResourceRequest(request); - request.Release(); + lastWebView_2Task.thenAcceptAsync(wv -> browser.getDisplay().syncExec(() -> { + wv.NavigateWithWebResourceRequest(request); + request.Release(); + browserResize(new Event()); + })); } else { - hr = webView.Navigate(pszUrl); + lastWebViewTask = acceptAsyncAndRunSync(lastWebViewTask, wv -> wv.Navigate(pszUrl)); } - return hr == COM.S_OK; + return true; } -private boolean waitForInitialization() { - try { - initializationProcessFinished.get(10, TimeUnit.SECONDS); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - SWT.error(SWT.ERROR_FAILED_EXEC, e); - } catch (CancellationException e) { - return false; - } - return true; +private CompletableFuture acceptAsyncAndRunSync(CompletableFuture webViewTask, Consumer action) { + return webViewTask.thenApply(wv -> { +// browser.getDisplay().syncExec(() -> { + action.accept(wv); + browserResize(new Event()); +// }); + return wv; + }); } @Override diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_browser_Browser.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_browser_Browser.java index 872087a34a..b1c8472fdf 100644 --- a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_browser_Browser.java +++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_browser_Browser.java @@ -34,6 +34,7 @@ import java.lang.management.ThreadMXBean; import java.net.HttpURLConnection; import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import java.nio.file.DirectoryStream; import java.nio.file.Files; @@ -676,6 +677,27 @@ public void changed(LocationEvent event) { */ } +@Test +public void test_LocationListener_LocationListener_ordered_changing () { + List locations = new ArrayList<>(); + browser.addLocationListener(changingAdapter(event -> locations.add(event.location))); + shell.open(); + browser.setText("You should not see this message."); + String url; + String pluginPath = System.getProperty("PLUGIN_PATH"); + // Depending on how the jUnit test is ran, (gui/maven/ant), url for local file needs to be acquired differently. + if (pluginPath != null) { + url = pluginPath + "/data/testWebsiteWithTitle.html"; + } else { + // used when ran from Eclipse gui. + url = Test_org_eclipse_swt_browser_Browser.class.getClassLoader().getResource("testWebsiteWithTitle.html").toString(); + } + browser.setUrl(url); + waitForPassCondition(() -> locations.size() == 2); + + assertTrue("Change of locations do not fire in order.", browser.isLocationForCustomText(locations.get(0)) && URI.create(url).equals(URI.create(locations.get(1)))); +} + @Test /** Ensue that only one changed and one completed event are fired for url changes */ @@ -1151,18 +1173,8 @@ private void validateTitleChanged(String expectedTitle, Runnable browserSetFunc) browserSetFunc.run(); shell.open(); - boolean passed = waitForPassCondition(() -> actualTitle.get().equals(expectedTitle)); - String errMsg = ""; - if (!passed) { - if (actualTitle.get().length() == 0) { - errMsg = "Test timed out. TitleListener not fired"; - } else { - errMsg = "\nExpected title and actual title do not match." - + "\nExpected: " + expectedTitle - + "\nActual: " + actualTitle; - } - } - assertTrue(errMsg + testLog.toString(), passed); + assertTrue("Test timed out. TitleListener not fired", waitForPassCondition(() -> actualTitle.get().equals(expectedTitle))); + assertEquals(testLog.toString(), expectedTitle, actualTitle.get()); } @Test