view win/edge.cpp @ 2123:589896c07c91

Win: Use GetTempPathW() to get a location for the embedded Edge (Chromium) data. This allows apps to run from \Program Files\ since the apps can't normally write to this location, prior to this change trying to open a web view would fail after install.
author bsmith@81767d24-ef19-dc11-ae90-00e081727c95
date Thu, 25 Jun 2020 00:00:48 +0000
parents 9a5dbda8f2ab
children f9a2fc59611c
line wrap: on
line source

/* edge.cpp
 *
 * Allows dw_html_new() to embed a Microsoft Edge (Chromium) browser.
 *
 * Requires Windows 10, 8 or 7 with Microsoft Edge (Chromium) installed.
 * 
 * Only included when BUILD_EDGE is defined, will fall back to embedded IE.
 */
#include "dw.h"
#include "webview2.h"
#include <wrl.h>

using namespace Microsoft::WRL;

#define _DW_HTML_DATA_NAME "_dw_edge"
#define _DW_HTML_DATA_LOCATION "_dw_edge_location"
#define _DW_HTML_DATA_RAW "_dw_edge_raw"

extern "C" {

	/* Import the character conversion functions from dw.c */
	LPWSTR _myUTF8toWide(const char* utf8string, void* outbuf);
	char* _myWideToUTF8(LPCWSTR widestring, void* outbuf);
	#define UTF8toWide(a) _myUTF8toWide(a, a ? _alloca(MultiByteToWideChar(CP_UTF8, 0, a, -1, NULL, 0) * sizeof(WCHAR)) : NULL)
	#define WideToUTF8(a) _myWideToUTF8(a, a ? _alloca(WideCharToMultiByte(CP_UTF8, 0, a, -1, NULL, 0, NULL, NULL)) : NULL)
	LRESULT CALLBACK _wndproc(HWND hWnd, UINT msg, WPARAM mp1, LPARAM mp2);
}

class EdgeBrowser
{
public:
	LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
	BOOL Detect(VOID);
protected:
	Microsoft::WRL::ComPtr<ICoreWebView2Environment> Env;
};

class EdgeWebView
{
public:
	VOID Action(int action);
	int Raw(const char* string);
	int URL(const char* url);
	int JavascriptRun(const char* script, void* scriptdata);
	VOID DoSize(VOID);
	VOID Setup(HWND hwnd, ICoreWebView2Controller* webview);
	VOID Close(VOID);
protected:
	HWND hWnd = nullptr;
	Microsoft::WRL::ComPtr<ICoreWebView2> WebView;
	Microsoft::WRL::ComPtr<ICoreWebView2Controller> WebHost;
};

LRESULT CALLBACK EdgeBrowser::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg)
	{
		case WM_SIZE:
		{
			// Resize the browser object to fit the window
			EdgeWebView *webview;

			// Retrieve the browser object's pointer we stored in our window's GWL_USERDATA when
			// we initially attached the browser object to this window.
			webview = (EdgeWebView*)dw_window_get_data(hWnd, _DW_HTML_DATA_NAME);
			// Resize WebView to fit the bounds of the parent window
			if (webview)
				webview->DoSize();
			return(0);
		}

		case WM_PAINT:
		{
			PAINTSTRUCT ps;
			HDC hdc = BeginPaint(hWnd, &ps);
			EndPaint(hWnd, &ps);
			return(0);
		}

		case WM_CREATE:
		{
			// Step 3 - Create a single WebView within the parent window
			// Create a WebView, whose parent is the main window hWnd
			Env->CreateCoreWebView2Controller(hWnd, Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
				[hWnd](HRESULT result, ICoreWebView2Controller* webhost) -> HRESULT {
					EdgeWebView* WebView = new EdgeWebView;
					ICoreWebView2* webview;

					WebView->Setup(hWnd, webhost);
					dw_window_set_data(hWnd, _DW_HTML_DATA_NAME, DW_POINTER(WebView));

					if (SUCCEEDED(webhost->get_CoreWebView2(&webview))) {
						// Add a few settings for the webview
						// this is a redundant demo step as they are the default settings values
						ICoreWebView2Settings* Settings;
						webview->get_Settings(&Settings);
						Settings->put_IsScriptEnabled(TRUE);
						Settings->put_AreDefaultScriptDialogsEnabled(TRUE);
						Settings->put_IsWebMessageEnabled(TRUE);
#ifndef DEBUG
						Settings->put_AreDevToolsEnabled(FALSE);
#endif

						// Save the token, we might need to dw_window_set_data() this value
						// for later use to remove the handlers
						EventRegistrationToken token;

						// Register a handler for the NavigationStarting event.
						webview->add_NavigationStarting(
							Callback<ICoreWebView2NavigationStartingEventHandler>(
								[hWnd](ICoreWebView2* sender,
									ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT
								{
									LPWSTR uri;
									sender->get_Source(&uri);

									_wndproc(hWnd, WM_USER + 101, (WPARAM)DW_INT_TO_POINTER(DW_HTML_CHANGE_STARTED),
										!wcscmp(uri, L"about:blank") ? (LPARAM)"" : (LPARAM)WideToUTF8((LPWSTR)uri));

									return S_OK;
								}).Get(), &token);

						// Register a handler for the SourceChanged event.
						webview->add_SourceChanged(
							Callback<ICoreWebView2SourceChangedEventHandler >(
								[hWnd](ICoreWebView2* sender,
									ICoreWebView2SourceChangedEventArgs* args) -> HRESULT
								{
									LPWSTR uri;
									sender->get_Source(&uri);

									_wndproc(hWnd, WM_USER + 101, (WPARAM)DW_INT_TO_POINTER(DW_HTML_CHANGE_REDIRECT),
										!wcscmp(uri, L"about:blank") ? (LPARAM)"" : (LPARAM)WideToUTF8((LPWSTR)uri));

									return S_OK;
								}).Get(), &token);

						// Register a handler for the ContentLoading event.
						webview->add_ContentLoading(
							Callback<ICoreWebView2ContentLoadingEventHandler >(
								[hWnd](ICoreWebView2* sender,
									ICoreWebView2ContentLoadingEventArgs* args) -> HRESULT
								{
									LPWSTR uri;
									sender->get_Source(&uri);

									_wndproc(hWnd, WM_USER + 101, (WPARAM)DW_INT_TO_POINTER(DW_HTML_CHANGE_LOADING),
										!wcscmp(uri, L"about:blank") ? (LPARAM)"" : (LPARAM)WideToUTF8((LPWSTR)uri));

									return S_OK;
								}).Get(), &token);

						// Register a handler for the NavigationCompleted event.
						webview->add_NavigationCompleted(
							Callback<ICoreWebView2NavigationCompletedEventHandler>(
								[hWnd](ICoreWebView2* sender,
									ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT
								{
									LPWSTR uri;
									sender->get_Source(&uri);

									_wndproc(hWnd, WM_USER + 101, (WPARAM)DW_INT_TO_POINTER(DW_HTML_CHANGE_COMPLETE),
										!wcscmp(uri, L"about:blank") ? (LPARAM)"" : (LPARAM)WideToUTF8((LPWSTR)uri));

									return S_OK;
								}).Get(), &token);
					}

					// Resize WebView to fit the bounds of the parent window
					WebView->DoSize();

					// Handle cached load requests due to delayed
					// loading of the edge webview contexts
					char *url = (char *)dw_window_get_data(hWnd, _DW_HTML_DATA_LOCATION);
					if (url)
					{
						WebView->URL(url);
						dw_window_set_data(hWnd, _DW_HTML_DATA_LOCATION, NULL);
						free((void*)url);
					}
					char *raw = (char *)dw_window_get_data(hWnd, _DW_HTML_DATA_RAW);
					if (raw)
					{
						WebView->Raw(raw);
						dw_window_set_data(hWnd, _DW_HTML_DATA_RAW, NULL);
						free((void*)raw);
					}
					return S_OK;
				}).Get());
			// Success
			return(0);
		}

		case WM_DESTROY:
		{
			// Detach the browser object from this window, and free resources.
			EdgeWebView *webview;

			// Retrieve the browser object's pointer we stored in our window's GWL_USERDATA when
			// we initially attached the browser object to this window.
			webview = (EdgeWebView*)dw_window_get_data(hWnd, _DW_HTML_DATA_NAME);
			if (webview)
			{
				dw_window_set_data(hWnd, _DW_HTML_DATA_NAME, NULL);
				webview->Close();
				delete webview;
			}
			return(TRUE);
		}
	}

	return(DefWindowProc(hWnd, uMsg, wParam, lParam));
}

VOID EdgeWebView::DoSize(VOID)
{
	RECT bounds;
	BOOL isVisible;

	GetClientRect(hWnd, &bounds);
	WebHost->put_Bounds(bounds);
	WebHost->get_IsVisible(&isVisible);
	if(!isVisible)
		WebHost->put_IsVisible(TRUE);
}

BOOL EdgeBrowser::Detect(VOID)
{
	WCHAR tempdir[MAX_PATH+1];

	GetTempPathW(MAX_PATH, tempdir);

	CreateCoreWebView2EnvironmentWithOptions(nullptr, tempdir, nullptr,
		Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
			[this](HRESULT result, ICoreWebView2Environment* env) -> HRESULT {
				// Successfully created Edge environment, return TRUE 
				Env = env;
				return S_OK;
			}).Get());
	return Env ? TRUE : FALSE;
}

void EdgeWebView::Action(int action)
{
	// We want to get the base address (ie, a pointer) to the IWebView2WebView object embedded within the browser
	// object, so we can call some of the functions in the former's table.
	if (WebView)
	{
		// Call the desired function
		switch (action)
		{
			case DW_HTML_GOBACK:
			{
				// Call the IWebView2WebView object's GoBack function.
				WebView->GoBack();
				break;
			}

			case DW_HTML_GOFORWARD:
			{
				// Call the IWebView2WebView object's GoForward function.
				WebView->GoForward();
				break;
			}

			case DW_HTML_GOHOME:
			{
				// Call the IWebView2WebView object's GoHome function.
				dw_html_url(hWnd, (char*)DW_HOME_URL);
				break;
			}

			case DW_HTML_SEARCH:
			{
				// Call the IWebView2WebView object's GoSearch function.
				//WebView->GoSearch();
				break;
			}

			case DW_HTML_RELOAD:
			{
				// Call the IWebView2WebView object's Refresh function.
				WebView->Reload();
			}

			case DW_HTML_STOP:
			{
				// Call the IWebView2WebView object's Stop function.
				WebView->Stop();
			}
		}
	}
}

int EdgeWebView::Raw(const char* string)
{
	if (WebView)
		WebView->NavigateToString(UTF8toWide(string));
	return DW_ERROR_NONE;
}

int EdgeWebView::URL(const char* url)
{
	if (WebView)
		WebView->Navigate(UTF8toWide(url));
	return DW_ERROR_NONE;
}

int EdgeWebView::JavascriptRun(const char* script, void* scriptdata)
{
	HWND thishwnd = hWnd;

	if (WebView)
		WebView->ExecuteScript(UTF8toWide(script),
			Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
				[thishwnd, scriptdata](HRESULT error, PCWSTR result) -> HRESULT
				{
					char *scriptresult;
					
					/* Result is unquoted "null" when we should return NULL */
					if (result && _wcsicmp(result, L"null") == 0)
						scriptresult = NULL;
					else
						scriptresult = result ? WideToUTF8((LPWSTR)result) : NULL;
					
					/* String results are enclosed in quotations, remove the quotes */
					if(scriptresult && *scriptresult == '\"')
					{
						char *end = strrchr(scriptresult, '\"');
						if(end)
							*end = '\0';
						scriptresult++;
					}
					void *params[2] = { (void *)scriptresult, DW_INT_TO_POINTER((error == S_OK ? DW_ERROR_NONE : DW_ERROR_UNKNOWN)) };
					_wndproc(thishwnd, WM_USER + 100, (WPARAM)params, (LPARAM)scriptdata);
					return S_OK;
				}).Get());
	return DW_ERROR_NONE;
}

VOID EdgeWebView::Setup(HWND hwnd, ICoreWebView2Controller* host)
{
	hWnd = hwnd;
	WebHost = host;
	host->get_CoreWebView2(&WebView);
}

VOID EdgeWebView::Close(VOID)
{
	if (WebHost)
		WebHost->Close();
}

EdgeBrowser *DW_EDGE = NULL;

extern "C" {
	/******************************* dw_edge_detect() **************************
	 * Attempts to create a temporary Edge (Chromium) browser context...
	 * If we succeed return TRUE and use Edge for HTML windows.
	 * If it fails return FALSE and fall back to using embedded IE.
	 */
	BOOL _dw_edge_detect(VOID)
	{
		DW_EDGE = new EdgeBrowser;
		if (DW_EDGE)
		{
			BOOL result = DW_EDGE->Detect();
			if (!result)
			{
				delete DW_EDGE;
				DW_EDGE = NULL;
			}
			return result;
		}
		return FALSE;
	}

	/******************************* dw_edge_action() **************************
	 * Implements the functionality of a "Back". "Forward", "Home", "Search",
	 * "Refresh", or "Stop" button.
	 *
	 * hwnd =		Handle to the window hosting the browser object.
	 * action =		One of the following:
	 *				0 = Move back to the previously viewed web page.
	 *				1 = Move forward to the previously viewed web page.
	 *				2 = Move to the home page.
	 *				3 = Search.
	 *				4 = Refresh the page.
	 *				5 = Stop the currently loading page.
	 */

	void _dw_edge_action(HWND hwnd, int action)
	{
		EdgeWebView* webview = (EdgeWebView *)dw_window_get_data(hwnd, _DW_HTML_DATA_NAME);
		if (webview)
			webview->Action(action);
	}

	/******************************* dw_edge_raw() ****************************
	 * Takes a string containing some HTML BODY, and displays it in the specified
	 * window. For example, perhaps you want to display the HTML text of...
	 *
	 * <P>This is a picture.<P><IMG src="mypic.jpg">
	 *
	 * hwnd =		Handle to the window hosting the browser object.
	 * string =		Pointer to nul-terminated string containing the HTML BODY.
	 *				(NOTE: No <BODY></BODY> tags are required in the string).
	 *
	 * RETURNS: 0 if success, or non-zero if an error.
	 */

	int _dw_edge_raw(HWND hwnd, const char* string)
	{
		EdgeWebView* webview = (EdgeWebView*)dw_window_get_data(hwnd, _DW_HTML_DATA_NAME);
		if (webview)
			return webview->Raw(string);
		else
			dw_window_set_data(hwnd, _DW_HTML_DATA_RAW, strdup(string));
		return DW_ERROR_NONE;
	}

	/******************************* dw_edge_url() ****************************
	 * Displays a URL, or HTML file on disk.
	 *
	 * hwnd =		Handle to the window hosting the browser object.
	 * url	=		Pointer to nul-terminated name of the URL/file.
	 *
	 * RETURNS: 0 if success, or non-zero if an error.
	 */

	int _dw_edge_url(HWND hwnd, const char* url)
	{
		EdgeWebView* webview = (EdgeWebView*)dw_window_get_data(hwnd, _DW_HTML_DATA_NAME);
		if (webview)
			return webview->URL(url);
		else
			dw_window_set_data(hwnd, _DW_HTML_DATA_LOCATION, strdup(url));
		return DW_ERROR_NONE;
	}

	/******************************* dw_edge_javascript_run() ****************************
	 * Runs a javascript in the specified browser context.
	 *
	 * hwnd			=	Handle to the window hosting the browser object.
	 * script		=	Pointer to nul-terminated javascript string.
	 * scriptdata	=   Pointer to user data to be passed to the callback.
	 *
	 * RETURNS: 0 if success, or non-zero if an error.
	 */

	int _dw_edge_javascript_run(HWND hwnd, const char* script, void* scriptdata)
	{
		EdgeWebView* webview = (EdgeWebView*)dw_window_get_data(hwnd, _DW_HTML_DATA_NAME);
		if (webview)
			return webview->JavascriptRun(script, scriptdata);
		return DW_ERROR_UNKNOWN;
	}

	/************************** edgeWindowProc() *************************
	 * Our message handler for our window to host the browser.
	 */

	LRESULT CALLBACK _edgeWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
	{
		if (DW_EDGE)
			return DW_EDGE->WndProc(hWnd, uMsg, wParam, lParam);
		return DefWindowProc(hWnd, uMsg, wParam, lParam);
	}
}