// Copyright 2015 The Chromium Embedded Framework Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "libcef/browser/native/browser_platform_delegate_native_win.h" #include #include #include #include "libcef/browser/alloy/alloy_browser_host_impl.h" #include "libcef/browser/context.h" #include "libcef/browser/geometry_util.h" #include "libcef/browser/native/window_delegate_view.h" #include "libcef/browser/thread_util.h" #include "base/base_paths_win.h" #include "base/files/file_util.h" #include "base/path_service.h" #include "base/strings/utf_string_conversions.h" #include "base/win/win_util.h" #include "content/public/common/input/native_web_keyboard_event.h" #include "third_party/blink/public/common/input/web_mouse_event.h" #include "third_party/blink/public/common/input/web_mouse_wheel_event.h" #include "ui/aura/window.h" #include "ui/base/win/shell.h" #include "ui/display/display.h" #include "ui/display/screen.h" #include "ui/display/win/screen_win.h" #include "ui/events/keycodes/dom/dom_key.h" #include "ui/events/keycodes/dom/keycode_converter.h" #include "ui/events/keycodes/keyboard_code_conversion_win.h" #include "ui/events/keycodes/platform_key_map_win.h" #include "ui/gfx/geometry/vector2d.h" #include "ui/gfx/win/hwnd_util.h" #include "ui/views/widget/desktop_aura/desktop_window_tree_host_win.h" #include "ui/views/widget/widget.h" #include "ui/views/win/hwnd_util.h" #pragma comment(lib, "dwmapi.lib") namespace { void WriteTempFileAndView(const std::string& data) { CEF_REQUIRE_BLOCKING(); base::FilePath tmp_file; if (!base::CreateTemporaryFile(&tmp_file)) { return; } // The shell command will look at the file extension to identify the correct // program to open. tmp_file = tmp_file.AddExtension(L"txt"); int write_ct = base::WriteFile(tmp_file, data.c_str(), data.size()); DCHECK_EQ(static_cast(data.size()), write_ct); ui::win::OpenFileViaShell(tmp_file); } gfx::Rect GetDisplayWorkAreaNearestPoint(gfx::Point dip_point) { const auto display = display::Screen::GetScreen()->GetDisplayNearestPoint(dip_point); // Work area in DIP. return display.work_area(); } CefRect GetScreenFrameRectFromDIPContentRect(HWND window, gfx::Rect dip_rect, DWORD style, DWORD ex_style, bool has_menu) { // Convert from DIP using a method that can handle multiple displays with // different DPI. If |window| is nullptr the closest display will be used. const auto screen_rect = display::win::ScreenWin::DIPToScreenRect(window, dip_rect); RECT rect = {screen_rect.x(), screen_rect.y(), screen_rect.x() + screen_rect.width(), screen_rect.y() + screen_rect.height()}; AdjustWindowRectEx(&rect, style, has_menu, ex_style); // Keep the original origin while potentially increasing the size to include // the frame non-client area. return CefRect(screen_rect.x(), screen_rect.y(), rect.right - rect.left, rect.bottom - rect.top); } CefRect GetAdjustedScreenFrameRect(CefRect screen_rect, DWORD style, DWORD ex_style, bool has_menu) { // If height or width is not provided let the OS determine the position and // size similar to Chromium behavior. Note that |CW_USEDEFAULT| cannot be // stored in a gfx::Rect due to clamping. if (screen_rect.width == CW_USEDEFAULT || screen_rect.height == CW_USEDEFAULT) { return CefRect(CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT); } if (screen_rect.x == CW_USEDEFAULT) { screen_rect.x = 0; } if (screen_rect.y == CW_USEDEFAULT) { screen_rect.y = 0; } // Convert to DIP using a method that can handle multiple displays with // different DPI. const auto dip_rect = display::win::ScreenWin::ScreenToDIPRect( nullptr, gfx::Rect(screen_rect.x, screen_rect.y, screen_rect.width, screen_rect.height)); const auto visible_dip_rect = MakeVisibleOnScreenRect( dip_rect, GetDisplayWorkAreaNearestPoint(dip_rect.origin())); return GetScreenFrameRectFromDIPContentRect( /*window=*/nullptr, visible_dip_rect, style, ex_style, has_menu); } } // namespace CefBrowserPlatformDelegateNativeWin::CefBrowserPlatformDelegateNativeWin( const CefWindowInfo& window_info, SkColor background_color) : CefBrowserPlatformDelegateNativeAura(window_info, background_color) {} void CefBrowserPlatformDelegateNativeWin::set_widget( views::Widget* widget, CefWindowHandle widget_handle) { DCHECK(!window_widget_); window_widget_ = widget; DCHECK(!window_info_.window); window_info_.window = widget_handle; } void CefBrowserPlatformDelegateNativeWin::BrowserDestroyed( CefBrowserHostBase* browser) { CefBrowserPlatformDelegateNativeAura::BrowserDestroyed(browser); if (host_window_created_) { // Release the reference added in CreateHostWindow(). browser->Release(); } } bool CefBrowserPlatformDelegateNativeWin::CreateHostWindow() { RegisterWindowClass(); if (window_info_.style == 0) { // Client didn't intialize the CefWindowInfo. Provide reasonable defaults. window_info_.SetAsPopup(nullptr, CefString()); } has_frame_ = !(window_info_.style & WS_CHILD); const std::wstring windowName(CefString(&window_info_.window_name)); CefRect window_rect = window_info_.bounds; if (!window_info_.parent_window) { const bool has_menu = !(window_info_.style & WS_CHILD) && (window_info_.menu != nullptr); window_rect = GetAdjustedScreenFrameRect(window_rect, window_info_.style, window_info_.ex_style, has_menu); } // Create the new browser window. CreateWindowEx(window_info_.ex_style, GetWndClass(), windowName.c_str(), window_info_.style, window_rect.x, window_rect.y, window_rect.width, window_rect.height, window_info_.parent_window, window_info_.menu, ::GetModuleHandle(nullptr), this); // It's possible for CreateWindowEx to fail if the parent window was // destroyed between the call to CreateBrowser and the above one. DCHECK(window_info_.window); if (!window_info_.window) { return false; } host_window_created_ = true; // Add a reference that will later be released in DestroyBrowser(). browser_->AddRef(); if (!called_enable_non_client_dpi_scaling_ && has_frame_) { // This call gets Windows to scale the non-client area when WM_DPICHANGED // is fired on Windows versions < 10.0.14393.0. // Derived signature; not available in headers. using EnableChildWindowDpiMessagePtr = LRESULT(WINAPI*)(HWND, BOOL); static const auto enable_child_window_dpi_message_func = reinterpret_cast( base::win::GetUser32FunctionPointer("EnableChildWindowDpiMessage")); if (enable_child_window_dpi_message_func) { enable_child_window_dpi_message_func(window_info_.window, TRUE); } } DCHECK(!window_widget_); RECT cr; GetClientRect(window_info_.window, &cr); // Convert to DIP using a method that can handle multiple displays with // different DPI. Client coordinates always have origin (0,0). const gfx::Rect dip_rect = display::win::ScreenWin::ScreenToDIPRect( window_info_.window, gfx::Rect(0, 0, cr.right, cr.bottom)); // Stay on top if top-most window hosting the web view is topmost. HWND top_level_window = GetAncestor(window_info_.window, GA_ROOT); DWORD top_level_window_ex_styles = GetWindowLongPtr(top_level_window, GWL_EXSTYLE); bool always_on_top = (top_level_window_ex_styles & WS_EX_TOPMOST) == WS_EX_TOPMOST; CefWindowDelegateView* delegate_view = new CefWindowDelegateView( GetBackgroundColor(), always_on_top, GetBoundsChangedCallback(), GetWidgetDeleteCallback()); delegate_view->Init(window_info_.window, web_contents_, gfx::Rect(0, 0, dip_rect.width(), dip_rect.height())); window_widget_ = delegate_view->GetWidget(); const HWND widget_hwnd = HWNDForWidget(window_widget_); DCHECK(widget_hwnd); const DWORD widget_ex_styles = GetWindowLongPtr(widget_hwnd, GWL_EXSTYLE); if (window_info_.ex_style & WS_EX_NOACTIVATE) { // Add the WS_EX_NOACTIVATE style on the DesktopWindowTreeHostWin HWND // so that HWNDMessageHandler::Show() called via Widget::Show() does not // activate the window. SetWindowLongPtr(widget_hwnd, GWL_EXSTYLE, widget_ex_styles | WS_EX_NOACTIVATE); } window_widget_->Show(); if (window_info_.ex_style & WS_EX_NOACTIVATE) { // Remove the WS_EX_NOACTIVATE style so that future mouse clicks inside the // browser correctly activate and focus the window. SetWindowLongPtr(widget_hwnd, GWL_EXSTYLE, widget_ex_styles); } return true; } void CefBrowserPlatformDelegateNativeWin::CloseHostWindow() { if (window_info_.window != nullptr) { HWND frameWnd = GetAncestor(window_info_.window, GA_ROOT); PostMessage(frameWnd, WM_CLOSE, 0, 0); } } CefWindowHandle CefBrowserPlatformDelegateNativeWin::GetHostWindowHandle() const { if (windowless_handler_) { return windowless_handler_->GetParentWindowHandle(); } return window_info_.window; } views::Widget* CefBrowserPlatformDelegateNativeWin::GetWindowWidget() const { return window_widget_; } void CefBrowserPlatformDelegateNativeWin::SetFocus(bool setFocus) { if (!setFocus) { return; } if (window_widget_) { // Give native focus to the DesktopWindowTreeHostWin ("Chrome_WidgetWin_0") // associated with the root window. The currently focused HWND may be // "CefBrowserWindow" if we're called in response to our WndProc receiving // the WM_SETFOCUS event (possibly due to "CefBrowserWindow" recieving the // top-level WM_ACTIVATE event), or some other HWND if the client calls // CefBrowserHost::SetFocus(true) directly. DesktopWindowTreeHostWin may // also receive focus/blur and mouse click events from the OS directly, in // which case this method will not be called but the below discussion still // applies. // // The DesktopWindowTreeHostWin::HandleNativeFocus/HandleNativeBlur methods // are called in response to WM_SETFOCUS/WM_KILLFOCUS respectively. The // DesktopWindowTreeHostWin::HandleMouseEvent method is called if the user // clicks on the WebContents. These methods have all been patched to call // HandleActivationChanged (indirectly via ::SetFocus in the case of mouse // clicks). HandleActivationChanged will then trigger the following // behaviors: // 1. Update focus/activation state of the aura::Window indirectly via // wm::FocusController. This allows focus-related behaviors (e.g. focus // rings, flashing caret, onFocus/onBlur JS events, etc.) to work as // expected (see issue #1677) and also triggers an initial call to // WebContents::Focus which gives logical focus to the // RenderWidgetHostViewAura in the views hierarchy (see issue #3306). // 2. Update focus state of the ui::InputMethod. If this does not occur // then: // (a) InputMethodBase::GetTextInputClient will return NULL and // InputMethodWin::OnChar will fail to send character events to the // renderer (see issue #1700); and // (b) InputMethodWinBase::IsWindowFocused will return false due to // ::GetFocus() returning the currently focused HWND (e.g. // "CefBrowserWindow") instead of the expected "Chrome_WidgetWin_0" HWND, // causing TSF not to handle IME events (see issue #3306). For this same // reason, ::SetFocus needs to be called before WebContents::Focus which // sends the InputMethod OnWillChangeFocusedClient notification that then // calls IsWindowFocused (e.g. WebContents::Focus is intentionally called // multiple times). // // This differs from activation in Chrome which is handled via // HWNDMessageHandler::PostProcessActivateMessage (Widget::Show indirectly // calls HWNDMessageHandler::Activate which calls ::SetForegroundWindow // resulting in a WM_ACTIVATE message being sent to the window). The Chrome // code path doesn't work for CEF because IsTopLevelWindow in // hwnd_message_handler.cc will return false and consequently // HWNDMessageHandler::PostProcessActivateMessage will not be called. // // Activation events are usually reserved for the top-level window so // triggering activation based on focus events may be incorrect in some // circumstances. Revisit this implementation if additional problems are // discovered. ::SetFocus(HWNDForWidget(window_widget_)); } if (web_contents_) { // Give logical focus to the RenderWidgetHostViewAura in the views // hierarchy. This does not change the native keyboard focus. When // |window_widget_| exists this additional Focus() call is necessary to // correctly assign focus/input state after native focus resulting from // window activation (see the InputMethod discussion above). web_contents_->Focus(); } } void CefBrowserPlatformDelegateNativeWin::NotifyMoveOrResizeStarted() { // Call the parent method to dismiss any existing popups. CefBrowserPlatformDelegateNativeAura::NotifyMoveOrResizeStarted(); if (!window_widget_) { return; } // Notify DesktopWindowTreeHostWin of move events so that screen rectangle // information is communicated to the renderer process and popups are // displayed in the correct location. views::DesktopWindowTreeHostWin* tree_host = static_cast( aura::WindowTreeHost::GetForAcceleratedWidget( HWNDForWidget(window_widget_))); DCHECK(tree_host); if (tree_host) { // Cast to HWNDMessageHandlerDelegate so we can access HandleMove(). static_cast(tree_host)->HandleMove(); } } void CefBrowserPlatformDelegateNativeWin::SizeTo(int width, int height) { HWND window = window_info_.window; const DWORD style = GetWindowLong(window, GWL_STYLE); const DWORD ex_style = GetWindowLong(window, GWL_EXSTYLE); const bool has_menu = !(style & WS_CHILD) && (GetMenu(window) != nullptr); const auto frame_rect = GetScreenFrameRectFromDIPContentRect( window, gfx::Rect(0, 0, width, height), style, ex_style, has_menu); // Size the window. The left/top values may be negative. SetWindowPos(window, nullptr, 0, 0, frame_rect.width, frame_rect.height, SWP_NOZORDER | SWP_NOMOVE | SWP_NOACTIVATE); } void CefBrowserPlatformDelegateNativeWin::ViewText(const std::string& text) { CEF_POST_USER_VISIBLE_TASK(base::BindOnce(WriteTempFileAndView, text)); } bool CefBrowserPlatformDelegateNativeWin::HandleKeyboardEvent( const content::NativeWebKeyboardEvent& event) { // Any unhandled keyboard/character messages are sent to DefWindowProc so that // shortcut keys work correctly. if (event.os_event) { const auto& msg = event.os_event->native_event(); return !DefWindowProc(msg.hwnd, msg.message, msg.wParam, msg.lParam); } else { MSG msg = {}; msg.hwnd = GetHostWindowHandle(); if (!msg.hwnd) { return false; } switch (event.GetType()) { case blink::WebInputEvent::Type::kRawKeyDown: msg.message = event.is_system_key ? WM_SYSKEYDOWN : WM_KEYDOWN; break; case blink::WebInputEvent::Type::kKeyUp: msg.message = event.is_system_key ? WM_SYSKEYUP : WM_KEYUP; break; case blink::WebInputEvent::Type::kChar: msg.message = event.is_system_key ? WM_SYSCHAR : WM_CHAR; break; default: DCHECK(false); return false; } msg.wParam = event.windows_key_code; UINT scan_code = ::MapVirtualKeyW(event.windows_key_code, MAPVK_VK_TO_VSC); msg.lParam = (scan_code << 16) | // key scan code 1; // key repeat count if (event.GetModifiers() & content::NativeWebKeyboardEvent::kAltKey) { msg.lParam |= (1 << 29); } return !DefWindowProc(msg.hwnd, msg.message, msg.wParam, msg.lParam); } } CefEventHandle CefBrowserPlatformDelegateNativeWin::GetEventHandle( const content::NativeWebKeyboardEvent& event) const { if (!event.os_event) { return nullptr; } return ChromeToWindowsType( const_cast(&event.os_event->native_event())); } ui::KeyEvent CefBrowserPlatformDelegateNativeWin::TranslateUiKeyEvent( const CefKeyEvent& key_event) const { int flags = TranslateUiEventModifiers(key_event.modifiers); ui::KeyboardCode key_code = ui::KeyboardCodeForWindowsKeyCode(key_event.windows_key_code); ui::DomCode dom_code = ui::KeycodeConverter::NativeKeycodeToDomCode(key_event.native_key_code); base::TimeTicks time_stamp = GetEventTimeStamp(); if (key_event.type == KEYEVENT_CHAR) { return ui::KeyEvent::FromCharacter(/*character=*/key_event.windows_key_code, key_code, dom_code, flags, time_stamp); } ui::EventType type = ui::ET_UNKNOWN; switch (key_event.type) { case KEYEVENT_RAWKEYDOWN: case KEYEVENT_KEYDOWN: type = ui::ET_KEY_PRESSED; break; case KEYEVENT_KEYUP: type = ui::ET_KEY_RELEASED; break; default: DCHECK(false); } ui::DomKey dom_key = ui::PlatformKeyMap::DomKeyFromKeyboardCode(key_code, &flags); return ui::KeyEvent(type, key_code, dom_code, flags, dom_key, time_stamp); } gfx::Vector2d CefBrowserPlatformDelegateNativeWin::GetUiWheelEventOffset( int deltaX, int deltaY) const { static const ULONG defaultScrollCharsPerWheelDelta = 1; static const FLOAT scrollbarPixelsPerLine = 100.0f / 3.0f; static const ULONG defaultScrollLinesPerWheelDelta = 3; float wheelDeltaX = float(deltaX) / WHEEL_DELTA; float wheelDeltaY = float(deltaY) / WHEEL_DELTA; float scrollDeltaX = wheelDeltaX; float scrollDeltaY = wheelDeltaY; ULONG scrollChars = defaultScrollCharsPerWheelDelta; SystemParametersInfo(SPI_GETWHEELSCROLLCHARS, 0, &scrollChars, 0); scrollDeltaX *= static_cast(scrollChars) * scrollbarPixelsPerLine; ULONG scrollLines = defaultScrollLinesPerWheelDelta; SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &scrollLines, 0); scrollDeltaY *= static_cast(scrollLines) * scrollbarPixelsPerLine; return gfx::Vector2d(scrollDeltaX, scrollDeltaY); } // static void CefBrowserPlatformDelegateNativeWin::RegisterWindowClass() { static bool registered = false; if (registered) { return; } // Register the window class WNDCLASSEX wcex = { /* cbSize = */ sizeof(WNDCLASSEX), /* style = */ CS_HREDRAW | CS_VREDRAW, /* lpfnWndProc = */ CefBrowserPlatformDelegateNativeWin::WndProc, /* cbClsExtra = */ 0, /* cbWndExtra = */ 0, /* hInstance = */ ::GetModuleHandle(nullptr), /* hIcon = */ nullptr, /* hCursor = */ LoadCursor(nullptr, IDC_ARROW), /* hbrBackground = */ nullptr, /* lpszMenuName = */ nullptr, /* lpszClassName = */ CefBrowserPlatformDelegateNativeWin::GetWndClass(), /* hIconSm = */ nullptr, }; RegisterClassEx(&wcex); registered = true; } // static LPCTSTR CefBrowserPlatformDelegateNativeWin::GetWndClass() { return L"CefBrowserWindow"; } // static LRESULT CALLBACK CefBrowserPlatformDelegateNativeWin::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { CefBrowserPlatformDelegateNativeWin* platform_delegate = nullptr; CefBrowserHostBase* browser = nullptr; if (message != WM_NCCREATE) { platform_delegate = static_cast( gfx::GetWindowUserData(hwnd)); if (platform_delegate) { browser = platform_delegate->browser_; } } switch (message) { case WM_CLOSE: if (browser && !browser->TryCloseBrowser()) { // Cancel the close. return 0; } // Allow the close. break; case WM_NCCREATE: { CREATESTRUCT* cs = reinterpret_cast(lParam); platform_delegate = reinterpret_cast( cs->lpCreateParams); DCHECK(platform_delegate); // Associate |platform_delegate| with the window handle. gfx::SetWindowUserData(hwnd, platform_delegate); platform_delegate->window_info_.window = hwnd; if (platform_delegate->has_frame_) { // This call gets Windows to scale the non-client area when // WM_DPICHANGED is fired on Windows versions >= 10.0.14393.0. using EnableNonClientDpiScalingPtr = decltype(::EnableNonClientDpiScaling)*; static const auto enable_non_client_dpi_scaling_func = reinterpret_cast( base::win::GetUser32FunctionPointer( "EnableNonClientDpiScaling")); platform_delegate->called_enable_non_client_dpi_scaling_ = !!(enable_non_client_dpi_scaling_func && enable_non_client_dpi_scaling_func(hwnd)); } } break; case WM_NCDESTROY: if (platform_delegate) { // Clear the user data pointer. gfx::SetWindowUserData(hwnd, nullptr); // Force the browser to be destroyed. This will result in a call to // BrowserDestroyed() that will release the reference added in // CreateHostWindow(). AlloyBrowserHostImpl::FromBaseChecked(browser)->WindowDestroyed(); } break; case WM_SIZE: if (platform_delegate && platform_delegate->window_widget_) { // Pass window resize events to the HWND for the DesktopNativeWidgetAura // root window. Passing size 0x0 (wParam == SIZE_MINIMIZED, for example) // will cause the widget to be hidden which reduces resource usage. RECT rc; GetClientRect(hwnd, &rc); SetWindowPos(HWNDForWidget(platform_delegate->window_widget_), nullptr, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER); } return 0; case WM_MOVING: case WM_MOVE: if (browser) { browser->NotifyMoveOrResizeStarted(); } return 0; case WM_SETFOCUS: // Selecting "Close window" from the task bar menu may send a focus // notification even though the window is currently disabled (e.g. while // a modal JS dialog is displayed). if (browser && ::IsWindowEnabled(hwnd)) { browser->SetFocus(true); } return 0; case WM_ERASEBKGND: return 0; case WM_DPICHANGED: if (platform_delegate && platform_delegate->has_frame_) { // Suggested size and position of the current window scaled for the // new DPI. const RECT* rect = reinterpret_cast(lParam); SetWindowPos(platform_delegate->GetHostWindowHandle(), nullptr, rect->left, rect->top, rect->right - rect->left, rect->bottom - rect->top, SWP_NOZORDER | SWP_NOACTIVATE); } break; case WM_ENABLE: if (wParam == TRUE && browser) { // Give focus to the browser after EnableWindow enables this window // (e.g. after a modal dialog is dismissed). browser->SetFocus(true); return 0; } break; } return DefWindowProc(hwnd, message, wParam, lParam); }