Skip to content

Edge: Implement mouse-related event listener support #2097

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -88,8 +89,29 @@ public WebViewEnvironment(ICoreWebView2Environment environment) {
private boolean ignoreFocusIn;
private String lastCustomText;

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<String> 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 = () -> {
Expand Down Expand Up @@ -759,6 +781,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();
Expand Down Expand Up @@ -798,7 +823,6 @@ 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();

// 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
Expand Down Expand Up @@ -865,52 +889,6 @@ void browserResize(Event event) {
controller.put_IsVisible(true);
}

private void scheduleMouseMovementHandling() {
browser.getDisplay().timerExec(100, () -> {
if (browser.isDisposed()) {
return;
}
if (browser.isVisible() && hasDisplayFocus()) {
handleMouseMovement();
}
scheduleMouseMovementHandling();
});
}

private void handleMouseMovement() {
final Point currentCursorLocation = browser.getDisplay().getCursorLocation();
Point cursorLocationInControlCoordinate = browser.toControl(currentCursorLocation);
boolean isCursorInsideBrowser = browser.getBounds().contains(cursorLocationInControlCoordinate);
boolean hasCursorLocationChanged = !currentCursorLocation.equals(previousCursorPosition.location);

boolean mousePassedBrowserBorder = previousCursorPosition.isInsideBrowser != isCursorInsideBrowser;
boolean mouseMovedInsideBrowser = isCursorInsideBrowser && hasCursorLocationChanged;
if (mousePassedBrowserBorder) {
if (isCursorInsideBrowser) {
sendMouseEvent(cursorLocationInControlCoordinate, SWT.MouseEnter);
} else {
sendMouseEvent(cursorLocationInControlCoordinate, SWT.MouseExit);
}
} else if (mouseMovedInsideBrowser) {
sendMouseEvent(cursorLocationInControlCoordinate, SWT.MouseMove);
}
previousCursorPosition = new CursorPosition(currentCursorLocation, isCursorInsideBrowser);
}

private void sendMouseEvent(Point cursorLocationInControlCoordinate, int mouseEvent) {
Event newEvent = new Event();
newEvent.widget = browser;
Point position = cursorLocationInControlCoordinate;
newEvent.x = position.x;
newEvent.y = position.y;
newEvent.type = mouseEvent;
browser.notifyListeners(newEvent.type, newEvent);
}

private boolean hasDisplayFocus() {
return browser.getDisplay().getFocusControl() != null;
}

@Override
public Object evaluate(String script) throws SWTException {
// Feature in WebView2. ExecuteScript works regardless of IsScriptEnabled setting.
Expand Down Expand Up @@ -953,6 +931,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();
Expand Down Expand Up @@ -1173,6 +1159,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;
}

Expand Down Expand Up @@ -1496,6 +1505,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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading