|
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,9 +89,40 @@ public WebViewEnvironment(ICoreWebView2Environment environment) { |
88 | 89 | private boolean ignoreFocusIn; |
89 | 90 | private String lastCustomText; |
90 | 91 |
|
| 92 | + /** |
| 93 | + * We need the fallback whenever javascript is *not* enabled, but also for fresh |
| 94 | + * Edge instances until the first javascript-enabled page has successfully |
| 95 | + * loaded. Hence, this is initialized with <code>true</true> but will then be |
| 96 | + * updated following changes to {@link #jsEnabled}. |
| 97 | + */ |
| 98 | + private boolean needsMouseMovementFallback = true; |
91 | 99 | private static record CursorPosition(Point location, boolean isInsideBrowser) {}; |
92 | 100 | private CursorPosition previousCursorPosition = new CursorPosition(new Point(0, 0), false); |
93 | 101 |
|
| 102 | + private static final String WEBMESSAGE_KIND_MOUSE_RELATED_DOM_EVENT = "mouse-related-dom-event"; |
| 103 | + private static final String EVENT_MOUSEDOWN = "mousedown"; //$NON-NLS-1$ |
| 104 | + private static final String EVENT_MOUSEUP = "mouseup"; //$NON-NLS-1$ |
| 105 | + private static final String EVENT_MOUSEMOVE = "mousemove"; //$NON-NLS-1$ |
| 106 | + private static final String EVENT_MOUSEOVER = "mouseover"; //$NON-NLS-1$ |
| 107 | + private static final String EVENT_MOUSEOUT = "mouseout"; //$NON-NLS-1$ |
| 108 | + private static final String EVENT_DRAGSTART = "dragstart"; //$NON-NLS-1$ |
| 109 | + private static final String EVENT_DRAGEND = "dragend"; //$NON-NLS-1$ |
| 110 | + private static final String EVENT_DOUBLECLICK = "dblclick"; //$NON-NLS-1$ |
| 111 | + private static final String EVENT_MOUSEWHEEL = "wheel"; //$NON-NLS-1$ |
| 112 | + private static final Set<String> MOUSE_RELATED_DOM_EVENTS = Set.of( // |
| 113 | + EVENT_MOUSEDOWN, EVENT_MOUSEUP, // |
| 114 | + EVENT_MOUSEMOVE, // |
| 115 | + EVENT_MOUSEOVER, EVENT_MOUSEOUT, // |
| 116 | + EVENT_DRAGSTART, EVENT_DRAGEND, // |
| 117 | + EVENT_DOUBLECLICK, // |
| 118 | + EVENT_MOUSEWHEEL// |
| 119 | + ); |
| 120 | + private static record MouseRelatedDomEvent(String eventType, boolean altKey, boolean ctrlKey, boolean shiftKey, |
| 121 | + long clientX, long clientY, long button, boolean fromElementSet, boolean toElementSet, long deltaY) { |
| 122 | + } |
| 123 | + private int lastMouseMoveX; |
| 124 | + private int lastMouseMoveY; |
| 125 | + |
94 | 126 | static { |
95 | 127 | NativeClearSessions = () -> { |
96 | 128 | ICoreWebView2CookieManager manager = getCookieManager(); |
@@ -775,6 +807,9 @@ void setupBrowser(int hr, long pv) { |
775 | 807 | handler = newCallback(this::handleSourceChanged); |
776 | 808 | webView.add_SourceChanged(handler, token); |
777 | 809 | handler.Release(); |
| 810 | + handler = newCallback(this::handleWebMessageReceived); |
| 811 | + webView.add_WebMessageReceived(handler, token); |
| 812 | + handler.Release(); |
778 | 813 | handler = newCallback(this::handleMoveFocusRequested); |
779 | 814 | controller.add_MoveFocusRequested(handler, token); |
780 | 815 | handler.Release(); |
@@ -814,7 +849,7 @@ void setupBrowser(int hr, long pv) { |
814 | 849 | browser.addListener(SWT.FocusIn, this::browserFocusIn); |
815 | 850 | browser.addListener(SWT.Resize, this::browserResize); |
816 | 851 | browser.addListener(SWT.Move, this::browserMove); |
817 | | - scheduleMouseMovementHandling(); |
| 852 | + scheduleFallbackMouseMovementHandlingIfNeeded(); |
818 | 853 |
|
819 | 854 | // Sometimes when the shell of the browser is opened before the browser is |
820 | 855 | // initialized, nothing is drawn on the shell. We need browserResize to force |
@@ -881,19 +916,25 @@ void browserResize(Event event) { |
881 | 916 | controller.put_IsVisible(true); |
882 | 917 | } |
883 | 918 |
|
884 | | -private void scheduleMouseMovementHandling() { |
| 919 | +private void scheduleFallbackMouseMovementHandlingIfNeeded() { |
| 920 | + if (!needsMouseMovementFallback) { |
| 921 | + return; |
| 922 | + } |
885 | 923 | browser.getDisplay().timerExec(100, () -> { |
886 | 924 | if (browser.isDisposed()) { |
887 | 925 | return; |
888 | 926 | } |
889 | 927 | if (browser.isVisible() && hasDisplayFocus()) { |
890 | | - handleMouseMovement(); |
| 928 | + handleFallbackMouseMovement(); |
891 | 929 | } |
892 | | - scheduleMouseMovementHandling(); |
| 930 | + if (!needsMouseMovementFallback) { |
| 931 | + return; |
| 932 | + } |
| 933 | + scheduleFallbackMouseMovementHandlingIfNeeded(); |
893 | 934 | }); |
894 | 935 | } |
895 | 936 |
|
896 | | -private void handleMouseMovement() { |
| 937 | +private void handleFallbackMouseMovement() { |
897 | 938 | final Point currentCursorLocation = browser.getDisplay().getCursorLocation(); |
898 | 939 | Point cursorLocationInControlCoordinate = browser.toControl(currentCursorLocation); |
899 | 940 | boolean isCursorInsideBrowser = browser.getBounds().contains(cursorLocationInControlCoordinate); |
@@ -969,6 +1010,14 @@ public boolean execute(String script) { |
969 | 1010 | // Feature in WebView2. ExecuteScript works regardless of IsScriptEnabled setting. |
970 | 1011 | // Disallow programmatic execution manually. |
971 | 1012 | if (!jsEnabled) return false; |
| 1013 | + return executeInternal(script); |
| 1014 | +} |
| 1015 | + |
| 1016 | +/** |
| 1017 | + * Unconditional script execution, bypassing {@link WebBrowser#jsEnabled} flag / |
| 1018 | + * {@link Browser#setJavascriptEnabled(boolean)}. |
| 1019 | + */ |
| 1020 | +private boolean executeInternal(String script) { |
972 | 1021 | IUnknown completion = newCallback((long result, long json) -> COM.S_OK); |
973 | 1022 | int hr = webViewProvider.getWebView(true).ExecuteScript(stringToWstr(script), completion); |
974 | 1023 | completion.Release(); |
@@ -1098,8 +1147,7 @@ int handleNavigationStarting(long pView, long pArgs, boolean top) { |
1098 | 1147 | // will be eventually cleared again in handleNavigationCompleted(). |
1099 | 1148 | navigations.put(pNavId[0], event); |
1100 | 1149 | if (event.doit) { |
1101 | | - jsEnabled = jsEnabledOnNextPage; |
1102 | | - settings.put_IsScriptEnabled(jsEnabled); |
| 1150 | + settings.put_IsScriptEnabled(jsEnabledOnNextPage); |
1103 | 1151 | // Register browser functions in the new document. |
1104 | 1152 | if (!functions.isEmpty()) { |
1105 | 1153 | StringBuilder sb = new StringBuilder(); |
@@ -1201,6 +1249,29 @@ int handleDOMContentLoaded(long pView, long pArgs) { |
1201 | 1249 | sendProgressCompleted(); |
1202 | 1250 | } |
1203 | 1251 | } |
| 1252 | + executeInternal( |
| 1253 | + """ |
| 1254 | + const events = [%%events%%]; |
| 1255 | + events.forEach(eventType => { |
| 1256 | + window.addEventListener(eventType, function(event) { |
| 1257 | + window.chrome.webview.postMessage([ |
| 1258 | + '%%webmessagekind%%', |
| 1259 | + eventType, |
| 1260 | + event.altKey, |
| 1261 | + event.ctrlKey, |
| 1262 | + event.shiftKey, |
| 1263 | + event.clientX, |
| 1264 | + event.clientY, |
| 1265 | + event.button, |
| 1266 | + "fromElement" in event && event.fromElement != null, |
| 1267 | + "toElement" in event && event.toElement != null, |
| 1268 | + "deltaY" in event ? event.deltaY : 0, |
| 1269 | + ]); |
| 1270 | + }); |
| 1271 | + }); |
| 1272 | + """ // |
| 1273 | + .replace("%%events%%", MOUSE_RELATED_DOM_EVENTS.stream().map(x -> "'" + x + "'").collect(Collectors.joining(", "))) // |
| 1274 | + .replace("%%webmessagekind%%", WEBMESSAGE_KIND_MOUSE_RELATED_DOM_EVENT)); |
1204 | 1275 | return COM.S_OK; |
1205 | 1276 | } |
1206 | 1277 |
|
@@ -1323,6 +1394,11 @@ int handleNavigationCompleted(long pView, long pArgs, boolean top) { |
1323 | 1394 | // ProgressListener.completed from here. |
1324 | 1395 | sendProgressCompleted(); |
1325 | 1396 | } |
| 1397 | + if (top) { |
| 1398 | + jsEnabled = jsEnabledOnNextPage; |
| 1399 | + needsMouseMovementFallback = !jsEnabled; |
| 1400 | + scheduleFallbackMouseMovementHandlingIfNeeded(); |
| 1401 | + } |
1326 | 1402 | int[] pIsSuccess = new int[1]; |
1327 | 1403 | args.get_IsSuccess(pIsSuccess); |
1328 | 1404 | if (pIsSuccess[0] != 0) { |
@@ -1529,6 +1605,136 @@ int handleMoveFocusRequested(long pView, long pArgs) { |
1529 | 1605 | return COM.S_OK; |
1530 | 1606 | } |
1531 | 1607 |
|
| 1608 | +int handleWebMessageReceived(long pView, long pArgs) { |
| 1609 | + ICoreWebView2WebMessageReceivedEventArgs args = new ICoreWebView2WebMessageReceivedEventArgs(pArgs); |
| 1610 | + long[] ppszWebMessageJson = new long[1]; |
| 1611 | + int hr = args.get_WebMessageAsJson(ppszWebMessageJson); |
| 1612 | + if (hr != COM.S_OK) return hr; |
| 1613 | + try { |
| 1614 | + String webMessageJson = wstrToString(ppszWebMessageJson[0], true); |
| 1615 | + Object[] data = (Object[]) JSON.parse(webMessageJson); |
| 1616 | + if (WEBMESSAGE_KIND_MOUSE_RELATED_DOM_EVENT.equals(data[0])) { |
| 1617 | + MouseRelatedDomEvent mouseRelatedDomEvent = new MouseRelatedDomEvent( // |
| 1618 | + (String) data[1], // |
| 1619 | + (boolean) data[2], // |
| 1620 | + (boolean) data[3], // |
| 1621 | + (boolean) data[4], // |
| 1622 | + Math.round((double) data[5]), // |
| 1623 | + Math.round((double) data[6]), // |
| 1624 | + Math.round((double) data[7]), // |
| 1625 | + (boolean) data[8], // |
| 1626 | + (boolean) data[9], // |
| 1627 | + Math.round((double) data[10]) // |
| 1628 | + ); |
| 1629 | + handleMouseRelatedDomEvent(mouseRelatedDomEvent); |
| 1630 | + } |
| 1631 | + } catch (Exception e) { |
| 1632 | + System.err.println(e); |
| 1633 | + } |
| 1634 | + return COM.S_OK; |
| 1635 | +} |
| 1636 | + |
| 1637 | +/** |
| 1638 | + * Insipired by the mouse-event related parts of |
| 1639 | + * {@link IE#handleDOMEvent(org.eclipse.swt.ole.win32.OleEvent)} |
| 1640 | + */ |
| 1641 | +private void handleMouseRelatedDomEvent(MouseRelatedDomEvent domEvent) { |
| 1642 | + String eventType = domEvent.eventType(); |
| 1643 | + |
| 1644 | + /* |
| 1645 | + * Feature in Edge. MouseOver/MouseOut events are fired any time the mouse enters |
| 1646 | + * or exits any element within the Browser. To ensure that SWT events are only |
| 1647 | + * fired for mouse movements into or out of the Browser, do not fire an event if |
| 1648 | + * the element being exited (on MouseOver) or entered (on MouseExit) is within |
| 1649 | + * the Browser. |
| 1650 | + */ |
| 1651 | + if (eventType.equals(EVENT_MOUSEOVER)) { |
| 1652 | + if (domEvent.fromElementSet()) { |
| 1653 | + return; |
| 1654 | + } |
| 1655 | + } |
| 1656 | + if (eventType.equals(EVENT_MOUSEOUT)) { |
| 1657 | + if (domEvent.toElementSet()) { |
| 1658 | + return; |
| 1659 | + } |
| 1660 | + } |
| 1661 | + |
| 1662 | + int mask = 0; |
| 1663 | + Event newEvent = new Event(); |
| 1664 | + newEvent.widget = browser; |
| 1665 | + newEvent.x = (int) domEvent.clientX(); newEvent.y = (int) domEvent.clientY(); |
| 1666 | + if (domEvent.ctrlKey()) mask |= SWT.CTRL; |
| 1667 | + if (domEvent.altKey()) mask |= SWT.ALT; |
| 1668 | + if (domEvent.shiftKey()) mask |= SWT.SHIFT; |
| 1669 | + newEvent.stateMask = mask; |
| 1670 | + |
| 1671 | + int button = (int) domEvent.button(); |
| 1672 | + switch (button) { |
| 1673 | + case 1: button = 1; break; |
| 1674 | + case 2: button = 3; break; |
| 1675 | + case 4: button = 2; break; |
| 1676 | + } |
| 1677 | + |
| 1678 | + if (eventType.equals(EVENT_MOUSEDOWN)) { |
| 1679 | + newEvent.type = SWT.MouseDown; |
| 1680 | + newEvent.button = button; |
| 1681 | + newEvent.count = 1; |
| 1682 | + } else if (eventType.equals(EVENT_MOUSEUP) || eventType.equals(EVENT_DRAGEND)) { |
| 1683 | + newEvent.type = SWT.MouseUp; |
| 1684 | + newEvent.button = button != 0 ? button : 1; /* button assumed to be 1 for dragends */ |
| 1685 | + newEvent.count = 1; |
| 1686 | + switch (newEvent.button) { |
| 1687 | + case 1: newEvent.stateMask |= SWT.BUTTON1; break; |
| 1688 | + case 2: newEvent.stateMask |= SWT.BUTTON2; break; |
| 1689 | + case 3: newEvent.stateMask |= SWT.BUTTON3; break; |
| 1690 | + case 4: newEvent.stateMask |= SWT.BUTTON4; break; |
| 1691 | + case 5: newEvent.stateMask |= SWT.BUTTON5; break; |
| 1692 | + } |
| 1693 | + } else if (eventType.equals(EVENT_MOUSEWHEEL)) { |
| 1694 | + newEvent.type = SWT.MouseWheel; |
| 1695 | + // Chromium/Edge uses deltaMode DOM_DELTA_PIXEL which |
| 1696 | + // - has a different sign than the legacy MouseWheelEvent wheelDelta |
| 1697 | + // - depends on the zoom of the browser |
| 1698 | + // https://github.com/w3c/uievents/issues/181 |
| 1699 | + // The literal value of deltaY is therefore useless. |
| 1700 | + // Instead, we simply use the sign for the direction and combine |
| 1701 | + // it with the internal hard-coded value of '3 lines'. |
| 1702 | + newEvent.count = domEvent.deltaY() > 0 ? -3 : 3; |
| 1703 | + } else if (eventType.equals(EVENT_MOUSEMOVE)) { |
| 1704 | + /* |
| 1705 | + * Feature in Edge. Spurious and redundant mousemove events are often received. The workaround |
| 1706 | + * is to not fire MouseMove events whose x and y values match the last MouseMove. |
| 1707 | + */ |
| 1708 | + if (newEvent.x == lastMouseMoveX && newEvent.y == lastMouseMoveY) { |
| 1709 | + return; |
| 1710 | + } |
| 1711 | + newEvent.type = SWT.MouseMove; |
| 1712 | + lastMouseMoveX = newEvent.x; lastMouseMoveY = newEvent.y; |
| 1713 | + } else if (eventType.equals(EVENT_MOUSEOVER)) { |
| 1714 | + newEvent.type = SWT.MouseEnter; |
| 1715 | + } else if (eventType.equals(EVENT_MOUSEOUT)) { |
| 1716 | + newEvent.type = SWT.MouseExit; |
| 1717 | + } else if (eventType.equals(EVENT_DRAGSTART)) { |
| 1718 | + newEvent.type = SWT.DragDetect; |
| 1719 | + newEvent.button = 1; /* button assumed to be 1 for dragstarts */ |
| 1720 | + newEvent.stateMask |= SWT.BUTTON1; |
| 1721 | + } |
| 1722 | + |
| 1723 | + browser.notifyListeners(newEvent.type, newEvent); |
| 1724 | + |
| 1725 | + if (eventType.equals(EVENT_DOUBLECLICK)) { |
| 1726 | + newEvent = new Event (); |
| 1727 | + newEvent.widget = browser; |
| 1728 | + newEvent.type = SWT.MouseDoubleClick; |
| 1729 | + newEvent.x = (int) domEvent.clientX(); newEvent.y = (int) domEvent.clientY(); |
| 1730 | + newEvent.stateMask = mask; |
| 1731 | + newEvent.type = SWT.MouseDoubleClick; |
| 1732 | + newEvent.button = 1; /* dblclick only comes for button 1 and does not set the button property */ |
| 1733 | + newEvent.count = 2; |
| 1734 | + browser.notifyListeners (newEvent.type, newEvent); |
| 1735 | + } |
| 1736 | +} |
| 1737 | + |
1532 | 1738 | @Override |
1533 | 1739 | public boolean isBackEnabled() { |
1534 | 1740 | int[] pval = new int[1]; |
|
0 commit comments