diff --git a/bundles/org.eclipse.swt/Eclipse SWT Browser/common/org/eclipse/swt/browser/Browser.java b/bundles/org.eclipse.swt/Eclipse SWT Browser/common/org/eclipse/swt/browser/Browser.java index 3f4eb80708e..f5b7c794f75 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Browser/common/org/eclipse/swt/browser/Browser.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Browser/common/org/eclipse/swt/browser/Browser.java @@ -16,6 +16,7 @@ import java.util.*; import org.eclipse.swt.*; +import org.eclipse.swt.events.*; import org.eclipse.swt.program.*; import org.eclipse.swt.widgets.*; @@ -1106,6 +1107,11 @@ public void removeVisibilityWindowListener (VisibilityWindowListener listener) { * Sets whether javascript will be allowed to run in pages subsequently * viewed in the receiver. Note that setting this value does not affect * the running of javascript in the current page. + *

+ * Note: When using the {@link SWT#EDGE} browser on Windows disabling javascript + * has certain side effects, e.g. proper support for {@link MouseEvent} depends + * on it and, when disabled, a limited timer-based fallback implementation is + * used. * * @param enabled the receiver's new javascript enabled state * 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 49cb5e1b926..d7560f91b9e 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 @@ -24,6 +24,7 @@ import java.util.concurrent.*; import java.util.concurrent.atomic.*; import java.util.function.*; +import java.util.stream.*; import org.eclipse.swt.*; import org.eclipse.swt.graphics.*; @@ -88,9 +89,34 @@ public WebViewEnvironment(ICoreWebView2Environment environment) { private boolean ignoreFocusIn; private String lastCustomText; + private boolean needsMouseMovementFallback = true; private static record CursorPosition(Point location, boolean isInsideBrowser) {}; private CursorPosition previousCursorPosition = new CursorPosition(new Point(0, 0), false); + private static final String WEBMESSAGE_KIND_MOUSE_RELATED_DOM_EVENT = "mouse-related-dom-event"; + private static final String EVENT_MOUSEDOWN = "mousedown"; //$NON-NLS-1$ + private static final String EVENT_MOUSEUP = "mouseup"; //$NON-NLS-1$ + private static final String EVENT_MOUSEMOVE = "mousemove"; //$NON-NLS-1$ + private static final String EVENT_MOUSEOVER = "mouseover"; //$NON-NLS-1$ + private static final String EVENT_MOUSEOUT = "mouseout"; //$NON-NLS-1$ + private static final String EVENT_DRAGSTART = "dragstart"; //$NON-NLS-1$ + private static final String EVENT_DRAGEND = "dragend"; //$NON-NLS-1$ + private static final String EVENT_DOUBLECLICK = "dblclick"; //$NON-NLS-1$ + private static final String EVENT_MOUSEWHEEL = "wheel"; //$NON-NLS-1$ + private static final Set MOUSE_RELATED_DOM_EVENTS = Set.of( // + EVENT_MOUSEDOWN, EVENT_MOUSEUP, // + EVENT_MOUSEMOVE, // + EVENT_MOUSEOVER, EVENT_MOUSEOUT, // + EVENT_DRAGSTART, EVENT_DRAGEND, // + EVENT_DOUBLECLICK, // + EVENT_MOUSEWHEEL// + ); + private static record MouseRelatedDomEvent(String eventType, boolean altKey, boolean ctrlKey, boolean shiftKey, + long clientX, long clientY, long button, boolean fromElementSet, boolean toElementSet, long deltaY) { + } + private int lastMouseMoveX; + private int lastMouseMoveY; + static { NativeClearSessions = () -> { ICoreWebView2CookieManager manager = getCookieManager(); @@ -775,6 +801,9 @@ void setupBrowser(int hr, long pv) { handler = newCallback(this::handleSourceChanged); webView.add_SourceChanged(handler, token); handler.Release(); + handler = newCallback(this::handleWebMessageReceived); + webView.add_WebMessageReceived(handler, token); + handler.Release(); handler = newCallback(this::handleMoveFocusRequested); controller.add_MoveFocusRequested(handler, token); handler.Release(); @@ -814,7 +843,7 @@ void setupBrowser(int hr, long pv) { browser.addListener(SWT.FocusIn, this::browserFocusIn); browser.addListener(SWT.Resize, this::browserResize); browser.addListener(SWT.Move, this::browserMove); - scheduleMouseMovementHandling(); + scheduleMouseMovementHandlingIfNeeded(); // Sometimes when the shell of the browser is opened before the browser is // initialized, nothing is drawn on the shell. We need browserResize to force @@ -881,7 +910,10 @@ void browserResize(Event event) { controller.put_IsVisible(true); } -private void scheduleMouseMovementHandling() { +private void scheduleMouseMovementHandlingIfNeeded() { + if (!needsMouseMovementFallback) { + return; + } browser.getDisplay().timerExec(100, () -> { if (browser.isDisposed()) { return; @@ -889,7 +921,10 @@ private void scheduleMouseMovementHandling() { if (browser.isVisible() && hasDisplayFocus()) { handleMouseMovement(); } - scheduleMouseMovementHandling(); + if (!needsMouseMovementFallback) { + return; + } + scheduleMouseMovementHandlingIfNeeded(); }); } @@ -969,6 +1004,14 @@ public boolean execute(String script) { // Feature in WebView2. ExecuteScript works regardless of IsScriptEnabled setting. // Disallow programmatic execution manually. if (!jsEnabled) return false; + return executeInternal(script); +} + +/** + * Unconditional script execution, bypassing {@link WebBrowser#jsEnabled} flag / + * {@link Browser#setJavascriptEnabled(boolean)}. + */ +private boolean executeInternal(String script) { IUnknown completion = newCallback((long result, long json) -> COM.S_OK); int hr = webViewProvider.getWebView(true).ExecuteScript(stringToWstr(script), completion); completion.Release(); @@ -1098,8 +1141,7 @@ int handleNavigationStarting(long pView, long pArgs, boolean top) { // will be eventually cleared again in handleNavigationCompleted(). navigations.put(pNavId[0], event); if (event.doit) { - jsEnabled = jsEnabledOnNextPage; - settings.put_IsScriptEnabled(jsEnabled); + settings.put_IsScriptEnabled(jsEnabledOnNextPage); // Register browser functions in the new document. if (!functions.isEmpty()) { StringBuilder sb = new StringBuilder(); @@ -1201,6 +1243,29 @@ int handleDOMContentLoaded(long pView, long pArgs) { sendProgressCompleted(); } } + executeInternal( + """ + const events = [%%events%%]; + events.forEach(eventType => { + window.addEventListener(eventType, function(event) { + window.chrome.webview.postMessage([ + '%%webmessagekind%%', + eventType, + event.altKey, + event.ctrlKey, + event.shiftKey, + event.clientX, + event.clientY, + event.button, + "fromElement" in event && event.fromElement != null, + "toElement" in event && event.toElement != null, + "deltaY" in event ? event.deltaY : 0, + ]); + }); + }); + """ // + .replace("%%events%%", MOUSE_RELATED_DOM_EVENTS.stream().map(x -> "'" + x + "'").collect(Collectors.joining(", "))) // + .replace("%%webmessagekind%%", WEBMESSAGE_KIND_MOUSE_RELATED_DOM_EVENT)); return COM.S_OK; } @@ -1323,6 +1388,11 @@ int handleNavigationCompleted(long pView, long pArgs, boolean top) { // ProgressListener.completed from here. sendProgressCompleted(); } + if (top) { + jsEnabled = jsEnabledOnNextPage; + needsMouseMovementFallback = !jsEnabled; + scheduleMouseMovementHandlingIfNeeded(); + } int[] pIsSuccess = new int[1]; args.get_IsSuccess(pIsSuccess); if (pIsSuccess[0] != 0) { @@ -1524,6 +1594,136 @@ int handleMoveFocusRequested(long pView, long pArgs) { return COM.S_OK; } +int handleWebMessageReceived(long pView, long pArgs) { + ICoreWebView2WebMessageReceivedEventArgs args = new ICoreWebView2WebMessageReceivedEventArgs(pArgs); + long[] ppszWebMessageJson = new long[1]; + int hr = args.get_WebMessageAsJson(ppszWebMessageJson); + if (hr != COM.S_OK) return hr; + try { + String webMessageJson = wstrToString(ppszWebMessageJson[0], true); + Object[] data = (Object[]) JSON.parse(webMessageJson); + if (WEBMESSAGE_KIND_MOUSE_RELATED_DOM_EVENT.equals(data[0])) { + MouseRelatedDomEvent mouseRelatedDomEvent = new MouseRelatedDomEvent( // + (String) data[1], // + (boolean) data[2], // + (boolean) data[3], // + (boolean) data[4], // + Math.round((double) data[5]), // + Math.round((double) data[6]), // + Math.round((double) data[7]), // + (boolean) data[8], // + (boolean) data[9], // + Math.round((double) data[10]) // + ); + handleMouseRelatedDomEvent(mouseRelatedDomEvent); + } + } catch (Exception e) { + System.err.println(e); + } + return COM.S_OK; +} + +/** + * Insipired by the mouse-event related parts of + * {@link IE#handleDOMEvent(org.eclipse.swt.ole.win32.OleEvent)} + */ +private void handleMouseRelatedDomEvent(MouseRelatedDomEvent domEvent) { + String eventType = domEvent.eventType(); + + /* + * Feature in Edge. MouseOver/MouseOut events are fired any time the mouse enters + * or exits any element within the Browser. To ensure that SWT events are only + * fired for mouse movements into or out of the Browser, do not fire an event if + * the element being exited (on MouseOver) or entered (on MouseExit) is within + * the Browser. + */ + if (eventType.equals(EVENT_MOUSEOVER)) { + if (domEvent.fromElementSet()) { + return; + } + } + if (eventType.equals(EVENT_MOUSEOUT)) { + if (domEvent.toElementSet()) { + return; + } + } + + int mask = 0; + Event newEvent = new Event(); + newEvent.widget = browser; + newEvent.x = (int) domEvent.clientX(); newEvent.y = (int) domEvent.clientY(); + if (domEvent.ctrlKey()) mask |= SWT.CTRL; + if (domEvent.altKey()) mask |= SWT.ALT; + if (domEvent.shiftKey()) mask |= SWT.SHIFT; + newEvent.stateMask = mask; + + int button = (int) domEvent.button(); + switch (button) { + case 1: button = 1; break; + case 2: button = 3; break; + case 4: button = 2; break; + } + + if (eventType.equals(EVENT_MOUSEDOWN)) { + newEvent.type = SWT.MouseDown; + newEvent.button = button; + newEvent.count = 1; + } else if (eventType.equals(EVENT_MOUSEUP) || eventType.equals(EVENT_DRAGEND)) { + newEvent.type = SWT.MouseUp; + newEvent.button = button != 0 ? button : 1; /* button assumed to be 1 for dragends */ + newEvent.count = 1; + switch (newEvent.button) { + case 1: newEvent.stateMask |= SWT.BUTTON1; break; + case 2: newEvent.stateMask |= SWT.BUTTON2; break; + case 3: newEvent.stateMask |= SWT.BUTTON3; break; + case 4: newEvent.stateMask |= SWT.BUTTON4; break; + case 5: newEvent.stateMask |= SWT.BUTTON5; break; + } + } else if (eventType.equals(EVENT_MOUSEWHEEL)) { + newEvent.type = SWT.MouseWheel; + // Chromium/Edge uses deltaMode DOM_DELTA_PIXEL which + // - has a different sign than the legacy MouseWheelEvent wheelDelta + // - depends on the zoom of the browser + // https://github.com/w3c/uievents/issues/181 + // The literal value of deltaY is therefore useless. + // Instead, we simply use the sign for the direction and combine + // it with the internal hard-coded value of '3 lines'. + newEvent.count = domEvent.deltaY() > 0 ? -3 : 3; + } else if (eventType.equals(EVENT_MOUSEMOVE)) { + /* + * Feature in Edge. Spurious and redundant mousemove events are often received. The workaround + * is to not fire MouseMove events whose x and y values match the last MouseMove. + */ + if (newEvent.x == lastMouseMoveX && newEvent.y == lastMouseMoveY) { + return; + } + newEvent.type = SWT.MouseMove; + lastMouseMoveX = newEvent.x; lastMouseMoveY = newEvent.y; + } else if (eventType.equals(EVENT_MOUSEOVER)) { + newEvent.type = SWT.MouseEnter; + } else if (eventType.equals(EVENT_MOUSEOUT)) { + newEvent.type = SWT.MouseExit; + } else if (eventType.equals(EVENT_DRAGSTART)) { + newEvent.type = SWT.DragDetect; + newEvent.button = 1; /* button assumed to be 1 for dragstarts */ + newEvent.stateMask |= SWT.BUTTON1; + } + + browser.notifyListeners(newEvent.type, newEvent); + + if (eventType.equals(EVENT_DOUBLECLICK)) { + newEvent = new Event (); + newEvent.widget = browser; + newEvent.type = SWT.MouseDoubleClick; + newEvent.x = (int) domEvent.clientX(); newEvent.y = (int) domEvent.clientY(); + newEvent.stateMask = mask; + newEvent.type = SWT.MouseDoubleClick; + newEvent.button = 1; /* dblclick only comes for button 1 and does not set the button property */ + newEvent.count = 2; + browser.notifyListeners (newEvent.type, newEvent); + } +} + @Override public boolean isBackEnabled() { int[] pval = new int[1]; diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/ole/win32/ICoreWebView2.java b/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/ole/win32/ICoreWebView2.java index 77b3c8e5f5b..473a3d981d0 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/ole/win32/ICoreWebView2.java +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/ole/win32/ICoreWebView2.java @@ -79,8 +79,8 @@ public int PostWebMessageAsJson(char[] webMessageAsJson) { return COM.VtblCall(32, address, webMessageAsJson); } -public int add_WebMessageReceived(long handler, long[] token) { - return COM.VtblCall(34, address, handler, token); +public int add_WebMessageReceived(IUnknown eventHandler, long[] token) { + return COM.VtblCall(34, address, eventHandler.address, token); } public int get_CanGoBack(int[] canGoBack) {