From c8ac9dd3dc531d7cebd1344fd967b7c91037bb74 Mon Sep 17 00:00:00 2001 From: Sebastian Ratz Date: Mon, 5 May 2025 15:12:56 +0100 Subject: [PATCH] Edge: Implement mouse-related event listener support - MouseListener up down doubleClick - MouseMoveListener move - MouseTrackListener exit enter - MouseWheelListener scroll - DragDetectListener dragDetected The implementation is JavaScript-based by attaching listeners to the DOM for all relevant events and forwarding them to the WebView via window.chrome.webview.postMessage(). The event handling is analogous to IE's IE#handleDOMEvent(org.eclipse.swt.ole.win32.OleEvent). Note: Since the implementation is JavaScript-based, this requires JavaScript to be enabled on the Browser instance. When JavaScript is disabled, we keep using the timer-based fallback implementation introduced in #1551. As JavaScript is only truly enabled *after* navigation has finished, we also keep using this workaround when the Browser is first instantiated and before the first page has loaded. This change also fixes the jsEnabled flag lifecycle by updating it in handleNavigationCompleted(), same as in IE. This resolves #2164 as long as JavaScript is enabled. --- .../org/eclipse/swt/browser/Browser.java | 6 + .../win32/org/eclipse/swt/browser/Edge.java | 210 +++++++++++++++++- .../swt/internal/ole/win32/ICoreWebView2.java | 4 +- 3 files changed, 213 insertions(+), 7 deletions(-) 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) {