|
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.*;
|
@@ -91,6 +92,30 @@ public WebViewEnvironment(ICoreWebView2Environment environment) {
|
91 | 92 | private static record CursorPosition(Point location, boolean isInsideBrowser) {};
|
92 | 93 | private CursorPosition previousCursorPosition = new CursorPosition(new Point(0, 0), false);
|
93 | 94 |
|
| 95 | + private static final String WEBMESSAGE_KIND_MOUSE_RELATED_DOM_EVENT = "mouse-related-dom-event"; |
| 96 | + private static final String EVENT_MOUSEDOWN = "mousedown"; //$NON-NLS-1$ |
| 97 | + private static final String EVENT_MOUSEUP = "mouseup"; //$NON-NLS-1$ |
| 98 | + private static final String EVENT_MOUSEMOVE = "mousemove"; //$NON-NLS-1$ |
| 99 | + private static final String EVENT_MOUSEOVER = "mouseover"; //$NON-NLS-1$ |
| 100 | + private static final String EVENT_MOUSEOUT = "mouseout"; //$NON-NLS-1$ |
| 101 | + private static final String EVENT_DRAGSTART = "dragstart"; //$NON-NLS-1$ |
| 102 | + private static final String EVENT_DRAGEND = "dragend"; //$NON-NLS-1$ |
| 103 | + private static final String EVENT_DOUBLECLICK = "dblclick"; //$NON-NLS-1$ |
| 104 | + private static final String EVENT_MOUSEWHEEL = "wheel"; //$NON-NLS-1$ |
| 105 | + private static final Set<String> MOUSE_RELATED_DOM_EVENTS = Set.of( // |
| 106 | + EVENT_MOUSEDOWN, EVENT_MOUSEUP, // |
| 107 | + EVENT_MOUSEMOVE, // |
| 108 | + EVENT_MOUSEOVER, EVENT_MOUSEOUT, // |
| 109 | + EVENT_DRAGSTART, EVENT_DRAGEND, // |
| 110 | + EVENT_DOUBLECLICK, // |
| 111 | + EVENT_MOUSEWHEEL// |
| 112 | + ); |
| 113 | + private static record MouseRelatedDomEvent(String eventType, boolean altKey, boolean ctrlKey, boolean shiftKey, |
| 114 | + long clientX, long clientY, long button, boolean fromElementSet, boolean toElementSet, long deltaY) { |
| 115 | + } |
| 116 | + private int lastMouseMoveX; |
| 117 | + private int lastMouseMoveY; |
| 118 | + |
94 | 119 | static {
|
95 | 120 | NativeClearSessions = () -> {
|
96 | 121 | ICoreWebView2CookieManager manager = getCookieManager();
|
@@ -775,6 +800,9 @@ void setupBrowser(int hr, long pv) {
|
775 | 800 | handler = newCallback(this::handleSourceChanged);
|
776 | 801 | webView.add_SourceChanged(handler, token);
|
777 | 802 | handler.Release();
|
| 803 | + handler = newCallback(this::handleWebMessageReceived); |
| 804 | + webView.add_WebMessageReceived(handler, token); |
| 805 | + handler.Release(); |
778 | 806 | handler = newCallback(this::handleMoveFocusRequested);
|
779 | 807 | controller.add_MoveFocusRequested(handler, token);
|
780 | 808 | handler.Release();
|
@@ -814,7 +842,7 @@ void setupBrowser(int hr, long pv) {
|
814 | 842 | browser.addListener(SWT.FocusIn, this::browserFocusIn);
|
815 | 843 | browser.addListener(SWT.Resize, this::browserResize);
|
816 | 844 | browser.addListener(SWT.Move, this::browserMove);
|
817 |
| - scheduleMouseMovementHandling(); |
| 845 | + scheduleMouseMovementHandlingIfNeeded(); |
818 | 846 |
|
819 | 847 | // Sometimes when the shell of the browser is opened before the browser is
|
820 | 848 | // initialized, nothing is drawn on the shell. We need browserResize to force
|
@@ -881,15 +909,21 @@ void browserResize(Event event) {
|
881 | 909 | controller.put_IsVisible(true);
|
882 | 910 | }
|
883 | 911 |
|
884 |
| -private void scheduleMouseMovementHandling() { |
| 912 | +private void scheduleMouseMovementHandlingIfNeeded() { |
| 913 | + if (jsEnabled) { |
| 914 | + return; |
| 915 | + } |
885 | 916 | browser.getDisplay().timerExec(100, () -> {
|
886 | 917 | if (browser.isDisposed()) {
|
887 | 918 | return;
|
888 | 919 | }
|
889 | 920 | if (browser.isVisible() && hasDisplayFocus()) {
|
890 | 921 | handleMouseMovement();
|
891 | 922 | }
|
892 |
| - scheduleMouseMovementHandling(); |
| 923 | + if (jsEnabled) { |
| 924 | + return; |
| 925 | + } |
| 926 | + scheduleMouseMovementHandlingIfNeeded(); |
893 | 927 | });
|
894 | 928 | }
|
895 | 929 |
|
@@ -969,6 +1003,14 @@ public boolean execute(String script) {
|
969 | 1003 | // Feature in WebView2. ExecuteScript works regardless of IsScriptEnabled setting.
|
970 | 1004 | // Disallow programmatic execution manually.
|
971 | 1005 | if (!jsEnabled) return false;
|
| 1006 | + return executeInternal(script); |
| 1007 | +} |
| 1008 | + |
| 1009 | +/** |
| 1010 | + * Unconditional script execution, bypassing {@link WebBrowser#jsEnabled} flag / |
| 1011 | + * {@link Browser#setJavascriptEnabled(boolean)}. |
| 1012 | + */ |
| 1013 | +private boolean executeInternal(String script) { |
972 | 1014 | IUnknown completion = newCallback((long result, long json) -> COM.S_OK);
|
973 | 1015 | int hr = webViewProvider.getWebView(true).ExecuteScript(stringToWstr(script), completion);
|
974 | 1016 | completion.Release();
|
@@ -1098,8 +1140,7 @@ int handleNavigationStarting(long pView, long pArgs, boolean top) {
|
1098 | 1140 | // will be eventually cleared again in handleNavigationCompleted().
|
1099 | 1141 | navigations.put(pNavId[0], event);
|
1100 | 1142 | if (event.doit) {
|
1101 |
| - jsEnabled = jsEnabledOnNextPage; |
1102 |
| - settings.put_IsScriptEnabled(jsEnabled); |
| 1143 | + settings.put_IsScriptEnabled(jsEnabledOnNextPage); |
1103 | 1144 | // Register browser functions in the new document.
|
1104 | 1145 | if (!functions.isEmpty()) {
|
1105 | 1146 | StringBuilder sb = new StringBuilder();
|
@@ -1201,6 +1242,29 @@ int handleDOMContentLoaded(long pView, long pArgs) {
|
1201 | 1242 | sendProgressCompleted();
|
1202 | 1243 | }
|
1203 | 1244 | }
|
| 1245 | + executeInternal( |
| 1246 | + """ |
| 1247 | + const events = [%%events%%]; |
| 1248 | + events.forEach(eventType => { |
| 1249 | + window.addEventListener(eventType, function(event) { |
| 1250 | + window.chrome.webview.postMessage([ |
| 1251 | + '%%webmessagekind%%', |
| 1252 | + eventType, |
| 1253 | + event.altKey, |
| 1254 | + event.ctrlKey, |
| 1255 | + event.shiftKey, |
| 1256 | + event.clientX, |
| 1257 | + event.clientY, |
| 1258 | + event.button, |
| 1259 | + "fromElement" in event && event.fromElement != null, |
| 1260 | + "toElement" in event && event.toElement != null, |
| 1261 | + "deltaY" in event ? event.deltaY : 0, |
| 1262 | + ]); |
| 1263 | + }); |
| 1264 | + }); |
| 1265 | + """ // |
| 1266 | + .replace("%%events%%", MOUSE_RELATED_DOM_EVENTS.stream().map(x -> "'" + x + "'").collect(Collectors.joining(", "))) // |
| 1267 | + .replace("%%webmessagekind%%", WEBMESSAGE_KIND_MOUSE_RELATED_DOM_EVENT)); |
1204 | 1268 | return COM.S_OK;
|
1205 | 1269 | }
|
1206 | 1270 |
|
@@ -1323,6 +1387,10 @@ int handleNavigationCompleted(long pView, long pArgs, boolean top) {
|
1323 | 1387 | // ProgressListener.completed from here.
|
1324 | 1388 | sendProgressCompleted();
|
1325 | 1389 | }
|
| 1390 | + if (top) { |
| 1391 | + jsEnabled = jsEnabledOnNextPage; |
| 1392 | + scheduleMouseMovementHandlingIfNeeded(); |
| 1393 | + } |
1326 | 1394 | int[] pIsSuccess = new int[1];
|
1327 | 1395 | args.get_IsSuccess(pIsSuccess);
|
1328 | 1396 | if (pIsSuccess[0] != 0) {
|
@@ -1524,6 +1592,136 @@ int handleMoveFocusRequested(long pView, long pArgs) {
|
1524 | 1592 | return COM.S_OK;
|
1525 | 1593 | }
|
1526 | 1594 |
|
| 1595 | +int handleWebMessageReceived(long pView, long pArgs) { |
| 1596 | + ICoreWebView2WebMessageReceivedEventArgs args = new ICoreWebView2WebMessageReceivedEventArgs(pArgs); |
| 1597 | + long[] ppszWebMessageJson = new long[1]; |
| 1598 | + int hr = args.get_WebMessageAsJson(ppszWebMessageJson); |
| 1599 | + if (hr != COM.S_OK) return hr; |
| 1600 | + try { |
| 1601 | + String webMessageJson = wstrToString(ppszWebMessageJson[0], true); |
| 1602 | + Object[] data = (Object[]) JSON.parse(webMessageJson); |
| 1603 | + if (WEBMESSAGE_KIND_MOUSE_RELATED_DOM_EVENT.equals(data[0])) { |
| 1604 | + MouseRelatedDomEvent mouseRelatedDomEvent = new MouseRelatedDomEvent( // |
| 1605 | + (String) data[1], // |
| 1606 | + (boolean) data[2], // |
| 1607 | + (boolean) data[3], // |
| 1608 | + (boolean) data[4], // |
| 1609 | + Math.round((double) data[5]), // |
| 1610 | + Math.round((double) data[6]), // |
| 1611 | + Math.round((double) data[7]), // |
| 1612 | + (boolean) data[8], // |
| 1613 | + (boolean) data[9], // |
| 1614 | + Math.round((double) data[10]) // |
| 1615 | + ); |
| 1616 | + handleMouseRelatedDomEvent(mouseRelatedDomEvent); |
| 1617 | + } |
| 1618 | + } catch (Exception e) { |
| 1619 | + System.err.println(e); |
| 1620 | + } |
| 1621 | + return COM.S_OK; |
| 1622 | +} |
| 1623 | + |
| 1624 | +/** |
| 1625 | + * Insipired by the mouse-event related parts of |
| 1626 | + * {@link IE#handleDOMEvent(org.eclipse.swt.ole.win32.OleEvent)} |
| 1627 | + */ |
| 1628 | +private void handleMouseRelatedDomEvent(MouseRelatedDomEvent domEvent) { |
| 1629 | + String eventType = domEvent.eventType(); |
| 1630 | + |
| 1631 | + /* |
| 1632 | + * Feature in Edge. MouseOver/MouseOut events are fired any time the mouse enters |
| 1633 | + * or exits any element within the Browser. To ensure that SWT events are only |
| 1634 | + * fired for mouse movements into or out of the Browser, do not fire an event if |
| 1635 | + * the element being exited (on MouseOver) or entered (on MouseExit) is within |
| 1636 | + * the Browser. |
| 1637 | + */ |
| 1638 | + if (eventType.equals(EVENT_MOUSEOVER)) { |
| 1639 | + if (domEvent.fromElementSet()) { |
| 1640 | + return; |
| 1641 | + } |
| 1642 | + } |
| 1643 | + if (eventType.equals(EVENT_MOUSEOUT)) { |
| 1644 | + if (domEvent.toElementSet()) { |
| 1645 | + return; |
| 1646 | + } |
| 1647 | + } |
| 1648 | + |
| 1649 | + int mask = 0; |
| 1650 | + Event newEvent = new Event(); |
| 1651 | + newEvent.widget = browser; |
| 1652 | + newEvent.x = (int) domEvent.clientX(); newEvent.y = (int) domEvent.clientY(); |
| 1653 | + if (domEvent.ctrlKey()) mask |= SWT.CTRL; |
| 1654 | + if (domEvent.altKey()) mask |= SWT.ALT; |
| 1655 | + if (domEvent.shiftKey()) mask |= SWT.SHIFT; |
| 1656 | + newEvent.stateMask = mask; |
| 1657 | + |
| 1658 | + int button = (int) domEvent.button(); |
| 1659 | + switch (button) { |
| 1660 | + case 1: button = 1; break; |
| 1661 | + case 2: button = 3; break; |
| 1662 | + case 4: button = 2; break; |
| 1663 | + } |
| 1664 | + |
| 1665 | + if (eventType.equals(EVENT_MOUSEDOWN)) { |
| 1666 | + newEvent.type = SWT.MouseDown; |
| 1667 | + newEvent.button = button; |
| 1668 | + newEvent.count = 1; |
| 1669 | + } else if (eventType.equals(EVENT_MOUSEUP) || eventType.equals(EVENT_DRAGEND)) { |
| 1670 | + newEvent.type = SWT.MouseUp; |
| 1671 | + newEvent.button = button != 0 ? button : 1; /* button assumed to be 1 for dragends */ |
| 1672 | + newEvent.count = 1; |
| 1673 | + switch (newEvent.button) { |
| 1674 | + case 1: newEvent.stateMask |= SWT.BUTTON1; break; |
| 1675 | + case 2: newEvent.stateMask |= SWT.BUTTON2; break; |
| 1676 | + case 3: newEvent.stateMask |= SWT.BUTTON3; break; |
| 1677 | + case 4: newEvent.stateMask |= SWT.BUTTON4; break; |
| 1678 | + case 5: newEvent.stateMask |= SWT.BUTTON5; break; |
| 1679 | + } |
| 1680 | + } else if (eventType.equals(EVENT_MOUSEWHEEL)) { |
| 1681 | + newEvent.type = SWT.MouseWheel; |
| 1682 | + // Chromium/Edge uses deltaMode DOM_DELTA_PIXEL which |
| 1683 | + // - has a different sign than the legacy MouseWheelEvent wheelDelta |
| 1684 | + // - depends on the zoom of the browser |
| 1685 | + // https://github.com/w3c/uievents/issues/181 |
| 1686 | + // The literal value of deltaY is therefore useless. |
| 1687 | + // Instead, we simply use the sign for the direction and combine |
| 1688 | + // it with the internal hard-coded value of '3 lines'. |
| 1689 | + newEvent.count = domEvent.deltaY() > 0 ? -3 : 3; |
| 1690 | + } else if (eventType.equals(EVENT_MOUSEMOVE)) { |
| 1691 | + /* |
| 1692 | + * Feature in Edge. Spurious and redundant mousemove events are often received. The workaround |
| 1693 | + * is to not fire MouseMove events whose x and y values match the last MouseMove. |
| 1694 | + */ |
| 1695 | + if (newEvent.x == lastMouseMoveX && newEvent.y == lastMouseMoveY) { |
| 1696 | + return; |
| 1697 | + } |
| 1698 | + newEvent.type = SWT.MouseMove; |
| 1699 | + lastMouseMoveX = newEvent.x; lastMouseMoveY = newEvent.y; |
| 1700 | + } else if (eventType.equals(EVENT_MOUSEOVER)) { |
| 1701 | + newEvent.type = SWT.MouseEnter; |
| 1702 | + } else if (eventType.equals(EVENT_MOUSEOUT)) { |
| 1703 | + newEvent.type = SWT.MouseExit; |
| 1704 | + } else if (eventType.equals(EVENT_DRAGSTART)) { |
| 1705 | + newEvent.type = SWT.DragDetect; |
| 1706 | + newEvent.button = 1; /* button assumed to be 1 for dragstarts */ |
| 1707 | + newEvent.stateMask |= SWT.BUTTON1; |
| 1708 | + } |
| 1709 | + |
| 1710 | + browser.notifyListeners(newEvent.type, newEvent); |
| 1711 | + |
| 1712 | + if (eventType.equals(EVENT_DOUBLECLICK)) { |
| 1713 | + newEvent = new Event (); |
| 1714 | + newEvent.widget = browser; |
| 1715 | + newEvent.type = SWT.MouseDoubleClick; |
| 1716 | + newEvent.x = (int) domEvent.clientX(); newEvent.y = (int) domEvent.clientY(); |
| 1717 | + newEvent.stateMask = mask; |
| 1718 | + newEvent.type = SWT.MouseDoubleClick; |
| 1719 | + newEvent.button = 1; /* dblclick only comes for button 1 and does not set the button property */ |
| 1720 | + newEvent.count = 2; |
| 1721 | + browser.notifyListeners (newEvent.type, newEvent); |
| 1722 | + } |
| 1723 | +} |
| 1724 | + |
1527 | 1725 | @Override
|
1528 | 1726 | public boolean isBackEnabled() {
|
1529 | 1727 | int[] pval = new int[1];
|
|
0 commit comments