Skip to content

Commit 0d75c70

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). This obsoletes and removes the workaound implemented in #1551.
1 parent 3d459ab commit 0d75c70

File tree

2 files changed

+190
-51
lines changed
  • bundles/org.eclipse.swt

2 files changed

+190
-51
lines changed

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

Lines changed: 188 additions & 49 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,8 +89,29 @@ public WebViewEnvironment(ICoreWebView2Environment environment) {
8889
private boolean ignoreFocusIn;
8990
private String lastCustomText;
9091

91-
private static record CursorPosition(Point location, boolean isInsideBrowser) {};
92-
private CursorPosition previousCursorPosition = new CursorPosition(new Point(0, 0), false);
92+
private static final String WEBMESSAGE_KIND_MOUSE_RELATED_DOM_EVENT = "mouse-related-dom-event";
93+
private static final String EVENT_MOUSEDOWN = "mousedown"; //$NON-NLS-1$
94+
private static final String EVENT_MOUSEUP = "mouseup"; //$NON-NLS-1$
95+
private static final String EVENT_MOUSEMOVE = "mousemove"; //$NON-NLS-1$
96+
private static final String EVENT_MOUSEOVER = "mouseover"; //$NON-NLS-1$
97+
private static final String EVENT_MOUSEOUT = "mouseout"; //$NON-NLS-1$
98+
private static final String EVENT_DRAGSTART = "dragstart"; //$NON-NLS-1$
99+
private static final String EVENT_DRAGEND = "dragend"; //$NON-NLS-1$
100+
private static final String EVENT_DOUBLECLICK = "dblclick"; //$NON-NLS-1$
101+
private static final String EVENT_MOUSEWHEEL = "wheel"; //$NON-NLS-1$
102+
private static final Set<String> MOUSE_RELATED_DOM_EVENTS = Set.of( //
103+
EVENT_MOUSEDOWN, EVENT_MOUSEUP, //
104+
EVENT_MOUSEMOVE, //
105+
EVENT_MOUSEOVER, EVENT_MOUSEOUT, //
106+
EVENT_DRAGSTART, EVENT_DRAGEND, //
107+
EVENT_DOUBLECLICK, //
108+
EVENT_MOUSEWHEEL//
109+
);
110+
private static record MouseRelatedDomEvent(String eventType, boolean altKey, boolean ctrlKey, boolean shiftKey,
111+
long clientX, long clientY, long button, boolean fromElementSet, boolean toElementSet, long deltaY) {
112+
}
113+
private int lastMouseMoveX;
114+
private int lastMouseMoveY;
93115

94116
static {
95117
NativeClearSessions = () -> {
@@ -759,6 +781,9 @@ void setupBrowser(int hr, long pv) {
759781
handler = newCallback(this::handleSourceChanged);
760782
webView.add_SourceChanged(handler, token);
761783
handler.Release();
784+
handler = newCallback(this::handleWebMessageReceived);
785+
webView.add_WebMessageReceived(handler, token);
786+
handler.Release();
762787
handler = newCallback(this::handleMoveFocusRequested);
763788
controller.add_MoveFocusRequested(handler, token);
764789
handler.Release();
@@ -798,7 +823,6 @@ void setupBrowser(int hr, long pv) {
798823
browser.addListener(SWT.FocusIn, this::browserFocusIn);
799824
browser.addListener(SWT.Resize, this::browserResize);
800825
browser.addListener(SWT.Move, this::browserMove);
801-
scheduleMouseMovementHandling();
802826

803827
// Sometimes when the shell of the browser is opened before the browser is
804828
// initialized, nothing is drawn on the shell. We need browserResize to force
@@ -865,52 +889,6 @@ void browserResize(Event event) {
865889
controller.put_IsVisible(true);
866890
}
867891

868-
private void scheduleMouseMovementHandling() {
869-
browser.getDisplay().timerExec(100, () -> {
870-
if (browser.isDisposed()) {
871-
return;
872-
}
873-
if (browser.isVisible() && hasDisplayFocus()) {
874-
handleMouseMovement();
875-
}
876-
scheduleMouseMovementHandling();
877-
});
878-
}
879-
880-
private void handleMouseMovement() {
881-
final Point currentCursorLocation = browser.getDisplay().getCursorLocation();
882-
Point cursorLocationInControlCoordinate = browser.toControl(currentCursorLocation);
883-
boolean isCursorInsideBrowser = browser.getBounds().contains(cursorLocationInControlCoordinate);
884-
boolean hasCursorLocationChanged = !currentCursorLocation.equals(previousCursorPosition.location);
885-
886-
boolean mousePassedBrowserBorder = previousCursorPosition.isInsideBrowser != isCursorInsideBrowser;
887-
boolean mouseMovedInsideBrowser = isCursorInsideBrowser && hasCursorLocationChanged;
888-
if (mousePassedBrowserBorder) {
889-
if (isCursorInsideBrowser) {
890-
sendMouseEvent(cursorLocationInControlCoordinate, SWT.MouseEnter);
891-
} else {
892-
sendMouseEvent(cursorLocationInControlCoordinate, SWT.MouseExit);
893-
}
894-
} else if (mouseMovedInsideBrowser) {
895-
sendMouseEvent(cursorLocationInControlCoordinate, SWT.MouseMove);
896-
}
897-
previousCursorPosition = new CursorPosition(currentCursorLocation, isCursorInsideBrowser);
898-
}
899-
900-
private void sendMouseEvent(Point cursorLocationInControlCoordinate, int mouseEvent) {
901-
Event newEvent = new Event();
902-
newEvent.widget = browser;
903-
Point position = cursorLocationInControlCoordinate;
904-
newEvent.x = position.x;
905-
newEvent.y = position.y;
906-
newEvent.type = mouseEvent;
907-
browser.notifyListeners(newEvent.type, newEvent);
908-
}
909-
910-
private boolean hasDisplayFocus() {
911-
return browser.getDisplay().getFocusControl() != null;
912-
}
913-
914892
@Override
915893
public Object evaluate(String script) throws SWTException {
916894
// Feature in WebView2. ExecuteScript works regardless of IsScriptEnabled setting.
@@ -953,6 +931,14 @@ public boolean execute(String script) {
953931
// Feature in WebView2. ExecuteScript works regardless of IsScriptEnabled setting.
954932
// Disallow programmatic execution manually.
955933
if (!jsEnabled) return false;
934+
return executeInternal(script);
935+
}
936+
937+
/**
938+
* Unconditional script execution, bypassing {@link WebBrowser#jsEnabled} flag /
939+
* {@link Browser#setJavascriptEnabled(boolean)}.
940+
*/
941+
private boolean executeInternal(String script) {
956942
IUnknown completion = newCallback((long result, long json) -> COM.S_OK);
957943
int hr = webViewProvider.getWebView(true).ExecuteScript(stringToWstr(script), completion);
958944
completion.Release();
@@ -1173,6 +1159,29 @@ int handleDOMContentLoaded(long pView, long pArgs) {
11731159
sendProgressCompleted();
11741160
}
11751161
}
1162+
executeInternal(
1163+
"""
1164+
const events = [%%events%%];
1165+
events.forEach(eventType => {
1166+
window.addEventListener(eventType, function(event) {
1167+
window.chrome.webview.postMessage([
1168+
'%%webmessagekind%%',
1169+
eventType,
1170+
event.altKey,
1171+
event.ctrlKey,
1172+
event.shiftKey,
1173+
event.clientX,
1174+
event.clientY,
1175+
event.button,
1176+
"fromElement" in event && event.fromElement != null,
1177+
"toElement" in event && event.toElement != null,
1178+
"deltaY" in event ? event.deltaY : 0,
1179+
]);
1180+
});
1181+
});
1182+
""" //
1183+
.replace("%%events%%", MOUSE_RELATED_DOM_EVENTS.stream().map(x -> "'" + x + "'").collect(Collectors.joining(", "))) //
1184+
.replace("%%webmessagekind%%", WEBMESSAGE_KIND_MOUSE_RELATED_DOM_EVENT));
11761185
return COM.S_OK;
11771186
}
11781187

@@ -1496,6 +1505,136 @@ int handleMoveFocusRequested(long pView, long pArgs) {
14961505
return COM.S_OK;
14971506
}
14981507

1508+
int handleWebMessageReceived(long pView, long pArgs) {
1509+
ICoreWebView2WebMessageReceivedEventArgs args = new ICoreWebView2WebMessageReceivedEventArgs(pArgs);
1510+
long[] ppszWebMessageJson = new long[1];
1511+
int hr = args.get_WebMessageAsJson(ppszWebMessageJson);
1512+
if (hr != COM.S_OK) return hr;
1513+
try {
1514+
String webMessageJson = wstrToString(ppszWebMessageJson[0], true);
1515+
Object[] data = (Object[]) JSON.parse(webMessageJson);
1516+
if (WEBMESSAGE_KIND_MOUSE_RELATED_DOM_EVENT.equals(data[0])) {
1517+
MouseRelatedDomEvent mouseRelatedDomEvent = new MouseRelatedDomEvent( //
1518+
(String) data[1], //
1519+
(boolean) data[2], //
1520+
(boolean) data[3], //
1521+
(boolean) data[4], //
1522+
Math.round((double) data[5]), //
1523+
Math.round((double) data[6]), //
1524+
Math.round((double) data[7]), //
1525+
(boolean) data[8], //
1526+
(boolean) data[9], //
1527+
Math.round((double) data[10]) //
1528+
);
1529+
handleMouseRelatedDomEvent(mouseRelatedDomEvent);
1530+
}
1531+
} catch (Exception e) {
1532+
System.err.println(e);
1533+
}
1534+
return COM.S_OK;
1535+
}
1536+
1537+
/**
1538+
* Insipired by the mouse-event related parts of
1539+
* {@link IE#handleDOMEvent(org.eclipse.swt.ole.win32.OleEvent)}
1540+
*/
1541+
private void handleMouseRelatedDomEvent(MouseRelatedDomEvent domEvent) {
1542+
String eventType = domEvent.eventType();
1543+
1544+
/*
1545+
* Feature in Edge. MouseOver/MouseOut events are fired any time the mouse enters
1546+
* or exits any element within the Browser. To ensure that SWT events are only
1547+
* fired for mouse movements into or out of the Browser, do not fire an event if
1548+
* the element being exited (on MouseOver) or entered (on MouseExit) is within
1549+
* the Browser.
1550+
*/
1551+
if (eventType.equals(EVENT_MOUSEOVER)) {
1552+
if (domEvent.fromElementSet()) {
1553+
return;
1554+
}
1555+
}
1556+
if (eventType.equals(EVENT_MOUSEOUT)) {
1557+
if (domEvent.toElementSet()) {
1558+
return;
1559+
}
1560+
}
1561+
1562+
int mask = 0;
1563+
Event newEvent = new Event();
1564+
newEvent.widget = browser;
1565+
newEvent.x = (int) domEvent.clientX(); newEvent.y = (int) domEvent.clientY();
1566+
if (domEvent.ctrlKey()) mask |= SWT.CTRL;
1567+
if (domEvent.altKey()) mask |= SWT.ALT;
1568+
if (domEvent.shiftKey()) mask |= SWT.SHIFT;
1569+
newEvent.stateMask = mask;
1570+
1571+
int button = (int) domEvent.button();
1572+
switch (button) {
1573+
case 1: button = 1; break;
1574+
case 2: button = 3; break;
1575+
case 4: button = 2; break;
1576+
}
1577+
1578+
if (eventType.equals(EVENT_MOUSEDOWN)) {
1579+
newEvent.type = SWT.MouseDown;
1580+
newEvent.button = button;
1581+
newEvent.count = 1;
1582+
} else if (eventType.equals(EVENT_MOUSEUP) || eventType.equals(EVENT_DRAGEND)) {
1583+
newEvent.type = SWT.MouseUp;
1584+
newEvent.button = button != 0 ? button : 1; /* button assumed to be 1 for dragends */
1585+
newEvent.count = 1;
1586+
switch (newEvent.button) {
1587+
case 1: newEvent.stateMask |= SWT.BUTTON1; break;
1588+
case 2: newEvent.stateMask |= SWT.BUTTON2; break;
1589+
case 3: newEvent.stateMask |= SWT.BUTTON3; break;
1590+
case 4: newEvent.stateMask |= SWT.BUTTON4; break;
1591+
case 5: newEvent.stateMask |= SWT.BUTTON5; break;
1592+
}
1593+
} else if (eventType.equals(EVENT_MOUSEWHEEL)) {
1594+
newEvent.type = SWT.MouseWheel;
1595+
// Chromium/Edge uses deltaMode DOM_DELTA_PIXEL which
1596+
// - has a different sign than the legacy MouseWheelEvent wheelDelta
1597+
// - depends on the zoom of the browser
1598+
// https://github.com/w3c/uievents/issues/181
1599+
// The literal value of deltaY is therefore useless.
1600+
// Instead, we simply use the sign for the direction and combine
1601+
// it with the internal hard-coded value of '3 lines'.
1602+
newEvent.count = domEvent.deltaY() > 0 ? -3 : 3;
1603+
} else if (eventType.equals(EVENT_MOUSEMOVE)) {
1604+
/*
1605+
* Feature in Edge. Spurious and redundant mousemove events are often received. The workaround
1606+
* is to not fire MouseMove events whose x and y values match the last MouseMove.
1607+
*/
1608+
if (newEvent.x == lastMouseMoveX && newEvent.y == lastMouseMoveY) {
1609+
return;
1610+
}
1611+
newEvent.type = SWT.MouseMove;
1612+
lastMouseMoveX = newEvent.x; lastMouseMoveY = newEvent.y;
1613+
} else if (eventType.equals(EVENT_MOUSEOVER)) {
1614+
newEvent.type = SWT.MouseEnter;
1615+
} else if (eventType.equals(EVENT_MOUSEOUT)) {
1616+
newEvent.type = SWT.MouseExit;
1617+
} else if (eventType.equals(EVENT_DRAGSTART)) {
1618+
newEvent.type = SWT.DragDetect;
1619+
newEvent.button = 1; /* button assumed to be 1 for dragstarts */
1620+
newEvent.stateMask |= SWT.BUTTON1;
1621+
}
1622+
1623+
browser.notifyListeners(newEvent.type, newEvent);
1624+
1625+
if (eventType.equals(EVENT_DOUBLECLICK)) {
1626+
newEvent = new Event ();
1627+
newEvent.widget = browser;
1628+
newEvent.type = SWT.MouseDoubleClick;
1629+
newEvent.x = (int) domEvent.clientX(); newEvent.y = (int) domEvent.clientY();
1630+
newEvent.stateMask = mask;
1631+
newEvent.type = SWT.MouseDoubleClick;
1632+
newEvent.button = 1; /* dblclick only comes for button 1 and does not set the button property */
1633+
newEvent.count = 2;
1634+
browser.notifyListeners (newEvent.type, newEvent);
1635+
}
1636+
}
1637+
14991638
@Override
15001639
public boolean isBackEnabled() {
15011640
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)