Skip to content

Commit 54645b9

Browse files
committed
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.
1 parent 79e69bb commit 54645b9

File tree

3 files changed

+221
-9
lines changed
  • bundles/org.eclipse.swt

3 files changed

+221
-9
lines changed

bundles/org.eclipse.swt/Eclipse SWT Browser/common/org/eclipse/swt/browser/Browser.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import java.util.*;
1717

1818
import org.eclipse.swt.*;
19+
import org.eclipse.swt.events.*;
1920
import org.eclipse.swt.program.*;
2021
import org.eclipse.swt.widgets.*;
2122

@@ -1109,6 +1110,11 @@ public void removeVisibilityWindowListener (VisibilityWindowListener listener) {
11091110
* Sets whether javascript will be allowed to run in pages subsequently
11101111
* viewed in the receiver. Note that setting this value does not affect
11111112
* the running of javascript in the current page.
1113+
* <p>
1114+
* Note: When using the {@link SWT#EDGE} browser on Windows disabling javascript
1115+
* has certain side effects, e.g. proper support for {@link MouseEvent} depends
1116+
* on it and, when disabled, a limited timer-based fallback implementation is
1117+
* used.
11121118
*
11131119
* @param enabled the receiver's new javascript enabled state
11141120
*

bundles/org.eclipse.swt/Eclipse SWT Browser/win32/org/eclipse/swt/browser/Edge.java

Lines changed: 213 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.concurrent.*;
2525
import java.util.concurrent.atomic.*;
2626
import java.util.function.*;
27+
import java.util.stream.*;
2728

2829
import org.eclipse.swt.*;
2930
import org.eclipse.swt.graphics.*;
@@ -88,9 +89,40 @@ public WebViewEnvironment(ICoreWebView2Environment environment) {
8889
private boolean ignoreFocusIn;
8990
private String lastCustomText;
9091

92+
/**
93+
* We need the fallback whenever javascript is *not* enabled, but also for fresh
94+
* Edge instances until the first javascript-enabled page has successfully
95+
* loaded. Hence, this is initialized with <code>true</true> but will then be
96+
* updated following changes to {@link #jsEnabled}.
97+
*/
98+
private boolean needsMouseMovementFallback = true;
9199
private static record CursorPosition(Point location, boolean isInsideBrowser) {};
92100
private CursorPosition previousCursorPosition = new CursorPosition(new Point(0, 0), false);
93101

102+
private static final String WEBMESSAGE_KIND_MOUSE_RELATED_DOM_EVENT = "mouse-related-dom-event";
103+
private static final String EVENT_MOUSEDOWN = "mousedown"; //$NON-NLS-1$
104+
private static final String EVENT_MOUSEUP = "mouseup"; //$NON-NLS-1$
105+
private static final String EVENT_MOUSEMOVE = "mousemove"; //$NON-NLS-1$
106+
private static final String EVENT_MOUSEOVER = "mouseover"; //$NON-NLS-1$
107+
private static final String EVENT_MOUSEOUT = "mouseout"; //$NON-NLS-1$
108+
private static final String EVENT_DRAGSTART = "dragstart"; //$NON-NLS-1$
109+
private static final String EVENT_DRAGEND = "dragend"; //$NON-NLS-1$
110+
private static final String EVENT_DOUBLECLICK = "dblclick"; //$NON-NLS-1$
111+
private static final String EVENT_MOUSEWHEEL = "wheel"; //$NON-NLS-1$
112+
private static final Set<String> MOUSE_RELATED_DOM_EVENTS = Set.of( //
113+
EVENT_MOUSEDOWN, EVENT_MOUSEUP, //
114+
EVENT_MOUSEMOVE, //
115+
EVENT_MOUSEOVER, EVENT_MOUSEOUT, //
116+
EVENT_DRAGSTART, EVENT_DRAGEND, //
117+
EVENT_DOUBLECLICK, //
118+
EVENT_MOUSEWHEEL//
119+
);
120+
private static record MouseRelatedDomEvent(String eventType, boolean altKey, boolean ctrlKey, boolean shiftKey,
121+
long clientX, long clientY, long button, boolean fromElementSet, boolean toElementSet, long deltaY) {
122+
}
123+
private int lastMouseMoveX;
124+
private int lastMouseMoveY;
125+
94126
static {
95127
NativeClearSessions = () -> {
96128
ICoreWebView2CookieManager manager = getCookieManager();
@@ -775,6 +807,9 @@ void setupBrowser(int hr, long pv) {
775807
handler = newCallback(this::handleSourceChanged);
776808
webView.add_SourceChanged(handler, token);
777809
handler.Release();
810+
handler = newCallback(this::handleWebMessageReceived);
811+
webView.add_WebMessageReceived(handler, token);
812+
handler.Release();
778813
handler = newCallback(this::handleMoveFocusRequested);
779814
controller.add_MoveFocusRequested(handler, token);
780815
handler.Release();
@@ -814,7 +849,7 @@ void setupBrowser(int hr, long pv) {
814849
browser.addListener(SWT.FocusIn, this::browserFocusIn);
815850
browser.addListener(SWT.Resize, this::browserResize);
816851
browser.addListener(SWT.Move, this::browserMove);
817-
scheduleMouseMovementHandling();
852+
scheduleFallbackMouseMovementHandlingIfNeeded();
818853

819854
// Sometimes when the shell of the browser is opened before the browser is
820855
// initialized, nothing is drawn on the shell. We need browserResize to force
@@ -881,19 +916,25 @@ void browserResize(Event event) {
881916
controller.put_IsVisible(true);
882917
}
883918

884-
private void scheduleMouseMovementHandling() {
919+
private void scheduleFallbackMouseMovementHandlingIfNeeded() {
920+
if (!needsMouseMovementFallback) {
921+
return;
922+
}
885923
browser.getDisplay().timerExec(100, () -> {
886924
if (browser.isDisposed()) {
887925
return;
888926
}
889927
if (browser.isVisible() && hasDisplayFocus()) {
890-
handleMouseMovement();
928+
handleFallbackMouseMovement();
891929
}
892-
scheduleMouseMovementHandling();
930+
if (!needsMouseMovementFallback) {
931+
return;
932+
}
933+
scheduleFallbackMouseMovementHandlingIfNeeded();
893934
});
894935
}
895936

896-
private void handleMouseMovement() {
937+
private void handleFallbackMouseMovement() {
897938
final Point currentCursorLocation = browser.getDisplay().getCursorLocation();
898939
Point cursorLocationInControlCoordinate = browser.toControl(currentCursorLocation);
899940
boolean isCursorInsideBrowser = browser.getBounds().contains(cursorLocationInControlCoordinate);
@@ -969,6 +1010,14 @@ public boolean execute(String script) {
9691010
// Feature in WebView2. ExecuteScript works regardless of IsScriptEnabled setting.
9701011
// Disallow programmatic execution manually.
9711012
if (!jsEnabled) return false;
1013+
return executeInternal(script);
1014+
}
1015+
1016+
/**
1017+
* Unconditional script execution, bypassing {@link WebBrowser#jsEnabled} flag /
1018+
* {@link Browser#setJavascriptEnabled(boolean)}.
1019+
*/
1020+
private boolean executeInternal(String script) {
9721021
IUnknown completion = newCallback((long result, long json) -> COM.S_OK);
9731022
int hr = webViewProvider.getWebView(true).ExecuteScript(stringToWstr(script), completion);
9741023
completion.Release();
@@ -1098,8 +1147,7 @@ int handleNavigationStarting(long pView, long pArgs, boolean top) {
10981147
// will be eventually cleared again in handleNavigationCompleted().
10991148
navigations.put(pNavId[0], event);
11001149
if (event.doit) {
1101-
jsEnabled = jsEnabledOnNextPage;
1102-
settings.put_IsScriptEnabled(jsEnabled);
1150+
settings.put_IsScriptEnabled(jsEnabledOnNextPage);
11031151
// Register browser functions in the new document.
11041152
if (!functions.isEmpty()) {
11051153
StringBuilder sb = new StringBuilder();
@@ -1201,6 +1249,29 @@ int handleDOMContentLoaded(long pView, long pArgs) {
12011249
sendProgressCompleted();
12021250
}
12031251
}
1252+
executeInternal(
1253+
"""
1254+
const events = [%%events%%];
1255+
events.forEach(eventType => {
1256+
window.addEventListener(eventType, function(event) {
1257+
window.chrome.webview.postMessage([
1258+
'%%webmessagekind%%',
1259+
eventType,
1260+
event.altKey,
1261+
event.ctrlKey,
1262+
event.shiftKey,
1263+
event.clientX,
1264+
event.clientY,
1265+
event.button,
1266+
"fromElement" in event && event.fromElement != null,
1267+
"toElement" in event && event.toElement != null,
1268+
"deltaY" in event ? event.deltaY : 0,
1269+
]);
1270+
});
1271+
});
1272+
""" //
1273+
.replace("%%events%%", MOUSE_RELATED_DOM_EVENTS.stream().map(x -> "'" + x + "'").collect(Collectors.joining(", "))) //
1274+
.replace("%%webmessagekind%%", WEBMESSAGE_KIND_MOUSE_RELATED_DOM_EVENT));
12041275
return COM.S_OK;
12051276
}
12061277

@@ -1323,6 +1394,11 @@ int handleNavigationCompleted(long pView, long pArgs, boolean top) {
13231394
// ProgressListener.completed from here.
13241395
sendProgressCompleted();
13251396
}
1397+
if (top) {
1398+
jsEnabled = jsEnabledOnNextPage;
1399+
needsMouseMovementFallback = !jsEnabled;
1400+
scheduleFallbackMouseMovementHandlingIfNeeded();
1401+
}
13261402
int[] pIsSuccess = new int[1];
13271403
args.get_IsSuccess(pIsSuccess);
13281404
if (pIsSuccess[0] != 0) {
@@ -1529,6 +1605,136 @@ int handleMoveFocusRequested(long pView, long pArgs) {
15291605
return COM.S_OK;
15301606
}
15311607

1608+
int handleWebMessageReceived(long pView, long pArgs) {
1609+
ICoreWebView2WebMessageReceivedEventArgs args = new ICoreWebView2WebMessageReceivedEventArgs(pArgs);
1610+
long[] ppszWebMessageJson = new long[1];
1611+
int hr = args.get_WebMessageAsJson(ppszWebMessageJson);
1612+
if (hr != COM.S_OK) return hr;
1613+
try {
1614+
String webMessageJson = wstrToString(ppszWebMessageJson[0], true);
1615+
Object[] data = (Object[]) JSON.parse(webMessageJson);
1616+
if (WEBMESSAGE_KIND_MOUSE_RELATED_DOM_EVENT.equals(data[0])) {
1617+
MouseRelatedDomEvent mouseRelatedDomEvent = new MouseRelatedDomEvent( //
1618+
(String) data[1], //
1619+
(boolean) data[2], //
1620+
(boolean) data[3], //
1621+
(boolean) data[4], //
1622+
Math.round((double) data[5]), //
1623+
Math.round((double) data[6]), //
1624+
Math.round((double) data[7]), //
1625+
(boolean) data[8], //
1626+
(boolean) data[9], //
1627+
Math.round((double) data[10]) //
1628+
);
1629+
handleMouseRelatedDomEvent(mouseRelatedDomEvent);
1630+
}
1631+
} catch (Exception e) {
1632+
System.err.println(e);
1633+
}
1634+
return COM.S_OK;
1635+
}
1636+
1637+
/**
1638+
* Insipired by the mouse-event related parts of
1639+
* {@link IE#handleDOMEvent(org.eclipse.swt.ole.win32.OleEvent)}
1640+
*/
1641+
private void handleMouseRelatedDomEvent(MouseRelatedDomEvent domEvent) {
1642+
String eventType = domEvent.eventType();
1643+
1644+
/*
1645+
* Feature in Edge. MouseOver/MouseOut events are fired any time the mouse enters
1646+
* or exits any element within the Browser. To ensure that SWT events are only
1647+
* fired for mouse movements into or out of the Browser, do not fire an event if
1648+
* the element being exited (on MouseOver) or entered (on MouseExit) is within
1649+
* the Browser.
1650+
*/
1651+
if (eventType.equals(EVENT_MOUSEOVER)) {
1652+
if (domEvent.fromElementSet()) {
1653+
return;
1654+
}
1655+
}
1656+
if (eventType.equals(EVENT_MOUSEOUT)) {
1657+
if (domEvent.toElementSet()) {
1658+
return;
1659+
}
1660+
}
1661+
1662+
int mask = 0;
1663+
Event newEvent = new Event();
1664+
newEvent.widget = browser;
1665+
newEvent.x = (int) domEvent.clientX(); newEvent.y = (int) domEvent.clientY();
1666+
if (domEvent.ctrlKey()) mask |= SWT.CTRL;
1667+
if (domEvent.altKey()) mask |= SWT.ALT;
1668+
if (domEvent.shiftKey()) mask |= SWT.SHIFT;
1669+
newEvent.stateMask = mask;
1670+
1671+
int button = (int) domEvent.button();
1672+
switch (button) {
1673+
case 1: button = 1; break;
1674+
case 2: button = 3; break;
1675+
case 4: button = 2; break;
1676+
}
1677+
1678+
if (eventType.equals(EVENT_MOUSEDOWN)) {
1679+
newEvent.type = SWT.MouseDown;
1680+
newEvent.button = button;
1681+
newEvent.count = 1;
1682+
} else if (eventType.equals(EVENT_MOUSEUP) || eventType.equals(EVENT_DRAGEND)) {
1683+
newEvent.type = SWT.MouseUp;
1684+
newEvent.button = button != 0 ? button : 1; /* button assumed to be 1 for dragends */
1685+
newEvent.count = 1;
1686+
switch (newEvent.button) {
1687+
case 1: newEvent.stateMask |= SWT.BUTTON1; break;
1688+
case 2: newEvent.stateMask |= SWT.BUTTON2; break;
1689+
case 3: newEvent.stateMask |= SWT.BUTTON3; break;
1690+
case 4: newEvent.stateMask |= SWT.BUTTON4; break;
1691+
case 5: newEvent.stateMask |= SWT.BUTTON5; break;
1692+
}
1693+
} else if (eventType.equals(EVENT_MOUSEWHEEL)) {
1694+
newEvent.type = SWT.MouseWheel;
1695+
// Chromium/Edge uses deltaMode DOM_DELTA_PIXEL which
1696+
// - has a different sign than the legacy MouseWheelEvent wheelDelta
1697+
// - depends on the zoom of the browser
1698+
// https://github.com/w3c/uievents/issues/181
1699+
// The literal value of deltaY is therefore useless.
1700+
// Instead, we simply use the sign for the direction and combine
1701+
// it with the internal hard-coded value of '3 lines'.
1702+
newEvent.count = domEvent.deltaY() > 0 ? -3 : 3;
1703+
} else if (eventType.equals(EVENT_MOUSEMOVE)) {
1704+
/*
1705+
* Feature in Edge. Spurious and redundant mousemove events are often received. The workaround
1706+
* is to not fire MouseMove events whose x and y values match the last MouseMove.
1707+
*/
1708+
if (newEvent.x == lastMouseMoveX && newEvent.y == lastMouseMoveY) {
1709+
return;
1710+
}
1711+
newEvent.type = SWT.MouseMove;
1712+
lastMouseMoveX = newEvent.x; lastMouseMoveY = newEvent.y;
1713+
} else if (eventType.equals(EVENT_MOUSEOVER)) {
1714+
newEvent.type = SWT.MouseEnter;
1715+
} else if (eventType.equals(EVENT_MOUSEOUT)) {
1716+
newEvent.type = SWT.MouseExit;
1717+
} else if (eventType.equals(EVENT_DRAGSTART)) {
1718+
newEvent.type = SWT.DragDetect;
1719+
newEvent.button = 1; /* button assumed to be 1 for dragstarts */
1720+
newEvent.stateMask |= SWT.BUTTON1;
1721+
}
1722+
1723+
browser.notifyListeners(newEvent.type, newEvent);
1724+
1725+
if (eventType.equals(EVENT_DOUBLECLICK)) {
1726+
newEvent = new Event ();
1727+
newEvent.widget = browser;
1728+
newEvent.type = SWT.MouseDoubleClick;
1729+
newEvent.x = (int) domEvent.clientX(); newEvent.y = (int) domEvent.clientY();
1730+
newEvent.stateMask = mask;
1731+
newEvent.type = SWT.MouseDoubleClick;
1732+
newEvent.button = 1; /* dblclick only comes for button 1 and does not set the button property */
1733+
newEvent.count = 2;
1734+
browser.notifyListeners (newEvent.type, newEvent);
1735+
}
1736+
}
1737+
15321738
@Override
15331739
public boolean isBackEnabled() {
15341740
int[] pval = new int[1];

bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/ole/win32/ICoreWebView2.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ public int PostWebMessageAsJson(char[] webMessageAsJson) {
7979
return COM.VtblCall(32, address, webMessageAsJson);
8080
}
8181

82-
public int add_WebMessageReceived(long handler, long[] token) {
83-
return COM.VtblCall(34, address, handler, token);
82+
public int add_WebMessageReceived(IUnknown eventHandler, long[] token) {
83+
return COM.VtblCall(34, address, eventHandler.address, token);
8484
}
8585

8686
public int get_CanGoBack(int[] canGoBack) {

0 commit comments

Comments
 (0)