-
Notifications
You must be signed in to change notification settings - Fork 184
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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.*; | ||
|
@@ -88,9 +89,34 @@ public WebViewEnvironment(ICoreWebView2Environment environment) { | |
private boolean ignoreFocusIn; | ||
private String lastCustomText; | ||
|
||
private boolean atLeastOneTopNavigationCompleted; | ||
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 = () -> { | ||
ICoreWebView2CookieManager manager = getCookieManager(); | ||
|
@@ -775,6 +801,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(); | ||
|
@@ -814,7 +843,7 @@ 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(); | ||
scheduleFallbackMouseMovementHandlingIfNeeded(); | ||
|
||
// 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 | ||
|
@@ -881,19 +910,27 @@ void browserResize(Event event) { | |
controller.put_IsVisible(true); | ||
} | ||
|
||
private void scheduleMouseMovementHandling() { | ||
/** | ||
* We need the fallback whenever javascript is *not* enabled, but also for fresh | ||
* Edge instances until the first javascript-enabled page has successfully | ||
* loaded. | ||
*/ | ||
private void scheduleFallbackMouseMovementHandlingIfNeeded() { | ||
if (atLeastOneTopNavigationCompleted && jsEnabled) { | ||
return; | ||
} | ||
browser.getDisplay().timerExec(100, () -> { | ||
if (browser.isDisposed()) { | ||
return; | ||
} | ||
if (browser.isVisible() && hasDisplayFocus()) { | ||
handleMouseMovement(); | ||
handleFallbackMouseMovement(); | ||
} | ||
scheduleMouseMovementHandling(); | ||
scheduleFallbackMouseMovementHandlingIfNeeded(); | ||
}); | ||
} | ||
|
||
private void handleMouseMovement() { | ||
private void handleFallbackMouseMovement() { | ||
final Point currentCursorLocation = browser.getDisplay().getCursorLocation(); | ||
Point cursorLocationInControlCoordinate = browser.toControl(currentCursorLocation); | ||
boolean isCursorInsideBrowser = browser.getBounds().contains(cursorLocationInControlCoordinate); | ||
|
@@ -969,6 +1006,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(); | ||
|
@@ -1098,8 +1143,7 @@ int handleNavigationStarting(long pView, long pArgs, boolean top) { | |
// will be eventually cleared again in handleNavigationCompleted(). | ||
navigations.put(pNavId[0], event); | ||
if (event.doit) { | ||
jsEnabled = jsEnabledOnNextPage; | ||
settings.put_IsScriptEnabled(jsEnabled); | ||
settings.put_IsScriptEnabled(jsEnabledOnNextPage); | ||
// Register browser functions in the new document. | ||
if (!functions.isEmpty()) { | ||
StringBuilder sb = new StringBuilder(); | ||
|
@@ -1201,6 +1245,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; | ||
} | ||
|
||
|
@@ -1323,6 +1390,11 @@ int handleNavigationCompleted(long pView, long pArgs, boolean top) { | |
// ProgressListener.completed from here. | ||
sendProgressCompleted(); | ||
} | ||
if (top) { | ||
jsEnabled = jsEnabledOnNextPage; | ||
atLeastOneTopNavigationCompleted = true; | ||
scheduleFallbackMouseMovementHandlingIfNeeded(); | ||
} | ||
int[] pIsSuccess = new int[1]; | ||
args.get_IsSuccess(pIsSuccess); | ||
if (pIsSuccess[0] != 0) { | ||
|
@@ -1529,6 +1601,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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this method can take care of mouse related events, shall we get rid of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure I understand.
Getting rid of I don't think this makes sense. But we should probably rename
for clarity. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I completely misunderstood. The name changes are fine. I would like to propose that we change the field |
||
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]; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we get rid of the trailing //? what is the purpose of them here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to group the different semantics / pairs per line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I often do that too to make details more readable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay good to know :)