blob: 9a4f8ecdceb7eefc8994633d92f65388c345b9a5 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2020 Nikita Nemkin and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Nikita Nemkin <nikita@nemkin.ru> - initial implementation
*******************************************************************************/
package org.eclipse.swt.browser;
import java.nio.charset.*;
import java.util.*;
import org.eclipse.swt.*;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.internal.*;
import org.eclipse.swt.internal.ole.win32.*;
import org.eclipse.swt.internal.win32.*;
import org.eclipse.swt.widgets.*;
class Edge extends WebBrowser {
static {
Library.loadLibrary("WebView2Loader", false);
}
// WebView2Loader.dll compatible version. This is NOT the minimal required version.
static final String SDK_TARGET_VERSION = "89.0.721.0";
// Display.getData() keys
static final String APPLOCAL_DIR_KEY = "org.eclipse.swt.internal.win32.appLocalDir";
// System.getProperty() keys
static final String BROWSER_DIR_PROP = "org.eclipse.swt.browser.EdgeDir";
static final String BROWSER_ARGS_PROP = "org.eclipse.swt.browser.EdgeArgs";
static final String DATA_DIR_PROP = "org.eclipse.swt.browser.EdgeDataDir";
static final String LANGUAGE_PROP = "org.eclipse.swt.browser.EdgeLanguage";
static final String VERSIONT_PROP = "org.eclipse.swt.browser.EdgeVersion";
static String DataDir;
static ICoreWebView2Environment Environment;
ICoreWebView2 webView;
ICoreWebView2_2 webView2;
ICoreWebView2Controller controller;
ICoreWebView2Settings settings;
ICoreWebView2Environment2 environment2;
static boolean inCallback;
boolean inNewWindow;
HashMap<Long, LocationEvent> navigations = new HashMap<>();
static String wstrToString(long psz, boolean free) {
if (psz == 0) return "";
int len = OS.wcslen(psz);
char[] data = new char[len];
OS.MoveMemory(data, psz, len * Character.BYTES);
if (free) OS.CoTaskMemFree(psz);
return String.valueOf(data);
}
static String bstrToString(long bstr) {
if (bstr == 0) return "";
int len = COM.SysStringLen(bstr);
char[] data = new char[len];
OS.MoveMemory(data, bstr, len * Character.BYTES);
return String.valueOf(data);
}
static void error(int code, int hr) {
SWT.error(code, null, String.format(" [0x%08x]", hr));
}
static IUnknown newCallback(ICoreWebView2SwtCallback handler) {
long punk = COM.CreateSwtWebView2Callback((arg0, arg1) -> {
inCallback = true;
try {
return handler.Invoke(arg0, arg1);
} finally {
inCallback = false;
}
});
if (punk == 0) error(SWT.ERROR_NO_HANDLES, COM.E_OUTOFMEMORY);
return new IUnknown(punk);
}
IUnknown newHostObject(ICoreWebView2SwtHost handler) {
long pdisp = COM.CreateSwtWebView2Host(handler);
if (pdisp == 0) error(SWT.ERROR_NO_HANDLES, COM.E_OUTOFMEMORY);
return new IUnknown(pdisp);
}
void checkDeadlock() {
// Feature in WebView2. All event handlers, completion handlers
// and JavaScript callbacks are serialized. An event handler waiting
// for a completion of another handler will deadlock. Detect this
// situation and throw an exception instead.
if (inCallback || inNewWindow) {
SWT.error(SWT.ERROR_FAILED_EVALUATE, null, " [WebView2: deadlock detected]");
}
}
ICoreWebView2Environment createEnvironment() {
if (Environment != null) return Environment;
Display display = Display.getCurrent();
// Gather customization properties
String browserDir = System.getProperty(BROWSER_DIR_PROP);
String dataDir = System.getProperty(DATA_DIR_PROP);
String browserArgs = System.getProperty(BROWSER_ARGS_PROP);
String language = System.getProperty(LANGUAGE_PROP);
if (dataDir == null) {
// WebView2 will append "\\EBWebView"
dataDir = (String)display.getData(APPLOCAL_DIR_KEY);
}
// Initialize options
long pOpts = COM.CreateSwtWebView2Options();
if (pOpts == 0) error(SWT.ERROR_NO_HANDLES, COM.E_OUTOFMEMORY);
ICoreWebView2EnvironmentOptions options = new ICoreWebView2EnvironmentOptions(pOpts);
char[] pVersion = (SDK_TARGET_VERSION + "\0").toCharArray();
options.put_TargetCompatibleBrowserVersion(pVersion);
if (browserArgs != null) {
char[] pBrowserArgs = (browserArgs + "\0").toCharArray();
options.put_AdditionalBrowserArguments(pBrowserArgs);
}
if (language != null) {
char[] pLanguage = (language + "\0").toCharArray();
options.put_Language(pLanguage);
}
// Create the environment
char[] pBrowserDir = (browserDir != null) ? (browserDir + "\0").toCharArray() : null;
char[] pDataDir = (dataDir + "\0").toCharArray();
int[] pResult = {COM.S_OK};
long[] ppEnv = new long[1];
IUnknown completion = newCallback((result, pEnv) -> {
pResult[0] = (int)result;
if ((int)result == COM.S_OK) {
ppEnv[0] = pEnv;
new IUnknown(pEnv).AddRef();
}
return COM.S_OK;
});
pResult[0] = COM.CreateCoreWebView2EnvironmentWithOptions(
pBrowserDir, pDataDir, options.getAddress(), completion.getAddress());
completion.Release();
options.Release();
if (pResult[0] == OS.HRESULT_FROM_WIN32(OS.ERROR_FILE_NOT_FOUND)) {
SWT.error(SWT.ERROR_NOT_IMPLEMENTED, null, " [WebView2 runtime not found]");
}
while (pResult[0] == COM.S_OK && ppEnv[0] == 0) {
if (!display.readAndDispatch()) display.sleep();
}
if (pResult[0] != COM.S_OK) error(SWT.ERROR_NO_HANDLES, pResult[0]);
Environment = new ICoreWebView2Environment(ppEnv[0]);
DataDir = dataDir;
// Save Edge version for reporting
long[] ppVersion = new long[1];
Environment.get_BrowserVersionString(ppVersion);
String version = wstrToString(ppVersion[0], true);
System.setProperty(VERSIONT_PROP, version);
// Destroy the environment on app exit.
display.disposeExec(() -> {
Environment.Release();
Environment = null;
});
return Environment;
}
@Override
public void create(Composite parent, int style) {
checkDeadlock();
ICoreWebView2Environment environment = createEnvironment();
int[] pResult = {COM.S_OK};
long[] ppv = new long[1], ppHost = new long[1];
int hr = environment.QueryInterface(COM.IID_ICoreWebView2Environment2, ppv);
if (hr == COM.S_OK) environment2 = new ICoreWebView2Environment2(ppv[0]);
IUnknown completion = newCallback((result, pHost) -> {
pResult[0] = (int)result;
if ((int)result == COM.S_OK) {
ppHost[0] = pHost;
new IUnknown(pHost).AddRef();
}
return COM.S_OK;
});
pResult[0] = environment.CreateCoreWebView2Controller(browser.handle, completion);
completion.Release();
Display display = browser.getDisplay();
while (pResult[0] == COM.S_OK && ppHost[0] == 0) {
if (!display.readAndDispatch()) display.sleep();
}
if (pResult[0] != COM.S_OK) error(SWT.ERROR_NO_HANDLES, pResult[0]);
controller = new ICoreWebView2Controller(ppHost[0]);
controller.get_CoreWebView2(ppv);
webView = new ICoreWebView2(ppv[0]);
webView.get_Settings(ppv);
settings = new ICoreWebView2Settings(ppv[0]);
hr = webView.QueryInterface(COM.IID_ICoreWebView2_2, ppv);
if (hr == COM.S_OK) webView2 = new ICoreWebView2_2(ppv[0]);
long[] token = new long[1];
IUnknown handler;
handler = newCallback(this::handleCloseRequested);
webView.add_WindowCloseRequested(handler, token);
handler.Release();
handler = newCallback(this::handleNavigationStarting);
webView.add_NavigationStarting(handler, token);
handler.Release();
handler = newCallback(this::handleFrameNavigationStarting);
webView.add_FrameNavigationStarting(handler, token);
handler.Release();
handler = newCallback(this::handleNavigationCompleted);
webView.add_NavigationCompleted(handler, token);
handler.Release();
handler = newCallback(this::handleFrameNavigationCompleted);
webView.add_FrameNavigationCompleted(handler, token);
handler.Release();
handler = newCallback(this::handleDocumentTitleChanged);
webView.add_DocumentTitleChanged(handler, token);
handler.Release();
handler = newCallback(this::handleNewWindowRequested);
webView.add_NewWindowRequested(handler, token);
handler.Release();
handler = newCallback(this::handleSourceChanged);
webView.add_SourceChanged(handler, token);
handler.Release();
handler = newCallback(this::handleMoveFocusRequested);
controller.add_MoveFocusRequested(handler, token);
handler.Release();
if (webView2 != null) {
handler = newCallback(this::handleDOMContentLoaded);
webView2.add_DOMContentLoaded(handler, token);
handler.Release();
}
IUnknown hostDisp = newHostObject(this::handleCallJava);
long[] hostObj = { COM.VT_DISPATCH, hostDisp.getAddress(), 0 }; // VARIANT
webView.AddHostObjectToScript("swt\0".toCharArray(), hostObj);
hostDisp.Release();
browser.addListener(SWT.Dispose, this::browserDispose);
browser.addListener(SWT.FocusIn, this::browserFocusIn);
browser.addListener(SWT.Resize, this::browserResize);
browser.addListener(SWT.Move, this::browserMove);
}
void browserDispose(Event event) {
if (webView2 != null) webView2.Release();
if (environment2 != null) environment2.Release();
settings.Release();
webView.Release();
webView2 = null;
environment2 = null;
settings = null;
webView = null;
// Bug in WebView2. Closing the controller from an event handler results
// in a crash. The fix is to delay the closure with asyncExec.
if (inCallback) {
ICoreWebView2Controller controller1 = controller;
controller.put_IsVisible(false);
browser.getDisplay().asyncExec(() -> {
controller1.Close();
controller1.Release();
});
} else {
controller.Close();
controller.Release();
}
controller = null;
}
void browserFocusIn(Event event) {
// TODO: directional traversals
controller.MoveFocus(COM.COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC);
}
void browserMove(Event event) {
controller.NotifyParentWindowPositionChanged();
}
void browserResize(Event event) {
RECT rect = new RECT();
OS.GetClientRect(browser.handle, rect);
controller.put_Bounds(rect);
controller.put_IsVisible(true);
}
@Override
public Object evaluate(String script) throws SWTException {
checkDeadlock();
// Feature in WebView2. ExecuteScript works regardless of IsScriptEnabled setting.
// Disallow programmatic execution manually.
if (!jsEnabled) return null;
int[] pResult = new int[1];
String[] pJson = new String[1];
IUnknown completion = newCallback((result, pszJson) -> {
pResult[0] = (int)result;
if ((int)result == COM.S_OK) {
pJson[0] = wstrToString(pszJson, false);
}
return COM.S_OK;
});
script = "(function() {try { " + script + " } catch (e) { return '" + ERROR_ID + "' + e.message; } })();\0";
pResult[0] = webView.ExecuteScript(script.toCharArray(), completion);
completion.Release();
Display display = browser.getDisplay();
while (pResult[0] == COM.S_OK && pJson[0] == null) {
if (!display.readAndDispatch()) display.sleep();
}
if (pResult[0] != COM.S_OK) error(SWT.ERROR_FAILED_EVALUATE, pResult[0]);
Object data = JSON.parse(pJson[0]);
if (data instanceof String && ((String) data).startsWith(ERROR_ID)) {
String errorMessage = ((String) data).substring(ERROR_ID.length());
throw new SWTException (SWT.ERROR_FAILED_EVALUATE, errorMessage);
}
return data;
}
@Override
public boolean execute(String script) {
// Feature in WebView2. ExecuteScript works regardless of IsScriptEnabled setting.
// Disallow programmatic execution manually.
if (!jsEnabled) return false;
IUnknown completion = newCallback((long result, long json) -> COM.S_OK);
int hr = webView.ExecuteScript((script + "\0").toCharArray(), completion);
completion.Release();
return hr == COM.S_OK;
}
@Override
public String getBrowserType() {
return "edge";
}
@Override
String getJavaCallDeclaration() {
return "if (!window.callJava) { window.callJava = function(index, token, args) {\n"
+"return JSON.parse(window.chrome.webview.hostObjects.sync.swt.CallJava(index, token, JSON.stringify(args)));\n"
+"}};\n";
}
@Override
public String getText() {
return (String)evaluate("return document.documentElement.outerHTML;");
}
@Override
public String getUrl() {
long ppsz[] = new long[1];
webView.get_Source(ppsz);
return wstrToString(ppsz[0], true);
}
int handleCloseRequested(long pView, long pArgs) {
browser.getDisplay().asyncExec(() -> {
if (browser.isDisposed()) return;
WindowEvent event = new WindowEvent(browser);
event.display = browser.getDisplay();
event.widget = browser;
for (CloseWindowListener listener : closeWindowListeners) {
listener.close(event);
if (browser.isDisposed()) return;
}
browser.dispose();
});
return COM.S_OK;
}
int handleDocumentTitleChanged(long pView, long pArgs) {
long[] ppsz = new long[1];
webView.get_DocumentTitle(ppsz);
String title = wstrToString(ppsz[0], true);
browser.getDisplay().asyncExec(() -> {
if (browser.isDisposed()) return;
TitleEvent event = new TitleEvent(browser);
event.display = browser.getDisplay();
event.widget = browser;
event.title = title;
for (TitleListener listener : titleListeners) {
listener.changed(event);
if (browser.isDisposed()) return;
}
});
return COM.S_OK;
}
long handleCallJava(int index, long bstrToken, long bstrArgsJson) {
Object result = null;
String token = bstrToString(bstrToken);
BrowserFunction function = functions.get(index);
if (function != null && token.equals (function.token)) {
try {
String argsJson = bstrToString(bstrArgsJson);
Object args = (Object[]) JSON.parse(argsJson.toCharArray());
result = function.function ((Object[]) args);
} catch (Throwable e) {
result = WebBrowser.CreateErrorString(e.getLocalizedMessage());
}
}
String json = JSON.stringify(result);
return COM.SysAllocStringLen(json.toCharArray(), json.length());
}
int handleFrameNavigationStarting(long pView, long pArgs) {
return handleNavigationStarting(pView, pArgs, false);
}
int handleNavigationStarting(long pView, long pArgs) {
return handleNavigationStarting(pView, pArgs, true);
}
int handleNavigationStarting(long pView, long pArgs, boolean top) {
ICoreWebView2NavigationStartingEventArgs args = new ICoreWebView2NavigationStartingEventArgs(pArgs);
long[] ppszUrl = new long[1];
int hr = args.get_Uri(ppszUrl);
if (hr != COM.S_OK) return hr;
String url = wstrToString(ppszUrl[0], true);
long[] pNavId = new long[1];
args.get_NavigationId(pNavId);
LocationEvent event = new LocationEvent(browser);
event.display = browser.getDisplay();
event.widget = browser;
event.location = url;
event.top = top;
event.doit = true;
for (LocationListener listener : locationListeners) {
listener.changing(event);
if (browser.isDisposed()) return COM.S_OK;
}
if (event.doit) {
// Save location and top for all events that use navigationId.
navigations.put(pNavId[0], event);
jsEnabled = jsEnabledOnNextPage;
settings.put_IsScriptEnabled(jsEnabled);
// Register browser functions in the new document.
if (!functions.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (BrowserFunction function : functions.values()) {
sb.append(function.functionString);
}
execute(sb.toString());
}
} else {
args.put_Cancel(true);
}
return COM.S_OK;
}
int handleSourceChanged(long pView, long pArgs) {
// Feature in WebView2. Navigations to data URIs set the Source
// to an empty string. Navigations with NavigateToString set the Source
// to about:blank. Initial Source is about:blank. If Source value
// is the same between navigations, SourceChanged isn't fired.
// TODO: emit missing location changed events
long[] ppsz = new long[1];
int hr = webView.get_Source(ppsz);
if (hr != COM.S_OK) return hr;
String url = wstrToString(ppsz[0], true);
browser.getDisplay().asyncExec(() -> {
if (browser.isDisposed()) return;
LocationEvent event = new LocationEvent(browser);
event.display = browser.getDisplay();
event.widget = browser;
event.location = url;
event.top = true;
for (LocationListener listener : locationListeners) {
listener.changed(event);
if (browser.isDisposed()) return;
}
});
return COM.S_OK;
}
void sendProgressCompleted() {
browser.getDisplay().asyncExec(() -> {
if (browser.isDisposed()) return;
ProgressEvent event = new ProgressEvent(browser);
event.display = browser.getDisplay();
event.widget = browser;
for (ProgressListener listener : progressListeners) {
listener.completed(event);
if (browser.isDisposed()) return;
}
});
}
int handleDOMContentLoaded(long pView, long pArgs) {
ICoreWebView2DOMContentLoadedEventArgs args = new ICoreWebView2DOMContentLoadedEventArgs(pArgs);
long[] pNavId = new long[1];
args.get_NavigationId(pNavId);
LocationEvent startEvent = navigations.get(pNavId[0]);
if (startEvent != null && startEvent.top) {
sendProgressCompleted();
}
return COM.S_OK;
}
int handleNavigationCompleted(long pView, long pArgs) {
return handleNavigationCompleted(pView, pArgs, true);
}
int handleFrameNavigationCompleted(long pView, long pArgs) {
return handleNavigationCompleted(pView, pArgs, false);
}
int handleNavigationCompleted(long pView, long pArgs, boolean top) {
ICoreWebView2NavigationCompletedEventArgs args = new ICoreWebView2NavigationCompletedEventArgs(pArgs);
long[] pNavId = new long[1];
args.get_NavigationId(pNavId);
LocationEvent startEvent = navigations.remove(pNavId[0]);
if (webView2 == null && startEvent != null && startEvent.top) {
// If DOMContentLoaded isn't available, fire
// ProgressListener.completed from here.
sendProgressCompleted();
}
return COM.S_OK;
}
void updateWindowFeatures(ICoreWebView2NewWindowRequestedEventArgs args, WindowEvent event) {
long[] ppv = new long[1];
int hr = args.get_WindowFeatures(ppv);
if (hr != COM.S_OK) return;
ICoreWebView2WindowFeatures features = new ICoreWebView2WindowFeatures(ppv[0]);
int[] px = new int[1], py = new int[1];
features.get_HasPosition(px);
if (px[0] != 0) {
features.get_Left(px);
features.get_Top(py);
event.location = new Point(px[0], py[0]);
}
features.get_HasSize(px);
if (px[0] != 0) {
features.get_Width(px);
features.get_Height(py);
event.size = new Point(px[0], py[0]);
}
// event.addressBar = ???
features.get_ShouldDisplayMenuBar(px);
event.menuBar = px[0] != 0;
features.get_ShouldDisplayStatus(px);
event.statusBar = px[0] != 0;
features.get_ShouldDisplayToolbar(px);
event.toolBar = px[0] != 0;
}
int handleNewWindowRequested(long pView, long pArgs) {
ICoreWebView2NewWindowRequestedEventArgs args = new ICoreWebView2NewWindowRequestedEventArgs(pArgs);
args.AddRef();
long[] ppv = new long[1];
args.GetDeferral(ppv);
ICoreWebView2Deferral deferral = new ICoreWebView2Deferral(ppv[0]);
inNewWindow = true;
browser.getDisplay().asyncExec(() -> {
try {
if (browser.isDisposed()) return;
WindowEvent openEvent = new WindowEvent(browser);
openEvent.display = browser.getDisplay();
openEvent.widget = browser;
openEvent.required = false;
for (OpenWindowListener openListener : openWindowListeners) {
openListener.open(openEvent);
if (browser.isDisposed()) return;
}
if (openEvent.browser != null && !openEvent.browser.isDisposed()) {
WebBrowser other = openEvent.browser.webBrowser;
args.put_Handled(true);
if (other instanceof Edge) {
args.put_NewWindow(((Edge)other).webView.getAddress());
// Send show event to the other browser.
WindowEvent showEvent = new WindowEvent (other.browser);
showEvent.display = browser.getDisplay();
showEvent.widget = other.browser;
updateWindowFeatures(args, showEvent);
for (VisibilityWindowListener showListener : other.visibilityWindowListeners) {
showListener.show(showEvent);
if (other.browser.isDisposed()) return;
}
}
} else if (openEvent.required) {
args.put_Handled(true);
}
} finally {
deferral.Complete();
deferral.Release();
args.Release();
inNewWindow = false;
}
});
return COM.S_OK;
}
int handleMoveFocusRequested(long pView, long pArgs) {
ICoreWebView2MoveFocusRequestedEventArgs args = new ICoreWebView2MoveFocusRequestedEventArgs(pArgs);
int[] pReason = new int[1];
args.get_Reason(pReason);
args.put_Handled(true);
switch (pReason[0]) {
case COM.COREWEBVIEW2_MOVE_FOCUS_REASON_NEXT:
browser.traverse(SWT.TRAVERSE_TAB_NEXT);
break;
case COM.COREWEBVIEW2_MOVE_FOCUS_REASON_PREVIOUS:
browser.traverse(SWT.TRAVERSE_TAB_PREVIOUS);
break;
}
return COM.S_OK;
}
@Override
public boolean isBackEnabled() {
int[] pval = new int[1];
webView.get_CanGoBack(pval);
return pval[0] != 0;
}
@Override
public boolean isForwardEnabled() {
int[] pval = new int[1];
webView.get_CanGoForward(pval);
return pval[0] != 0;
}
@Override
public boolean back() {
// Feature in WebView2. GoBack returns S_OK even when CanGoBack is FALSE.
return isBackEnabled() && webView.GoBack() == COM.S_OK;
}
@Override
public boolean forward() {
// Feature in WebView2. GoForward returns S_OK even when CanGoForward is FALSE.
return isForwardEnabled() && webView.GoForward() == COM.S_OK;
}
@Override
public void refresh() {
webView.Reload();
}
@Override
public void stop() {
webView.Stop();
}
@Override
public boolean setText(String html, boolean trusted) {
char[] data = new char[html.length() + 1];
html.getChars(0, html.length(), data, 0);
return webView.NavigateToString(data) == COM.S_OK;
}
@Override
public boolean setUrl(String url, String postData, String[] headers) {
// Feature in WebView2. Partial URLs like "www.example.com" are not accepted.
// Prepend the protocol if it's missing.
if (!url.matches("[a-z][a-z0-9+.-]*:.*")) {
url = "http://" + url;
}
int hr;
char[] pszUrl = (url + "\0").toCharArray();
if (postData != null || headers != null) {
if (environment2 == null || webView2 == null) {
SWT.error(SWT.ERROR_NOT_IMPLEMENTED, null, " [WebView2 version 88+ is required to set postData and headers]");
}
long[] ppRequest = new long[1];
char[] pszMethod = null;
char[] pszHeaders = null;
IStream stream = null;
if (postData != null) {
pszMethod = "POST\0".toCharArray();
byte[] postDataBytes = postData.getBytes(StandardCharsets.UTF_8);
long pStream = COM.SHCreateMemStream(postDataBytes, postData.length());
if (pStream == 0) error(SWT.ERROR_NO_HANDLES, COM.E_OUTOFMEMORY);
stream = new IStream(pStream);
} else {
pszMethod = "GET\0".toCharArray();
}
if (headers != null) {
String hblock = String.join("\r\n", Arrays.asList(headers));
pszHeaders = new char[hblock.length() + 1];
hblock.getChars(0, hblock.length(), pszHeaders, 0);
}
hr = environment2.CreateWebResourceRequest(pszUrl, pszMethod, stream, pszHeaders, ppRequest);
if (stream != null) stream.Release();
if (hr != COM.S_OK) error(SWT.ERROR_NO_HANDLES, hr);
IUnknown request = new IUnknown(ppRequest[0]);
hr = webView2.NavigateWithWebResourceRequest(request);
request.Release();
} else {
hr = webView.Navigate(pszUrl);
}
return hr == COM.S_OK;
}
}