├── .clang-format ├── .github ├── CODE_OF_CONDUCT.md └── ISSUE_TEMPLATE │ ├── config.yml │ └── issue-with-webview2browser-app.md ├── .gitignore ├── BrowserWindow.cpp ├── BrowserWindow.h ├── LICENSE ├── README.md ├── Resource.h ├── Tab.cpp ├── Tab.h ├── WebView2Browser.ico ├── WebViewBrowserApp.cpp ├── WebViewBrowserApp.h ├── WebViewBrowserApp.rc ├── WebViewBrowserApp.sln ├── WebViewBrowserApp.vcxproj ├── WebViewBrowserApp.vcxproj.filters ├── framework.h ├── packages.config ├── screenshots ├── WebView2Browser.png └── layout.png ├── targetver.h └── wvbrowser_ui ├── commands.js ├── content_ui ├── favorites.html ├── favorites.js ├── history.css ├── history.html ├── history.js ├── img │ ├── close.png │ ├── favorites.png │ ├── history.png │ └── settings.png ├── items.css ├── settings.css ├── settings.html ├── settings.js └── styles.css └── controls_ui ├── address-bar.css ├── controls.css ├── default.css ├── default.html ├── default.js ├── favorites.js ├── history.js ├── img ├── cancel.png ├── favicon.png ├── favorite.png ├── favorited.png ├── goBack.png ├── goBack_disabled.png ├── goForward.png ├── goForward_disabled.png ├── insecure.png ├── neutral.png ├── options.png ├── reload.png ├── secure.png └── unknown.png ├── options.css ├── options.html ├── options.js ├── storage.js ├── strip.css ├── styles.css └── tabs.js /.clang-format: -------------------------------------------------------------------------------- 1 | # Defines the coding style to apply. See: 2 | # 3 | DisableFormat: true 4 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Issue with WebView2 4 | url: https://github.com/MicrosoftEdge/WebViewFeedback/issues/new/choose 5 | about: Please open issues with the WebView2 control in the Feedback repo 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-with-webview2browser-app.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue with WebView2Browser app 3 | about: Open an issue specifically about the WebView2Browser application (not the WebView2 4 | control) 5 | title: '' 6 | labels: '' 7 | assignees: '' 8 | 9 | --- 10 | 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.vcxproj.user 2 | Release_x64/ 3 | Release_x86/ 4 | Debug_x64/ 5 | Debug_x86/ 6 | packages/ 7 | *.vscode 8 | .vscode/ 9 | Release/ 10 | Debug/ 11 | ipch/ 12 | x64/ 13 | x86/ 14 | .vs/ 15 | 16 | // Ignore the binary generated version of the resource (.rc) file 17 | *.aps -------------------------------------------------------------------------------- /BrowserWindow.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include "BrowserWindow.h" 6 | #include "shlobj.h" 7 | #include 8 | #pragma comment (lib, "Urlmon.lib") 9 | 10 | using namespace Microsoft::WRL; 11 | 12 | WCHAR BrowserWindow::s_windowClass[] = { 0 }; 13 | WCHAR BrowserWindow::s_title[] = { 0 }; 14 | 15 | // 16 | // FUNCTION: RegisterClass() 17 | // 18 | // PURPOSE: Registers the window class. 19 | // 20 | ATOM BrowserWindow::RegisterClass(_In_ HINSTANCE hInstance) 21 | { 22 | // Initialize window class string 23 | LoadStringW(hInstance, IDC_WEBVIEWBROWSERAPP, s_windowClass, MAX_LOADSTRING); 24 | WNDCLASSEXW wcex; 25 | 26 | wcex.cbSize = sizeof(WNDCLASSEX); 27 | 28 | wcex.style = CS_HREDRAW | CS_VREDRAW; 29 | wcex.lpfnWndProc = WndProcStatic; 30 | wcex.cbClsExtra = 0; 31 | wcex.cbWndExtra = 0; 32 | wcex.hInstance = hInstance; 33 | wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WEBVIEWBROWSERAPP)); 34 | wcex.hCursor = LoadCursor(nullptr, IDC_ARROW); 35 | wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); 36 | wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_WEBVIEWBROWSERAPP); 37 | wcex.lpszClassName = s_windowClass; 38 | wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL)); 39 | 40 | return RegisterClassExW(&wcex); 41 | } 42 | 43 | // 44 | // FUNCTION: WndProcStatic(HWND, UINT, WPARAM, LPARAM) 45 | // 46 | // PURPOSE: Redirect messages to approriate instance or call default proc 47 | // 48 | // WM_COMMAND - process the application menu 49 | // WM_PAINT - Paint the main window 50 | // WM_DESTROY - post a quit message and return 51 | // 52 | // 53 | LRESULT CALLBACK BrowserWindow::WndProcStatic(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) 54 | { 55 | // Get the ptr to the BrowserWindow instance who created this hWnd. 56 | // The pointer was set when the hWnd was created during InitInstance. 57 | BrowserWindow* browser_window = reinterpret_cast(GetWindowLongPtr(hWnd, GWLP_USERDATA)); 58 | if (browser_window != nullptr) 59 | { 60 | return browser_window->WndProc(hWnd, message, wParam, lParam); // Forward message to instance-aware WndProc 61 | } 62 | else 63 | { 64 | return DefWindowProc(hWnd, message, wParam, lParam); 65 | } 66 | } 67 | 68 | // 69 | // FUNCTION: WndProc(HWND, UINT, WPARAM, LPARAM) 70 | // 71 | // PURPOSE: Processes messages for each browser window instance. 72 | // 73 | // WM_COMMAND - process the application menu 74 | // WM_PAINT - Paint the main window 75 | // WM_DESTROY - post a quit message and return 76 | // 77 | // 78 | LRESULT CALLBACK BrowserWindow::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) 79 | { 80 | 81 | switch (message) 82 | { 83 | case WM_GETMINMAXINFO: 84 | { 85 | MINMAXINFO* minmax = reinterpret_cast(lParam); 86 | minmax->ptMinTrackSize.x = m_minWindowWidth; 87 | minmax->ptMinTrackSize.y = m_minWindowHeight; 88 | } 89 | break; 90 | case WM_DPICHANGED: 91 | { 92 | UpdateMinWindowSize(); 93 | } 94 | case WM_SIZE: 95 | { 96 | ResizeUIWebViews(); 97 | if (m_tabs.find(m_activeTabId) != m_tabs.end()) 98 | { 99 | m_tabs.at(m_activeTabId)->ResizeWebView(); 100 | } 101 | } 102 | break; 103 | case WM_CLOSE: 104 | { 105 | web::json::value jsonObj = web::json::value::parse(L"{}"); 106 | jsonObj[L"message"] = web::json::value(MG_CLOSE_WINDOW); 107 | jsonObj[L"args"] = web::json::value::parse(L"{}"); 108 | 109 | CheckFailure(PostJsonToWebView(jsonObj, m_controlsWebView.Get()), L"Try again."); 110 | } 111 | break; 112 | case WM_NCDESTROY: 113 | { 114 | SetWindowLongPtr(hWnd, GWLP_USERDATA, NULL); 115 | delete this; 116 | PostQuitMessage(0); 117 | } 118 | case WM_PAINT: 119 | { 120 | PAINTSTRUCT ps; 121 | HDC hdc = BeginPaint(hWnd, &ps); 122 | EndPaint(hWnd, &ps); 123 | } 124 | break; 125 | default: 126 | { 127 | return DefWindowProc(hWnd, message, wParam, lParam); 128 | } 129 | break; 130 | } 131 | return 0; 132 | } 133 | 134 | 135 | BOOL BrowserWindow::LaunchWindow(_In_ HINSTANCE hInstance, _In_ int nCmdShow) 136 | { 137 | // BrowserWindow keeps a reference to itself in its host window and will 138 | // delete itself when the window is destroyed. 139 | BrowserWindow* window = new BrowserWindow(); 140 | if (!window->InitInstance(hInstance, nCmdShow)) 141 | { 142 | delete window; 143 | return FALSE; 144 | } 145 | return TRUE; 146 | } 147 | 148 | // 149 | // FUNCTION: InitInstance(HINSTANCE, int) 150 | // 151 | // PURPOSE: Saves instance handle and creates main window 152 | // 153 | // COMMENTS: 154 | // 155 | // In this function, we save the instance handle in a global variable and 156 | // create and display the main program window. 157 | // 158 | BOOL BrowserWindow::InitInstance(HINSTANCE hInstance, int nCmdShow) 159 | { 160 | m_hInst = hInstance; // Store app instance handle 161 | LoadStringW(m_hInst, IDS_APP_TITLE, s_title, MAX_LOADSTRING); 162 | 163 | SetUIMessageBroker(); 164 | 165 | m_hWnd = CreateWindowW(s_windowClass, s_title, WS_OVERLAPPEDWINDOW, 166 | CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, m_hInst, nullptr); 167 | 168 | if (!m_hWnd) 169 | { 170 | return FALSE; 171 | } 172 | 173 | // Make the BrowserWindow instance ptr available through the hWnd 174 | SetWindowLongPtr(m_hWnd, GWLP_USERDATA, reinterpret_cast(this)); 175 | 176 | UpdateMinWindowSize(); 177 | ShowWindow(m_hWnd, nCmdShow); 178 | UpdateWindow(m_hWnd); 179 | 180 | // Get directory for user data. This will be kept separated from the 181 | // directory for the browser UI data. 182 | std::wstring userDataDirectory = GetAppDataDirectory(); 183 | userDataDirectory.append(L"\\User Data"); 184 | 185 | // Create WebView environment for web content requested by the user. All 186 | // tabs will be created from this environment and kept isolated from the 187 | // browser UI. This enviroment is created first so the UI can request new 188 | // tabs when it's ready. 189 | HRESULT hr = CreateCoreWebView2EnvironmentWithOptions(nullptr, userDataDirectory.c_str(), 190 | nullptr, Callback( 191 | [this](HRESULT result, ICoreWebView2Environment* env) -> HRESULT 192 | { 193 | RETURN_IF_FAILED(result); 194 | 195 | m_contentEnv = env; 196 | HRESULT hr = InitUIWebViews(); 197 | 198 | if (!SUCCEEDED(hr)) 199 | { 200 | OutputDebugString(L"UI WebViews environment creation failed\n"); 201 | } 202 | 203 | return hr; 204 | }).Get()); 205 | 206 | if (!SUCCEEDED(hr)) 207 | { 208 | OutputDebugString(L"Content WebViews environment creation failed\n"); 209 | return FALSE; 210 | } 211 | 212 | return TRUE; 213 | } 214 | 215 | HRESULT BrowserWindow::InitUIWebViews() 216 | { 217 | // Get data directory for browser UI data 218 | std::wstring browserDataDirectory = GetAppDataDirectory(); 219 | browserDataDirectory.append(L"\\Browser Data"); 220 | 221 | // Create WebView environment for browser UI. A separate data directory is 222 | // used to isolate the browser UI from web content requested by the user. 223 | return CreateCoreWebView2EnvironmentWithOptions(nullptr, browserDataDirectory.c_str(), 224 | nullptr, Callback( 225 | [this](HRESULT result, ICoreWebView2Environment* env) -> HRESULT 226 | { 227 | // Environment is ready, create the WebView 228 | m_uiEnv = env; 229 | 230 | RETURN_IF_FAILED(CreateBrowserControlsWebView()); 231 | RETURN_IF_FAILED(CreateBrowserOptionsWebView()); 232 | 233 | return S_OK; 234 | }).Get()); 235 | } 236 | 237 | HRESULT BrowserWindow::CreateBrowserControlsWebView() 238 | { 239 | return m_uiEnv->CreateCoreWebView2Controller(m_hWnd, Callback( 240 | [this](HRESULT result, ICoreWebView2Controller* host) -> HRESULT 241 | { 242 | if (!SUCCEEDED(result)) 243 | { 244 | OutputDebugString(L"Controls WebView creation failed\n"); 245 | return result; 246 | } 247 | // WebView created 248 | m_controlsController = host; 249 | CheckFailure(m_controlsController->get_CoreWebView2(&m_controlsWebView), L""); 250 | 251 | wil::com_ptr settings; 252 | RETURN_IF_FAILED(m_controlsWebView->get_Settings(&settings)); 253 | RETURN_IF_FAILED(settings->put_AreDevToolsEnabled(FALSE)); 254 | 255 | RETURN_IF_FAILED(m_controlsController->add_ZoomFactorChanged(Callback( 256 | [](ICoreWebView2Controller* host, IUnknown* args) -> HRESULT 257 | { 258 | host->put_ZoomFactor(1.0); 259 | return S_OK; 260 | } 261 | ).Get(), &m_controlsZoomToken)); 262 | 263 | RETURN_IF_FAILED(m_controlsWebView->add_WebMessageReceived(m_uiMessageBroker.Get(), &m_controlsUIMessageBrokerToken)); 264 | RETURN_IF_FAILED(ResizeUIWebViews()); 265 | 266 | std::wstring controlsPath = GetFullPathFor(L"wvbrowser_ui\\controls_ui\\default.html"); 267 | RETURN_IF_FAILED(m_controlsWebView->Navigate(controlsPath.c_str())); 268 | 269 | return S_OK; 270 | }).Get()); 271 | } 272 | 273 | HRESULT BrowserWindow::CreateBrowserOptionsWebView() 274 | { 275 | return m_uiEnv->CreateCoreWebView2Controller(m_hWnd, Callback( 276 | [this](HRESULT result, ICoreWebView2Controller* host) -> HRESULT 277 | { 278 | if (!SUCCEEDED(result)) 279 | { 280 | OutputDebugString(L"Options WebView creation failed\n"); 281 | return result; 282 | } 283 | // WebView created 284 | m_optionsController = host; 285 | CheckFailure(m_optionsController->get_CoreWebView2(&m_optionsWebView), L""); 286 | 287 | wil::com_ptr settings; 288 | RETURN_IF_FAILED(m_optionsWebView->get_Settings(&settings)); 289 | RETURN_IF_FAILED(settings->put_AreDevToolsEnabled(FALSE)); 290 | 291 | RETURN_IF_FAILED(m_optionsController->add_ZoomFactorChanged(Callback( 292 | [](ICoreWebView2Controller* host, IUnknown* args) -> HRESULT 293 | { 294 | host->put_ZoomFactor(1.0); 295 | return S_OK; 296 | } 297 | ).Get(), &m_optionsZoomToken)); 298 | 299 | // Hide by default 300 | RETURN_IF_FAILED(m_optionsController->put_IsVisible(FALSE)); 301 | RETURN_IF_FAILED(m_optionsWebView->add_WebMessageReceived(m_uiMessageBroker.Get(), &m_optionsUIMessageBrokerToken)); 302 | 303 | // Hide menu when focus is lost 304 | RETURN_IF_FAILED(m_optionsController->add_LostFocus(Callback( 305 | [this](ICoreWebView2Controller* sender, IUnknown* args) -> HRESULT 306 | { 307 | web::json::value jsonObj = web::json::value::parse(L"{}"); 308 | jsonObj[L"message"] = web::json::value(MG_OPTIONS_LOST_FOCUS); 309 | jsonObj[L"args"] = web::json::value::parse(L"{}"); 310 | 311 | PostJsonToWebView(jsonObj, m_controlsWebView.Get()); 312 | 313 | return S_OK; 314 | }).Get(), &m_lostOptionsFocus)); 315 | 316 | RETURN_IF_FAILED(ResizeUIWebViews()); 317 | 318 | std::wstring optionsPath = GetFullPathFor(L"wvbrowser_ui\\controls_ui\\options.html"); 319 | RETURN_IF_FAILED(m_optionsWebView->Navigate(optionsPath.c_str())); 320 | 321 | return S_OK; 322 | }).Get()); 323 | } 324 | 325 | // Set the message broker for the UI webview. This will capture messages from ui web content. 326 | // Lambda is used to capture the instance while satisfying Microsoft::WRL::Callback() 327 | void BrowserWindow::SetUIMessageBroker() 328 | { 329 | m_uiMessageBroker = Callback( 330 | [this](ICoreWebView2* webview, ICoreWebView2WebMessageReceivedEventArgs* eventArgs) -> HRESULT 331 | { 332 | wil::unique_cotaskmem_string jsonString; 333 | CheckFailure(eventArgs->get_WebMessageAsJson(&jsonString), L""); // Get the message from the UI WebView as JSON formatted string 334 | web::json::value jsonObj = web::json::value::parse(jsonString.get()); 335 | 336 | if (!jsonObj.has_field(L"message")) 337 | { 338 | OutputDebugString(L"No message code provided\n"); 339 | return S_OK; 340 | } 341 | 342 | if (!jsonObj.has_field(L"args")) 343 | { 344 | OutputDebugString(L"The message has no args field\n"); 345 | return S_OK; 346 | } 347 | 348 | int message = jsonObj.at(L"message").as_integer(); 349 | web::json::value args = jsonObj.at(L"args"); 350 | 351 | switch (message) 352 | { 353 | case MG_CREATE_TAB: 354 | { 355 | size_t id = args.at(L"tabId").as_number().to_uint32(); 356 | bool shouldBeActive = args.at(L"active").as_bool(); 357 | std::unique_ptr newTab = Tab::CreateNewTab(m_hWnd, m_contentEnv.Get(), id, shouldBeActive); 358 | 359 | std::map>::iterator it = m_tabs.find(id); 360 | if (it == m_tabs.end()) 361 | { 362 | m_tabs.insert(std::pair>(id, std::move(newTab))); 363 | } 364 | else 365 | { 366 | m_tabs.at(id)->m_contentController->Close(); 367 | it->second = std::move(newTab); 368 | } 369 | } 370 | break; 371 | case MG_NAVIGATE: 372 | { 373 | std::wstring uri(args.at(L"uri").as_string()); 374 | std::wstring browserScheme(L"browser://"); 375 | 376 | if (uri.substr(0, browserScheme.size()).compare(browserScheme) == 0) 377 | { 378 | // No encoded search URI 379 | std::wstring path = uri.substr(browserScheme.size()); 380 | if (path.compare(L"favorites") == 0 || 381 | path.compare(L"settings") == 0 || 382 | path.compare(L"history") == 0) 383 | { 384 | std::wstring filePath(L"wvbrowser_ui\\content_ui\\"); 385 | filePath.append(path); 386 | filePath.append(L".html"); 387 | std::wstring fullPath = GetFullPathFor(filePath.c_str()); 388 | CheckFailure(m_tabs.at(m_activeTabId)->m_contentWebView->Navigate(fullPath.c_str()), L"Can't navigate to browser page."); 389 | } 390 | else 391 | { 392 | OutputDebugString(L"Requested unknown browser page\n"); 393 | } 394 | } 395 | else if (!SUCCEEDED(m_tabs.at(m_activeTabId)->m_contentWebView->Navigate(uri.c_str()))) 396 | { 397 | CheckFailure(m_tabs.at(m_activeTabId)->m_contentWebView->Navigate(args.at(L"encodedSearchURI").as_string().c_str()), L"Can't navigate to requested page."); 398 | } 399 | } 400 | break; 401 | case MG_GO_FORWARD: 402 | { 403 | CheckFailure(m_tabs.at(m_activeTabId)->m_contentWebView->GoForward(), L""); 404 | } 405 | break; 406 | case MG_GO_BACK: 407 | { 408 | CheckFailure(m_tabs.at(m_activeTabId)->m_contentWebView->GoBack(), L""); 409 | } 410 | break; 411 | case MG_RELOAD: 412 | { 413 | CheckFailure(m_tabs.at(m_activeTabId)->m_contentWebView->Reload(), L""); 414 | } 415 | break; 416 | case MG_CANCEL: 417 | { 418 | CheckFailure(m_tabs.at(m_activeTabId)->m_contentWebView->CallDevToolsProtocolMethod(L"Page.stopLoading", L"{}", nullptr), L""); 419 | } 420 | break; 421 | case MG_SWITCH_TAB: 422 | { 423 | size_t tabId = args.at(L"tabId").as_number().to_uint32(); 424 | 425 | SwitchToTab(tabId); 426 | } 427 | break; 428 | case MG_CLOSE_TAB: 429 | { 430 | size_t id = args.at(L"tabId").as_number().to_uint32(); 431 | m_tabs.at(id)->m_contentController->Close(); 432 | m_tabs.erase(id); 433 | } 434 | break; 435 | case MG_CLOSE_WINDOW: 436 | { 437 | DestroyWindow(m_hWnd); 438 | } 439 | break; 440 | case MG_SHOW_OPTIONS: 441 | { 442 | CheckFailure(m_optionsController->put_IsVisible(TRUE), L""); 443 | m_optionsController->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC); 444 | } 445 | break; 446 | case MG_HIDE_OPTIONS: 447 | { 448 | CheckFailure(m_optionsController->put_IsVisible(FALSE), L"Something went wrong when trying to close the options dropdown."); 449 | } 450 | break; 451 | case MG_OPTION_SELECTED: 452 | { 453 | m_tabs.at(m_activeTabId)->m_contentController->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC); 454 | } 455 | break; 456 | case MG_GET_FAVORITES: 457 | case MG_GET_SETTINGS: 458 | case MG_GET_HISTORY: 459 | { 460 | // Forward back to requesting tab 461 | size_t tabId = args.at(L"tabId").as_number().to_uint32(); 462 | jsonObj[L"args"].erase(L"tabId"); 463 | 464 | CheckFailure(PostJsonToWebView(jsonObj, m_tabs.at(tabId)->m_contentWebView.Get()), L"Requesting history failed."); 465 | } 466 | break; 467 | default: 468 | { 469 | OutputDebugString(L"Unexpected message\n"); 470 | } 471 | break; 472 | } 473 | 474 | return S_OK; 475 | }); 476 | } 477 | 478 | HRESULT BrowserWindow::SwitchToTab(size_t tabId) 479 | { 480 | size_t previousActiveTab = m_activeTabId; 481 | 482 | RETURN_IF_FAILED(m_tabs.at(tabId)->ResizeWebView()); 483 | RETURN_IF_FAILED(m_tabs.at(tabId)->m_contentController->put_IsVisible(TRUE)); 484 | m_activeTabId = tabId; 485 | 486 | if (previousActiveTab != INVALID_TAB_ID && previousActiveTab != m_activeTabId) { 487 | auto previousTabIterator = m_tabs.find(previousActiveTab); 488 | if (previousTabIterator != m_tabs.end() && previousTabIterator->second && 489 | previousTabIterator->second->m_contentController) 490 | { 491 | auto hr = m_tabs.at(previousActiveTab)->m_contentController->put_IsVisible(FALSE); 492 | if (hr == HRESULT_FROM_WIN32(ERROR_INVALID_STATE)) { 493 | web::json::value jsonObj = web::json::value::parse(L"{}"); 494 | jsonObj[L"message"] = web::json::value(MG_CLOSE_TAB); 495 | jsonObj[L"args"] = web::json::value::parse(L"{}"); 496 | jsonObj[L"args"][L"tabId"] = web::json::value::number(previousActiveTab); 497 | 498 | PostJsonToWebView(jsonObj, m_controlsWebView.Get()); 499 | } 500 | RETURN_IF_FAILED(hr); 501 | } 502 | } 503 | 504 | return S_OK; 505 | } 506 | 507 | HRESULT BrowserWindow::HandleTabURIUpdate(size_t tabId, ICoreWebView2* webview) 508 | { 509 | wil::unique_cotaskmem_string source; 510 | RETURN_IF_FAILED(webview->get_Source(&source)); 511 | 512 | web::json::value jsonObj = web::json::value::parse(L"{}"); 513 | jsonObj[L"message"] = web::json::value(MG_UPDATE_URI); 514 | jsonObj[L"args"] = web::json::value::parse(L"{}"); 515 | jsonObj[L"args"][L"tabId"] = web::json::value::number(tabId); 516 | jsonObj[L"args"][L"uri"] = web::json::value(source.get()); 517 | 518 | std::wstring uri(source.get()); 519 | std::wstring favoritesURI = GetFilePathAsURI(GetFullPathFor(L"wvbrowser_ui\\content_ui\\favorites.html")); 520 | std::wstring settingsURI = GetFilePathAsURI(GetFullPathFor(L"wvbrowser_ui\\content_ui\\settings.html")); 521 | std::wstring historyURI = GetFilePathAsURI(GetFullPathFor(L"wvbrowser_ui\\content_ui\\history.html")); 522 | 523 | if (uri.compare(favoritesURI) == 0) 524 | { 525 | jsonObj[L"args"][L"uriToShow"] = web::json::value(L"browser://favorites"); 526 | } 527 | else if (uri.compare(settingsURI) == 0) 528 | { 529 | jsonObj[L"args"][L"uriToShow"] = web::json::value(L"browser://settings"); 530 | } 531 | else if (uri.compare(historyURI) == 0) 532 | { 533 | jsonObj[L"args"][L"uriToShow"] = web::json::value(L"browser://history"); 534 | } 535 | 536 | RETURN_IF_FAILED(PostJsonToWebView(jsonObj, m_controlsWebView.Get())); 537 | 538 | return S_OK; 539 | } 540 | 541 | HRESULT BrowserWindow::HandleTabHistoryUpdate(size_t tabId, ICoreWebView2* webview) 542 | { 543 | wil::unique_cotaskmem_string source; 544 | RETURN_IF_FAILED(webview->get_Source(&source)); 545 | 546 | web::json::value jsonObj = web::json::value::parse(L"{}"); 547 | jsonObj[L"message"] = web::json::value(MG_UPDATE_URI); 548 | jsonObj[L"args"] = web::json::value::parse(L"{}"); 549 | jsonObj[L"args"][L"tabId"] = web::json::value::number(tabId); 550 | jsonObj[L"args"][L"uri"] = web::json::value(source.get()); 551 | 552 | BOOL canGoForward = FALSE; 553 | RETURN_IF_FAILED(webview->get_CanGoForward(&canGoForward)); 554 | jsonObj[L"args"][L"canGoForward"] = web::json::value::boolean(canGoForward); 555 | 556 | BOOL canGoBack = FALSE; 557 | RETURN_IF_FAILED(webview->get_CanGoBack(&canGoBack)); 558 | jsonObj[L"args"][L"canGoBack"] = web::json::value::boolean(canGoBack); 559 | 560 | RETURN_IF_FAILED(PostJsonToWebView(jsonObj, m_controlsWebView.Get())); 561 | 562 | return S_OK; 563 | } 564 | 565 | HRESULT BrowserWindow::HandleTabNavStarting(size_t tabId, ICoreWebView2* webview) 566 | { 567 | web::json::value jsonObj = web::json::value::parse(L"{}"); 568 | jsonObj[L"message"] = web::json::value(MG_NAV_STARTING); 569 | jsonObj[L"args"] = web::json::value::parse(L"{}"); 570 | jsonObj[L"args"][L"tabId"] = web::json::value::number(tabId); 571 | 572 | return PostJsonToWebView(jsonObj, m_controlsWebView.Get()); 573 | } 574 | 575 | HRESULT BrowserWindow::HandleTabNavCompleted(size_t tabId, ICoreWebView2* webview, ICoreWebView2NavigationCompletedEventArgs* args) 576 | { 577 | std::wstring getTitleScript( 578 | // Look for a title tag 579 | L"(() => {" 580 | L" const titleTag = document.getElementsByTagName('title')[0];" 581 | L" if (titleTag) {" 582 | L" return titleTag.innerHTML;" 583 | L" }" 584 | // No title tag, look for the file name 585 | L" pathname = window.location.pathname;" 586 | L" var filename = pathname.split('/').pop();" 587 | L" if (filename) {" 588 | L" return filename;" 589 | L" }" 590 | // No file name, look for the hostname 591 | L" const hostname = window.location.hostname;" 592 | L" if (hostname) {" 593 | L" return hostname;" 594 | L" }" 595 | // Fallback: let the UI use a generic title 596 | L" return '';" 597 | L"})();" 598 | ); 599 | 600 | std::wstring getFaviconURI( 601 | L"(() => {" 602 | // Let the UI use a fallback favicon 603 | L" let faviconURI = '';" 604 | L" let links = document.getElementsByTagName('link');" 605 | // Test each link for a favicon 606 | L" Array.from(links).map(element => {" 607 | L" let rel = element.rel;" 608 | // Favicon is declared, try to get the href 609 | L" if (rel && (rel == 'shortcut icon' || rel == 'icon')) {" 610 | L" if (!element.href) {" 611 | L" return;" 612 | L" }" 613 | // href to icon found, check it's full URI 614 | L" try {" 615 | L" let urlParser = new URL(element.href);" 616 | L" faviconURI = urlParser.href;" 617 | L" } catch(e) {" 618 | // Try prepending origin 619 | L" let origin = window.location.origin;" 620 | L" let faviconLocation = `${origin}/${element.href}`;" 621 | L" try {" 622 | L" urlParser = new URL(faviconLocation);" 623 | L" faviconURI = urlParser.href;" 624 | L" } catch (e2) {" 625 | L" return;" 626 | L" }" 627 | L" }" 628 | L" }" 629 | L" });" 630 | L" return faviconURI;" 631 | L"})();" 632 | ); 633 | 634 | CheckFailure(webview->ExecuteScript(getTitleScript.c_str(), Callback( 635 | [this, tabId](HRESULT error, PCWSTR result) -> HRESULT 636 | { 637 | RETURN_IF_FAILED(error); 638 | 639 | web::json::value jsonObj = web::json::value::parse(L"{}"); 640 | jsonObj[L"message"] = web::json::value(MG_UPDATE_TAB); 641 | jsonObj[L"args"] = web::json::value::parse(L"{}"); 642 | jsonObj[L"args"][L"title"] = web::json::value::parse(result); 643 | jsonObj[L"args"][L"tabId"] = web::json::value::number(tabId); 644 | 645 | CheckFailure(PostJsonToWebView(jsonObj, m_controlsWebView.Get()), L"Can't update title."); 646 | return S_OK; 647 | }).Get()), L"Can't update title."); 648 | 649 | CheckFailure(webview->ExecuteScript(getFaviconURI.c_str(), Callback( 650 | [this, tabId](HRESULT error, PCWSTR result) -> HRESULT 651 | { 652 | RETURN_IF_FAILED(error); 653 | 654 | web::json::value jsonObj = web::json::value::parse(L"{}"); 655 | jsonObj[L"message"] = web::json::value(MG_UPDATE_FAVICON); 656 | jsonObj[L"args"] = web::json::value::parse(L"{}"); 657 | jsonObj[L"args"][L"uri"] = web::json::value::parse(result); 658 | jsonObj[L"args"][L"tabId"] = web::json::value::number(tabId); 659 | 660 | CheckFailure(PostJsonToWebView(jsonObj, m_controlsWebView.Get()), L"Can't update favicon."); 661 | return S_OK; 662 | }).Get()), L"Can't update favicon"); 663 | 664 | web::json::value jsonObj = web::json::value::parse(L"{}"); 665 | jsonObj[L"message"] = web::json::value(MG_NAV_COMPLETED); 666 | jsonObj[L"args"] = web::json::value::parse(L"{}"); 667 | jsonObj[L"args"][L"tabId"] = web::json::value::number(tabId); 668 | 669 | BOOL navigationSucceeded = FALSE; 670 | if (SUCCEEDED(args->get_IsSuccess(&navigationSucceeded))) 671 | { 672 | jsonObj[L"args"][L"isError"] = web::json::value::boolean(!navigationSucceeded); 673 | } 674 | 675 | return PostJsonToWebView(jsonObj, m_controlsWebView.Get()); 676 | } 677 | 678 | HRESULT BrowserWindow::HandleTabSecurityUpdate(size_t tabId, ICoreWebView2* webview, ICoreWebView2DevToolsProtocolEventReceivedEventArgs* args) 679 | { 680 | wil::unique_cotaskmem_string jsonArgs; 681 | RETURN_IF_FAILED(args->get_ParameterObjectAsJson(&jsonArgs)); 682 | web::json::value securityEvent = web::json::value::parse(jsonArgs.get()); 683 | 684 | web::json::value jsonObj = web::json::value::parse(L"{}"); 685 | jsonObj[L"message"] = web::json::value(MG_SECURITY_UPDATE); 686 | jsonObj[L"args"] = web::json::value::parse(L"{}"); 687 | jsonObj[L"args"][L"tabId"] = web::json::value::number(tabId); 688 | jsonObj[L"args"][L"state"] = securityEvent.at(L"securityState"); 689 | 690 | return PostJsonToWebView(jsonObj, m_controlsWebView.Get()); 691 | } 692 | 693 | void BrowserWindow::HandleTabCreated(size_t tabId, bool shouldBeActive) 694 | { 695 | if (shouldBeActive) 696 | { 697 | CheckFailure(SwitchToTab(tabId), L""); 698 | } 699 | } 700 | 701 | HRESULT BrowserWindow::HandleTabMessageReceived(size_t tabId, ICoreWebView2* webview, ICoreWebView2WebMessageReceivedEventArgs* eventArgs) 702 | { 703 | wil::unique_cotaskmem_string jsonString; 704 | RETURN_IF_FAILED(eventArgs->get_WebMessageAsJson(&jsonString)); 705 | web::json::value jsonObj = web::json::value::parse(jsonString.get()); 706 | 707 | wil::unique_cotaskmem_string uri; 708 | RETURN_IF_FAILED(webview->get_Source(&uri)); 709 | 710 | int message = jsonObj.at(L"message").as_integer(); 711 | web::json::value args = jsonObj.at(L"args"); 712 | 713 | wil::unique_cotaskmem_string source; 714 | RETURN_IF_FAILED(webview->get_Source(&source)); 715 | 716 | switch (message) 717 | { 718 | case MG_GET_FAVORITES: 719 | case MG_REMOVE_FAVORITE: 720 | { 721 | std::wstring fileURI = GetFilePathAsURI(GetFullPathFor(L"wvbrowser_ui\\content_ui\\favorites.html")); 722 | // Only the favorites UI can request favorites 723 | if (fileURI.compare(source.get()) == 0) 724 | { 725 | jsonObj[L"args"][L"tabId"] = web::json::value::number(tabId); 726 | CheckFailure(PostJsonToWebView(jsonObj, m_controlsWebView.Get()), L"Couldn't perform favorites operation."); 727 | } 728 | } 729 | break; 730 | case MG_GET_SETTINGS: 731 | { 732 | std::wstring fileURI = GetFilePathAsURI(GetFullPathFor(L"wvbrowser_ui\\content_ui\\settings.html")); 733 | // Only the settings UI can request settings 734 | if (fileURI.compare(source.get()) == 0) 735 | { 736 | jsonObj[L"args"][L"tabId"] = web::json::value::number(tabId); 737 | CheckFailure(PostJsonToWebView(jsonObj, m_controlsWebView.Get()), L"Couldn't retrieve settings."); 738 | } 739 | } 740 | break; 741 | case MG_CLEAR_CACHE: 742 | { 743 | std::wstring fileURI = GetFilePathAsURI(GetFullPathFor(L"wvbrowser_ui\\content_ui\\settings.html")); 744 | // Only the settings UI can request cache clearing 745 | if (fileURI.compare(uri.get()) == 0) 746 | { 747 | jsonObj[L"args"][L"content"] = web::json::value::boolean(false); 748 | jsonObj[L"args"][L"controls"] = web::json::value::boolean(false); 749 | 750 | if (SUCCEEDED(ClearContentCache())) 751 | { 752 | jsonObj[L"args"][L"content"] = web::json::value::boolean(true); 753 | } 754 | 755 | if (SUCCEEDED(ClearControlsCache())) 756 | { 757 | jsonObj[L"args"][L"controls"] = web::json::value::boolean(true); 758 | } 759 | 760 | CheckFailure(PostJsonToWebView(jsonObj, m_tabs.at(tabId)->m_contentWebView.Get()), L""); 761 | } 762 | } 763 | break; 764 | case MG_CLEAR_COOKIES: 765 | { 766 | std::wstring fileURI = GetFilePathAsURI(GetFullPathFor(L"wvbrowser_ui\\content_ui\\settings.html")); 767 | // Only the settings UI can request cookies clearing 768 | if (fileURI.compare(uri.get()) == 0) 769 | { 770 | jsonObj[L"args"][L"content"] = web::json::value::boolean(false); 771 | jsonObj[L"args"][L"controls"] = web::json::value::boolean(false); 772 | 773 | if (SUCCEEDED(ClearContentCookies())) 774 | { 775 | jsonObj[L"args"][L"content"] = web::json::value::boolean(true); 776 | } 777 | 778 | 779 | if (SUCCEEDED(ClearControlsCookies())) 780 | { 781 | jsonObj[L"args"][L"controls"] = web::json::value::boolean(true); 782 | } 783 | 784 | CheckFailure(PostJsonToWebView(jsonObj, m_tabs.at(tabId)->m_contentWebView.Get()), L""); 785 | } 786 | } 787 | break; 788 | case MG_GET_HISTORY: 789 | case MG_REMOVE_HISTORY_ITEM: 790 | case MG_CLEAR_HISTORY: 791 | { 792 | std::wstring fileURI = GetFilePathAsURI(GetFullPathFor(L"wvbrowser_ui\\content_ui\\history.html")); 793 | // Only the history UI can request history 794 | if (fileURI.compare(uri.get()) == 0) 795 | { 796 | jsonObj[L"args"][L"tabId"] = web::json::value::number(tabId); 797 | CheckFailure(PostJsonToWebView(jsonObj, m_controlsWebView.Get()), L"Couldn't perform history operation"); 798 | } 799 | } 800 | break; 801 | default: 802 | { 803 | OutputDebugString(L"Unexpected message\n"); 804 | } 805 | break; 806 | } 807 | 808 | return S_OK; 809 | } 810 | 811 | HRESULT BrowserWindow::ClearContentCache() 812 | { 813 | return m_tabs.at(m_activeTabId)->m_contentWebView->CallDevToolsProtocolMethod(L"Network.clearBrowserCache", L"{}", nullptr); 814 | } 815 | 816 | HRESULT BrowserWindow::ClearControlsCache() 817 | { 818 | return m_controlsWebView->CallDevToolsProtocolMethod(L"Network.clearBrowserCache", L"{}", nullptr); 819 | } 820 | 821 | HRESULT BrowserWindow::ClearContentCookies() 822 | { 823 | return m_tabs.at(m_activeTabId)->m_contentWebView->CallDevToolsProtocolMethod(L"Network.clearBrowserCookies", L"{}", nullptr); 824 | } 825 | 826 | HRESULT BrowserWindow::ClearControlsCookies() 827 | { 828 | return m_controlsWebView->CallDevToolsProtocolMethod(L"Network.clearBrowserCookies", L"{}", nullptr); 829 | } 830 | 831 | HRESULT BrowserWindow::ResizeUIWebViews() 832 | { 833 | if (m_controlsWebView != nullptr) 834 | { 835 | RECT bounds; 836 | GetClientRect(m_hWnd, &bounds); 837 | bounds.bottom = bounds.top + GetDPIAwareBound(c_uiBarHeight); 838 | bounds.bottom += 1; 839 | 840 | RETURN_IF_FAILED(m_controlsController->put_Bounds(bounds)); 841 | } 842 | 843 | if (m_optionsWebView != nullptr) 844 | { 845 | RECT bounds; 846 | GetClientRect(m_hWnd, &bounds); 847 | bounds.top = GetDPIAwareBound(c_uiBarHeight); 848 | bounds.bottom = bounds.top + GetDPIAwareBound(c_optionsDropdownHeight); 849 | bounds.left = bounds.right - GetDPIAwareBound(c_optionsDropdownWidth); 850 | 851 | RETURN_IF_FAILED(m_optionsController->put_Bounds(bounds)); 852 | } 853 | 854 | // Workaround for black controls WebView issue in Windows 7 855 | HWND wvWindow = GetWindow(m_hWnd, GW_CHILD); 856 | while (wvWindow != nullptr) 857 | { 858 | UpdateWindow(wvWindow); 859 | wvWindow = GetWindow(wvWindow, GW_HWNDNEXT); 860 | } 861 | 862 | return S_OK; 863 | } 864 | 865 | void BrowserWindow::UpdateMinWindowSize() 866 | { 867 | RECT clientRect; 868 | RECT windowRect; 869 | 870 | GetClientRect(m_hWnd, &clientRect); 871 | GetWindowRect(m_hWnd, &windowRect); 872 | 873 | int bordersWidth = (windowRect.right - windowRect.left) - clientRect.right; 874 | int bordersHeight = (windowRect.bottom - windowRect.top) - clientRect.bottom; 875 | 876 | m_minWindowWidth = GetDPIAwareBound(MIN_WINDOW_WIDTH) + bordersWidth; 877 | m_minWindowHeight = GetDPIAwareBound(MIN_WINDOW_HEIGHT) + bordersHeight; 878 | } 879 | 880 | void BrowserWindow::CheckFailure(HRESULT hr, LPCWSTR errorMessage) 881 | { 882 | if (FAILED(hr)) 883 | { 884 | std::wstring message; 885 | if (!errorMessage || !errorMessage[0]) 886 | { 887 | message = std::wstring(L"Something went wrong."); 888 | } 889 | else 890 | { 891 | message = std::wstring(errorMessage); 892 | } 893 | 894 | MessageBoxW(nullptr, message.c_str(), nullptr, MB_OK); 895 | } 896 | } 897 | 898 | int BrowserWindow::GetDPIAwareBound(int bound) 899 | { 900 | // Remove the GetDpiForWindow call when using Windows 7 or any version 901 | // below 1607 (Windows 10). You will also have to make sure the build 902 | // directory is clean before building again. 903 | return (bound * GetDpiForWindow(m_hWnd) / DEFAULT_DPI); 904 | } 905 | 906 | std::wstring BrowserWindow::GetAppDataDirectory() 907 | { 908 | TCHAR path[MAX_PATH]; 909 | std::wstring dataDirectory; 910 | HRESULT hr = SHGetFolderPath(nullptr, CSIDL_APPDATA, NULL, 0, path); 911 | if (SUCCEEDED(hr)) 912 | { 913 | dataDirectory = std::wstring(path); 914 | dataDirectory.append(L"\\Microsoft\\"); 915 | } 916 | else 917 | { 918 | dataDirectory = std::wstring(L".\\"); 919 | } 920 | 921 | dataDirectory.append(s_title); 922 | return dataDirectory; 923 | } 924 | 925 | std::wstring BrowserWindow::GetFullPathFor(LPCWSTR relativePath) 926 | { 927 | WCHAR path[MAX_PATH]; 928 | GetModuleFileNameW(m_hInst, path, MAX_PATH); 929 | std::wstring pathName(path); 930 | 931 | std::size_t index = pathName.find_last_of(L"\\") + 1; 932 | pathName.replace(index, pathName.length(), relativePath); 933 | 934 | return pathName; 935 | } 936 | 937 | std::wstring BrowserWindow::GetFilePathAsURI(std::wstring fullPath) 938 | { 939 | std::wstring fileURI; 940 | ComPtr uri; 941 | DWORD uriFlags = Uri_CREATE_ALLOW_IMPLICIT_FILE_SCHEME; 942 | HRESULT hr = CreateUri(fullPath.c_str(), uriFlags, 0, &uri); 943 | 944 | if (SUCCEEDED(hr)) 945 | { 946 | wil::unique_bstr absoluteUri; 947 | uri->GetAbsoluteUri(&absoluteUri); 948 | fileURI = std::wstring(absoluteUri.get()); 949 | } 950 | 951 | return fileURI; 952 | } 953 | 954 | HRESULT BrowserWindow::PostJsonToWebView(web::json::value jsonObj, ICoreWebView2* webview) 955 | { 956 | utility::stringstream_t stream; 957 | jsonObj.serialize(stream); 958 | 959 | return webview->PostWebMessageAsJson(stream.str().c_str()); 960 | } 961 | -------------------------------------------------------------------------------- /BrowserWindow.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #pragma once 6 | 7 | #include "framework.h" 8 | #include "Tab.h" 9 | 10 | class BrowserWindow 11 | { 12 | public: 13 | static const int c_uiBarHeight = 70; 14 | static const int c_optionsDropdownHeight = 108; 15 | static const int c_optionsDropdownWidth = 200; 16 | 17 | static ATOM RegisterClass(_In_ HINSTANCE hInstance); 18 | static LRESULT CALLBACK WndProcStatic(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); 19 | LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); 20 | 21 | static BOOL LaunchWindow(_In_ HINSTANCE hInstance, _In_ int nCmdShow); 22 | static std::wstring GetAppDataDirectory(); 23 | std::wstring GetFullPathFor(LPCWSTR relativePath); 24 | HRESULT HandleTabURIUpdate(size_t tabId, ICoreWebView2* webview); 25 | HRESULT HandleTabHistoryUpdate(size_t tabId, ICoreWebView2* webview); 26 | HRESULT HandleTabNavStarting(size_t tabId, ICoreWebView2* webview); 27 | HRESULT HandleTabNavCompleted(size_t tabId, ICoreWebView2* webview, ICoreWebView2NavigationCompletedEventArgs* args); 28 | HRESULT HandleTabSecurityUpdate(size_t tabId, ICoreWebView2* webview, ICoreWebView2DevToolsProtocolEventReceivedEventArgs* args); 29 | void HandleTabCreated(size_t tabId, bool shouldBeActive); 30 | HRESULT HandleTabMessageReceived(size_t tabId, ICoreWebView2* webview, ICoreWebView2WebMessageReceivedEventArgs* eventArgs); 31 | int GetDPIAwareBound(int bound); 32 | static void CheckFailure(HRESULT hr, LPCWSTR errorMessage); 33 | protected: 34 | HINSTANCE m_hInst = nullptr; // Current app instance 35 | HWND m_hWnd = nullptr; 36 | 37 | static WCHAR s_windowClass[MAX_LOADSTRING]; // The window class name 38 | static WCHAR s_title[MAX_LOADSTRING]; // The title bar text 39 | 40 | int m_minWindowWidth = 0; 41 | int m_minWindowHeight = 0; 42 | 43 | Microsoft::WRL::ComPtr m_uiEnv; 44 | Microsoft::WRL::ComPtr m_contentEnv; 45 | Microsoft::WRL::ComPtr m_controlsController; 46 | Microsoft::WRL::ComPtr m_optionsController; 47 | Microsoft::WRL::ComPtr m_controlsWebView; 48 | Microsoft::WRL::ComPtr m_optionsWebView; 49 | std::map> m_tabs; 50 | size_t m_activeTabId = 0; 51 | 52 | EventRegistrationToken m_controlsUIMessageBrokerToken = {}; // Token for the UI message handler in controls WebView 53 | EventRegistrationToken m_controlsZoomToken = {}; 54 | EventRegistrationToken m_optionsUIMessageBrokerToken = {}; // Token for the UI message handler in options WebView 55 | EventRegistrationToken m_optionsZoomToken = {}; 56 | EventRegistrationToken m_lostOptionsFocus = {}; // Token for the lost focus handler in options WebView 57 | Microsoft::WRL::ComPtr m_uiMessageBroker; 58 | 59 | BOOL InitInstance(HINSTANCE hInstance, int nCmdShow); 60 | HRESULT InitUIWebViews(); 61 | HRESULT CreateBrowserControlsWebView(); 62 | HRESULT CreateBrowserOptionsWebView(); 63 | HRESULT ClearContentCache(); 64 | HRESULT ClearControlsCache(); 65 | HRESULT ClearContentCookies(); 66 | HRESULT ClearControlsCookies(); 67 | 68 | void SetUIMessageBroker(); 69 | HRESULT ResizeUIWebViews(); 70 | void UpdateMinWindowSize(); 71 | HRESULT PostJsonToWebView(web::json::value jsonObj, ICoreWebView2* webview); 72 | HRESULT SwitchToTab(size_t tabId); 73 | std::wstring GetFilePathAsURI(std::wstring fullPath); 74 | }; 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // * Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // * Redistributions in binary form must reproduce the above 10 | // copyright notice, this list of conditions and the following disclaimer 11 | // in the documentation and/or other materials provided with the 12 | // distribution. 13 | // * The name of Google Inc, Microsoft Corporation, or the names of its 14 | // contributors may not be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "A web browser built with the Microsoft Edge WebView2 control." 3 | extendedZipContent: 4 | - 5 | path: LICENSE 6 | target: LICENSE 7 | languages: 8 | - cpp 9 | - javascript 10 | page_type: sample 11 | products: 12 | - microsoft-edge 13 | urlFragment: webview2browser 14 | --- 15 | # WebView2Browser 16 | 17 | A web browser built with the [Microsoft Edge WebView2](https://aka.ms/webview2) control. 18 | 19 | ![WebView2Browser](https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/master/screenshots/WebView2Browser.png) 20 | 21 | WebView2Browser is a sample Windows desktop application demonstrating the WebView2 control capabilities. It is built as a Win32 [Visual Studio 2019](https://visualstudio.microsoft.com/vs/) project and makes use of both C++ and JavaScript in the WebView2 environment to power its features. 22 | 23 | WebView2Browser shows some of the simplest uses of WebView2 -such as creating and navigating a WebView, but also some more complex workflows like using the [PostWebMessageAsJson API](https://learn.microsoft.com/microsoft-edge/webview2/reference/win32/icorewebview2#postwebmessageasjson) to communicate WebViews in separate environments. It is intended as a rich code sample to look at how you can use WebView2 APIs to build your own app. 24 | 25 | For more information, see the article in the Microsoft Edge Developer documentation: [WebView2Browser](https://learn.microsoft.com/microsoft-edge/webview2/samples/webview2browser). 26 | 27 | ## Requisites 28 | 29 | * [Microsoft Edge (Chromium)](https://www.microsoftedgeinsider.com/download/) installed on a supported OS. 30 | * [Visual Studio](https://visualstudio.microsoft.com/vs/) with C++ support installed. 31 | 32 | The [WebView2 Runtime](https://developer.microsoft.com/microsoft-edge/webview2/) is recommended for the installation and the minimum version is 86.0.622.38. 33 | 34 | ## Build the browser 35 | 36 | Clone the repository and open the solution in Visual Studio. WebView2 is already included as a NuGet package* in the project! 37 | 38 | * Clone this repository 39 | * Open the solution in Visual Studio 2019** 40 | * Make the changes listed below if you're using a Windows version below Windows 10. 41 | * Set the target you want to build (Debug/Release, x86/x64) 42 | * Build the solution 43 | 44 | That's it. Everything should be ready to just launch the app. 45 | 46 | *You can get the WebView2 NuGet Package through the Visual Studio NuGet Package Manager. 47 | **You can also use Visual Studio 2017 by changing the project's Platform Toolset in Project Properties/Configuration properties/General/Platform Toolset. You might also need to change the Windows SDK to the latest version available to you. 48 | 49 | ## Using versions below Windows 10 50 | 51 | There's a couple of changes you need to make if you want to build and run the browser in other versions of Windows. This is because of how DPI is handled in Windows 10 vs previous versions of Windows. 52 | 53 | In `WebViewBrowserApp.cpp`, you will need to replace the call to `SetProcessDpiAwarenessContext` and call `SetProcessDPIAware` instead. 54 | 55 | ```cpp 56 | int APIENTRY wWinMain(_In_ HINSTANCE hInstance, 57 | _In_opt_ HINSTANCE hPrevInstance, 58 | _In_ LPWSTR lpCmdLine, 59 | _In_ int nCmdShow) 60 | { 61 | UNREFERENCED_PARAMETER(hPrevInstance); 62 | UNREFERENCED_PARAMETER(lpCmdLine); 63 | 64 | // Call SetProcessDPIAware() instead when using Windows 7 or any version 65 | // below 1703 (Windows 10). 66 | SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); 67 | 68 | BrowserWindow::RegisterClass(hInstance); 69 | 70 | // ... 71 | ``` 72 | 73 | In `BrowserWindow.cpp`, you will need to remove the call to `GetDpiForWindow`. 74 | 75 | ```cpp 76 | int BrowserWindow::GetDPIAwareBound(int bound) 77 | { 78 | // Remove the GetDpiForWindow call when using Windows 7 or any version 79 | // below 1607 (Windows 10). You will also have to make sure the build 80 | // directory is clean before building again. 81 | return (bound * GetDpiForWindow(m_hWnd) / DEFAULT_DPI); 82 | } 83 | ``` 84 | 85 | ## Browser layout 86 | 87 | WebView2Browser has a multi-WebView approach to integrate web content and application UI into a Windows Desktop application. This allows the browser to use standard web technologies (HTML, CSS, JavaScript) to light up the interface but also enables the app to fetch favicons from the web and use IndexedDB for storing favorites and history. 88 | 89 | The multi-WebView approach involves using two separate WebView environments (each with its own user data directory): one for the UI WebViews and the other for all content WebViews. UI WebViews (controls and options dropdown) use the UI environment while web content WebViews (one per tab) use the content environment. 90 | 91 | ![Browser layout](https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/master/screenshots/layout.png) 92 | 93 | ## Features 94 | 95 | WebView2Browser provides all the functionalities to make a basic web browser, but there's plenty of room for you to play around. 96 | 97 | * Go back/forward 98 | * Reload page 99 | * Cancel navigation 100 | * Multiple tabs 101 | * History 102 | * Favorites 103 | * Search from the address bar 104 | * Page security status 105 | * Clearing cache and cookies 106 | 107 | ## WebView2 APIs 108 | 109 | WebView2Browser makes use of a handful of the APIs available in WebView2. For the APIs not used here, you can find more about them in the [Microsoft Edge WebView2 Reference](https://learn.microsoft.com/microsoft-edge/webview2/reference/win32). The following is a list of the most interesting APIs WebView2Browser uses and the feature(s) they enable. 110 | 111 | API | Feature(s) 112 | :--- | :--- 113 | CreateCoreWebView2EnvironmentWithOptions | Used to create the environments for UI and content WebViews. Different user data directories are passed to isolate UI from web content. | 114 | ICoreWebView2 | There are several WebViews in WebView2Browser and most features make use of members in this interface, the table below shows how they're used. 115 | ICoreWebView2DevToolsProtocolEventReceivedEventHandler | Used along with add_DevToolsProtocolEventReceived to listen for CDP security events to update the lock icon in the browser UI. | 116 | ICoreWebView2DevToolsProtocolEventReceiver | Used along with add_DevToolsProtocolEventReceived to listen for CDP security events to update the lock icon in the browser UI. | 117 | ICoreWebView2ExecuteScriptCompletedHandler | Used along with ExecuteScript to get the title and favicon from the visited page. | 118 | ICoreWebView2FocusChangedEventHandler | Used along with add_LostFocus to hide the browser options dropdown when it loses focus. 119 | ICoreWebView2HistoryChangedEventHandler | Used along with add_HistoryChanged to update the navigation buttons in the browser UI. | 120 | ICoreWebView2Controller | There are several WebViewControllers in WebView2Browser and we fetch the associated WebViews from them. 121 | ICoreWebView2NavigationCompletedEventHandler | Used along with add_NavigationCompleted to update the reload button in the browser UI. 122 | ICoreWebView2Settings | Used to disable DevTools in the browser UI. 123 | ICoreWebView2SourceChangedEventHandler | Used along with add_SourceChanged to update the address bar in the browser UI. | 124 | ICoreWebView2WebMessageReceivedEventHandler | This is one of the most important APIs to WebView2Browser. Most functionalities involving communication across WebViews use this. 125 | 126 | ICoreWebView2 API | Feature(s) 127 | :--- | :--- 128 | add_NavigationStarting | Used to display the cancel navigation button in the controls WebView. 129 | add_SourceChanged | Used to update the address bar. 130 | add_HistoryChanged | Used to update go back/forward buttons. 131 | add_NavigationCompleted | Used to display the reload button once a navigation completes. 132 | ExecuteScript | Used to get the title and favicon of a visited page. 133 | PostWebMessageAsJson | Used to communicate WebViews. All messages use JSON to pass parameters needed. 134 | add_WebMessageReceived | Used to handle web messages posted to the WebView. 135 | CallDevToolsProtocolMethod | Used to enable listening for security events, which will notify of security status changes in a document. 136 | 137 | ICoreWebView2Controller API | Feature(s) 138 | :--- | :--- 139 | get_CoreWebView2 | Used to get the CoreWebView2 associated with this CoreWebView2Controller. 140 | add_LostFocus | Used to hide the options dropdown when the user clicks away of it. 141 | 142 | ## Implementing the features 143 | 144 | The sections below describe how some of the features in WebView2Browser were implemented. You can look at the source code for more details about how everything works here. 145 | 146 | * [The basics](#the-basics) 147 | * [Set up the environment, create a WebView](#set-up-the-environment-create-a-webview) 148 | * [Navigate to web page](#navigate-to-web-page) 149 | * [Updating the address bar](#updating-the-address-bar) 150 | * [Going back, going forward](#going-back-going-forward) 151 | * [Some interesting features](#some-interesting-features) 152 | * [Communicating the WebViews](#communicating-the-webviews) 153 | * [Tab handling](#tab-handling) 154 | * [Updating the security icon](#updating-the-security-icon) 155 | * [Populating the history](#populating-the-history) 156 | * [Handling JSON and URIs](#handling-json-and-uris) 157 | 158 | ## The basics 159 | 160 | ### Set up the environment, create a WebView 161 | 162 | WebView2 allows you to host web content in your Windows app. It exposes the globals [CreateCoreWebView2Environment](https://learn.microsoft.com/microsoft-edge/webview2/reference/win32/webview2-idl#createcorewebview2environment) and [CreateCoreWebView2EnvironmentWithOptions](https://learn.microsoft.com/microsoft-edge/webview2/reference/win32/webview2-idl#createcorewebview2environmentwithoptions) from which we can create the two separate environments for the browser's UI and content. 163 | 164 | ```cpp 165 | // Get directory for user data. This will be kept separated from the 166 | // directory for the browser UI data. 167 | std::wstring userDataDirectory = GetAppDataDirectory(); 168 | userDataDirectory.append(L"\\User Data"); 169 | 170 | // Create WebView environment for web content requested by the user. All 171 | // tabs will be created from this environment and kept isolated from the 172 | // browser UI. This environment is created first so the UI can request new 173 | // tabs when it's ready. 174 | HRESULT hr = CreateCoreWebView2EnvironmentWithOptions(nullptr, userDataDirectory.c_str(), 175 | L"", Callback( 176 | [this](HRESULT result, ICoreWebView2Environment* env) -> HRESULT 177 | { 178 | RETURN_IF_FAILED(result); 179 | 180 | m_contentEnv = env; 181 | HRESULT hr = InitUIWebViews(); 182 | 183 | if (!SUCCEEDED(hr)) 184 | { 185 | OutputDebugString(L"UI WebViews environment creation failed\n"); 186 | } 187 | 188 | return hr; 189 | }).Get()); 190 | ``` 191 | 192 | ```cpp 193 | HRESULT BrowserWindow::InitUIWebViews() 194 | { 195 | // Get data directory for browser UI data 196 | std::wstring browserDataDirectory = GetAppDataDirectory(); 197 | browserDataDirectory.append(L"\\Browser Data"); 198 | 199 | // Create WebView environment for browser UI. A separate data directory is 200 | // used to isolate the browser UI from web content requested by the user. 201 | return CreateCoreWebView2EnvironmentWithOptions(nullptr, browserDataDirectory.c_str(), 202 | L"", Callback( 203 | [this](HRESULT result, ICoreWebView2Environment* env) -> HRESULT 204 | { 205 | // Environment is ready, create the WebView 206 | m_uiEnv = env; 207 | 208 | RETURN_IF_FAILED(CreateBrowserControlsWebView()); 209 | RETURN_IF_FAILED(CreateBrowserOptionsWebView()); 210 | 211 | return S_OK; 212 | }).Get()); 213 | } 214 | ``` 215 | 216 | We use the [ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler](https://learn.microsoft.com/microsoft-edge/webview2/reference/win32/icorewebview2createcorewebview2environmentcompletedhandler) to create the UI WebViews once the environment is ready. 217 | 218 | ```cpp 219 | HRESULT BrowserWindow::CreateBrowserControlsWebView() 220 | { 221 | return m_uiEnv->CreateCoreWebView2Controller(m_hWnd, Callback( 222 | [this](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT 223 | { 224 | if (!SUCCEEDED(result)) 225 | { 226 | OutputDebugString(L"Controls WebView creation failed\n"); 227 | return result; 228 | } 229 | // WebView created 230 | m_controlsController = controller; 231 | CheckFailure(m_controlsController->get_CoreWebView2(&m_controlsWebView), L""); 232 | 233 | wil::com_ptr settings; 234 | RETURN_IF_FAILED(m_controlsWebView->get_Settings(&settings)); 235 | RETURN_IF_FAILED(settings->put_AreDevToolsEnabled(FALSE)); 236 | 237 | RETURN_IF_FAILED(m_controlsController->add_ZoomFactorChanged(Callback( 238 | [](ICoreWebView2Controller* controller, IUnknown* args) -> HRESULT 239 | { 240 | controller->put_ZoomFactor(1.0); 241 | return S_OK; 242 | } 243 | ).Get(), &m_controlsZoomToken)); 244 | 245 | RETURN_IF_FAILED(m_controlsWebView->add_WebMessageReceived(m_uiMessageBroker.Get(), &m_controlsUIMessageBrokerToken)); 246 | RETURN_IF_FAILED(ResizeUIWebViews()); 247 | 248 | std::wstring controlsPath = GetFullPathFor(L"wvbrowser_ui\\controls_ui\\default.html"); 249 | RETURN_IF_FAILED(m_controlsWebView->Navigate(controlsPath.c_str())); 250 | 251 | return S_OK; 252 | }).Get()); 253 | } 254 | ``` 255 | 256 | We're setting up a few things here. The [ICoreWebView2Settings](https://learn.microsoft.com/microsoft-edge/webview2/reference/win32/icorewebview2settings) interface is used to disable DevTools in the WebView powering the browser controls. We're also adding a handler for received web messages. This handler will enable us to do something when the user interacts with the controls in this WebView. 257 | 258 | ### Navigate to web page 259 | 260 | You can navigate to a web page by entering its URI in the address bar. When pressing Enter, the controls WebView will post a web message to the host app so it can navigate the active tab to the specified location. Code below shows how the host Win32 application will handle that message. 261 | 262 | ```cpp 263 | case MG_NAVIGATE: 264 | { 265 | std::wstring uri(args.at(L"uri").as_string()); 266 | std::wstring browserScheme(L"browser://"); 267 | 268 | if (uri.substr(0, browserScheme.size()).compare(browserScheme) == 0) 269 | { 270 | // No encoded search URI 271 | std::wstring path = uri.substr(browserScheme.size()); 272 | if (path.compare(L"favorites") == 0 || 273 | path.compare(L"settings") == 0 || 274 | path.compare(L"history") == 0) 275 | { 276 | std::wstring filePath(L"wvbrowser_ui\\content_ui\\"); 277 | filePath.append(path); 278 | filePath.append(L".html"); 279 | std::wstring fullPath = GetFullPathFor(filePath.c_str()); 280 | CheckFailure(m_tabs.at(m_activeTabId)->m_contentWebView->Navigate(fullPath.c_str()), L"Can't navigate to browser page."); 281 | } 282 | else 283 | { 284 | OutputDebugString(L"Requested unknown browser page\n"); 285 | } 286 | } 287 | else if (!SUCCEEDED(m_tabs.at(m_activeTabId)->m_contentWebView->Navigate(uri.c_str()))) 288 | { 289 | CheckFailure(m_tabs.at(m_activeTabId)->m_contentWebView->Navigate(args.at(L"encodedSearchURI").as_string().c_str()), L"Can't navigate to requested page."); 290 | } 291 | } 292 | break; 293 | ``` 294 | 295 | WebView2Browser will check the URI against browser pages (i.e. favorites, settings, history) and navigate to the requested location or use the provided URI to search Bing as a fallback. 296 | 297 | ### Updating the address bar 298 | 299 | The address bar is updated every time there is a change in the active tab's document source and along with other controls when switching tabs. Each WebView will fire an event when the state of the document changes, we can use this event to get the new source on updates and forward the change to the controls WebView (we'll also update the go back and go forward buttons). 300 | 301 | ```cpp 302 | // Register event handler for doc state change 303 | RETURN_IF_FAILED(m_contentWebView->add_SourceChanged(Callback( 304 | [this, browserWindow](ICoreWebView2* webview, ICoreWebView2SourceChangedEventArgs* args) -> HRESULT 305 | { 306 | BrowserWindow::CheckFailure(browserWindow->HandleTabURIUpdate(m_tabId, webview), L"Can't update address bar"); 307 | 308 | return S_OK; 309 | }).Get(), &m_uriUpdateForwarderToken)); 310 | ``` 311 | 312 | ```cpp 313 | HRESULT BrowserWindow::HandleTabURIUpdate(size_t tabId, ICoreWebView2* webview) 314 | { 315 | wil::unique_cotaskmem_string source; 316 | RETURN_IF_FAILED(webview->get_Source(&source)); 317 | 318 | web::json::value jsonObj = web::json::value::parse(L"{}"); 319 | jsonObj[L"message"] = web::json::value(MG_UPDATE_URI); 320 | jsonObj[L"args"] = web::json::value::parse(L"{}"); 321 | jsonObj[L"args"][L"tabId"] = web::json::value::number(tabId); 322 | jsonObj[L"args"][L"uri"] = web::json::value(source.get()); 323 | 324 | // ... 325 | 326 | RETURN_IF_FAILED(PostJsonToWebView(jsonObj, m_controlsWebView.Get())); 327 | 328 | return S_OK; 329 | } 330 | 331 | HRESULT BrowserWindow::HandleTabHistoryUpdate(size_t tabId, ICoreWebView2* webview) 332 | { 333 | // ... 334 | 335 | BOOL canGoForward = FALSE; 336 | RETURN_IF_FAILED(webview->get_CanGoForward(&canGoForward)); 337 | jsonObj[L"args"][L"canGoForward"] = web::json::value::boolean(canGoForward); 338 | 339 | BOOL canGoBack = FALSE; 340 | RETURN_IF_FAILED(webview->get_CanGoBack(&canGoBack)); 341 | jsonObj[L"args"][L"canGoBack"] = web::json::value::boolean(canGoBack); 342 | 343 | RETURN_IF_FAILED(PostJsonToWebView(jsonObj, m_controlsWebView.Get())); 344 | 345 | return S_OK; 346 | } 347 | ``` 348 | 349 | We have sent the `MG_UPDATE_URI` message along with the URI to the controls WebView. Now we want to reflect those changes on the tab state and update the UI if necessary. 350 | 351 | ```javascript 352 | case commands.MG_UPDATE_URI: 353 | if (isValidTabId(args.tabId)) { 354 | const tab = tabs.get(args.tabId); 355 | let previousURI = tab.uri; 356 | 357 | // Update the tab state 358 | tab.uri = args.uri; 359 | tab.uriToShow = args.uriToShow; 360 | tab.canGoBack = args.canGoBack; 361 | tab.canGoForward = args.canGoForward; 362 | 363 | // If the tab is active, update the controls UI 364 | if (args.tabId == activeTabId) { 365 | updateNavigationUI(message); 366 | } 367 | 368 | // ... 369 | } 370 | break; 371 | ``` 372 | 373 | ### Going back, going forward 374 | 375 | Each WebView will keep a history for the navigations it has performed so we only need to connect the browser UI with the corresponding methods. If the active tab's WebView can be navigated back/forward, the buttons will post a web message to the host application when clicked. 376 | 377 | The JavaScript side: 378 | 379 | ```javascript 380 | document.querySelector('#btn-forward').addEventListener('click', function(e) { 381 | if (document.getElementById('btn-forward').className === 'btn') { 382 | var message = { 383 | message: commands.MG_GO_FORWARD, 384 | args: {} 385 | }; 386 | window.chrome.webview.postMessage(message); 387 | } 388 | }); 389 | 390 | document.querySelector('#btn-back').addEventListener('click', function(e) { 391 | if (document.getElementById('btn-back').className === 'btn') { 392 | var message = { 393 | message: commands.MG_GO_BACK, 394 | args: {} 395 | }; 396 | window.chrome.webview.postMessage(message); 397 | } 398 | }); 399 | ``` 400 | 401 | The host application side: 402 | 403 | ```cpp 404 | case MG_GO_FORWARD: 405 | { 406 | CheckFailure(m_tabs.at(m_activeTabId)->m_contentWebView->GoForward(), L""); 407 | } 408 | break; 409 | case MG_GO_BACK: 410 | { 411 | CheckFailure(m_tabs.at(m_activeTabId)->m_contentWebView->GoBack(), L""); 412 | } 413 | break; 414 | ``` 415 | 416 | ### Reloading, stop navigation 417 | 418 | We use the `NavigationStarting` event fired by a content WebView to update its associated tab loading state in the controls WebView. Similarly, when a WebView fires the `NavigationCompleted` event, we use that event to instruct the controls WebView to update the tab state. The active tab state in the controls WebView will determine whether to show the reload or the cancel button. Each of those will post a message back to the host application when clicked, so that the WebView for that tab can be reloaded or have its navigation canceled, accordingly. 419 | 420 | ```javascript 421 | function reloadActiveTabContent() { 422 | var message = { 423 | message: commands.MG_RELOAD, 424 | args: {} 425 | }; 426 | window.chrome.webview.postMessage(message); 427 | } 428 | 429 | // ... 430 | 431 | document.querySelector('#btn-reload').addEventListener('click', function(e) { 432 | var btnReload = document.getElementById('btn-reload'); 433 | if (btnReload.className === 'btn-cancel') { 434 | var message = { 435 | message: commands.MG_CANCEL, 436 | args: {} 437 | }; 438 | window.chrome.webview.postMessage(message); 439 | } else if (btnReload.className === 'btn') { 440 | reloadActiveTabContent(); 441 | } 442 | }); 443 | ``` 444 | 445 | ```cpp 446 | case MG_RELOAD: 447 | { 448 | CheckFailure(m_tabs.at(m_activeTabId)->m_contentWebView->Reload(), L""); 449 | } 450 | break; 451 | case MG_CANCEL: 452 | { 453 | CheckFailure(m_tabs.at(m_activeTabId)->m_contentWebView->CallDevToolsProtocolMethod(L"Page.stopLoading", L"{}", nullptr), L""); 454 | } 455 | ``` 456 | 457 | ## Some interesting features 458 | 459 | ### Communicating the WebViews 460 | 461 | We need to communicate the WebViews powering tabs and UI so that user interactions in one have the desired effect in the other. WebView2Browser makes use of set of very useful WebView2 APIs for this purpose, including [PostWebMessageAsJson](https://learn.microsoft.com/microsoft-edge/webview2/reference/win32/icorewebview2#postwebmessageasjson), [add_WebMessageReceived](https://learn.microsoft.com/microsoft-edge/webview2/reference/win32/icorewebview2#add_webmessagereceived) and [ICoreWebView2WebMessageReceivedEventHandler](https://learn.microsoft.com/microsoft-edge/webview2/reference/win32/icorewebview2webmessagereceivedeventhandler). On the JavaScript side, we're making use of the `window.chrome.webview` object exposed to call the `postMessage` method and add an event lister for received messages. 462 | 463 | ```cpp 464 | HRESULT BrowserWindow::CreateBrowserControlsWebView() 465 | { 466 | return m_uiEnv->CreateCoreWebView2Controller(m_hWnd, Callback( 467 | [this](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT 468 | { 469 | // ... 470 | 471 | RETURN_IF_FAILED(m_controlsWebView->add_WebMessageReceived(m_uiMessageBroker.Get(), &m_controlsUIMessageBrokerToken)); 472 | 473 | // ... 474 | 475 | return S_OK; 476 | }).Get()); 477 | } 478 | ``` 479 | 480 | ```cpp 481 | HRESULT BrowserWindow::PostJsonToWebView(web::json::value jsonObj, ICoreWebView2* webview) 482 | { 483 | utility::stringstream_t stream; 484 | jsonObj.serialize(stream); 485 | 486 | return webview->PostWebMessageAsJson(stream.str().c_str()); 487 | } 488 | 489 | // ... 490 | 491 | HRESULT BrowserWindow::HandleTabNavStarting(size_t tabId, ICoreWebView2* webview) 492 | { 493 | web::json::value jsonObj = web::json::value::parse(L"{}"); 494 | jsonObj[L"message"] = web::json::value(MG_NAV_STARTING); 495 | jsonObj[L"args"] = web::json::value::parse(L"{}"); 496 | jsonObj[L"args"][L"tabId"] = web::json::value::number(tabId); 497 | 498 | return PostJsonToWebView(jsonObj, m_controlsWebView.Get()); 499 | } 500 | ``` 501 | 502 | ```javascript 503 | function init() { 504 | window.chrome.webview.addEventListener('message', messageHandler); 505 | refreshControls(); 506 | refreshTabs(); 507 | 508 | createNewTab(true); 509 | } 510 | 511 | // ... 512 | 513 | function reloadActiveTabContent() { 514 | var message = { 515 | message: commands.MG_RELOAD, 516 | args: {} 517 | }; 518 | window.chrome.webview.postMessage(message); 519 | } 520 | ``` 521 | 522 | ### Tab handling 523 | 524 | A new tab will be created whenever the user clicks on the new tab button to the right of the open tabs. The controls WebView will post a message to the host application to create the WebView for that tab and create an object tracking its state. 525 | 526 | ```javascript 527 | function createNewTab(shouldBeActive) { 528 | const tabId = getNewTabId(); 529 | 530 | var message = { 531 | message: commands.MG_CREATE_TAB, 532 | args: { 533 | tabId: parseInt(tabId), 534 | active: shouldBeActive || false 535 | } 536 | }; 537 | 538 | window.chrome.webview.postMessage(message); 539 | 540 | tabs.set(parseInt(tabId), { 541 | title: 'New Tab', 542 | uri: '', 543 | uriToShow: '', 544 | favicon: 'img/favicon.png', 545 | isFavorite: false, 546 | isLoading: false, 547 | canGoBack: false, 548 | canGoForward: false, 549 | securityState: 'unknown', 550 | historyItemId: INVALID_HISTORY_ID 551 | }); 552 | 553 | loadTabUI(tabId); 554 | 555 | if (shouldBeActive) { 556 | switchToTab(tabId, false); 557 | } 558 | } 559 | ``` 560 | 561 | On the host app side, the registered [ICoreWebView2WebMessageReceivedEventHandler](https://learn.microsoft.com/microsoft-edge/webview2/reference/win32/icorewebview2webmessagereceivedeventhandler) will catch the message and create the WebView for that tab. 562 | 563 | ```cpp 564 | case MG_CREATE_TAB: 565 | { 566 | size_t id = args.at(L"tabId").as_number().to_uint32(); 567 | bool shouldBeActive = args.at(L"active").as_bool(); 568 | std::unique_ptr newTab = Tab::CreateNewTab(m_hWnd, m_contentEnv.Get(), id, shouldBeActive); 569 | 570 | std::map>::iterator it = m_tabs.find(id); 571 | if (it == m_tabs.end()) 572 | { 573 | m_tabs.insert(std::pair>(id, std::move(newTab))); 574 | } 575 | else 576 | { 577 | m_tabs.at(id)->m_contentWebView->Close(); 578 | it->second = std::move(newTab); 579 | } 580 | } 581 | break; 582 | ``` 583 | 584 | ```cpp 585 | std::unique_ptr Tab::CreateNewTab(HWND hWnd, ICoreWebView2Environment* env, size_t id, bool shouldBeActive) 586 | { 587 | std::unique_ptr tab = std::make_unique(); 588 | 589 | tab->m_parentHWnd = hWnd; 590 | tab->m_tabId = id; 591 | tab->SetMessageBroker(); 592 | tab->Init(env, shouldBeActive); 593 | 594 | return tab; 595 | } 596 | 597 | HRESULT Tab::Init(ICoreWebView2Environment* env, bool shouldBeActive) 598 | { 599 | return env->CreateCoreWebView2Controller(m_parentHWnd, Callback( 600 | [this, shouldBeActive](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT { 601 | if (!SUCCEEDED(result)) 602 | { 603 | OutputDebugString(L"Tab WebView creation failed\n"); 604 | return result; 605 | } 606 | m_contentController = controller; 607 | BrowserWindow::CheckFailure(m_contentController->get_CoreWebView2(&m_contentWebView), L""); 608 | BrowserWindow* browserWindow = reinterpret_cast(GetWindowLongPtr(m_parentHWnd, GWLP_USERDATA)); 609 | RETURN_IF_FAILED(m_contentWebView->add_WebMessageReceived(m_messageBroker.Get(), &m_messageBrokerToken)); 610 | 611 | // Register event handler for history change 612 | RETURN_IF_FAILED(m_contentWebView->add_HistoryChanged(Callback( 613 | [this, browserWindow](ICoreWebView2* webview, IUnknown* args) -> HRESULT 614 | { 615 | BrowserWindow::CheckFailure(browserWindow->HandleTabHistoryUpdate(m_tabId, webview), L"Can't update go back/forward buttons."); 616 | 617 | return S_OK; 618 | }).Get(), &m_historyUpdateForwarderToken)); 619 | 620 | // Register event handler for source change 621 | RETURN_IF_FAILED(m_contentWebView->add_SourceChanged(Callback( 622 | [this, browserWindow](ICoreWebView2* webview, ICoreWebView2SourceChangedEventArgs* args) -> HRESULT 623 | { 624 | BrowserWindow::CheckFailure(browserWindow->HandleTabURIUpdate(m_tabId, webview), L"Can't update address bar"); 625 | 626 | return S_OK; 627 | }).Get(), &m_uriUpdateForwarderToken)); 628 | 629 | RETURN_IF_FAILED(m_contentWebView->add_NavigationStarting(Callback( 630 | [this, browserWindow](ICoreWebView2* webview, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT 631 | { 632 | BrowserWindow::CheckFailure(browserWindow->HandleTabNavStarting(m_tabId, webview), L"Can't update reload button"); 633 | 634 | return S_OK; 635 | }).Get(), &m_navStartingToken)); 636 | 637 | RETURN_IF_FAILED(m_contentWebView->add_NavigationCompleted(Callback( 638 | [this, browserWindow](ICoreWebView2* webview, ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT 639 | { 640 | BrowserWindow::CheckFailure(browserWindow->HandleTabNavCompleted(m_tabId, webview, args), L"Can't update reload button"); 641 | return S_OK; 642 | }).Get(), &m_navCompletedToken)); 643 | 644 | // Handle security state updates 645 | 646 | RETURN_IF_FAILED(m_contentWebView->Navigate(L"https://www.bing.com")); 647 | browserWindow->HandleTabCreated(m_tabId, shouldBeActive); 648 | 649 | return S_OK; 650 | }).Get()); 651 | } 652 | ``` 653 | 654 | The tab registers all handlers so it can forward updates to the controls WebView when events fire. The tab is ready and will be shown on the content area of the browser. Clicking on a tab in the controls WebView will post a message to the host application, which will in turn hide the WebView for the previously active tab and show the one for the clicked tab. 655 | 656 | ```cpp 657 | HRESULT BrowserWindow::SwitchToTab(size_t tabId) 658 | { 659 | size_t previousActiveTab = m_activeTabId; 660 | 661 | RETURN_IF_FAILED(m_tabs.at(tabId)->ResizeWebView()); 662 | RETURN_IF_FAILED(m_tabs.at(tabId)->m_contentWebView->put_IsVisible(TRUE)); 663 | m_activeTabId = tabId; 664 | 665 | if (previousActiveTab != INVALID_TAB_ID && previousActiveTab != m_activeTabId) 666 | { 667 | RETURN_IF_FAILED(m_tabs.at(previousActiveTab)->m_contentWebView->put_IsVisible(FALSE)); 668 | } 669 | 670 | return S_OK; 671 | } 672 | ``` 673 | 674 | ### Updating the security icon 675 | 676 | We use the [CallDevToolsProtocolMethod](https://learn.microsoft.com/microsoft-edge/webview2/reference/win32/icorewebview2#calldevtoolsprotocolmethod) to enable listening for security events. Whenever a `securityStateChanged` event is fired, we will use the new state to update the security icon on the controls WebView. 677 | 678 | ```cpp 679 | // Enable listening for security events to update secure icon 680 | RETURN_IF_FAILED(m_contentWebView->CallDevToolsProtocolMethod(L"Security.enable", L"{}", nullptr)); 681 | 682 | BrowserWindow::CheckFailure(m_contentWebView->GetDevToolsProtocolEventReceiver(L"Security.securityStateChanged", &m_securityStateChangedReceiver), L""); 683 | 684 | // Forward security status updates to browser 685 | RETURN_IF_FAILED(m_securityStateChangedReceiver->add_DevToolsProtocolEventReceived(Callback( 686 | [this, browserWindow](ICoreWebView2* webview, ICoreWebView2DevToolsProtocolEventReceivedEventArgs* args) -> HRESULT 687 | { 688 | BrowserWindow::CheckFailure(browserWindow->HandleTabSecurityUpdate(m_tabId, webview, args), L"Can't update security icon"); 689 | return S_OK; 690 | }).Get(), &m_securityUpdateToken)); 691 | ``` 692 | 693 | ```cpp 694 | HRESULT BrowserWindow::HandleTabSecurityUpdate(size_t tabId, ICoreWebView2* webview, ICoreWebView2DevToolsProtocolEventReceivedEventArgs* args) 695 | { 696 | wil::unique_cotaskmem_string jsonArgs; 697 | RETURN_IF_FAILED(args->get_ParameterObjectAsJson(&jsonArgs)); 698 | web::json::value securityEvent = web::json::value::parse(jsonArgs.get()); 699 | 700 | web::json::value jsonObj = web::json::value::parse(L"{}"); 701 | jsonObj[L"message"] = web::json::value(MG_SECURITY_UPDATE); 702 | jsonObj[L"args"] = web::json::value::parse(L"{}"); 703 | jsonObj[L"args"][L"tabId"] = web::json::value::number(tabId); 704 | jsonObj[L"args"][L"state"] = securityEvent.at(L"securityState"); 705 | 706 | return PostJsonToWebView(jsonObj, m_controlsWebView.Get()); 707 | } 708 | ``` 709 | 710 | ```javascript 711 | case commands.MG_SECURITY_UPDATE: 712 | if (isValidTabId(args.tabId)) { 713 | const tab = tabs.get(args.tabId); 714 | tab.securityState = args.state; 715 | 716 | if (args.tabId == activeTabId) { 717 | updateNavigationUI(message); 718 | } 719 | } 720 | break; 721 | ``` 722 | 723 | ### Populating the history 724 | 725 | WebView2Browser uses IndexedDB in the controls WebView to store history items, just an example of how WebView2 enables you to access standard web technologies as you would in the browser. The item for a navigation will be created as soon as the URI is updated. These items are then retrieved by the history UI in a tab making use of `window.chrome.postMessage`. 726 | 727 | In this case, most functionality is implemented using JavaScript on both ends (controls WebView and content WebView loading the UI) so the host application is only acting as a message broker to communicate those ends. 728 | 729 | ```javascript 730 | case commands.MG_UPDATE_URI: 731 | if (isValidTabId(args.tabId)) { 732 | // ... 733 | 734 | // Don't add history entry if URI has not changed 735 | if (tab.uri == previousURI) { 736 | break; 737 | } 738 | 739 | // Filter URIs that should not appear in history 740 | if (!tab.uri || tab.uri == 'about:blank') { 741 | tab.historyItemId = INVALID_HISTORY_ID; 742 | break; 743 | } 744 | 745 | if (tab.uriToShow && tab.uriToShow.substring(0, 10) == 'browser://') { 746 | tab.historyItemId = INVALID_HISTORY_ID; 747 | break; 748 | } 749 | 750 | addHistoryItem(historyItemFromTab(args.tabId), (id) => { 751 | tab.historyItemId = id; 752 | }); 753 | } 754 | break; 755 | ``` 756 | 757 | ```javascript 758 | function addHistoryItem(item, callback) { 759 | queryDB((db) => { 760 | let transaction = db.transaction(['history'], 'readwrite'); 761 | let historyStore = transaction.objectStore('history'); 762 | 763 | // Check if an item for this URI exists on this day 764 | let currentDate = new Date(); 765 | let year = currentDate.getFullYear(); 766 | let month = currentDate.getMonth(); 767 | let date = currentDate.getDate(); 768 | let todayDate = new Date(year, month, date); 769 | 770 | let existingItemsIndex = historyStore.index('stampedURI'); 771 | let lowerBound = [item.uri, todayDate]; 772 | let upperBound = [item.uri, currentDate]; 773 | let range = IDBKeyRange.bound(lowerBound, upperBound); 774 | let request = existingItemsIndex.openCursor(range); 775 | 776 | request.onsuccess = function(event) { 777 | let cursor = event.target.result; 778 | if (cursor) { 779 | // There's an entry for this URI, update the item 780 | cursor.value.timestamp = item.timestamp; 781 | let updateRequest = cursor.update(cursor.value); 782 | 783 | updateRequest.onsuccess = function(event) { 784 | if (callback) { 785 | callback(event.target.result.primaryKey); 786 | } 787 | }; 788 | } else { 789 | // No entry for this URI, add item 790 | let addItemRequest = historyStore.add(item); 791 | 792 | addItemRequest.onsuccess = function(event) { 793 | if (callback) { 794 | callback(event.target.result); 795 | } 796 | }; 797 | } 798 | }; 799 | 800 | }); 801 | } 802 | ``` 803 | 804 | ## Handling JSON and URIs 805 | 806 | WebView2Browser uses Microsoft's [cpprestsdk (Casablanca)](https://github.com/Microsoft/cpprestsdk) to handle all JSON in the C++ side of things. IUri and CreateUri are also used to parse file paths into URIs and can be used to for other URIs as well. 807 | 808 | ## Code of Conduct 809 | 810 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 811 | -------------------------------------------------------------------------------- /Resource.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | //{{NO_DEPENDENCIES}} 6 | // Microsoft Visual C++ generated include file. 7 | // Used by WebViewBrowserApp.rc 8 | // 9 | #define IDC_MYICON 2 10 | #define IDD_WEBVIEWBROWSERAPP_DIALOG 102 11 | #define IDS_APP_TITLE 103 12 | #define IDM_ABOUT 104 13 | #define IDI_WEBVIEWBROWSERAPP 107 14 | #define IDI_SMALL 108 15 | #define IDC_WEBVIEWBROWSERAPP 109 16 | #define IDR_MAINFRAME 128 17 | #define IDC_STATIC -1 18 | 19 | // Next default values for new objects 20 | // 21 | #ifdef APSTUDIO_INVOKED 22 | #ifndef APSTUDIO_READONLY_SYMBOLS 23 | #define _APS_NO_MFC 1 24 | #define _APS_NEXT_RESOURCE_VALUE 131 25 | #define _APS_NEXT_COMMAND_VALUE 32771 26 | #define _APS_NEXT_CONTROL_VALUE 1000 27 | #define _APS_NEXT_SYMED_VALUE 110 28 | #endif 29 | #endif 30 | -------------------------------------------------------------------------------- /Tab.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include "BrowserWindow.h" 6 | #include "Tab.h" 7 | 8 | using namespace Microsoft::WRL; 9 | 10 | std::unique_ptr Tab::CreateNewTab(HWND hWnd, ICoreWebView2Environment* env, size_t id, bool shouldBeActive) 11 | { 12 | std::unique_ptr tab = std::make_unique(); 13 | 14 | tab->m_parentHWnd = hWnd; 15 | tab->m_tabId = id; 16 | tab->SetMessageBroker(); 17 | tab->Init(env, shouldBeActive); 18 | 19 | return tab; 20 | } 21 | 22 | HRESULT Tab::Init(ICoreWebView2Environment* env, bool shouldBeActive) 23 | { 24 | return env->CreateCoreWebView2Controller(m_parentHWnd, Callback( 25 | [this, shouldBeActive](HRESULT result, ICoreWebView2Controller* host) -> HRESULT { 26 | if (!SUCCEEDED(result)) 27 | { 28 | OutputDebugString(L"Tab WebView creation failed\n"); 29 | return result; 30 | } 31 | m_contentController = host; 32 | BrowserWindow::CheckFailure(m_contentController->get_CoreWebView2(&m_contentWebView), L""); 33 | BrowserWindow* browserWindow = reinterpret_cast(GetWindowLongPtr(m_parentHWnd, GWLP_USERDATA)); 34 | RETURN_IF_FAILED(m_contentWebView->add_WebMessageReceived(m_messageBroker.Get(), &m_messageBrokerToken)); 35 | 36 | // Register event handler for history change 37 | RETURN_IF_FAILED(m_contentWebView->add_HistoryChanged(Callback( 38 | [this, browserWindow](ICoreWebView2* webview, IUnknown* args) -> HRESULT 39 | { 40 | BrowserWindow::CheckFailure(browserWindow->HandleTabHistoryUpdate(m_tabId, webview), L"Can't update go back/forward buttons."); 41 | 42 | return S_OK; 43 | }).Get(), &m_historyUpdateForwarderToken)); 44 | 45 | // Register event handler for source change 46 | RETURN_IF_FAILED(m_contentWebView->add_SourceChanged(Callback( 47 | [this, browserWindow](ICoreWebView2* webview, ICoreWebView2SourceChangedEventArgs* args) -> HRESULT 48 | { 49 | BrowserWindow::CheckFailure(browserWindow->HandleTabURIUpdate(m_tabId, webview), L"Can't update address bar"); 50 | 51 | return S_OK; 52 | }).Get(), &m_uriUpdateForwarderToken)); 53 | 54 | RETURN_IF_FAILED(m_contentWebView->add_NavigationStarting(Callback( 55 | [this, browserWindow](ICoreWebView2* webview, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT 56 | { 57 | BrowserWindow::CheckFailure(browserWindow->HandleTabNavStarting(m_tabId, webview), L"Can't update reload button"); 58 | 59 | return S_OK; 60 | }).Get(), &m_navStartingToken)); 61 | 62 | RETURN_IF_FAILED(m_contentWebView->add_NavigationCompleted(Callback( 63 | [this, browserWindow](ICoreWebView2* webview, ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT 64 | { 65 | BrowserWindow::CheckFailure(browserWindow->HandleTabNavCompleted(m_tabId, webview, args), L"Can't udpate reload button"); 66 | return S_OK; 67 | }).Get(), &m_navCompletedToken)); 68 | 69 | // Enable listening for security events to update secure icon 70 | RETURN_IF_FAILED(m_contentWebView->CallDevToolsProtocolMethod(L"Security.enable", L"{}", nullptr)); 71 | 72 | BrowserWindow::CheckFailure(m_contentWebView->GetDevToolsProtocolEventReceiver(L"Security.securityStateChanged", &m_securityStateChangedReceiver), L""); 73 | 74 | // Forward security status updates to browser 75 | RETURN_IF_FAILED(m_securityStateChangedReceiver->add_DevToolsProtocolEventReceived(Callback( 76 | [this, browserWindow](ICoreWebView2* webview, ICoreWebView2DevToolsProtocolEventReceivedEventArgs* args) -> HRESULT 77 | { 78 | BrowserWindow::CheckFailure(browserWindow->HandleTabSecurityUpdate(m_tabId, webview, args), L"Can't udpate security icon"); 79 | return S_OK; 80 | }).Get(), &m_securityUpdateToken)); 81 | 82 | RETURN_IF_FAILED(m_contentWebView->Navigate(L"https://www.bing.com")); 83 | browserWindow->HandleTabCreated(m_tabId, shouldBeActive); 84 | 85 | return S_OK; 86 | }).Get()); 87 | } 88 | 89 | void Tab::SetMessageBroker() 90 | { 91 | m_messageBroker = Callback( 92 | [this](ICoreWebView2* webview, ICoreWebView2WebMessageReceivedEventArgs* eventArgs) -> HRESULT 93 | { 94 | BrowserWindow* browserWindow = reinterpret_cast(GetWindowLongPtr(m_parentHWnd, GWLP_USERDATA)); 95 | BrowserWindow::CheckFailure(browserWindow->HandleTabMessageReceived(m_tabId, webview, eventArgs), L""); 96 | 97 | return S_OK; 98 | }); 99 | } 100 | 101 | HRESULT Tab::ResizeWebView() 102 | { 103 | RECT bounds; 104 | GetClientRect(m_parentHWnd, &bounds); 105 | 106 | BrowserWindow* browserWindow = reinterpret_cast(GetWindowLongPtr(m_parentHWnd, GWLP_USERDATA)); 107 | bounds.top += browserWindow->GetDPIAwareBound(BrowserWindow::c_uiBarHeight); 108 | 109 | return m_contentController->put_Bounds(bounds); 110 | } 111 | -------------------------------------------------------------------------------- /Tab.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #pragma once 6 | 7 | #include "framework.h" 8 | 9 | class Tab 10 | { 11 | public: 12 | Microsoft::WRL::ComPtr m_contentController; 13 | Microsoft::WRL::ComPtr m_contentWebView; 14 | Microsoft::WRL::ComPtr m_securityStateChangedReceiver; 15 | 16 | static std::unique_ptr CreateNewTab(HWND hWnd, ICoreWebView2Environment* env, size_t id, bool shouldBeActive); 17 | HRESULT ResizeWebView(); 18 | protected: 19 | HWND m_parentHWnd = nullptr; 20 | size_t m_tabId = INVALID_TAB_ID; 21 | EventRegistrationToken m_historyUpdateForwarderToken = {}; 22 | EventRegistrationToken m_uriUpdateForwarderToken = {}; 23 | EventRegistrationToken m_navStartingToken = {}; 24 | EventRegistrationToken m_navCompletedToken = {}; 25 | EventRegistrationToken m_securityUpdateToken = {}; 26 | EventRegistrationToken m_messageBrokerToken = {}; // Message broker for browser pages loaded in a tab 27 | Microsoft::WRL::ComPtr m_messageBroker; 28 | 29 | HRESULT Init(ICoreWebView2Environment* env, bool shouldBeActive); 30 | void SetMessageBroker(); 31 | }; 32 | -------------------------------------------------------------------------------- /WebView2Browser.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/WebView2Browser.ico -------------------------------------------------------------------------------- /WebViewBrowserApp.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | // WebViewBrowserApp.cpp : Defines the entry point for the application. 6 | // 7 | 8 | #include "BrowserWindow.h" 9 | #include "WebViewBrowserApp.h" 10 | 11 | using namespace Microsoft::WRL; 12 | 13 | void tryLaunchWindow(HINSTANCE hInstance, int nCmdShow); 14 | 15 | int APIENTRY wWinMain(_In_ HINSTANCE hInstance, 16 | _In_opt_ HINSTANCE hPrevInstance, 17 | _In_ LPWSTR lpCmdLine, 18 | _In_ int nCmdShow) 19 | { 20 | UNREFERENCED_PARAMETER(hPrevInstance); 21 | UNREFERENCED_PARAMETER(lpCmdLine); 22 | 23 | // Call SetProcessDPIAware() instead when using Windows 7 or any version 24 | // below 1703 (Windows 10). 25 | SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); 26 | 27 | BrowserWindow::RegisterClass(hInstance); 28 | 29 | tryLaunchWindow(hInstance, nCmdShow); 30 | 31 | HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_WEBVIEWBROWSERAPP)); 32 | 33 | MSG msg; 34 | 35 | // Main message loop: 36 | while (GetMessage(&msg, nullptr, 0, 0)) 37 | { 38 | if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) 39 | { 40 | TranslateMessage(&msg); 41 | DispatchMessage(&msg); 42 | } 43 | } 44 | 45 | return (int) msg.wParam; 46 | } 47 | 48 | void tryLaunchWindow(HINSTANCE hInstance, int nCmdShow) 49 | { 50 | BOOL launched = BrowserWindow::LaunchWindow(hInstance, nCmdShow); 51 | if (!launched) 52 | { 53 | int msgboxID = MessageBox(NULL, L"Could not launch the browser", L"Error", MB_RETRYCANCEL); 54 | 55 | switch (msgboxID) 56 | { 57 | case IDRETRY: 58 | tryLaunchWindow(hInstance, nCmdShow); 59 | break; 60 | case IDCANCEL: 61 | default: 62 | PostQuitMessage(0); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /WebViewBrowserApp.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #pragma once 6 | 7 | #include "framework.h" 8 | -------------------------------------------------------------------------------- /WebViewBrowserApp.rc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/WebViewBrowserApp.rc -------------------------------------------------------------------------------- /WebViewBrowserApp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29001.49 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WebView2Browser", "WebViewBrowserApp.vcxproj", "{D65018E5-6B31-4DC7-AFAC-7999384BA4BD}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Debug|x86 = Debug|x86 12 | Release|x64 = Release|x64 13 | Release|x86 = Release|x86 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {D65018E5-6B31-4DC7-AFAC-7999384BA4BD}.Debug|x64.ActiveCfg = Debug|x64 17 | {D65018E5-6B31-4DC7-AFAC-7999384BA4BD}.Debug|x64.Build.0 = Debug|x64 18 | {D65018E5-6B31-4DC7-AFAC-7999384BA4BD}.Debug|x86.ActiveCfg = Debug|Win32 19 | {D65018E5-6B31-4DC7-AFAC-7999384BA4BD}.Debug|x86.Build.0 = Debug|Win32 20 | {D65018E5-6B31-4DC7-AFAC-7999384BA4BD}.Release|x64.ActiveCfg = Release|x64 21 | {D65018E5-6B31-4DC7-AFAC-7999384BA4BD}.Release|x64.Build.0 = Release|x64 22 | {D65018E5-6B31-4DC7-AFAC-7999384BA4BD}.Release|x86.ActiveCfg = Release|Win32 23 | {D65018E5-6B31-4DC7-AFAC-7999384BA4BD}.Release|x86.Build.0 = Release|Win32 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {0C9921DB-4634-415A-9AB1-087EA9B2EB24} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /WebViewBrowserApp.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 16.0 23 | {D65018E5-6B31-4DC7-AFAC-7999384BA4BD} 24 | Win32Proj 25 | WebViewBrowserApp 26 | 10.0 27 | WebView2Browser 28 | 29 | 30 | 31 | Application 32 | true 33 | v142 34 | Unicode 35 | 36 | 37 | Application 38 | false 39 | v142 40 | true 41 | Unicode 42 | 43 | 44 | Application 45 | true 46 | v142 47 | Unicode 48 | 49 | 50 | Application 51 | false 52 | v142 53 | true 54 | Unicode 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | true 76 | $(Configuration)_x86 77 | $(Configuration)\ 78 | $(ProjectName) 79 | 80 | 81 | true 82 | $(Configuration)_$(Platform) 83 | $(IncludePath) 84 | $(ProjectName) 85 | $(Configuration)\ 86 | 87 | 88 | false 89 | $(Configuration)_x86 90 | $(Configuration)\ 91 | $(ProjectName) 92 | 93 | 94 | false 95 | $(Configuration)_$(Platform) 96 | $(ProjectName) 97 | $(Configuration)\ 98 | 99 | 100 | 101 | 102 | 103 | Level3 104 | Disabled 105 | true 106 | WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) 107 | true 108 | 109 | 110 | Windows 111 | true 112 | 113 | 114 | xcopy "$(ProjectDir)wvbrowser_ui" "$(OutDir)wvbrowser_ui" /S /I /Y 115 | 116 | 117 | 118 | 119 | 120 | 121 | Level3 122 | Disabled 123 | true 124 | _DEBUG;_WINDOWS;%(PreprocessorDefinitions) 125 | true 126 | 127 | 128 | Windows 129 | true 130 | 131 | 132 | 133 | 134 | xcopy "$(ProjectDir)wvbrowser_ui" "$(OutDir)wvbrowser_ui" /S /I /Y 135 | 136 | 137 | 138 | 139 | 140 | 141 | Level3 142 | MaxSpeed 143 | true 144 | true 145 | true 146 | WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) 147 | true 148 | 149 | 150 | Windows 151 | true 152 | true 153 | true 154 | 155 | 156 | xcopy "$(ProjectDir)wvbrowser_ui" "$(OutDir)wvbrowser_ui" /S /I /Y 157 | 158 | 159 | 160 | 161 | 162 | 163 | Level3 164 | MaxSpeed 165 | true 166 | true 167 | true 168 | NDEBUG;_WINDOWS;%(PreprocessorDefinitions) 169 | true 170 | 171 | 172 | Windows 173 | true 174 | true 175 | true 176 | 177 | 178 | xcopy "$(ProjectDir)wvbrowser_ui" "$(OutDir)wvbrowser_ui" /S /I /Y 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 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}. 214 | 215 | 216 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /WebViewBrowserApp.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | Header Files 20 | 21 | 22 | Header Files 23 | 24 | 25 | Header Files 26 | 27 | 28 | Header Files 29 | 30 | 31 | Header Files 32 | 33 | 34 | Header Files 35 | 36 | 37 | 38 | 39 | Source Files 40 | 41 | 42 | Source Files 43 | 44 | 45 | Source Files 46 | 47 | 48 | 49 | 50 | Resource Files 51 | 52 | 53 | 54 | 55 | Resource Files 56 | 57 | 58 | Resource Files 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /framework.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #pragma once 6 | 7 | #include "targetver.h" 8 | #define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers 9 | // Windows Header Files 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | // C RunTime Header Files 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | // App specific includes 26 | #include "resource.h" 27 | #include "webview2.h" 28 | 29 | #define DEFAULT_DPI 96 30 | #define MIN_WINDOW_WIDTH 510 31 | #define MIN_WINDOW_HEIGHT 75 32 | #define MAX_LOADSTRING 256 33 | 34 | #define INVALID_TAB_ID 0 35 | #define MG_NAVIGATE 1 36 | #define MG_UPDATE_URI 2 37 | #define MG_GO_FORWARD 3 38 | #define MG_GO_BACK 4 39 | #define MG_NAV_STARTING 5 40 | #define MG_NAV_COMPLETED 6 41 | #define MG_RELOAD 7 42 | #define MG_CANCEL 8 43 | #define MG_CREATE_TAB 10 44 | #define MG_UPDATE_TAB 11 45 | #define MG_SWITCH_TAB 12 46 | #define MG_CLOSE_TAB 13 47 | #define MG_CLOSE_WINDOW 14 48 | #define MG_SHOW_OPTIONS 15 49 | #define MG_HIDE_OPTIONS 16 50 | #define MG_OPTIONS_LOST_FOCUS 17 51 | #define MG_OPTION_SELECTED 18 52 | #define MG_SECURITY_UPDATE 19 53 | #define MG_UPDATE_FAVICON 20 54 | #define MG_GET_SETTINGS 21 55 | #define MG_GET_FAVORITES 22 56 | #define MG_REMOVE_FAVORITE 23 57 | #define MG_CLEAR_CACHE 24 58 | #define MG_CLEAR_COOKIES 25 59 | #define MG_GET_HISTORY 26 60 | #define MG_REMOVE_HISTORY_ITEM 27 61 | #define MG_CLEAR_HISTORY 28 62 | -------------------------------------------------------------------------------- /packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /screenshots/WebView2Browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/screenshots/WebView2Browser.png -------------------------------------------------------------------------------- /screenshots/layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/screenshots/layout.png -------------------------------------------------------------------------------- /targetver.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #pragma once 6 | 7 | // Including SDKDDKVer.h defines the highest available Windows platform. 8 | // If you wish to build your application for a previous Windows platform, include WinSDKVer.h and 9 | // set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. 10 | #include 11 | -------------------------------------------------------------------------------- /wvbrowser_ui/commands.js: -------------------------------------------------------------------------------- 1 | const commands = { 2 | MG_NAVIGATE: 1, 3 | MG_UPDATE_URI: 2, 4 | MG_GO_FORWARD: 3, 5 | MG_GO_BACK: 4, 6 | MG_NAV_STARTING: 5, 7 | MG_NAV_COMPLETED: 6, 8 | MG_RELOAD: 7, 9 | MG_CANCEL: 8, 10 | MG_CREATE_TAB: 10, 11 | MG_UPDATE_TAB: 11, 12 | MG_SWITCH_TAB: 12, 13 | MG_CLOSE_TAB: 13, 14 | MG_CLOSE_WINDOW: 14, 15 | MG_SHOW_OPTIONS: 15, 16 | MG_HIDE_OPTIONS: 16, 17 | MG_OPTIONS_LOST_FOCUS: 17, 18 | MG_OPTION_SELECTED: 18, 19 | MG_SECURITY_UPDATE: 19, 20 | MG_UPDATE_FAVICON: 20, 21 | MG_GET_SETTINGS: 21, 22 | MG_GET_FAVORITES: 22, 23 | MG_REMOVE_FAVORITE: 23, 24 | MG_CLEAR_CACHE: 24, 25 | MG_CLEAR_COOKIES: 25, 26 | MG_GET_HISTORY: 26, 27 | MG_REMOVE_HISTORY_ITEM: 27, 28 | MG_CLEAR_HISTORY: 28 29 | }; 30 | -------------------------------------------------------------------------------- /wvbrowser_ui/content_ui/favorites.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Favorites 4 | 5 | 6 | 7 | 8 | 9 |

Favorites

10 |
11 | You don't have any favorites. 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /wvbrowser_ui/content_ui/favorites.js: -------------------------------------------------------------------------------- 1 | const messageHandler = event => { 2 | var message = event.data.message; 3 | var args = event.data.args; 4 | 5 | switch (message) { 6 | case commands.MG_GET_FAVORITES: 7 | loadFavorites(args.favorites); 8 | break; 9 | default: 10 | console.log(`Unexpected message: ${JSON.stringify(event.data)}`); 11 | break; 12 | } 13 | }; 14 | 15 | function requestFavorites() { 16 | let message = { 17 | message: commands.MG_GET_FAVORITES, 18 | args: {} 19 | }; 20 | 21 | window.chrome.webview.postMessage(message); 22 | } 23 | 24 | function removeFavorite(uri) { 25 | let message = { 26 | message: commands.MG_REMOVE_FAVORITE, 27 | args: { 28 | uri: uri 29 | } 30 | }; 31 | 32 | window.chrome.webview.postMessage(message); 33 | } 34 | 35 | function loadFavorites(payload) { 36 | let fragment = document.createDocumentFragment(); 37 | 38 | if (payload.length > 0) { 39 | let container = document.getElementById('entries-container'); 40 | container.textContent = ''; 41 | } 42 | 43 | payload.map(favorite => { 44 | let favoriteContainer = document.createElement('div'); 45 | favoriteContainer.className = 'item-container'; 46 | let favoriteElement = document.createElement('div'); 47 | favoriteElement.className = 'item'; 48 | 49 | let faviconElement = document.createElement('div'); 50 | faviconElement.className = 'favicon'; 51 | let faviconImage = document.createElement('img'); 52 | faviconImage.src = favorite.favicon; 53 | faviconElement.appendChild(faviconImage); 54 | 55 | let labelElement = document.createElement('div'); 56 | labelElement.className = 'label-title'; 57 | let linkElement = document.createElement('a'); 58 | linkElement.textContent = favorite.title; 59 | linkElement.href = favorite.uri; 60 | linkElement.title = favorite.title; 61 | labelElement.appendChild(linkElement); 62 | 63 | let uriElement = document.createElement('div'); 64 | uriElement.className = 'label-uri'; 65 | let textElement = document.createElement('p'); 66 | textElement.textContent = favorite.uriToShow || favorite.uri; 67 | textElement.title = favorite.uriToShow || favorite.uri; 68 | uriElement.appendChild(textElement); 69 | 70 | let buttonElement = document.createElement('div'); 71 | buttonElement.className = 'btn-close'; 72 | buttonElement.addEventListener('click', function(e) { 73 | favoriteContainer.parentNode.removeChild(favoriteContainer); 74 | removeFavorite(favorite.uri); 75 | }); 76 | 77 | favoriteElement.appendChild(faviconElement); 78 | favoriteElement.appendChild(labelElement); 79 | favoriteElement.appendChild(uriElement); 80 | favoriteElement.appendChild(buttonElement); 81 | 82 | favoriteContainer.appendChild(favoriteElement); 83 | fragment.appendChild(favoriteContainer); 84 | }); 85 | 86 | let container = document.getElementById('entries-container'); 87 | container.appendChild(fragment); 88 | } 89 | 90 | function init() { 91 | window.chrome.webview.addEventListener('message', messageHandler); 92 | requestFavorites(); 93 | } 94 | 95 | init(); 96 | -------------------------------------------------------------------------------- /wvbrowser_ui/content_ui/history.css: -------------------------------------------------------------------------------- 1 | .header-date { 2 | font-weight: 400; 3 | font-size: 14px; 4 | color: rgb(16, 16, 16); 5 | line-height: 20px; 6 | padding-top: 10px; 7 | padding-bottom: 4px; 8 | margin: 0; 9 | } 10 | 11 | #btn-clear { 12 | font-size: 14px; 13 | color: rgb(0, 97, 171); 14 | cursor: pointer; 15 | line-height: 20px; 16 | } 17 | 18 | #btn-clear.hidden { 19 | display: none; 20 | } 21 | 22 | #overlay { 23 | position: fixed; 24 | top: 0; 25 | left: 0; 26 | height: 100%; 27 | width: 100%; 28 | background-color: rgba(0, 0, 0, 0.2); 29 | } 30 | 31 | #overlay.hidden { 32 | display: none; 33 | } 34 | 35 | #prompt-box { 36 | display: flex; 37 | box-sizing: border-box; 38 | flex-direction: column; 39 | position: fixed; 40 | left: calc(50% - 130px); 41 | top: calc(50% - 70px); 42 | width: 260px; 43 | height: 140px; 44 | padding: 20px; 45 | border-radius: 5px; 46 | background-color: white; 47 | 48 | box-shadow: rgba(0, 0, 0, 0.13) 0px 1.6px 20px, rgba(0, 0, 0, 0.11) 0px 0.3px 10px; 49 | } 50 | 51 | #prompt-options { 52 | flex: 1; 53 | display: flex; 54 | justify-content: flex-end; 55 | 56 | user-select: none; 57 | } 58 | 59 | .prompt-btn { 60 | flex: 1; 61 | flex-grow: 0; 62 | align-self: flex-end; 63 | cursor: pointer; 64 | font-family: 'system-ui', sans-serif; 65 | display: inline-block; 66 | padding: 2px 7px; 67 | font-size: 14px; 68 | line-height: 20px; 69 | border-radius: 3px; 70 | font-weight: 400; 71 | } 72 | 73 | #prompt-true { 74 | background-color: rgb(0, 112, 198); 75 | color: white; 76 | } 77 | 78 | #prompt-false { 79 | background-color: rgb(210, 210, 210); 80 | margin-right: 5px; 81 | } 82 | -------------------------------------------------------------------------------- /wvbrowser_ui/content_ui/history.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | History 4 | 5 | 6 | 7 | 8 | 9 | 10 | 19 |

History

20 |
21 | 22 |
23 |
24 | Loading... 25 |
26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /wvbrowser_ui/content_ui/history.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_HISTORY_ITEM_COUNT = 20; 2 | const EMPTY_HISTORY_MESSAGE = `You haven't visited any sites yet.`; 3 | let requestedTop = 0; 4 | let lastRequestSize = 0; 5 | let itemHeight = 48; 6 | 7 | const dateStringFormat = new Intl.DateTimeFormat('default', { 8 | weekday: 'long', 9 | year: 'numeric', 10 | month: 'long', 11 | day: 'numeric' 12 | }); 13 | 14 | const timeStringFormat = new Intl.DateTimeFormat('default', { 15 | hour: '2-digit', 16 | minute: '2-digit' 17 | }); 18 | 19 | const messageHandler = event => { 20 | var message = event.data.message; 21 | var args = event.data.args; 22 | 23 | switch (message) { 24 | case commands.MG_GET_HISTORY: 25 | let entriesContainer = document.getElementById('entries-container'); 26 | if (args.from == 0 && args.items.length) { 27 | entriesContainer.textContent = ''; 28 | 29 | let clearButton = document.getElementById('btn-clear'); 30 | clearButton.classList.remove('hidden'); 31 | } 32 | 33 | loadItems(args.items); 34 | if (args.items.length == lastRequestSize) { 35 | document.addEventListener('scroll', requestTrigger); 36 | } else if (entriesContainer.childElementCount == 0) { 37 | loadUIForEmptyHistory(); 38 | } 39 | break; 40 | default: 41 | console.log(`Unexpected message: ${JSON.stringify(event.data)}`); 42 | break; 43 | } 44 | }; 45 | 46 | const requestTrigger = function(event) { 47 | let triggerRange = 50; 48 | let element = document.body; 49 | 50 | if (element.scrollTop + element.clientHeight >= element.scrollHeight - triggerRange) { 51 | getMoreHistoryItems(); 52 | event.target.removeEventListener('scroll', requestTrigger); 53 | } 54 | }; 55 | 56 | function requestHistoryItems(from, count) { 57 | let message = { 58 | message: commands.MG_GET_HISTORY, 59 | args: { 60 | from: from, 61 | count: count || DEFAULT_HISTORY_ITEM_COUNT 62 | } 63 | }; 64 | 65 | window.chrome.webview.postMessage(message); 66 | } 67 | 68 | function removeItem(id) { 69 | let message = { 70 | message: commands.MG_REMOVE_HISTORY_ITEM, 71 | args: { 72 | id: id 73 | } 74 | }; 75 | 76 | window.chrome.webview.postMessage(message); 77 | } 78 | 79 | function createItemElement(item, id, date) { 80 | let itemContainer = document.createElement('div'); 81 | itemContainer.id = id; 82 | itemContainer.className = 'item-container'; 83 | 84 | let itemElement = document.createElement('div'); 85 | itemElement.className = 'item'; 86 | 87 | // Favicon 88 | let faviconElement = document.createElement('div'); 89 | faviconElement.className = 'favicon'; 90 | let faviconImage = document.createElement('img'); 91 | faviconImage.src = item.favicon; 92 | faviconElement.append(faviconImage); 93 | itemElement.append(faviconElement); 94 | 95 | // Title 96 | let titleLabel = document.createElement('div'); 97 | titleLabel.className = 'label-title'; 98 | let linkElement = document.createElement('a'); 99 | linkElement.href = item.uri; 100 | linkElement.title = item.title; 101 | linkElement.textContent = item.title; 102 | titleLabel.append(linkElement); 103 | itemElement.append(titleLabel); 104 | 105 | // URI 106 | let uriLabel = document.createElement('div'); 107 | uriLabel.className = 'label-uri'; 108 | let textElement = document.createElement('p'); 109 | textElement.title = item.uri; 110 | textElement.textContent = item.uri; 111 | uriLabel.append(textElement); 112 | itemElement.append(uriLabel); 113 | 114 | // Time 115 | let timeLabel = document.createElement('div'); 116 | timeLabel.className = 'label-time'; 117 | let timeText = document.createElement('p'); 118 | timeText.textContent = timeStringFormat.format(date); 119 | timeLabel.append(timeText); 120 | itemElement.append(timeLabel); 121 | 122 | // Close button 123 | let closeButton = document.createElement('div'); 124 | closeButton.className = 'btn-close'; 125 | closeButton.addEventListener('click', function(e) { 126 | if (itemContainer.parentNode.children.length <= 2) { 127 | itemContainer.parentNode.remove(); 128 | } else { 129 | itemContainer.remove(); 130 | } 131 | 132 | let entriesContainer = document.getElementById('entries-container'); 133 | if (entriesContainer.childElementCount == 0) { 134 | loadUIForEmptyHistory(); 135 | } 136 | removeItem(parseInt(id.split('-')[1])); 137 | }); 138 | itemElement.append(closeButton); 139 | itemContainer.append(itemElement); 140 | 141 | return itemContainer; 142 | } 143 | 144 | function createDateContainer(id, date) { 145 | let dateContainer = document.createElement('div'); 146 | dateContainer.id = id; 147 | 148 | let dateLabel = document.createElement('h3'); 149 | dateLabel.className = 'header-date'; 150 | dateLabel.textContent = dateStringFormat.format(date); 151 | dateContainer.append(dateLabel); 152 | 153 | return dateContainer; 154 | } 155 | 156 | function loadItems(items) { 157 | let dateContainer; 158 | let fragment; 159 | 160 | items.map((entry) => { 161 | let id = entry.id; 162 | let item = entry.item; 163 | let itemContainerId = `item-${id}`; 164 | 165 | // Skip the item if already loaded. This could happen if the user 166 | // visits an item for the current date again before requesting more 167 | // history items. 168 | let itemContainer = document.getElementById(itemContainerId); 169 | if (itemContainer) { 170 | return; 171 | } 172 | 173 | let date = new Date(item.timestamp); 174 | let day = date.getDate(); 175 | let month = date.getMonth(); 176 | let year = date.getFullYear(); 177 | let dateContainerId = `entries-${month}-${day}-${year}`; 178 | 179 | // If entry belongs to a new date, append buffered items for previous 180 | // date. 181 | if (dateContainer && dateContainer.id != dateContainerId) { 182 | dateContainer.append(fragment); 183 | } 184 | 185 | dateContainer = document.getElementById(dateContainerId); 186 | if (!dateContainer) { 187 | dateContainer = createDateContainer(dateContainerId, date); 188 | fragment = document.createDocumentFragment(); 189 | 190 | let entriesContainer = document.getElementById('entries-container'); 191 | entriesContainer.append(dateContainer); 192 | } else if (!fragment) { 193 | fragment = document.createDocumentFragment(); 194 | } 195 | 196 | itemContainer = createItemElement(item, itemContainerId, date); 197 | fragment.append(itemContainer); 198 | }); 199 | 200 | // Append remaining items in buffer 201 | if (fragment) { 202 | dateContainer.append(fragment); 203 | } 204 | } 205 | 206 | function getMoreHistoryItems(n) { 207 | n = n ? n : DEFAULT_HISTORY_ITEM_COUNT; 208 | 209 | requestHistoryItems(requestedTop, n); 210 | requestedTop += n; 211 | lastRequestSize = n; 212 | document.removeEventListener('scroll', requestTrigger); 213 | } 214 | 215 | function addUIListeners() { 216 | let confirmButton = document.getElementById('prompt-true'); 217 | confirmButton.addEventListener('click', function(event) { 218 | clearHistory(); 219 | event.stopPropagation(); 220 | }); 221 | 222 | let cancelButton = document.getElementById('prompt-false'); 223 | cancelButton.addEventListener('click', function(event) { 224 | toggleClearPrompt(); 225 | event.stopPropagation(); 226 | }); 227 | 228 | let promptBox = document.getElementById('prompt-box'); 229 | promptBox.addEventListener('click', function(event) { 230 | event.stopPropagation(); 231 | }); 232 | 233 | let promptOverlay = document.getElementById('overlay'); 234 | promptOverlay.addEventListener('click', toggleClearPrompt); 235 | 236 | let clearButton = document.getElementById('btn-clear'); 237 | clearButton.addEventListener('click', toggleClearPrompt); 238 | } 239 | 240 | function toggleClearPrompt() { 241 | let promptOverlay = document.getElementById('overlay'); 242 | promptOverlay.classList.toggle('hidden'); 243 | } 244 | 245 | function loadUIForEmptyHistory() { 246 | let entriesContainer = document.getElementById('entries-container'); 247 | entriesContainer.textContent = EMPTY_HISTORY_MESSAGE; 248 | 249 | let clearButton = document.getElementById('btn-clear'); 250 | clearButton.classList.add('hidden'); 251 | } 252 | 253 | function clearHistory() { 254 | toggleClearPrompt(); 255 | loadUIForEmptyHistory(); 256 | 257 | let message = { 258 | message: commands.MG_CLEAR_HISTORY, 259 | args: {} 260 | }; 261 | 262 | window.chrome.webview.postMessage(message); 263 | } 264 | 265 | function init() { 266 | window.chrome.webview.addEventListener('message', messageHandler); 267 | 268 | let viewportItemsCapacity = Math.round(window.innerHeight / itemHeight); 269 | addUIListeners(); 270 | getMoreHistoryItems(viewportItemsCapacity); 271 | } 272 | 273 | init(); 274 | -------------------------------------------------------------------------------- /wvbrowser_ui/content_ui/img/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/wvbrowser_ui/content_ui/img/close.png -------------------------------------------------------------------------------- /wvbrowser_ui/content_ui/img/favorites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/wvbrowser_ui/content_ui/img/favorites.png -------------------------------------------------------------------------------- /wvbrowser_ui/content_ui/img/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/wvbrowser_ui/content_ui/img/history.png -------------------------------------------------------------------------------- /wvbrowser_ui/content_ui/img/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/wvbrowser_ui/content_ui/img/settings.png -------------------------------------------------------------------------------- /wvbrowser_ui/content_ui/items.css: -------------------------------------------------------------------------------- 1 | .item-container { 2 | box-sizing: border-box; 3 | padding: 4px 0; 4 | height: 48px; 5 | } 6 | 7 | .item { 8 | display: flex; 9 | width: 100%; 10 | max-width: 820px; 11 | height: 40px; 12 | width: 100%; 13 | border-radius: 4px; 14 | box-sizing: border-box; 15 | box-shadow: rgba(0, 0, 0, 0.13) 0px 1.6px 3.6px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px; 16 | align-items: center; 17 | background: rgb(255, 255, 255); 18 | } 19 | 20 | .item:hover { 21 | box-shadow: 0px 4.8px 10.8px rgba(0,0,0,0.13), 0px 0.9px 2.7px rgba(0,0,0,0.11); 22 | } 23 | 24 | .favicon { 25 | display: flex; 26 | height: 40px; 27 | width: 40px; 28 | justify-content: center; 29 | align-items: center; 30 | } 31 | 32 | .favicon img { 33 | width: 16px; 34 | height: 16px; 35 | } 36 | 37 | .label-title, .label-uri { 38 | display: flex; 39 | flex: 1; 40 | min-width: 0; 41 | height: 40px; 42 | align-items: center; 43 | } 44 | 45 | .label-title a { 46 | cursor: pointer; 47 | display: inline-block; 48 | white-space: nowrap; 49 | text-overflow: ellipsis; 50 | font-weight: 600; 51 | font-size: 12px; 52 | line-height: 16px; 53 | color: rgb(16, 16, 16); 54 | border-width: initial; 55 | border-style: none; 56 | border-color: initial; 57 | border-image: initial; 58 | overflow: hidden; 59 | background: none; 60 | text-decoration: none; 61 | } 62 | 63 | .label-title a:hover { 64 | text-decoration: underline; 65 | } 66 | 67 | .label-uri, .label-time { 68 | margin: 0 6px; 69 | line-height: 16px; 70 | font-size: 12px; 71 | color: rgb(115, 115, 115); 72 | } 73 | 74 | .label-uri p, .label-time p { 75 | display: block; 76 | white-space: nowrap; 77 | overflow: hidden; 78 | text-overflow: ellipsis; 79 | } 80 | 81 | .btn-close { 82 | height: 28px; 83 | width: 28px; 84 | margin: 6px; 85 | border-radius: 2px; 86 | background-image: url('img/close.png'); 87 | background-size: 100%; 88 | } 89 | 90 | .btn-close:hover { 91 | background-color: rgb(243, 243, 243); 92 | } 93 | -------------------------------------------------------------------------------- /wvbrowser_ui/content_ui/settings.css: -------------------------------------------------------------------------------- 1 | .settings-entry { 2 | display: block; 3 | height: 48px; 4 | width: 100%; 5 | max-width: 500px; 6 | 7 | background: none; 8 | border: none; 9 | padding: 4px 0; 10 | font: inherit; 11 | cursor: pointer; 12 | } 13 | 14 | #entry-script, #entry-popups { 15 | display: none; 16 | } 17 | 18 | .entry { 19 | display: block; 20 | height: 100%; 21 | text-align: left; 22 | border-radius: 5px; 23 | } 24 | 25 | .entry:hover { 26 | background-color: rgb(220, 220, 220); 27 | } 28 | 29 | .entry:focus { 30 | outline: none; 31 | } 32 | 33 | .entry-name, .entry-value { 34 | display: inline-flex; 35 | height: 100%; 36 | vertical-align: middle; 37 | } 38 | 39 | .entry-name span, .entry-value span { 40 | flex: 1; 41 | align-self: center; 42 | } 43 | 44 | .entry-name { 45 | padding-left: 10px; 46 | } 47 | 48 | .entry-value { 49 | float: right; 50 | vertical-align: middle; 51 | margin: 0 15px; 52 | font-size: 0.8em; 53 | color: gray; 54 | } 55 | -------------------------------------------------------------------------------- /wvbrowser_ui/content_ui/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Settings 4 | 5 | 6 | 7 | 8 | 9 |

Settings

10 |
11 | 21 | 31 | 41 | 51 |
52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /wvbrowser_ui/content_ui/settings.js: -------------------------------------------------------------------------------- 1 | const messageHandler = event => { 2 | var message = event.data.message; 3 | var args = event.data.args; 4 | 5 | switch (message) { 6 | case commands.MG_GET_SETTINGS: 7 | loadSettings(args.settings); 8 | break; 9 | case commands.MG_CLEAR_CACHE: 10 | if (args.content && args.controls) { 11 | updateLabelForEntry('entry-cache', 'Cleared'); 12 | } else { 13 | updateLabelForEntry('entry-cache', 'Try again'); 14 | } 15 | break; 16 | case commands.MG_CLEAR_COOKIES: 17 | if (args.content && args.controls) { 18 | updateLabelForEntry('entry-cookies', 'Cleared'); 19 | } else { 20 | updateLabelForEntry('entry-cookies', 'Try again'); 21 | } 22 | break; 23 | default: 24 | console.log(`Unexpected message: ${JSON.stringify(event.data)}`); 25 | break; 26 | } 27 | }; 28 | 29 | function addEntriesListeners() { 30 | let cacheEntry = document.getElementById('entry-cache'); 31 | cacheEntry.addEventListener('click', function(e) { 32 | let message = { 33 | message: commands.MG_CLEAR_CACHE, 34 | args: {} 35 | }; 36 | 37 | window.chrome.webview.postMessage(message); 38 | }); 39 | 40 | let cookiesEntry = document.getElementById('entry-cookies'); 41 | cookiesEntry.addEventListener('click', function(e) { 42 | let message = { 43 | message: commands.MG_CLEAR_COOKIES, 44 | args: {} 45 | }; 46 | 47 | window.chrome.webview.postMessage(message); 48 | }); 49 | 50 | let scriptEntry = document.getElementById('entry-script'); 51 | scriptEntry.addEventListener('click', function(e) { 52 | // Toggle script support 53 | }); 54 | 55 | let popupsEntry = document.getElementById('entry-popups'); 56 | popupsEntry.addEventListener('click', function(e) { 57 | // Toggle popups 58 | }); 59 | } 60 | 61 | function requestBrowserSettings() { 62 | let message = { 63 | message: commands.MG_GET_SETTINGS, 64 | args: {} 65 | }; 66 | 67 | window.chrome.webview.postMessage(message); 68 | } 69 | 70 | function loadSettings(settings) { 71 | if (settings.scriptsEnabled) { 72 | updateLabelForEntry('entry-script', 'Enabled'); 73 | } else { 74 | updateLabelForEntry('entry-script', 'Disabled'); 75 | } 76 | 77 | if (settings.blockPopups) { 78 | updateLabelForEntry('entry-popups', 'Blocked'); 79 | } else { 80 | updateLabelForEntry('entry-popups', 'Allowed'); 81 | } 82 | } 83 | 84 | function updateLabelForEntry(elementId, label) { 85 | let entryElement = document.getElementById(elementId); 86 | if (!entryElement) { 87 | console.log(`Element with id ${elementId} does not exist`); 88 | return; 89 | } 90 | 91 | let labelSpan = entryElement.querySelector(`.entry-value span`); 92 | 93 | if (!labelSpan) { 94 | console.log(`${elementId} does not have a label`); 95 | return; 96 | } 97 | 98 | labelSpan.textContent = label; 99 | } 100 | 101 | function init() { 102 | window.chrome.webview.addEventListener('message', messageHandler); 103 | requestBrowserSettings(); 104 | addEntriesListeners(); 105 | } 106 | 107 | init(); 108 | -------------------------------------------------------------------------------- /wvbrowser_ui/content_ui/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | border: none; 4 | padding: 0; 5 | 6 | font-family: Arial, Helvetica, sans-serif; 7 | background-color: rgb(240, 240, 242); 8 | } 9 | 10 | p { 11 | margin: 0; 12 | } 13 | 14 | body { 15 | padding: 32px 50px; 16 | font-family: 'system-ui', sans-serif; 17 | } 18 | 19 | .main-title { 20 | display: block; 21 | margin-bottom: 18px; 22 | font-size: 24px; 23 | font-weight: 600; 24 | color: rgb(16, 16, 16); 25 | } 26 | -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/address-bar.css: -------------------------------------------------------------------------------- 1 | #address-bar-container { 2 | display: flex; 3 | height: calc(100% - 10px); 4 | width: 80%; 5 | max-width: calc(100% - 160px); 6 | 7 | background-color: white; 8 | border: 1px solid gray; 9 | border-radius: 5px; 10 | 11 | position: relative; 12 | align-self: center; 13 | } 14 | 15 | #address-bar-container:focus-within { 16 | outline: none; 17 | box-shadow: 0 0 3px dodgerblue; 18 | } 19 | 20 | #address-bar-container:focus-within #btn-clear { 21 | display: block; 22 | } 23 | 24 | #security-label { 25 | display: inline-flex; 26 | height: 100%; 27 | margin-left: 2px; 28 | 29 | vertical-align: top; 30 | } 31 | 32 | #security-label span { 33 | font-family: Arial; 34 | font-size: 0.9em; 35 | color: gray; 36 | vertical-align: middle; 37 | flex: 1; 38 | align-self: center; 39 | text-align: left; 40 | padding-left: 5px; 41 | white-space: nowrap; 42 | } 43 | 44 | .icn { 45 | display: inline-block; 46 | margin: 2px 0; 47 | border-radius: 5px; 48 | top: 0; 49 | width: 26px; 50 | height: 26px; 51 | } 52 | 53 | #icn-lock { 54 | background-size: 100%; 55 | } 56 | 57 | #security-label.label-unknown .icn { 58 | background-image: url('img/unknown.png'); 59 | } 60 | 61 | #security-label.label-insecure .icn { 62 | background-image: url('img/insecure.png'); 63 | } 64 | 65 | #security-label.label-insecure span { 66 | color: rgb(192, 0, 0); 67 | } 68 | 69 | #security-label.label-neutral .icn { 70 | background-image: url('img/neutral.png'); 71 | } 72 | 73 | #security-label.label-secure .icn { 74 | background-image: url('img/secure.png'); 75 | } 76 | 77 | #security-label.label-secure span, #security-label.label-neutral span { 78 | display: none; 79 | } 80 | 81 | #icn-favicon { 82 | background-size: 100%; 83 | } 84 | 85 | #img-favicon { 86 | width: 18px; 87 | height: 18px; 88 | padding: 4px; 89 | } 90 | 91 | #address-form { 92 | margin: 0; 93 | } 94 | 95 | #address-field { 96 | flex: 1; 97 | padding: 0; 98 | border: none; 99 | border-radius: 5px; 100 | margin: 0; 101 | 102 | line-height: 30px; 103 | width: 100%; 104 | } 105 | 106 | #address-field:focus { 107 | outline: none; 108 | } 109 | 110 | #btn-fav { 111 | margin: 2px 5px; 112 | background-size: 100%; 113 | background-image: url('img/favorite.png'); 114 | } 115 | 116 | #btn-fav:hover, #btn-clear:hover { 117 | background-color: rgb(230, 230, 230); 118 | } 119 | 120 | #btn-fav.favorited { 121 | background-image: url('img/favorited.png'); 122 | } 123 | 124 | #btn-clear { 125 | display: none; 126 | width: 16px; 127 | height: 16px; 128 | border: none; 129 | align-self: center; 130 | background-color: transparent; 131 | background-image: url(img/cancel.png); 132 | background-size: 100%; 133 | border: none; 134 | border-radius: 8px; 135 | } 136 | -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/controls.css: -------------------------------------------------------------------------------- 1 | #controls-bar { 2 | display: flex; 3 | justify-content: space-between; 4 | flex-direction: row; 5 | height: 40px; 6 | background-color: rgb(230, 230, 230); 7 | } 8 | 9 | .btn, .btn-disabled, .btn-cancel, .btn-active { 10 | display: inline-block; 11 | border: none; 12 | margin: 5px 0; 13 | border-radius: 5px; 14 | outline: none; 15 | height: 30px; 16 | width: 30px; 17 | 18 | background-size: 100%; 19 | } 20 | 21 | #btn-forward { 22 | background-image: url('img/goForward.png'); 23 | } 24 | 25 | .btn-disabled#btn-forward { 26 | background-image: url('img/goForward_disabled.png'); 27 | } 28 | 29 | #btn-back { 30 | background-image: url('img/goBack.png'); 31 | } 32 | 33 | .btn-disabled#btn-back { 34 | background-image: url('img/goBack_disabled.png'); 35 | } 36 | 37 | #btn-reload { 38 | background-image: url('img/reload.png'); 39 | } 40 | 41 | .btn-cancel#btn-reload { 42 | background-image: url('img/cancel.png'); 43 | } 44 | 45 | #btn-options { 46 | background-image: url('img/options.png'); 47 | } 48 | 49 | .controls-group { 50 | display: inline-block; 51 | height: 40px; 52 | } 53 | 54 | #nav-controls-container { 55 | align-self: flex-start; 56 | padding-left: 10px; 57 | } 58 | 59 | #manage-controls-container { 60 | align-self: flex-end; 61 | padding-right: 10px; 62 | } 63 | 64 | .btn:hover, .btn-cancel:hover, .btn-active { 65 | background-color: rgb(200, 200, 200); 66 | } 67 | -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/default.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 70px; 3 | } 4 | -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/default.js: -------------------------------------------------------------------------------- 1 | const WORD_REGEX = /^[^//][^.]*$/; 2 | const VALID_URI_REGEX = /^[-:.&#+()[\]$'*;@~!,?%=\/\w]+$/; // Will check that only RFC3986 allowed characters are included 3 | const SCHEMED_URI_REGEX = /^\w+:.+$/; 4 | 5 | let settings = { 6 | scriptsEnabled: true, 7 | blockPopups: true 8 | }; 9 | 10 | const messageHandler = event => { 11 | var message = event.data.message; 12 | var args = event.data.args; 13 | 14 | switch (message) { 15 | case commands.MG_UPDATE_URI: 16 | if (isValidTabId(args.tabId)) { 17 | const tab = tabs.get(args.tabId); 18 | let previousURI = tab.uri; 19 | 20 | // Update the tab state 21 | tab.uri = args.uri; 22 | tab.uriToShow = args.uriToShow; 23 | tab.canGoBack = args.canGoBack; 24 | tab.canGoForward = args.canGoForward; 25 | 26 | // If the tab is active, update the controls UI 27 | if (args.tabId == activeTabId) { 28 | updateNavigationUI(message); 29 | } 30 | 31 | isFavorite(tab.uri, (isFavorite) => { 32 | tab.isFavorite = isFavorite; 33 | updateFavoriteIcon(); 34 | }); 35 | 36 | // Don't add history entry if URI has not changed 37 | if (tab.uri == previousURI) { 38 | break; 39 | } 40 | 41 | // Filter URIs that should not appear in history 42 | if (!tab.uri || tab.uri == 'about:blank') { 43 | tab.historyItemId = INVALID_HISTORY_ID; 44 | break; 45 | } 46 | 47 | if (tab.uriToShow && tab.uriToShow.substring(0, 10) == 'browser://') { 48 | tab.historyItemId = INVALID_HISTORY_ID; 49 | break; 50 | } 51 | 52 | addHistoryItem(historyItemFromTab(args.tabId), (id) => { 53 | tab.historyItemId = id; 54 | }); 55 | } 56 | break; 57 | case commands.MG_NAV_STARTING: 58 | if (isValidTabId(args.tabId)) { 59 | // Update the tab state 60 | tabs.get(args.tabId).isLoading = true; 61 | 62 | // If the tab is active, update the controls UI 63 | if (args.tabId == activeTabId) { 64 | updateNavigationUI(message); 65 | } 66 | } 67 | break; 68 | case commands.MG_NAV_COMPLETED: 69 | if (isValidTabId(args.tabId)) { 70 | // Update tab state 71 | tabs.get(args.tabId).isLoading = false; 72 | 73 | // If the tab is active, update the controls UI 74 | if (args.tabId == activeTabId) { 75 | updateNavigationUI(message); 76 | } 77 | } 78 | break; 79 | case commands.MG_UPDATE_TAB: 80 | if (isValidTabId(args.tabId)) { 81 | const tab = tabs.get(args.tabId); 82 | const tabElement = document.getElementById(`tab-${args.tabId}`); 83 | 84 | if (!tabElement) { 85 | refreshTabs(); 86 | return; 87 | } 88 | 89 | // Update tab label 90 | // Use given title or fall back to a generic tab title 91 | tab.title = args.title || 'Tab'; 92 | const tabLabel = tabElement.firstChild; 93 | const tabLabelSpan = tabLabel.firstChild; 94 | tabLabelSpan.textContent = tab.title; 95 | 96 | // Update title in history item 97 | // Browser pages will keep an invalid history ID 98 | if (tab.historyItemId != INVALID_HISTORY_ID) { 99 | updateHistoryItem(tab.historyItemId, historyItemFromTab(args.tabId)); 100 | } 101 | } 102 | break; 103 | case commands.MG_OPTIONS_LOST_FOCUS: 104 | let optionsButton = document.getElementById('btn-options'); 105 | if (optionsButton) { 106 | if (optionsButton.className = 'btn-active') { 107 | toggleOptionsDropdown(); 108 | } 109 | } 110 | break; 111 | case commands.MG_SECURITY_UPDATE: 112 | if (isValidTabId(args.tabId)) { 113 | const tab = tabs.get(args.tabId); 114 | tab.securityState = args.state; 115 | 116 | if (args.tabId == activeTabId) { 117 | updateNavigationUI(message); 118 | } 119 | } 120 | break; 121 | case commands.MG_UPDATE_FAVICON: 122 | if (isValidTabId(args.tabId)) { 123 | updateFaviconURI(args.tabId, args.uri); 124 | } 125 | break; 126 | case commands.MG_CLOSE_WINDOW: 127 | closeWindow(); 128 | break; 129 | case commands.MG_CLOSE_TAB: 130 | if (isValidTabId(args.tabId)) { 131 | closeTab(args.tabId); 132 | } 133 | break; 134 | case commands.MG_GET_FAVORITES: 135 | if (isValidTabId(args.tabId)) { 136 | getFavoritesAsJson((payload) => { 137 | args.favorites = payload; 138 | window.chrome.webview.postMessage(event.data); 139 | }); 140 | } 141 | break; 142 | case commands.MG_REMOVE_FAVORITE: 143 | removeFavorite(args.uri); 144 | break; 145 | case commands.MG_GET_SETTINGS: 146 | if (isValidTabId(args.tabId)) { 147 | args.settings = settings; 148 | window.chrome.webview.postMessage(event.data); 149 | } 150 | break; 151 | case commands.MG_GET_HISTORY: 152 | if (isValidTabId(args.tabId)) { 153 | getHistoryItems(args.from, args.count, (payload) => { 154 | args.items = payload; 155 | window.chrome.webview.postMessage(event.data); 156 | }); 157 | } 158 | break; 159 | case commands.MG_REMOVE_HISTORY_ITEM: 160 | removeHistoryItem(args.id); 161 | break; 162 | case commands.MG_CLEAR_HISTORY: 163 | clearHistory(); 164 | break; 165 | default: 166 | console.log(`Received unexpected message: ${JSON.stringify(event.data)}`); 167 | } 168 | }; 169 | 170 | function processAddressBarInput() { 171 | var text = document.querySelector('#address-field').value; 172 | tryNavigate(text); 173 | } 174 | 175 | function tryNavigate(text) { 176 | try { 177 | var uriParser = new URL(text); 178 | 179 | // URL creation succeeded, verify protocol is allowed 180 | switch (uriParser.protocol) { 181 | case 'http:': 182 | case 'https:': 183 | case 'file:': 184 | case 'ftp:': 185 | // Allowed protocol, navigate 186 | navigateActiveTab(uriParser.href, false); 187 | break; 188 | default: 189 | // Protocol not allowed, search Bing 190 | navigateActiveTab(getSearchURI(text), true); 191 | break; 192 | } 193 | } catch (e) { 194 | // URL creation failed, check for invalid characters 195 | if (containsIlegalCharacters(text) || isSingleWord(text)) { 196 | // Search Bing 197 | navigateActiveTab(getSearchURI(text), true); 198 | } else { 199 | // Try with HTTP 200 | if (!hasScheme(text)) { 201 | tryNavigate(`http:${text}`); 202 | } else { 203 | navigateActiveTab(getSearchURI(text), true); 204 | } 205 | } 206 | } 207 | } 208 | 209 | function navigateActiveTab(uri, isSearch) { 210 | var message = { 211 | message: commands.MG_NAVIGATE, 212 | args: { 213 | uri: uri, 214 | encodedSearchURI: isSearch ? uri : getSearchURI(uri) 215 | } 216 | }; 217 | 218 | window.chrome.webview.postMessage(message); 219 | } 220 | 221 | function reloadActiveTabContent() { 222 | var message = { 223 | message: commands.MG_RELOAD, 224 | args: {} 225 | }; 226 | window.chrome.webview.postMessage(message); 227 | } 228 | 229 | function containsIlegalCharacters(query) { 230 | return !VALID_URI_REGEX.test(query); 231 | } 232 | 233 | function isSingleWord(query) { 234 | return WORD_REGEX.test(query); 235 | } 236 | 237 | function hasScheme(query) { 238 | return SCHEMED_URI_REGEX.test(query); 239 | } 240 | 241 | function getSearchURI(query) { 242 | return `https://www.bing.com/search?q=${encodeURIComponent(query)}`; 243 | } 244 | 245 | function closeWindow() { 246 | var message = { 247 | message: commands.MG_CLOSE_WINDOW, 248 | args: {} 249 | }; 250 | 251 | window.chrome.webview.postMessage(message); 252 | } 253 | 254 | // Show active tab's URI in the address bar 255 | function updateURI() { 256 | if (activeTabId == INVALID_TAB_ID) { 257 | return; 258 | } 259 | 260 | let activeTab = tabs.get(activeTabId); 261 | document.getElementById('address-field').value = activeTab.uriToShow || activeTab.uri; 262 | } 263 | 264 | // Show active tab's favicon in the address bar 265 | function updateFavicon() { 266 | if (activeTabId == INVALID_TAB_ID) { 267 | return; 268 | } 269 | 270 | let activeTab = tabs.get(activeTabId); 271 | 272 | let faviconElement = document.getElementById('img-favicon'); 273 | if (!faviconElement) { 274 | refreshControls(); 275 | return; 276 | } 277 | 278 | faviconElement.src = activeTab.favicon; 279 | 280 | } 281 | 282 | // Update back and forward buttons for the active tab 283 | function updateBackForwardButtons() { 284 | if (activeTabId == INVALID_TAB_ID) { 285 | return; 286 | } 287 | 288 | let activeTab = tabs.get(activeTabId); 289 | let btnForward = document.getElementById('btn-forward'); 290 | let btnBack = document.getElementById('btn-back'); 291 | 292 | if (!btnBack || !btnForward) { 293 | refreshControls(); 294 | return; 295 | } 296 | 297 | if (activeTab.canGoForward) 298 | btnForward.className = 'btn'; 299 | else 300 | btnForward.className = 'btn-disabled'; 301 | 302 | if (activeTab.canGoBack) 303 | btnBack.className = 'btn'; 304 | else 305 | btnBack.className = 'btn-disabled'; 306 | } 307 | 308 | // Update reload button for the active tab 309 | function updateReloadButton() { 310 | if (activeTabId == INVALID_TAB_ID) { 311 | return; 312 | } 313 | 314 | let activeTab = tabs.get(activeTabId); 315 | 316 | let btnReload = document.getElementById('btn-reload'); 317 | if (!btnReload) { 318 | refreshControls(); 319 | return; 320 | } 321 | 322 | btnReload.className = activeTab.isLoading ? 'btn-cancel' : 'btn'; 323 | } 324 | 325 | // Update lock icon for the active tab 326 | function updateLockIcon() { 327 | if (activeTabId == INVALID_TAB_ID) { 328 | return; 329 | } 330 | 331 | let activeTab = tabs.get(activeTabId); 332 | 333 | let labelElement = document.getElementById('security-label'); 334 | if (!labelElement) { 335 | refreshControls(); 336 | return; 337 | } 338 | 339 | switch (activeTab.securityState) { 340 | case 'insecure': 341 | labelElement.className = 'label-insecure'; 342 | break; 343 | case 'neutral': 344 | labelElement.className = 'label-neutral'; 345 | break; 346 | case 'secure': 347 | labelElement.className = 'label-secure'; 348 | break; 349 | default: 350 | labelElement.className = 'label-unknown'; 351 | break; 352 | } 353 | } 354 | 355 | // Update favorite status for the active tab 356 | function updateFavoriteIcon() { 357 | if (activeTabId == INVALID_TAB_ID) { 358 | return; 359 | } 360 | 361 | let activeTab = tabs.get(activeTabId); 362 | isFavorite(activeTab.uri, (isFavorite) => { 363 | let favoriteElement = document.getElementById('btn-fav'); 364 | if (!favoriteElement) { 365 | refreshControls(); 366 | return; 367 | } 368 | 369 | if (isFavorite) { 370 | favoriteElement.classList.add('favorited'); 371 | activeTab.isFavorite = true; 372 | } else { 373 | favoriteElement.classList.remove('favorited'); 374 | activeTab.isFavorite = false; 375 | } 376 | }); 377 | 378 | } 379 | 380 | function updateNavigationUI(reason) { 381 | switch (reason) { 382 | case commands.MG_UPDATE_URI: 383 | updateURI(); 384 | updateFavoriteIcon(); 385 | updateBackForwardButtons(); 386 | break; 387 | case commands.MG_NAV_COMPLETED: 388 | case commands.MG_NAV_STARTING: 389 | updateReloadButton(); 390 | break; 391 | case commands.MG_SECURITY_UPDATE: 392 | updateLockIcon(); 393 | break; 394 | case commands.MG_UPDATE_FAVICON: 395 | updateFavicon(); 396 | break; 397 | // If a reason is not provided (for requests not originating from a 398 | // message), default to switch tab behavior. 399 | default: 400 | case commands.MG_SWITCH_TAB: 401 | updateURI(); 402 | updateLockIcon(); 403 | updateFavicon(); 404 | updateFavoriteIcon(); 405 | updateReloadButton(); 406 | updateBackForwardButtons(); 407 | break; 408 | } 409 | } 410 | 411 | function loadTabUI(tabId) { 412 | if (isValidTabId(tabId)) { 413 | let tab = tabs.get(tabId); 414 | 415 | let tabElement = document.createElement('div'); 416 | tabElement.className = tabId == activeTabId ? 'tab-active' : 'tab'; 417 | tabElement.id = `tab-${tabId}`; 418 | 419 | let tabLabel = document.createElement('div'); 420 | tabLabel.className = 'tab-label'; 421 | 422 | let labelText = document.createElement('span'); 423 | labelText.textContent = tab.title; 424 | tabLabel.appendChild(labelText); 425 | 426 | let closeButton = document.createElement('div'); 427 | closeButton.className = 'btn-tab-close'; 428 | closeButton.addEventListener('click', function(e) { 429 | closeTab(tabId); 430 | }); 431 | 432 | tabElement.appendChild(tabLabel); 433 | tabElement.appendChild(closeButton); 434 | 435 | var createTabButton = document.getElementById('btn-new-tab'); 436 | document.getElementById('tabs-strip').insertBefore(tabElement, createTabButton); 437 | 438 | tabElement.addEventListener('click', function(e) { 439 | if (e.srcElement.className != 'btn-tab-close') { 440 | switchToTab(tabId, true); 441 | } 442 | }); 443 | } 444 | } 445 | 446 | function toggleOptionsDropdown() { 447 | const optionsButtonElement = document.getElementById('btn-options'); 448 | const elementClass = optionsButtonElement.className; 449 | 450 | var message; 451 | if (elementClass === 'btn') { 452 | // Update UI 453 | optionsButtonElement.className = 'btn-active'; 454 | 455 | message = { 456 | message: commands.MG_SHOW_OPTIONS, 457 | args: {} 458 | }; 459 | } else { 460 | // Update UI 461 | optionsButtonElement.className = 'btn'; 462 | 463 | message = { 464 | message:commands.MG_HIDE_OPTIONS, 465 | args: {} 466 | }; 467 | } 468 | 469 | window.chrome.webview.postMessage(message); 470 | } 471 | 472 | function refreshControls() { 473 | let controlsElement = document.getElementById('controls-bar'); 474 | if (controlsElement) { 475 | controlsElement.remove(); 476 | } 477 | 478 | controlsElement = document.createElement('div'); 479 | controlsElement.id = 'controls-bar'; 480 | 481 | // Navigation controls 482 | let navControls = document.createElement('div'); 483 | navControls.className = 'controls-group'; 484 | navControls.id = 'nav-controls-container'; 485 | 486 | let backButton = document.createElement('div'); 487 | backButton.className = 'btn-disabled'; 488 | backButton.id = 'btn-back'; 489 | navControls.append(backButton); 490 | 491 | let forwardButton = document.createElement('div'); 492 | forwardButton.className = 'btn-disabled'; 493 | forwardButton.id = 'btn-forward'; 494 | navControls.append(forwardButton); 495 | 496 | let reloadButton = document.createElement('div'); 497 | reloadButton.className = 'btn'; 498 | reloadButton.id = 'btn-reload'; 499 | navControls.append(reloadButton); 500 | 501 | controlsElement.append(navControls); 502 | 503 | // Address bar 504 | let addressBar = document.createElement('div'); 505 | addressBar.id = 'address-bar-container'; 506 | 507 | let securityLabel = document.createElement('div'); 508 | securityLabel.className = 'label-unknown'; 509 | securityLabel.id = 'security-label'; 510 | 511 | let labelSpan = document.createElement('span'); 512 | labelSpan.textContent = 'Not secure'; 513 | securityLabel.append(labelSpan); 514 | 515 | let lockIcon = document.createElement('div'); 516 | lockIcon.className = 'icn'; 517 | lockIcon.id = 'icn-lock'; 518 | securityLabel.append(lockIcon); 519 | addressBar.append(securityLabel); 520 | 521 | let faviconElement = document.createElement('div'); 522 | faviconElement.className = 'icn'; 523 | faviconElement.id = 'icn-favicon'; 524 | 525 | let faviconImage = document.createElement('img'); 526 | faviconImage.id = 'img-favicon'; 527 | faviconImage.src = 'img/favicon.png'; 528 | faviconElement.append(faviconImage); 529 | addressBar.append(faviconElement); 530 | 531 | let addressInput = document.createElement('input'); 532 | addressInput.id = 'address-field'; 533 | addressInput.placeholder = 'Search or enter web address'; 534 | addressInput.type = 'text'; 535 | addressInput.spellcheck = false; 536 | addressBar.append(addressInput); 537 | 538 | let clearButton = document.createElement('button'); 539 | clearButton.id = 'btn-clear'; 540 | addressBar.append(clearButton); 541 | 542 | let favoriteButton = document.createElement('div'); 543 | favoriteButton.className = 'icn'; 544 | favoriteButton.id = 'btn-fav'; 545 | addressBar.append(favoriteButton); 546 | controlsElement.append(addressBar); 547 | 548 | // Manage controls 549 | let manageControls = document.createElement('div'); 550 | manageControls.className = 'controls-group'; 551 | manageControls.id = 'manage-controls-container'; 552 | 553 | let optionsButton = document.createElement('div'); 554 | optionsButton.className = 'btn'; 555 | optionsButton.id = 'btn-options'; 556 | manageControls.append(optionsButton); 557 | controlsElement.append(manageControls); 558 | 559 | // Insert controls bar into document 560 | let tabsElement = document.getElementById('tabs-strip'); 561 | if (tabsElement) { 562 | tabsElement.parentElement.insertBefore(controlsElement, tabsElement); 563 | } else { 564 | let bodyElement = document.getElementsByTagName('body')[0]; 565 | bodyElement.append(controlsElement); 566 | } 567 | 568 | addControlsListeners(); 569 | updateNavigationUI(); 570 | } 571 | 572 | function refreshTabs() { 573 | let tabsStrip = document.getElementById('tabs-strip'); 574 | if (tabsStrip) { 575 | tabsStrip.remove(); 576 | } 577 | 578 | tabsStrip = document.createElement('div'); 579 | tabsStrip.id = 'tabs-strip'; 580 | 581 | let newTabButton = document.createElement('div'); 582 | newTabButton.id = 'btn-new-tab'; 583 | 584 | let buttonSpan = document.createElement('span'); 585 | buttonSpan.textContent = '+'; 586 | buttonSpan.id = 'plus-label'; 587 | newTabButton.append(buttonSpan); 588 | tabsStrip.append(newTabButton); 589 | 590 | let bodyElement = document.getElementsByTagName('body')[0]; 591 | bodyElement.append(tabsStrip); 592 | 593 | addTabsListeners(); 594 | 595 | Array.from(tabs).map((tabEntry) => { 596 | loadTabUI(tabEntry[0]); 597 | }); 598 | } 599 | 600 | function toggleFavorite() { 601 | activeTab = tabs.get(activeTabId); 602 | if (activeTab.isFavorite) { 603 | removeFavorite(activeTab.uri, () => { 604 | activeTab.isFavorite = false; 605 | updateFavoriteIcon(); 606 | }); 607 | } else { 608 | addFavorite(favoriteFromTab(activeTabId), () => { 609 | activeTab.isFavorite = true; 610 | updateFavoriteIcon(); 611 | }); 612 | } 613 | } 614 | 615 | function addControlsListeners() { 616 | let inputField = document.querySelector('#address-field'); 617 | let clearButton = document.querySelector('#btn-clear'); 618 | 619 | inputField.addEventListener('keypress', function(e) { 620 | var key = e.which || e.keyCode; 621 | if (key === 13) { // 13 is enter 622 | e.preventDefault(); 623 | processAddressBarInput(); 624 | } 625 | }); 626 | 627 | inputField.addEventListener('focus', function(e) { 628 | e.target.select(); 629 | }); 630 | 631 | inputField.addEventListener('blur', function(e) { 632 | inputField.setSelectionRange(0, 0); 633 | if (!inputField.value) { 634 | updateURI(); 635 | } 636 | }); 637 | 638 | clearButton.addEventListener('click', function(e) { 639 | inputField.value = ''; 640 | inputField.focus(); 641 | e.preventDefault(); 642 | }); 643 | 644 | document.querySelector('#btn-forward').addEventListener('click', function(e) { 645 | if (document.getElementById('btn-forward').className === 'btn') { 646 | var message = { 647 | message: commands.MG_GO_FORWARD, 648 | args: {} 649 | }; 650 | window.chrome.webview.postMessage(message); 651 | } 652 | }); 653 | 654 | document.querySelector('#btn-back').addEventListener('click', function(e) { 655 | if (document.getElementById('btn-back').className === 'btn') { 656 | var message = { 657 | message: commands.MG_GO_BACK, 658 | args: {} 659 | }; 660 | window.chrome.webview.postMessage(message); 661 | } 662 | }); 663 | 664 | document.querySelector('#btn-reload').addEventListener('click', function(e) { 665 | var btnReload = document.getElementById('btn-reload'); 666 | if (btnReload.className === 'btn-cancel') { 667 | var message = { 668 | message: commands.MG_CANCEL, 669 | args: {} 670 | }; 671 | window.chrome.webview.postMessage(message); 672 | } else if (btnReload.className === 'btn') { 673 | reloadActiveTabContent(); 674 | } 675 | }); 676 | 677 | document.querySelector('#btn-options').addEventListener('click', function(e) { 678 | toggleOptionsDropdown(); 679 | }); 680 | 681 | window.onkeydown = function(event) { 682 | if (event.ctrlKey) { 683 | switch (event.key) { 684 | case 'r': 685 | case 'R': 686 | reloadActiveTabContent(); 687 | break; 688 | case 'd': 689 | case 'D': 690 | toggleFavorite(); 691 | break; 692 | case 't': 693 | case 'T': 694 | createNewTab(true); 695 | break; 696 | case 'p': 697 | case 'P': 698 | case '+': 699 | case '-': 700 | case '_': 701 | case '=': 702 | break; 703 | default: 704 | return; 705 | } 706 | 707 | event.preventDefault(); 708 | } 709 | }; 710 | 711 | // Prevent zooming the UI 712 | window.addEventListener('wheel', function(event) { 713 | if (event.ctrlKey) { 714 | event.preventDefault(); 715 | }}, { passive: false }); 716 | } 717 | 718 | function addTabsListeners() { 719 | document.querySelector('#btn-new-tab').addEventListener('click', function(e) { 720 | createNewTab(true); 721 | }); 722 | 723 | document.querySelector('#btn-fav').addEventListener('click', function(e) { 724 | toggleFavorite(); 725 | }); 726 | } 727 | 728 | function init() { 729 | window.chrome.webview.addEventListener('message', messageHandler); 730 | refreshControls(); 731 | refreshTabs(); 732 | 733 | createNewTab(true); 734 | } 735 | 736 | init(); 737 | -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/favorites.js: -------------------------------------------------------------------------------- 1 | function isFavorite(uri, callback) { 2 | queryDB((db) => { 3 | let transaction = db.transaction(['favorites']); 4 | let favoritesStore = transaction.objectStore('favorites'); 5 | let favoriteStatusRequest = favoritesStore.get(uri); 6 | 7 | favoriteStatusRequest.onerror = function(event) { 8 | console.log(`Could not query for ${uri}: ${event.target.error.message}`); 9 | }; 10 | 11 | favoriteStatusRequest.onsuccess = function() { 12 | callback(favoriteStatusRequest.result); 13 | }; 14 | }); 15 | } 16 | 17 | function addFavorite(favorite, callback) { 18 | queryDB((db) => { 19 | let transaction = db.transaction(['favorites'], 'readwrite'); 20 | let favoritesStore = transaction.objectStore('favorites'); 21 | let addFavoriteRequest = favoritesStore.add(favorite); 22 | 23 | addFavoriteRequest.onerror = function(event) { 24 | console.log(`Could not add favorite with key: ${favorite.uri}`); 25 | console.log(event.target.error.message); 26 | }; 27 | 28 | addFavoriteRequest.onsuccess = function(event) { 29 | if (callback) { 30 | callback(); 31 | } 32 | }; 33 | }); 34 | } 35 | 36 | function removeFavorite(key, callback) { 37 | queryDB((db) => { 38 | let transaction = db.transaction(['favorites'], 'readwrite'); 39 | let favoritesStore = transaction.objectStore('favorites'); 40 | let removeFavoriteRequest = favoritesStore.delete(key); 41 | 42 | removeFavoriteRequest.onerror = function(event) { 43 | console.log(`Could not remove favorite with key: ${key}`); 44 | console.log(event.target.error.message); 45 | }; 46 | 47 | removeFavoriteRequest.onsuccess = function(event) { 48 | if (callback) { 49 | callback(); 50 | } 51 | }; 52 | }); 53 | } 54 | 55 | function getFavoritesAsJson(callback) { 56 | queryDB((db) => { 57 | let transaction = db.transaction(['favorites']); 58 | let favoritesStore = transaction.objectStore('favorites'); 59 | let getFavoritesRequest = favoritesStore.getAll(); 60 | 61 | getFavoritesRequest.onerror = function(event) { 62 | console.log(`Could retrieve favorites`); 63 | console.log(event.target.error.message); 64 | }; 65 | 66 | getFavoritesRequest.onsuccess = function(event) { 67 | if (callback) { 68 | callback(getFavoritesRequest.result); 69 | } 70 | }; 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/history.js: -------------------------------------------------------------------------------- 1 | const INVALID_HISTORY_ID = -1; 2 | 3 | function addHistoryItem(item, callback) { 4 | queryDB((db) => { 5 | let transaction = db.transaction(['history'], 'readwrite'); 6 | let historyStore = transaction.objectStore('history'); 7 | 8 | // Check if an item for this URI exists on this day 9 | let currentDate = new Date(); 10 | let year = currentDate.getFullYear(); 11 | let month = currentDate.getMonth(); 12 | let date = currentDate.getDate(); 13 | let todayDate = new Date(year, month, date); 14 | 15 | let existingItemsIndex = historyStore.index('stampedURI'); 16 | let lowerBound = [item.uri, todayDate]; 17 | let upperBound = [item.uri, currentDate]; 18 | let range = IDBKeyRange.bound(lowerBound, upperBound); 19 | let request = existingItemsIndex.openCursor(range); 20 | 21 | request.onsuccess = function(event) { 22 | let cursor = event.target.result; 23 | if (cursor) { 24 | // There's an entry for this URI, update the item 25 | cursor.value.timestamp = item.timestamp; 26 | let updateRequest = cursor.update(cursor.value); 27 | 28 | updateRequest.onsuccess = function(event) { 29 | if (callback) { 30 | callback(event.target.result.primaryKey); 31 | } 32 | }; 33 | } else { 34 | // No entry for this URI, add item 35 | let addItemRequest = historyStore.add(item); 36 | 37 | addItemRequest.onsuccess = function(event) { 38 | if (callback) { 39 | callback(event.target.result); 40 | } 41 | }; 42 | } 43 | }; 44 | 45 | }); 46 | } 47 | 48 | function updateHistoryItem(id, item, callback) { 49 | if (!id) { 50 | return; 51 | } 52 | 53 | queryDB((db) => { 54 | let transaction = db.transaction(['history'], 'readwrite'); 55 | let historyStore = transaction.objectStore('history'); 56 | let storedItemRequest = historyStore.get(id); 57 | storedItemRequest.onsuccess = function(event) { 58 | let storedItem = event.target.result; 59 | item.timestamp = storedItem.timestamp; 60 | 61 | let updateRequest = historyStore.put(item, id); 62 | 63 | updateRequest.onsuccess = function(event) { 64 | if (callback) { 65 | callback(); 66 | } 67 | }; 68 | }; 69 | 70 | }); 71 | } 72 | 73 | function getHistoryItems(from, n, callback) { 74 | if (n <= 0 || from < 0) { 75 | if (callback) { 76 | callback([]); 77 | } 78 | } 79 | 80 | queryDB((db) => { 81 | let transaction = db.transaction(['history']); 82 | let historyStore = transaction.objectStore('history'); 83 | let timestampIndex = historyStore.index('timestamp'); 84 | let cursorRequest = timestampIndex.openCursor(null, 'prev'); 85 | 86 | let current = 0; 87 | let items = []; 88 | cursorRequest.onsuccess = function(event) { 89 | let cursor = event.target.result; 90 | 91 | if (!cursor || current >= from + n) { 92 | if (callback) { 93 | callback(items); 94 | } 95 | 96 | return; 97 | } 98 | 99 | if (current >= from) { 100 | items.push({ 101 | id: cursor.primaryKey, 102 | item: cursor.value 103 | }); 104 | } 105 | 106 | ++current; 107 | cursor.continue(); 108 | }; 109 | }); 110 | } 111 | 112 | function removeHistoryItem(id, callback) { 113 | queryDB((db) => { 114 | let transaction = db.transaction(['history'], 'readwrite'); 115 | let historyStore = transaction.objectStore('history'); 116 | let removeItemRequest = historyStore.delete(id); 117 | 118 | removeItemRequest.onerror = function(event) { 119 | console.log(`Could not remove history item with ID: ${id}`); 120 | console.log(event.target.error.message); 121 | }; 122 | 123 | removeItemRequest.onsuccess = function(event) { 124 | if (callback) { 125 | callback(); 126 | } 127 | }; 128 | }); 129 | } 130 | 131 | function clearHistory(callback) { 132 | queryDB((db) => { 133 | let transaction = db.transaction(['history'], 'readwrite'); 134 | let historyStore = transaction.objectStore('history'); 135 | let clearRequest = historyStore.clear(); 136 | 137 | clearRequest.onsuccess = function(event) { 138 | if (callback) { 139 | callback(); 140 | } 141 | }; 142 | }); 143 | } 144 | -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/img/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/wvbrowser_ui/controls_ui/img/cancel.png -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/wvbrowser_ui/controls_ui/img/favicon.png -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/img/favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/wvbrowser_ui/controls_ui/img/favorite.png -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/img/favorited.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/wvbrowser_ui/controls_ui/img/favorited.png -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/img/goBack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/wvbrowser_ui/controls_ui/img/goBack.png -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/img/goBack_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/wvbrowser_ui/controls_ui/img/goBack_disabled.png -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/img/goForward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/wvbrowser_ui/controls_ui/img/goForward.png -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/img/goForward_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/wvbrowser_ui/controls_ui/img/goForward_disabled.png -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/img/insecure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/wvbrowser_ui/controls_ui/img/insecure.png -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/img/neutral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/wvbrowser_ui/controls_ui/img/neutral.png -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/img/options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/wvbrowser_ui/controls_ui/img/options.png -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/img/reload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/wvbrowser_ui/controls_ui/img/reload.png -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/img/secure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/wvbrowser_ui/controls_ui/img/secure.png -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/img/unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/WebView2Browser/5b03bc945d77241cf27294f7a9205a4092857a06/wvbrowser_ui/controls_ui/img/unknown.png -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/options.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 200px; 3 | height: 107px; 4 | } 5 | 6 | #dropdown-wrapper { 7 | width: calc(100% - 2px); 8 | height: calc(100% - 2px); 9 | border: 1px solid gray; 10 | } 11 | 12 | .dropdown-item { 13 | background-color: rgb(240, 240, 240); 14 | } 15 | 16 | .dropdown-item:hover { 17 | background-color: rgb(220, 220, 220); 18 | } 19 | 20 | .item-label { 21 | display: flex; 22 | height: 35px; 23 | text-align: center; 24 | vertical-align: middle; 25 | 26 | white-space: nowrap; 27 | overflow: hidden; 28 | } 29 | 30 | .item-label span { 31 | font-family: Arial; 32 | font-size: 0.8em; 33 | vertical-align: middle; 34 | flex: 1; 35 | align-self: center; 36 | text-align: left; 37 | padding-left: 10px; 38 | } 39 | -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/options.js: -------------------------------------------------------------------------------- 1 | function navigateToBrowserPage(path) { 2 | const navMessage = { 3 | message: commands.MG_NAVIGATE, 4 | args: { 5 | uri: `browser://${path}` 6 | } 7 | }; 8 | 9 | window.chrome.webview.postMessage(navMessage); 10 | } 11 | 12 | // Add listener for the options menu entries 13 | function addItemsListeners() { 14 | 15 | // Close dropdown when item is selected 16 | (() => { 17 | const dropdownItems = Array.from(document.getElementsByClassName('dropdown-item')); 18 | dropdownItems.map(item => { 19 | item.addEventListener('click', function(e) { 20 | const closeMessage = { 21 | message: commands.MG_OPTION_SELECTED, 22 | args: {} 23 | }; 24 | window.chrome.webview.postMessage(closeMessage); 25 | }); 26 | 27 | // Navigate to browser page 28 | let entry = item.id.split('-')[1]; 29 | switch (entry) { 30 | case 'settings': 31 | case 'history': 32 | case 'favorites': 33 | item.addEventListener('click', function(e) { 34 | navigateToBrowserPage(entry); 35 | }); 36 | break; 37 | } 38 | }); 39 | })(); 40 | } 41 | 42 | function init() { 43 | addItemsListeners(); 44 | } 45 | 46 | init(); 47 | -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/storage.js: -------------------------------------------------------------------------------- 1 | function handleUpgradeEvent(event) { 2 | console.log('Creating DB'); 3 | let newDB = event.target.result; 4 | 5 | newDB.onerror = function(event) { 6 | console.log('Something went wrong'); 7 | console.log(event); 8 | }; 9 | 10 | let newFavoritesStore = newDB.createObjectStore('favorites', { 11 | keyPath: 'uri' 12 | }); 13 | 14 | newFavoritesStore.transaction.oncomplete = function(event) { 15 | console.log('Object stores created'); 16 | }; 17 | 18 | let newHistoryStore = newDB.createObjectStore('history', { 19 | autoIncrement: true 20 | }); 21 | 22 | newHistoryStore.createIndex('timestamp', 'timestamp', { 23 | unique: false 24 | }); 25 | 26 | newHistoryStore.createIndex('stampedURI', ['uri', 'timestamp'], { 27 | unique: false 28 | }); 29 | } 30 | 31 | function queryDB(query) { 32 | let request = window.indexedDB.open('WVBrowser'); 33 | 34 | request.onerror = function(event) { 35 | console.log('Failed to open database'); 36 | }; 37 | 38 | request.onsuccess = function(event) { 39 | let db = event.target.result; 40 | query(db); 41 | }; 42 | 43 | request.onupgradeneeded = handleUpgradeEvent; 44 | } 45 | -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/strip.css: -------------------------------------------------------------------------------- 1 | #tabs-strip { 2 | display: flex; 3 | height: 28px; 4 | 5 | border-top: 1px solid rgb(200, 200, 200); 6 | border-bottom: 1px solid rgb(200, 200, 200); 7 | background-color: rgb(230, 230, 230); 8 | } 9 | 10 | .tab, .tab-active { 11 | display: flex; 12 | flex: 1; 13 | height: 100%; 14 | border-right: 1px solid rgb(200, 200, 200); 15 | overflow: hidden; 16 | max-width: 250px; 17 | } 18 | 19 | .tab-active { 20 | background-color: rgb(240, 240, 240); 21 | } 22 | 23 | #btn-new-tab { 24 | display: flex; 25 | height: 100%; 26 | width: 30px; 27 | } 28 | 29 | #btn-new-tab:hover, .btn-tab-close:hover { 30 | background-color: rgb(200, 200, 200); 31 | } 32 | 33 | #plus-label { 34 | display: block; 35 | flex: 1; 36 | align-self: center; 37 | line-height: 30px; 38 | width: 30px; 39 | text-align: center; 40 | } 41 | 42 | .btn-tab-close { 43 | display: inline-block; 44 | align-self: center; 45 | min-width: 16px; 46 | width: 16px; 47 | height: 16px; 48 | margin: 0 5px; 49 | border-radius: 4px; 50 | 51 | background-size: 100%; 52 | background-image: url('img/cancel.png'); 53 | } 54 | 55 | .tab-label { 56 | display: flex; 57 | flex: 1; 58 | padding: 5px; 59 | padding-left: 10px; 60 | height: calc(100% - 10px); 61 | vertical-align: middle; 62 | 63 | white-space:nowrap; 64 | overflow:hidden; 65 | } 66 | 67 | .tab-label span { 68 | font-family: Arial; 69 | font-size: 0.8em; 70 | flex: 1; 71 | align-self: center; 72 | } 73 | -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | border: none; 4 | padding: 0; 5 | } 6 | 7 | p { 8 | margin: 0; 9 | } 10 | 11 | * { 12 | user-select: none; 13 | } 14 | -------------------------------------------------------------------------------- /wvbrowser_ui/controls_ui/tabs.js: -------------------------------------------------------------------------------- 1 | var tabs = new Map(); 2 | var tabIdCounter = 0; 3 | var activeTabId = 0; 4 | const INVALID_TAB_ID = 0; 5 | 6 | function getNewTabId() { 7 | return ++tabIdCounter; 8 | } 9 | 10 | function isValidTabId(tabId) { 11 | return tabId != INVALID_TAB_ID && tabs.has(tabId); 12 | } 13 | 14 | function createNewTab(shouldBeActive) { 15 | const tabId = getNewTabId(); 16 | 17 | var message = { 18 | message: commands.MG_CREATE_TAB, 19 | args: { 20 | tabId: parseInt(tabId), 21 | active: shouldBeActive || false 22 | } 23 | }; 24 | 25 | window.chrome.webview.postMessage(message); 26 | 27 | tabs.set(parseInt(tabId), { 28 | title: 'New Tab', 29 | uri: '', 30 | uriToShow: '', 31 | favicon: 'img/favicon.png', 32 | isFavorite: false, 33 | isLoading: false, 34 | canGoBack: false, 35 | canGoForward: false, 36 | securityState: 'unknown', 37 | historyItemId: INVALID_HISTORY_ID 38 | }); 39 | 40 | loadTabUI(tabId); 41 | 42 | if (shouldBeActive) { 43 | switchToTab(tabId, false); 44 | } 45 | } 46 | 47 | function switchToTab(id, updateOnHost) { 48 | if (!id) { 49 | console.log('ID not provided'); 50 | return; 51 | } 52 | 53 | // Check the tab to switch to is valid 54 | if (!isValidTabId(id)) { 55 | return; 56 | } 57 | 58 | // No need to switch if the tab is already active 59 | if (id == activeTabId) { 60 | return; 61 | } 62 | 63 | // Get the tab element to switch to 64 | var tab = document.getElementById(`tab-${id}`); 65 | if (!tab) { 66 | console.log(`Can't switch to tab ${id}: element does not exist`); 67 | return; 68 | } 69 | 70 | // Change the style for the previously active tab 71 | if (isValidTabId(activeTabId)) { 72 | const activeTabElement = document.getElementById(`tab-${activeTabId}`); 73 | 74 | // Check the previously active tab element does actually exist 75 | if (activeTabElement) { 76 | activeTabElement.className = 'tab'; 77 | } 78 | } 79 | 80 | // Set tab as active 81 | tab.className = 'tab-active'; 82 | activeTabId = id; 83 | 84 | // Instruct host app to switch tab 85 | if (updateOnHost) { 86 | var message = { 87 | message: commands.MG_SWITCH_TAB, 88 | args: { 89 | tabId: parseInt(activeTabId) 90 | } 91 | }; 92 | 93 | window.chrome.webview.postMessage(message); 94 | } 95 | 96 | updateNavigationUI(commands.MG_SWITCH_TAB); 97 | } 98 | 99 | function closeTab(id) { 100 | // If closing tab was active, switch tab or close window 101 | if (id == activeTabId) { 102 | if (tabs.size == 1) { 103 | // Last tab is closing, shut window down 104 | tabs.delete(id); 105 | closeWindow(); 106 | return; 107 | } 108 | 109 | // Other tabs are open, switch to rightmost tab 110 | var tabsEntries = Array.from(tabs.entries()); 111 | var lastEntry = tabsEntries.pop(); 112 | if (lastEntry[0] == id) { 113 | lastEntry = tabsEntries.pop(); 114 | } 115 | switchToTab(lastEntry[0], true); 116 | } 117 | 118 | // Remove tab element 119 | var tabElement = document.getElementById(`tab-${id}`); 120 | if (tabElement) { 121 | tabElement.parentNode.removeChild(tabElement); 122 | } 123 | // Remove tab from map 124 | tabs.delete(id); 125 | 126 | var message = { 127 | message: commands.MG_CLOSE_TAB, 128 | args: { 129 | tabId: id 130 | } 131 | }; 132 | 133 | window.chrome.webview.postMessage(message); 134 | } 135 | 136 | function updateFaviconURI(tabId, src) { 137 | let tab = tabs.get(tabId); 138 | if (tab.favicon != src) { 139 | let img = new Image(); 140 | 141 | // Update favicon element on successful load 142 | img.onload = () => { 143 | console.log('Favicon loaded'); 144 | tab.favicon = src; 145 | 146 | if (tabId == activeTabId) { 147 | updatedFaviconURIHandler(tabId, tab); 148 | } 149 | }; 150 | 151 | if (src) { 152 | // Try load from root on failed load 153 | img.onerror = () => { 154 | console.log('Cannot load favicon from link, trying with root'); 155 | updateFaviconURI(tabId, ''); 156 | }; 157 | } else { 158 | // No link for favicon, try loading from root 159 | try { 160 | let tabURI = new URL(tab.uri); 161 | src = `${tabURI.origin}/favicon.ico`; 162 | } catch(e) { 163 | console.log(`Could not parse tab ${tabId} URI`); 164 | } 165 | 166 | img.onerror = () => { 167 | console.log('No favicon in site root. Using default favicon.'); 168 | tab.favicon = 'img/favicon.png'; 169 | updatedFaviconURIHandler(tabId, tab); 170 | }; 171 | } 172 | 173 | img.src = src; 174 | } 175 | } 176 | 177 | function updatedFaviconURIHandler(tabId, tab) { 178 | updateNavigationUI(commands.MG_UPDATE_FAVICON); 179 | 180 | // Update favicon in history item 181 | if (tab.historyItemId != INVALID_HISTORY_ID) { 182 | updateHistoryItem(tab.historyItemId, historyItemFromTab(tabId)); 183 | } 184 | } 185 | 186 | function favoriteFromTab(tabId) { 187 | if (!isValidTabId(tabId)) { 188 | console.log('Invalid tab ID'); 189 | return; 190 | } 191 | 192 | let tab = tabs.get(tabId); 193 | let favicon = tab.favicon == 'img/favicon.png' ? '../controls_ui/' + tab.favicon : tab.favicon; 194 | return { 195 | uri: tab.uri, 196 | uriToShow: tab.uriToShow, 197 | title: tab.title, 198 | favicon: favicon 199 | }; 200 | } 201 | 202 | function historyItemFromTab(tabId) { 203 | if (!isValidTabId(tabId)) { 204 | console.log('Invalid tab ID'); 205 | return; 206 | } 207 | 208 | let tab = tabs.get(tabId); 209 | let favicon = tab.favicon == 'img/favicon.png' ? '../controls_ui/' + tab.favicon : tab.favicon; 210 | return { 211 | uri: tab.uri, 212 | title: tab.title, 213 | favicon: favicon, 214 | timestamp: new Date() 215 | } 216 | } 217 | --------------------------------------------------------------------------------