Skip to content

Commit c8ac9dd

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 e2ac7f1 commit c8ac9dd

File tree

3 files changed

+213
-7
lines changed
  • bundles/org.eclipse.swt

3 files changed

+213
-7
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

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

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

Lines changed: 205 additions & 5 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 needsMouseMovementFallback = true;
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+
scheduleMouseMovementHandlingIfNeeded();
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,15 +910,21 @@ void browserResize(Event event) {
881910
controller.put_IsVisible(true);
882911
}
883912

884-
private void scheduleMouseMovementHandling() {
913+
private void scheduleMouseMovementHandlingIfNeeded() {
914+
if (!needsMouseMovementFallback) {
915+
return;
916+
}
885917
browser.getDisplay().timerExec(100, () -> {
886918
if (browser.isDisposed()) {
887919
return;
888920
}
889921
if (browser.isVisible() && hasDisplayFocus()) {
890922
handleMouseMovement();
891923
}
892-
scheduleMouseMovementHandling();
924+
if (!needsMouseMovementFallback) {
925+
return;
926+
}
927+
scheduleMouseMovementHandlingIfNeeded();
893928
});
894929
}
895930

@@ -969,6 +1004,14 @@ public boolean execute(String script) {
9691004
// Feature in WebView2. ExecuteScript works regardless of IsScriptEnabled setting.
9701005
// Disallow programmatic execution manually.
9711006
if (!jsEnabled) return false;
1007+
return executeInternal(script);
1008+
}
1009+
1010+
/**
1011+
* Unconditional script execution, bypassing {@link WebBrowser#jsEnabled} flag /
1012+
* {@link Browser#setJavascriptEnabled(boolean)}.
1013+
*/
1014+
private boolean executeInternal(String script) {
9721015
IUnknown completion = newCallback((long result, long json) -> COM.S_OK);
9731016
int hr = webViewProvider.getWebView(true).ExecuteScript(stringToWstr(script), completion);
9741017
completion.Release();
@@ -1098,8 +1141,7 @@ int handleNavigationStarting(long pView, long pArgs, boolean top) {
10981141
// will be eventually cleared again in handleNavigationCompleted().
10991142
navigations.put(pNavId[0], event);
11001143
if (event.doit) {
1101-
jsEnabled = jsEnabledOnNextPage;
1102-
settings.put_IsScriptEnabled(jsEnabled);
1144+
settings.put_IsScriptEnabled(jsEnabledOnNextPage);
11031145
// Register browser functions in the new document.
11041146
if (!functions.isEmpty()) {
11051147
StringBuilder sb = new StringBuilder();
@@ -1201,6 +1243,29 @@ int handleDOMContentLoaded(long pView, long pArgs) {
12011243
sendProgressCompleted();
12021244
}
12031245
}
1246+
executeInternal(
1247+
"""
1248+
const events = [%%events%%];
1249+
events.forEach(eventType => {
1250+
window.addEventListener(eventType, function(event) {
1251+
window.chrome.webview.postMessage([
1252+
'%%webmessagekind%%',
1253+
eventType,
1254+
event.altKey,
1255+
event.ctrlKey,
1256+
event.shiftKey,
1257+
event.clientX,
1258+
event.clientY,
1259+
event.button,
1260+
"fromElement" in event && event.fromElement != null,
1261+
"toElement" in event && event.toElement != null,
1262+
"deltaY" in event ? event.deltaY : 0,
1263+
]);
1264+
});
1265+
});
1266+
""" //
1267+
.replace("%%events%%", MOUSE_RELATED_DOM_EVENTS.stream().map(x -> "'" + x + "'").collect(Collectors.joining(", "))) //
1268+
.replace("%%webmessagekind%%", WEBMESSAGE_KIND_MOUSE_RELATED_DOM_EVENT));
12041269
return COM.S_OK;
12051270
}
12061271

@@ -1323,6 +1388,11 @@ int handleNavigationCompleted(long pView, long pArgs, boolean top) {
13231388
// ProgressListener.completed from here.
13241389
sendProgressCompleted();
13251390
}
1391+
if (top) {
1392+
jsEnabled = jsEnabledOnNextPage;
1393+
needsMouseMovementFallback = !jsEnabled;
1394+
scheduleMouseMovementHandlingIfNeeded();
1395+
}
13261396
int[] pIsSuccess = new int[1];
13271397
args.get_IsSuccess(pIsSuccess);
13281398
if (pIsSuccess[0] != 0) {
@@ -1524,6 +1594,136 @@ int handleMoveFocusRequested(long pView, long pArgs) {
15241594
return COM.S_OK;
15251595
}
15261596

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