Skip to content
Merged
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 @@ -16,6 +16,7 @@
import java.util.*;

import org.eclipse.swt.*;
import org.eclipse.swt.events.*;
import org.eclipse.swt.program.*;
import org.eclipse.swt.widgets.*;

Expand Down Expand Up @@ -1109,6 +1110,11 @@ public void removeVisibilityWindowListener (VisibilityWindowListener listener) {
* Sets whether javascript will be allowed to run in pages subsequently
* viewed in the receiver. Note that setting this value does not affect
* the running of javascript in the current page.
* <p>
* Note: When using the {@link SWT#EDGE} browser on Windows disabling javascript
* has certain side effects, e.g. proper support for {@link MouseEvent} depends
* on it and, when disabled, a limited timer-based fallback implementation is
* used.
*
* @param enabled the receiver's new javascript enabled state
*
Expand Down
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,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, //
Copy link
Contributor

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?

Copy link
Member Author

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

Copy link
Contributor

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay good to know :)

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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Copy link
Contributor

@amartya4256 amartya4256 Sep 19, 2025

Choose a reason for hiding this comment

The 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 handleMouseMovement and implement it here instead? Then we can also handle them right here.

Copy link
Member Author

@sratz sratz Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand.

handleMouseMovement() is the fallback handling when javascript is disabled which produces artifical events.

handleMouseRelatedDomEvent(MouseRelatedDomEvent) is the logic that is being invoked from within javascript.

Getting rid of handleMouseMovement would mean we'd have to produce artificial DOM events inside the scheduleMouseMovementHandlingIfNeeded() lambda just to unpack them again in handleMouseRelatedDomEvent.

I don't think this makes sense.

But we should probably rename

  • scheduleMouseMovementHandlingIfNeeded() -> scheduleFallbackMouseMovementHandlingIfNeeded()
  • handleMouseMovement() -> handleFallbackMouseMovement()

for clarity.

Copy link
Contributor

Choose a reason for hiding this comment

The 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 needsMouseMovementFallback to a private method. I'll comment where its intended.

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