From b382e534f6ae574970aa588b52f2b8bd209df9c4 Mon Sep 17 00:00:00 2001 From: Jessica Chen <55165503+peiche-jessica@users.noreply.github.com> Date: Mon, 19 Oct 2020 09:16:36 -0700 Subject: [PATCH] Move projects to use latest WebView2 SDK 1.0.674-prerelease (#58) * Updated testing instructions and added missing images * Move win32 sample to use latest WebView2 prerelease SDK 0.9.579 * Move wpf sample to use latest WebView2 prerelease SDK 0.9.579 * Updated .gitignore * Move winforms sample to use latest WebView2 prerelease SDK 0.9.579 * Added VSCode debugging setup * Added screenshots that should be included in the previous commit * Improved script debugging attach and removed targeted * Moved WPF to use 0.9.579-prerelease * Improved testing instructions * Move projects to use latest WebView2 SDK 0.9.627-prerelease * Use 628 instead * Use 579 for testing purposes * Update package.config too * Added USE_WEBVIEW2_WIN10 * Revert "Update package.config too" This reverts commit 8df5be99620c958fb46d28986804f791a2acdd7a. * Revert "Use 579 for testing purposes" This reverts commit 8b42389c5f5af9971e7cf426fc97dd4e845eecc7. * Link to latest docs * Move projects to use latest WebView2 SDK 1.0.672-prerelease * Move projects to use latest WebView2 SDK 1.0.674-prerelease --- SampleApps/WebView2APISample/App.cpp | 428 +-- SampleApps/WebView2APISample/AppStartPage.cpp | 109 + SampleApps/WebView2APISample/AppStartPage.h | 15 + .../WebView2APISample/AppStartPage.html | 153 + SampleApps/WebView2APISample/AppStartPage.js | 56 + .../AppStartPageBackground.png | Bin 0 -> 129743 bytes SampleApps/WebView2APISample/AppWindow.cpp | 2517 +++++++++-------- SampleApps/WebView2APISample/AppWindow.h | 350 +-- .../WebView2APISample/DCompTargetImpl.cpp | 62 +- .../WebView2APISample/DCompTargetImpl.h | 48 +- .../ScenarioAddHostObject.html | 240 +- .../ScenarioCookieManagement.cpp | 227 ++ .../ScenarioCookieManagement.h | 30 + .../ScenarioCookieManagement.html | 62 + .../ScenarioDOMContentLoaded.cpp | 64 + .../ScenarioDOMContentLoaded.h | 26 + .../ScenarioDOMContentLoaded.html | 12 + ...ScenarioNavigateWithWebResourceRequest.cpp | 55 + .../ScenarioNavigateWithWebResourceRequest.h | 19 + .../ScenarioWebViewEventMonitor.cpp | 1357 ++++----- .../ScenarioWebViewEventMonitor.h | 123 +- .../WebView2APISample/SettingsComponent.cpp | 1316 ++++----- SampleApps/WebView2APISample/Toolbar.cpp | 300 +- .../WebView2APISample/ViewComponent.cpp | 1366 ++++----- SampleApps/WebView2APISample/ViewComponent.h | 217 +- .../WebView2APISample/WebView2APISample.rc | 596 ++-- .../WebView2APISample.vcxproj | 961 +++---- .../WebView2APISample.vcxproj.filters | 31 + .../documentation/Testing-Instructions.md | 20 +- SampleApps/WebView2APISample/packages.config | 8 +- SampleApps/WebView2APISample/resource.h | 231 +- .../WebView2WindowsFormsBrowser.csproj | 2 +- SampleApps/WebView2WpfBrowser/MainWindow.xaml | 15 + .../WebView2WpfBrowser/MainWindow.xaml.cs | 153 +- .../WebView2WpfBrowser.csproj | 2 +- 35 files changed, 6155 insertions(+), 5016 deletions(-) create mode 100644 SampleApps/WebView2APISample/AppStartPage.cpp create mode 100644 SampleApps/WebView2APISample/AppStartPage.h create mode 100644 SampleApps/WebView2APISample/AppStartPage.html create mode 100644 SampleApps/WebView2APISample/AppStartPage.js create mode 100644 SampleApps/WebView2APISample/AppStartPageBackground.png create mode 100644 SampleApps/WebView2APISample/ScenarioCookieManagement.cpp create mode 100644 SampleApps/WebView2APISample/ScenarioCookieManagement.h create mode 100644 SampleApps/WebView2APISample/ScenarioCookieManagement.html create mode 100644 SampleApps/WebView2APISample/ScenarioDOMContentLoaded.cpp create mode 100644 SampleApps/WebView2APISample/ScenarioDOMContentLoaded.h create mode 100644 SampleApps/WebView2APISample/ScenarioDOMContentLoaded.html create mode 100644 SampleApps/WebView2APISample/ScenarioNavigateWithWebResourceRequest.cpp create mode 100644 SampleApps/WebView2APISample/ScenarioNavigateWithWebResourceRequest.h diff --git a/SampleApps/WebView2APISample/App.cpp b/SampleApps/WebView2APISample/App.cpp index 3ac52dd5..3bd1d819 100644 --- a/SampleApps/WebView2APISample/App.cpp +++ b/SampleApps/WebView2APISample/App.cpp @@ -1,214 +1,214 @@ -// Copyright (C) Microsoft Corporation. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "stdafx.h" - -#include "App.h" - -#include -#include -#include -#include -#include -#include - -#include "AppWindow.h" -#include "DpiUtil.h" - -HINSTANCE g_hInstance; -int g_nCmdShow; -bool g_autoTabHandle = true; -static std::map s_threads; - -static int RunMessagePump(); -static DWORD WINAPI ThreadProc(void* pvParam); -static void WaitForOtherThreads(); - -#define NEXT_PARAM_CONTAINS(command) \ - _wcsnicmp(nextParam.c_str(), command, ARRAYSIZE(command) - 1) == 0 - -int APIENTRY wWinMain(HINSTANCE hInstance, - HINSTANCE hPrevInstance, - PWSTR lpCmdLine, - int nCmdShow) -{ - g_hInstance = hInstance; - UNREFERENCED_PARAMETER(hPrevInstance); - g_nCmdShow = nCmdShow; - - // Default DPI awareness to PerMonitorV2. The commandline parameters can - // override this. - DPI_AWARENESS_CONTEXT dpiAwarenessContext = - DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2; - std::wstring appId(L"EBWebView.SampleApp"); - std::wstring initialUri(L"https://www.bing.com"); - DWORD creationModeId = IDM_CREATION_MODE_WINDOWED; - - if (lpCmdLine && lpCmdLine[0]) - { - int paramCount = 0; - LPWSTR* params = CommandLineToArgvW(lpCmdLine, ¶mCount); - for (int i = 0; i < paramCount; ++i) - { - std::wstring nextParam; - if (params[i][0] == L'-') - { - if (params[i][1] == L'-') - { - nextParam.assign(params[i] + 2); - } - else - { - nextParam.assign(params[i] + 1); - } - } - if (NEXT_PARAM_CONTAINS(L"dpiunaware")) - { - dpiAwarenessContext = DPI_AWARENESS_CONTEXT_UNAWARE; - } - else if (NEXT_PARAM_CONTAINS(L"dpisystemaware")) - { - dpiAwarenessContext = DPI_AWARENESS_CONTEXT_SYSTEM_AWARE; - } - else if (NEXT_PARAM_CONTAINS(L"dpipermonitorawarev2")) - { - dpiAwarenessContext = DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2; - } - else if (NEXT_PARAM_CONTAINS(L"dpipermonitoraware")) - { - dpiAwarenessContext = DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE; - } - else if (NEXT_PARAM_CONTAINS(L"noinitialnavigation")) - { - initialUri = L""; - } - else if (NEXT_PARAM_CONTAINS(L"appid=")) - { - appId = nextParam.substr(nextParam.find(L'=') + 1); - } - else if (NEXT_PARAM_CONTAINS(L"initialUri=")) - { - initialUri = nextParam.substr(nextParam.find(L'=') + 1); - } - else if (NEXT_PARAM_CONTAINS(L"creationmode=")) - { - nextParam = nextParam.substr(nextParam.find(L'=') + 1); - if (NEXT_PARAM_CONTAINS(L"windowed")) - { - creationModeId = IDM_CREATION_MODE_WINDOWED; - } - else if (NEXT_PARAM_CONTAINS(L"visualdcomp")) - { - creationModeId = IDM_CREATION_MODE_VISUAL_DCOMP; - } - else if (NEXT_PARAM_CONTAINS(L"targetdcomp")) - { - creationModeId = IDM_CREATION_MODE_TARGET_DCOMP; - } -#ifdef USE_WEBVIEW2_WIN10 - else if (NEXT_PARAM_CONTAINS(L"visualwincomp")) - { - creationModeId = IDM_CREATION_MODE_VISUAL_WINCOMP; - } -#endif - } - } - LocalFree(params); - } - SetCurrentProcessExplicitAppUserModelID(appId.c_str()); - - DpiUtil::SetProcessDpiAwarenessContext(dpiAwarenessContext); - - new AppWindow(creationModeId, initialUri, true); - - int retVal = RunMessagePump(); - - WaitForOtherThreads(); - - return retVal; -} - -// Run the message pump for one thread. -static int RunMessagePump() -{ - HACCEL hAccelTable = - LoadAccelerators(g_hInstance, MAKEINTRESOURCE(IDC_WEBVIEW2APISAMPLE)); - - MSG msg; - - // Main message loop: - //! [MoveFocus0] - while (GetMessage(&msg, nullptr, 0, 0)) - { - if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) - { - // Calling IsDialogMessage handles Tab traversal automatically. If the - // app wants the platform to auto handle tab, then call IsDialogMessage - // before calling TranslateMessage/DispatchMessage. If the app wants to - // handle tabbing itself, then skip calling IsDialogMessage and call - // TranslateMessage/DispatchMessage directly. - if (!g_autoTabHandle || !IsDialogMessage(GetAncestor(msg.hwnd, GA_ROOT), &msg)) - { - TranslateMessage(&msg); - DispatchMessage(&msg); - } - } - } - //! [MoveFocus0] - - DWORD threadId = GetCurrentThreadId(); - auto it = s_threads.find(threadId); - if (it != s_threads.end()) - { - CloseHandle(it->second); - s_threads.erase(threadId); - } - - return (int)msg.wParam; -} - -// Make a new thread. -void CreateNewThread(UINT creationModeId) -{ - DWORD threadId; - HANDLE thread = CreateThread( - nullptr, 0, ThreadProc, reinterpret_cast(creationModeId), - STACK_SIZE_PARAM_IS_A_RESERVATION, &threadId); - s_threads.insert(std::pair(threadId, thread)); -} - -// This function is the starting point for new threads. It will open a new app window. -static DWORD WINAPI ThreadProc(void* pvParam) -{ - new AppWindow(reinterpret_cast(pvParam)); - return RunMessagePump(); -} - -// Called on the main thread. Wait for all other threads to complete before exiting. -static void WaitForOtherThreads() -{ - while (!s_threads.empty()) - { - std::vector threadHandles; - for (auto it = s_threads.begin(); it != s_threads.end(); ++it) - { - threadHandles.push_back(it->second); - } - - HANDLE* handleArray = threadHandles.data(); - DWORD dwIndex = MsgWaitForMultipleObjects( - static_cast(threadHandles.size()), threadHandles.data(), FALSE, - INFINITE, QS_ALLEVENTS); - - if (dwIndex == WAIT_OBJECT_0 + threadHandles.size()) - { - MSG msg; - while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) - { - TranslateMessage(&msg); - DispatchMessage(&msg); - } - } - } -} +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "stdafx.h" + +#include "App.h" + +#include +#include +#include +#include +#include +#include + +#include "AppWindow.h" +#include "DpiUtil.h" + +HINSTANCE g_hInstance; +int g_nCmdShow; +bool g_autoTabHandle = true; +static std::map s_threads; + +static int RunMessagePump(); +static DWORD WINAPI ThreadProc(void* pvParam); +static void WaitForOtherThreads(); + +#define NEXT_PARAM_CONTAINS(command) \ + _wcsnicmp(nextParam.c_str(), command, ARRAYSIZE(command) - 1) == 0 + +int APIENTRY wWinMain(HINSTANCE hInstance, + HINSTANCE hPrevInstance, + PWSTR lpCmdLine, + int nCmdShow) +{ + g_hInstance = hInstance; + UNREFERENCED_PARAMETER(hPrevInstance); + g_nCmdShow = nCmdShow; + + // Default DPI awareness to PerMonitorV2. The commandline parameters can + // override this. + DPI_AWARENESS_CONTEXT dpiAwarenessContext = + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2; + std::wstring appId(L"EBWebView.SampleApp"); + std::wstring initialUri; + DWORD creationModeId = IDM_CREATION_MODE_WINDOWED; + + if (lpCmdLine && lpCmdLine[0]) + { + int paramCount = 0; + LPWSTR* params = CommandLineToArgvW(lpCmdLine, ¶mCount); + for (int i = 0; i < paramCount; ++i) + { + std::wstring nextParam; + if (params[i][0] == L'-') + { + if (params[i][1] == L'-') + { + nextParam.assign(params[i] + 2); + } + else + { + nextParam.assign(params[i] + 1); + } + } + if (NEXT_PARAM_CONTAINS(L"dpiunaware")) + { + dpiAwarenessContext = DPI_AWARENESS_CONTEXT_UNAWARE; + } + else if (NEXT_PARAM_CONTAINS(L"dpisystemaware")) + { + dpiAwarenessContext = DPI_AWARENESS_CONTEXT_SYSTEM_AWARE; + } + else if (NEXT_PARAM_CONTAINS(L"dpipermonitorawarev2")) + { + dpiAwarenessContext = DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2; + } + else if (NEXT_PARAM_CONTAINS(L"dpipermonitoraware")) + { + dpiAwarenessContext = DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE; + } + else if (NEXT_PARAM_CONTAINS(L"noinitialnavigation")) + { + initialUri = L"none"; + } + else if (NEXT_PARAM_CONTAINS(L"appid=")) + { + appId = nextParam.substr(nextParam.find(L'=') + 1); + } + else if (NEXT_PARAM_CONTAINS(L"initialUri=")) + { + initialUri = nextParam.substr(nextParam.find(L'=') + 1); + } + else if (NEXT_PARAM_CONTAINS(L"creationmode=")) + { + nextParam = nextParam.substr(nextParam.find(L'=') + 1); + if (NEXT_PARAM_CONTAINS(L"windowed")) + { + creationModeId = IDM_CREATION_MODE_WINDOWED; + } + else if (NEXT_PARAM_CONTAINS(L"visualdcomp")) + { + creationModeId = IDM_CREATION_MODE_VISUAL_DCOMP; + } + else if (NEXT_PARAM_CONTAINS(L"targetdcomp")) + { + creationModeId = IDM_CREATION_MODE_TARGET_DCOMP; + } +#ifdef USE_WEBVIEW2_WIN10 + else if (NEXT_PARAM_CONTAINS(L"visualwincomp")) + { + creationModeId = IDM_CREATION_MODE_VISUAL_WINCOMP; + } +#endif + } + } + LocalFree(params); + } + SetCurrentProcessExplicitAppUserModelID(appId.c_str()); + + DpiUtil::SetProcessDpiAwarenessContext(dpiAwarenessContext); + + new AppWindow(creationModeId, initialUri, true); + + int retVal = RunMessagePump(); + + WaitForOtherThreads(); + + return retVal; +} + +// Run the message pump for one thread. +static int RunMessagePump() +{ + HACCEL hAccelTable = + LoadAccelerators(g_hInstance, MAKEINTRESOURCE(IDC_WEBVIEW2APISAMPLE)); + + MSG msg; + + // Main message loop: + //! [MoveFocus0] + while (GetMessage(&msg, nullptr, 0, 0)) + { + if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) + { + // Calling IsDialogMessage handles Tab traversal automatically. If the + // app wants the platform to auto handle tab, then call IsDialogMessage + // before calling TranslateMessage/DispatchMessage. If the app wants to + // handle tabbing itself, then skip calling IsDialogMessage and call + // TranslateMessage/DispatchMessage directly. + if (!g_autoTabHandle || !IsDialogMessage(GetAncestor(msg.hwnd, GA_ROOT), &msg)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + } + //! [MoveFocus0] + + DWORD threadId = GetCurrentThreadId(); + auto it = s_threads.find(threadId); + if (it != s_threads.end()) + { + CloseHandle(it->second); + s_threads.erase(threadId); + } + + return (int)msg.wParam; +} + +// Make a new thread. +void CreateNewThread(UINT creationModeId) +{ + DWORD threadId; + HANDLE thread = CreateThread( + nullptr, 0, ThreadProc, reinterpret_cast(creationModeId), + STACK_SIZE_PARAM_IS_A_RESERVATION, &threadId); + s_threads.insert(std::pair(threadId, thread)); +} + +// This function is the starting point for new threads. It will open a new app window. +static DWORD WINAPI ThreadProc(void* pvParam) +{ + new AppWindow(reinterpret_cast(pvParam)); + return RunMessagePump(); +} + +// Called on the main thread. Wait for all other threads to complete before exiting. +static void WaitForOtherThreads() +{ + while (!s_threads.empty()) + { + std::vector threadHandles; + for (auto it = s_threads.begin(); it != s_threads.end(); ++it) + { + threadHandles.push_back(it->second); + } + + HANDLE* handleArray = threadHandles.data(); + DWORD dwIndex = MsgWaitForMultipleObjects( + static_cast(threadHandles.size()), threadHandles.data(), FALSE, + INFINITE, QS_ALLEVENTS); + + if (dwIndex == WAIT_OBJECT_0 + threadHandles.size()) + { + MSG msg; + while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + } +} diff --git a/SampleApps/WebView2APISample/AppStartPage.cpp b/SampleApps/WebView2APISample/AppStartPage.cpp new file mode 100644 index 00000000..594438d7 --- /dev/null +++ b/SampleApps/WebView2APISample/AppStartPage.cpp @@ -0,0 +1,109 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "stdafx.h" + +#include +#include +#include + +#include "AppStartPage.h" +#include "AppWindow.h" +#include "CheckFailure.h" + +using namespace Microsoft::WRL; + +namespace AppStartPage +{ + +bool AreFileUrisEqual(std::wstring leftUri, std::wstring rightUri) +{ + // Have to to lower due to current bug + std::transform(leftUri.begin(), leftUri.end(), + leftUri.begin(), ::tolower); + std::transform(rightUri.begin(), rightUri.end(), + rightUri.begin(), ::tolower); + + return leftUri == rightUri; +} + +std::wstring ResolvePathAndTrimFile(std::wstring path) +{ + wchar_t resultPath[MAX_PATH]; + PathCchCanonicalize(resultPath, ARRAYSIZE(resultPath), path.c_str()); + PathCchRemoveFileSpec(resultPath, ARRAYSIZE(resultPath)); + return resultPath; +} + +std::wstring GetSdkBuild() +{ + auto options = Microsoft::WRL::Make(); + wil::unique_cotaskmem_string targetVersion; + CHECK_FAILURE(options->get_TargetCompatibleBrowserVersion(&targetVersion)); + + // The full version string A.B.C.D + const wchar_t* targetVersionMajorAndRest = targetVersion.get(); + // Should now be .B.C.D + const wchar_t* targetVersionMinorAndRest = wcschr(targetVersionMajorAndRest, L'.'); + CHECK_FAILURE((targetVersionMinorAndRest != nullptr && *targetVersionMinorAndRest == L'.') ? S_OK : E_UNEXPECTED); + + // Should now be .C.D + const wchar_t* targetVersionBuildAndRest = wcschr(targetVersionMinorAndRest + 1, L'.'); + CHECK_FAILURE((targetVersionBuildAndRest != nullptr && *targetVersionBuildAndRest == L'.') ? S_OK : E_UNEXPECTED); + + // Return + 1 to skip the first . so just C.D + return targetVersionBuildAndRest + 1; +} + +std::wstring GetRuntimeVersion(AppWindow* appWindow) +{ + wil::com_ptr environment = appWindow->GetWebViewEnvironment(); + wil::unique_cotaskmem_string runtimeVersion; + CHECK_FAILURE(environment->get_BrowserVersionString(&runtimeVersion)); + + return runtimeVersion.get(); +} + +std::wstring GetAppPath() +{ + wchar_t appPath[MAX_PATH]; + GetModuleFileName(nullptr, appPath, ARRAYSIZE(appPath)); + return ResolvePathAndTrimFile(appPath); +} + +std::wstring GetRuntimePath(AppWindow* appWindow) +{ + wil::com_ptr webview = appWindow->GetWebView(); + UINT32 browserProcessId = 0; + wchar_t runtimePath[MAX_PATH]; + CHECK_FAILURE(webview->get_BrowserProcessId(&browserProcessId)); + + HANDLE processHandle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, browserProcessId); + CHECK_FAILURE(processHandle == nullptr ? E_FAIL : S_OK); + GetModuleFileNameEx(processHandle, nullptr, runtimePath, ARRAYSIZE(runtimePath)); + CloseHandle(processHandle); + + return ResolvePathAndTrimFile(runtimePath); +} + +std::wstring GetUri(AppWindow* appWindow) +{ + std::wstring uri = appWindow->GetLocalUri(L"AppStartPage.html"); + + uri += L"?sdkBuild="; + uri += GetSdkBuild(); + + uri += L"&runtimeVersion="; + uri += GetRuntimeVersion(appWindow); + + uri += L"&appPath="; + uri += GetAppPath(); + + uri += L"&runtimePath="; + uri += GetRuntimePath(appWindow); + + return uri; +} + +}; // namespace AppStartPage \ No newline at end of file diff --git a/SampleApps/WebView2APISample/AppStartPage.h b/SampleApps/WebView2APISample/AppStartPage.h new file mode 100644 index 00000000..98654a76 --- /dev/null +++ b/SampleApps/WebView2APISample/AppStartPage.h @@ -0,0 +1,15 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#pragma once + +#include "stdafx.h" +#include + +class AppWindow; + +namespace AppStartPage +{ + std::wstring GetUri(AppWindow* appWindow); +}; diff --git a/SampleApps/WebView2APISample/AppStartPage.html b/SampleApps/WebView2APISample/AppStartPage.html new file mode 100644 index 00000000..1469a547 --- /dev/null +++ b/SampleApps/WebView2APISample/AppStartPage.html @@ -0,0 +1,153 @@ + + + + + Microsoft Edge WebView2 + + + +
+

Microsoft Edge WebView2

+
+ +
+
+
+
SDK build
...
+
Runtime version
...
+
App path
...
+
Runtime path
...
+
+
+
+ +
+
+

Issues

+
+ New issue +
+
+ +
+
+

Documentation

+
+ WebView2 documentation +
+
+ +
+
+

SDK releases

+
    +
+
+
+ + + +
+
+

Release Notes

+ +
+
+ + + + \ No newline at end of file diff --git a/SampleApps/WebView2APISample/AppStartPage.js b/SampleApps/WebView2APISample/AppStartPage.js new file mode 100644 index 00000000..235b9dba --- /dev/null +++ b/SampleApps/WebView2APISample/AppStartPage.js @@ -0,0 +1,56 @@ +(async function () { + async function uriToObject(uri) { + const responseFromFetch = await fetch(uri); + const responseAsText = await responseFromFetch.text(); + const response = JSON.parse(responseAsText); + return response; + } + + function parseQuery(query) { + if (query.startsWith("?")) { + query = query.substring(1); + } + + return query. + split("&"). + map(encodedNameValueStr => encodedNameValueStr.split("=")). + reduce((resultObject, encodedNameValueArr) => { + const nameValueArr = encodedNameValueArr.map(decodeURIComponent); + resultObject[nameValueArr[0]] = nameValueArr[1]; + return resultObject; + }, {}); + } + + const sdkReleasesNode = document.getElementById("sdkReleases"); + if (sdkReleasesNode) { + const nugetInfoUri = "https://azuresearch-usnc.nuget.org/query?q=PackageID%3aMicrosoft.Web.WebView2&prerelease=true&semVerLevel=2.0.0"; + const nugetInfo = await uriToObject(nugetInfoUri); + + let versions = nugetInfo.data[0].versions; + versions.reverse(); + versions.forEach(version => { + const versionText = version.version; + const aNode = document.createElement("a"); + aNode.href = "https://www.nuget.org/packages/Microsoft.Web.WebView2/" + versionText; + aNode.textContent = "WebView2 SDK " + versionText; + + const itemNode = document.createElement("li"); + itemNode.appendChild(aNode); + + sdkReleasesNode.appendChild(itemNode); + }); + } + + const query = parseQuery(location.search); + const fillIds = ["sdkBuild", "runtimeVersion", "appPath", "runtimePath"]; + fillIds.forEach(id => { + let content = query[id]; + if (content) { + const maxContentLength = 100; + if (content.length > maxContentLength) { + content = "..." + content.substring(content.length - maxContentLength); + } + document.getElementById(id).textContent = content; + } + }) +})(); \ No newline at end of file diff --git a/SampleApps/WebView2APISample/AppStartPageBackground.png b/SampleApps/WebView2APISample/AppStartPageBackground.png new file mode 100644 index 0000000000000000000000000000000000000000..271b6481c1e589ebe99f0d296224ebeab8d7cfd2 GIT binary patch literal 129743 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

t|8EwGPy_QBAWc8aFhjDdf+ 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ⅅ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 ag*;W`xOfVT^~*oNr>6Y)LB66%(EkDY&3%;s literal 0 HcmV?d00001 diff --git a/SampleApps/WebView2APISample/AppWindow.cpp b/SampleApps/WebView2APISample/AppWindow.cpp index 17f05097..6e2a7b22 100644 --- a/SampleApps/WebView2APISample/AppWindow.cpp +++ b/SampleApps/WebView2APISample/AppWindow.cpp @@ -1,1238 +1,1279 @@ -// Copyright (C) Microsoft Corporation. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "stdafx.h" - -#include "AppWindow.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include "App.h" -#include "CheckFailure.h" -#include "ControlComponent.h" -#include "DpiUtil.h" -#include "FileComponent.h" -#include "ProcessComponent.h" -#include "Resource.h" -#include "ScenarioAddHostObject.h" -#include "ScenarioAuthentication.h" -#include "ScenarioWebMessage.h" -#include "ScenarioWebViewEventMonitor.h" -#include "ScriptComponent.h" -#include "SettingsComponent.h" -#include "TextInputDialog.h" -#include "ViewComponent.h" -using namespace Microsoft::WRL; -static constexpr size_t s_maxLoadString = 100; -static constexpr UINT s_runAsyncWindowMessage = WM_APP; - -static thread_local size_t s_appInstances = 0; -// The minimum height and width for Window Features. -// See https://developer.mozilla.org/en-US/docs/Web/API/Window/open#Size -static constexpr int s_minNewWindowSize = 100; - -// Run Download and Install in another thread so we don't block the UI thread -DWORD WINAPI DownloadAndInstallWV2RT(_In_ LPVOID lpParameter) -{ - AppWindow* appWindow = (AppWindow*) lpParameter; - - int returnCode = 2; // Download failed - // Use fwlink to download WebView2 Bootstrapper at runtime and invoke installation - // Broken/Invalid Https Certificate will fail to download - // Use of the download link below is governed by the below terms. You may acquire the link for your use at https://developer.microsoft.com/microsoft-edge/webview2/. - // Microsoft owns all legal right, title, and interest in and to the WebView2 Runtime Bootstrapper ("Software") and related documentation, - // including any intellectual property in the Software. - // You must acquire all code, including any code obtained from a Microsoft URL, under a separate license directly from Microsoft, including a Microsoft download site - // (e.g., https://developer.microsoft.com/microsoft-edge/webview2/). - HRESULT hr = URLDownloadToFile(NULL, L"https://go.microsoft.com/fwlink/p/?LinkId=2124703", L".\\MicrosoftEdgeWebview2Setup.exe", 0, 0); - if (hr == S_OK) - { - // Either Package the WebView2 Bootstrapper with your app or download it using fwlink - // Then invoke install at Runtime. - SHELLEXECUTEINFO shExInfo = {0}; - shExInfo.cbSize = sizeof(shExInfo); - shExInfo.fMask = SEE_MASK_NOASYNC; - shExInfo.hwnd = 0; - shExInfo.lpVerb = L"runas"; - shExInfo.lpFile = L"MicrosoftEdgeWebview2Setup.exe"; - shExInfo.lpParameters = L" /silent /install"; - shExInfo.lpDirectory = 0; - shExInfo.nShow = 0; - shExInfo.hInstApp = 0; - - if (ShellExecuteEx(&shExInfo)) - { - returnCode = 0; // Install successfull - } - else - { - returnCode = 1; // Install failed - } - } - - appWindow->InstallComplete(returnCode); - appWindow->Release(); - return returnCode; -} - -// Creates a new window which is a copy of the entire app, but on the same thread. -AppWindow::AppWindow( - UINT creationModeId, - std::wstring initialUri, - bool isMainWindow, - std::function webviewCreatedCallback, - bool customWindowRect, - RECT windowRect, - bool shouldHaveToolbar) - : m_creationModeId(creationModeId), - m_initialUri(initialUri), - m_onWebViewFirstInitialized(webviewCreatedCallback) -{ - // Initialize COM as STA. - CHECK_FAILURE(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)); - - ++s_appInstances; - - WCHAR szTitle[s_maxLoadString]; // The title bar text - LoadStringW(g_hInstance, IDS_APP_TITLE, szTitle, s_maxLoadString); - - if (customWindowRect) - { - m_mainWindow = CreateWindowExW( - WS_EX_CONTROLPARENT, GetWindowClass(), szTitle, WS_OVERLAPPEDWINDOW, windowRect.left, - windowRect.top, windowRect.right-windowRect.left, windowRect.bottom-windowRect.top, nullptr, nullptr, g_hInstance, nullptr); - } - else - { - m_mainWindow = CreateWindowExW( - WS_EX_CONTROLPARENT, GetWindowClass(), szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, - 0, CW_USEDEFAULT, 0, nullptr, nullptr, g_hInstance, nullptr); - } - - SetWindowLongPtr(m_mainWindow, GWLP_USERDATA, (LONG_PTR)this); - -#ifdef USE_WEBVIEW2_WIN10 - //! [TextScaleChanged1] - if (winrt::try_get_activation_factory()) - { - m_uiSettings = winrt::Windows::UI::ViewManagement::UISettings(); - m_uiSettings.TextScaleFactorChanged({ this, &AppWindow::OnTextScaleChanged }); - } - //! [TextScaleChanged1] -#endif - - if (shouldHaveToolbar) - { - m_toolbar.Initialize(this); - } - - UpdateCreationModeMenu(); - ShowWindow(m_mainWindow, g_nCmdShow); - UpdateWindow(m_mainWindow); - - // If no WebVieRuntime installed, create new thread to do install/download. - // Otherwise just initialize webview. - wil::unique_cotaskmem_string version_info; - HRESULT hr = GetAvailableCoreWebView2BrowserVersionString(nullptr, &version_info); - if (hr == S_OK && version_info != nullptr) - { - RunAsync([this] { - InitializeWebView(); - }); - } - else - { - if (isMainWindow) { - AddRef(); - CreateThread(0, 0, DownloadAndInstallWV2RT, (void*) this, 0, 0); - } - else - { - MessageBox(m_mainWindow, L"WebView Runtime not installed", L"WebView Runtime Installation status", MB_OK); - } - } -} - -// Register the Win32 window class for the app window. -PCWSTR AppWindow::GetWindowClass() -{ - // Only do this once - static PCWSTR windowClass = [] { - static WCHAR windowClass[s_maxLoadString]; - LoadStringW(g_hInstance, IDC_WEBVIEW2APISAMPLE, windowClass, s_maxLoadString); - - WNDCLASSEXW wcex; - wcex.cbSize = sizeof(WNDCLASSEX); - - wcex.style = CS_HREDRAW | CS_VREDRAW; - wcex.lpfnWndProc = WndProcStatic; - wcex.cbClsExtra = 0; - wcex.cbWndExtra = 0; - wcex.hInstance = g_hInstance; - wcex.hIcon = LoadIcon(g_hInstance, MAKEINTRESOURCE(IDI_WEBVIEW2APISAMPLE)); - wcex.hCursor = LoadCursor(nullptr, IDC_ARROW); - wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); - wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_WEBVIEW2APISAMPLE); - wcex.lpszClassName = windowClass; - wcex.hIconSm = LoadIcon(g_hInstance, MAKEINTRESOURCE(IDI_SMALL)); - - RegisterClassExW(&wcex); - return windowClass; - }(); - return windowClass; -} - -LRESULT CALLBACK AppWindow::WndProcStatic(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - if (auto app = (AppWindow*)GetWindowLongPtr(hWnd, GWLP_USERDATA)) - { - LRESULT result = 0; - if (app->HandleWindowMessage(hWnd, message, wParam, lParam, &result)) - { - return result; - } - } - return DefWindowProc(hWnd, message, wParam, lParam); -} - -// Handle Win32 window messages sent to the main window -bool AppWindow::HandleWindowMessage( - HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, LRESULT* result) -{ - // Give all components a chance to handle the message first. - for (auto& component : m_components) - { - if (component->HandleWindowMessage(hWnd, message, wParam, lParam, result)) - { - return true; - } - } - - switch (message) - { - case WM_SIZE: - { - // Don't resize the app or webview when the app is minimized - // let WM_SYSCOMMAND to handle it - if (lParam != 0) - { - ResizeEverything(); - return true; - } - } - break; - //! [DPIChanged] - case WM_DPICHANGED: - { - m_toolbar.UpdateDpiAndTextScale(); - RECT* const newWindowSize = reinterpret_cast(lParam); - SetWindowPos(hWnd, - nullptr, - newWindowSize->left, - newWindowSize->top, - newWindowSize->right - newWindowSize->left, - newWindowSize->bottom - newWindowSize->top, - SWP_NOZORDER | SWP_NOACTIVATE); - return true; - } - break; - //! [DPIChanged] - case WM_PAINT: - { - PAINTSTRUCT ps; - BeginPaint(hWnd, &ps); - EndPaint(hWnd, &ps); - return true; - } - break; - case s_runAsyncWindowMessage: - { - auto* task = reinterpret_cast*>(wParam); - (*task)(); - delete task; - return true; - } - break; - case WM_NCDESTROY: - { - int retValue = 0; - SetWindowLongPtr(hWnd, GWLP_USERDATA, NULL); - NotifyClosed(); - if (--s_appInstances == 0) - { - PostQuitMessage(retValue); - } - } - break; - //! [RestartManager] - case WM_QUERYENDSESSION: - { - // yes, we can shut down - // Register how we might be restarted - RegisterApplicationRestart(L"--restore", RESTART_NO_CRASH | RESTART_NO_HANG); - *result = TRUE; - return true; - } - break; - case WM_ENDSESSION: - { - if (wParam == TRUE) - { - // save app state and exit. - PostQuitMessage(0); - return true; - } - } - break; - //! [RestartManager] - case WM_KEYDOWN: - { - // If bit 30 is set, it means the WM_KEYDOWN message is autorepeated. - // We want to ignore it in that case. - if (!(lParam & (1 << 30))) - { - if (auto action = GetAcceleratorKeyFunction((UINT)wParam)) - { - action(); - return true; - } - } - } - break; - case WM_COMMAND: - { - return ExecuteWebViewCommands(wParam, lParam) || ExecuteAppCommands(wParam, lParam); - } - break; - } - return false; -} - -// Handle commands related to the WebView. -// This will do nothing if the WebView is not initialized. -bool AppWindow::ExecuteWebViewCommands(WPARAM wParam, LPARAM lParam) -{ - if (!m_webView) - return false; - switch (LOWORD(wParam)) - { - case IDM_GET_BROWSER_VERSION_AFTER_CREATION: - { - //! [GetBrowserVersionString] - wil::unique_cotaskmem_string version_info; - m_webViewEnvironment->get_BrowserVersionString(&version_info); - MessageBox( - m_mainWindow, version_info.get(), L"Browser Version Info After WebView Creation", - MB_OK); - //! [GetBrowserVersionString] - return true; - } - case IDM_CLOSE_WEBVIEW: - { - CloseWebView(); - return true; - } - case IDM_CLOSE_WEBVIEW_CLEANUP: - { - CloseWebView(true); - return true; - } - case IDM_SCENARIO_POST_WEB_MESSAGE: - { - NewComponent(this); - return true; - } - case IDM_SCENARIO_ADD_HOST_OBJECT: - { - NewComponent(this); - return true; - } - case IDM_SCENARIO_WEB_VIEW_EVENT_MONITOR: - { - NewComponent(this); - return true; - } - case IDM_SCENARIO_JAVA_SCRIPT: - { - WCHAR c_scriptPath[] = L"ScenarioJavaScriptDebugIndex.html"; - std::wstring m_scriptUri = GetLocalUri(c_scriptPath); - CHECK_FAILURE(m_webView->Navigate(m_scriptUri.c_str())); - return true; - } - case IDM_SCENARIO_TYPE_SCRIPT: - { - WCHAR c_scriptPath[] = L"ScenarioTypeScriptDebugIndex.html"; - std::wstring m_scriptUri = GetLocalUri(c_scriptPath); - CHECK_FAILURE(m_webView->Navigate(m_scriptUri.c_str())); - } - } - return false; -} -// Handle commands not related to the WebView, which will work even if the WebView -// is not currently initialized. -bool AppWindow::ExecuteAppCommands(WPARAM wParam, LPARAM lParam) -{ - switch (LOWORD(wParam)) - { - case IDM_ABOUT: - DialogBox(g_hInstance, MAKEINTRESOURCE(IDD_ABOUTBOX), m_mainWindow, About); - return true; - case IDM_GET_BROWSER_VERSION_BEFORE_CREATION: - { - wil::unique_cotaskmem_string version_info; - GetAvailableCoreWebView2BrowserVersionString(nullptr, &version_info); - MessageBox( - m_mainWindow, version_info.get(), L"Browser Version Info Before WebView Creation", - MB_OK); - return true; - } - case IDM_EXIT: - CloseAppWindow(); - return true; - case IDM_CREATION_MODE_WINDOWED: - case IDM_CREATION_MODE_VISUAL_DCOMP: - case IDM_CREATION_MODE_TARGET_DCOMP: -#ifdef USE_WEBVIEW2_WIN10 - case IDM_CREATION_MODE_VISUAL_WINCOMP: -#endif - m_creationModeId = LOWORD(wParam); - UpdateCreationModeMenu(); - return true; - case IDM_REINIT: - InitializeWebView(); - return true; - case IDM_TOGGLE_FULLSCREEN_ALLOWED: - { - m_fullScreenAllowed = !m_fullScreenAllowed; - MessageBox( - nullptr, - (std::wstring(L"Fullscreen is now ") + - (m_fullScreenAllowed ? L"allowed" : L"disallowed")) - .c_str(), - L"", MB_OK); - return true; - } - case IDM_NEW_WINDOW: - new AppWindow(m_creationModeId); - return true; - case IDM_NEW_THREAD: - CreateNewThread(m_creationModeId); - return true; - case IDM_SET_LANGUAGE: - ChangeLanguage(); - return true; - case IDM_TOGGLE_AAD_SSO: - ToggleAADSSO(); - return true; - } - return false; -} - -// Prompt the user for a new language string -void AppWindow::ChangeLanguage() -{ - TextInputDialog dialog( - GetMainWindow(), L"Language", L"Language:", - L"Enter a language to use for WebView, or leave blank to restore default.", - m_language.empty() ? L"zh-cn" : m_language.c_str()); - if (dialog.confirmed) - { - m_language = (dialog.input); - } -} - -// Toggle AAD SSO enabled -void AppWindow::ToggleAADSSO() -{ - m_AADSSOEnabled = !m_AADSSOEnabled; - MessageBox( - nullptr, - m_AADSSOEnabled ? L"AAD single sign on will be enabled for new WebView " - L"created after all webviews are closed." : - L"AAD single sign on will be disabled for new WebView" - L" created after all webviews are closed.", - L"AAD SSO change", - MB_OK); -} - -// Message handler for about dialog. -INT_PTR CALLBACK AppWindow::About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) -{ - switch (message) - { - case WM_INITDIALOG: - return (INT_PTR)TRUE; - - case WM_COMMAND: - if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) - { - EndDialog(hDlg, LOWORD(wParam)); - return (INT_PTR)TRUE; - } - break; - } - return (INT_PTR)FALSE; -} - -// Decide what to do when an accelerator key is pressed. Instead of immediately performing -// the action, we hand it to the caller so they can decide whether to run it right away -// or running it asynchronously. Will return nullptr if there is no action for the key. -std::function AppWindow::GetAcceleratorKeyFunction(UINT key) -{ - if (GetKeyState(VK_CONTROL) < 0) - { - switch (key) - { - case 'N': - return [this] { new AppWindow(m_creationModeId); }; - case 'Q': - return [this] { CloseAppWindow(); }; - case 'S': - return [this] { - if (auto file = GetComponent()) - { - file->SaveScreenshot(); - } - }; - case 'T': - return [this] { CreateNewThread(m_creationModeId); }; - case 'W': - return [this] { CloseWebView(); }; - } - } - return nullptr; -} - -//! [CreateCoreWebView2Controller] -// Create or recreate the WebView and its environment. -void AppWindow::InitializeWebView() -{ - // To ensure browser switches get applied correctly, we need to close - // the existing WebView. This will result in a new browser process - // getting created which will apply the browser switches. - CloseWebView(); - m_dcompDevice = nullptr; -#ifdef USE_WEBVIEW2_WIN10 - m_wincompCompositor = nullptr; -#endif - LPCWSTR subFolder = nullptr; - - if (m_creationModeId == IDM_CREATION_MODE_VISUAL_DCOMP || - m_creationModeId == IDM_CREATION_MODE_TARGET_DCOMP) - { - HRESULT hr = DCompositionCreateDevice2(nullptr, IID_PPV_ARGS(&m_dcompDevice)); - if (!SUCCEEDED(hr)) - { - MessageBox( - m_mainWindow, - L"Attempting to create WebView using DComp Visual is not supported.\r\n" - "DComp device creation failed.\r\n" - "Current OS may not support DComp.", - L"Create with Windowless DComp Visual Failed", MB_OK); - return; - } - } -#ifdef USE_WEBVIEW2_WIN10 - else if (m_creationModeId == IDM_CREATION_MODE_VISUAL_WINCOMP) - { - HRESULT hr = TryCreateDispatcherQueue(); - if (!SUCCEEDED(hr)) - { - MessageBox( - m_mainWindow, - L"Attempting to create WebView using WinComp Visual is not supported.\r\n" - "WinComp compositor creation failed.\r\n" - "Current OS may not support WinComp.", - L"Create with Windowless WinComp Visual Failed", MB_OK); - return; - } - m_wincompCompositor = winrtComp::Compositor(); - } -#endif - //! [CreateCoreWebView2EnvironmentWithOptions] - auto options = Microsoft::WRL::Make(); - CHECK_FAILURE(options->put_AllowSingleSignOnUsingOSPrimaryAccount( - m_AADSSOEnabled ? TRUE : FALSE)); - if (!m_language.empty()) - CHECK_FAILURE(options->put_Language(m_language.c_str())); - HRESULT hr = CreateCoreWebView2EnvironmentWithOptions( - subFolder, nullptr, options.Get(), - Callback( - this, &AppWindow::OnCreateEnvironmentCompleted) - .Get()); - //! [CreateCoreWebView2EnvironmentWithOptions] - if (!SUCCEEDED(hr)) - { - if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) - { - MessageBox( - m_mainWindow, - L"Couldn't find Edge installation. " - "Do you have a version installed that's compatible with this " - "WebView2 SDK version?", - nullptr, MB_OK); - } - else - { - ShowFailure(hr, L"Failed to create webview environment"); - } - } -} -// This is the callback passed to CreateWebViewEnvironmentWithOptions. -// Here we simply create the WebView. -HRESULT AppWindow::OnCreateEnvironmentCompleted( - HRESULT result, ICoreWebView2Environment* environment) -{ - CHECK_FAILURE(result); - m_webViewEnvironment = environment; - - auto webViewExperimentalEnvironment = - m_webViewEnvironment.try_query(); -#ifdef USE_WEBVIEW2_WIN10 - if (webViewExperimentalEnvironment && (m_dcompDevice || m_wincompCompositor)) -#else - if (webViewExperimentalEnvironment && m_dcompDevice) -#endif - { - CHECK_FAILURE(webViewExperimentalEnvironment->CreateCoreWebView2CompositionController( - m_mainWindow, - Callback< - ICoreWebView2ExperimentalCreateCoreWebView2CompositionControllerCompletedHandler>( - [this]( - HRESULT result, - ICoreWebView2ExperimentalCompositionController* compositionController) -> HRESULT { - auto controller = - wil::com_ptr(compositionController) - .query(); - return OnCreateCoreWebView2ControllerCompleted(result, controller.get()); - }) - .Get())); - } - else - { - CHECK_FAILURE(m_webViewEnvironment->CreateCoreWebView2Controller( - m_mainWindow, Callback( - this, &AppWindow::OnCreateCoreWebView2ControllerCompleted) - .Get())); - } - - return S_OK; -} -//! [CreateCoreWebView2Controller] - -// This is the callback passed to CreateCoreWebView2Controller. Here we initialize all WebView-related -// state and register most of our event handlers with the WebView. -HRESULT AppWindow::OnCreateCoreWebView2ControllerCompleted(HRESULT result, ICoreWebView2Controller* controller) -{ - if (result == S_OK) - { - m_controller = controller; - wil::com_ptr coreWebView2; - CHECK_FAILURE(m_controller->get_CoreWebView2(&coreWebView2)); - // We should check for failure here because if this app is using a newer - // SDK version compared to the install of the Edge browser, the Edge - // browser might not have support for the latest version of the - // ICoreWebView2_N interface. - coreWebView2.query_to(&m_webView); - // Create components. These will be deleted when the WebView is closed. - NewComponent(this); - NewComponent(this); - NewComponent(this); - NewComponent( - this, m_webViewEnvironment.get(), m_oldSettingsComponent.get()); - m_oldSettingsComponent = nullptr; - NewComponent( - this, m_dcompDevice.get(), -#ifdef USE_WEBVIEW2_WIN10 - m_wincompCompositor, -#endif - m_creationModeId == IDM_CREATION_MODE_TARGET_DCOMP); - NewComponent(this, &m_toolbar); - - // We have a few of our own event handlers to register here as well - RegisterEventHandlers(); - - // Set the initial size of the WebView - ResizeEverything(); - - if (m_onWebViewFirstInitialized) - { - m_onWebViewFirstInitialized(); - m_onWebViewFirstInitialized = nullptr; - } - - if (!m_initialUri.empty()) - { - m_webView->Navigate(m_initialUri.c_str()); - } - } - else - { - ShowFailure(result, L"Failed to create webview"); - } - return S_OK; -} -void AppWindow::ReinitializeWebView() -{ - // Save the settings component from being deleted when the WebView is closed, so we can - // copy its properties to the next settings component. - m_oldSettingsComponent = MoveComponent(); - InitializeWebView(); -} - -void AppWindow::ReinitializeWebViewWithNewBrowser() -{ - // Save the settings component from being deleted when the WebView is closed, so we can - // copy its properties to the next settings component. - m_oldSettingsComponent = MoveComponent(); - - // Use the reference to the web view before we close it - UINT webviewProcessId = 0; - m_webView->get_BrowserProcessId(&webviewProcessId); - - // We need to close the current webviews and wait for the browser_process to exit - // This is so the new webviews don't use the old browser exe - CloseWebView(); - - // Make sure the browser process inside webview is closed - ProcessComponent::EnsureProcessIsClosed(webviewProcessId, 2000); - - InitializeWebView(); -} - -void AppWindow::RestartApp() -{ - // Use the reference to the web view before we close the app window - UINT webviewProcessId = 0; - m_webView->get_BrowserProcessId(&webviewProcessId); - - // To restart the app completely, first we close the current App Window - CloseAppWindow(); - - // Make sure the browser process inside webview is closed - ProcessComponent::EnsureProcessIsClosed(webviewProcessId, 2000); - - // Get the command line arguments used to start this app - // so we can re-create the process with them - LPWSTR args = GetCommandLineW(); - - STARTUPINFOW startup_info = {0}; - startup_info.cb = sizeof(startup_info); - PROCESS_INFORMATION temp_process_info = {}; - // Start a new process - if (!::CreateProcess( - nullptr, args, - nullptr, // default process attributes - nullptr, // default thread attributes - FALSE, // do not inherit handles - 0, - nullptr, // no environment - nullptr, // default current directory - &startup_info, &temp_process_info)) - { - // Log some error information if desired - } - - // Terminate this current process - ::exit(0); -} - -void AppWindow::RegisterEventHandlers() -{ - //! [ContainsFullScreenElementChanged] - // Register a handler for the ContainsFullScreenChanged event. - CHECK_FAILURE(m_webView->add_ContainsFullScreenElementChanged( - Callback( - [this](ICoreWebView2* sender, IUnknown* args) -> HRESULT { - if (m_fullScreenAllowed) - { - CHECK_FAILURE( - sender->get_ContainsFullScreenElement(&m_containsFullscreenElement)); - if (m_containsFullscreenElement) - { - EnterFullScreen(); - } - else - { - ExitFullScreen(); - } - } - return S_OK; - }) - .Get(), - nullptr)); - //! [ContainsFullScreenElementChanged] - - //! [NewWindowRequested] - // Register a handler for the NewWindowRequested event. - // This handler will defer the event, create a new app window, and then once the - // new window is ready, it'll provide that new window's WebView as the response to - // the request. - CHECK_FAILURE(m_webView->add_NewWindowRequested( - Callback( - [this](ICoreWebView2* sender, ICoreWebView2NewWindowRequestedEventArgs* args) { - wil::com_ptr deferral; - CHECK_FAILURE(args->GetDeferral(&deferral)); - AppWindow* newAppWindow; - - wil::com_ptr windowFeatures; - CHECK_FAILURE(args->get_WindowFeatures(&windowFeatures)); - - RECT windowRect = {0}; - UINT32 left = 0; - UINT32 top = 0; - UINT32 height = 0; - UINT32 width = 0; - BOOL shouldHaveToolbar = true; - - BOOL hasPosition = FALSE; - BOOL hasSize = FALSE; - CHECK_FAILURE(windowFeatures->HasPosition(&hasPosition)); - CHECK_FAILURE(windowFeatures->HasSize(&hasSize)); - - bool useDefaultWindow = true; - - if (!!hasPosition && !!hasSize) - { - CHECK_FAILURE(windowFeatures->get_Left(&left)); - CHECK_FAILURE(windowFeatures->get_Top(&top)); - CHECK_FAILURE(windowFeatures->get_Height(&height)); - CHECK_FAILURE(windowFeatures->get_Width(&width)); - useDefaultWindow = false; - } - CHECK_FAILURE(windowFeatures->get_Toolbar(&shouldHaveToolbar)); - - windowRect.left = left; - windowRect.right = left + (width < s_minNewWindowSize ? s_minNewWindowSize : width); - windowRect.top = top; - windowRect.bottom = top + (height < s_minNewWindowSize ? s_minNewWindowSize : height); - - if (!useDefaultWindow) - { - newAppWindow = new AppWindow(m_creationModeId, L"", false, nullptr, true, windowRect, !!shouldHaveToolbar); - } - else - { - newAppWindow = new AppWindow(m_creationModeId, L""); - } - newAppWindow->m_isPopupWindow = true; - newAppWindow->m_onWebViewFirstInitialized = [args, deferral, newAppWindow]() { - CHECK_FAILURE(args->put_NewWindow(newAppWindow->m_webView.get())); - CHECK_FAILURE(args->put_Handled(TRUE)); - CHECK_FAILURE(deferral->Complete()); - }; - - return S_OK; - }) - .Get(), - nullptr)); - //! [NewWindowRequested] - - //! [WindowCloseRequested] - // Register a handler for the WindowCloseRequested event. - // This handler will close the app window if it is not the main window. - CHECK_FAILURE(m_webView->add_WindowCloseRequested( - Callback([this]( - ICoreWebView2* sender, - IUnknown* args) { - if (m_isPopupWindow) - { - CloseAppWindow(); - } - return S_OK; - }).Get(), - nullptr)); - //! [WindowCloseRequested] - - //! [NewBrowserVersionAvailable] - // After the environment is successfully created, - // register a handler for the NewBrowserVersionAvailable event. - // This handler tells when there is a new Edge version available on the machine. - CHECK_FAILURE(m_webViewEnvironment->add_NewBrowserVersionAvailable( - Callback( - [this](ICoreWebView2Environment* sender, IUnknown* args) -> HRESULT { - std::wstring message = L"We detected there is a new version for the browser."; - if (m_webView) - { - message += L"Do you want to restart the app? \n\n"; - message += L"Click No if you only want to re-create the webviews. \n"; - message += L"Click Cancel for no action. \n"; - } - int response = MessageBox( - m_mainWindow, message.c_str(), L"New available version", - m_webView ? MB_YESNOCANCEL : MB_OK); - - if (response == IDYES) - { - RestartApp(); - } - else if (response == IDNO) - { - ReinitializeWebViewWithNewBrowser(); - } - else - { - // do nothing - } - - return S_OK; - }) - .Get(), - nullptr)); - //! [NewBrowserVersionAvailable] -} - -// Updates the sizing and positioning of everything in the window. -void AppWindow::ResizeEverything() -{ - RECT availableBounds = {0}; - GetClientRect(m_mainWindow, &availableBounds); - - if (!m_containsFullscreenElement) - { - availableBounds = m_toolbar.Resize(availableBounds); - } - - if (auto view = GetComponent()) - { - view->SetBounds(availableBounds); - } -} - -//! [Close] -// Close the WebView and deinitialize related state. This doesn't close the app window. -void AppWindow::CloseWebView(bool cleanupUserDataFolder) -{ - DeleteAllComponents(); - if (m_controller) - { - m_controller->Close(); - m_controller = nullptr; - m_webView = nullptr; - } - m_webViewEnvironment = nullptr; - if (cleanupUserDataFolder) - { - // For non-UWP apps, the default user data folder {Executable File Name}.WebView2 - // is in the same directory next to the app executable. If end - // developers specify userDataFolder during WebView environment - // creation, they would need to pass in that explicit value here. - // For more information about userDataFolder: - // https://docs.microsoft.com/microsoft-edge/webview2/reference/win32/webview2-idl#createcorewebview2environmentwithoptions - WCHAR userDataFolder[MAX_PATH] = L""; - // Obtain the absolute path for relative paths that include "./" or "../" - _wfullpath( - userDataFolder, GetLocalPath(L".WebView2", true).c_str(), MAX_PATH); - std::wstring userDataFolderPath(userDataFolder); - - std::wstring message = L"Are you sure you want to clean up the user data folder at\n"; - message += userDataFolderPath; - message += L"\n?\nWarning: This action is not reversible.\n\n"; - message += L"Click No if there are other open WebView instances.\n"; - - if (MessageBox(m_mainWindow, message.c_str(), L"Cleanup User Data Folder", MB_YESNO) == - IDYES) - { - CHECK_FAILURE(DeleteFileRecursive(userDataFolderPath)); - } - } -} -//! [Close] - -HRESULT AppWindow::DeleteFileRecursive(std::wstring path) -{ - wil::com_ptr fileOperation; - CHECK_FAILURE( - CoCreateInstance(CLSID_FileOperation, NULL, CLSCTX_ALL, IID_PPV_ARGS(&fileOperation))); - - // Turn off all UI from being shown to the user during the operation. - CHECK_FAILURE(fileOperation->SetOperationFlags(FOF_NO_UI)); - - wil::com_ptr userDataFolder; - CHECK_FAILURE( - SHCreateItemFromParsingName(path.c_str(), NULL, IID_PPV_ARGS(&userDataFolder))); - - // Add the operation - CHECK_FAILURE(fileOperation->DeleteItem(userDataFolder.get(), NULL)); - CHECK_FAILURE(userDataFolder->Release()); - - // Perform the operation to delete the directory - CHECK_FAILURE(fileOperation->PerformOperations()); - - CHECK_FAILURE(fileOperation->Release()); - CoUninitialize(); - return S_OK; -} - -void AppWindow::CloseAppWindow() -{ - CloseWebView(); - DestroyWindow(m_mainWindow); -} - -void AppWindow::DeleteComponent(ComponentBase* component) -{ - for (auto iter = m_components.begin(); iter != m_components.end(); iter++) - { - if (iter->get() == component) - { - m_components.erase(iter); - return; - } - } -} - -void AppWindow::DeleteAllComponents() -{ - // Delete components in reverse order of initialization. - while (!m_components.empty()) - { - m_components.pop_back(); - } -} - -template std::unique_ptr AppWindow::MoveComponent() -{ - for (auto iter = m_components.begin(); iter != m_components.end(); iter++) - { - if (dynamic_cast(iter->get())) - { - auto wanted = reinterpret_cast&&>(std::move(*iter)); - m_components.erase(iter); - return std::move(wanted); - } - } - return nullptr; -} - -void AppWindow::SetTitleText(PCWSTR titleText) -{ - SetWindowText(m_mainWindow, titleText); -} - -RECT AppWindow::GetWindowBounds() -{ - RECT hwndBounds = {0}; - GetClientRect(m_mainWindow, &hwndBounds); - return hwndBounds; -} - -std::wstring AppWindow::GetLocalPath(std::wstring relativePath, bool keep_exe_path) -{ - WCHAR rawPath[MAX_PATH]; - GetModuleFileNameW(g_hInstance, rawPath, MAX_PATH); - std::wstring path(rawPath); - if (keep_exe_path) - { - path.append(relativePath); - } - else - { - std::size_t index = path.find_last_of(L"\\") + 1; - path.replace(index, path.length(), relativePath); - } - return path; -} -std::wstring AppWindow::GetLocalUri(std::wstring relativePath) -{ - std::wstring path = GetLocalPath(relativePath, false); - - wil::com_ptr uri; - CHECK_FAILURE(CreateUri(path.c_str(), Uri_CREATE_ALLOW_IMPLICIT_FILE_SCHEME, 0, &uri)); - - wil::unique_bstr uriBstr; - CHECK_FAILURE(uri->GetAbsoluteUri(&uriBstr)); - return std::wstring(uriBstr.get()); -} - -void AppWindow::RunAsync(std::function callback) -{ - auto* task = new std::function(callback); - PostMessage(m_mainWindow, s_runAsyncWindowMessage, reinterpret_cast(task), 0); -} - -void AppWindow::EnterFullScreen() -{ - DWORD style = GetWindowLong(m_mainWindow, GWL_STYLE); - MONITORINFO monitor_info = {sizeof(monitor_info)}; - m_hMenu = ::GetMenu(m_mainWindow); - ::SetMenu(m_mainWindow, nullptr); - if (GetWindowRect(m_mainWindow, &m_previousWindowRect) && - GetMonitorInfo( - MonitorFromWindow(m_mainWindow, MONITOR_DEFAULTTOPRIMARY), &monitor_info)) - { - SetWindowLong(m_mainWindow, GWL_STYLE, style & ~WS_OVERLAPPEDWINDOW); - SetWindowPos( - m_mainWindow, HWND_TOP, monitor_info.rcMonitor.left, monitor_info.rcMonitor.top, - monitor_info.rcMonitor.right - monitor_info.rcMonitor.left, - monitor_info.rcMonitor.bottom - monitor_info.rcMonitor.top, - SWP_NOOWNERZORDER | SWP_FRAMECHANGED); - } -} - -void AppWindow::ExitFullScreen() -{ - DWORD style = GetWindowLong(m_mainWindow, GWL_STYLE); - ::SetMenu(m_mainWindow, m_hMenu); - SetWindowLong(m_mainWindow, GWL_STYLE, style | WS_OVERLAPPEDWINDOW); - SetWindowPos( - m_mainWindow, NULL, m_previousWindowRect.left, m_previousWindowRect.top, - m_previousWindowRect.right - m_previousWindowRect.left, - m_previousWindowRect.bottom - m_previousWindowRect.top, - SWP_NOOWNERZORDER | SWP_FRAMECHANGED); -} - -// We have our own implementation of DCompositionCreateDevice2 that dynamically -// loads dcomp.dll to create the device. Not having a static dependency on dcomp.dll -// enables the sample app to run on versions of Windows that don't support dcomp. -HRESULT AppWindow::DCompositionCreateDevice2(IUnknown* renderingDevice, REFIID riid, void** ppv) -{ - HRESULT hr = E_FAIL; - static decltype(::DCompositionCreateDevice2)* fnCreateDCompDevice2 = nullptr; - if (fnCreateDCompDevice2 == nullptr) - { - HMODULE hmod = ::LoadLibraryEx(L"dcomp.dll", nullptr, 0); - if (hmod != nullptr) - { - fnCreateDCompDevice2 = reinterpret_cast( - ::GetProcAddress(hmod, "DCompositionCreateDevice2")); - } - } - if (fnCreateDCompDevice2 != nullptr) - { - hr = fnCreateDCompDevice2(renderingDevice, riid, ppv); - } - return hr; -} - -// WinRT APIs cannot run without a DispatcherQueue. This helper function creates a -// DispatcherQueueController (which instantiates a DispatcherQueue under the covers) that will -// manage tasks for the WinRT APIs. The DispatcherQueue implementation lives in -// CoreMessaging.dll Similar to dcomp.dll, we load CoreMessaging.dll dynamically so the sample -// app can run on versions of windows that don't have CoreMessaging. -HRESULT AppWindow::TryCreateDispatcherQueue() -{ - namespace winSystem = winrt::Windows::System; - - HRESULT hr = S_OK; - thread_local winSystem::DispatcherQueueController dispatcherQueueController{ nullptr }; - - if (dispatcherQueueController == nullptr) - { - hr = E_FAIL; - static decltype(::CreateDispatcherQueueController)* fnCreateDispatcherQueueController = - nullptr; - if (fnCreateDispatcherQueueController == nullptr) - { - HMODULE hmod = ::LoadLibraryEx(L"CoreMessaging.dll", nullptr, 0); - if (hmod != nullptr) - { - fnCreateDispatcherQueueController = - reinterpret_cast( - ::GetProcAddress(hmod, "CreateDispatcherQueueController")); - } - } - if (fnCreateDispatcherQueueController != nullptr) - { - winSystem::DispatcherQueueController controller{ nullptr }; - DispatcherQueueOptions options - { - sizeof(DispatcherQueueOptions), - DQTYPE_THREAD_CURRENT, - DQTAT_COM_STA - }; - hr = fnCreateDispatcherQueueController( - options, reinterpret_cast( - winrt::put_abi(controller))); - dispatcherQueueController = controller; - } - } - - return hr; -} - -#ifdef USE_WEBVIEW2_WIN10 -//! [TextScaleChanged2] -void AppWindow::OnTextScaleChanged( - winrt::Windows::UI::ViewManagement::UISettings const& settings, - winrt::Windows::Foundation::IInspectable const& args) -{ - RunAsync([this] { - m_toolbar.UpdateDpiAndTextScale(); - }); -} -//! [TextScaleChanged2] -#endif -void AppWindow::UpdateCreationModeMenu() -{ - HMENU hMenu = GetMenu(m_mainWindow); - CheckMenuRadioItem( - hMenu, - IDM_CREATION_MODE_WINDOWED, -#ifdef USE_WEBVIEW2_WIN10 - IDM_CREATION_MODE_VISUAL_WINCOMP, -#else - IDM_CREATION_MODE_TARGET_DCOMP, -#endif - m_creationModeId, - MF_BYCOMMAND); -} - -double AppWindow::GetDpiScale() -{ - return DpiUtil::GetDpiForWindow(m_mainWindow) * 1.0f / USER_DEFAULT_SCREEN_DPI; -} - -#ifdef USE_WEBVIEW2_WIN10 -double AppWindow::GetTextScale() -{ - return m_uiSettings ? m_uiSettings.TextScaleFactor() : 1.0f; -} -#endif - -void AppWindow::AddRef() -{ - InterlockedIncrement((LONG *)&m_refCount); -} - -void AppWindow::Release() -{ - uint32_t refCount = InterlockedDecrement((LONG *)&m_refCount); - if (refCount == 0) - { - delete this; - } -} - -void AppWindow::NotifyClosed() -{ - m_isClosed = true; -} - -void AppWindow::InstallComplete(int return_code) -{ - if (!m_isClosed) - { - if (return_code == 0) - { - RunAsync([this] { - InitializeWebView(); - }); - } - else if (return_code == 1) - { - MessageBox(m_mainWindow, L"WebView Runtime failed to Install", L"WebView Runtime Installation status", MB_OK); - } - else if (return_code == 2) - { - MessageBox(m_mainWindow, L"WebView Bootstrapper failled to download", L"WebView Bootstrapper Download status", MB_OK); - } - } -} +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "stdafx.h" + +#include "AppWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include "App.h" +#include "AppStartPage.h" +#include "CheckFailure.h" +#include "ControlComponent.h" +#include "DpiUtil.h" +#include "FileComponent.h" +#include "ProcessComponent.h" +#include "Resource.h" +#include "ScenarioAddHostObject.h" +#include "ScenarioAuthentication.h" +#include "ScenarioCookieManagement.h" +#include "ScenarioDOMContentLoaded.h" +#include "ScenarioNavigateWithWebResourceRequest.h" +#include "ScenarioWebMessage.h" +#include "ScenarioWebViewEventMonitor.h" +#include "ScriptComponent.h" +#include "SettingsComponent.h" +#include "TextInputDialog.h" +#include "ViewComponent.h" +using namespace Microsoft::WRL; +static constexpr size_t s_maxLoadString = 100; +static constexpr UINT s_runAsyncWindowMessage = WM_APP; + +static thread_local size_t s_appInstances = 0; +// The minimum height and width for Window Features. +// See https://developer.mozilla.org/en-US/docs/Web/API/Window/open#Size +static constexpr int s_minNewWindowSize = 100; + +// Run Download and Install in another thread so we don't block the UI thread +DWORD WINAPI DownloadAndInstallWV2RT(_In_ LPVOID lpParameter) +{ + AppWindow* appWindow = (AppWindow*) lpParameter; + + int returnCode = 2; // Download failed + // Use fwlink to download WebView2 Bootstrapper at runtime and invoke installation + // Broken/Invalid Https Certificate will fail to download + // Use of the download link below is governed by the below terms. You may acquire the link for your use at https://developer.microsoft.com/microsoft-edge/webview2/. + // Microsoft owns all legal right, title, and interest in and to the WebView2 Runtime Bootstrapper ("Software") and related documentation, + // including any intellectual property in the Software. + // You must acquire all code, including any code obtained from a Microsoft URL, under a separate license directly from Microsoft, including a Microsoft download site + // (e.g., https://developer.microsoft.com/microsoft-edge/webview2/). + HRESULT hr = URLDownloadToFile(NULL, L"https://go.microsoft.com/fwlink/p/?LinkId=2124703", L".\\MicrosoftEdgeWebview2Setup.exe", 0, 0); + if (hr == S_OK) + { + // Either Package the WebView2 Bootstrapper with your app or download it using fwlink + // Then invoke install at Runtime. + SHELLEXECUTEINFO shExInfo = {0}; + shExInfo.cbSize = sizeof(shExInfo); + shExInfo.fMask = SEE_MASK_NOASYNC; + shExInfo.hwnd = 0; + shExInfo.lpVerb = L"runas"; + shExInfo.lpFile = L"MicrosoftEdgeWebview2Setup.exe"; + shExInfo.lpParameters = L" /silent /install"; + shExInfo.lpDirectory = 0; + shExInfo.nShow = 0; + shExInfo.hInstApp = 0; + + if (ShellExecuteEx(&shExInfo)) + { + returnCode = 0; // Install successfull + } + else + { + returnCode = 1; // Install failed + } + } + + appWindow->InstallComplete(returnCode); + appWindow->Release(); + return returnCode; +} + +// Creates a new window which is a copy of the entire app, but on the same thread. +AppWindow::AppWindow( + UINT creationModeId, + std::wstring initialUri, + bool isMainWindow, + std::function webviewCreatedCallback, + bool customWindowRect, + RECT windowRect, + bool shouldHaveToolbar) + : m_creationModeId(creationModeId), + m_initialUri(initialUri), + m_onWebViewFirstInitialized(webviewCreatedCallback) +{ + // Initialize COM as STA. + CHECK_FAILURE(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)); + + ++s_appInstances; + + WCHAR szTitle[s_maxLoadString]; // The title bar text + LoadStringW(g_hInstance, IDS_APP_TITLE, szTitle, s_maxLoadString); + + if (customWindowRect) + { + m_mainWindow = CreateWindowExW( + WS_EX_CONTROLPARENT, GetWindowClass(), szTitle, WS_OVERLAPPEDWINDOW, windowRect.left, + windowRect.top, windowRect.right-windowRect.left, windowRect.bottom-windowRect.top, nullptr, nullptr, g_hInstance, nullptr); + } + else + { + m_mainWindow = CreateWindowExW( + WS_EX_CONTROLPARENT, GetWindowClass(), szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, + 0, CW_USEDEFAULT, 0, nullptr, nullptr, g_hInstance, nullptr); + } + + SetWindowLongPtr(m_mainWindow, GWLP_USERDATA, (LONG_PTR)this); + +#ifdef USE_WEBVIEW2_WIN10 + //! [TextScaleChanged1] + if (winrt::try_get_activation_factory()) + { + m_uiSettings = winrt::Windows::UI::ViewManagement::UISettings(); + m_uiSettings.TextScaleFactorChanged({ this, &AppWindow::OnTextScaleChanged }); + } + //! [TextScaleChanged1] +#endif + + if (shouldHaveToolbar) + { + m_toolbar.Initialize(this); + } + + UpdateCreationModeMenu(); + ShowWindow(m_mainWindow, g_nCmdShow); + UpdateWindow(m_mainWindow); + + // If no WebVieRuntime installed, create new thread to do install/download. + // Otherwise just initialize webview. + wil::unique_cotaskmem_string version_info; + HRESULT hr = GetAvailableCoreWebView2BrowserVersionString(nullptr, &version_info); + if (hr == S_OK && version_info != nullptr) + { + RunAsync([this] { + InitializeWebView(); + }); + } + else + { + if (isMainWindow) { + AddRef(); + CreateThread(0, 0, DownloadAndInstallWV2RT, (void*) this, 0, 0); + } + else + { + MessageBox(m_mainWindow, L"WebView Runtime not installed", L"WebView Runtime Installation status", MB_OK); + } + } +} + +// Register the Win32 window class for the app window. +PCWSTR AppWindow::GetWindowClass() +{ + // Only do this once + static PCWSTR windowClass = [] { + static WCHAR windowClass[s_maxLoadString]; + LoadStringW(g_hInstance, IDC_WEBVIEW2APISAMPLE, windowClass, s_maxLoadString); + + WNDCLASSEXW wcex; + wcex.cbSize = sizeof(WNDCLASSEX); + + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = WndProcStatic; + wcex.cbClsExtra = 0; + wcex.cbWndExtra = 0; + wcex.hInstance = g_hInstance; + wcex.hIcon = LoadIcon(g_hInstance, MAKEINTRESOURCE(IDI_WEBVIEW2APISAMPLE)); + wcex.hCursor = LoadCursor(nullptr, IDC_ARROW); + wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_WEBVIEW2APISAMPLE); + wcex.lpszClassName = windowClass; + wcex.hIconSm = LoadIcon(g_hInstance, MAKEINTRESOURCE(IDI_SMALL)); + + RegisterClassExW(&wcex); + return windowClass; + }(); + return windowClass; +} + +LRESULT CALLBACK AppWindow::WndProcStatic(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + if (auto app = (AppWindow*)GetWindowLongPtr(hWnd, GWLP_USERDATA)) + { + LRESULT result = 0; + if (app->HandleWindowMessage(hWnd, message, wParam, lParam, &result)) + { + return result; + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} + +// Handle Win32 window messages sent to the main window +bool AppWindow::HandleWindowMessage( + HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, LRESULT* result) +{ + // Give all components a chance to handle the message first. + for (auto& component : m_components) + { + if (component->HandleWindowMessage(hWnd, message, wParam, lParam, result)) + { + return true; + } + } + + switch (message) + { + case WM_SIZE: + { + // Don't resize the app or webview when the app is minimized + // let WM_SYSCOMMAND to handle it + if (lParam != 0) + { + ResizeEverything(); + return true; + } + } + break; + //! [DPIChanged] + case WM_DPICHANGED: + { + m_toolbar.UpdateDpiAndTextScale(); + RECT* const newWindowSize = reinterpret_cast(lParam); + SetWindowPos(hWnd, + nullptr, + newWindowSize->left, + newWindowSize->top, + newWindowSize->right - newWindowSize->left, + newWindowSize->bottom - newWindowSize->top, + SWP_NOZORDER | SWP_NOACTIVATE); + return true; + } + break; + //! [DPIChanged] + case WM_PAINT: + { + PAINTSTRUCT ps; + BeginPaint(hWnd, &ps); + EndPaint(hWnd, &ps); + return true; + } + break; + case s_runAsyncWindowMessage: + { + auto* task = reinterpret_cast*>(wParam); + (*task)(); + delete task; + return true; + } + break; + case WM_NCDESTROY: + { + int retValue = 0; + SetWindowLongPtr(hWnd, GWLP_USERDATA, NULL); + NotifyClosed(); + if (--s_appInstances == 0) + { + PostQuitMessage(retValue); + } + } + break; + //! [RestartManager] + case WM_QUERYENDSESSION: + { + // yes, we can shut down + // Register how we might be restarted + RegisterApplicationRestart(L"--restore", RESTART_NO_CRASH | RESTART_NO_HANG); + *result = TRUE; + return true; + } + break; + case WM_ENDSESSION: + { + if (wParam == TRUE) + { + // save app state and exit. + PostQuitMessage(0); + return true; + } + } + break; + //! [RestartManager] + case WM_KEYDOWN: + { + // If bit 30 is set, it means the WM_KEYDOWN message is autorepeated. + // We want to ignore it in that case. + if (!(lParam & (1 << 30))) + { + if (auto action = GetAcceleratorKeyFunction((UINT)wParam)) + { + action(); + return true; + } + } + } + break; + case WM_COMMAND: + { + return ExecuteWebViewCommands(wParam, lParam) || ExecuteAppCommands(wParam, lParam); + } + break; + } + return false; +} + +// Handle commands related to the WebView. +// This will do nothing if the WebView is not initialized. +bool AppWindow::ExecuteWebViewCommands(WPARAM wParam, LPARAM lParam) +{ + if (!m_webView) + return false; + switch (LOWORD(wParam)) + { + case IDM_GET_BROWSER_VERSION_AFTER_CREATION: + { + //! [GetBrowserVersionString] + wil::unique_cotaskmem_string version_info; + m_webViewEnvironment->get_BrowserVersionString(&version_info); + MessageBox( + m_mainWindow, version_info.get(), L"Browser Version Info After WebView Creation", + MB_OK); + //! [GetBrowserVersionString] + return true; + } + case IDM_CLOSE_WEBVIEW: + { + CloseWebView(); + return true; + } + case IDM_CLOSE_WEBVIEW_CLEANUP: + { + CloseWebView(true); + return true; + } + case IDM_SCENARIO_POST_WEB_MESSAGE: + { + NewComponent(this); + return true; + } + case IDM_SCENARIO_ADD_HOST_OBJECT: + { + NewComponent(this); + return true; + } + case IDM_SCENARIO_WEB_VIEW_EVENT_MONITOR: + { + NewComponent(this); + return true; + } + case IDM_SCENARIO_JAVA_SCRIPT: + { + WCHAR c_scriptPath[] = L"ScenarioJavaScriptDebugIndex.html"; + std::wstring m_scriptUri = GetLocalUri(c_scriptPath); + CHECK_FAILURE(m_webView->Navigate(m_scriptUri.c_str())); + return true; + } + case IDM_SCENARIO_TYPE_SCRIPT: + { + WCHAR c_scriptPath[] = L"ScenarioTypeScriptDebugIndex.html"; + std::wstring m_scriptUri = GetLocalUri(c_scriptPath); + CHECK_FAILURE(m_webView->Navigate(m_scriptUri.c_str())); + } + case IDM_SCENARIO_AUTHENTICATION: + { + NewComponent(this); + + return true; + } + case IDM_SCENARIO_COOKIE_MANAGEMENT: + { + NewComponent(this); + return true; + } + case IDM_SCENARIO_DOM_CONTENT_LOADED: + { + NewComponent(this); + return true; + } + case IDM_SCENARIO_NAVIGATEWITHWEBRESOURCEREQUEST: + { + NewComponent(this); + return true; + } + } + return false; +} + +// Handle commands not related to the WebView, which will work even if the WebView +// is not currently initialized. +bool AppWindow::ExecuteAppCommands(WPARAM wParam, LPARAM lParam) +{ + switch (LOWORD(wParam)) + { + case IDM_ABOUT: + DialogBox(g_hInstance, MAKEINTRESOURCE(IDD_ABOUTBOX), m_mainWindow, About); + return true; + case IDM_GET_BROWSER_VERSION_BEFORE_CREATION: + { + wil::unique_cotaskmem_string version_info; + GetAvailableCoreWebView2BrowserVersionString(nullptr, &version_info); + MessageBox( + m_mainWindow, version_info.get(), L"Browser Version Info Before WebView Creation", + MB_OK); + return true; + } + case IDM_EXIT: + CloseAppWindow(); + return true; + case IDM_CREATION_MODE_WINDOWED: + case IDM_CREATION_MODE_VISUAL_DCOMP: + case IDM_CREATION_MODE_TARGET_DCOMP: +#ifdef USE_WEBVIEW2_WIN10 + case IDM_CREATION_MODE_VISUAL_WINCOMP: +#endif + m_creationModeId = LOWORD(wParam); + UpdateCreationModeMenu(); + return true; + case IDM_REINIT: + InitializeWebView(); + return true; + case IDM_TOGGLE_FULLSCREEN_ALLOWED: + { + m_fullScreenAllowed = !m_fullScreenAllowed; + MessageBox( + nullptr, + (std::wstring(L"Fullscreen is now ") + + (m_fullScreenAllowed ? L"allowed" : L"disallowed")) + .c_str(), + L"", MB_OK); + return true; + } + case IDM_NEW_WINDOW: + new AppWindow(m_creationModeId); + return true; + case IDM_NEW_THREAD: + CreateNewThread(m_creationModeId); + return true; + case IDM_SET_LANGUAGE: + ChangeLanguage(); + return true; + case IDM_TOGGLE_AAD_SSO: + ToggleAADSSO(); + return true; + } + return false; +} + +// Prompt the user for a new language string +void AppWindow::ChangeLanguage() +{ + TextInputDialog dialog( + GetMainWindow(), L"Language", L"Language:", + L"Enter a language to use for WebView, or leave blank to restore default.", + m_language.empty() ? L"zh-cn" : m_language.c_str()); + if (dialog.confirmed) + { + m_language = (dialog.input); + } +} + +// Toggle AAD SSO enabled +void AppWindow::ToggleAADSSO() +{ + m_AADSSOEnabled = !m_AADSSOEnabled; + MessageBox( + nullptr, + m_AADSSOEnabled ? L"AAD single sign on will be enabled for new WebView " + L"created after all webviews are closed." : + L"AAD single sign on will be disabled for new WebView" + L" created after all webviews are closed.", + L"AAD SSO change", + MB_OK); +} + +// Message handler for about dialog. +INT_PTR CALLBACK AppWindow::About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) +{ + switch (message) + { + case WM_INITDIALOG: + return (INT_PTR)TRUE; + + case WM_COMMAND: + if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) + { + EndDialog(hDlg, LOWORD(wParam)); + return (INT_PTR)TRUE; + } + break; + } + return (INT_PTR)FALSE; +} + +// Decide what to do when an accelerator key is pressed. Instead of immediately performing +// the action, we hand it to the caller so they can decide whether to run it right away +// or running it asynchronously. Will return nullptr if there is no action for the key. +std::function AppWindow::GetAcceleratorKeyFunction(UINT key) +{ + if (GetKeyState(VK_CONTROL) < 0) + { + switch (key) + { + case 'N': + return [this] { new AppWindow(m_creationModeId); }; + case 'Q': + return [this] { CloseAppWindow(); }; + case 'S': + return [this] { + if (auto file = GetComponent()) + { + file->SaveScreenshot(); + } + }; + case 'T': + return [this] { CreateNewThread(m_creationModeId); }; + case 'W': + return [this] { CloseWebView(); }; + } + } + return nullptr; +} + +//! [CreateCoreWebView2Controller] +// Create or recreate the WebView and its environment. +void AppWindow::InitializeWebView() +{ + // To ensure browser switches get applied correctly, we need to close + // the existing WebView. This will result in a new browser process + // getting created which will apply the browser switches. + CloseWebView(); + m_dcompDevice = nullptr; +#ifdef USE_WEBVIEW2_WIN10 + m_wincompCompositor = nullptr; +#endif + LPCWSTR subFolder = nullptr; + + if (m_creationModeId == IDM_CREATION_MODE_VISUAL_DCOMP || + m_creationModeId == IDM_CREATION_MODE_TARGET_DCOMP) + { + HRESULT hr = DCompositionCreateDevice2(nullptr, IID_PPV_ARGS(&m_dcompDevice)); + if (!SUCCEEDED(hr)) + { + MessageBox( + m_mainWindow, + L"Attempting to create WebView using DComp Visual is not supported.\r\n" + "DComp device creation failed.\r\n" + "Current OS may not support DComp.", + L"Create with Windowless DComp Visual Failed", MB_OK); + return; + } + } +#ifdef USE_WEBVIEW2_WIN10 + else if (m_creationModeId == IDM_CREATION_MODE_VISUAL_WINCOMP) + { + HRESULT hr = TryCreateDispatcherQueue(); + if (!SUCCEEDED(hr)) + { + MessageBox( + m_mainWindow, + L"Attempting to create WebView using WinComp Visual is not supported.\r\n" + "WinComp compositor creation failed.\r\n" + "Current OS may not support WinComp.", + L"Create with Windowless WinComp Visual Failed", MB_OK); + return; + } + m_wincompCompositor = winrtComp::Compositor(); + } +#endif + //! [CreateCoreWebView2EnvironmentWithOptions] + auto options = Microsoft::WRL::Make(); + CHECK_FAILURE(options->put_AllowSingleSignOnUsingOSPrimaryAccount( + m_AADSSOEnabled ? TRUE : FALSE)); + if (!m_language.empty()) + CHECK_FAILURE(options->put_Language(m_language.c_str())); + HRESULT hr = CreateCoreWebView2EnvironmentWithOptions( + subFolder, nullptr, options.Get(), + Callback( + this, &AppWindow::OnCreateEnvironmentCompleted) + .Get()); + //! [CreateCoreWebView2EnvironmentWithOptions] + if (!SUCCEEDED(hr)) + { + if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) + { + MessageBox( + m_mainWindow, + L"Couldn't find Edge installation. " + "Do you have a version installed that's compatible with this " + "WebView2 SDK version?", + nullptr, MB_OK); + } + else + { + ShowFailure(hr, L"Failed to create webview environment"); + } + } +} +// This is the callback passed to CreateWebViewEnvironmentWithOptions. +// Here we simply create the WebView. +HRESULT AppWindow::OnCreateEnvironmentCompleted( + HRESULT result, ICoreWebView2Environment* environment) +{ + CHECK_FAILURE(result); + m_webViewEnvironment = environment; + + auto webViewExperimentalEnvironment = + m_webViewEnvironment.try_query(); +#ifdef USE_WEBVIEW2_WIN10 + if (webViewExperimentalEnvironment && (m_dcompDevice || m_wincompCompositor)) +#else + if (webViewExperimentalEnvironment && m_dcompDevice) +#endif + { + CHECK_FAILURE(webViewExperimentalEnvironment->CreateCoreWebView2CompositionController( + m_mainWindow, + Callback< + ICoreWebView2ExperimentalCreateCoreWebView2CompositionControllerCompletedHandler>( + [this]( + HRESULT result, + ICoreWebView2ExperimentalCompositionController* compositionController) -> HRESULT { + auto controller = + wil::com_ptr(compositionController) + .query(); + return OnCreateCoreWebView2ControllerCompleted(result, controller.get()); + }) + .Get())); + } + else + { + CHECK_FAILURE(m_webViewEnvironment->CreateCoreWebView2Controller( + m_mainWindow, Callback( + this, &AppWindow::OnCreateCoreWebView2ControllerCompleted) + .Get())); + } + + return S_OK; +} +//! [CreateCoreWebView2Controller] + +// This is the callback passed to CreateCoreWebView2Controller. Here we initialize all WebView-related +// state and register most of our event handlers with the WebView. +HRESULT AppWindow::OnCreateCoreWebView2ControllerCompleted(HRESULT result, ICoreWebView2Controller* controller) +{ + if (result == S_OK) + { + m_controller = controller; + wil::com_ptr coreWebView2; + CHECK_FAILURE(m_controller->get_CoreWebView2(&coreWebView2)); + // We should check for failure here because if this app is using a newer + // SDK version compared to the install of the Edge browser, the Edge + // browser might not have support for the latest version of the + // ICoreWebView2_N interface. + coreWebView2.query_to(&m_webView); + // Create components. These will be deleted when the WebView is closed. + NewComponent(this); + NewComponent(this); + NewComponent(this); + NewComponent( + this, m_webViewEnvironment.get(), m_oldSettingsComponent.get()); + m_oldSettingsComponent = nullptr; + NewComponent( + this, m_dcompDevice.get(), +#ifdef USE_WEBVIEW2_WIN10 + m_wincompCompositor, +#endif + m_creationModeId == IDM_CREATION_MODE_TARGET_DCOMP); + NewComponent(this, &m_toolbar); + + // We have a few of our own event handlers to register here as well + RegisterEventHandlers(); + + // Set the initial size of the WebView + ResizeEverything(); + + if (m_onWebViewFirstInitialized) + { + m_onWebViewFirstInitialized(); + m_onWebViewFirstInitialized = nullptr; + } + + if (m_initialUri.empty()) + { + // StartPage uses initialized values of the WebView and Environment + // so we wait to call StartPage::GetUri until after the WebView is + // created. + m_initialUri = AppStartPage::GetUri(this); + } + + if (m_initialUri != L"none") + { + CHECK_FAILURE(m_webView->Navigate(m_initialUri.c_str())); + } + } + else + { + ShowFailure(result, L"Failed to create webview"); + } + return S_OK; +} +void AppWindow::ReinitializeWebView() +{ + // Save the settings component from being deleted when the WebView is closed, so we can + // copy its properties to the next settings component. + m_oldSettingsComponent = MoveComponent(); + InitializeWebView(); +} + +void AppWindow::ReinitializeWebViewWithNewBrowser() +{ + // Save the settings component from being deleted when the WebView is closed, so we can + // copy its properties to the next settings component. + m_oldSettingsComponent = MoveComponent(); + + // Use the reference to the web view before we close it + UINT webviewProcessId = 0; + m_webView->get_BrowserProcessId(&webviewProcessId); + + // We need to close the current webviews and wait for the browser_process to exit + // This is so the new webviews don't use the old browser exe + CloseWebView(); + + // Make sure the browser process inside webview is closed + ProcessComponent::EnsureProcessIsClosed(webviewProcessId, 2000); + + InitializeWebView(); +} + +void AppWindow::RestartApp() +{ + // Use the reference to the web view before we close the app window + UINT webviewProcessId = 0; + m_webView->get_BrowserProcessId(&webviewProcessId); + + // To restart the app completely, first we close the current App Window + CloseAppWindow(); + + // Make sure the browser process inside webview is closed + ProcessComponent::EnsureProcessIsClosed(webviewProcessId, 2000); + + // Get the command line arguments used to start this app + // so we can re-create the process with them + LPWSTR args = GetCommandLineW(); + + STARTUPINFOW startup_info = {0}; + startup_info.cb = sizeof(startup_info); + PROCESS_INFORMATION temp_process_info = {}; + // Start a new process + if (!::CreateProcess( + nullptr, args, + nullptr, // default process attributes + nullptr, // default thread attributes + FALSE, // do not inherit handles + 0, + nullptr, // no environment + nullptr, // default current directory + &startup_info, &temp_process_info)) + { + // Log some error information if desired + } + + // Terminate this current process + ::exit(0); +} + +void AppWindow::RegisterEventHandlers() +{ + //! [ContainsFullScreenElementChanged] + // Register a handler for the ContainsFullScreenChanged event. + CHECK_FAILURE(m_webView->add_ContainsFullScreenElementChanged( + Callback( + [this](ICoreWebView2* sender, IUnknown* args) -> HRESULT { + if (m_fullScreenAllowed) + { + CHECK_FAILURE( + sender->get_ContainsFullScreenElement(&m_containsFullscreenElement)); + if (m_containsFullscreenElement) + { + EnterFullScreen(); + } + else + { + ExitFullScreen(); + } + } + return S_OK; + }) + .Get(), + nullptr)); + //! [ContainsFullScreenElementChanged] + + //! [NewWindowRequested] + // Register a handler for the NewWindowRequested event. + // This handler will defer the event, create a new app window, and then once the + // new window is ready, it'll provide that new window's WebView as the response to + // the request. + CHECK_FAILURE(m_webView->add_NewWindowRequested( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2NewWindowRequestedEventArgs* args) { + wil::com_ptr deferral; + CHECK_FAILURE(args->GetDeferral(&deferral)); + AppWindow* newAppWindow; + + wil::com_ptr windowFeatures; + CHECK_FAILURE(args->get_WindowFeatures(&windowFeatures)); + + RECT windowRect = {0}; + UINT32 left = 0; + UINT32 top = 0; + UINT32 height = 0; + UINT32 width = 0; + BOOL shouldHaveToolbar = true; + + BOOL hasPosition = FALSE; + BOOL hasSize = FALSE; + CHECK_FAILURE(windowFeatures->get_HasPosition(&hasPosition)); + CHECK_FAILURE(windowFeatures->get_HasSize(&hasSize)); + + bool useDefaultWindow = true; + + if (!!hasPosition && !!hasSize) + { + CHECK_FAILURE(windowFeatures->get_Left(&left)); + CHECK_FAILURE(windowFeatures->get_Top(&top)); + CHECK_FAILURE(windowFeatures->get_Height(&height)); + CHECK_FAILURE(windowFeatures->get_Width(&width)); + useDefaultWindow = false; + } + CHECK_FAILURE(windowFeatures->get_ShouldDisplayToolbar(&shouldHaveToolbar)); + + windowRect.left = left; + windowRect.right = left + (width < s_minNewWindowSize ? s_minNewWindowSize : width); + windowRect.top = top; + windowRect.bottom = top + (height < s_minNewWindowSize ? s_minNewWindowSize : height); + + if (!useDefaultWindow) + { + newAppWindow = new AppWindow(m_creationModeId, L"", false, nullptr, true, windowRect, !!shouldHaveToolbar); + } + else + { + newAppWindow = new AppWindow(m_creationModeId, L""); + } + newAppWindow->m_isPopupWindow = true; + newAppWindow->m_onWebViewFirstInitialized = [args, deferral, newAppWindow]() { + CHECK_FAILURE(args->put_NewWindow(newAppWindow->m_webView.get())); + CHECK_FAILURE(args->put_Handled(TRUE)); + CHECK_FAILURE(deferral->Complete()); + }; + + return S_OK; + }) + .Get(), + nullptr)); + //! [NewWindowRequested] + + //! [WindowCloseRequested] + // Register a handler for the WindowCloseRequested event. + // This handler will close the app window if it is not the main window. + CHECK_FAILURE(m_webView->add_WindowCloseRequested( + Callback([this]( + ICoreWebView2* sender, + IUnknown* args) { + if (m_isPopupWindow) + { + CloseAppWindow(); + } + return S_OK; + }).Get(), + nullptr)); + //! [WindowCloseRequested] + + //! [NewBrowserVersionAvailable] + // After the environment is successfully created, + // register a handler for the NewBrowserVersionAvailable event. + // This handler tells when there is a new Edge version available on the machine. + CHECK_FAILURE(m_webViewEnvironment->add_NewBrowserVersionAvailable( + Callback( + [this](ICoreWebView2Environment* sender, IUnknown* args) -> HRESULT { + std::wstring message = L"We detected there is a new version for the browser."; + if (m_webView) + { + message += L"Do you want to restart the app? \n\n"; + message += L"Click No if you only want to re-create the webviews. \n"; + message += L"Click Cancel for no action. \n"; + } + int response = MessageBox( + m_mainWindow, message.c_str(), L"New available version", + m_webView ? MB_YESNOCANCEL : MB_OK); + + if (response == IDYES) + { + RestartApp(); + } + else if (response == IDNO) + { + ReinitializeWebViewWithNewBrowser(); + } + else + { + // do nothing + } + + return S_OK; + }) + .Get(), + nullptr)); + //! [NewBrowserVersionAvailable] +} + +// Updates the sizing and positioning of everything in the window. +void AppWindow::ResizeEverything() +{ + RECT availableBounds = {0}; + GetClientRect(m_mainWindow, &availableBounds); + + if (!m_containsFullscreenElement) + { + availableBounds = m_toolbar.Resize(availableBounds); + } + + if (auto view = GetComponent()) + { + view->SetBounds(availableBounds); + } +} + +//! [Close] +// Close the WebView and deinitialize related state. This doesn't close the app window. +void AppWindow::CloseWebView(bool cleanupUserDataFolder) +{ + DeleteAllComponents(); + if (m_controller) + { + m_controller->Close(); + m_controller = nullptr; + m_webView = nullptr; + } + m_webViewEnvironment = nullptr; + if (cleanupUserDataFolder) + { + // For non-UWP apps, the default user data folder {Executable File Name}.WebView2 + // is in the same directory next to the app executable. If end + // developers specify userDataFolder during WebView environment + // creation, they would need to pass in that explicit value here. + // For more information about userDataFolder: + // https://docs.microsoft.com/microsoft-edge/webview2/reference/win32/webview2-idl#createcorewebview2environmentwithoptions + WCHAR userDataFolder[MAX_PATH] = L""; + // Obtain the absolute path for relative paths that include "./" or "../" + _wfullpath( + userDataFolder, GetLocalPath(L".WebView2", true).c_str(), MAX_PATH); + std::wstring userDataFolderPath(userDataFolder); + + std::wstring message = L"Are you sure you want to clean up the user data folder at\n"; + message += userDataFolderPath; + message += L"\n?\nWarning: This action is not reversible.\n\n"; + message += L"Click No if there are other open WebView instances.\n"; + + if (MessageBox(m_mainWindow, message.c_str(), L"Cleanup User Data Folder", MB_YESNO) == + IDYES) + { + CHECK_FAILURE(DeleteFileRecursive(userDataFolderPath)); + } + } +} +//! [Close] + +HRESULT AppWindow::DeleteFileRecursive(std::wstring path) +{ + wil::com_ptr fileOperation; + CHECK_FAILURE( + CoCreateInstance(CLSID_FileOperation, NULL, CLSCTX_ALL, IID_PPV_ARGS(&fileOperation))); + + // Turn off all UI from being shown to the user during the operation. + CHECK_FAILURE(fileOperation->SetOperationFlags(FOF_NO_UI)); + + wil::com_ptr userDataFolder; + CHECK_FAILURE( + SHCreateItemFromParsingName(path.c_str(), NULL, IID_PPV_ARGS(&userDataFolder))); + + // Add the operation + CHECK_FAILURE(fileOperation->DeleteItem(userDataFolder.get(), NULL)); + CHECK_FAILURE(userDataFolder->Release()); + + // Perform the operation to delete the directory + CHECK_FAILURE(fileOperation->PerformOperations()); + + CHECK_FAILURE(fileOperation->Release()); + CoUninitialize(); + return S_OK; +} + +void AppWindow::CloseAppWindow() +{ + CloseWebView(); + DestroyWindow(m_mainWindow); +} + +void AppWindow::DeleteComponent(ComponentBase* component) +{ + for (auto iter = m_components.begin(); iter != m_components.end(); iter++) + { + if (iter->get() == component) + { + m_components.erase(iter); + return; + } + } +} + +void AppWindow::DeleteAllComponents() +{ + // Delete components in reverse order of initialization. + while (!m_components.empty()) + { + m_components.pop_back(); + } +} + +template std::unique_ptr AppWindow::MoveComponent() +{ + for (auto iter = m_components.begin(); iter != m_components.end(); iter++) + { + if (dynamic_cast(iter->get())) + { + auto wanted = reinterpret_cast&&>(std::move(*iter)); + m_components.erase(iter); + return std::move(wanted); + } + } + return nullptr; +} + +void AppWindow::SetTitleText(PCWSTR titleText) +{ + SetWindowText(m_mainWindow, titleText); +} + +RECT AppWindow::GetWindowBounds() +{ + RECT hwndBounds = {0}; + GetClientRect(m_mainWindow, &hwndBounds); + return hwndBounds; +} + +std::wstring AppWindow::GetLocalPath(std::wstring relativePath, bool keep_exe_path) +{ + WCHAR rawPath[MAX_PATH]; + GetModuleFileNameW(g_hInstance, rawPath, MAX_PATH); + std::wstring path(rawPath); + if (keep_exe_path) + { + path.append(relativePath); + } + else + { + std::size_t index = path.find_last_of(L"\\") + 1; + path.replace(index, path.length(), relativePath); + } + return path; +} +std::wstring AppWindow::GetLocalUri(std::wstring relativePath) +{ +#if 0 // To be enabled after AddHostMappingForLocalFolder fully works. + //! [LocalUrlUsage] + const std::wstring localFileRootUrl = L"https://app-file.invalid/"; + return localFileRootUrl + regex_replace(relativePath, std::wregex(L"\\"), L"/"); + //! [LocalUrlUsage] +#else + std::wstring path = GetLocalPath(relativePath, false); + + wil::com_ptr uri; + CHECK_FAILURE(CreateUri(path.c_str(), Uri_CREATE_ALLOW_IMPLICIT_FILE_SCHEME, 0, &uri)); + + wil::unique_bstr uriBstr; + CHECK_FAILURE(uri->GetAbsoluteUri(&uriBstr)); + return std::wstring(uriBstr.get()); +#endif +} + +void AppWindow::RunAsync(std::function callback) +{ + auto* task = new std::function(callback); + PostMessage(m_mainWindow, s_runAsyncWindowMessage, reinterpret_cast(task), 0); +} + +void AppWindow::EnterFullScreen() +{ + DWORD style = GetWindowLong(m_mainWindow, GWL_STYLE); + MONITORINFO monitor_info = {sizeof(monitor_info)}; + m_hMenu = ::GetMenu(m_mainWindow); + ::SetMenu(m_mainWindow, nullptr); + if (GetWindowRect(m_mainWindow, &m_previousWindowRect) && + GetMonitorInfo( + MonitorFromWindow(m_mainWindow, MONITOR_DEFAULTTOPRIMARY), &monitor_info)) + { + SetWindowLong(m_mainWindow, GWL_STYLE, style & ~WS_OVERLAPPEDWINDOW); + SetWindowPos( + m_mainWindow, HWND_TOP, monitor_info.rcMonitor.left, monitor_info.rcMonitor.top, + monitor_info.rcMonitor.right - monitor_info.rcMonitor.left, + monitor_info.rcMonitor.bottom - monitor_info.rcMonitor.top, + SWP_NOOWNERZORDER | SWP_FRAMECHANGED); + } +} + +void AppWindow::ExitFullScreen() +{ + DWORD style = GetWindowLong(m_mainWindow, GWL_STYLE); + ::SetMenu(m_mainWindow, m_hMenu); + SetWindowLong(m_mainWindow, GWL_STYLE, style | WS_OVERLAPPEDWINDOW); + SetWindowPos( + m_mainWindow, NULL, m_previousWindowRect.left, m_previousWindowRect.top, + m_previousWindowRect.right - m_previousWindowRect.left, + m_previousWindowRect.bottom - m_previousWindowRect.top, + SWP_NOOWNERZORDER | SWP_FRAMECHANGED); +} + +// We have our own implementation of DCompositionCreateDevice2 that dynamically +// loads dcomp.dll to create the device. Not having a static dependency on dcomp.dll +// enables the sample app to run on versions of Windows that don't support dcomp. +HRESULT AppWindow::DCompositionCreateDevice2(IUnknown* renderingDevice, REFIID riid, void** ppv) +{ + HRESULT hr = E_FAIL; + static decltype(::DCompositionCreateDevice2)* fnCreateDCompDevice2 = nullptr; + if (fnCreateDCompDevice2 == nullptr) + { + HMODULE hmod = ::LoadLibraryEx(L"dcomp.dll", nullptr, 0); + if (hmod != nullptr) + { + fnCreateDCompDevice2 = reinterpret_cast( + ::GetProcAddress(hmod, "DCompositionCreateDevice2")); + } + } + if (fnCreateDCompDevice2 != nullptr) + { + hr = fnCreateDCompDevice2(renderingDevice, riid, ppv); + } + return hr; +} + +// WinRT APIs cannot run without a DispatcherQueue. This helper function creates a +// DispatcherQueueController (which instantiates a DispatcherQueue under the covers) that will +// manage tasks for the WinRT APIs. The DispatcherQueue implementation lives in +// CoreMessaging.dll Similar to dcomp.dll, we load CoreMessaging.dll dynamically so the sample +// app can run on versions of windows that don't have CoreMessaging. +HRESULT AppWindow::TryCreateDispatcherQueue() +{ + namespace winSystem = winrt::Windows::System; + + HRESULT hr = S_OK; + thread_local winSystem::DispatcherQueueController dispatcherQueueController{ nullptr }; + + if (dispatcherQueueController == nullptr) + { + hr = E_FAIL; + static decltype(::CreateDispatcherQueueController)* fnCreateDispatcherQueueController = + nullptr; + if (fnCreateDispatcherQueueController == nullptr) + { + HMODULE hmod = ::LoadLibraryEx(L"CoreMessaging.dll", nullptr, 0); + if (hmod != nullptr) + { + fnCreateDispatcherQueueController = + reinterpret_cast( + ::GetProcAddress(hmod, "CreateDispatcherQueueController")); + } + } + if (fnCreateDispatcherQueueController != nullptr) + { + winSystem::DispatcherQueueController controller{ nullptr }; + DispatcherQueueOptions options + { + sizeof(DispatcherQueueOptions), + DQTYPE_THREAD_CURRENT, + DQTAT_COM_STA + }; + hr = fnCreateDispatcherQueueController( + options, reinterpret_cast( + winrt::put_abi(controller))); + dispatcherQueueController = controller; + } + } + + return hr; +} + +#ifdef USE_WEBVIEW2_WIN10 +//! [TextScaleChanged2] +void AppWindow::OnTextScaleChanged( + winrt::Windows::UI::ViewManagement::UISettings const& settings, + winrt::Windows::Foundation::IInspectable const& args) +{ + RunAsync([this] { + m_toolbar.UpdateDpiAndTextScale(); + }); +} +//! [TextScaleChanged2] +#endif +void AppWindow::UpdateCreationModeMenu() +{ + HMENU hMenu = GetMenu(m_mainWindow); + CheckMenuRadioItem( + hMenu, + IDM_CREATION_MODE_WINDOWED, +#ifdef USE_WEBVIEW2_WIN10 + IDM_CREATION_MODE_VISUAL_WINCOMP, +#else + IDM_CREATION_MODE_TARGET_DCOMP, +#endif + m_creationModeId, + MF_BYCOMMAND); +} + +double AppWindow::GetDpiScale() +{ + return DpiUtil::GetDpiForWindow(m_mainWindow) * 1.0f / USER_DEFAULT_SCREEN_DPI; +} + +#ifdef USE_WEBVIEW2_WIN10 +double AppWindow::GetTextScale() +{ + return m_uiSettings ? m_uiSettings.TextScaleFactor() : 1.0f; +} +#endif + +void AppWindow::AddRef() +{ + InterlockedIncrement((LONG *)&m_refCount); +} + +void AppWindow::Release() +{ + uint32_t refCount = InterlockedDecrement((LONG *)&m_refCount); + if (refCount == 0) + { + delete this; + } +} + +void AppWindow::NotifyClosed() +{ + m_isClosed = true; +} + +void AppWindow::InstallComplete(int return_code) +{ + if (!m_isClosed) + { + if (return_code == 0) + { + RunAsync([this] { + InitializeWebView(); + }); + } + else if (return_code == 1) + { + MessageBox(m_mainWindow, L"WebView Runtime failed to Install", L"WebView Runtime Installation status", MB_OK); + } + else if (return_code == 2) + { + MessageBox(m_mainWindow, L"WebView Bootstrapper failled to download", L"WebView Bootstrapper Download status", MB_OK); + } + } +} diff --git a/SampleApps/WebView2APISample/AppWindow.h b/SampleApps/WebView2APISample/AppWindow.h index 32e03242..ed1befcb 100644 --- a/SampleApps/WebView2APISample/AppWindow.h +++ b/SampleApps/WebView2APISample/AppWindow.h @@ -1,171 +1,179 @@ -// Copyright (C) Microsoft Corporation. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#pragma once - -#include "stdafx.h" - -#include "ComponentBase.h" -#include "Toolbar.h" -#include "resource.h" -#include -#include -#include -#include -#include -#include -#include -#ifdef USE_WEBVIEW2_WIN10 -#include -#include - -namespace winrtComp = winrt::Windows::UI::Composition; -#endif - -class SettingsComponent; - -class AppWindow -{ -public: - AppWindow( - UINT creationModeId, - std::wstring initialUri = L"https://www.bing.com/", - bool isMainWindow = false, - std::function webviewCreatedCallback = nullptr, - bool customWindowRect = false, - RECT windowRect = { 0 }, - bool shouldHaveToolbar = true); - - ICoreWebView2Controller* GetWebViewController() - { - return m_controller.get(); - } - ICoreWebView2* GetWebView() - { - return m_webView.get(); - } - HWND GetMainWindow() - { - return m_mainWindow; - } - void SetTitleText(PCWSTR titleText); - RECT GetWindowBounds(); - std::wstring GetLocalUri(std::wstring path); - std::function GetAcceleratorKeyFunction(UINT key); - double GetDpiScale(); -#ifdef USE_WEBVIEW2_WIN10 - double GetTextScale(); -#endif - - void ReinitializeWebView(); - - template void NewComponent(Args&&... args); - - template ComponentType* GetComponent(); - - void DeleteComponent(ComponentBase* scenario); - - void RunAsync(std::function callback); - - void InstallComplete(int return_code); - - void AddRef(); - void Release(); - void NotifyClosed(); - -private: - static PCWSTR GetWindowClass(); - - static INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam); - - static LRESULT CALLBACK - WndProcStatic(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); - bool HandleWindowMessage( - HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, LRESULT* result); - - bool ExecuteWebViewCommands(WPARAM wParam, LPARAM lParam); - bool ExecuteAppCommands(WPARAM wParam, LPARAM lParam); - - void ResizeEverything(); - void InitializeWebView(); - HRESULT OnCreateEnvironmentCompleted(HRESULT result, ICoreWebView2Environment* environment); - HRESULT OnCreateCoreWebView2ControllerCompleted(HRESULT result, ICoreWebView2Controller* controller); - HRESULT DeleteFileRecursive(std::wstring path); - void RegisterEventHandlers(); - void ReinitializeWebViewWithNewBrowser(); - void RestartApp(); - void CloseWebView(bool cleanupUserDataFolder = false); - void CloseAppWindow(); - void ChangeLanguage(); - void UpdateCreationModeMenu(); - void ToggleAADSSO(); -#ifdef USE_WEBVIEW2_WIN10 - void OnTextScaleChanged( - winrt::Windows::UI::ViewManagement::UISettings const& uiSettings, - winrt::Windows::Foundation::IInspectable const& args); -#endif - std::wstring GetLocalPath(std::wstring path, bool keep_exe_path); - void DeleteAllComponents(); - - template std::unique_ptr MoveComponent(); - - std::wstring m_initialUri; - HWND m_mainWindow = nullptr; - Toolbar m_toolbar; - std::function m_onWebViewFirstInitialized; - DWORD m_creationModeId = 0; - int m_refCount = 1; - bool m_isClosed = false; - - // The following is state that belongs with the webview, and should - // be reinitialized along with it. Everything here is undefined when - // m_webView is null. - wil::com_ptr m_webViewEnvironment; - wil::com_ptr m_controller; - wil::com_ptr m_webView; - - // All components are deleted when the WebView is closed. - std::vector> m_components; - std::unique_ptr m_oldSettingsComponent; - - std::wstring m_language; - - bool m_AADSSOEnabled = false; - - // Fullscreen related code - RECT m_previousWindowRect; - HMENU m_hMenu; - BOOL m_containsFullscreenElement = FALSE; - bool m_fullScreenAllowed = true; - bool m_isPopupWindow = false; - void EnterFullScreen(); - void ExitFullScreen(); - - // Compositor creation helper methods - HRESULT DCompositionCreateDevice2(IUnknown* renderingDevice, REFIID riid, void** ppv); - HRESULT TryCreateDispatcherQueue(); - - wil::com_ptr m_dcompDevice; -#ifdef USE_WEBVIEW2_WIN10 - winrtComp::Compositor m_wincompCompositor{ nullptr }; - winrt::Windows::UI::ViewManagement::UISettings m_uiSettings{ nullptr }; -#endif -}; - -template void AppWindow::NewComponent(Args&&... args) -{ - m_components.emplace_back(new ComponentType(std::forward(args)...)); -} - -template ComponentType* AppWindow::GetComponent() -{ - for (auto& component : m_components) - { - if (auto wanted = dynamic_cast(component.get())) - { - return wanted; - } - } - return nullptr; -} +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#pragma once + +#include "stdafx.h" + +#include "ComponentBase.h" +#include "Toolbar.h" +#include "resource.h" +#include +#include +#include +#include +#include +#include +#include +#ifdef USE_WEBVIEW2_WIN10 +#include +#include + +namespace winrtComp = winrt::Windows::UI::Composition; +#endif + +class SettingsComponent; + +class AppWindow +{ +public: + AppWindow( + UINT creationModeId, + std::wstring initialUri = L"", + bool isMainWindow = false, + std::function webviewCreatedCallback = nullptr, + bool customWindowRect = false, + RECT windowRect = { 0 }, + bool shouldHaveToolbar = true); + + ICoreWebView2Controller* GetWebViewController() + { + return m_controller.get(); + } + ICoreWebView2* GetWebView() + { + return m_webView.get(); + } + ICoreWebView2Environment* GetWebViewEnvironment() + { + return m_webViewEnvironment.get(); + } + HWND GetMainWindow() + { + return m_mainWindow; + } + void SetTitleText(PCWSTR titleText); + RECT GetWindowBounds(); + std::wstring GetLocalUri(std::wstring path); + std::function GetAcceleratorKeyFunction(UINT key); + double GetDpiScale(); +#ifdef USE_WEBVIEW2_WIN10 + double GetTextScale(); +#endif + + void ReinitializeWebView(); + + template void NewComponent(Args&&... args); + + template ComponentType* GetComponent(); + + void DeleteComponent(ComponentBase* scenario); + + void RunAsync(std::function callback); + + void InstallComplete(int return_code); + + void AddRef(); + void Release(); + void NotifyClosed(); + +private: + static PCWSTR GetWindowClass(); + + static INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam); + + static LRESULT CALLBACK + WndProcStatic(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + bool HandleWindowMessage( + HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, LRESULT* result); + + bool ExecuteWebViewCommands(WPARAM wParam, LPARAM lParam); + bool ExecuteAppCommands(WPARAM wParam, LPARAM lParam); + + void ResizeEverything(); + void InitializeWebView(); + HRESULT OnCreateEnvironmentCompleted(HRESULT result, ICoreWebView2Environment* environment); + HRESULT OnCreateCoreWebView2ControllerCompleted(HRESULT result, ICoreWebView2Controller* controller); + HRESULT DeleteFileRecursive(std::wstring path); + void RegisterEventHandlers(); + void ReinitializeWebViewWithNewBrowser(); + void RestartApp(); + void CloseWebView(bool cleanupUserDataFolder = false); + void CloseAppWindow(); + void ChangeLanguage(); + void UpdateCreationModeMenu(); + void ToggleAADSSO(); +#ifdef USE_WEBVIEW2_WIN10 + void OnTextScaleChanged( + winrt::Windows::UI::ViewManagement::UISettings const& uiSettings, + winrt::Windows::Foundation::IInspectable const& args); +#endif + std::wstring GetLocalPath(std::wstring path, bool keep_exe_path); + void DeleteAllComponents(); + + template std::unique_ptr MoveComponent(); + + // The initial URI to which to navigate the WebView2's top level document. + // This is either empty string in which case we will use StartPage::GetUri, + // or "none" to mean don't perform an initial navigate, + // or a valid absolute URI to which we will navigate. + std::wstring m_initialUri; + HWND m_mainWindow = nullptr; + Toolbar m_toolbar; + std::function m_onWebViewFirstInitialized; + DWORD m_creationModeId = 0; + int m_refCount = 1; + bool m_isClosed = false; + + // The following is state that belongs with the webview, and should + // be reinitialized along with it. Everything here is undefined when + // m_webView is null. + wil::com_ptr m_webViewEnvironment; + wil::com_ptr m_controller; + wil::com_ptr m_webView; + + // All components are deleted when the WebView is closed. + std::vector> m_components; + std::unique_ptr m_oldSettingsComponent; + + std::wstring m_language; + + bool m_AADSSOEnabled = false; + + // Fullscreen related code + RECT m_previousWindowRect; + HMENU m_hMenu; + BOOL m_containsFullscreenElement = FALSE; + bool m_fullScreenAllowed = true; + bool m_isPopupWindow = false; + void EnterFullScreen(); + void ExitFullScreen(); + + // Compositor creation helper methods + HRESULT DCompositionCreateDevice2(IUnknown* renderingDevice, REFIID riid, void** ppv); + HRESULT TryCreateDispatcherQueue(); + + wil::com_ptr m_dcompDevice; +#ifdef USE_WEBVIEW2_WIN10 + winrtComp::Compositor m_wincompCompositor{ nullptr }; + winrt::Windows::UI::ViewManagement::UISettings m_uiSettings{ nullptr }; +#endif +}; + +template void AppWindow::NewComponent(Args&&... args) +{ + m_components.emplace_back(new ComponentType(std::forward(args)...)); +} + +template ComponentType* AppWindow::GetComponent() +{ + for (auto& component : m_components) + { + if (auto wanted = dynamic_cast(component.get())) + { + return wanted; + } + } + return nullptr; +} diff --git a/SampleApps/WebView2APISample/DCompTargetImpl.cpp b/SampleApps/WebView2APISample/DCompTargetImpl.cpp index 5a8e91bd..f14b1900 100644 --- a/SampleApps/WebView2APISample/DCompTargetImpl.cpp +++ b/SampleApps/WebView2APISample/DCompTargetImpl.cpp @@ -1,32 +1,32 @@ -// Copyright (C) Microsoft Corporation. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "stdafx.h" - -#include "DCompTargetImpl.h" - -DCompTargetImpl::DCompTargetImpl(ViewComponent* owner) -{ - m_viewComponentOwner = owner; -} - -void DCompTargetImpl::RemoveOwnerRef() -{ - m_viewComponentOwner = nullptr; -} - -HRESULT __stdcall DCompTargetImpl::SetRoot(IDCompositionVisual* visual) -{ - HRESULT hr = S_OK; - if (m_viewComponentOwner) - { - hr = m_viewComponentOwner->m_dcompWebViewVisual->RemoveAllVisuals(); - if (SUCCEEDED(hr) && visual) - { - hr = m_viewComponentOwner->m_dcompWebViewVisual->AddVisual(visual, FALSE, nullptr); - } - } - - return hr; +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "stdafx.h" + +#include "DCompTargetImpl.h" + +DCompTargetImpl::DCompTargetImpl(ViewComponent* owner) +{ + m_viewComponentOwner = owner; +} + +void DCompTargetImpl::RemoveOwnerRef() +{ + m_viewComponentOwner = nullptr; +} + +HRESULT __stdcall DCompTargetImpl::SetRoot(IDCompositionVisual* visual) +{ + HRESULT hr = S_OK; + if (m_viewComponentOwner) + { + hr = m_viewComponentOwner->m_dcompWebViewVisual->RemoveAllVisuals(); + if (SUCCEEDED(hr) && visual) + { + hr = m_viewComponentOwner->m_dcompWebViewVisual->AddVisual(visual, FALSE, nullptr); + } + } + + return hr; } \ No newline at end of file diff --git a/SampleApps/WebView2APISample/DCompTargetImpl.h b/SampleApps/WebView2APISample/DCompTargetImpl.h index a05bf578..f21e6d03 100644 --- a/SampleApps/WebView2APISample/DCompTargetImpl.h +++ b/SampleApps/WebView2APISample/DCompTargetImpl.h @@ -1,24 +1,24 @@ -// Copyright (C) Microsoft Corporation. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#pragma once - -#include "stdafx.h" - -#include "ViewComponent.h" - -class DCompTargetImpl - : public Microsoft::WRL::RuntimeClass< - Microsoft::WRL::RuntimeClassFlags, IDCompositionTarget> -{ -public: - DCompTargetImpl(ViewComponent* owner); - void RemoveOwnerRef(); - - // Inherited via IDCompositionTarget - virtual HRESULT __stdcall SetRoot(IDCompositionVisual* visual) override; - -private: - ViewComponent* m_viewComponentOwner; -}; +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#pragma once + +#include "stdafx.h" + +#include "ViewComponent.h" + +class DCompTargetImpl + : public Microsoft::WRL::RuntimeClass< + Microsoft::WRL::RuntimeClassFlags, IDCompositionTarget> +{ +public: + DCompTargetImpl(ViewComponent* owner); + void RemoveOwnerRef(); + + // Inherited via IDCompositionTarget + virtual HRESULT __stdcall SetRoot(IDCompositionVisual* visual) override; + +private: + ViewComponent* m_viewComponentOwner; +}; diff --git a/SampleApps/WebView2APISample/ScenarioAddHostObject.html b/SampleApps/WebView2APISample/ScenarioAddHostObject.html index 4d522276..9ea09f92 100644 --- a/SampleApps/WebView2APISample/ScenarioAddHostObject.html +++ b/SampleApps/WebView2APISample/ScenarioAddHostObject.html @@ -1,121 +1,121 @@ - - - - AddHostObjectToScript Sample - - -

AddHostObjectToScript Sample

-

The following buttons interact with the chrome.webview.hostObjects.sample object. Open DevTools console to try running whatever code you like on this object.

-

Get Property

- - -
- - - -
- -

Set Property

- - -
- - - -
- - - -
- -

Indexed Property

- - -
- - - -
- -

Invoke Method

- - -
- - - -
- -

Invoke Callback

- - -
- - - + + + + AddHostObjectToScript Sample + + +

AddHostObjectToScript Sample

+

The following buttons interact with the chrome.webview.hostObjects.sample object. Open DevTools console to try running whatever code you like on this object.

+

Get Property

+ + +
+ + + +
+ +

Set Property

+ + +
+ + + +
+ + + +
+ +

Indexed Property

+ + +
+ + + +
+ +

Invoke Method

+ + +
+ + + +
+ +

Invoke Callback

+ + +
+ + + \ No newline at end of file diff --git a/SampleApps/WebView2APISample/ScenarioCookieManagement.cpp b/SampleApps/WebView2APISample/ScenarioCookieManagement.cpp new file mode 100644 index 00000000..ad01a435 --- /dev/null +++ b/SampleApps/WebView2APISample/ScenarioCookieManagement.cpp @@ -0,0 +1,227 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "stdafx.h" + +#include "ScenarioCookieManagement.h" + +#include "AppWindow.h" +#include "CheckFailure.h" +#include +#include + +using namespace Microsoft::WRL; + +static constexpr WCHAR c_samplePath[] = L"ScenarioCookieManagement.html"; + +ScenarioCookieManagement::ScenarioCookieManagement(AppWindow* appWindow) + : m_appWindow(appWindow), m_webView(appWindow->GetWebView()) +{ + m_sampleUri = m_appWindow->GetLocalUri(c_samplePath); + + ComPtr settings; + CHECK_FAILURE(m_webView->get_Settings(&settings)); + CHECK_FAILURE(settings->put_IsWebMessageEnabled(TRUE)); + + //! [CookieManager] + m_webViewExperimental = m_webView.try_query(); + CHECK_FAILURE(m_webViewExperimental->get_CookieManager(&m_cookieManager)); + //! [CookieManager] + + // Setup the web message received event handler before navigating to + // ensure we don't miss any messages. + CHECK_FAILURE(m_webView->add_WebMessageReceived( + Microsoft::WRL::Callback( + [this](ICoreWebView2* sender, ICoreWebView2WebMessageReceivedEventArgs* args) { + wil::unique_cotaskmem_string uri; + CHECK_FAILURE(args->get_Source(&uri)); + + // Always validate that the origin of the message is what you expect. + if (uri.get() != m_sampleUri) + { + return S_OK; + } + wil::unique_cotaskmem_string messageRaw; + CHECK_FAILURE(args->TryGetWebMessageAsString(&messageRaw)); + std::wstring message = messageRaw.get(); + std::wstring reply; + + if (message.compare(0, 11, L"GetCookies ") == 0) + { + std::wstring uri; + if (message.length() != 11) + { + uri = message.substr(11); + } + GetCookiesHelper(uri.c_str()); + } + else if (message.compare(0, 17, L"AddOrUpdateCookie") == 0) + { + //! [AddOrUpdateCookie] + wil::com_ptr cookie; + CHECK_FAILURE(m_cookieManager->CreateCookie( + L"CookieName", L"CookieValue", L".bing.com", L"/", &cookie)); + CHECK_FAILURE(m_cookieManager->AddOrUpdateCookie(cookie.get())); + //! [AddOrUpdateCookie] + } + else if (message.compare(0, 16, L"DeleteAllCookies") == 0) + { + CHECK_FAILURE(m_cookieManager->DeleteAllCookies()); + } + return S_OK; + }) + .Get(), + &m_webMessageReceivedToken)); + + // Turn off this scenario if we navigate away from the sample page + CHECK_FAILURE(m_webView->add_ContentLoading( + Callback( + [this]( + ICoreWebView2* sender, ICoreWebView2ContentLoadingEventArgs* args) -> HRESULT { + wil::unique_cotaskmem_string uri; + sender->get_Source(&uri); + if (uri.get() != m_sampleUri) + { + m_appWindow->DeleteComponent(this); + } + return S_OK; + }) + .Get(), + &m_contentLoadingToken)); + + CHECK_FAILURE(m_webView->Navigate(m_sampleUri.c_str())); +} + +ScenarioCookieManagement::~ScenarioCookieManagement() +{ + m_webView->remove_WebMessageReceived(m_webMessageReceivedToken); + m_webView->remove_ContentLoading(m_contentLoadingToken); +} + +static std::wstring BoolToString(BOOL value) +{ + return value ? L"true" : L"false"; +} + +static std::wstring EncodeQuote(std::wstring raw) +{ + return L"\"" + regex_replace(raw, std::wregex(L"\""), L"\\\"") + L"\""; +} + +static std::wstring SecondsToString(UINT32 time) +{ + WCHAR rawResult[26]; + time_t rawTime; + rawTime = (const time_t)time; + struct tm timeStruct; + gmtime_s(&timeStruct, &rawTime); + _wasctime_s(rawResult, 26, &timeStruct); + std::wstring result(rawResult); + return result; +} + +static std::wstring CookieToString(ICoreWebView2ExperimentalCookie* cookie) +{ + //! [CookieObject] + wil::unique_cotaskmem_string name; + CHECK_FAILURE(cookie->get_Name(&name)); + wil::unique_cotaskmem_string value; + CHECK_FAILURE(cookie->get_Value(&value)); + wil::unique_cotaskmem_string domain; + CHECK_FAILURE(cookie->get_Domain(&domain)); + wil::unique_cotaskmem_string path; + CHECK_FAILURE(cookie->get_Path(&path)); + double expires; + CHECK_FAILURE(cookie->get_Expires(&expires)); + BOOL isHttpOnly = FALSE; + CHECK_FAILURE(cookie->get_IsHttpOnly(&isHttpOnly)); + COREWEBVIEW2_COOKIE_SAME_SITE_KIND same_site; + std::wstring same_site_as_string; + CHECK_FAILURE(cookie->get_SameSite(&same_site)); + switch (same_site) + { + case COREWEBVIEW2_COOKIE_SAME_SITE_KIND_NONE: + same_site_as_string = L"None"; + break; + case COREWEBVIEW2_COOKIE_SAME_SITE_KIND_LAX: + same_site_as_string = L"Lax"; + break; + case COREWEBVIEW2_COOKIE_SAME_SITE_KIND_STRICT: + same_site_as_string = L"Strict"; + break; + } + BOOL isSecure = FALSE; + CHECK_FAILURE(cookie->get_IsSecure(&isSecure)); + BOOL isSession = FALSE; + CHECK_FAILURE(cookie->get_IsSession(&isSession)); + + std::wstring result = L"{"; + result += L"\"Name\": " + EncodeQuote(name.get()) + L", " + L"\"Value\": " + + EncodeQuote(value.get()) + L", " + L"\"Domain\": " + EncodeQuote(domain.get()) + + L", " + L"\"Path\": " + EncodeQuote(path.get()) + L", " + L"\"HttpOnly\": " + + BoolToString(isHttpOnly) + L", " + L"\"Secure\": " + BoolToString(isSecure) + L", " + + L"\"SameSite\": " + EncodeQuote(same_site_as_string) + L", " + L"\"Expires\": "; + if (!!isSession) + { + result += L"This is a session cookie."; + } + else + { + result += std::to_wstring(expires); + } + + return result + L"\"}"; + //! [CookieObject] +} + +void ScenarioCookieManagement::GetCookiesHelper(std::wstring uri) +{ + //! [GetCookies] + if (m_cookieManager) + { + CHECK_FAILURE(m_cookieManager->GetCookies( + uri.c_str(), + Callback( + [this, uri](HRESULT error_code, ICoreWebView2ExperimentalCookieList* list) -> HRESULT { + CHECK_FAILURE(error_code); + + std::wstring result; + UINT cookie_list_size; + CHECK_FAILURE(list->get_Count(&cookie_list_size)); + + if (cookie_list_size == 0) + { + result += L"No cookies found."; + } + else + { + result += std::to_wstring(cookie_list_size) + L" cookie(s) found"; + if (!uri.empty()) + { + result += L" on " + uri; + } + result += L"\n\n["; + for (int i = 0; i < cookie_list_size; ++i) + { + wil::com_ptr cookie; + CHECK_FAILURE(list->GetValueAtIndex(i, &cookie)); + + if (cookie.get()) + { + result += CookieToString(cookie.get()); + if (i != cookie_list_size - 1) + { + result += L",\n"; + } + } + } + result += L"]"; + } + MessageBox(nullptr, result.c_str(), L"GetCookies Result", MB_OK); + return S_OK; + }) + .Get())); + } + //! [GetCookies] +} diff --git a/SampleApps/WebView2APISample/ScenarioCookieManagement.h b/SampleApps/WebView2APISample/ScenarioCookieManagement.h new file mode 100644 index 00000000..de869f63 --- /dev/null +++ b/SampleApps/WebView2APISample/ScenarioCookieManagement.h @@ -0,0 +1,30 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#pragma once +#include "stdafx.h" + +#include + +#include "AppWindow.h" +#include "ComponentBase.h" + +class ScenarioCookieManagement : public ComponentBase +{ +public: + ScenarioCookieManagement(AppWindow* appWindow); + ~ScenarioCookieManagement() override; + +private: + void GetCookiesHelper(std::wstring uri); + + AppWindow* m_appWindow; + wil::com_ptr m_webViewEnvironment; + wil::com_ptr m_webView; + wil::com_ptr m_webViewExperimental; + wil::com_ptr m_cookieManager; + std::wstring m_sampleUri; + EventRegistrationToken m_webMessageReceivedToken = {}; + EventRegistrationToken m_contentLoadingToken = {}; +}; diff --git a/SampleApps/WebView2APISample/ScenarioCookieManagement.html b/SampleApps/WebView2APISample/ScenarioCookieManagement.html new file mode 100644 index 00000000..6dfc4327 --- /dev/null +++ b/SampleApps/WebView2APISample/ScenarioCookieManagement.html @@ -0,0 +1,62 @@ + + + + ScenarioCookieManagement + + + +

Cookie Management sample page

+

This page demonstrates basic cookie management.

+ +

Setup

+

+ Press ctrl+t to open another WebView under the same WebView2 environment that navigates to https://www.bing.com. +

+

Creating Cookie Manager

+

+ One can start off by getting the cookie manager associated with the WebView2 by using + ICoreWebView2Experimental::get_CookieManager. +

+ +

Getting Cookies

+

+ One can use the cookie manager to get an ICoreWebView2ExperimentalCookieList + that contains the ICoreWebView2ExperimentalCookies that are associated with the specified URI. + Try clicking the Get cookies button below. If calling GetCookies with an empty URI, all cookies under the same profile are + returned. +

+ + + + + +
+ +

Adding or Updating Cookie

+

One can set a cookie by first creating a cookie object calling ICoreWebView2ExperimentalCookieManager::CreateCookie.

+ + + + +
+ +

Clearing Cookies

+ + + + + diff --git a/SampleApps/WebView2APISample/ScenarioDOMContentLoaded.cpp b/SampleApps/WebView2APISample/ScenarioDOMContentLoaded.cpp new file mode 100644 index 00000000..64c6b0d1 --- /dev/null +++ b/SampleApps/WebView2APISample/ScenarioDOMContentLoaded.cpp @@ -0,0 +1,64 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "stdafx.h" + +#include "ScenarioDOMContentLoaded.h" + +#include "AppWindow.h" +#include "CheckFailure.h" + +using namespace Microsoft::WRL; + +static constexpr WCHAR c_samplePath[] = L"ScenarioDOMContentLoaded.html"; +ScenarioDOMContentLoaded::ScenarioDOMContentLoaded(AppWindow* appWindow) + : m_appWindow(appWindow), m_webView(appWindow->GetWebView()) +{ + m_sampleUri = m_appWindow->GetLocalUri(c_samplePath); + //! [DOMContentLoaded] + // Register a handler for the DOMContentLoaded event. + // Check whether the DOM content loaded + m_webViewExperimental = m_webView.query(); + CHECK_FAILURE(m_webViewExperimental->add_DOMContentLoaded( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2ExperimentalDOMContentLoadedEventArgs* args) + -> HRESULT { + m_webView->ExecuteScript( + L"let " + L"content=document.createElement(\"h2\");content.style.color='blue';" + L"content.textContent=\"This text was added by the host " + L"app\";document.body.appendChild(content);", + Callback( + [](HRESULT error, PCWSTR result) -> HRESULT { return S_OK; }) + .Get()); + return S_OK; + }) + .Get(), + &m_DOMContentLoadedToken)); + //! [DOMContentLoaded] + + // Turn off this scenario if we navigate away from the sample page + CHECK_FAILURE(m_webView->add_ContentLoading( + Callback( + [this]( + ICoreWebView2* sender, ICoreWebView2ContentLoadingEventArgs* args) -> HRESULT { + wil::unique_cotaskmem_string uri; + sender->get_Source(&uri); + if (uri.get() != m_sampleUri) + { + m_appWindow->DeleteComponent(this); + } + return S_OK; + }) + .Get(), + &m_contentLoadingToken)); + + CHECK_FAILURE(m_webView->Navigate(m_sampleUri.c_str())); +} + +ScenarioDOMContentLoaded::~ScenarioDOMContentLoaded() +{ + m_webViewExperimental->remove_DOMContentLoaded(m_DOMContentLoadedToken); + m_webView->remove_ContentLoading(m_contentLoadingToken); +} diff --git a/SampleApps/WebView2APISample/ScenarioDOMContentLoaded.h b/SampleApps/WebView2APISample/ScenarioDOMContentLoaded.h new file mode 100644 index 00000000..eebf6cbd --- /dev/null +++ b/SampleApps/WebView2APISample/ScenarioDOMContentLoaded.h @@ -0,0 +1,26 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#pragma once +#include "stdafx.h" + +#include + +#include "AppWindow.h" +#include "ComponentBase.h" + +class ScenarioDOMContentLoaded : public ComponentBase +{ +public: + ScenarioDOMContentLoaded(AppWindow* appWindow); + ~ScenarioDOMContentLoaded() override; + +private: + AppWindow* m_appWindow = nullptr; + wil::com_ptr m_webView; + wil::com_ptr m_webViewExperimental; + std::wstring m_sampleUri; + EventRegistrationToken m_DOMContentLoadedToken = {}; + EventRegistrationToken m_contentLoadingToken = {}; +}; diff --git a/SampleApps/WebView2APISample/ScenarioDOMContentLoaded.html b/SampleApps/WebView2APISample/ScenarioDOMContentLoaded.html new file mode 100644 index 00000000..2f4165c6 --- /dev/null +++ b/SampleApps/WebView2APISample/ScenarioDOMContentLoaded.html @@ -0,0 +1,12 @@ + + + + ScenarioDOMContentLoaded + + +

DOMContentLoaded sample page

+ +

The content below will be added after DOM content is loaded

+ + + diff --git a/SampleApps/WebView2APISample/ScenarioNavigateWithWebResourceRequest.cpp b/SampleApps/WebView2APISample/ScenarioNavigateWithWebResourceRequest.cpp new file mode 100644 index 00000000..42dbd56d --- /dev/null +++ b/SampleApps/WebView2APISample/ScenarioNavigateWithWebResourceRequest.cpp @@ -0,0 +1,55 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include "stdafx.h" +#include "ScenarioNavigateWithWebResourceRequest.h" +#include "AppWindow.h" +#include "CheckFailure.h" +#include "TextInputDialog.h" + +#include + +#include + +using namespace Microsoft::WRL; + +ScenarioNavigateWithWebResourceRequest::ScenarioNavigateWithWebResourceRequest( + AppWindow* appWindow) +{ + // Prepare post data as UTF-8 byte array and convert it to stream + // as required by the application/x-www-form-urlencoded Content-Type + TextInputDialog dialog( + appWindow->GetMainWindow(), L"Post data", L"Post data:", + L"Specify post data to submit to https://www.w3schools.com/action_page.php", + L""); + if (dialog.confirmed) + { + std::wstring postData = std::wstring(L"input=") + dialog.input; + int sizeNeededForMultiByte = WideCharToMultiByte( + CP_UTF8, 0, postData.c_str(), postData.size(), nullptr, + 0, + nullptr, nullptr); + + std::unique_ptr postDataBytes = std::make_unique(sizeNeededForMultiByte); + WideCharToMultiByte( + CP_UTF8, 0, postData.c_str(), postData.size(), postDataBytes.get(), + sizeNeededForMultiByte, nullptr, nullptr); + + //! [NavigateWithWebResourceRequest] + wil::com_ptr webviewExperimental; + CHECK_FAILURE(appWindow->GetWebView()->QueryInterface(IID_PPV_ARGS(&webviewExperimental))); + wil::com_ptr webviewEnvironmentExperimental; + CHECK_FAILURE(appWindow->GetWebViewEnvironment()->QueryInterface( + IID_PPV_ARGS(&webviewEnvironmentExperimental))); + wil::com_ptr webResourceRequest; + wil::com_ptr postDataStream = SHCreateMemStream( + reinterpret_cast(postDataBytes.get()), sizeNeededForMultiByte); + + // This is acts as a form submit to https://www.w3schools.com/action_page.php + CHECK_FAILURE(webviewEnvironmentExperimental->CreateWebResourceRequest( + L"https://www.w3schools.com/action_page.php", L"POST", postDataStream.get(), + L"Content-Type: application/x-www-form-urlencoded", &webResourceRequest)); + CHECK_FAILURE(webviewExperimental->NavigateWithWebResourceRequest(webResourceRequest.get())); + //! [NavigateWithWebResourceRequest] + } +} \ No newline at end of file diff --git a/SampleApps/WebView2APISample/ScenarioNavigateWithWebResourceRequest.h b/SampleApps/WebView2APISample/ScenarioNavigateWithWebResourceRequest.h new file mode 100644 index 00000000..6df3d24e --- /dev/null +++ b/SampleApps/WebView2APISample/ScenarioNavigateWithWebResourceRequest.h @@ -0,0 +1,19 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#pragma once +#include "stdafx.h" +#include + +#include "AppWindow.h" +#include "ComponentBase.h" + +class ScenarioNavigateWithWebResourceRequest : public ComponentBase +{ +public: + ScenarioNavigateWithWebResourceRequest(AppWindow* appWindow); + +private: + AppWindow* m_appWindow; +}; \ No newline at end of file diff --git a/SampleApps/WebView2APISample/ScenarioWebViewEventMonitor.cpp b/SampleApps/WebView2APISample/ScenarioWebViewEventMonitor.cpp index 9f8780f6..66cb2f23 100644 --- a/SampleApps/WebView2APISample/ScenarioWebViewEventMonitor.cpp +++ b/SampleApps/WebView2APISample/ScenarioWebViewEventMonitor.cpp @@ -1,666 +1,691 @@ -// Copyright (C) Microsoft Corporation. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "stdafx.h" - -#define _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING 1 - -#include "AppWindow.h" -#include "CheckFailure.h" -#include "ScenarioWebViewEventMonitor.h" -#include -#include -#include -#include -#include - -using namespace Microsoft::WRL; -using namespace std; - -static constexpr wchar_t c_samplePath[] = L"ScenarioWebViewEventMonitor.html"; - -ScenarioWebViewEventMonitor::ScenarioWebViewEventMonitor(AppWindow* appWindowEventSource) - : m_appWindowEventSource(appWindowEventSource), - m_webviewEventSource(appWindowEventSource->GetWebView()) -{ - m_sampleUri = m_appWindowEventSource->GetLocalUri(c_samplePath); - m_appWindowEventView = new AppWindow( - IDM_CREATION_MODE_WINDOWED, - m_sampleUri, - false, - [this]() -> void { - InitializeEventView(m_appWindowEventView->GetWebView()); - }); - m_webviewEventSourceExperimental = m_webviewEventSource.query(); -} - -ScenarioWebViewEventMonitor::~ScenarioWebViewEventMonitor() -{ - m_webviewEventSource->remove_NavigationStarting(m_navigationStartingToken); - m_webviewEventSource->remove_SourceChanged(m_sourceChangedToken); - m_webviewEventSource->remove_ContentLoading(m_contentLoadingToken); - m_webviewEventSource->remove_HistoryChanged(m_historyChangedToken); - m_webviewEventSource->remove_NavigationCompleted(m_navigationCompletedToken); - m_webviewEventSource->remove_DocumentTitleChanged(m_documentTitleChangedToken); - m_webviewEventSource->remove_WebMessageReceived(m_webMessageReceivedToken); - m_webviewEventSource->remove_NewWindowRequested(m_newWindowRequestedToken); - EnableWebResourceRequestedEvent(false); - EnableWebResourceResponseReceivedEvent(false); - - m_webviewEventView->remove_WebMessageReceived(m_eventViewWebMessageReceivedToken); -} - -std::wstring WebErrorStatusToString(COREWEBVIEW2_WEB_ERROR_STATUS status) -{ - switch (status) - { -#define STATUS_ENTRY(statusValue) \ - case statusValue: \ - return L#statusValue; - - STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_UNKNOWN); - STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CERTIFICATE_COMMON_NAME_IS_INCORRECT); - STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CERTIFICATE_EXPIRED); - STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CLIENT_CERTIFICATE_CONTAINS_ERRORS); - STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CERTIFICATE_REVOKED); - STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CERTIFICATE_IS_INVALID); - STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_SERVER_UNREACHABLE); - STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_TIMEOUT); - STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_ERROR_HTTP_INVALID_SERVER_RESPONSE); - STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CONNECTION_ABORTED); - STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CONNECTION_RESET); - STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_DISCONNECTED); - STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CANNOT_CONNECT); - STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_HOST_NAME_NOT_RESOLVED); - STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_OPERATION_CANCELED); - STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_REDIRECT_FAILED); - STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_UNEXPECTED_ERROR); - -#undef STATUS_ENTRY - } - - return L"ERROR"; -} - -std::wstring BoolToString(BOOL value) -{ - return value ? L"true" : L"false"; -} - -std::wstring EncodeQuote(std::wstring raw) -{ - std::wstring encoded; - // Allocate 10 more chars to reduce memory re-allocation - // due to adding potential escaping chars. - encoded.reserve(raw.length() + 10); - encoded.push_back(L'"'); - for (int i = 0; i < raw.length(); ++i) - { - // Escape chars as listed in https://tc39.es/ecma262/#sec-json.stringify. - switch (raw[i]) - { - case '\b': - encoded.append(L"\\b"); - break; - case '\f': - encoded.append(L"\\f"); - break; - case '\n': - encoded.append(L"\\n"); - break; - case '\r': - encoded.append(L"\\r"); - break; - case '\t': - encoded.append(L"\\t"); - break; - case '\\': - encoded.append(L"\\\\"); - break; - case '"': - encoded.append(L"\\\""); - break; - default: - encoded.push_back(raw[i]); - } - } - encoded.push_back(L'"'); - return encoded; -} - -//! [HttpRequestHeaderIterator] -std::wstring RequestHeadersToJsonString(ICoreWebView2HttpRequestHeaders* requestHeaders) -{ - wil::com_ptr iterator; - CHECK_FAILURE(requestHeaders->GetIterator(&iterator)); - BOOL hasCurrent = FALSE; - std::wstring result = L"["; - - while (SUCCEEDED(iterator->get_HasCurrentHeader(&hasCurrent)) && hasCurrent) - { - wil::unique_cotaskmem_string name; - wil::unique_cotaskmem_string value; - - CHECK_FAILURE(iterator->GetCurrentHeader(&name, &value)); - result += L"{\"name\": " + EncodeQuote(name.get()) - + L", \"value\": " + EncodeQuote(value.get()) + L"}"; - - BOOL hasNext = FALSE; - CHECK_FAILURE(iterator->MoveNext(&hasNext)); - if (hasNext) - { - result += L", "; - } - } - - return result + L"]"; -} -//! [HttpRequestHeaderIterator] - -std::wstring ResponseHeadersToJsonString(ICoreWebView2HttpResponseHeaders* responseHeaders) -{ - wil::com_ptr iterator; - CHECK_FAILURE(responseHeaders->GetIterator(&iterator)); - BOOL hasCurrent = FALSE; - std::wstring result = L"["; - - while (SUCCEEDED(iterator->get_HasCurrentHeader(&hasCurrent)) && hasCurrent) - { - wil::unique_cotaskmem_string name; - wil::unique_cotaskmem_string value; - - CHECK_FAILURE(iterator->GetCurrentHeader(&name, &value)); - result += EncodeQuote(std::wstring(name.get()) + L": " + value.get()); - - BOOL hasNext = FALSE; - CHECK_FAILURE(iterator->MoveNext(&hasNext)); - if (hasNext) - { - result += L", "; - } - } - - return result + L"]"; -} - -std::wstring RequestToJsonString(ICoreWebView2WebResourceRequest* request) -{ - wil::com_ptr content; - CHECK_FAILURE(request->get_Content(&content)); - wil::com_ptr headers; - CHECK_FAILURE(request->get_Headers(&headers)); - wil::unique_cotaskmem_string method; - CHECK_FAILURE(request->get_Method(&method)); - wil::unique_cotaskmem_string uri; - CHECK_FAILURE(request->get_Uri(&uri)); - - std::wstring result = L"{"; - - result += L"\"content\": "; - result += (content == nullptr ? L"null" : L"\"...\""); - result += L", "; - - result += L"\"headers\": " + RequestHeadersToJsonString(headers.get()) + L", "; - result += L"\"method\": " + EncodeQuote(method.get()) + L", "; - result += L"\"uri\": " + EncodeQuote(uri.get()) + L" "; - - result += L"}"; - - return result; -} - -std::wstring GetPreviewOfContent(IStream* content, bool& readAll) -{ - char buffer[50]; - unsigned long read; - content->Read(buffer, 50U, &read); - readAll = read < 50; - - WCHAR converted[50]; - CHECK_FAILURE(MultiByteToWideChar(CP_UTF8, MB_PRECOMPOSED, buffer, 50, converted, 50)); - return std::wstring(converted); -} - -std::wstring ResponseToJsonString(ICoreWebView2WebResourceResponse* response) -{ - wil::com_ptr content; - CHECK_FAILURE(response->get_Content(&content)); - wil::com_ptr headers; - CHECK_FAILURE(response->get_Headers(&headers)); - int statusCode; - CHECK_FAILURE(response->get_StatusCode(&statusCode)); - wil::unique_cotaskmem_string reasonPhrase; - CHECK_FAILURE(response->get_ReasonPhrase(&reasonPhrase)); - BOOL containsContentType = FALSE; - headers->Contains(L"Content-Type", &containsContentType); - wil::unique_cotaskmem_string contentType; - bool isBinaryContent = true; - if (containsContentType) - { - headers->GetHeader(L"Content-Type", &contentType); - if (wcsncmp(L"text/", contentType.get(), ARRAYSIZE(L"text/")) == 0) - { - isBinaryContent = false; - } - } - std::wstring result = L"{"; - - result += L"\"content\": "; - if (!content) - { - result += L"null"; - } - else - { - if (isBinaryContent) - { - result += EncodeQuote(L"BINARY_DATA"); - } - else - { - bool readAll = false; - result += EncodeQuote(GetPreviewOfContent(content.get(), readAll)); - if (!readAll) - { - result += L"..."; - } - } - } - result += L", "; - - result += L"\"headers\": " + ResponseHeadersToJsonString(headers.get()) + L", "; - result += L"\"status\": "; - WCHAR statusCodeString[4]; - _itow_s(statusCode, statusCodeString, 4, 10); - result += statusCodeString; - result += L", "; - result += L"\"reason\": " + EncodeQuote(reasonPhrase.get()) + L" "; - - result += L"}"; - - return result; -} - -std::wstring WebViewPropertiesToJsonString(ICoreWebView2* webview) -{ - wil::unique_cotaskmem_string documentTitle; - CHECK_FAILURE(webview->get_DocumentTitle(&documentTitle)); - wil::unique_cotaskmem_string source; - CHECK_FAILURE(webview->get_Source(&source)); - - std::wstring result = L", \"webview\": {" - L"\"documentTitle\": " + EncodeQuote(documentTitle.get()) + L", " - + L"\"source\": " + EncodeQuote(source.get()) + L" " - + L"}"; - - return result; -} - -void ScenarioWebViewEventMonitor::EnableWebResourceResponseReceivedEvent(bool enable) { - if (!enable && m_webResourceResponseReceivedToken.value != 0) - { - m_webviewEventSourceExperimental->remove_WebResourceResponseReceived(m_webResourceResponseReceivedToken); - m_webResourceResponseReceivedToken.value = 0; - } - else if (enable && m_webResourceResponseReceivedToken.value == 0) - { - m_webviewEventSourceExperimental->add_WebResourceResponseReceived( - Callback( - [this](ICoreWebView2Experimental* webview, ICoreWebView2ExperimentalWebResourceResponseReceivedEventArgs* args) - -> HRESULT { - wil::com_ptr webResourceRequest; - CHECK_FAILURE(args->get_Request(&webResourceRequest)); - wil::com_ptr webResourceResponse; - CHECK_FAILURE(args->get_Response(&webResourceResponse)); - //! [PopulateResponseContent] - args->PopulateResponseContent( - Callback< - ICoreWebView2ExperimentalWebResourceResponseReceivedEventArgsPopulateResponseContentCompletedHandler>( - [this, webResourceRequest, webResourceResponse](HRESULT result) { - std::wstring message = - L"{ \"kind\": \"event\", \"name\": " - L"\"WebResourceResponseReceived\", \"args\": {" - L"\"request\": " + - RequestToJsonString(webResourceRequest.get()) + - L", " - L"\"response\": " + - ResponseToJsonString(webResourceResponse.get()) + L"}"; - - message += - WebViewPropertiesToJsonString(m_webviewEventSource.get()); - message += L"}"; - PostEventMessage(message); - return S_OK; - }) - .Get()); - //! [PopulateResponseContent] - return S_OK; - }) - .Get(), - &m_webResourceResponseReceivedToken); - } -} - -void ScenarioWebViewEventMonitor::EnableWebResourceRequestedEvent(bool enable) -{ - if (!enable && m_webResourceRequestedToken.value != 0) - { - m_webviewEventSource->remove_WebResourceRequested(m_webResourceRequestedToken); - m_webResourceRequestedToken.value = 0; - } - else if (enable && m_webResourceRequestedToken.value == 0) - { - m_webviewEventSource->AddWebResourceRequestedFilter( - L"*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL); - m_webviewEventSource->add_WebResourceRequested( - Callback( - [this](ICoreWebView2* webview, ICoreWebView2WebResourceRequestedEventArgs* args) - -> HRESULT { - wil::com_ptr webResourceRequest; - CHECK_FAILURE(args->get_Request(&webResourceRequest)); - wil::com_ptr webResourceResponse; - CHECK_FAILURE(args->get_Response(&webResourceResponse)); - - std::wstring message = L"{ \"kind\": \"event\", \"name\": " - L"\"WebResourceRequested\", \"args\": {" - L"\"request\": " + RequestToJsonString(webResourceRequest.get()) + L", " - L"\"response\": null" - L"}"; - - message += WebViewPropertiesToJsonString(m_webviewEventSource.get()); - message += L"}"; - PostEventMessage(message); - - return S_OK; - }) - .Get(), - &m_webResourceRequestedToken); - } -} - -void ScenarioWebViewEventMonitor::InitializeEventView(ICoreWebView2* webviewEventView) -{ - m_webviewEventView = webviewEventView; - - m_webviewEventView->add_WebMessageReceived( - Callback( - [this](ICoreWebView2* sender, ICoreWebView2WebMessageReceivedEventArgs* args) - -> HRESULT { - wil::unique_cotaskmem_string source; - CHECK_FAILURE(args->get_Source(&source)); - wil::unique_cotaskmem_string webMessageAsString; - if (SUCCEEDED(args->TryGetWebMessageAsString(&webMessageAsString))) - { - if (wcscmp(source.get(), m_sampleUri.c_str()) == 0) - { - if (wcscmp(webMessageAsString.get(), L"webResourceRequested,on") == 0) - { - EnableWebResourceRequestedEvent(true); - } - else if (wcscmp(webMessageAsString.get(), L"webResourceRequested,off") == 0) - { - EnableWebResourceRequestedEvent(false); - } - else if (wcscmp(webMessageAsString.get(), L"webResourceResponseReceived,on") == 0) - { - EnableWebResourceResponseReceivedEvent(true); - } - else if ( - wcscmp(webMessageAsString.get(), L"webResourceResponseReceived,off") == 0) - { - EnableWebResourceResponseReceivedEvent(false); - } - } - } - - return S_OK; - }) - .Get(), - &m_eventViewWebMessageReceivedToken); - - m_webviewEventSource->add_WebMessageReceived( - Callback( - [this](ICoreWebView2* sender, ICoreWebView2WebMessageReceivedEventArgs* args) - -> HRESULT { - wil::unique_cotaskmem_string source; - CHECK_FAILURE(args->get_Source(&source)); - wil::unique_cotaskmem_string webMessageAsString; - HRESULT webMessageAsStringHR = - args->TryGetWebMessageAsString(&webMessageAsString); - wil::unique_cotaskmem_string webMessageAsJson; - CHECK_FAILURE(args->get_WebMessageAsJson(&webMessageAsJson)); - - std::wstring message = - L"{ \"kind\": \"event\", \"name\": \"WebMessageReceived\", \"args\": {" - L"\"source\": " + EncodeQuote(source.get()) + L", "; - - if (SUCCEEDED(webMessageAsStringHR)) - { - message += L"\"webMessageAsString\": " + EncodeQuote(webMessageAsString.get()) + L", "; - } - else - { - message += L"\"webMessageAsString\": null, "; - } - - message += L"\"webMessageAsJson\": " + EncodeQuote(webMessageAsJson.get()) + L" " - L"}"; - message += WebViewPropertiesToJsonString(m_webviewEventSource.get()); - - message += L"}"; - PostEventMessage(message); - - return S_OK; - }) - .Get(), - &m_webMessageReceivedToken); - - m_webviewEventSource->add_NewWindowRequested( - Callback( - [this](ICoreWebView2* sender, ICoreWebView2NewWindowRequestedEventArgs* args) - -> HRESULT { - BOOL handled = FALSE; - CHECK_FAILURE(args->get_Handled(&handled)); - BOOL isUserInitiated = FALSE; - CHECK_FAILURE(args->get_IsUserInitiated(&isUserInitiated)); - wil::unique_cotaskmem_string uri; - CHECK_FAILURE(args->get_Uri(&uri)); - - std::wstring message = - L"{ \"kind\": \"event\", \"name\": \"NewWindowRequested\", \"args\": {" - L"\"handled\": " + BoolToString(handled) + L", " - L"\"isUserInitiated\": " + BoolToString(isUserInitiated) + L", " - L"\"uri\": " + EncodeQuote(uri.get()) + L", " - L"\"newWindow\": null" - L"}" - + WebViewPropertiesToJsonString(m_webviewEventSource.get()) - + L"}"; - PostEventMessage(message); - - return S_OK; - }) - .Get(), - &m_newWindowRequestedToken); - - m_webviewEventSource->add_NavigationStarting( - Callback( - [this](ICoreWebView2* sender, ICoreWebView2NavigationStartingEventArgs* args) - -> HRESULT { - BOOL cancel = FALSE; - CHECK_FAILURE(args->get_Cancel(&cancel)); - BOOL isRedirected = FALSE; - CHECK_FAILURE(args->get_IsRedirected(&isRedirected)); - BOOL isUserInitiated = FALSE; - CHECK_FAILURE(args->get_IsUserInitiated(&isUserInitiated)); - wil::com_ptr requestHeaders; - CHECK_FAILURE(args->get_RequestHeaders(&requestHeaders)); - wil::unique_cotaskmem_string uri; - CHECK_FAILURE(args->get_Uri(&uri)); - UINT64 navigationId = 0; - CHECK_FAILURE(args->get_NavigationId(&navigationId)); - - std::wstring message = - L"{ \"kind\": \"event\", \"name\": \"NavigationStarting\", \"args\": {"; - - message += L"\"navigationId\": " + std::to_wstring(navigationId) + L", "; - - message += L"\"cancel\": " + BoolToString(cancel) + L", " + - L"\"isRedirected\": " + BoolToString(isRedirected) + L", " + - L"\"isUserInitiated\": " + BoolToString(isUserInitiated) + L", " + - L"\"requestHeaders\": " + RequestHeadersToJsonString(requestHeaders.get()) + L", " + - L"\"uri\": " + EncodeQuote(uri.get()) + L" " + - L"}" + - WebViewPropertiesToJsonString(m_webviewEventSource.get()) + - L"}"; - - PostEventMessage(message); - - return S_OK; - }) - .Get(), - &m_navigationStartingToken); - - m_webviewEventSource->add_FrameNavigationStarting( - Callback( - [this](ICoreWebView2* sender, ICoreWebView2NavigationStartingEventArgs* args) - -> HRESULT { - BOOL cancel = FALSE; - CHECK_FAILURE(args->get_Cancel(&cancel)); - BOOL isRedirected = FALSE; - CHECK_FAILURE(args->get_IsRedirected(&isRedirected)); - BOOL isUserInitiated = FALSE; - CHECK_FAILURE(args->get_IsUserInitiated(&isUserInitiated)); - wil::com_ptr requestHeaders; - CHECK_FAILURE(args->get_RequestHeaders(&requestHeaders)); - wil::unique_cotaskmem_string uri; - CHECK_FAILURE(args->get_Uri(&uri)); - - std::wstring message = - L"{ \"kind\": \"event\", \"name\": " - L"\"FrameNavigationStarting\", \"args\": {" - L"\"cancel\": " + BoolToString(cancel) + L", " - L"\"isRedirected\": " + BoolToString(isRedirected) + L", " - L"\"isUserInitiated\": " + BoolToString(isUserInitiated) + L", " - L"\"requestHeaders\": " + RequestHeadersToJsonString(requestHeaders.get()) + L", " - L"\"uri\": " + EncodeQuote(uri.get()) + L" " - L"}" + - WebViewPropertiesToJsonString(m_webviewEventSource.get()) + - L"}"; - - PostEventMessage(message); - - return S_OK; - }) - .Get(), - &m_frameNavigationStartingToken); - - m_webviewEventSource->add_SourceChanged( - Callback( - [this](ICoreWebView2* sender, ICoreWebView2SourceChangedEventArgs* args) - -> HRESULT { - BOOL isNewDocument = FALSE; - CHECK_FAILURE(args->get_IsNewDocument(&isNewDocument)); - - std::wstring message = - L"{ \"kind\": \"event\", \"name\": \"SourceChanged\", \"args\": {"; - message += L"\"isNewDocument\": " + BoolToString(isNewDocument) + L"}" + - WebViewPropertiesToJsonString(m_webviewEventSource.get()) + L"}"; - PostEventMessage(message); - - return S_OK; - }) - .Get(), - &m_navigationStartingToken); - - m_webviewEventSource->add_ContentLoading( - Callback( - [this]( - ICoreWebView2* sender, - ICoreWebView2ContentLoadingEventArgs* args) -> HRESULT { - BOOL isErrorPage = FALSE; - CHECK_FAILURE(args->get_IsErrorPage(&isErrorPage)); - UINT64 navigationId = 0; - CHECK_FAILURE(args->get_NavigationId(&navigationId)); - - std::wstring message = - L"{ \"kind\": \"event\", \"name\": \"ContentLoading\", \"args\": {"; - - message += L"\"navigationId\": " + std::to_wstring(navigationId) + L", "; - - message += L"\"isErrorPage\": " + BoolToString(isErrorPage) + L"}" + - WebViewPropertiesToJsonString(m_webviewEventSource.get()) + L"}"; - PostEventMessage(message); - - return S_OK; - }) - .Get(), - &m_navigationStartingToken); - - m_webviewEventSource->add_HistoryChanged( - Callback( - [this](ICoreWebView2* sender, IUnknown* args) -> HRESULT { - std::wstring message = - L"{ \"kind\": \"event\", \"name\": \"HistoryChanged\", \"args\": {"; - message += - L"}" + WebViewPropertiesToJsonString(m_webviewEventSource.get()) + L"}"; - PostEventMessage(message); - - return S_OK; - }) - .Get(), - &m_navigationStartingToken); - - m_webviewEventSource->add_NavigationCompleted( - Callback( - [this](ICoreWebView2* sender, ICoreWebView2NavigationCompletedEventArgs* args) - -> HRESULT { - BOOL isSuccess = FALSE; - CHECK_FAILURE(args->get_IsSuccess(&isSuccess)); - COREWEBVIEW2_WEB_ERROR_STATUS webErrorStatus; - CHECK_FAILURE(args->get_WebErrorStatus(&webErrorStatus)); - UINT64 navigationId = 0; - CHECK_FAILURE(args->get_NavigationId(&navigationId)); - - std::wstring message = - L"{ \"kind\": \"event\", \"name\": \"NavigationCompleted\", \"args\": {"; - - message += L"\"navigationId\": " + std::to_wstring(navigationId) + L", "; - - message += - L"\"isSuccess\": " + BoolToString(isSuccess) + L", " - L"\"webErrorStatus\": " + EncodeQuote(WebErrorStatusToString(webErrorStatus)) + L" " - L"}" + - WebViewPropertiesToJsonString(m_webviewEventSource.get()) + - L"}"; - PostEventMessage(message); - - return S_OK; - }) - .Get(), - &m_navigationStartingToken); - - m_webviewEventSource->add_DocumentTitleChanged( - Callback( - [this](ICoreWebView2* sender, IUnknown* args) -> HRESULT { - std::wstring message = - L"{ \"kind\": \"event\", \"name\": \"DocumentTitleChanged\", \"args\": {" - L"}" + - WebViewPropertiesToJsonString(m_webviewEventSource.get()) + - L"}"; - PostEventMessage(message); - - return S_OK; - }) - .Get(), - &m_navigationStartingToken); -} - -void ScenarioWebViewEventMonitor::PostEventMessage(std::wstring message) -{ - HRESULT hr = m_webviewEventView->PostWebMessageAsJson(message.c_str()); - if (FAILED(hr)) - { - ShowFailure(hr, L"PostWebMessageAsJson failed:\n" + message); - } -} +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "stdafx.h" + +#define _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING 1 + +#include "AppWindow.h" +#include "CheckFailure.h" +#include "ScenarioWebViewEventMonitor.h" +#include +#include +#include +#include +#include + +using namespace Microsoft::WRL; +using namespace std; + +static constexpr wchar_t c_samplePath[] = L"ScenarioWebViewEventMonitor.html"; + +ScenarioWebViewEventMonitor::ScenarioWebViewEventMonitor(AppWindow* appWindowEventSource) + : m_appWindowEventSource(appWindowEventSource), + m_webviewEventSource(appWindowEventSource->GetWebView()) +{ + m_sampleUri = m_appWindowEventSource->GetLocalUri(c_samplePath); + m_appWindowEventView = new AppWindow( + IDM_CREATION_MODE_WINDOWED, + m_sampleUri, + false, + [this]() -> void { + InitializeEventView(m_appWindowEventView->GetWebView()); + }); + m_webviewEventSourceExperimental = m_webviewEventSource.query(); +} + +ScenarioWebViewEventMonitor::~ScenarioWebViewEventMonitor() +{ + m_webviewEventSource->remove_NavigationStarting(m_navigationStartingToken); + m_webviewEventSource->remove_FrameNavigationStarting(m_frameNavigationStartingToken); + m_webviewEventSource->remove_SourceChanged(m_sourceChangedToken); + m_webviewEventSource->remove_ContentLoading(m_contentLoadingToken); + m_webviewEventSource->remove_HistoryChanged(m_historyChangedToken); + m_webviewEventSource->remove_NavigationCompleted(m_navigationCompletedToken); + m_webviewEventSource->remove_DocumentTitleChanged(m_documentTitleChangedToken); + m_webviewEventSource->remove_WebMessageReceived(m_webMessageReceivedToken); + m_webviewEventSource->remove_NewWindowRequested(m_newWindowRequestedToken); + m_webviewEventSourceExperimental->remove_DOMContentLoaded(m_DOMContentLoadedToken); + EnableWebResourceRequestedEvent(false); + EnableWebResourceResponseReceivedEvent(false); + + m_webviewEventView->remove_WebMessageReceived(m_eventViewWebMessageReceivedToken); +} + +std::wstring WebErrorStatusToString(COREWEBVIEW2_WEB_ERROR_STATUS status) +{ + switch (status) + { +#define STATUS_ENTRY(statusValue) \ + case statusValue: \ + return L#statusValue; + + STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_UNKNOWN); + STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CERTIFICATE_COMMON_NAME_IS_INCORRECT); + STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CERTIFICATE_EXPIRED); + STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CLIENT_CERTIFICATE_CONTAINS_ERRORS); + STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CERTIFICATE_REVOKED); + STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CERTIFICATE_IS_INVALID); + STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_SERVER_UNREACHABLE); + STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_TIMEOUT); + STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_ERROR_HTTP_INVALID_SERVER_RESPONSE); + STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CONNECTION_ABORTED); + STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CONNECTION_RESET); + STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_DISCONNECTED); + STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_CANNOT_CONNECT); + STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_HOST_NAME_NOT_RESOLVED); + STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_OPERATION_CANCELED); + STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_REDIRECT_FAILED); + STATUS_ENTRY(COREWEBVIEW2_WEB_ERROR_STATUS_UNEXPECTED_ERROR); + +#undef STATUS_ENTRY + } + + return L"ERROR"; +} + +std::wstring BoolToString(BOOL value) +{ + return value ? L"true" : L"false"; +} + +std::wstring EncodeQuote(std::wstring raw) +{ + std::wstring encoded; + // Allocate 10 more chars to reduce memory re-allocation + // due to adding potential escaping chars. + encoded.reserve(raw.length() + 10); + encoded.push_back(L'"'); + for (int i = 0; i < raw.length(); ++i) + { + // Escape chars as listed in https://tc39.es/ecma262/#sec-json.stringify. + switch (raw[i]) + { + case '\b': + encoded.append(L"\\b"); + break; + case '\f': + encoded.append(L"\\f"); + break; + case '\n': + encoded.append(L"\\n"); + break; + case '\r': + encoded.append(L"\\r"); + break; + case '\t': + encoded.append(L"\\t"); + break; + case '\\': + encoded.append(L"\\\\"); + break; + case '"': + encoded.append(L"\\\""); + break; + default: + encoded.push_back(raw[i]); + } + } + encoded.push_back(L'"'); + return encoded; +} + +//! [HttpRequestHeaderIterator] +std::wstring RequestHeadersToJsonString(ICoreWebView2HttpRequestHeaders* requestHeaders) +{ + wil::com_ptr iterator; + CHECK_FAILURE(requestHeaders->GetIterator(&iterator)); + BOOL hasCurrent = FALSE; + std::wstring result = L"["; + + while (SUCCEEDED(iterator->get_HasCurrentHeader(&hasCurrent)) && hasCurrent) + { + wil::unique_cotaskmem_string name; + wil::unique_cotaskmem_string value; + + CHECK_FAILURE(iterator->GetCurrentHeader(&name, &value)); + result += L"{\"name\": " + EncodeQuote(name.get()) + + L", \"value\": " + EncodeQuote(value.get()) + L"}"; + + BOOL hasNext = FALSE; + CHECK_FAILURE(iterator->MoveNext(&hasNext)); + if (hasNext) + { + result += L", "; + } + } + + return result + L"]"; +} +//! [HttpRequestHeaderIterator] + +std::wstring ResponseHeadersToJsonString(ICoreWebView2HttpResponseHeaders* responseHeaders) +{ + wil::com_ptr iterator; + CHECK_FAILURE(responseHeaders->GetIterator(&iterator)); + BOOL hasCurrent = FALSE; + std::wstring result = L"["; + + while (SUCCEEDED(iterator->get_HasCurrentHeader(&hasCurrent)) && hasCurrent) + { + wil::unique_cotaskmem_string name; + wil::unique_cotaskmem_string value; + + CHECK_FAILURE(iterator->GetCurrentHeader(&name, &value)); + result += EncodeQuote(std::wstring(name.get()) + L": " + value.get()); + + BOOL hasNext = FALSE; + CHECK_FAILURE(iterator->MoveNext(&hasNext)); + if (hasNext) + { + result += L", "; + } + } + + return result + L"]"; +} + +std::wstring RequestToJsonString(ICoreWebView2WebResourceRequest* request) +{ + wil::com_ptr content; + CHECK_FAILURE(request->get_Content(&content)); + wil::com_ptr headers; + CHECK_FAILURE(request->get_Headers(&headers)); + wil::unique_cotaskmem_string method; + CHECK_FAILURE(request->get_Method(&method)); + wil::unique_cotaskmem_string uri; + CHECK_FAILURE(request->get_Uri(&uri)); + + std::wstring result = L"{"; + + result += L"\"content\": "; + result += (content == nullptr ? L"null" : L"\"...\""); + result += L", "; + + result += L"\"headers\": " + RequestHeadersToJsonString(headers.get()) + L", "; + result += L"\"method\": " + EncodeQuote(method.get()) + L", "; + result += L"\"uri\": " + EncodeQuote(uri.get()) + L" "; + + result += L"}"; + + return result; +} + +std::wstring GetPreviewOfContent(IStream* content, bool& readAll) +{ + char buffer[50]; + unsigned long read; + content->Read(buffer, 50U, &read); + readAll = read < 50; + + WCHAR converted[50]; + CHECK_FAILURE(MultiByteToWideChar(CP_UTF8, MB_PRECOMPOSED, buffer, 50, converted, 50)); + return std::wstring(converted); +} + +std::wstring ResponseToJsonString( + ICoreWebView2ExperimentalWebResourceResponseView* response, IStream* content) +{ + wil::com_ptr headers; + CHECK_FAILURE(response->get_Headers(&headers)); + int statusCode; + CHECK_FAILURE(response->get_StatusCode(&statusCode)); + wil::unique_cotaskmem_string reasonPhrase; + CHECK_FAILURE(response->get_ReasonPhrase(&reasonPhrase)); + BOOL containsContentType = FALSE; + headers->Contains(L"Content-Type", &containsContentType); + wil::unique_cotaskmem_string contentType; + bool isBinaryContent = true; + if (containsContentType) + { + headers->GetHeader(L"Content-Type", &contentType); + if (wcsncmp(L"text/", contentType.get(), ARRAYSIZE(L"text/")) == 0) + { + isBinaryContent = false; + } + } + std::wstring result = L"{"; + + result += L"\"content\": "; + if (!content) + { + result += L"null"; + } + else + { + if (isBinaryContent) + { + result += EncodeQuote(L"BINARY_DATA"); + } + else + { + bool readAll = false; + result += EncodeQuote(GetPreviewOfContent(content, readAll)); + if (!readAll) + { + result += L"..."; + } + } + } + result += L", "; + + result += L"\"headers\": " + ResponseHeadersToJsonString(headers.get()) + L", "; + result += L"\"status\": "; + WCHAR statusCodeString[4]; + _itow_s(statusCode, statusCodeString, 4, 10); + result += statusCodeString; + result += L", "; + result += L"\"reason\": " + EncodeQuote(reasonPhrase.get()) + L" "; + + result += L"}"; + + return result; +} + +std::wstring WebViewPropertiesToJsonString(ICoreWebView2* webview) +{ + wil::unique_cotaskmem_string documentTitle; + CHECK_FAILURE(webview->get_DocumentTitle(&documentTitle)); + wil::unique_cotaskmem_string source; + CHECK_FAILURE(webview->get_Source(&source)); + + std::wstring result = L", \"webview\": {" + L"\"documentTitle\": " + EncodeQuote(documentTitle.get()) + L", " + + L"\"source\": " + EncodeQuote(source.get()) + L" " + + L"}"; + + return result; +} + +void ScenarioWebViewEventMonitor::EnableWebResourceResponseReceivedEvent(bool enable) { + if (!enable && m_webResourceResponseReceivedToken.value != 0) + { + m_webviewEventSourceExperimental->remove_WebResourceResponseReceived(m_webResourceResponseReceivedToken); + m_webResourceResponseReceivedToken.value = 0; + } + else if (enable && m_webResourceResponseReceivedToken.value == 0) + { + m_webviewEventSourceExperimental->add_WebResourceResponseReceived( + Callback( + [this](ICoreWebView2Experimental* webview, ICoreWebView2ExperimentalWebResourceResponseReceivedEventArgs* args) + -> HRESULT { + wil::com_ptr webResourceRequest; + CHECK_FAILURE(args->get_Request(&webResourceRequest)); + wil::com_ptr + webResourceResponse; + CHECK_FAILURE(args->get_Response(&webResourceResponse)); + //! [GetContent] + webResourceResponse->GetContent( + Callback< + ICoreWebView2ExperimentalWebResourceResponseViewGetContentCompletedHandler>( + [this, webResourceRequest, + webResourceResponse](HRESULT result, IStream* content) { + std::wstring message = + L"{ \"kind\": \"event\", \"name\": " + L"\"WebResourceResponseReceived\", \"args\": {" + L"\"request\": " + + RequestToJsonString(webResourceRequest.get()) + + L", " + L"\"response\": " + + ResponseToJsonString(webResourceResponse.get(), content) + + L"}"; + + message += + WebViewPropertiesToJsonString(m_webviewEventSource.get()); + message += L"}"; + PostEventMessage(message); + return S_OK; + }) + .Get()); + //! [GetContent] + return S_OK; + }) + .Get(), + &m_webResourceResponseReceivedToken); + } +} + +void ScenarioWebViewEventMonitor::EnableWebResourceRequestedEvent(bool enable) +{ + if (!enable && m_webResourceRequestedToken.value != 0) + { + m_webviewEventSource->remove_WebResourceRequested(m_webResourceRequestedToken); + m_webResourceRequestedToken.value = 0; + } + else if (enable && m_webResourceRequestedToken.value == 0) + { + m_webviewEventSource->AddWebResourceRequestedFilter( + L"*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL); + m_webviewEventSource->add_WebResourceRequested( + Callback( + [this](ICoreWebView2* webview, ICoreWebView2WebResourceRequestedEventArgs* args) + -> HRESULT { + wil::com_ptr webResourceRequest; + CHECK_FAILURE(args->get_Request(&webResourceRequest)); + wil::com_ptr webResourceResponse; + CHECK_FAILURE(args->get_Response(&webResourceResponse)); + + std::wstring message = L"{ \"kind\": \"event\", \"name\": " + L"\"WebResourceRequested\", \"args\": {" + L"\"request\": " + RequestToJsonString(webResourceRequest.get()) + L", " + L"\"response\": null" + L"}"; + + message += WebViewPropertiesToJsonString(m_webviewEventSource.get()); + message += L"}"; + PostEventMessage(message); + + return S_OK; + }) + .Get(), + &m_webResourceRequestedToken); + } +} + +void ScenarioWebViewEventMonitor::InitializeEventView(ICoreWebView2* webviewEventView) +{ + m_webviewEventView = webviewEventView; + + m_webviewEventView->add_WebMessageReceived( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2WebMessageReceivedEventArgs* args) + -> HRESULT { + wil::unique_cotaskmem_string source; + CHECK_FAILURE(args->get_Source(&source)); + wil::unique_cotaskmem_string webMessageAsString; + if (SUCCEEDED(args->TryGetWebMessageAsString(&webMessageAsString))) + { + if (wcscmp(source.get(), m_sampleUri.c_str()) == 0) + { + if (wcscmp(webMessageAsString.get(), L"webResourceRequested,on") == 0) + { + EnableWebResourceRequestedEvent(true); + } + else if (wcscmp(webMessageAsString.get(), L"webResourceRequested,off") == 0) + { + EnableWebResourceRequestedEvent(false); + } + else if (wcscmp(webMessageAsString.get(), L"webResourceResponseReceived,on") == 0) + { + EnableWebResourceResponseReceivedEvent(true); + } + else if ( + wcscmp(webMessageAsString.get(), L"webResourceResponseReceived,off") == 0) + { + EnableWebResourceResponseReceivedEvent(false); + } + } + } + + return S_OK; + }) + .Get(), + &m_eventViewWebMessageReceivedToken); + + m_webviewEventSource->add_WebMessageReceived( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2WebMessageReceivedEventArgs* args) + -> HRESULT { + wil::unique_cotaskmem_string source; + CHECK_FAILURE(args->get_Source(&source)); + wil::unique_cotaskmem_string webMessageAsString; + HRESULT webMessageAsStringHR = + args->TryGetWebMessageAsString(&webMessageAsString); + wil::unique_cotaskmem_string webMessageAsJson; + CHECK_FAILURE(args->get_WebMessageAsJson(&webMessageAsJson)); + + std::wstring message = + L"{ \"kind\": \"event\", \"name\": \"WebMessageReceived\", \"args\": {" + L"\"source\": " + EncodeQuote(source.get()) + L", "; + + if (SUCCEEDED(webMessageAsStringHR)) + { + message += L"\"webMessageAsString\": " + EncodeQuote(webMessageAsString.get()) + L", "; + } + else + { + message += L"\"webMessageAsString\": null, "; + } + + message += L"\"webMessageAsJson\": " + EncodeQuote(webMessageAsJson.get()) + L" " + L"}"; + message += WebViewPropertiesToJsonString(m_webviewEventSource.get()); + + message += L"}"; + PostEventMessage(message); + + return S_OK; + }) + .Get(), + &m_webMessageReceivedToken); + + m_webviewEventSource->add_NewWindowRequested( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2NewWindowRequestedEventArgs* args) + -> HRESULT { + BOOL handled = FALSE; + CHECK_FAILURE(args->get_Handled(&handled)); + BOOL isUserInitiated = FALSE; + CHECK_FAILURE(args->get_IsUserInitiated(&isUserInitiated)); + wil::unique_cotaskmem_string uri; + CHECK_FAILURE(args->get_Uri(&uri)); + + std::wstring message = + L"{ \"kind\": \"event\", \"name\": \"NewWindowRequested\", \"args\": {" + L"\"handled\": " + BoolToString(handled) + L", " + L"\"isUserInitiated\": " + BoolToString(isUserInitiated) + L", " + L"\"uri\": " + EncodeQuote(uri.get()) + L", " + L"\"newWindow\": null" + L"}" + + WebViewPropertiesToJsonString(m_webviewEventSource.get()) + + L"}"; + PostEventMessage(message); + + return S_OK; + }) + .Get(), + &m_newWindowRequestedToken); + + m_webviewEventSource->add_NavigationStarting( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2NavigationStartingEventArgs* args) + -> HRESULT { + BOOL cancel = FALSE; + CHECK_FAILURE(args->get_Cancel(&cancel)); + BOOL isRedirected = FALSE; + CHECK_FAILURE(args->get_IsRedirected(&isRedirected)); + BOOL isUserInitiated = FALSE; + CHECK_FAILURE(args->get_IsUserInitiated(&isUserInitiated)); + wil::com_ptr requestHeaders; + CHECK_FAILURE(args->get_RequestHeaders(&requestHeaders)); + wil::unique_cotaskmem_string uri; + CHECK_FAILURE(args->get_Uri(&uri)); + UINT64 navigationId = 0; + CHECK_FAILURE(args->get_NavigationId(&navigationId)); + + std::wstring message = + L"{ \"kind\": \"event\", \"name\": \"NavigationStarting\", \"args\": {"; + + message += L"\"navigationId\": " + std::to_wstring(navigationId) + L", "; + + message += L"\"cancel\": " + BoolToString(cancel) + L", " + + L"\"isRedirected\": " + BoolToString(isRedirected) + L", " + + L"\"isUserInitiated\": " + BoolToString(isUserInitiated) + L", " + + L"\"requestHeaders\": " + RequestHeadersToJsonString(requestHeaders.get()) + L", " + + L"\"uri\": " + EncodeQuote(uri.get()) + L" " + + L"}" + + WebViewPropertiesToJsonString(m_webviewEventSource.get()) + + L"}"; + + PostEventMessage(message); + + return S_OK; + }) + .Get(), + &m_navigationStartingToken); + + m_webviewEventSource->add_FrameNavigationStarting( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2NavigationStartingEventArgs* args) + -> HRESULT { + BOOL cancel = FALSE; + CHECK_FAILURE(args->get_Cancel(&cancel)); + BOOL isRedirected = FALSE; + CHECK_FAILURE(args->get_IsRedirected(&isRedirected)); + BOOL isUserInitiated = FALSE; + CHECK_FAILURE(args->get_IsUserInitiated(&isUserInitiated)); + wil::com_ptr requestHeaders; + CHECK_FAILURE(args->get_RequestHeaders(&requestHeaders)); + wil::unique_cotaskmem_string uri; + CHECK_FAILURE(args->get_Uri(&uri)); + + std::wstring message = + L"{ \"kind\": \"event\", \"name\": " + L"\"FrameNavigationStarting\", \"args\": {" + L"\"cancel\": " + BoolToString(cancel) + L", " + L"\"isRedirected\": " + BoolToString(isRedirected) + L", " + L"\"isUserInitiated\": " + BoolToString(isUserInitiated) + L", " + L"\"requestHeaders\": " + RequestHeadersToJsonString(requestHeaders.get()) + L", " + L"\"uri\": " + EncodeQuote(uri.get()) + L" " + L"}" + + WebViewPropertiesToJsonString(m_webviewEventSource.get()) + + L"}"; + + PostEventMessage(message); + + return S_OK; + }) + .Get(), + &m_frameNavigationStartingToken); + + m_webviewEventSource->add_SourceChanged( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2SourceChangedEventArgs* args) + -> HRESULT { + BOOL isNewDocument = FALSE; + CHECK_FAILURE(args->get_IsNewDocument(&isNewDocument)); + + std::wstring message = + L"{ \"kind\": \"event\", \"name\": \"SourceChanged\", \"args\": {"; + message += L"\"isNewDocument\": " + BoolToString(isNewDocument) + L"}" + + WebViewPropertiesToJsonString(m_webviewEventSource.get()) + L"}"; + PostEventMessage(message); + + return S_OK; + }) + .Get(), + &m_sourceChangedToken); + + m_webviewEventSource->add_ContentLoading( + Callback( + [this]( + ICoreWebView2* sender, + ICoreWebView2ContentLoadingEventArgs* args) -> HRESULT { + BOOL isErrorPage = FALSE; + CHECK_FAILURE(args->get_IsErrorPage(&isErrorPage)); + UINT64 navigationId = 0; + CHECK_FAILURE(args->get_NavigationId(&navigationId)); + + std::wstring message = + L"{ \"kind\": \"event\", \"name\": \"ContentLoading\", \"args\": {"; + + message += L"\"navigationId\": " + std::to_wstring(navigationId) + L", "; + + message += L"\"isErrorPage\": " + BoolToString(isErrorPage) + L"}" + + WebViewPropertiesToJsonString(m_webviewEventSource.get()) + L"}"; + PostEventMessage(message); + + return S_OK; + }) + .Get(), + &m_contentLoadingToken); + + m_webviewEventSource->add_HistoryChanged( + Callback( + [this](ICoreWebView2* sender, IUnknown* args) -> HRESULT { + std::wstring message = + L"{ \"kind\": \"event\", \"name\": \"HistoryChanged\", \"args\": {"; + message += + L"}" + WebViewPropertiesToJsonString(m_webviewEventSource.get()) + L"}"; + PostEventMessage(message); + + return S_OK; + }) + .Get(), + &m_historyChangedToken); + + m_webviewEventSource->add_NavigationCompleted( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2NavigationCompletedEventArgs* args) + -> HRESULT { + BOOL isSuccess = FALSE; + CHECK_FAILURE(args->get_IsSuccess(&isSuccess)); + COREWEBVIEW2_WEB_ERROR_STATUS webErrorStatus; + CHECK_FAILURE(args->get_WebErrorStatus(&webErrorStatus)); + UINT64 navigationId = 0; + CHECK_FAILURE(args->get_NavigationId(&navigationId)); + + std::wstring message = + L"{ \"kind\": \"event\", \"name\": \"NavigationCompleted\", \"args\": {"; + + message += L"\"navigationId\": " + std::to_wstring(navigationId) + L", "; + + message += + L"\"isSuccess\": " + BoolToString(isSuccess) + L", " + L"\"webErrorStatus\": " + EncodeQuote(WebErrorStatusToString(webErrorStatus)) + L" " + L"}" + + WebViewPropertiesToJsonString(m_webviewEventSource.get()) + + L"}"; + PostEventMessage(message); + + return S_OK; + }) + .Get(), + &m_navigationCompletedToken); + + m_webviewEventSourceExperimental->add_DOMContentLoaded( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2ExperimentalDOMContentLoadedEventArgs* args) + -> HRESULT { + UINT64 navigationId = 0; + CHECK_FAILURE(args->get_NavigationId(&navigationId)); + + std::wstring message = + L"{ \"kind\": \"event\", \"name\": \"DOMContentLoaded\", \"args\": {"; + + message += L"\"navigationId\": " + std::to_wstring(navigationId) + L", "; + + message += + L"}" + WebViewPropertiesToJsonString(m_webviewEventSource.get()) + L"}"; + PostEventMessage(message); + + return S_OK; + }) + .Get(), + &m_DOMContentLoadedToken); + + m_webviewEventSource->add_DocumentTitleChanged( + Callback( + [this](ICoreWebView2* sender, IUnknown* args) -> HRESULT { + std::wstring message = + L"{ \"kind\": \"event\", \"name\": \"DocumentTitleChanged\", \"args\": {" + L"}" + + WebViewPropertiesToJsonString(m_webviewEventSource.get()) + + L"}"; + PostEventMessage(message); + + return S_OK; + }) + .Get(), + &m_documentTitleChangedToken); +} + +void ScenarioWebViewEventMonitor::PostEventMessage(std::wstring message) +{ + HRESULT hr = m_webviewEventView->PostWebMessageAsJson(message.c_str()); + if (FAILED(hr)) + { + ShowFailure(hr, L"PostWebMessageAsJson failed:\n" + message); + } +} diff --git a/SampleApps/WebView2APISample/ScenarioWebViewEventMonitor.h b/SampleApps/WebView2APISample/ScenarioWebViewEventMonitor.h index 17ec8d91..142665a6 100644 --- a/SampleApps/WebView2APISample/ScenarioWebViewEventMonitor.h +++ b/SampleApps/WebView2APISample/ScenarioWebViewEventMonitor.h @@ -1,61 +1,62 @@ -// Copyright (C) Microsoft Corporation. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#pragma once - -#include "stdafx.h" -#include -#include "ComponentBase.h" - -std::wstring WebErrorStatusToString(COREWEBVIEW2_WEB_ERROR_STATUS status); - -// The event monitor examines events from the m_appWindowEventSource and -// m_webviewEventSource and displays the details of those events in -// m_appWindowEventView and m_webviewEventView. -class ScenarioWebViewEventMonitor : public ComponentBase -{ -public: - ScenarioWebViewEventMonitor(AppWindow* appWindowEventSource); - ~ScenarioWebViewEventMonitor() override; - - void InitializeEventView(ICoreWebView2* webviewEventView); - -private: - // Because WebResourceRequested fires so much more often than - // all other events, we default to it off and it is configurable. - void EnableWebResourceRequestedEvent(bool enable); - - void EnableWebResourceResponseReceivedEvent(bool enable); - // Send information about an event to the event view. - void PostEventMessage(std::wstring messageAsJson); - - // The event view displays the events and their details. - AppWindow* m_appWindowEventView; - wil::com_ptr m_webviewEventView; - // The URI of the HTML document that displays the events. - std::wstring m_sampleUri; - - // The event source objects fire the events. - AppWindow* m_appWindowEventSource; - wil::com_ptr m_webviewEventSource; - wil::com_ptr m_webviewEventSourceExperimental; - - // The events we register on the event source - EventRegistrationToken m_frameNavigationStartingToken = {}; - EventRegistrationToken m_navigationStartingToken = {}; - EventRegistrationToken m_sourceChangedToken = {}; - EventRegistrationToken m_contentLoadingToken = {}; - EventRegistrationToken m_historyChangedToken = {}; - EventRegistrationToken m_navigationCompletedToken = {}; - EventRegistrationToken m_documentTitleChangedToken = {}; - EventRegistrationToken m_webMessageReceivedToken = {}; - EventRegistrationToken m_webResourceRequestedToken = {}; - EventRegistrationToken m_newWindowRequestedToken = {}; - EventRegistrationToken m_webResourceResponseReceivedToken = {}; - - // This event is registered with the event viewer so they - // can communicate back to us for toggling the WebResourceRequested - // event. - EventRegistrationToken m_eventViewWebMessageReceivedToken = {}; -}; +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#pragma once + +#include "stdafx.h" +#include +#include "ComponentBase.h" + +std::wstring WebErrorStatusToString(COREWEBVIEW2_WEB_ERROR_STATUS status); + +// The event monitor examines events from the m_appWindowEventSource and +// m_webviewEventSource and displays the details of those events in +// m_appWindowEventView and m_webviewEventView. +class ScenarioWebViewEventMonitor : public ComponentBase +{ +public: + ScenarioWebViewEventMonitor(AppWindow* appWindowEventSource); + ~ScenarioWebViewEventMonitor() override; + + void InitializeEventView(ICoreWebView2* webviewEventView); + +private: + // Because WebResourceRequested fires so much more often than + // all other events, we default to it off and it is configurable. + void EnableWebResourceRequestedEvent(bool enable); + + void EnableWebResourceResponseReceivedEvent(bool enable); + // Send information about an event to the event view. + void PostEventMessage(std::wstring messageAsJson); + + // The event view displays the events and their details. + AppWindow* m_appWindowEventView; + wil::com_ptr m_webviewEventView; + // The URI of the HTML document that displays the events. + std::wstring m_sampleUri; + + // The event source objects fire the events. + AppWindow* m_appWindowEventSource; + wil::com_ptr m_webviewEventSource; + wil::com_ptr m_webviewEventSourceExperimental; + + // The events we register on the event source + EventRegistrationToken m_frameNavigationStartingToken = {}; + EventRegistrationToken m_navigationStartingToken = {}; + EventRegistrationToken m_sourceChangedToken = {}; + EventRegistrationToken m_contentLoadingToken = {}; + EventRegistrationToken m_historyChangedToken = {}; + EventRegistrationToken m_navigationCompletedToken = {}; + EventRegistrationToken m_DOMContentLoadedToken = {}; + EventRegistrationToken m_documentTitleChangedToken = {}; + EventRegistrationToken m_webMessageReceivedToken = {}; + EventRegistrationToken m_webResourceRequestedToken = {}; + EventRegistrationToken m_newWindowRequestedToken = {}; + EventRegistrationToken m_webResourceResponseReceivedToken = {}; + + // This event is registered with the event viewer so they + // can communicate back to us for toggling the WebResourceRequested + // event. + EventRegistrationToken m_eventViewWebMessageReceivedToken = {}; +}; diff --git a/SampleApps/WebView2APISample/SettingsComponent.cpp b/SampleApps/WebView2APISample/SettingsComponent.cpp index bc7f7e7a..91b2dfc0 100644 --- a/SampleApps/WebView2APISample/SettingsComponent.cpp +++ b/SampleApps/WebView2APISample/SettingsComponent.cpp @@ -1,656 +1,660 @@ -// Copyright (C) Microsoft Corporation. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "stdafx.h" - -#include "SettingsComponent.h" - -#include "CheckFailure.h" -#include "TextInputDialog.h" - -using namespace Microsoft::WRL; - -// Some utility functions -static wil::unique_bstr GetDomainOfUri(PWSTR uri); -static PCWSTR NameOfPermissionKind(COREWEBVIEW2_PERMISSION_KIND kind); - -SettingsComponent::SettingsComponent( - AppWindow* appWindow, ICoreWebView2Environment* environment, SettingsComponent* old) - : m_appWindow(appWindow), m_webViewEnvironment(environment), - m_webView(appWindow->GetWebView()) -{ - CHECK_FAILURE(m_webView->get_Settings(&m_settings)); - - // Copy old settings if desired - if (old) - { - BOOL setting; - CHECK_FAILURE(old->m_settings->get_IsScriptEnabled(&setting)); - CHECK_FAILURE(m_settings->put_IsScriptEnabled(setting)); - CHECK_FAILURE(old->m_settings->get_IsWebMessageEnabled(&setting)); - CHECK_FAILURE(m_settings->put_IsWebMessageEnabled(setting)); - CHECK_FAILURE(old->m_settings->get_AreDefaultScriptDialogsEnabled(&setting)); - CHECK_FAILURE(m_settings->put_AreDefaultScriptDialogsEnabled(setting)); - CHECK_FAILURE(old->m_settings->get_IsStatusBarEnabled(&setting)); - CHECK_FAILURE(m_settings->put_IsStatusBarEnabled(setting)); - CHECK_FAILURE(old->m_settings->get_AreDevToolsEnabled(&setting)); - CHECK_FAILURE(m_settings->put_AreDevToolsEnabled(setting)); - SetBlockImages(old->m_blockImages); - SetUserAgent(old->m_overridingUserAgent); - m_deferScriptDialogs = old->m_deferScriptDialogs; - m_isScriptEnabled = old->m_isScriptEnabled; - m_blockedSitesSet = old->m_blockedSitesSet; - m_blockedSites = std::move(old->m_blockedSites); - m_overridingUserAgent = std::move(old->m_overridingUserAgent); - } - - //! [NavigationStarting] - // Register a handler for the NavigationStarting event. - // This handler will check the domain being navigated to, and if the domain - // matches a list of blocked sites, it will cancel the navigation and - // possibly display a warning page. It will also disable JavaScript on - // selected websites. - CHECK_FAILURE(m_webView->add_NavigationStarting( - Callback( - [this](ICoreWebView2* sender, - ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT - { - wil::unique_cotaskmem_string uri; - CHECK_FAILURE(args->get_Uri(&uri)); - - if (ShouldBlockUri(uri.get())) - { - CHECK_FAILURE(args->put_Cancel(true)); - - // If the user clicked a link to navigate, show a warning page. - BOOL userInitiated; - CHECK_FAILURE(args->get_IsUserInitiated(&userInitiated)); - //! [NavigateToString] - static const PCWSTR htmlContent = - L"

Domain Blocked

" - L"

You've attempted to navigate to a domain in the blocked " - L"sites list. Press back to return to the previous page.

"; - CHECK_FAILURE(sender->NavigateToString(htmlContent)); - //! [NavigateToString] - } - //! [IsScriptEnabled] - // Changes to settings will apply at the next navigation, which includes the - // navigation after a NavigationStarting event. We can use this to change - // settings according to what site we're visiting. - if (ShouldBlockScriptForUri(uri.get())) - { - m_settings->put_IsScriptEnabled(FALSE); - } - else - { - m_settings->put_IsScriptEnabled(m_isScriptEnabled); - } - //! [IsScriptEnabled] - return S_OK; - }).Get(), &m_navigationStartingToken)); - //! [NavigationStarting] - - //! [FrameNavigationStarting] - // Register a handler for the FrameNavigationStarting event. - // This handler will prevent a frame from navigating to a blocked domain. - CHECK_FAILURE(m_webView->add_FrameNavigationStarting( - Callback( - [this](ICoreWebView2* sender, - ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT - { - wil::unique_cotaskmem_string uri; - CHECK_FAILURE(args->get_Uri(&uri)); - - if (ShouldBlockUri(uri.get())) - { - CHECK_FAILURE(args->put_Cancel(true)); - } - return S_OK; - }).Get(), &m_frameNavigationStartingToken)); - //! [FrameNavigationStarting] - - //! [ScriptDialogOpening] - // Register a handler for the ScriptDialogOpening event. - // This handler will set up a custom prompt dialog for the user, - // and may defer the event if the setting to defer dialogs is enabled. - CHECK_FAILURE(m_webView->add_ScriptDialogOpening( - Callback( - [this]( - ICoreWebView2* sender, - ICoreWebView2ScriptDialogOpeningEventArgs* args) -> HRESULT - { - wil::com_ptr eventArgs = args; - auto showDialog = [this, eventArgs] - { - wil::unique_cotaskmem_string uri; - COREWEBVIEW2_SCRIPT_DIALOG_KIND type; - wil::unique_cotaskmem_string message; - wil::unique_cotaskmem_string defaultText; - - CHECK_FAILURE(eventArgs->get_Uri(&uri)); - CHECK_FAILURE(eventArgs->get_Kind(&type)); - CHECK_FAILURE(eventArgs->get_Message(&message)); - CHECK_FAILURE(eventArgs->get_DefaultText(&defaultText)); - - std::wstring promptString = std::wstring(L"The page at '") - + uri.get() + L"' says:"; - TextInputDialog dialog( - m_appWindow->GetMainWindow(), - L"Script Dialog", - promptString.c_str(), - message.get(), - defaultText.get(), - /* readonly */ type != COREWEBVIEW2_SCRIPT_DIALOG_KIND_PROMPT); - if (dialog.confirmed) - { - CHECK_FAILURE(eventArgs->put_ResultText(dialog.input.c_str())); - CHECK_FAILURE(eventArgs->Accept()); - } - }; - - if (m_deferScriptDialogs) - { - wil::com_ptr deferral; - CHECK_FAILURE(args->GetDeferral(&deferral)); - m_completeDeferredDialog = [showDialog, deferral] - { - showDialog(); - CHECK_FAILURE(deferral->Complete()); - }; - } - else - { - showDialog(); - } - - return S_OK; - }).Get(), &m_scriptDialogOpeningToken)); - //! [ScriptDialogOpening] - - //! [PermissionRequested] - // Register a handler for the PermissionRequested event. - // This handler prompts the user to allow or deny the request. - CHECK_FAILURE(m_webView->add_PermissionRequested( - Callback( - [this]( - ICoreWebView2* sender, - ICoreWebView2PermissionRequestedEventArgs* args) -> HRESULT - { - wil::unique_cotaskmem_string uri; - COREWEBVIEW2_PERMISSION_KIND kind = COREWEBVIEW2_PERMISSION_KIND_UNKNOWN_PERMISSION; - BOOL userInitiated = FALSE; - - CHECK_FAILURE(args->get_Uri(&uri)); - CHECK_FAILURE(args->get_PermissionKind(&kind)); - CHECK_FAILURE(args->get_IsUserInitiated(&userInitiated)); - - std::wstring message = L"Do you want to grant permission for "; - message += NameOfPermissionKind(kind); - message += L" to the website at "; - message += uri.get(); - message += L"?\n\n"; - message += (userInitiated - ? L"This request came from a user gesture." - : L"This request did not come from a user gesture."); - - int response = MessageBox(nullptr, message.c_str(), L"Permission Request", - MB_YESNOCANCEL | MB_ICONWARNING); - - COREWEBVIEW2_PERMISSION_STATE state = - response == IDYES ? COREWEBVIEW2_PERMISSION_STATE_ALLOW - : response == IDNO ? COREWEBVIEW2_PERMISSION_STATE_DENY - : COREWEBVIEW2_PERMISSION_STATE_DEFAULT; - CHECK_FAILURE(args->put_State(state)); - - return S_OK; - }).Get(), &m_permissionRequestedToken)); - //! [PermissionRequested] -} - -bool SettingsComponent::HandleWindowMessage( - HWND hWnd, - UINT message, - WPARAM wParam, - LPARAM lParam, - LRESULT* result) -{ - if (message == WM_COMMAND) - { - switch (LOWORD(wParam)) - { - case ID_BLOCKEDSITES: - { - ChangeBlockedSites(); - return true; - } - case ID_SETTINGS_SETUSERAGENT: - { - ChangeUserAgent(); - return true; - } - case IDM_TOGGLE_JAVASCRIPT: - { - m_isScriptEnabled = !m_isScriptEnabled; - m_settings->put_IsScriptEnabled(m_isScriptEnabled); - MessageBox(nullptr, - (std::wstring(L"JavaScript will be ") - + (m_isScriptEnabled ? L"enabled" : L"disabled") - + L" after the next navigation.").c_str(), - L"Settings change", MB_OK); - return true; - } - case IDM_TOGGLE_WEB_MESSAGING: - { - BOOL isWebMessageEnabled; - m_settings->get_IsWebMessageEnabled(&isWebMessageEnabled); - m_settings->put_IsWebMessageEnabled(!isWebMessageEnabled); - MessageBox(nullptr, - (std::wstring(L"Web Messaging will be ") - + (!isWebMessageEnabled ? L"enabled" : L"disabled") - + L" after the next navigation.").c_str(), - L"Settings change", MB_OK); - return true; - } - case ID_SETTINGS_STATUS_BAR_ENABLED: - { - BOOL isStatusBarEnabled; - m_settings->get_IsStatusBarEnabled(&isStatusBarEnabled); - m_settings->put_IsStatusBarEnabled(!isStatusBarEnabled); - MessageBox(nullptr, - (std::wstring(L"Status bar will be ") + - + (!isStatusBarEnabled ? L"enabled" : L"disabled") - + L" after the next navigation.").c_str(), - L"Settings change", MB_OK); - return true; - } - case ID_SETTINGS_DEV_TOOLS_ENABLED: - { - BOOL areDevToolsEnabled = FALSE; - m_settings->get_AreDevToolsEnabled(&areDevToolsEnabled); - m_settings->put_AreDevToolsEnabled(!areDevToolsEnabled); - MessageBox(nullptr, - (std::wstring(L"Dev tools will be ") - + (!areDevToolsEnabled ? L"enabled" : L"disabled") - + L" after the next navigation.").c_str(), - L"Settings change", MB_OK); - return true; - } - case IDM_USE_DEFAULT_SCRIPT_DIALOGS: - { - BOOL defaultCurrentlyEnabled; - m_settings->get_AreDefaultScriptDialogsEnabled(&defaultCurrentlyEnabled); - if (!defaultCurrentlyEnabled) - { - m_settings->put_AreDefaultScriptDialogsEnabled(TRUE); - MessageBox(nullptr, - L"Default script dialogs will be used after the next navigation.", - L"Settings change", MB_OK); - } - return true; - } - case IDM_USE_CUSTOM_SCRIPT_DIALOGS: - { - BOOL defaultCurrentlyEnabled; - m_settings->get_AreDefaultScriptDialogsEnabled(&defaultCurrentlyEnabled); - if (defaultCurrentlyEnabled) - { - m_settings->put_AreDefaultScriptDialogsEnabled(FALSE); - m_deferScriptDialogs = false; - MessageBox(nullptr, - L"Custom script dialogs without deferral will be used after the next navigation.", - L"Settings change", MB_OK); - } - else if (m_deferScriptDialogs) - { - m_deferScriptDialogs = false; - MessageBox(nullptr, - L"Custom script dialogs without deferral will be used now.", - L"Settings change", MB_OK); - } - return true; - } - case IDM_USE_DEFERRED_SCRIPT_DIALOGS: - { - BOOL defaultCurrentlyEnabled; - m_settings->get_AreDefaultScriptDialogsEnabled(&defaultCurrentlyEnabled); - if (defaultCurrentlyEnabled) - { - m_settings->put_AreDefaultScriptDialogsEnabled(FALSE); - m_deferScriptDialogs = true; - MessageBox(nullptr, - L"Custom script dialogs with deferral will be used after the next navigation.", - L"Settings change", MB_OK); - } - else if (!m_deferScriptDialogs) - { - m_deferScriptDialogs = true; - MessageBox(nullptr, - L"Custom script dialogs with deferral will be used now.", - L"Settings change", MB_OK); - } - return true; - } - case IDM_COMPLETE_JAVASCRIPT_DIALOG: - { - CompleteScriptDialogDeferral(); - return true; - } - case ID_SETTINGS_BLOCKALLIMAGES: - { - SetBlockImages(!m_blockImages); - MessageBox(nullptr, - (std::wstring(L"Image blocking has been ") + - (m_blockImages ? L"enabled." : L"disabled.")) - .c_str(), - L"Settings change", MB_OK); - return true; - } - case ID_SETTINGS_CONTEXT_MENUS_ENABLED: - { - //! [DisableContextMenu] - BOOL allowContextMenus; - CHECK_FAILURE(m_settings->get_AreDefaultContextMenusEnabled( - &allowContextMenus)); - if (allowContextMenus) { - CHECK_FAILURE(m_settings->put_AreDefaultContextMenusEnabled(FALSE)); - MessageBox(nullptr, - L"Context menus will be disabled after the next navigation.", - L"Settings change", MB_OK); - } - else { - CHECK_FAILURE(m_settings->put_AreDefaultContextMenusEnabled(TRUE)); - MessageBox(nullptr, - L"Context menus will be enabled after the next navigation.", - L"Settings change", MB_OK); - } - //! [DisableContextMenu] - return true; - } - case ID_SETTINGS_HOST_OBJECTS_ALLOWED: - { - //! [HostObjectsAccess] - BOOL allowHostObjects; - CHECK_FAILURE(m_settings->get_AreHostObjectsAllowed(&allowHostObjects)); - if (allowHostObjects) - { - CHECK_FAILURE(m_settings->put_AreHostObjectsAllowed(FALSE)); - MessageBox( - nullptr, L"Access to host objects will be denied after the next navigation.", - L"Settings change", MB_OK); - } - else - { - CHECK_FAILURE(m_settings->put_AreHostObjectsAllowed(TRUE)); - MessageBox( - nullptr, L"Access to host objects will be allowed after the next navigation.", - L"Settings change", MB_OK); - } - //! [HostObjectsAccess] - return true; - } - case ID_SETTINGS_ZOOM_ENABLED: - { - //! [DisableZoomControl] - BOOL zoomControlEnabled; - CHECK_FAILURE(m_settings->get_IsZoomControlEnabled(&zoomControlEnabled)); - if (zoomControlEnabled) - { - CHECK_FAILURE(m_settings->put_IsZoomControlEnabled(FALSE)); - MessageBox( - nullptr, L"Zoom control will be disabled after the next navigation.", L"Settings change", - MB_OK); - } - else - { - CHECK_FAILURE(m_settings->put_IsZoomControlEnabled(TRUE)); - MessageBox( - nullptr, L"Zoom control will be enabled after the next navigation.", L"Settings change", - MB_OK); - } - //! [DisableZoomControl] - return true; - } - case ID_SETTINGS_BUILTIN_ERROR_PAGE_ENABLED: - { - //! [BuiltInErrorPageEnabled] - BOOL enabled; - CHECK_FAILURE(m_settings->get_IsBuiltInErrorPageEnabled(&enabled)); - if (enabled) - { - CHECK_FAILURE(m_settings->put_IsBuiltInErrorPageEnabled(FALSE)); - MessageBox( - nullptr, L"Built-in error page will be disabled for future navigation.", - L"Settings change", MB_OK); - } - else - { - CHECK_FAILURE(m_settings->put_IsBuiltInErrorPageEnabled(TRUE)); - MessageBox( - nullptr, L"Built-in error page will be enabled for future navigation.", - L"Settings change", MB_OK); - } - //! [BuiltInErrorPageEnabled] - return true; - } - } - } - return false; -} - -// Prompt the user for a list of blocked domains -void SettingsComponent::ChangeBlockedSites() -{ - std::wstring blockedSitesString; - if (m_blockedSitesSet) - { - for (auto& site : m_blockedSites) - { - if (!blockedSitesString.empty()) - { - blockedSitesString += L";"; - } - blockedSitesString += site; - } - } - else - { - blockedSitesString = L"foo.com;bar.org"; - } - - TextInputDialog dialog( - m_appWindow->GetMainWindow(), - L"Blocked Sites", - L"Sites:", - L"Enter hostnames to block, separated by semicolons.", - blockedSitesString.c_str()); - if (dialog.confirmed) - { - m_blockedSitesSet = true; - m_blockedSites.clear(); - size_t begin = 0; - size_t end = 0; - while (end != std::wstring::npos) - { - end = dialog.input.find(L';', begin); - if (end != begin) - { - m_blockedSites.push_back(dialog.input.substr(begin, end - begin)); - } - begin = end + 1; - } - } -} - -// Check the URI's domain against the blocked sites list -bool SettingsComponent::ShouldBlockUri(PWSTR uri) -{ - wil::unique_bstr domain = GetDomainOfUri(uri); - - for (auto site = m_blockedSites.begin(); - site != m_blockedSites.end(); site++) - { - if (wcscmp(site->c_str(), domain.get()) == 0) - { - return true; - } - } - return false; -} - -// Decide whether a website should have script disabled. Since we're only using this -// for sample code and we don't actually want to break any websites, just return false. -bool SettingsComponent::ShouldBlockScriptForUri(PWSTR uri) -{ - return false; -} - -// Turn on or off image blocking by adding or removing a WebResourceRequested handler -// which selectively intercepts requests for images. -void SettingsComponent::SetBlockImages(bool blockImages) -{ - if (blockImages != m_blockImages) - { - m_blockImages = blockImages; - - //! [WebResourceRequested] - if (m_blockImages) - { - m_webView->AddWebResourceRequestedFilter(L"*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_IMAGE); - CHECK_FAILURE(m_webView->add_WebResourceRequested( - Callback( - [this]( - ICoreWebView2* sender, - ICoreWebView2WebResourceRequestedEventArgs* args) { - COREWEBVIEW2_WEB_RESOURCE_CONTEXT resourceContext; - CHECK_FAILURE( - args->get_ResourceContext(&resourceContext)); - // Ensure that the type is image - if (resourceContext != COREWEBVIEW2_WEB_RESOURCE_CONTEXT_IMAGE) - { - return E_INVALIDARG; - } - // Override the response with an empty one to block the image. - // If put_Response is not called, the request will continue as normal. - wil::com_ptr response; - CHECK_FAILURE(m_webViewEnvironment->CreateWebResourceResponse( - nullptr, 403 /*NoContent*/, L"Blocked", L"", &response)); - CHECK_FAILURE(args->put_Response(response.get())); - return S_OK; - }) - .Get(), - &m_webResourceRequestedTokenForImageBlocking)); - } - else - { - CHECK_FAILURE(m_webView->remove_WebResourceRequested( - m_webResourceRequestedTokenForImageBlocking)); - } - //! [WebResourceRequested] - } -} - -// Prompt the user for a new User Agent string -void SettingsComponent::ChangeUserAgent() { - TextInputDialog dialog( - m_appWindow->GetMainWindow(), - L"User Agent", - L"User agent:", - L"Enter user agent, or leave blank to restore default.", - m_changeUserAgent - ? m_overridingUserAgent.c_str() - : L"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3818.0 Safari/537.36 Edg/77.0.188.0"); - if (dialog.confirmed) - { - SetUserAgent(dialog.input); - } -} - -// Register a WebResourceRequested handler which adds a custom User-Agent -// HTTP header to all requests. -void SettingsComponent::SetUserAgent(const std::wstring& userAgent) -{ - if (m_changeUserAgent) - { - CHECK_FAILURE(m_webView->remove_WebResourceRequested( - m_webResourceRequestedTokenForUserAgent)); - } - m_overridingUserAgent = userAgent; - if (m_overridingUserAgent.empty()) - { - m_changeUserAgent = false; - } - else - { - m_changeUserAgent = true; - // Register a handler for the WebResourceRequested event. - // This handler adds a User-Agent HTTP header to the request, - // then lets the request continue normally. - m_webView->AddWebResourceRequestedFilter(L"*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL); - m_webView->add_WebResourceRequested( - Callback( - [this](ICoreWebView2* sender, ICoreWebView2WebResourceRequestedEventArgs* args) { - wil::com_ptr request; - CHECK_FAILURE(args->get_Request(&request)); - wil::com_ptr requestHeaders; - CHECK_FAILURE(request->get_Headers(&requestHeaders)); - requestHeaders->SetHeader(L"User-Agent", m_overridingUserAgent.c_str()); - - return S_OK; - }) - .Get(), - &m_webResourceRequestedTokenForUserAgent); - } -} - -void SettingsComponent::CompleteScriptDialogDeferral() -{ - if (m_completeDeferredDialog) - { - m_completeDeferredDialog(); - m_completeDeferredDialog = nullptr; - } -} - -SettingsComponent::~SettingsComponent() -{ - m_webView->remove_NavigationStarting(m_navigationStartingToken); - m_webView->remove_FrameNavigationStarting(m_frameNavigationStartingToken); - m_webView->remove_WebResourceRequested(m_webResourceRequestedTokenForImageBlocking); - m_webView->remove_WebResourceRequested(m_webResourceRequestedTokenForUserAgent); - m_webView->remove_ScriptDialogOpening(m_scriptDialogOpeningToken); - m_webView->remove_PermissionRequested(m_permissionRequestedToken); -} - -// Take advantage of urlmon's URI library to parse a URI -static wil::unique_bstr GetDomainOfUri(PWSTR uri) -{ - wil::com_ptr uriObject; - CreateUri(uri, - Uri_CREATE_CANONICALIZE | Uri_CREATE_NO_DECODE_EXTRA_INFO, - 0, &uriObject); - wil::unique_bstr domain; - uriObject->GetHost(&domain); - return domain; -} - -static PCWSTR NameOfPermissionKind(COREWEBVIEW2_PERMISSION_KIND kind) -{ - switch (kind) - { - case COREWEBVIEW2_PERMISSION_KIND_MICROPHONE: - return L"Microphone"; - case COREWEBVIEW2_PERMISSION_KIND_CAMERA: - return L"Camera"; - case COREWEBVIEW2_PERMISSION_KIND_GEOLOCATION: - return L"Geolocation"; - case COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS: - return L"Notifications"; - case COREWEBVIEW2_PERMISSION_KIND_OTHER_SENSORS: - return L"Generic Sensors"; - case COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ: - return L"Clipboard Read"; - default: - return L"Unknown resources"; - } -} +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "stdafx.h" + +#include "SettingsComponent.h" + +#include "CheckFailure.h" +#include "TextInputDialog.h" + +using namespace Microsoft::WRL; + +// Some utility functions +static wil::unique_bstr GetDomainOfUri(PWSTR uri); +static PCWSTR NameOfPermissionKind(COREWEBVIEW2_PERMISSION_KIND kind); + +SettingsComponent::SettingsComponent( + AppWindow* appWindow, ICoreWebView2Environment* environment, SettingsComponent* old) + : m_appWindow(appWindow), m_webViewEnvironment(environment), + m_webView(appWindow->GetWebView()) +{ + CHECK_FAILURE(m_webView->get_Settings(&m_settings)); + + // Copy old settings if desired + if (old) + { + BOOL setting; + CHECK_FAILURE(old->m_settings->get_IsScriptEnabled(&setting)); + CHECK_FAILURE(m_settings->put_IsScriptEnabled(setting)); + CHECK_FAILURE(old->m_settings->get_IsWebMessageEnabled(&setting)); + CHECK_FAILURE(m_settings->put_IsWebMessageEnabled(setting)); + CHECK_FAILURE(old->m_settings->get_AreDefaultScriptDialogsEnabled(&setting)); + CHECK_FAILURE(m_settings->put_AreDefaultScriptDialogsEnabled(setting)); + CHECK_FAILURE(old->m_settings->get_IsStatusBarEnabled(&setting)); + CHECK_FAILURE(m_settings->put_IsStatusBarEnabled(setting)); + CHECK_FAILURE(old->m_settings->get_AreDevToolsEnabled(&setting)); + CHECK_FAILURE(m_settings->put_AreDevToolsEnabled(setting)); + SetBlockImages(old->m_blockImages); + SetUserAgent(old->m_overridingUserAgent); + m_deferScriptDialogs = old->m_deferScriptDialogs; + m_isScriptEnabled = old->m_isScriptEnabled; + m_blockedSitesSet = old->m_blockedSitesSet; + m_blockedSites = std::move(old->m_blockedSites); + m_overridingUserAgent = std::move(old->m_overridingUserAgent); + } + + //! [NavigationStarting] + // Register a handler for the NavigationStarting event. + // This handler will check the domain being navigated to, and if the domain + // matches a list of blocked sites, it will cancel the navigation and + // possibly display a warning page. It will also disable JavaScript on + // selected websites. + CHECK_FAILURE(m_webView->add_NavigationStarting( + Callback( + [this](ICoreWebView2* sender, + ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT + { + wil::unique_cotaskmem_string uri; + CHECK_FAILURE(args->get_Uri(&uri)); + + if (ShouldBlockUri(uri.get())) + { + CHECK_FAILURE(args->put_Cancel(true)); + + // If the user clicked a link to navigate, show a warning page. + BOOL userInitiated; + CHECK_FAILURE(args->get_IsUserInitiated(&userInitiated)); + //! [NavigateToString] + static const PCWSTR htmlContent = + L"

Domain Blocked

" + L"

You've attempted to navigate to a domain in the blocked " + L"sites list. Press back to return to the previous page.

"; + CHECK_FAILURE(sender->NavigateToString(htmlContent)); + //! [NavigateToString] + } + //! [IsScriptEnabled] + // Changes to settings will apply at the next navigation, which includes the + // navigation after a NavigationStarting event. We can use this to change + // settings according to what site we're visiting. + if (ShouldBlockScriptForUri(uri.get())) + { + m_settings->put_IsScriptEnabled(FALSE); + } + else + { + m_settings->put_IsScriptEnabled(m_isScriptEnabled); + } + //! [IsScriptEnabled] + return S_OK; + }).Get(), &m_navigationStartingToken)); + //! [NavigationStarting] + + //! [FrameNavigationStarting] + // Register a handler for the FrameNavigationStarting event. + // This handler will prevent a frame from navigating to a blocked domain. + CHECK_FAILURE(m_webView->add_FrameNavigationStarting( + Callback( + [this](ICoreWebView2* sender, + ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT + { + wil::unique_cotaskmem_string uri; + CHECK_FAILURE(args->get_Uri(&uri)); + + if (ShouldBlockUri(uri.get())) + { + CHECK_FAILURE(args->put_Cancel(true)); + } + return S_OK; + }).Get(), &m_frameNavigationStartingToken)); + //! [FrameNavigationStarting] + + //! [ScriptDialogOpening] + // Register a handler for the ScriptDialogOpening event. + // This handler will set up a custom prompt dialog for the user, + // and may defer the event if the setting to defer dialogs is enabled. + CHECK_FAILURE(m_webView->add_ScriptDialogOpening( + Callback( + [this]( + ICoreWebView2* sender, + ICoreWebView2ScriptDialogOpeningEventArgs* args) -> HRESULT + { + wil::com_ptr eventArgs = args; + auto showDialog = [this, eventArgs] + { + wil::unique_cotaskmem_string uri; + COREWEBVIEW2_SCRIPT_DIALOG_KIND type; + wil::unique_cotaskmem_string message; + wil::unique_cotaskmem_string defaultText; + + CHECK_FAILURE(eventArgs->get_Uri(&uri)); + CHECK_FAILURE(eventArgs->get_Kind(&type)); + CHECK_FAILURE(eventArgs->get_Message(&message)); + CHECK_FAILURE(eventArgs->get_DefaultText(&defaultText)); + + std::wstring promptString = std::wstring(L"The page at '") + + uri.get() + L"' says:"; + TextInputDialog dialog( + m_appWindow->GetMainWindow(), + L"Script Dialog", + promptString.c_str(), + message.get(), + defaultText.get(), + /* readonly */ type != COREWEBVIEW2_SCRIPT_DIALOG_KIND_PROMPT); + if (dialog.confirmed) + { + CHECK_FAILURE(eventArgs->put_ResultText(dialog.input.c_str())); + CHECK_FAILURE(eventArgs->Accept()); + } + }; + + if (m_deferScriptDialogs) + { + wil::com_ptr deferral; + CHECK_FAILURE(args->GetDeferral(&deferral)); + m_completeDeferredDialog = [showDialog, deferral] + { + showDialog(); + CHECK_FAILURE(deferral->Complete()); + }; + } + else + { + showDialog(); + } + + return S_OK; + }).Get(), &m_scriptDialogOpeningToken)); + //! [ScriptDialogOpening] + + //! [PermissionRequested] + // Register a handler for the PermissionRequested event. + // This handler prompts the user to allow or deny the request. + CHECK_FAILURE(m_webView->add_PermissionRequested( + Callback( + [this]( + ICoreWebView2* sender, + ICoreWebView2PermissionRequestedEventArgs* args) -> HRESULT + { + wil::unique_cotaskmem_string uri; + COREWEBVIEW2_PERMISSION_KIND kind = COREWEBVIEW2_PERMISSION_KIND_UNKNOWN_PERMISSION; + BOOL userInitiated = FALSE; + + CHECK_FAILURE(args->get_Uri(&uri)); + CHECK_FAILURE(args->get_PermissionKind(&kind)); + CHECK_FAILURE(args->get_IsUserInitiated(&userInitiated)); + + std::wstring message = L"Do you want to grant permission for "; + message += NameOfPermissionKind(kind); + message += L" to the website at "; + message += uri.get(); + message += L"?\n\n"; + message += (userInitiated + ? L"This request came from a user gesture." + : L"This request did not come from a user gesture."); + + int response = MessageBox(nullptr, message.c_str(), L"Permission Request", + MB_YESNOCANCEL | MB_ICONWARNING); + + COREWEBVIEW2_PERMISSION_STATE state = + response == IDYES ? COREWEBVIEW2_PERMISSION_STATE_ALLOW + : response == IDNO ? COREWEBVIEW2_PERMISSION_STATE_DENY + : COREWEBVIEW2_PERMISSION_STATE_DEFAULT; + CHECK_FAILURE(args->put_State(state)); + + return S_OK; + }).Get(), &m_permissionRequestedToken)); + //! [PermissionRequested] +} + +bool SettingsComponent::HandleWindowMessage( + HWND hWnd, + UINT message, + WPARAM wParam, + LPARAM lParam, + LRESULT* result) +{ + if (message == WM_COMMAND) + { + switch (LOWORD(wParam)) + { + case ID_BLOCKEDSITES: + { + ChangeBlockedSites(); + return true; + } + case ID_SETTINGS_SETUSERAGENT: + { + ChangeUserAgent(); + return true; + } + case IDM_TOGGLE_JAVASCRIPT: + { + m_isScriptEnabled = !m_isScriptEnabled; + m_settings->put_IsScriptEnabled(m_isScriptEnabled); + MessageBox(nullptr, + (std::wstring(L"JavaScript will be ") + + (m_isScriptEnabled ? L"enabled" : L"disabled") + + L" after the next navigation.").c_str(), + L"Settings change", MB_OK); + return true; + } + case IDM_TOGGLE_WEB_MESSAGING: + { + BOOL isWebMessageEnabled; + m_settings->get_IsWebMessageEnabled(&isWebMessageEnabled); + m_settings->put_IsWebMessageEnabled(!isWebMessageEnabled); + MessageBox(nullptr, + (std::wstring(L"Web Messaging will be ") + + (!isWebMessageEnabled ? L"enabled" : L"disabled") + + L" after the next navigation.").c_str(), + L"Settings change", MB_OK); + return true; + } + case ID_SETTINGS_STATUS_BAR_ENABLED: + { + BOOL isStatusBarEnabled; + m_settings->get_IsStatusBarEnabled(&isStatusBarEnabled); + m_settings->put_IsStatusBarEnabled(!isStatusBarEnabled); + MessageBox(nullptr, + (std::wstring(L"Status bar will be ") + + + (!isStatusBarEnabled ? L"enabled" : L"disabled") + + L" after the next navigation.").c_str(), + L"Settings change", MB_OK); + return true; + } + case ID_SETTINGS_DEV_TOOLS_ENABLED: + { + BOOL areDevToolsEnabled = FALSE; + m_settings->get_AreDevToolsEnabled(&areDevToolsEnabled); + m_settings->put_AreDevToolsEnabled(!areDevToolsEnabled); + MessageBox(nullptr, + (std::wstring(L"Dev tools will be ") + + (!areDevToolsEnabled ? L"enabled" : L"disabled") + + L" after the next navigation.").c_str(), + L"Settings change", MB_OK); + return true; + } + case IDM_USE_DEFAULT_SCRIPT_DIALOGS: + { + BOOL defaultCurrentlyEnabled; + m_settings->get_AreDefaultScriptDialogsEnabled(&defaultCurrentlyEnabled); + if (!defaultCurrentlyEnabled) + { + m_settings->put_AreDefaultScriptDialogsEnabled(TRUE); + MessageBox(nullptr, + L"Default script dialogs will be used after the next navigation.", + L"Settings change", MB_OK); + } + return true; + } + case IDM_USE_CUSTOM_SCRIPT_DIALOGS: + { + BOOL defaultCurrentlyEnabled; + m_settings->get_AreDefaultScriptDialogsEnabled(&defaultCurrentlyEnabled); + if (defaultCurrentlyEnabled) + { + m_settings->put_AreDefaultScriptDialogsEnabled(FALSE); + m_deferScriptDialogs = false; + MessageBox(nullptr, + L"Custom script dialogs without deferral will be used after the next navigation.", + L"Settings change", MB_OK); + } + else if (m_deferScriptDialogs) + { + m_deferScriptDialogs = false; + MessageBox(nullptr, + L"Custom script dialogs without deferral will be used now.", + L"Settings change", MB_OK); + } + return true; + } + case IDM_USE_DEFERRED_SCRIPT_DIALOGS: + { + BOOL defaultCurrentlyEnabled; + m_settings->get_AreDefaultScriptDialogsEnabled(&defaultCurrentlyEnabled); + if (defaultCurrentlyEnabled) + { + m_settings->put_AreDefaultScriptDialogsEnabled(FALSE); + m_deferScriptDialogs = true; + MessageBox(nullptr, + L"Custom script dialogs with deferral will be used after the next navigation.", + L"Settings change", MB_OK); + } + else if (!m_deferScriptDialogs) + { + m_deferScriptDialogs = true; + MessageBox(nullptr, + L"Custom script dialogs with deferral will be used now.", + L"Settings change", MB_OK); + } + return true; + } + case IDM_COMPLETE_JAVASCRIPT_DIALOG: + { + CompleteScriptDialogDeferral(); + return true; + } + case ID_SETTINGS_BLOCKALLIMAGES: + { + SetBlockImages(!m_blockImages); + MessageBox(nullptr, + (std::wstring(L"Image blocking has been ") + + (m_blockImages ? L"enabled." : L"disabled.")) + .c_str(), + L"Settings change", MB_OK); + return true; + } + case ID_SETTINGS_CONTEXT_MENUS_ENABLED: + { + //! [DisableContextMenu] + BOOL allowContextMenus; + CHECK_FAILURE(m_settings->get_AreDefaultContextMenusEnabled( + &allowContextMenus)); + if (allowContextMenus) { + CHECK_FAILURE(m_settings->put_AreDefaultContextMenusEnabled(FALSE)); + MessageBox(nullptr, + L"Context menus will be disabled after the next navigation.", + L"Settings change", MB_OK); + } + else { + CHECK_FAILURE(m_settings->put_AreDefaultContextMenusEnabled(TRUE)); + MessageBox(nullptr, + L"Context menus will be enabled after the next navigation.", + L"Settings change", MB_OK); + } + //! [DisableContextMenu] + return true; + } + case ID_SETTINGS_HOST_OBJECTS_ALLOWED: + { + //! [HostObjectsAccess] + BOOL allowHostObjects; + CHECK_FAILURE(m_settings->get_AreHostObjectsAllowed(&allowHostObjects)); + if (allowHostObjects) + { + CHECK_FAILURE(m_settings->put_AreHostObjectsAllowed(FALSE)); + MessageBox( + nullptr, L"Access to host objects will be denied after the next navigation.", + L"Settings change", MB_OK); + } + else + { + CHECK_FAILURE(m_settings->put_AreHostObjectsAllowed(TRUE)); + MessageBox( + nullptr, L"Access to host objects will be allowed after the next navigation.", + L"Settings change", MB_OK); + } + //! [HostObjectsAccess] + return true; + } + case ID_SETTINGS_ZOOM_ENABLED: + { + //! [DisableZoomControl] + BOOL zoomControlEnabled; + CHECK_FAILURE(m_settings->get_IsZoomControlEnabled(&zoomControlEnabled)); + if (zoomControlEnabled) + { + CHECK_FAILURE(m_settings->put_IsZoomControlEnabled(FALSE)); + MessageBox( + nullptr, L"Zoom control will be disabled after the next navigation.", L"Settings change", + MB_OK); + } + else + { + CHECK_FAILURE(m_settings->put_IsZoomControlEnabled(TRUE)); + MessageBox( + nullptr, L"Zoom control will be enabled after the next navigation.", L"Settings change", + MB_OK); + } + //! [DisableZoomControl] + return true; + } + case ID_SETTINGS_BUILTIN_ERROR_PAGE_ENABLED: + { + //! [BuiltInErrorPageEnabled] + BOOL enabled; + CHECK_FAILURE(m_settings->get_IsBuiltInErrorPageEnabled(&enabled)); + if (enabled) + { + CHECK_FAILURE(m_settings->put_IsBuiltInErrorPageEnabled(FALSE)); + MessageBox( + nullptr, L"Built-in error page will be disabled for future navigation.", + L"Settings change", MB_OK); + } + else + { + CHECK_FAILURE(m_settings->put_IsBuiltInErrorPageEnabled(TRUE)); + MessageBox( + nullptr, L"Built-in error page will be enabled for future navigation.", + L"Settings change", MB_OK); + } + //! [BuiltInErrorPageEnabled] + return true; + } + } + } + return false; +} + +// Prompt the user for a list of blocked domains +void SettingsComponent::ChangeBlockedSites() +{ + std::wstring blockedSitesString; + if (m_blockedSitesSet) + { + for (auto& site : m_blockedSites) + { + if (!blockedSitesString.empty()) + { + blockedSitesString += L";"; + } + blockedSitesString += site; + } + } + else + { + blockedSitesString = L"foo.com;bar.org"; + } + + TextInputDialog dialog( + m_appWindow->GetMainWindow(), + L"Blocked Sites", + L"Sites:", + L"Enter hostnames to block, separated by semicolons.", + blockedSitesString.c_str()); + if (dialog.confirmed) + { + m_blockedSitesSet = true; + m_blockedSites.clear(); + size_t begin = 0; + size_t end = 0; + while (end != std::wstring::npos) + { + end = dialog.input.find(L';', begin); + if (end != begin) + { + m_blockedSites.push_back(dialog.input.substr(begin, end - begin)); + } + begin = end + 1; + } + } +} + +// Check the URI's domain against the blocked sites list +bool SettingsComponent::ShouldBlockUri(PWSTR uri) +{ + wil::unique_bstr domain = GetDomainOfUri(uri); + + for (auto site = m_blockedSites.begin(); + site != m_blockedSites.end(); site++) + { + if (wcscmp(site->c_str(), domain.get()) == 0) + { + return true; + } + } + return false; +} + +// Decide whether a website should have script disabled. Since we're only using this +// for sample code and we don't actually want to break any websites, just return false. +bool SettingsComponent::ShouldBlockScriptForUri(PWSTR uri) +{ + return false; +} + +// Turn on or off image blocking by adding or removing a WebResourceRequested handler +// which selectively intercepts requests for images. +void SettingsComponent::SetBlockImages(bool blockImages) +{ + if (blockImages != m_blockImages) + { + m_blockImages = blockImages; + + //! [WebResourceRequested] + if (m_blockImages) + { + m_webView->AddWebResourceRequestedFilter(L"*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_IMAGE); + CHECK_FAILURE(m_webView->add_WebResourceRequested( + Callback( + [this]( + ICoreWebView2* sender, + ICoreWebView2WebResourceRequestedEventArgs* args) { + COREWEBVIEW2_WEB_RESOURCE_CONTEXT resourceContext; + CHECK_FAILURE( + args->get_ResourceContext(&resourceContext)); + // Ensure that the type is image + if (resourceContext != COREWEBVIEW2_WEB_RESOURCE_CONTEXT_IMAGE) + { + return E_INVALIDARG; + } + // Override the response with an empty one to block the image. + // If put_Response is not called, the request will continue as normal. + wil::com_ptr response; + wil::com_ptr m_webViewExperimental = + m_webView.try_query(); + wil::com_ptr environment; + CHECK_FAILURE(m_webViewExperimental->get_Environment(&environment)); + CHECK_FAILURE(environment->CreateWebResourceResponse( + nullptr, 403 /*NoContent*/, L"Blocked", L"", &response)); + CHECK_FAILURE(args->put_Response(response.get())); + return S_OK; + }) + .Get(), + &m_webResourceRequestedTokenForImageBlocking)); + } + else + { + CHECK_FAILURE(m_webView->remove_WebResourceRequested( + m_webResourceRequestedTokenForImageBlocking)); + } + //! [WebResourceRequested] + } +} + +// Prompt the user for a new User Agent string +void SettingsComponent::ChangeUserAgent() { + TextInputDialog dialog( + m_appWindow->GetMainWindow(), + L"User Agent", + L"User agent:", + L"Enter user agent, or leave blank to restore default.", + m_changeUserAgent + ? m_overridingUserAgent.c_str() + : L"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3818.0 Safari/537.36 Edg/77.0.188.0"); + if (dialog.confirmed) + { + SetUserAgent(dialog.input); + } +} + +// Register a WebResourceRequested handler which adds a custom User-Agent +// HTTP header to all requests. +void SettingsComponent::SetUserAgent(const std::wstring& userAgent) +{ + if (m_changeUserAgent) + { + CHECK_FAILURE(m_webView->remove_WebResourceRequested( + m_webResourceRequestedTokenForUserAgent)); + } + m_overridingUserAgent = userAgent; + if (m_overridingUserAgent.empty()) + { + m_changeUserAgent = false; + } + else + { + m_changeUserAgent = true; + // Register a handler for the WebResourceRequested event. + // This handler adds a User-Agent HTTP header to the request, + // then lets the request continue normally. + m_webView->AddWebResourceRequestedFilter(L"*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL); + m_webView->add_WebResourceRequested( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2WebResourceRequestedEventArgs* args) { + wil::com_ptr request; + CHECK_FAILURE(args->get_Request(&request)); + wil::com_ptr requestHeaders; + CHECK_FAILURE(request->get_Headers(&requestHeaders)); + requestHeaders->SetHeader(L"User-Agent", m_overridingUserAgent.c_str()); + + return S_OK; + }) + .Get(), + &m_webResourceRequestedTokenForUserAgent); + } +} + +void SettingsComponent::CompleteScriptDialogDeferral() +{ + if (m_completeDeferredDialog) + { + m_completeDeferredDialog(); + m_completeDeferredDialog = nullptr; + } +} + +SettingsComponent::~SettingsComponent() +{ + m_webView->remove_NavigationStarting(m_navigationStartingToken); + m_webView->remove_FrameNavigationStarting(m_frameNavigationStartingToken); + m_webView->remove_WebResourceRequested(m_webResourceRequestedTokenForImageBlocking); + m_webView->remove_WebResourceRequested(m_webResourceRequestedTokenForUserAgent); + m_webView->remove_ScriptDialogOpening(m_scriptDialogOpeningToken); + m_webView->remove_PermissionRequested(m_permissionRequestedToken); +} + +// Take advantage of urlmon's URI library to parse a URI +static wil::unique_bstr GetDomainOfUri(PWSTR uri) +{ + wil::com_ptr uriObject; + CreateUri(uri, + Uri_CREATE_CANONICALIZE | Uri_CREATE_NO_DECODE_EXTRA_INFO, + 0, &uriObject); + wil::unique_bstr domain; + uriObject->GetHost(&domain); + return domain; +} + +static PCWSTR NameOfPermissionKind(COREWEBVIEW2_PERMISSION_KIND kind) +{ + switch (kind) + { + case COREWEBVIEW2_PERMISSION_KIND_MICROPHONE: + return L"Microphone"; + case COREWEBVIEW2_PERMISSION_KIND_CAMERA: + return L"Camera"; + case COREWEBVIEW2_PERMISSION_KIND_GEOLOCATION: + return L"Geolocation"; + case COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS: + return L"Notifications"; + case COREWEBVIEW2_PERMISSION_KIND_OTHER_SENSORS: + return L"Generic Sensors"; + case COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ: + return L"Clipboard Read"; + default: + return L"Unknown resources"; + } +} diff --git a/SampleApps/WebView2APISample/Toolbar.cpp b/SampleApps/WebView2APISample/Toolbar.cpp index 4b9fc74d..083b29db 100644 --- a/SampleApps/WebView2APISample/Toolbar.cpp +++ b/SampleApps/WebView2APISample/Toolbar.cpp @@ -1,150 +1,150 @@ -// Copyright (C) Microsoft Corporation. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "stdafx.h" - -#include "Toolbar.h" - -#include "AppWindow.h" -#include "resource.h" - -Toolbar::Toolbar() : m_items{ Item_LAST } {} - -Toolbar::~Toolbar() -{ - if (m_font) - { - DeleteObject(m_font); - } -} - -void Toolbar::Initialize(AppWindow* appWindow) -{ - m_appWindow = appWindow; - HWND mainWindow = m_appWindow->GetMainWindow(); - - m_items[Item_BackButton] = CreateWindow( - L"button", L"Back", WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 0, 0, 0, 0, - mainWindow, (HMENU)IDE_BACK, nullptr, 0); - m_items[Item_ForwardButton] = CreateWindow( - L"button", L"Forward", WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 0, 0, 0, 0, - mainWindow, (HMENU)IDE_FORWARD, nullptr, 0); - m_items[Item_ReloadButton] = CreateWindow( - L"button", L"Reload", WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 0, 0, 0, 0, - mainWindow, (HMENU)IDE_ADDRESSBAR_RELOAD, nullptr, 0); - m_items[Item_CancelButton] = CreateWindow( - L"button", L"Cancel", WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 0, 0, 0, 0, - mainWindow, (HMENU)IDE_CANCEL, nullptr, 0); - m_items[Item_AddressBar] = CreateWindow( - L"edit", nullptr, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 0, 0, 0, 0, - mainWindow, (HMENU)IDE_ADDRESSBAR, nullptr, 0); - m_items[Item_GoButton] = CreateWindow( - L"button", L"Go", WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | BS_DEFPUSHBUTTON, - 0, 0, 0, 0, mainWindow, (HMENU)IDE_ADDRESSBAR_GO, nullptr, 0); - - UpdateDpiAndTextScale(); - RECT availableBounds = { 0 }; - GetClientRect(mainWindow, &availableBounds); - Resize(availableBounds); - - DisableAllItems(); -} - -void Toolbar::SetItemEnabled(Item item, bool enabled) -{ - EnableWindow(m_items[item], enabled); -} - -void Toolbar::DisableAllItems() -{ - for (HWND hwnd : m_items) - { - EnableWindow(hwnd, FALSE); - } -} - -HWND Toolbar::GetItem(Item item) const -{ - return m_items[item]; -} - -const std::vector& Toolbar::GetItems() const -{ - return m_items; -} - -RECT Toolbar::Resize(RECT availableBounds) -{ - const int clientWidth = availableBounds.right - availableBounds.left; - const int clientHeight = availableBounds.bottom - availableBounds.top; - const float dpiScale = m_appWindow->GetDpiScale(); - const int clientLogicalWidth = clientWidth / dpiScale; - const int itemHeight = 32 * dpiScale; - - int nextOffsetX = 0; - - for (Item item = Item_BackButton; item < Item_LAST; item = Item(item + 1)) - { - int itemWidth = GetItemLogicalWidth(item, clientLogicalWidth) * dpiScale; - SetWindowPos(m_items[item], nullptr, nextOffsetX, 0, itemWidth, itemHeight, - SWP_NOZORDER | SWP_NOACTIVATE); - nextOffsetX += itemWidth; - } - - return { 0, itemHeight, clientWidth, clientHeight }; -} - -void Toolbar::UpdateDpiAndTextScale() -{ - UpdateFont(); - for (Item item = Item_BackButton; item < Item_LAST; item = Item(item + 1)) - { - SendMessage(m_items[item], WM_SETFONT, (WPARAM)m_font, FALSE); - } -} - -int Toolbar::GetItemLogicalWidth(Item item, int clientLogicalWidth) const -{ - static const int s_itemButtonLogicalWidth = 64; - - int itemLogicalWidth = 0; - switch (item) - { - case Item_BackButton: - case Item_ForwardButton: - case Item_ReloadButton: - case Item_CancelButton: - case Item_GoButton: - itemLogicalWidth = s_itemButtonLogicalWidth; - break; - case Item_AddressBar: - itemLogicalWidth = clientLogicalWidth - s_itemButtonLogicalWidth * (Item_LAST - 1); - break; - default: - FAIL_FAST(); - } - return itemLogicalWidth; -} - -void Toolbar::UpdateFont() -{ - static const WCHAR s_fontName[] = L"WebView2APISample Font"; - if (m_font) - { - DeleteObject(m_font); - } - LOGFONT logFont; - GetObject(GetStockObject(SYSTEM_FONT), sizeof(LOGFONT), &logFont); - double dpiScale = m_appWindow->GetDpiScale(); -#ifdef USE_WEBVIEW2_WIN10 - double textScale = m_appWindow->GetTextScale(); - logFont.lfHeight *= dpiScale * textScale; - logFont.lfWidth *= dpiScale * textScale; -#else - logFont.lfHeight *= dpiScale; - logFont.lfWidth *= dpiScale; -#endif - StringCchCopy(logFont.lfFaceName, ARRAYSIZE(logFont.lfFaceName), s_fontName); - m_font = CreateFontIndirect(&logFont); -} +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "stdafx.h" + +#include "Toolbar.h" + +#include "AppWindow.h" +#include "resource.h" + +Toolbar::Toolbar() : m_items{ Item_LAST } {} + +Toolbar::~Toolbar() +{ + if (m_font) + { + DeleteObject(m_font); + } +} + +void Toolbar::Initialize(AppWindow* appWindow) +{ + m_appWindow = appWindow; + HWND mainWindow = m_appWindow->GetMainWindow(); + + m_items[Item_BackButton] = CreateWindow( + L"button", L"Back", WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 0, 0, 0, 0, + mainWindow, (HMENU)IDE_BACK, nullptr, 0); + m_items[Item_ForwardButton] = CreateWindow( + L"button", L"Forward", WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 0, 0, 0, 0, + mainWindow, (HMENU)IDE_FORWARD, nullptr, 0); + m_items[Item_ReloadButton] = CreateWindow( + L"button", L"Reload", WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 0, 0, 0, 0, + mainWindow, (HMENU)IDE_ADDRESSBAR_RELOAD, nullptr, 0); + m_items[Item_CancelButton] = CreateWindow( + L"button", L"Cancel", WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 0, 0, 0, 0, + mainWindow, (HMENU)IDE_CANCEL, nullptr, 0); + m_items[Item_AddressBar] = CreateWindow( + L"edit", nullptr, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 0, 0, 0, 0, + mainWindow, (HMENU)IDE_ADDRESSBAR, nullptr, 0); + m_items[Item_GoButton] = CreateWindow( + L"button", L"Go", WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | BS_DEFPUSHBUTTON, + 0, 0, 0, 0, mainWindow, (HMENU)IDE_ADDRESSBAR_GO, nullptr, 0); + + UpdateDpiAndTextScale(); + RECT availableBounds = { 0 }; + GetClientRect(mainWindow, &availableBounds); + Resize(availableBounds); + + DisableAllItems(); +} + +void Toolbar::SetItemEnabled(Item item, bool enabled) +{ + EnableWindow(m_items[item], enabled); +} + +void Toolbar::DisableAllItems() +{ + for (HWND hwnd : m_items) + { + EnableWindow(hwnd, FALSE); + } +} + +HWND Toolbar::GetItem(Item item) const +{ + return m_items[item]; +} + +const std::vector& Toolbar::GetItems() const +{ + return m_items; +} + +RECT Toolbar::Resize(RECT availableBounds) +{ + const int clientWidth = availableBounds.right - availableBounds.left; + const int clientHeight = availableBounds.bottom - availableBounds.top; + const float dpiScale = m_appWindow->GetDpiScale(); + const int clientLogicalWidth = clientWidth / dpiScale; + const int itemHeight = 32 * dpiScale; + + int nextOffsetX = 0; + + for (Item item = Item_BackButton; item < Item_LAST; item = Item(item + 1)) + { + int itemWidth = GetItemLogicalWidth(item, clientLogicalWidth) * dpiScale; + SetWindowPos(m_items[item], nullptr, nextOffsetX, 0, itemWidth, itemHeight, + SWP_NOZORDER | SWP_NOACTIVATE); + nextOffsetX += itemWidth; + } + + return { 0, itemHeight, clientWidth, clientHeight }; +} + +void Toolbar::UpdateDpiAndTextScale() +{ + UpdateFont(); + for (Item item = Item_BackButton; item < Item_LAST; item = Item(item + 1)) + { + SendMessage(m_items[item], WM_SETFONT, (WPARAM)m_font, FALSE); + } +} + +int Toolbar::GetItemLogicalWidth(Item item, int clientLogicalWidth) const +{ + static const int s_itemButtonLogicalWidth = 64; + + int itemLogicalWidth = 0; + switch (item) + { + case Item_BackButton: + case Item_ForwardButton: + case Item_ReloadButton: + case Item_CancelButton: + case Item_GoButton: + itemLogicalWidth = s_itemButtonLogicalWidth; + break; + case Item_AddressBar: + itemLogicalWidth = clientLogicalWidth - s_itemButtonLogicalWidth * (Item_LAST - 1); + break; + default: + FAIL_FAST(); + } + return itemLogicalWidth; +} + +void Toolbar::UpdateFont() +{ + static const WCHAR s_fontName[] = L"WebView2APISample Font"; + if (m_font) + { + DeleteObject(m_font); + } + LOGFONT logFont; + GetObject(GetStockObject(SYSTEM_FONT), sizeof(LOGFONT), &logFont); + double dpiScale = m_appWindow->GetDpiScale(); +#ifdef USE_WEBVIEW2_WIN10 + double textScale = m_appWindow->GetTextScale(); + logFont.lfHeight *= dpiScale * textScale; + logFont.lfWidth *= dpiScale * textScale; +#else + logFont.lfHeight *= dpiScale; + logFont.lfWidth *= dpiScale; +#endif + StringCchCopy(logFont.lfFaceName, ARRAYSIZE(logFont.lfFaceName), s_fontName); + m_font = CreateFontIndirect(&logFont); +} diff --git a/SampleApps/WebView2APISample/ViewComponent.cpp b/SampleApps/WebView2APISample/ViewComponent.cpp index 539a56d9..c16f3c60 100644 --- a/SampleApps/WebView2APISample/ViewComponent.cpp +++ b/SampleApps/WebView2APISample/ViewComponent.cpp @@ -1,672 +1,694 @@ -// Copyright (C) Microsoft Corporation. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "stdafx.h" - -#include "ViewComponent.h" -#include "DCompTargetImpl.h" - -#include -#include -#include -#ifdef USE_WEBVIEW2_WIN10 -#include -#endif - -#include "CheckFailure.h" - -using namespace Microsoft::WRL; -namespace numerics = winrt::Windows::Foundation::Numerics; -static D2D1_MATRIX_4X4_F Convert3x2MatrixTo4x4Matrix(D2D1_MATRIX_3X2_F* matrix3x2); - -ViewComponent::ViewComponent( - AppWindow* appWindow, - IDCompositionDevice* dcompDevice, -#ifdef USE_WEBVIEW2_WIN10 - winrtComp::Compositor wincompCompositor, -#endif - bool isDcompTargetMode) - : m_appWindow(appWindow), m_controller(appWindow->GetWebViewController()), - m_webView(appWindow->GetWebView()), m_dcompDevice(dcompDevice), -#ifdef USE_WEBVIEW2_WIN10 - m_wincompCompositor(wincompCompositor), -#endif - m_isDcompTargetMode(isDcompTargetMode) -{ - //! [ZoomFactorChanged] - // Register a handler for the ZoomFactorChanged event. - // This handler just announces the new level of zoom on the window's title bar. - CHECK_FAILURE(m_controller->add_ZoomFactorChanged( - Callback( - [this](ICoreWebView2Controller* sender, IUnknown* args) -> HRESULT { - double zoomFactor; - CHECK_FAILURE(sender->get_ZoomFactor(&zoomFactor)); - - std::wstring message = L"WebView2APISample (Zoom: " + - std::to_wstring(int(zoomFactor * 100)) + L"%)"; - SetWindowText(m_appWindow->GetMainWindow(), message.c_str()); - return S_OK; - }) - .Get(), - &m_zoomFactorChangedToken)); - //! [ZoomFactorChanged] - - // Set up compositor if we're running in windowless mode - m_compositionController = m_controller.try_query(); - if (m_compositionController) - { - if (m_dcompDevice) - { - //! [SetRootVisualTarget] - // Set the host app visual that the WebView will connect its visual - // tree to. - BuildDCompTreeUsingVisual(); - if (m_isDcompTargetMode) - { - if (!m_dcompTarget) - { - m_dcompTarget = Make(this); - } - CHECK_FAILURE( - m_compositionController->put_RootVisualTarget(m_dcompTarget.get())); - } - else - { - CHECK_FAILURE( - m_compositionController->put_RootVisualTarget(m_dcompWebViewVisual.get())); - } - CHECK_FAILURE(m_dcompDevice->Commit()); - //! [SetRootVisualTarget] - } -#ifdef USE_WEBVIEW2_WIN10 - else if (m_wincompCompositor) - { - BuildWinCompVisualTree(); - CHECK_FAILURE(m_compositionController->put_RootVisualTarget(m_wincompWebViewVisual.as().get())); - } -#endif - else - { - FAIL_FAST(); - } - //! [CursorChanged] - // Register a handler for the CursorChanged event. - CHECK_FAILURE(m_compositionController->add_CursorChanged( - Callback( - [this](ICoreWebView2ExperimentalCompositionController* sender, IUnknown* args) - -> HRESULT { - HRESULT hr = S_OK; - HCURSOR cursor; - CHECK_FAILURE(sender->get_Cursor(&cursor)); - if (SUCCEEDED(hr)) - { - SetClassLongPtr( - m_appWindow->GetMainWindow(), GCLP_HCURSOR, (LONG_PTR)cursor); - } - return hr; - }) - .Get(), - &m_cursorChangedToken)); - //! [CursorChanged] - } -#ifdef USE_WEBVIEW2_WIN10 - else if (m_dcompDevice || m_wincompCompositor) -#else - else if (m_dcompDevice) -#endif - { - FAIL_FAST(); - } - ResizeWebView(); -} -bool ViewComponent::HandleWindowMessage( - HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, LRESULT* result) -{ - if (message == WM_COMMAND) - { - switch (LOWORD(wParam)) - { - case IDM_TOGGLE_VISIBILITY: - ToggleVisibility(); - return true; - case IDM_ZOOM_05: - SetZoomFactor(0.5f); - return true; - case IDM_ZOOM_10: - SetZoomFactor(1.0f); - return true; - case IDM_ZOOM_20: - SetZoomFactor(2.0f); - return true; - case IDM_SIZE_25: - SetSizeRatio(0.5f); - return true; - case IDM_SIZE_50: - SetSizeRatio(0.7071f); - return true; - case IDM_SIZE_75: - SetSizeRatio(0.866f); - return true; - case IDM_SIZE_100: - SetSizeRatio(1.0f); - return true; - case IDM_TRANSFORM_NONE: - SetTransform(TransformType::kIdentity); - return true; - case IDM_TRANSFORM_ROTATE_30DEG: - SetTransform(TransformType::kRotate30Deg); - return true; - case IDM_TRANSFORM_ROTATE_60DEG_DIAG: - SetTransform(TransformType::kRotate60DegDiagonally); - return true; - case IDM_SCALE_50: - SetScale(0.5f); - return true; - case IDM_SCALE_100: - SetScale(1.0f); - return true; - case IDM_SCALE_125: - SetScale(1.25f); - return true; - case IDM_SCALE_150: - SetScale(1.5f); - return true; - case IDM_GET_WEBVIEW_BOUNDS: - ShowWebViewBounds(); - return true; - case IDM_GET_WEBVIEW_ZOOM: - ShowWebViewZoom(); - return true; - } - } - //! [ToggleIsVisibleOnMinimize] - if (message == WM_SYSCOMMAND) - { - if (wParam == SC_MINIMIZE) - { - // Hide the webview when the app window is minimized. - m_controller->put_IsVisible(FALSE); - } - else if (wParam == SC_RESTORE) - { - // When the app window is restored, show the webview - // (unless the user has toggle visibility off). - if (m_isVisible) - { - m_controller->put_IsVisible(TRUE); - } - } - } - //! [ToggleIsVisibleOnMinimize] - if ((message >= WM_MOUSEFIRST && message <= WM_MOUSELAST) || message == WM_MOUSELEAVE) - { - OnMouseMessage(message, wParam, lParam); - } - else if ( - message == WM_POINTERACTIVATE || message == WM_POINTERDOWN || - message == WM_POINTERENTER || message == WM_POINTERLEAVE || message == WM_POINTERUP || - message == WM_POINTERUPDATE) - { - OnPointerMessage(message, wParam, lParam); - } - //! [NotifyParentWindowPositionChanged] - if (message == WM_MOVE || message == WM_MOVING) - { - m_controller->NotifyParentWindowPositionChanged(); - return true; - } - //! [NotifyParentWindowPositionChanged] - return false; -} -//! [ToggleIsVisible] -void ViewComponent::ToggleVisibility() -{ - BOOL visible; - m_controller->get_IsVisible(&visible); - m_isVisible = !visible; - m_controller->put_IsVisible(m_isVisible); -} -//! [ToggleIsVisible] - -void ViewComponent::SetSizeRatio(float ratio) -{ - m_webViewRatio = ratio; - ResizeWebView(); -} - -void ViewComponent::SetZoomFactor(float zoom) -{ - m_webViewZoomFactor = zoom; - m_controller->put_ZoomFactor(zoom); -} - -void ViewComponent::SetBounds(RECT bounds) -{ - m_webViewBounds = bounds; - ResizeWebView(); -} - -//! [SetBoundsAndZoomFactor] -void ViewComponent::SetScale(float scale) -{ - RECT bounds; - CHECK_FAILURE(m_controller->get_Bounds(&bounds)); - double scaleChange = scale / m_webViewScale; - - bounds.bottom = LONG( - (bounds.bottom - bounds.top) * scaleChange + bounds.top); - bounds.right = LONG( - (bounds.right - bounds.left) * scaleChange + bounds.left); - - m_webViewScale = scale; - m_controller->SetBoundsAndZoomFactor(bounds, scale); -} -//! [SetBoundsAndZoomFactor] - -//! [ResizeWebView] -// Update the bounds of the WebView window to fit available space. -void ViewComponent::ResizeWebView() -{ - SIZE webViewSize = { - LONG((m_webViewBounds.right - m_webViewBounds.left) * m_webViewRatio * m_webViewScale), - LONG((m_webViewBounds.bottom - m_webViewBounds.top) * m_webViewRatio * m_webViewScale) }; - - RECT desiredBounds = m_webViewBounds; - desiredBounds.bottom = LONG( - webViewSize.cy + m_webViewBounds.top); - desiredBounds.right = LONG( - webViewSize.cx + m_webViewBounds.left); - - m_controller->put_Bounds(desiredBounds); - if (m_compositionController) - { - POINT webViewOffset = {m_webViewBounds.left, m_webViewBounds.top}; - - if (m_dcompDevice) - { - CHECK_FAILURE(m_dcompRootVisual->SetOffsetX(float(webViewOffset.x))); - CHECK_FAILURE(m_dcompRootVisual->SetOffsetY(float(webViewOffset.y))); - CHECK_FAILURE(m_dcompRootVisual->SetClip( - {0, 0, float(webViewSize.cx), float(webViewSize.cy)})); - CHECK_FAILURE(m_dcompDevice->Commit()); - } -#ifdef USE_WEBVIEW2_WIN10 - else if (m_wincompCompositor) - { - if (m_wincompRootVisual != nullptr) - { - numerics::float2 size = {static_cast(webViewSize.cx), - static_cast(webViewSize.cy)}; - m_wincompRootVisual.Size(size); - - numerics::float3 offset = {static_cast(webViewOffset.x), - static_cast(webViewOffset.y), 0.0f}; - m_wincompRootVisual.Offset(offset); - - winrtComp::IInsetClip insetClip = m_wincompCompositor.CreateInsetClip(); - m_wincompRootVisual.Clip(insetClip.as()); - } - } -#endif - } -} -//! [ResizeWebView] - -// Show the current bounds of the WebView. -void ViewComponent::ShowWebViewBounds() -{ - RECT bounds; - HRESULT result = m_controller->get_Bounds(&bounds); - if (SUCCEEDED(result)) - { - std::wstringstream message; - message << L"Left:\t" << bounds.left << L"\n" - << L"Top:\t" << bounds.top << L"\n" - << L"Right:\t" << bounds.right << L"\n" - << L"Bottom:\t" << bounds.bottom << std::endl; - MessageBox(nullptr, message.str().c_str(), L"WebView Bounds", MB_OK); - } -} - -// Show the current zoom factor of the WebView. -void ViewComponent::ShowWebViewZoom() -{ - double zoomFactor; - HRESULT result = m_controller->get_ZoomFactor(&zoomFactor); - if (SUCCEEDED(result)) - { - std::wstringstream message; - message << L"Zoom Factor:\t" << zoomFactor << std::endl; - MessageBox(nullptr, message.str().c_str(), L"WebView Zoom Factor", MB_OK); - } -} - -void ViewComponent::SetTransform(TransformType transformType) -{ - if (!m_compositionController) - { - MessageBox( - nullptr, - L"Setting transform is not supported in windowed mode." - "Choose a windowless mode for creation before trying to apply a transform.", - L"Applying transform failed.", MB_OK); - return; - } - - if (transformType == TransformType::kRotate30Deg) - { - D2D1_POINT_2F center = D2D1::Point2F( - (m_webViewBounds.right - m_webViewBounds.left) / 2.f, - (m_webViewBounds.bottom - m_webViewBounds.top) / 2.f); - m_webViewTransformMatrix = - Convert3x2MatrixTo4x4Matrix(&D2D1::Matrix3x2F::Rotation(30.0f, center)); - } - else if (transformType == TransformType::kRotate60DegDiagonally) - { - m_webViewTransformMatrix = D2D1::Matrix4x4F::RotationArbitraryAxis( - float(m_webViewBounds.right), float(m_webViewBounds.bottom), 0, 60); - } - else if (transformType == TransformType::kIdentity) - { - m_webViewTransformMatrix = D2D1::Matrix4x4F(); - } - -#ifdef USE_WEBVIEW2_WIN10 - if (m_dcompDevice && !m_wincompCompositor) -#else - if (m_dcompDevice) -#endif - { - wil::com_ptr dcompWebViewVisual3; - m_dcompWebViewVisual->QueryInterface(IID_PPV_ARGS(&dcompWebViewVisual3)); - CHECK_FAILURE(dcompWebViewVisual3->SetTransform(m_webViewTransformMatrix)); - CHECK_FAILURE(m_dcompDevice->Commit()); - } -#ifdef USE_WEBVIEW2_WIN10 - else if (m_wincompCompositor && !m_dcompDevice) - { - if (m_wincompWebViewVisual != nullptr) - { - m_wincompWebViewVisual.TransformMatrix( - *reinterpret_cast(&m_webViewTransformMatrix)); - } - } -#endif - else - { - FAIL_FAST(); - } -} - -static D2D1_MATRIX_4X4_F Convert3x2MatrixTo4x4Matrix(D2D1_MATRIX_3X2_F* matrix3x2) -{ - D2D1_MATRIX_4X4_F matrix4x4 = D2D1::Matrix4x4F(); - matrix4x4._11 = matrix3x2->m11; - matrix4x4._12 = matrix3x2->m12; - matrix4x4._21 = matrix3x2->m21; - matrix4x4._22 = matrix3x2->m22; - matrix4x4._41 = matrix3x2->dx; - matrix4x4._42 = matrix3x2->dy; - return matrix4x4; -} - -//! [SendMouseInput] -bool ViewComponent::OnMouseMessage(UINT message, WPARAM wParam, LPARAM lParam) -{ - // Manually relay mouse messages to the WebView -#ifdef USE_WEBVIEW2_WIN10 - if (m_dcompDevice || m_wincompCompositor) -#else - if (m_dcompDevice) -#endif - { - POINT point; - POINTSTOPOINT(point, lParam); - if (message == WM_MOUSEWHEEL || message == WM_MOUSEHWHEEL) - { - // Mouse wheel messages are delivered in screen coordinates. - // SendMouseInput expects client coordinates for the WebView, so convert - // the point from screen to client. - ::ScreenToClient(m_appWindow->GetMainWindow(), &point); - } - // Send the message to the WebView if the mouse location is inside the - // bounds of the WebView, if the message is telling the WebView the - // mouse has left the client area, or if we are currently capturing - // mouse events. - bool isMouseInWebView = PtInRect(&m_webViewBounds, point); - if (isMouseInWebView || message == WM_MOUSELEAVE || m_isCapturingMouse) - { - DWORD mouseData = 0; - - switch (message) - { - case WM_MOUSEWHEEL: - case WM_MOUSEHWHEEL: - mouseData = GET_WHEEL_DELTA_WPARAM(wParam); - break; - case WM_XBUTTONDBLCLK: - case WM_XBUTTONDOWN: - case WM_XBUTTONUP: - mouseData = GET_XBUTTON_WPARAM(wParam); - break; - case WM_MOUSEMOVE: - if (!m_isTrackingMouse) - { - // WebView needs to know when the mouse leaves the client area - // so that it can dismiss hover popups. TrackMouseEvent will - // provide a notification when the mouse leaves the client area. - TrackMouseEvents(TME_LEAVE); - m_isTrackingMouse = true; - } - break; - case WM_MOUSELEAVE: - m_isTrackingMouse = false; - break; - } - - // We need to capture the mouse in case the user drags the - // mouse outside of the window bounds and we still need to send - // mouse messages to the WebView process. This is useful for - // scenarios like dragging the scroll bar or panning a map. - // This is very similar to the Pointer Message case where a - // press started inside of the WebView. - if (message == WM_LBUTTONDOWN || message == WM_MBUTTONDOWN || - message == WM_RBUTTONDOWN || message == WM_XBUTTONDOWN) - { - if (isMouseInWebView && ::GetCapture() != m_appWindow->GetMainWindow()) - { - m_isCapturingMouse = true; - ::SetCapture(m_appWindow->GetMainWindow()); - } - } - else if (message == WM_LBUTTONUP || message == WM_MBUTTONUP || - message == WM_RBUTTONUP || message == WM_XBUTTONUP) - { - if (::GetCapture() == m_appWindow->GetMainWindow()) - { - m_isCapturingMouse = false; - ::ReleaseCapture(); - } - } - - // Adjust the point from app client coordinates to webview client coordinates. - // WM_MOUSELEAVE messages don't have a point, so don't adjust the point. - if (message != WM_MOUSELEAVE) - { - point.x -= m_webViewBounds.left; - point.y -= m_webViewBounds.top; - } - - CHECK_FAILURE(m_compositionController->SendMouseInput( - static_cast(message), - static_cast(GET_KEYSTATE_WPARAM(wParam)), - mouseData, point)); - return true; - } - else if (message == WM_MOUSEMOVE && m_isTrackingMouse) - { - // When the mouse moves outside of the WebView, but still inside the app - // turn off mouse tracking and send the WebView a leave event. - m_isTrackingMouse = false; - TrackMouseEvents(TME_LEAVE | TME_CANCEL); - OnMouseMessage(WM_MOUSELEAVE, 0, 0); - } - } - return false; -} -//! [SendMouseInput] - -bool ViewComponent::OnPointerMessage(UINT message, WPARAM wParam, LPARAM lParam) -{ - bool handled = false; -#ifdef USE_WEBVIEW2_WIN10 - if (m_dcompDevice || m_wincompCompositor) -#else - if (m_dcompDevice) -#endif - { - POINT point; - POINTSTOPOINT(point, lParam); - UINT pointerId = GET_POINTERID_WPARAM(wParam); - - ::ScreenToClient(m_appWindow->GetMainWindow(), &point); - - bool pointerStartedInWebView = m_pointerIdsStartingInWebView.find(pointerId) != - m_pointerIdsStartingInWebView.end(); - // We want to send pointer input to the WebView for all pointers that either is in the - // WebView or started inside the WebView. For example, if a user started a page scroll - // inside of the WebView but dragged their finger outside of the WebView, we need to - // keep sending pointer events for those pointers. - if (PtInRect(&m_webViewBounds, point) || pointerStartedInWebView) - { - if (!pointerStartedInWebView && - (message == WM_POINTERENTER || message == WM_POINTERDOWN)) - { - m_pointerIdsStartingInWebView.insert(pointerId); - } - else if (message == WM_POINTERLEAVE) - { - m_pointerIdsStartingInWebView.erase(pointerId); - } - - handled = true; - wil::com_ptr pointer_info; - COREWEBVIEW2_MATRIX_4X4* webviewMatrix = - reinterpret_cast(&m_webViewTransformMatrix); - CHECK_FAILURE(m_compositionController->CreateCoreWebView2PointerInfoFromPointerId( - pointerId, m_appWindow->GetMainWindow(), *webviewMatrix, &pointer_info)); - CHECK_FAILURE(m_compositionController->SendPointerInput( - static_cast(message), pointer_info.get())); - } - } - return handled; -} - -void ViewComponent::TrackMouseEvents(DWORD mouseTrackingFlags) -{ - TRACKMOUSEEVENT tme; - tme.cbSize = sizeof(tme); - tme.dwFlags = mouseTrackingFlags; - tme.hwndTrack = m_appWindow->GetMainWindow(); - tme.dwHoverTime = 0; - ::TrackMouseEvent(&tme); -} - -//! [BuildDCompTree] -// Create host app visual that the WebView will connect to. -// - Create a IDCompositionTarget for the host window -// - Create a visual and set that as the IDCompositionTarget's root -// - Create another visual and add that to the IDCompositionTarget's root. -// This visual will be the visual root for the WebView. -void ViewComponent::BuildDCompTreeUsingVisual() -{ - CHECK_FAILURE_BOOL(m_dcompDevice != nullptr); - - if (m_dcompWebViewVisual == nullptr) - { - CHECK_FAILURE(m_dcompDevice->CreateTargetForHwnd( - m_appWindow->GetMainWindow(), TRUE, &m_dcompHwndTarget)); - CHECK_FAILURE(m_dcompDevice->CreateVisual(&m_dcompRootVisual)); - CHECK_FAILURE(m_dcompHwndTarget->SetRoot(m_dcompRootVisual.get())); - CHECK_FAILURE(m_dcompDevice->CreateVisual(&m_dcompWebViewVisual)); - CHECK_FAILURE(m_dcompRootVisual->AddVisual(m_dcompWebViewVisual.get(), TRUE, nullptr)); - } -} -//! [BuildDCompTree] - -void ViewComponent::DestroyDCompVisualTree() -{ - if (m_dcompWebViewVisual) - { - m_dcompWebViewVisual->RemoveAllVisuals(); - m_dcompWebViewVisual.reset(); - - m_dcompRootVisual->RemoveAllVisuals(); - m_dcompRootVisual.reset(); - - m_dcompHwndTarget->SetRoot(nullptr); - m_dcompHwndTarget.reset(); - - m_dcompDevice->Commit(); - } - - if (m_dcompTarget) - { - m_dcompTarget->RemoveOwnerRef(); - m_dcompTarget = nullptr; - } -} - -#ifdef USE_WEBVIEW2_WIN10 -void ViewComponent::BuildWinCompVisualTree() -{ - namespace abiComp = ABI::Windows::UI::Composition; - - if (m_wincompWebViewVisual == nullptr) - { - auto interop = m_wincompCompositor.as(); - winrt::check_hresult(interop->CreateDesktopWindowTarget( - m_appWindow->GetMainWindow(), false, - reinterpret_cast(winrt::put_abi(m_wincompHwndTarget)))); - - m_wincompRootVisual = m_wincompCompositor.CreateContainerVisual(); - m_wincompHwndTarget.Root(m_wincompRootVisual); - - m_wincompWebViewVisual = m_wincompCompositor.CreateContainerVisual(); - m_wincompRootVisual.Children().InsertAtTop(m_wincompWebViewVisual); - } -} - -void ViewComponent::DestroyWinCompVisualTree() -{ - if (m_wincompWebViewVisual != nullptr) - { - m_wincompWebViewVisual.Children().RemoveAll(); - m_wincompWebViewVisual = nullptr; - - m_wincompRootVisual.Children().RemoveAll(); - m_wincompRootVisual = nullptr; - - m_wincompHwndTarget.Root(nullptr); - m_wincompHwndTarget = nullptr; - } -} -#endif - -ViewComponent::~ViewComponent() -{ - m_controller->remove_ZoomFactorChanged(m_zoomFactorChangedToken); - if (m_compositionController) - { - m_compositionController->remove_CursorChanged(m_cursorChangedToken); - // If the webview closes because the AppWindow is closed (as opposed to being closed - // explicitly), this will no-op because in this case, the webview closes before the ViewComponent - // is destroyed. If the webview is closed explicitly, this will succeed. - m_compositionController->put_RootVisualTarget(nullptr); - DestroyDCompVisualTree(); -#ifdef USE_WEBVIEW2_WIN10 - DestroyWinCompVisualTree(); -#endif - } -} +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "stdafx.h" + +#include "ViewComponent.h" +#include "DCompTargetImpl.h" + +#include +#include +#include +#ifdef USE_WEBVIEW2_WIN10 +#include +#endif + +#include "CheckFailure.h" + +using namespace Microsoft::WRL; +namespace numerics = winrt::Windows::Foundation::Numerics; +static D2D1_MATRIX_4X4_F Convert3x2MatrixTo4x4Matrix(D2D1_MATRIX_3X2_F* matrix3x2); + +ViewComponent::ViewComponent( + AppWindow* appWindow, + IDCompositionDevice* dcompDevice, +#ifdef USE_WEBVIEW2_WIN10 + winrtComp::Compositor wincompCompositor, +#endif + bool isDcompTargetMode) + : m_appWindow(appWindow), m_controller(appWindow->GetWebViewController()), + m_webView(appWindow->GetWebView()), m_dcompDevice(dcompDevice), +#ifdef USE_WEBVIEW2_WIN10 + m_wincompCompositor(wincompCompositor), +#endif + m_isDcompTargetMode(isDcompTargetMode) +{ + //! [ZoomFactorChanged] + // Register a handler for the ZoomFactorChanged event. + // This handler just announces the new level of zoom on the window's title bar. + CHECK_FAILURE(m_controller->add_ZoomFactorChanged( + Callback( + [this](ICoreWebView2Controller* sender, IUnknown* args) -> HRESULT { + double zoomFactor; + CHECK_FAILURE(sender->get_ZoomFactor(&zoomFactor)); + + std::wstring message = L"WebView2APISample (Zoom: " + + std::to_wstring(int(zoomFactor * 100)) + L"%)"; + SetWindowText(m_appWindow->GetMainWindow(), message.c_str()); + return S_OK; + }) + .Get(), + &m_zoomFactorChangedToken)); + //! [ZoomFactorChanged] + + // Set up compositor if we're running in windowless mode + m_compositionController = m_controller.try_query(); + if (m_compositionController) + { + if (m_dcompDevice) + { + //! [SetRootVisualTarget] + // Set the host app visual that the WebView will connect its visual + // tree to. + BuildDCompTreeUsingVisual(); + if (m_isDcompTargetMode) + { + if (!m_dcompTarget) + { + m_dcompTarget = Make(this); + } + CHECK_FAILURE( + m_compositionController->put_RootVisualTarget(m_dcompTarget.get())); + } + else + { + CHECK_FAILURE( + m_compositionController->put_RootVisualTarget(m_dcompWebViewVisual.get())); + } + CHECK_FAILURE(m_dcompDevice->Commit()); + //! [SetRootVisualTarget] + } +#ifdef USE_WEBVIEW2_WIN10 + else if (m_wincompCompositor) + { + BuildWinCompVisualTree(); + CHECK_FAILURE(m_compositionController->put_RootVisualTarget(m_wincompWebViewVisual.as().get())); + } +#endif + else + { + FAIL_FAST(); + } + //! [CursorChanged] + // Register a handler for the CursorChanged event. + CHECK_FAILURE(m_compositionController->add_CursorChanged( + Callback( + [this](ICoreWebView2ExperimentalCompositionController* sender, IUnknown* args) + -> HRESULT { + HRESULT hr = S_OK; + HCURSOR cursor; + if (!m_useCursorId) + { + CHECK_FAILURE(sender->get_Cursor(&cursor)); + } + else + { + //! [SystemCursorId] + UINT32 cursorId; + wil::com_ptr compositionController2 = + m_controller.query(); + CHECK_FAILURE(compositionController2->get_SystemCursorId(&cursorId)); + cursor = ::LoadCursor(nullptr, MAKEINTRESOURCE(cursorId)); + if (cursor == nullptr) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + //! [SystemCursorId] + } + + if (SUCCEEDED(hr)) + { + SetClassLongPtr( + m_appWindow->GetMainWindow(), GCLP_HCURSOR, (LONG_PTR)cursor); + } + return hr; + }) + .Get(), + &m_cursorChangedToken)); + //! [CursorChanged] + } +#ifdef USE_WEBVIEW2_WIN10 + else if (m_dcompDevice || m_wincompCompositor) +#else + else if (m_dcompDevice) +#endif + { + FAIL_FAST(); + } + + ResizeWebView(); +} +bool ViewComponent::HandleWindowMessage( + HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, LRESULT* result) +{ + if (message == WM_COMMAND) + { + switch (LOWORD(wParam)) + { + case IDM_TOGGLE_VISIBILITY: + ToggleVisibility(); + return true; + case IDM_ZOOM_05: + SetZoomFactor(0.5f); + return true; + case IDM_ZOOM_10: + SetZoomFactor(1.0f); + return true; + case IDM_ZOOM_20: + SetZoomFactor(2.0f); + return true; + case IDM_SIZE_25: + SetSizeRatio(0.5f); + return true; + case IDM_SIZE_50: + SetSizeRatio(0.7071f); + return true; + case IDM_SIZE_75: + SetSizeRatio(0.866f); + return true; + case IDM_SIZE_100: + SetSizeRatio(1.0f); + return true; + case IDM_TRANSFORM_NONE: + SetTransform(TransformType::kIdentity); + return true; + case IDM_TRANSFORM_ROTATE_30DEG: + SetTransform(TransformType::kRotate30Deg); + return true; + case IDM_TRANSFORM_ROTATE_60DEG_DIAG: + SetTransform(TransformType::kRotate60DegDiagonally); + return true; + case IDM_SCALE_50: + SetScale(0.5f); + return true; + case IDM_SCALE_100: + SetScale(1.0f); + return true; + case IDM_SCALE_125: + SetScale(1.25f); + return true; + case IDM_SCALE_150: + SetScale(1.5f); + return true; + case IDM_GET_WEBVIEW_BOUNDS: + ShowWebViewBounds(); + return true; + case IDM_GET_WEBVIEW_ZOOM: + ShowWebViewZoom(); + return true; + case IDM_TOGGLE_CURSOR_HANDLING: + m_useCursorId = !m_useCursorId; + return true; + } + } + //! [ToggleIsVisibleOnMinimize] + if (message == WM_SYSCOMMAND) + { + if (wParam == SC_MINIMIZE) + { + // Hide the webview when the app window is minimized. + m_controller->put_IsVisible(FALSE); + } + else if (wParam == SC_RESTORE) + { + // When the app window is restored, show the webview + // (unless the user has toggle visibility off). + if (m_isVisible) + { + m_controller->put_IsVisible(TRUE); + } + } + } + //! [ToggleIsVisibleOnMinimize] + if ((message >= WM_MOUSEFIRST && message <= WM_MOUSELAST) || message == WM_MOUSELEAVE) + { + OnMouseMessage(message, wParam, lParam); + } + else if ( + message == WM_POINTERACTIVATE || message == WM_POINTERDOWN || + message == WM_POINTERENTER || message == WM_POINTERLEAVE || message == WM_POINTERUP || + message == WM_POINTERUPDATE) + { + OnPointerMessage(message, wParam, lParam); + } + //! [NotifyParentWindowPositionChanged] + if (message == WM_MOVE || message == WM_MOVING) + { + m_controller->NotifyParentWindowPositionChanged(); + return true; + } + //! [NotifyParentWindowPositionChanged] + return false; +} +//! [ToggleIsVisible] +void ViewComponent::ToggleVisibility() +{ + BOOL visible; + m_controller->get_IsVisible(&visible); + m_isVisible = !visible; + m_controller->put_IsVisible(m_isVisible); +} +//! [ToggleIsVisible] + +void ViewComponent::SetSizeRatio(float ratio) +{ + m_webViewRatio = ratio; + ResizeWebView(); +} + +void ViewComponent::SetZoomFactor(float zoom) +{ + m_webViewZoomFactor = zoom; + m_controller->put_ZoomFactor(zoom); +} + +void ViewComponent::SetBounds(RECT bounds) +{ + m_webViewBounds = bounds; + ResizeWebView(); +} + +//! [SetBoundsAndZoomFactor] +void ViewComponent::SetScale(float scale) +{ + RECT bounds; + CHECK_FAILURE(m_controller->get_Bounds(&bounds)); + double scaleChange = scale / m_webViewScale; + + bounds.bottom = LONG( + (bounds.bottom - bounds.top) * scaleChange + bounds.top); + bounds.right = LONG( + (bounds.right - bounds.left) * scaleChange + bounds.left); + + m_webViewScale = scale; + m_controller->SetBoundsAndZoomFactor(bounds, scale); +} +//! [SetBoundsAndZoomFactor] + +//! [ResizeWebView] +// Update the bounds of the WebView window to fit available space. +void ViewComponent::ResizeWebView() +{ + SIZE webViewSize = { + LONG((m_webViewBounds.right - m_webViewBounds.left) * m_webViewRatio * m_webViewScale), + LONG((m_webViewBounds.bottom - m_webViewBounds.top) * m_webViewRatio * m_webViewScale) }; + + RECT desiredBounds = m_webViewBounds; + desiredBounds.bottom = LONG( + webViewSize.cy + m_webViewBounds.top); + desiredBounds.right = LONG( + webViewSize.cx + m_webViewBounds.left); + + m_controller->put_Bounds(desiredBounds); + if (m_compositionController) + { + POINT webViewOffset = {m_webViewBounds.left, m_webViewBounds.top}; + + if (m_dcompDevice) + { + CHECK_FAILURE(m_dcompRootVisual->SetOffsetX(float(webViewOffset.x))); + CHECK_FAILURE(m_dcompRootVisual->SetOffsetY(float(webViewOffset.y))); + CHECK_FAILURE(m_dcompRootVisual->SetClip( + {0, 0, float(webViewSize.cx), float(webViewSize.cy)})); + CHECK_FAILURE(m_dcompDevice->Commit()); + } +#ifdef USE_WEBVIEW2_WIN10 + else if (m_wincompCompositor) + { + if (m_wincompRootVisual != nullptr) + { + numerics::float2 size = {static_cast(webViewSize.cx), + static_cast(webViewSize.cy)}; + m_wincompRootVisual.Size(size); + + numerics::float3 offset = {static_cast(webViewOffset.x), + static_cast(webViewOffset.y), 0.0f}; + m_wincompRootVisual.Offset(offset); + + winrtComp::IInsetClip insetClip = m_wincompCompositor.CreateInsetClip(); + m_wincompRootVisual.Clip(insetClip.as()); + } + } +#endif + } +} +//! [ResizeWebView] + +// Show the current bounds of the WebView. +void ViewComponent::ShowWebViewBounds() +{ + RECT bounds; + HRESULT result = m_controller->get_Bounds(&bounds); + if (SUCCEEDED(result)) + { + std::wstringstream message; + message << L"Left:\t" << bounds.left << L"\n" + << L"Top:\t" << bounds.top << L"\n" + << L"Right:\t" << bounds.right << L"\n" + << L"Bottom:\t" << bounds.bottom << std::endl; + MessageBox(nullptr, message.str().c_str(), L"WebView Bounds", MB_OK); + } +} + +// Show the current zoom factor of the WebView. +void ViewComponent::ShowWebViewZoom() +{ + double zoomFactor; + HRESULT result = m_controller->get_ZoomFactor(&zoomFactor); + if (SUCCEEDED(result)) + { + std::wstringstream message; + message << L"Zoom Factor:\t" << zoomFactor << std::endl; + MessageBox(nullptr, message.str().c_str(), L"WebView Zoom Factor", MB_OK); + } +} + +void ViewComponent::SetTransform(TransformType transformType) +{ + if (!m_compositionController) + { + MessageBox( + nullptr, + L"Setting transform is not supported in windowed mode." + "Choose a windowless mode for creation before trying to apply a transform.", + L"Applying transform failed.", MB_OK); + return; + } + + if (transformType == TransformType::kRotate30Deg) + { + D2D1_POINT_2F center = D2D1::Point2F( + (m_webViewBounds.right - m_webViewBounds.left) / 2.f, + (m_webViewBounds.bottom - m_webViewBounds.top) / 2.f); + m_webViewTransformMatrix = + Convert3x2MatrixTo4x4Matrix(&D2D1::Matrix3x2F::Rotation(30.0f, center)); + } + else if (transformType == TransformType::kRotate60DegDiagonally) + { + m_webViewTransformMatrix = D2D1::Matrix4x4F::RotationArbitraryAxis( + float(m_webViewBounds.right), float(m_webViewBounds.bottom), 0, 60); + } + else if (transformType == TransformType::kIdentity) + { + m_webViewTransformMatrix = D2D1::Matrix4x4F(); + } + +#ifdef USE_WEBVIEW2_WIN10 + if (m_dcompDevice && !m_wincompCompositor) +#else + if (m_dcompDevice) +#endif + { + wil::com_ptr dcompWebViewVisual3; + m_dcompWebViewVisual->QueryInterface(IID_PPV_ARGS(&dcompWebViewVisual3)); + CHECK_FAILURE(dcompWebViewVisual3->SetTransform(m_webViewTransformMatrix)); + CHECK_FAILURE(m_dcompDevice->Commit()); + } +#ifdef USE_WEBVIEW2_WIN10 + else if (m_wincompCompositor && !m_dcompDevice) + { + if (m_wincompWebViewVisual != nullptr) + { + m_wincompWebViewVisual.TransformMatrix( + *reinterpret_cast(&m_webViewTransformMatrix)); + } + } +#endif + else + { + FAIL_FAST(); + } +} + +static D2D1_MATRIX_4X4_F Convert3x2MatrixTo4x4Matrix(D2D1_MATRIX_3X2_F* matrix3x2) +{ + D2D1_MATRIX_4X4_F matrix4x4 = D2D1::Matrix4x4F(); + matrix4x4._11 = matrix3x2->m11; + matrix4x4._12 = matrix3x2->m12; + matrix4x4._21 = matrix3x2->m21; + matrix4x4._22 = matrix3x2->m22; + matrix4x4._41 = matrix3x2->dx; + matrix4x4._42 = matrix3x2->dy; + return matrix4x4; +} + +//! [SendMouseInput] +bool ViewComponent::OnMouseMessage(UINT message, WPARAM wParam, LPARAM lParam) +{ + // Manually relay mouse messages to the WebView +#ifdef USE_WEBVIEW2_WIN10 + if (m_dcompDevice || m_wincompCompositor) +#else + if (m_dcompDevice) +#endif + { + POINT point; + POINTSTOPOINT(point, lParam); + if (message == WM_MOUSEWHEEL || message == WM_MOUSEHWHEEL) + { + // Mouse wheel messages are delivered in screen coordinates. + // SendMouseInput expects client coordinates for the WebView, so convert + // the point from screen to client. + ::ScreenToClient(m_appWindow->GetMainWindow(), &point); + } + // Send the message to the WebView if the mouse location is inside the + // bounds of the WebView, if the message is telling the WebView the + // mouse has left the client area, or if we are currently capturing + // mouse events. + bool isMouseInWebView = PtInRect(&m_webViewBounds, point); + if (isMouseInWebView || message == WM_MOUSELEAVE || m_isCapturingMouse) + { + DWORD mouseData = 0; + + switch (message) + { + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: + mouseData = GET_WHEEL_DELTA_WPARAM(wParam); + break; + case WM_XBUTTONDBLCLK: + case WM_XBUTTONDOWN: + case WM_XBUTTONUP: + mouseData = GET_XBUTTON_WPARAM(wParam); + break; + case WM_MOUSEMOVE: + if (!m_isTrackingMouse) + { + // WebView needs to know when the mouse leaves the client area + // so that it can dismiss hover popups. TrackMouseEvent will + // provide a notification when the mouse leaves the client area. + TrackMouseEvents(TME_LEAVE); + m_isTrackingMouse = true; + } + break; + case WM_MOUSELEAVE: + m_isTrackingMouse = false; + break; + } + + // We need to capture the mouse in case the user drags the + // mouse outside of the window bounds and we still need to send + // mouse messages to the WebView process. This is useful for + // scenarios like dragging the scroll bar or panning a map. + // This is very similar to the Pointer Message case where a + // press started inside of the WebView. + if (message == WM_LBUTTONDOWN || message == WM_MBUTTONDOWN || + message == WM_RBUTTONDOWN || message == WM_XBUTTONDOWN) + { + if (isMouseInWebView && ::GetCapture() != m_appWindow->GetMainWindow()) + { + m_isCapturingMouse = true; + ::SetCapture(m_appWindow->GetMainWindow()); + } + } + else if (message == WM_LBUTTONUP || message == WM_MBUTTONUP || + message == WM_RBUTTONUP || message == WM_XBUTTONUP) + { + if (::GetCapture() == m_appWindow->GetMainWindow()) + { + m_isCapturingMouse = false; + ::ReleaseCapture(); + } + } + + // Adjust the point from app client coordinates to webview client coordinates. + // WM_MOUSELEAVE messages don't have a point, so don't adjust the point. + if (message != WM_MOUSELEAVE) + { + point.x -= m_webViewBounds.left; + point.y -= m_webViewBounds.top; + } + + CHECK_FAILURE(m_compositionController->SendMouseInput( + static_cast(message), + static_cast(GET_KEYSTATE_WPARAM(wParam)), + mouseData, point)); + return true; + } + else if (message == WM_MOUSEMOVE && m_isTrackingMouse) + { + // When the mouse moves outside of the WebView, but still inside the app + // turn off mouse tracking and send the WebView a leave event. + m_isTrackingMouse = false; + TrackMouseEvents(TME_LEAVE | TME_CANCEL); + OnMouseMessage(WM_MOUSELEAVE, 0, 0); + } + } + return false; +} +//! [SendMouseInput] + +bool ViewComponent::OnPointerMessage(UINT message, WPARAM wParam, LPARAM lParam) +{ + bool handled = false; +#ifdef USE_WEBVIEW2_WIN10 + if (m_dcompDevice || m_wincompCompositor) +#else + if (m_dcompDevice) +#endif + { + POINT point; + POINTSTOPOINT(point, lParam); + UINT pointerId = GET_POINTERID_WPARAM(wParam); + + ::ScreenToClient(m_appWindow->GetMainWindow(), &point); + + bool pointerStartedInWebView = m_pointerIdsStartingInWebView.find(pointerId) != + m_pointerIdsStartingInWebView.end(); + // We want to send pointer input to the WebView for all pointers that either is in the + // WebView or started inside the WebView. For example, if a user started a page scroll + // inside of the WebView but dragged their finger outside of the WebView, we need to + // keep sending pointer events for those pointers. + if (PtInRect(&m_webViewBounds, point) || pointerStartedInWebView) + { + if (!pointerStartedInWebView && + (message == WM_POINTERENTER || message == WM_POINTERDOWN)) + { + m_pointerIdsStartingInWebView.insert(pointerId); + } + else if (message == WM_POINTERLEAVE) + { + m_pointerIdsStartingInWebView.erase(pointerId); + } + + handled = true; + wil::com_ptr pointer_info; + COREWEBVIEW2_MATRIX_4X4* webviewMatrix = + reinterpret_cast(&m_webViewTransformMatrix); + CHECK_FAILURE(m_compositionController->CreateCoreWebView2PointerInfoFromPointerId( + pointerId, m_appWindow->GetMainWindow(), *webviewMatrix, &pointer_info)); + CHECK_FAILURE(m_compositionController->SendPointerInput( + static_cast(message), pointer_info.get())); + } + } + return handled; +} + +void ViewComponent::TrackMouseEvents(DWORD mouseTrackingFlags) +{ + TRACKMOUSEEVENT tme; + tme.cbSize = sizeof(tme); + tme.dwFlags = mouseTrackingFlags; + tme.hwndTrack = m_appWindow->GetMainWindow(); + tme.dwHoverTime = 0; + ::TrackMouseEvent(&tme); +} + +//! [BuildDCompTree] +// Create host app visual that the WebView will connect to. +// - Create a IDCompositionTarget for the host window +// - Create a visual and set that as the IDCompositionTarget's root +// - Create another visual and add that to the IDCompositionTarget's root. +// This visual will be the visual root for the WebView. +void ViewComponent::BuildDCompTreeUsingVisual() +{ + CHECK_FAILURE_BOOL(m_dcompDevice != nullptr); + + if (m_dcompWebViewVisual == nullptr) + { + CHECK_FAILURE(m_dcompDevice->CreateTargetForHwnd( + m_appWindow->GetMainWindow(), TRUE, &m_dcompHwndTarget)); + CHECK_FAILURE(m_dcompDevice->CreateVisual(&m_dcompRootVisual)); + CHECK_FAILURE(m_dcompHwndTarget->SetRoot(m_dcompRootVisual.get())); + CHECK_FAILURE(m_dcompDevice->CreateVisual(&m_dcompWebViewVisual)); + CHECK_FAILURE(m_dcompRootVisual->AddVisual(m_dcompWebViewVisual.get(), TRUE, nullptr)); + } +} +//! [BuildDCompTree] + +void ViewComponent::DestroyDCompVisualTree() +{ + if (m_dcompWebViewVisual) + { + m_dcompWebViewVisual->RemoveAllVisuals(); + m_dcompWebViewVisual.reset(); + + m_dcompRootVisual->RemoveAllVisuals(); + m_dcompRootVisual.reset(); + + m_dcompHwndTarget->SetRoot(nullptr); + m_dcompHwndTarget.reset(); + + m_dcompDevice->Commit(); + } + + if (m_dcompTarget) + { + m_dcompTarget->RemoveOwnerRef(); + m_dcompTarget = nullptr; + } +} + +#ifdef USE_WEBVIEW2_WIN10 +void ViewComponent::BuildWinCompVisualTree() +{ + namespace abiComp = ABI::Windows::UI::Composition; + + if (m_wincompWebViewVisual == nullptr) + { + auto interop = m_wincompCompositor.as(); + winrt::check_hresult(interop->CreateDesktopWindowTarget( + m_appWindow->GetMainWindow(), false, + reinterpret_cast(winrt::put_abi(m_wincompHwndTarget)))); + + m_wincompRootVisual = m_wincompCompositor.CreateContainerVisual(); + m_wincompHwndTarget.Root(m_wincompRootVisual); + + m_wincompWebViewVisual = m_wincompCompositor.CreateContainerVisual(); + m_wincompRootVisual.Children().InsertAtTop(m_wincompWebViewVisual); + } +} + +void ViewComponent::DestroyWinCompVisualTree() +{ + if (m_wincompWebViewVisual != nullptr) + { + m_wincompWebViewVisual.Children().RemoveAll(); + m_wincompWebViewVisual = nullptr; + + m_wincompRootVisual.Children().RemoveAll(); + m_wincompRootVisual = nullptr; + + m_wincompHwndTarget.Root(nullptr); + m_wincompHwndTarget = nullptr; + } +} +#endif + +ViewComponent::~ViewComponent() +{ + m_controller->remove_ZoomFactorChanged(m_zoomFactorChangedToken); + if (m_compositionController) + { + m_compositionController->remove_CursorChanged(m_cursorChangedToken); + // If the webview closes because the AppWindow is closed (as opposed to being closed + // explicitly), this will no-op because in this case, the webview closes before the ViewComponent + // is destroyed. If the webview is closed explicitly, this will succeed. + m_compositionController->put_RootVisualTarget(nullptr); + DestroyDCompVisualTree(); +#ifdef USE_WEBVIEW2_WIN10 + DestroyWinCompVisualTree(); +#endif + } +} diff --git a/SampleApps/WebView2APISample/ViewComponent.h b/SampleApps/WebView2APISample/ViewComponent.h index 1703ca20..2c4acd75 100644 --- a/SampleApps/WebView2APISample/ViewComponent.h +++ b/SampleApps/WebView2APISample/ViewComponent.h @@ -1,108 +1,109 @@ -// Copyright (C) Microsoft Corporation. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#pragma once - -#include "stdafx.h" - -#include "AppWindow.h" -#include "ComponentBase.h" -#include -#include -#ifdef USE_WEBVIEW2_WIN10 -#include -#endif - -// This component handles commands from the View menu, as well as the ZoomFactorChanged -// event, and any functionality related to sizing and visibility of the WebView. -// It also manages interaction with the compositor if running in windowless mode. - -class DCompTargetImpl; - -class ViewComponent : public ComponentBase -{ - friend class DCompTargetImpl; - -public: - ViewComponent( - AppWindow* appWindow, - IDCompositionDevice* dcompDevice, -#ifdef USE_WEBVIEW2_WIN10 - winrtComp::Compositor wincompCompositor, -#endif - bool isDCompTargetMode - ); - - bool HandleWindowMessage( - HWND hWnd, - UINT message, - WPARAM wParam, - LPARAM lParam, - LRESULT* result) override; - - void SetBounds(RECT bounds); - - ~ViewComponent() override; - -private: - enum class TransformType - { - kIdentity = 0, - kScale2X, - kRotate30Deg, - kRotate60DegDiagonally - }; - void ResizeWebView(); - void ToggleVisibility(); - void SetSizeRatio(float ratio); - void SetZoomFactor(float zoom); - void SetScale(float scale); - void SetTransform(TransformType transformType); - void ShowWebViewBounds(); - void ShowWebViewZoom(); - AppWindow* m_appWindow = nullptr; - wil::com_ptr m_controller; - wil::com_ptr m_webView; - bool m_isDcompTargetMode; - bool m_isVisible = true; - float m_webViewRatio = 1.0f; - float m_webViewZoomFactor = 1.0f; - RECT m_webViewBounds = {}; - float m_webViewScale = 1.0f; - EventRegistrationToken m_zoomFactorChangedToken = {}; - - bool OnMouseMessage(UINT message, WPARAM wParam, LPARAM lParam); - bool OnPointerMessage(UINT message, WPARAM wParam, LPARAM lParam); - void TrackMouseEvents(DWORD mouseTrackingFlags); - - wil::com_ptr m_compositionController; - bool m_isTrackingMouse = false; - bool m_isCapturingMouse = false; - std::unordered_set m_pointerIdsStartingInWebView; - D2D1_MATRIX_4X4_F m_webViewTransformMatrix = D2D1::Matrix4x4F(); - - void BuildDCompTreeUsingVisual(); - void DestroyDCompVisualTree(); - - wil::com_ptr m_dcompDevice; - wil::com_ptr m_dcompHwndTarget; - wil::com_ptr m_dcompRootVisual; - wil::com_ptr m_dcompWebViewVisual; - -#ifdef USE_WEBVIEW2_WIN10 - void BuildWinCompVisualTree(); - void DestroyWinCompVisualTree(); - - winrt::Windows::UI::Composition::Compositor m_wincompCompositor{ nullptr }; - winrt::Windows::UI::Composition::Desktop::DesktopWindowTarget m_wincompHwndTarget{ nullptr }; - winrt::Windows::UI::Composition::ContainerVisual m_wincompRootVisual{ nullptr }; - winrt::Windows::UI::Composition::ContainerVisual m_wincompWebViewVisual{ nullptr }; -#endif - - // This member is used to exercise the put_RootVisualTarget API with an IDCompositionTarget. - // Distinct/unrelated to the dcompHwndTarget - wil::com_ptr m_dcompTarget; - - EventRegistrationToken m_cursorChangedToken = {}; -}; +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#pragma once + +#include "stdafx.h" + +#include "AppWindow.h" +#include "ComponentBase.h" +#include +#include +#ifdef USE_WEBVIEW2_WIN10 +#include +#endif + +// This component handles commands from the View menu, as well as the ZoomFactorChanged +// event, and any functionality related to sizing and visibility of the WebView. +// It also manages interaction with the compositor if running in windowless mode. + +class DCompTargetImpl; + +class ViewComponent : public ComponentBase +{ + friend class DCompTargetImpl; + +public: + ViewComponent( + AppWindow* appWindow, + IDCompositionDevice* dcompDevice, +#ifdef USE_WEBVIEW2_WIN10 + winrtComp::Compositor wincompCompositor, +#endif + bool isDCompTargetMode + ); + + bool HandleWindowMessage( + HWND hWnd, + UINT message, + WPARAM wParam, + LPARAM lParam, + LRESULT* result) override; + + void SetBounds(RECT bounds); + + ~ViewComponent() override; + +private: + enum class TransformType + { + kIdentity = 0, + kScale2X, + kRotate30Deg, + kRotate60DegDiagonally + }; + void ResizeWebView(); + void ToggleVisibility(); + void SetSizeRatio(float ratio); + void SetZoomFactor(float zoom); + void SetScale(float scale); + void SetTransform(TransformType transformType); + void ShowWebViewBounds(); + void ShowWebViewZoom(); + AppWindow* m_appWindow = nullptr; + wil::com_ptr m_controller; + wil::com_ptr m_webView; + bool m_isDcompTargetMode; + bool m_isVisible = true; + float m_webViewRatio = 1.0f; + float m_webViewZoomFactor = 1.0f; + RECT m_webViewBounds = {}; + float m_webViewScale = 1.0f; + bool m_useCursorId = false; + EventRegistrationToken m_zoomFactorChangedToken = {}; + + bool OnMouseMessage(UINT message, WPARAM wParam, LPARAM lParam); + bool OnPointerMessage(UINT message, WPARAM wParam, LPARAM lParam); + void TrackMouseEvents(DWORD mouseTrackingFlags); + + wil::com_ptr m_compositionController; + bool m_isTrackingMouse = false; + bool m_isCapturingMouse = false; + std::unordered_set m_pointerIdsStartingInWebView; + D2D1_MATRIX_4X4_F m_webViewTransformMatrix = D2D1::Matrix4x4F(); + + void BuildDCompTreeUsingVisual(); + void DestroyDCompVisualTree(); + + wil::com_ptr m_dcompDevice; + wil::com_ptr m_dcompHwndTarget; + wil::com_ptr m_dcompRootVisual; + wil::com_ptr m_dcompWebViewVisual; + +#ifdef USE_WEBVIEW2_WIN10 + void BuildWinCompVisualTree(); + void DestroyWinCompVisualTree(); + + winrt::Windows::UI::Composition::Compositor m_wincompCompositor{ nullptr }; + winrt::Windows::UI::Composition::Desktop::DesktopWindowTarget m_wincompHwndTarget{ nullptr }; + winrt::Windows::UI::Composition::ContainerVisual m_wincompRootVisual{ nullptr }; + winrt::Windows::UI::Composition::ContainerVisual m_wincompWebViewVisual{ nullptr }; +#endif + + // This member is used to exercise the put_RootVisualTarget API with an IDCompositionTarget. + // Distinct/unrelated to the dcompHwndTarget + wil::com_ptr m_dcompTarget; + + EventRegistrationToken m_cursorChangedToken = {}; +}; diff --git a/SampleApps/WebView2APISample/WebView2APISample.rc b/SampleApps/WebView2APISample/WebView2APISample.rc index 3567fbb8..0cdb3f80 100644 --- a/SampleApps/WebView2APISample/WebView2APISample.rc +++ b/SampleApps/WebView2APISample/WebView2APISample.rc @@ -1,296 +1,300 @@ -// Microsoft Visual C++ generated resource script. -// -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#ifndef APSTUDIO_INVOKED -#include "targetver.h" -#endif -#define APSTUDIO_HIDDEN_SYMBOLS -#include "windows.h" -#undef APSTUDIO_HIDDEN_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (United States) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US -#pragma code_page(1252) - -///////////////////////////////////////////////////////////////////////////// -// -// Icon -// - -// Icon with lowest ID value placed first to ensure application icon -// remains consistent on all systems. -IDI_WEBVIEW2APISAMPLE ICON "WebView2APISample.ico" - -IDI_SMALL ICON "small.ico" - - -///////////////////////////////////////////////////////////////////////////// -// -// Menu -// - -IDC_WEBVIEW2APISAMPLE MENU -BEGIN - POPUP "&File" - BEGIN - MENUITEM "Save Screenshot", IDM_SAVE_SCREENSHOT - MENUITEM "Get Document Title", IDM_GET_DOCUMENT_TITLE - MENUITEM "Get Browser Version After Creation", IDM_GET_BROWSER_VERSION_AFTER_CREATION - MENUITEM "Get Browser Version Before Creation", IDM_GET_BROWSER_VERSION_BEFORE_CREATION - MENUITEM "E&xit", IDM_EXIT - END - POPUP "&Script" - BEGIN - MENUITEM "Inject Script", IDM_INJECT_SCRIPT - MENUITEM "Add Initialize Script", ID_ADD_INITIALIZE_SCRIPT - MENUITEM "Remove Initialize Script", ID_REMOVE_INITIALIZE_SCRIPT - MENUITEM SEPARATOR - MENUITEM "Post Message String", IDM_POST_WEB_MESSAGE_STRING - MENUITEM "Post Message JSON", IDM_POST_WEB_MESSAGE_JSON - MENUITEM SEPARATOR - MENUITEM "Subscribe to CDP event", IDM_SUBSCRIBE_TO_CDP_EVENT - MENUITEM "Call CDP method", IDM_CALL_CDP_METHOD - MENUITEM SEPARATOR - MENUITEM "Add COM object", IDM_ADD_HOST_OBJECT - MENUITEM SEPARATOR - MENUITEM "Open DevTools Window", IDM_OPEN_DEVTOOLS_WINDOW - END - POPUP "&Window" - BEGIN - MENUITEM "Set WebView Language", IDM_SET_LANGUAGE - MENUITEM "Toggle AAD SSO enabled", IDM_TOGGLE_AAD_SSO - MENUITEM "Close WebView", IDM_CLOSE_WEBVIEW - MENUITEM "Close WebView and cleanup user data folder", IDM_CLOSE_WEBVIEW_CLEANUP - MENUITEM SEPARATOR - POPUP "WebView Creation Mode" - BEGIN - MENUITEM "Windowed", IDM_CREATION_MODE_WINDOWED - MENUITEM "Visual - DComp", IDM_CREATION_MODE_VISUAL_DCOMP - MENUITEM "Target - DComp", IDM_CREATION_MODE_TARGET_DCOMP -#ifdef USE_WEBVIEW2_WIN10 - MENUITEM "Visual - WinComp", IDM_CREATION_MODE_VISUAL_WINCOMP -#endif - END - MENUITEM "Create WebView" IDM_REINIT - MENUITEM "Create New Window", IDM_NEW_WINDOW - MENUITEM "Create New Thread", IDM_NEW_THREAD - END - POPUP "&Process" - BEGIN - MENUITEM "Browser Process Info", IDM_PROCESS_INFO - MENUITEM "Crash Browser Process", IDM_CRASH_PROCESS - MENUITEM "Crash Render Process", IDM_CRASH_RENDER_PROCESS - END - POPUP "S&ettings" - BEGIN - MENUITEM "Blocked Domains", ID_BLOCKEDSITES - MENUITEM "Set User Agent", ID_SETTINGS_SETUSERAGENT - MENUITEM SEPARATOR - MENUITEM "Toggle JavaScript", IDM_TOGGLE_JAVASCRIPT - MENUITEM "Toggle Web Messaging", IDM_TOGGLE_WEB_MESSAGING - MENUITEM "Toggle Fullscreen allowed", IDM_TOGGLE_FULLSCREEN_ALLOWED - MENUITEM "Toggle Status Bar enabled", ID_SETTINGS_STATUS_BAR_ENABLED - MENUITEM "Toggle DevTools enabled", ID_SETTINGS_DEV_TOOLS_ENABLED - MENUITEM "Toggle Block images", ID_SETTINGS_BLOCKALLIMAGES - POPUP "JavaScript Dialogs" - BEGIN - MENUITEM "Use Default Script Dialogs", IDM_USE_DEFAULT_SCRIPT_DIALOGS - MENUITEM "Use Custom Script Dialogs", IDM_USE_CUSTOM_SCRIPT_DIALOGS - MENUITEM "Use Deferred Script Dialogs", IDM_USE_DEFERRED_SCRIPT_DIALOGS - MENUITEM "Complete Deferred Script Dialog", IDM_COMPLETE_JAVASCRIPT_DIALOG - END - MENUITEM "Toggle context menus enabled", ID_SETTINGS_CONTEXT_MENUS_ENABLED - MENUITEM "Toggle host objects allowed", ID_SETTINGS_HOST_OBJECTS_ALLOWED - MENUITEM "Toggle zoom control enabled", ID_SETTINGS_ZOOM_ENABLED - MENUITEM "Toggle built-in error page enabled", ID_SETTINGS_BUILTIN_ERROR_PAGE_ENABLED - END - POPUP "&View" - BEGIN - MENUITEM "Toggle Visibility", IDM_TOGGLE_VISIBILITY - POPUP "WebView Area" - BEGIN - MENUITEM "Get WebView Bounds", IDM_GET_WEBVIEW_BOUNDS - MENUITEM "25%", IDM_SIZE_25 - MENUITEM "50%", IDM_SIZE_50 - MENUITEM "75%", IDM_SIZE_75 - MENUITEM "100%", IDM_SIZE_100 - END - POPUP "WebView Zoom" - BEGIN - MENUITEM "Get WebView Zoom", IDM_GET_WEBVIEW_ZOOM - MENUITEM "0.5x", IDM_ZOOM_05 - MENUITEM "1.0x", IDM_ZOOM_10 - MENUITEM "2.0x", IDM_ZOOM_20 - END - POPUP "WebView Scaling" - BEGIN - MENUITEM "Scale 0.5x" IDM_SCALE_50 - MENUITEM "Scale 1.0x" IDM_SCALE_100 - MENUITEM "Scale 1.25x" IDM_SCALE_125 - MENUITEM "Scale 1.5x" IDM_SCALE_150 - END - POPUP "WebView Transform" - BEGIN - MENUITEM "No transform" IDM_TRANSFORM_NONE - MENUITEM "Rotate 30Deg" IDM_TRANSFORM_ROTATE_30DEG - MENUITEM "Rotate 60Deg Diagonally" IDM_TRANSFORM_ROTATE_60DEG_DIAG - END - MENUITEM "Set Focus", IDM_FOCUS_SET - MENUITEM "Tab In", IDM_FOCUS_TAB_IN - MENUITEM "Reverse Tab In", IDM_FOCUS_REVERSE_TAB_IN - MENUITEM "Toggle Tab Handling", IDM_TOGGLE_TAB_HANDLING - END - POPUP "S&cenario" - BEGIN - MENUITEM "Web Messaging", IDM_SCENARIO_POST_WEB_MESSAGE - MENUITEM "Host Objects", IDM_SCENARIO_ADD_HOST_OBJECT - MENUITEM "Event Monitor", IDM_SCENARIO_WEB_VIEW_EVENT_MONITOR - POPUP "Script Debugging" - BEGIN - MENUITEM "JavaScript", IDM_SCENARIO_JAVA_SCRIPT - MENUITEM "TypeScript", IDM_SCENARIO_TYPE_SCRIPT - END - MENUITEM "Authentication", IDM_SCENARIO_AUTHENTICATION - END - POPUP "&Help" - BEGIN - MENUITEM "&About ...", IDM_ABOUT - END -END - -///////////////////////////////////////////////////////////////////////////// -// -// Accelerator -// - -IDC_WEBVIEW2APISAMPLE ACCELERATORS -BEGIN - "?", IDM_ABOUT, ASCII, ALT - "/", IDM_ABOUT, ASCII, ALT -END - - -///////////////////////////////////////////////////////////////////////////// -// -// Dialog -// - -IDD_ABOUTBOX DIALOGEX 0, 0, 170, 62 -STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU -CAPTION "About WebView2APISample" -FONT 8, "MS Shell Dlg", 0, 0, 0x1 -BEGIN - ICON IDR_MAINFRAME,IDC_STATIC,14,14,21,20 - LTEXT "WebView2APISample, Version 1.0",IDC_STATIC,42,14,114,8,SS_NOPREFIX - LTEXT "Copyright (C) 2019",IDC_STATIC,42,26,114,8 - DEFPUSHBUTTON "OK",IDOK,113,41,50,14,WS_GROUP -END - -IDD_DIALOG_INPUT DIALOGEX 0, 0, 309, 151 -STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU -CAPTION "Input" -FONT 8, "MS Shell Dlg", 400, 0, 0x1 -BEGIN - GROUPBOX "Static",IDC_STATIC_LABEL,7,7,295,121 - EDITTEXT IDC_EDIT_INPUT,14,55,281,69,ES_MULTILINE | ES_AUTOHSCROLL - DEFPUSHBUTTON "OK",IDOK,198,130,50,14 - PUSHBUTTON "Cancel",IDCANCEL,252,130,50,14 - EDITTEXT IDC_EDIT_DESCRIPTION,14,18,281,33,ES_MULTILINE | ES_AUTOHSCROLL | ES_READONLY -END - - -///////////////////////////////////////////////////////////////////////////// -// -// DESIGNINFO -// - -#ifdef APSTUDIO_INVOKED -GUIDELINES DESIGNINFO -BEGIN - IDD_ABOUTBOX, DIALOG - BEGIN - LEFTMARGIN, 7 - RIGHTMARGIN, 163 - TOPMARGIN, 7 - BOTTOMMARGIN, 55 - END - - IDD_DIALOG_INPUT, DIALOG - BEGIN - LEFTMARGIN, 7 - RIGHTMARGIN, 302 - TOPMARGIN, 7 - BOTTOMMARGIN, 144 - END -END -#endif // APSTUDIO_INVOKED - - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#ifndef APSTUDIO_INVOKED\r\n" - "#include ""targetver.h""\r\n" - "#endif\r\n" - "#define APSTUDIO_HIDDEN_SYMBOLS\r\n" - "#include ""windows.h""\r\n" - "#undef APSTUDIO_HIDDEN_SYMBOLS\r\n" - "\0" -END - -3 TEXTINCLUDE -BEGIN - "\r\n" - "\0" -END - -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// String Table -// - -STRINGTABLE -BEGIN - IDS_APP_TITLE "WebView2APISample" - IDC_WEBVIEW2APISAMPLE "WEBVIEW2APISAMPLE" -END - -#endif // English (United States) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#ifndef APSTUDIO_INVOKED +#include "targetver.h" +#endif +#define APSTUDIO_HIDDEN_SYMBOLS +#include "windows.h" +#undef APSTUDIO_HIDDEN_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_WEBVIEW2APISAMPLE ICON "WebView2APISample.ico" + +IDI_SMALL ICON "small.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Menu +// + +IDC_WEBVIEW2APISAMPLE MENU +BEGIN + POPUP "&File" + BEGIN + MENUITEM "Save Screenshot", IDM_SAVE_SCREENSHOT + MENUITEM "Get Document Title", IDM_GET_DOCUMENT_TITLE + MENUITEM "Get Browser Version After Creation", IDM_GET_BROWSER_VERSION_AFTER_CREATION + MENUITEM "Get Browser Version Before Creation", IDM_GET_BROWSER_VERSION_BEFORE_CREATION + MENUITEM "E&xit", IDM_EXIT + END + POPUP "&Script" + BEGIN + MENUITEM "Inject Script", IDM_INJECT_SCRIPT + MENUITEM "Add Initialize Script", ID_ADD_INITIALIZE_SCRIPT + MENUITEM "Remove Initialize Script", ID_REMOVE_INITIALIZE_SCRIPT + MENUITEM SEPARATOR + MENUITEM "Post Message String", IDM_POST_WEB_MESSAGE_STRING + MENUITEM "Post Message JSON", IDM_POST_WEB_MESSAGE_JSON + MENUITEM SEPARATOR + MENUITEM "Subscribe to CDP event", IDM_SUBSCRIBE_TO_CDP_EVENT + MENUITEM "Call CDP method", IDM_CALL_CDP_METHOD + MENUITEM SEPARATOR + MENUITEM "Add COM object", IDM_ADD_HOST_OBJECT + MENUITEM SEPARATOR + MENUITEM "Open DevTools Window", IDM_OPEN_DEVTOOLS_WINDOW + END + POPUP "&Window" + BEGIN + MENUITEM "Set WebView Language", IDM_SET_LANGUAGE + MENUITEM "Toggle AAD SSO enabled", IDM_TOGGLE_AAD_SSO + MENUITEM "Close WebView", IDM_CLOSE_WEBVIEW + MENUITEM "Close WebView and cleanup user data folder", IDM_CLOSE_WEBVIEW_CLEANUP + MENUITEM SEPARATOR + POPUP "WebView Creation Mode" + BEGIN + MENUITEM "Windowed", IDM_CREATION_MODE_WINDOWED + MENUITEM "Visual - DComp", IDM_CREATION_MODE_VISUAL_DCOMP + MENUITEM "Target - DComp", IDM_CREATION_MODE_TARGET_DCOMP +#ifdef USE_WEBVIEW2_WIN10 + MENUITEM "Visual - WinComp", IDM_CREATION_MODE_VISUAL_WINCOMP +#endif + END + MENUITEM "Create WebView" IDM_REINIT + MENUITEM "Create New Window", IDM_NEW_WINDOW + MENUITEM "Create New Thread", IDM_NEW_THREAD + END + POPUP "&Process" + BEGIN + MENUITEM "Browser Process Info", IDM_PROCESS_INFO + MENUITEM "Crash Browser Process", IDM_CRASH_PROCESS + MENUITEM "Crash Render Process", IDM_CRASH_RENDER_PROCESS + END + POPUP "S&ettings" + BEGIN + MENUITEM "Blocked Domains", ID_BLOCKEDSITES + MENUITEM "Set User Agent", ID_SETTINGS_SETUSERAGENT + MENUITEM SEPARATOR + MENUITEM "Toggle JavaScript", IDM_TOGGLE_JAVASCRIPT + MENUITEM "Toggle Web Messaging", IDM_TOGGLE_WEB_MESSAGING + MENUITEM "Toggle Fullscreen allowed", IDM_TOGGLE_FULLSCREEN_ALLOWED + MENUITEM "Toggle Status Bar enabled", ID_SETTINGS_STATUS_BAR_ENABLED + MENUITEM "Toggle DevTools enabled", ID_SETTINGS_DEV_TOOLS_ENABLED + MENUITEM "Toggle Block images", ID_SETTINGS_BLOCKALLIMAGES + POPUP "JavaScript Dialogs" + BEGIN + MENUITEM "Use Default Script Dialogs", IDM_USE_DEFAULT_SCRIPT_DIALOGS + MENUITEM "Use Custom Script Dialogs", IDM_USE_CUSTOM_SCRIPT_DIALOGS + MENUITEM "Use Deferred Script Dialogs", IDM_USE_DEFERRED_SCRIPT_DIALOGS + MENUITEM "Complete Deferred Script Dialog", IDM_COMPLETE_JAVASCRIPT_DIALOG + END + MENUITEM "Toggle context menus enabled", ID_SETTINGS_CONTEXT_MENUS_ENABLED + MENUITEM "Toggle host objects allowed", ID_SETTINGS_HOST_OBJECTS_ALLOWED + MENUITEM "Toggle zoom control enabled", ID_SETTINGS_ZOOM_ENABLED + MENUITEM "Toggle built-in error page enabled", ID_SETTINGS_BUILTIN_ERROR_PAGE_ENABLED + END + POPUP "&View" + BEGIN + MENUITEM "Toggle Visibility", IDM_TOGGLE_VISIBILITY + POPUP "WebView Area" + BEGIN + MENUITEM "Get WebView Bounds", IDM_GET_WEBVIEW_BOUNDS + MENUITEM "25%", IDM_SIZE_25 + MENUITEM "50%", IDM_SIZE_50 + MENUITEM "75%", IDM_SIZE_75 + MENUITEM "100%", IDM_SIZE_100 + END + POPUP "WebView Zoom" + BEGIN + MENUITEM "Get WebView Zoom", IDM_GET_WEBVIEW_ZOOM + MENUITEM "0.5x", IDM_ZOOM_05 + MENUITEM "1.0x", IDM_ZOOM_10 + MENUITEM "2.0x", IDM_ZOOM_20 + END + POPUP "WebView Scaling" + BEGIN + MENUITEM "Scale 0.5x" IDM_SCALE_50 + MENUITEM "Scale 1.0x" IDM_SCALE_100 + MENUITEM "Scale 1.25x" IDM_SCALE_125 + MENUITEM "Scale 1.5x" IDM_SCALE_150 + END + POPUP "WebView Transform" + BEGIN + MENUITEM "No transform" IDM_TRANSFORM_NONE + MENUITEM "Rotate 30Deg" IDM_TRANSFORM_ROTATE_30DEG + MENUITEM "Rotate 60Deg Diagonally" IDM_TRANSFORM_ROTATE_60DEG_DIAG + END + MENUITEM "Toggle Cursor Handling" IDM_TOGGLE_CURSOR_HANDLING + MENUITEM "Set Focus", IDM_FOCUS_SET + MENUITEM "Tab In", IDM_FOCUS_TAB_IN + MENUITEM "Reverse Tab In", IDM_FOCUS_REVERSE_TAB_IN + MENUITEM "Toggle Tab Handling", IDM_TOGGLE_TAB_HANDLING + END + POPUP "S&cenario" + BEGIN + MENUITEM "Web Messaging", IDM_SCENARIO_POST_WEB_MESSAGE + MENUITEM "Host Objects", IDM_SCENARIO_ADD_HOST_OBJECT + MENUITEM "Event Monitor", IDM_SCENARIO_WEB_VIEW_EVENT_MONITOR + POPUP "Script Debugging" + BEGIN + MENUITEM "JavaScript", IDM_SCENARIO_JAVA_SCRIPT + MENUITEM "TypeScript", IDM_SCENARIO_TYPE_SCRIPT + END + MENUITEM "Authentication", IDM_SCENARIO_AUTHENTICATION + MENUITEM "Cookie Management", IDM_SCENARIO_COOKIE_MANAGEMENT + MENUITEM "DOM Content Loaded", IDM_SCENARIO_DOM_CONTENT_LOADED + MENUITEM "NavigateWithWebResourceRequest", IDM_SCENARIO_NAVIGATEWITHWEBRESOURCEREQUEST + END + POPUP "&Help" + BEGIN + MENUITEM "&About ...", IDM_ABOUT + END +END + +///////////////////////////////////////////////////////////////////////////// +// +// Accelerator +// + +IDC_WEBVIEW2APISAMPLE ACCELERATORS +BEGIN + "?", IDM_ABOUT, ASCII, ALT + "/", IDM_ABOUT, ASCII, ALT +END + + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_ABOUTBOX DIALOGEX 0, 0, 170, 62 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "About WebView2APISample" +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + ICON IDR_MAINFRAME,IDC_STATIC,14,14,21,20 + LTEXT "WebView2APISample, Version 1.0",IDC_STATIC,42,14,114,8,SS_NOPREFIX + LTEXT "Copyright (C) 2019",IDC_STATIC,42,26,114,8 + DEFPUSHBUTTON "OK",IDOK,113,41,50,14,WS_GROUP +END + +IDD_DIALOG_INPUT DIALOGEX 0, 0, 309, 151 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Input" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + GROUPBOX "Static",IDC_STATIC_LABEL,7,7,295,121 + EDITTEXT IDC_EDIT_INPUT,14,55,281,69,ES_MULTILINE | ES_AUTOHSCROLL + DEFPUSHBUTTON "OK",IDOK,198,130,50,14 + PUSHBUTTON "Cancel",IDCANCEL,252,130,50,14 + EDITTEXT IDC_EDIT_DESCRIPTION,14,18,281,33,ES_MULTILINE | ES_AUTOHSCROLL | ES_READONLY +END + + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO +BEGIN + IDD_ABOUTBOX, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 163 + TOPMARGIN, 7 + BOTTOMMARGIN, 55 + END + + IDD_DIALOG_INPUT, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 302 + TOPMARGIN, 7 + BOTTOMMARGIN, 144 + END +END +#endif // APSTUDIO_INVOKED + + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#ifndef APSTUDIO_INVOKED\r\n" + "#include ""targetver.h""\r\n" + "#endif\r\n" + "#define APSTUDIO_HIDDEN_SYMBOLS\r\n" + "#include ""windows.h""\r\n" + "#undef APSTUDIO_HIDDEN_SYMBOLS\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDS_APP_TITLE "WebView2APISample" + IDC_WEBVIEW2APISAMPLE "WEBVIEW2APISAMPLE" +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/SampleApps/WebView2APISample/WebView2APISample.vcxproj b/SampleApps/WebView2APISample/WebView2APISample.vcxproj index 321ebc34..e625a505 100644 --- a/SampleApps/WebView2APISample/WebView2APISample.vcxproj +++ b/SampleApps/WebView2APISample/WebView2APISample.vcxproj @@ -1,475 +1,488 @@ - - - - - Debug - Win32 - - - Release - Win32 - - - Debug - x64 - - - Release - x64 - - - Debug - ARM64 - - - Release - ARM64 - - - Win7 Debug - ARM64 - - - Win7 Debug - Win32 - - - Win7 Debug - x64 - - - Win7 Release - ARM64 - - - Win7 Release - Win32 - - - Win7 Release - x64 - - - - 15.0 - {4F0CEEF3-12B3-425E-9BB0-105200411592} - Win32Proj - 10.0 - - - - Application - true - v142 - Unicode - - - Application - true - v142 - Unicode - - - Application - false - v142 - Unicode - - - Application - false - v142 - Unicode - - - Application - true - v142 - Unicode - - - Application - true - v142 - Unicode - - - Application - false - v142 - Unicode - - - Application - false - v142 - Unicode - - - Application - true - v142 - Unicode - - - Application - true - v142 - Unicode - - - Application - false - v142 - Unicode - - - Application - false - v142 - Unicode - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - $(ProjectDir)$(Configuration)\$(Platform)\ - $(Platform)\$(Configuration)\ - $(ProjectName) - - - true - $(ProjectDir)$(Configuration)\$(Platform)\ - $(Platform)\$(Configuration)\ - $(ProjectName)_Win7 - - - true - $(ProjectDir)$(Configuration)\$(Platform)\ - $(Platform)\$(Configuration)\ - - - true - $(ProjectDir)$(Configuration)\$(Platform)\ - $(Platform)\$(Configuration)\ - $(ProjectName)_Win7 - - - $(ProjectDir)$(Configuration)\$(Platform)\ - $(ProjectName) - - - $(ProjectDir)$(Configuration)\$(Platform)\ - $(ProjectName)_Win7 - - - $(ProjectDir)$(Configuration)\$(Platform)\ - - - $(ProjectDir)$(Configuration)\$(Platform)\ - $(ProjectName)_Win7 - - - $(ProjectDir)$(Configuration)\$(Platform)\ - $(ProjectName) - - - $(ProjectDir)$(Configuration)\$(Platform)\ - $(ProjectName)_Win7 - - - $(ProjectDir)$(Configuration)\$(Platform)\ - - - $(ProjectDir)$(Configuration)\$(Platform)\ - $(ProjectName)_Win7 - - - - USE_WEBVIEW2_WIN10;WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) - MultiThreadedDebug - Level3 - ProgramDatabase - Disabled - stdcpp17 - - - MachineX86 - true - Windows - kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) - onecoreuap.lib %(AdditionalOptions) - - - - - WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) - MultiThreadedDebug - Level3 - ProgramDatabase - Disabled - stdcpp17 - - - MachineX86 - true - Windows - kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) - onecoreuap.lib %(AdditionalOptions) - - - - - USE_WEBVIEW2_WIN10;WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) - MultiThreaded - Level3 - ProgramDatabase - stdcpp17 - - - MachineX86 - true - Windows - true - true - kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) - onecoreuap.lib %(AdditionalOptions) - - - - - WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) - MultiThreaded - Level3 - ProgramDatabase - stdcpp17 - - - MachineX86 - true - Windows - true - true - kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) - onecoreuap.lib %(AdditionalOptions) - - - - - MultiThreadedDebug - stdcpp17 - USE_WEBVIEW2_WIN10;_UNICODE;UNICODE;%(PreprocessorDefinitions) - - - kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) - onecoreuap.lib %(AdditionalOptions) - - - - - MultiThreadedDebug - stdcpp17 - - - kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) - onecoreuap.lib %(AdditionalOptions) - - - - - MultiThreaded - stdcpp17 - USE_WEBVIEW2_WIN10;_UNICODE;UNICODE;%(PreprocessorDefinitions) - - - kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) - - - onecoreuap.lib %(AdditionalOptions) - - - - - MultiThreaded - stdcpp17 - - - kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) - - - onecoreuap.lib %(AdditionalOptions) - - - - - MultiThreadedDebug - stdcpp17 - USE_WEBVIEW2_WIN10;_ARM64_WINAPI_PARTITION_DESKTOP_SDK_AVAILABLE=1;%(ClCompile.PreprocessorDefinitions) - - - kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) - onecoreuap.lib %(AdditionalOptions) - - - - - MultiThreadedDebug - stdcpp17 - - - kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) - onecoreuap.lib %(AdditionalOptions) - - - - - MultiThreaded - stdcpp17 - USE_WEBVIEW2_WIN10;_ARM64_WINAPI_PARTITION_DESKTOP_SDK_AVAILABLE=1;%(ClCompile.PreprocessorDefinitions) - - - kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) - onecoreuap.lib %(AdditionalOptions) - - - - - MultiThreaded - stdcpp17 - - - kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) - onecoreuap.lib %(AdditionalOptions) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Create - Create - Create - Create - Create - Create - Create - Create - Create - Create - Create - Create - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + Debug + ARM64 + + + Release + ARM64 + + + Win7 Debug + ARM64 + + + Win7 Debug + Win32 + + + Win7 Debug + x64 + + + Win7 Release + ARM64 + + + Win7 Release + Win32 + + + Win7 Release + x64 + + + + 15.0 + {4F0CEEF3-12B3-425E-9BB0-105200411592} + Win32Proj + 10.0 + + + + Application + true + v142 + Unicode + + + Application + true + v142 + Unicode + + + Application + false + v142 + Unicode + + + Application + false + v142 + Unicode + + + Application + true + v142 + Unicode + + + Application + true + v142 + Unicode + + + Application + false + v142 + Unicode + + + Application + false + v142 + Unicode + + + Application + true + v142 + Unicode + + + Application + true + v142 + Unicode + + + Application + false + v142 + Unicode + + + Application + false + v142 + Unicode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + $(ProjectDir)$(Configuration)\$(Platform)\ + $(Platform)\$(Configuration)\ + $(ProjectName) + + + true + $(ProjectDir)$(Configuration)\$(Platform)\ + $(Platform)\$(Configuration)\ + $(ProjectName)_Win7 + + + true + $(ProjectDir)$(Configuration)\$(Platform)\ + $(Platform)\$(Configuration)\ + + + true + $(ProjectDir)$(Configuration)\$(Platform)\ + $(Platform)\$(Configuration)\ + $(ProjectName)_Win7 + + + $(ProjectDir)$(Configuration)\$(Platform)\ + $(ProjectName) + + + $(ProjectDir)$(Configuration)\$(Platform)\ + $(ProjectName)_Win7 + + + $(ProjectDir)$(Configuration)\$(Platform)\ + + + $(ProjectDir)$(Configuration)\$(Platform)\ + $(ProjectName)_Win7 + + + $(ProjectDir)$(Configuration)\$(Platform)\ + $(ProjectName) + + + $(ProjectDir)$(Configuration)\$(Platform)\ + $(ProjectName)_Win7 + + + $(ProjectDir)$(Configuration)\$(Platform)\ + + + $(ProjectDir)$(Configuration)\$(Platform)\ + $(ProjectName)_Win7 + + + + USE_WEBVIEW2_WIN10;WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) + MultiThreadedDebug + Level3 + ProgramDatabase + Disabled + stdcpp17 + + + MachineX86 + true + Windows + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) + onecoreuap.lib %(AdditionalOptions) + + + + + WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) + MultiThreadedDebug + Level3 + ProgramDatabase + Disabled + stdcpp17 + + + MachineX86 + true + Windows + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) + onecoreuap.lib %(AdditionalOptions) + + + + + USE_WEBVIEW2_WIN10;WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) + MultiThreaded + Level3 + ProgramDatabase + stdcpp17 + + + MachineX86 + true + Windows + true + true + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) + onecoreuap.lib %(AdditionalOptions) + + + + + WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) + MultiThreaded + Level3 + ProgramDatabase + stdcpp17 + + + MachineX86 + true + Windows + true + true + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) + onecoreuap.lib %(AdditionalOptions) + + + + + MultiThreadedDebug + stdcpp17 + USE_WEBVIEW2_WIN10;_UNICODE;UNICODE;%(PreprocessorDefinitions) + + + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) + onecoreuap.lib %(AdditionalOptions) + + + + + MultiThreadedDebug + stdcpp17 + + + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) + onecoreuap.lib %(AdditionalOptions) + + + + + MultiThreaded + stdcpp17 + USE_WEBVIEW2_WIN10;_UNICODE;UNICODE;%(PreprocessorDefinitions) + + + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) + + + onecoreuap.lib %(AdditionalOptions) + + + + + MultiThreaded + stdcpp17 + + + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) + + + onecoreuap.lib %(AdditionalOptions) + + + + + MultiThreadedDebug + stdcpp17 + USE_WEBVIEW2_WIN10;_ARM64_WINAPI_PARTITION_DESKTOP_SDK_AVAILABLE=1;%(ClCompile.PreprocessorDefinitions) + + + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) + onecoreuap.lib %(AdditionalOptions) + + + + + MultiThreadedDebug + stdcpp17 + + + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) + onecoreuap.lib %(AdditionalOptions) + + + + + MultiThreaded + stdcpp17 + USE_WEBVIEW2_WIN10;_ARM64_WINAPI_PARTITION_DESKTOP_SDK_AVAILABLE=1;%(ClCompile.PreprocessorDefinitions) + + + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) + onecoreuap.lib %(AdditionalOptions) + + + + + MultiThreaded + stdcpp17 + + + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;shlwapi.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;urlmon.lib;%(AdditionalDependencies) + onecoreuap.lib %(AdditionalOptions) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + Create + Create + Create + Create + Create + Create + Create + Create + Create + Create + Create + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/SampleApps/WebView2APISample/WebView2APISample.vcxproj.filters b/SampleApps/WebView2APISample/WebView2APISample.vcxproj.filters index d89ec716..495b2b34 100644 --- a/SampleApps/WebView2APISample/WebView2APISample.vcxproj.filters +++ b/SampleApps/WebView2APISample/WebView2APISample.vcxproj.filters @@ -72,6 +72,18 @@ Source Files + + Source Files + + + Source Files + + + Source Files + + + Source Files + @@ -140,6 +152,18 @@ Header Files + + Header Files + + + Header Files + + + Header Files + + + Header Files + @@ -150,13 +174,20 @@ + + + + + + Resource Files + diff --git a/SampleApps/WebView2APISample/documentation/Testing-Instructions.md b/SampleApps/WebView2APISample/documentation/Testing-Instructions.md index 763024d6..ffcd17a4 100644 --- a/SampleApps/WebView2APISample/documentation/Testing-Instructions.md +++ b/SampleApps/WebView2APISample/documentation/Testing-Instructions.md @@ -6,7 +6,7 @@ These are instructions for manually testing all the features of the WebView2 API * [Getting started](#Getting-started) * [Install a NuGet package Locally in VS](#Install-a-NuGet-package-Locally-in-VS) -* [UI Entries](#UI_Entries) +* [UI Entries](#ui-entries) * [File](#File) * [Save Screenshot](#Save-Screenshot) * [Get Document Title](#Get-Document-Title) @@ -17,11 +17,9 @@ These are instructions for manually testing all the features of the WebView2 API * [Inject Script](#Inject-Script) * [Post Message String](#Post-Message-String) * [Post Message JSON](#Post-Message-JSON) - * [Add Initialize Script](#Add/Remove-Initialize-Script) - * [Remove Initialize Script](#Add/Remove-Initialize-Script) - * [Subscribe to CDP event](#Subscribe-to-CDP-event-&-Call-CDP-method) - * [Call CDP method](#Subscribe-to-CDP-event-&-Call-CDP-method) - * [Add COM Object](#Add-COM-Object) + * [Add Initialize Script](#addremove-initialize-script) + * [Remove Initialize Script](#addremove-initialize-script) + * [Subscribe to CDP event & Call CDP method](#subscribe-to-cdp-event--call-cdp-method) * [Open DevTools Window](#Open-DevTools-Window) * [Window](#Window) * [Close WebView](#Close-WebView) @@ -60,7 +58,7 @@ These are instructions for manually testing all the features of the WebView2 API * [Host Objects](#Host-Objects) * [Script Debugging](#Script-Debugging) * [Help](#Help) - * [About ...](#About-...) + * [About ...](#about-) * [Miscellaneous](#Miscellaneous) * [Accelerator Key Support](#Accelerator-Key-Support) * [Language](#Language) @@ -723,7 +721,7 @@ Menu item `Script -> Host Objects` is demonstrated. Test Single WebView JavaScript Debugging with **both** [Debugger For Microsoft Edge](https://github.com/microsoft/vscode-edge-debug2) and [JavaScript Debugger Nightly](https://github.com/microsoft/vscode-js-debug) in VSCode -1. Follow [Debugging Setup](#[vscode]-debugging-setup) +1. Follow [Debugging Setup](#vscode-debugging-setup) 1. Go to Debug tab via `View -> Run` 1. On the top drop down, select `$(Debugger): Sample app (Script $(Configuration)|$(Platform))`. (e.g. `Debugger For Microsoft Edge: Sample app (Script Debug|x64)` and `JavaScript Debugger(Nightly): Sample app (Script Release|x64)`) ![debugger-dropdown](screenshots/debugger-dropdown.png) @@ -738,7 +736,7 @@ Test Single WebView JavaScript Debugging with **both** [Debugger For Microsoft E Test Single WebView TypeScript Debugging with **both** [Debugger For Microsoft Edge](https://github.com/microsoft/vscode-edge-debug2) and [JavaScript Debugger Nightly](https://github.com/microsoft/vscode-js-debug) in VSCode -1. Follow [Debugging Setup](#[vscode]-debugging-setup) +1. Follow [Debugging Setup](#vscode-debugging-setup) 1. Go to Debug tab via `View -> Run` 1. On the top drop down, select `$(Debugger): Sample app (Script $(Configuration)|$(Platform))`. (e.g. `Debugger For Microsoft Edge: Sample app (Script Debug|x64)` and `JavaScript Debugger(Nightly): Sample app (Script Release|x64)`) ![debugger-dropdown](screenshots/debugger-dropdown.png) @@ -755,7 +753,7 @@ Test Single WebView Script Debugging with **both** [Debugger For Microsoft Edge] 1. Add a new REGKEY `additionalBrowserArguments=--remote-debugging-port=9222` under `Computer\HKEY_CURRENT_USER\Software\Policies\Microsoft\EmbeddedBrowserWebView\LoaderOverride\*` ![step 1](screenshots/script-debugging-reg-key.png) -1. Follow [Debugging Setup](#[vscode]-debugging-setup) +1. Follow [Debugging Setup](#vscode-debugging-setup) 1. Go to Debug tab via `View -> Run` 1. On the top drop down, select `$(Debugger): Attach to Edge`. (e.g. `Debugger For Microsoft Edge: Attach to Edge` and `JavaScript Debugger(Nightly): Attach to Edge`) 1. Press `F5` or click the green Button (GO) to Start Debugging @@ -772,7 +770,7 @@ Test Single WebView Script Debugging with **both** [Debugger For Microsoft Edge] 1. Add a new REGKEY `additionalBrowserArguments=--remote-debugging-port=9222` under `Computer\HKEY_CURRENT_USER\Software\Policies\Microsoft\EmbeddedBrowserWebView\LoaderOverride\*` ![step 1](screenshots/script-debugging-reg-key.png) -1. Follow [Debugging Setup](#[vscode]-debugging-setup) +1. Follow [Debugging Setup](#vscode-debugging-setup) 1. Go to Debug tab via `View -> Run` 1. On the top drop down, select `$(Debugger): Attach to Edge`. (e.g. `Debugger For Microsoft Edge: Attach to Edge` and `JavaScript Debugger(Nightly): Attach to Edge`) 1. Press `F5` or click the green Button (GO) to Start Debugging diff --git a/SampleApps/WebView2APISample/packages.config b/SampleApps/WebView2APISample/packages.config index 3c8f0d64..413b11d1 100644 --- a/SampleApps/WebView2APISample/packages.config +++ b/SampleApps/WebView2APISample/packages.config @@ -1,5 +1,5 @@ - - - - + + + + \ No newline at end of file diff --git a/SampleApps/WebView2APISample/resource.h b/SampleApps/WebView2APISample/resource.h index 842ba9ea..5e1cd9d7 100644 --- a/SampleApps/WebView2APISample/resource.h +++ b/SampleApps/WebView2APISample/resource.h @@ -1,113 +1,118 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by WebView2APISample.rc -// -// clang-format off -#define IDC_MYICON 2 -#define IDS_APP_TITLE 102 -#define IDD_ABOUTBOX 103 -#define IDM_ABOUT 104 -#define IDM_EXIT 105 -#define IDI_WEBVIEW2APISAMPLE 107 -#define IDI_SMALL 108 -#define IDC_WEBVIEW2APISAMPLE 109 -#define IDC_WEBVIEW2APISAMPLEHOST 110 -#define IDM_ZOOM_05 111 -#define IDM_ZOOM_10 112 -#define IDM_ZOOM_20 113 -#define IDM_SAVE_SCREENSHOT 114 -#define IDM_TOGGLE_VISIBILITY 115 -#define IDM_CLOSE_WEBVIEW 116 -#define IDM_NEW_WINDOW 120 -#define IDM_PROCESS_INFO 121 -#define IDM_NEW_THREAD 122 -#define IDM_REINIT 123 -#define IDM_CRASH_PROCESS 125 -#define IDM_INJECT_SCRIPT 126 -#define IDM_GET_WEBVIEW_BOUNDS 127 -#define IDR_MAINFRAME 128 -#define IDM_POST_WEB_MESSAGE_STRING 130 -#define IDM_POST_WEB_MESSAGE_JSON 131 -#define IDM_TOGGLE_JAVASCRIPT 132 -#define IDM_TOGGLE_WEB_MESSAGING 133 -#define IDM_COMPLETE_JAVASCRIPT_DIALOG 137 -#define IDM_SUBSCRIBE_TO_CDP_EVENT 138 -#define IDM_CALL_CDP_METHOD 139 -#define IDM_TOGGLE_FULLSCREEN_ALLOWED 141 -#define IDD_DIALOG_INPUT 142 -#define IDM_FOCUS_SET 147 -#define IDM_FOCUS_TAB_IN 148 -#define IDM_FOCUS_REVERSE_TAB_IN 149 -#define IDM_SIZE_25 151 -#define IDM_SIZE_50 152 -#define IDM_SIZE_75 153 -#define IDM_SIZE_100 154 -#define IDM_TOGGLE_TAB_HANDLING 155 -#define ID_SETTINGS_STATUS_BAR_ENABLED 156 -#define ID_SETTINGS_DEV_TOOLS_ENABLED 157 -#define IDM_USE_DEFAULT_SCRIPT_DIALOGS 158 -#define IDM_USE_CUSTOM_SCRIPT_DIALOGS 159 -#define IDM_USE_DEFERRED_SCRIPT_DIALOGS 160 -#define IDM_GET_DOCUMENT_TITLE 161 -#define IDM_TRANSFORM_NONE 163 -#define IDM_TRANSFORM_ROTATE_30DEG 165 -#define IDM_TRANSFORM_ROTATE_60DEG_DIAG 166 -#define IDM_ADD_HOST_OBJECT 169 -#define IDM_GET_BROWSER_VERSION_AFTER_CREATION 170 -#define IDM_GET_BROWSER_VERSION_BEFORE_CREATION 171 -#define IDM_OPEN_DEVTOOLS_WINDOW 174 -#define IDM_CLOSE_WEBVIEW_CLEANUP 175 -#define IDM_SCALE_50 186 -#define IDM_SCALE_100 187 -#define IDM_SCALE_125 188 -#define IDM_SCALE_150 189 -#define IDM_GET_WEBVIEW_ZOOM 190 -#define IDM_CRASH_RENDER_PROCESS 191 -#define IDM_SET_LANGUAGE 192 -#define IDM_CREATION_MODE_WINDOWED 193 -#define IDM_CREATION_MODE_VISUAL_DCOMP 194 -#define IDM_CREATION_MODE_TARGET_DCOMP 195 -#ifdef USE_WEBVIEW2_WIN10 -#define IDM_CREATION_MODE_VISUAL_WINCOMP 196 -#endif -#define IDM_SCENARIO_JAVA_SCRIPT 199 -#define IDM_SCENARIO_TYPE_SCRIPT 200 -#define IDM_TOGGLE_AAD_SSO 201 - -#define IDE_ADDRESSBAR 1000 -#define IDE_ADDRESSBAR_GO 1001 -#define IDE_BACK 1002 -#define IDE_FORWARD 1003 -#define IDE_ADDRESSBAR_RELOAD 1004 -#define IDE_CANCEL 1005 -#define IDC_EDIT_INPUT 1006 -#define IDC_STATIC_LABEL 1007 -#define IDC_EDIT_DESCRIPTION 1008 -// Scenario IDMs -#define IDM_SCENARIO_AUTHENTICATION 2000 -#define IDM_SCENARIO_POST_WEB_MESSAGE 2001 -#define IDM_SCENARIO_WEB_VIEW_EVENT_MONITOR 2002 -#define IDM_SCENARIO_ADD_HOST_OBJECT 2003 -#define ID_BLOCKEDSITES 32773 -#define ID_SETTINGS_NAVIGATETOSTRING 32774 -#define ID_ADD_INITIALIZE_SCRIPT 32775 -#define ID_REMOVE_INITIALIZE_SCRIPT 32776 -#define ID_SETTINGS_BLOCKALLIMAGES 32777 -#define ID_SETTINGS_SETUSERAGENT 32778 -#define ID_SETTINGS_HOST_OBJECTS_ALLOWED 32779 -#define ID_SETTINGS_CONTEXT_MENUS_ENABLED 32780 -#define ID_SETTINGS_ZOOM_ENABLED 32781 -#define ID_SETTINGS_BUILTIN_ERROR_PAGE_ENABLED 32782 -#define IDC_STATIC -1 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NO_MFC 1 -#define _APS_NEXT_RESOURCE_VALUE 210 -#define _APS_NEXT_COMMAND_VALUE 32783 -#define _APS_NEXT_CONTROL_VALUE 1007 -#define _APS_NEXT_SYMED_VALUE 110 -#endif -#endif +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by WebView2APISample.rc +// +// clang-format off +#define IDC_MYICON 2 +#define IDS_APP_TITLE 102 +#define IDD_ABOUTBOX 103 +#define IDM_ABOUT 104 +#define IDM_EXIT 105 +#define IDI_WEBVIEW2APISAMPLE 107 +#define IDI_SMALL 108 +#define IDC_WEBVIEW2APISAMPLE 109 +#define IDC_WEBVIEW2APISAMPLEHOST 110 +#define IDM_ZOOM_05 111 +#define IDM_ZOOM_10 112 +#define IDM_ZOOM_20 113 +#define IDM_SAVE_SCREENSHOT 114 +#define IDM_TOGGLE_VISIBILITY 115 +#define IDM_CLOSE_WEBVIEW 116 +#define IDM_NEW_WINDOW 120 +#define IDM_PROCESS_INFO 121 +#define IDM_NEW_THREAD 122 +#define IDM_REINIT 123 +#define IDM_CRASH_PROCESS 125 +#define IDM_INJECT_SCRIPT 126 +#define IDM_GET_WEBVIEW_BOUNDS 127 +#define IDR_MAINFRAME 128 +#define IDM_POST_WEB_MESSAGE_STRING 130 +#define IDM_POST_WEB_MESSAGE_JSON 131 +#define IDM_TOGGLE_JAVASCRIPT 132 +#define IDM_TOGGLE_WEB_MESSAGING 133 +#define IDM_COMPLETE_JAVASCRIPT_DIALOG 137 +#define IDM_SUBSCRIBE_TO_CDP_EVENT 138 +#define IDM_CALL_CDP_METHOD 139 +#define IDM_TOGGLE_FULLSCREEN_ALLOWED 141 +#define IDD_DIALOG_INPUT 142 +#define IDM_FOCUS_SET 147 +#define IDM_FOCUS_TAB_IN 148 +#define IDM_FOCUS_REVERSE_TAB_IN 149 +#define IDM_SIZE_25 151 +#define IDM_SIZE_50 152 +#define IDM_SIZE_75 153 +#define IDM_SIZE_100 154 +#define IDM_TOGGLE_TAB_HANDLING 155 +#define ID_SETTINGS_STATUS_BAR_ENABLED 156 +#define ID_SETTINGS_DEV_TOOLS_ENABLED 157 +#define IDM_USE_DEFAULT_SCRIPT_DIALOGS 158 +#define IDM_USE_CUSTOM_SCRIPT_DIALOGS 159 +#define IDM_USE_DEFERRED_SCRIPT_DIALOGS 160 +#define IDM_GET_DOCUMENT_TITLE 161 +#define IDM_TRANSFORM_NONE 163 +#define IDM_TRANSFORM_ROTATE_30DEG 165 +#define IDM_TRANSFORM_ROTATE_60DEG_DIAG 166 +#define IDM_ADD_HOST_OBJECT 169 +#define IDM_GET_BROWSER_VERSION_AFTER_CREATION 170 +#define IDM_GET_BROWSER_VERSION_BEFORE_CREATION 171 +#define IDM_OPEN_DEVTOOLS_WINDOW 174 +#define IDM_CLOSE_WEBVIEW_CLEANUP 175 +#define IDM_SCALE_50 186 +#define IDM_SCALE_100 187 +#define IDM_SCALE_125 188 +#define IDM_SCALE_150 189 +#define IDM_GET_WEBVIEW_ZOOM 190 +#define IDM_CRASH_RENDER_PROCESS 191 +#define IDM_SET_LANGUAGE 192 +#define IDM_CREATION_MODE_WINDOWED 193 +#define IDM_CREATION_MODE_VISUAL_DCOMP 194 +#define IDM_CREATION_MODE_TARGET_DCOMP 195 +#ifdef USE_WEBVIEW2_WIN10 +#define IDM_CREATION_MODE_VISUAL_WINCOMP 196 +#endif +#define IDM_SCENARIO_JAVA_SCRIPT 199 +#define IDM_SCENARIO_TYPE_SCRIPT 200 +#define IDM_TOGGLE_AAD_SSO 201 +#define IDM_TOGGLE_CURSOR_HANDLING 208 + +#define IDE_ADDRESSBAR 1000 +#define IDE_ADDRESSBAR_GO 1001 +#define IDE_BACK 1002 +#define IDE_FORWARD 1003 +#define IDE_ADDRESSBAR_RELOAD 1004 +#define IDE_CANCEL 1005 +#define IDC_EDIT_INPUT 1006 +#define IDC_STATIC_LABEL 1007 +#define IDC_EDIT_DESCRIPTION 1008 +// Scenario IDMs +#define IDM_SCENARIO_COOKIE_MANAGEMENT 1999 +#define IDM_SCENARIO_AUTHENTICATION 2000 +#define IDM_SCENARIO_POST_WEB_MESSAGE 2001 +#define IDM_SCENARIO_WEB_VIEW_EVENT_MONITOR 2002 +#define IDM_SCENARIO_ADD_HOST_OBJECT 2003 +#define IDM_SCENARIO_DOM_CONTENT_LOADED 2004 +#define IDM_SCENARIO_NAVIGATEWITHWEBRESOURCEREQUEST 2005 + +#define ID_BLOCKEDSITES 32773 +#define ID_SETTINGS_NAVIGATETOSTRING 32774 +#define ID_ADD_INITIALIZE_SCRIPT 32775 +#define ID_REMOVE_INITIALIZE_SCRIPT 32776 +#define ID_SETTINGS_BLOCKALLIMAGES 32777 +#define ID_SETTINGS_SETUSERAGENT 32778 +#define ID_SETTINGS_HOST_OBJECTS_ALLOWED 32779 +#define ID_SETTINGS_CONTEXT_MENUS_ENABLED 32780 +#define ID_SETTINGS_ZOOM_ENABLED 32781 +#define ID_SETTINGS_BUILTIN_ERROR_PAGE_ENABLED 32782 +#define IDC_STATIC -1 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NO_MFC 1 +#define _APS_NEXT_RESOURCE_VALUE 210 +#define _APS_NEXT_COMMAND_VALUE 32783 +#define _APS_NEXT_CONTROL_VALUE 1007 +#define _APS_NEXT_SYMED_VALUE 110 +#endif +#endif diff --git a/SampleApps/WebView2WindowsFormsBrowser/WebView2WindowsFormsBrowser.csproj b/SampleApps/WebView2WindowsFormsBrowser/WebView2WindowsFormsBrowser.csproj index 907c05c8..6401a5a3 100644 --- a/SampleApps/WebView2WindowsFormsBrowser/WebView2WindowsFormsBrowser.csproj +++ b/SampleApps/WebView2WindowsFormsBrowser/WebView2WindowsFormsBrowser.csproj @@ -10,6 +10,6 @@ Debug;Release;Win7 Release;Win7 Debug - + \ No newline at end of file diff --git a/SampleApps/WebView2WpfBrowser/MainWindow.xaml b/SampleApps/WebView2WpfBrowser/MainWindow.xaml index 22273701..ccea6602 100644 --- a/SampleApps/WebView2WpfBrowser/MainWindow.xaml +++ b/SampleApps/WebView2WpfBrowser/MainWindow.xaml @@ -28,6 +28,11 @@ found in the LICENSE file. + + + + + @@ -40,6 +45,16 @@ found in the LICENSE file. + + + + + + + + + + diff --git a/SampleApps/WebView2WpfBrowser/MainWindow.xaml.cs b/SampleApps/WebView2WpfBrowser/MainWindow.xaml.cs index 6db19100..a4eb5e1f 100644 --- a/SampleApps/WebView2WpfBrowser/MainWindow.xaml.cs +++ b/SampleApps/WebView2WpfBrowser/MainWindow.xaml.cs @@ -3,9 +3,12 @@ // found in the LICENSE file. using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; +using System.Net; using System.Text; using System.Threading.Tasks; using System.Windows; @@ -18,6 +21,7 @@ using System.Windows.Navigation; using System.Windows.Shapes; using Microsoft.Web.WebView2.Core; +using Microsoft.Web.WebView2.Wpf; namespace WebView2WpfBrowser { @@ -27,6 +31,11 @@ namespace WebView2WpfBrowser public partial class MainWindow : Window { public static RoutedCommand InjectScriptCommand = new RoutedCommand(); + public static RoutedCommand NavigateWithWebResourceRequestCommand = new RoutedCommand(); + public static RoutedCommand DOMContentLoadedCommand = new RoutedCommand(); + public static RoutedCommand AddOrUpdateCookieCommand = new RoutedCommand(); + public static RoutedCommand DeleteCookiesCommand = new RoutedCommand(); + public static RoutedCommand DeleteAllCookiesCommand = new RoutedCommand(); bool _isNavigating = false; public MainWindow() @@ -131,12 +140,75 @@ async void InjectScriptCmdExecuted(object target, ExecutedRoutedEventArgs e) title: "Inject Script", description: "Enter some JavaScript to be executed in the context of this page.", defaultInput: "window.getComputedStyle(document.body).backgroundColor"); - if (dialog.ShowDialog() == true) { + if (dialog.ShowDialog() == true) + { string scriptResult = await webView.ExecuteScriptAsync(dialog.Input.Text); MessageBox.Show(this, scriptResult, "Script Result"); } } + void AddOrUpdateCookieCmdExecuted(object target, ExecutedRoutedEventArgs e) + { + CoreWebView2Cookie cookie = webView.CoreWebView2.CookieManager.CreateCookie("CookieName", "CookieValue", ".bing.com", "/"); + webView.CoreWebView2.CookieManager.AddOrUpdateCookie(cookie); + } + + void DeleteAllCookiesCmdExecuted(object target, ExecutedRoutedEventArgs e) + { + webView.CoreWebView2.CookieManager.DeleteAllCookies(); + } + + void DeleteCookiesCmdExecuted(object target, ExecutedRoutedEventArgs e) + { + webView.CoreWebView2.CookieManager.DeleteCookiesWithDomainAndPath("CookieName", ".bing.com", "/"); + } + + void DOMContentLoadedCmdExecuted(object target, ExecutedRoutedEventArgs e) + { + webView.CoreWebView2.DOMContentLoaded += (object sender, CoreWebView2DOMContentLoadedEventArgs arg) => + { + webView.ExecuteScriptAsync("let " + + "content=document.createElement(\"h2\");content.style.color=" + + "'blue';content.textContent= \"This text was added by the " + + "host app\";document.body.appendChild(content);"); + }; + webView.NavigateToString(@"

DOMContentLoaded sample page

The content below will be added after DOM content is loaded

"); + } + + private CoreWebView2Environment _coreWebView2Environment; + + async void NavigateWithWebResourceRequestCmdExecuted(object target, ExecutedRoutedEventArgs e) + { + // Need CoreWebView2Environment + if (_coreWebView2Environment == null) + { + _coreWebView2Environment = webView.CoreWebView2.Environment; + } + + // Prepare post data as UTF-8 byte array and convert it to stream + // as required by the application/x-www-form-urlencoded Content-Type + var dialog = new TextInputDialog( + title: "NavigateWithWebResourceRequest", + description: "Specify post data to submit to https://www.w3schools.com/action_page.php."); + if (dialog.ShowDialog() == true) + { + string postDataString = "input=" + dialog.Input.Text; + UTF8Encoding utfEncoding = new UTF8Encoding(); + byte[] postData = utfEncoding.GetBytes( + postDataString); + MemoryStream postDataStream = new MemoryStream(postDataString.Length); + postDataStream.Write(postData, 0, postData.Length); + postDataStream.Seek(0, SeekOrigin.Begin); + CoreWebView2WebResourceRequest webResourceRequest = + _coreWebView2Environment.CreateWebResourceRequest( + "https://www.w3schools.com/action_page.php", + "POST", + postDataStream, + "Content-Type: application/x-www-form-urlencoded\r\n"); + webView.CoreWebView2.NavigateWithWebResourceRequest(webResourceRequest); + } + } + void GoToPageCmdCanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = webView != null && !_isNavigating; @@ -144,12 +216,11 @@ void GoToPageCmdCanExecute(object sender, CanExecuteRoutedEventArgs e) async void GoToPageCmdExecuted(object target, ExecutedRoutedEventArgs e) { - // Setting webView.Source will not trigger a navigation if the Source is the same - // as the previous Source. CoreWebView.Navigate() will always trigger a navigation. + // Setting webView.Source will not trigger a navigation if the Source is the same + // as the previous Source. CoreWebView.Navigate() will always trigger a navigation. await webView.EnsureCoreWebView2Async(); webView.CoreWebView2.Navigate((string)e.Parameter); } - void WebView_NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs e) { _isNavigating = true; @@ -162,6 +233,78 @@ void WebView_NavigationCompleted(object sender, CoreWebView2NavigationCompletedE RequeryCommands(); } + private static void OnShowNextWebResponseChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + MainWindow window = (MainWindow)d; + if ((bool)e.NewValue) + { + window.webView.CoreWebView2.WebResourceResponseReceived += window.CoreWebView2_WebResourceResponseReceived; + } + else + { + window.webView.CoreWebView2.WebResourceResponseReceived -= window.CoreWebView2_WebResourceResponseReceived; + } + } + + public static readonly DependencyProperty ShowNextWebResponseProperty = DependencyProperty.Register( + nameof(ShowNextWebResponse), + typeof(Boolean), + typeof(MainWindow), + new PropertyMetadata(false, OnShowNextWebResponseChanged)); + + public bool ShowNextWebResponse + { + get => (bool)this.GetValue(ShowNextWebResponseProperty); + set => this.SetValue(ShowNextWebResponseProperty, value); + } + + async void CoreWebView2_WebResourceResponseReceived(object sender, CoreWebView2WebResourceResponseReceivedEventArgs e) + { + ShowNextWebResponse = false; + + CoreWebView2WebResourceRequest request = e.Request; + CoreWebView2WebResourceResponseView response = e.Response; + + string caption = "Web Resource Response Received"; + // Start with capacity 64 for minimum message size + StringBuilder messageBuilder = new StringBuilder(64); + string HttpMessageContentToString(System.IO.Stream content) => content == null ? "[null]" : "[data]"; + void AppendHeaders(IEnumerable headers) + { + foreach (var header in headers) + { + messageBuilder.AppendLine($" {header}"); + } + } + + // Request + messageBuilder.AppendLine("Request"); + messageBuilder.AppendLine($"URI: {request.Uri}"); + messageBuilder.AppendLine($"Method: {request.Method}"); + messageBuilder.AppendLine("Headers:"); + AppendHeaders(request.Headers); + messageBuilder.AppendLine($"Content: {HttpMessageContentToString(request.Content)}"); + messageBuilder.AppendLine(); + + // Response + messageBuilder.AppendLine("Response"); + messageBuilder.AppendLine($"Status: {response.StatusCode}"); + messageBuilder.AppendLine($"Reason: {response.ReasonPhrase}"); + messageBuilder.AppendLine("Headers:"); + AppendHeaders(response.Headers); + try + { + Stream content = await response.GetContentAsync(); + messageBuilder.AppendLine($"Content: {HttpMessageContentToString(content)}"); + } + catch (System.Runtime.InteropServices.COMException) + { + messageBuilder.AppendLine($"Content: [failed to load]"); + } + + MessageBox.Show(messageBuilder.ToString(), caption); + } + void RequeryCommands() { // Seems like there should be a way to bind CanExecute directly to a bool property @@ -170,7 +313,7 @@ void RequeryCommands() // which signal that one of the underlying bool properties might have changed and // bluntly tell all commands to re-check their CanExecute status. // - // Another way to trigger this re-check would be to create our own bool dependency + // Another way to trigger this re-check would be to create our own bool dependency // properties on this class, bind them to the underlying properties, and implement a // PropertyChangedCallback on them. That arguably more directly binds the status of // the commands to the WebView's state, but at the cost of having an extraneous diff --git a/SampleApps/WebView2WpfBrowser/WebView2WpfBrowser.csproj b/SampleApps/WebView2WpfBrowser/WebView2WpfBrowser.csproj index e9acce7a..83554b51 100644 --- a/SampleApps/WebView2WpfBrowser/WebView2WpfBrowser.csproj +++ b/SampleApps/WebView2WpfBrowser/WebView2WpfBrowser.csproj @@ -11,6 +11,6 @@ Debug;Release;Win7 Release;Win7 Debug - + \ No newline at end of file