|
24 | 24 | import java.util.concurrent.*;
|
25 | 25 | import java.util.concurrent.atomic.*;
|
26 | 26 | import java.util.function.*;
|
| 27 | +import java.util.stream.*; |
27 | 28 |
|
28 | 29 | import org.eclipse.swt.*;
|
29 | 30 | import org.eclipse.swt.graphics.*;
|
@@ -88,8 +89,29 @@ public WebViewEnvironment(ICoreWebView2Environment environment) {
|
88 | 89 | private boolean ignoreFocusIn;
|
89 | 90 | private String lastCustomText;
|
90 | 91 |
|
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; |
93 | 115 |
|
94 | 116 | static {
|
95 | 117 | NativeClearSessions = () -> {
|
@@ -759,6 +781,9 @@ void setupBrowser(int hr, long pv) {
|
759 | 781 | handler = newCallback(this::handleSourceChanged);
|
760 | 782 | webView.add_SourceChanged(handler, token);
|
761 | 783 | handler.Release();
|
| 784 | + handler = newCallback(this::handleWebMessageReceived); |
| 785 | + webView.add_WebMessageReceived(handler, token); |
| 786 | + handler.Release(); |
762 | 787 | handler = newCallback(this::handleMoveFocusRequested);
|
763 | 788 | controller.add_MoveFocusRequested(handler, token);
|
764 | 789 | handler.Release();
|
@@ -798,7 +823,6 @@ void setupBrowser(int hr, long pv) {
|
798 | 823 | browser.addListener(SWT.FocusIn, this::browserFocusIn);
|
799 | 824 | browser.addListener(SWT.Resize, this::browserResize);
|
800 | 825 | browser.addListener(SWT.Move, this::browserMove);
|
801 |
| - scheduleMouseMovementHandling(); |
802 | 826 |
|
803 | 827 | // Sometimes when the shell of the browser is opened before the browser is
|
804 | 828 | // initialized, nothing is drawn on the shell. We need browserResize to force
|
@@ -865,52 +889,6 @@ void browserResize(Event event) {
|
865 | 889 | controller.put_IsVisible(true);
|
866 | 890 | }
|
867 | 891 |
|
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 |
| - |
914 | 892 | @Override
|
915 | 893 | public Object evaluate(String script) throws SWTException {
|
916 | 894 | // Feature in WebView2. ExecuteScript works regardless of IsScriptEnabled setting.
|
@@ -953,6 +931,14 @@ public boolean execute(String script) {
|
953 | 931 | // Feature in WebView2. ExecuteScript works regardless of IsScriptEnabled setting.
|
954 | 932 | // Disallow programmatic execution manually.
|
955 | 933 | 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) { |
956 | 942 | IUnknown completion = newCallback((long result, long json) -> COM.S_OK);
|
957 | 943 | int hr = webViewProvider.getWebView(true).ExecuteScript(stringToWstr(script), completion);
|
958 | 944 | completion.Release();
|
@@ -1173,6 +1159,29 @@ int handleDOMContentLoaded(long pView, long pArgs) {
|
1173 | 1159 | sendProgressCompleted();
|
1174 | 1160 | }
|
1175 | 1161 | }
|
| 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)); |
1176 | 1185 | return COM.S_OK;
|
1177 | 1186 | }
|
1178 | 1187 |
|
@@ -1496,6 +1505,136 @@ int handleMoveFocusRequested(long pView, long pArgs) {
|
1496 | 1505 | return COM.S_OK;
|
1497 | 1506 | }
|
1498 | 1507 |
|
| 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 | + |
1499 | 1638 | @Override
|
1500 | 1639 | public boolean isBackEnabled() {
|
1501 | 1640 | int[] pval = new int[1];
|
|
0 commit comments