Skip to content

Commit cbf7f7a

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 43f54cc commit cbf7f7a

File tree

3 files changed

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

3 files changed

+217
-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: 209 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,34 @@ public WebViewEnvironment(ICoreWebView2Environment environment) {
8889
private boolean ignoreFocusIn;
8990
private String lastCustomText;
9091

92+
private boolean atLeastOneTopNavigationCompleted;
9193
private static record CursorPosition(Point location, boolean isInsideBrowser) {};
9294
private CursorPosition previousCursorPosition = new CursorPosition(new Point(0, 0), false);
9395

96+
private static final String WEBMESSAGE_KIND_MOUSE_RELATED_DOM_EVENT = "mouse-related-dom-event";
97+
private static final String EVENT_MOUSEDOWN = "mousedown"; //$NON-NLS-1$
98+
private static final String EVENT_MOUSEUP = "mouseup"; //$NON-NLS-1$
99+
private static final String EVENT_MOUSEMOVE = "mousemove"; //$NON-NLS-1$
100+
private static final String EVENT_MOUSEOVER = "mouseover"; //$NON-NLS-1$
101+
private static final String EVENT_MOUSEOUT = "mouseout"; //$NON-NLS-1$
102+
private static final String EVENT_DRAGSTART = "dragstart"; //$NON-NLS-1$
103+
private static final String EVENT_DRAGEND = "dragend"; //$NON-NLS-1$
104+
private static final String EVENT_DOUBLECLICK = "dblclick"; //$NON-NLS-1$
105+
private static final String EVENT_MOUSEWHEEL = "wheel"; //$NON-NLS-1$
106+
private static final Set<String> MOUSE_RELATED_DOM_EVENTS = Set.of( //
107+
EVENT_MOUSEDOWN, EVENT_MOUSEUP, //
108+
EVENT_MOUSEMOVE, //
109+
EVENT_MOUSEOVER, EVENT_MOUSEOUT, //
110+
EVENT_DRAGSTART, EVENT_DRAGEND, //
111+
EVENT_DOUBLECLICK, //
112+
EVENT_MOUSEWHEEL//
113+
);
114+
private static record MouseRelatedDomEvent(String eventType, boolean altKey, boolean ctrlKey, boolean shiftKey,
115+
long clientX, long clientY, long button, boolean fromElementSet, boolean toElementSet, long deltaY) {
116+
}
117+
private int lastMouseMoveX;
118+
private int lastMouseMoveY;
119+
94120
static {
95121
NativeClearSessions = () -> {
96122
ICoreWebView2CookieManager manager = getCookieManager();
@@ -775,6 +801,9 @@ void setupBrowser(int hr, long pv) {
775801
handler = newCallback(this::handleSourceChanged);
776802
webView.add_SourceChanged(handler, token);
777803
handler.Release();
804+
handler = newCallback(this::handleWebMessageReceived);
805+
webView.add_WebMessageReceived(handler, token);
806+
handler.Release();
778807
handler = newCallback(this::handleMoveFocusRequested);
779808
controller.add_MoveFocusRequested(handler, token);
780809
handler.Release();
@@ -814,7 +843,7 @@ void setupBrowser(int hr, long pv) {
814843
browser.addListener(SWT.FocusIn, this::browserFocusIn);
815844
browser.addListener(SWT.Resize, this::browserResize);
816845
browser.addListener(SWT.Move, this::browserMove);
817-
scheduleMouseMovementHandling();
846+
scheduleFallbackMouseMovementHandlingIfNeeded();
818847

819848
// Sometimes when the shell of the browser is opened before the browser is
820849
// initialized, nothing is drawn on the shell. We need browserResize to force
@@ -881,19 +910,27 @@ void browserResize(Event event) {
881910
controller.put_IsVisible(true);
882911
}
883912

884-
private void scheduleMouseMovementHandling() {
913+
/**
914+
* We need the fallback whenever javascript is *not* enabled, but also for fresh
915+
* Edge instances until the first javascript-enabled page has successfully
916+
* loaded.
917+
*/
918+
private void scheduleFallbackMouseMovementHandlingIfNeeded() {
919+
if (atLeastOneTopNavigationCompleted && jsEnabled) {
920+
return;
921+
}
885922
browser.getDisplay().timerExec(100, () -> {
886923
if (browser.isDisposed()) {
887924
return;
888925
}
889926
if (browser.isVisible() && hasDisplayFocus()) {
890-
handleMouseMovement();
927+
handleFallbackMouseMovement();
891928
}
892-
scheduleMouseMovementHandling();
929+
scheduleFallbackMouseMovementHandlingIfNeeded();
893930
});
894931
}
895932

896-
private void handleMouseMovement() {
933+
private void handleFallbackMouseMovement() {
897934
final Point currentCursorLocation = browser.getDisplay().getCursorLocation();
898935
Point cursorLocationInControlCoordinate = browser.toControl(currentCursorLocation);
899936
boolean isCursorInsideBrowser = browser.getBounds().contains(cursorLocationInControlCoordinate);
@@ -969,6 +1006,14 @@ public boolean execute(String script) {
9691006
// Feature in WebView2. ExecuteScript works regardless of IsScriptEnabled setting.
9701007
// Disallow programmatic execution manually.
9711008
if (!jsEnabled) return false;
1009+
return executeInternal(script);
1010+
}
1011+
1012+
/**
1013+
* Unconditional script execution, bypassing {@link WebBrowser#jsEnabled} flag /
1014+
* {@link Browser#setJavascriptEnabled(boolean)}.
1015+
*/
1016+
private boolean executeInternal(String script) {
9721017
IUnknown completion = newCallback((long result, long json) -> COM.S_OK);
9731018
int hr = webViewProvider.getWebView(true).ExecuteScript(stringToWstr(script), completion);
9741019
completion.Release();
@@ -1098,8 +1143,7 @@ int handleNavigationStarting(long pView, long pArgs, boolean top) {
10981143
// will be eventually cleared again in handleNavigationCompleted().
10991144
navigations.put(pNavId[0], event);
11001145
if (event.doit) {
1101-
jsEnabled = jsEnabledOnNextPage;
1102-
settings.put_IsScriptEnabled(jsEnabled);
1146+
settings.put_IsScriptEnabled(jsEnabledOnNextPage);
11031147
// Register browser functions in the new document.
11041148
if (!functions.isEmpty()) {
11051149
StringBuilder sb = new StringBuilder();
@@ -1201,6 +1245,29 @@ int handleDOMContentLoaded(long pView, long pArgs) {
12011245
sendProgressCompleted();
12021246
}
12031247
}
1248+
executeInternal(
1249+
"""
1250+
const events = [%%events%%];
1251+
events.forEach(eventType => {
1252+
window.addEventListener(eventType, function(event) {
1253+
window.chrome.webview.postMessage([
1254+
'%%webmessagekind%%',
1255+
eventType,
1256+
event.altKey,
1257+
event.ctrlKey,
1258+
event.shiftKey,
1259+
event.clientX,
1260+
event.clientY,
1261+
event.button,
1262+
"fromElement" in event && event.fromElement != null,
1263+
"toElement" in event && event.toElement != null,
1264+
"deltaY" in event ? event.deltaY : 0,
1265+
]);
1266+
});
1267+
});
1268+
""" //
1269+
.replace("%%events%%", MOUSE_RELATED_DOM_EVENTS.stream().map(x -> "'" + x + "'").collect(Collectors.joining(", "))) //
1270+
.replace("%%webmessagekind%%", WEBMESSAGE_KIND_MOUSE_RELATED_DOM_EVENT));
12041271
return COM.S_OK;
12051272
}
12061273

@@ -1323,6 +1390,11 @@ int handleNavigationCompleted(long pView, long pArgs, boolean top) {
13231390
// ProgressListener.completed from here.
13241391
sendProgressCompleted();
13251392
}
1393+
if (top) {
1394+
jsEnabled = jsEnabledOnNextPage;
1395+
atLeastOneTopNavigationCompleted = true;
1396+
scheduleFallbackMouseMovementHandlingIfNeeded();
1397+
}
13261398
int[] pIsSuccess = new int[1];
13271399
args.get_IsSuccess(pIsSuccess);
13281400
if (pIsSuccess[0] != 0) {
@@ -1529,6 +1601,136 @@ int handleMoveFocusRequested(long pView, long pArgs) {
15291601
return COM.S_OK;
15301602
}
15311603

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