// Copyright (c) 2012 The Chromium 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 #include #include #include #include "base/bind.h" #include "base/compiler_specific.h" #include "base/location.h" #include "base/logging.h" #include "base/memory/weak_ptr.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "base/time/time.h" #include "base/timer/timer.h" #include "base/win/current_module.h" #include "base/win/scoped_gdi_object.h" #include "base/win/scoped_hdc.h" #include "base/win/scoped_select_object.h" #include "remoting/host/client_session_control.h" #include "remoting/host/host_window.h" #include "remoting/host/input_monitor/local_input_monitor.h" #include "remoting/host/win/core_resource.h" #include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h" #include "ui/events/event.h" namespace remoting { namespace { constexpr int DISCONNECT_HOTKEY_ID = 1000; // Maximum length of "Your desktop is shared with ..." message in UTF-16 // characters. constexpr size_t kMaxSharingWithTextLength = 100; constexpr wchar_t kShellTrayWindowName[] = L"Shell_TrayWnd"; constexpr int kWindowBorderRadius = 14; // Margin between dialog controls (in dialog units). constexpr int kWindowTextMargin = 8; // The amount of time to wait before hiding the disconnect window. constexpr base::TimeDelta kAutoHideTimeout = base::Seconds(10); // The length of the hide and show animations. constexpr DWORD kAnimationDurationMs = 200; class DisconnectWindowWin : public HostWindow { public: DisconnectWindowWin(); DisconnectWindowWin(const DisconnectWindowWin&) = delete; DisconnectWindowWin& operator=(const DisconnectWindowWin&) = delete; ~DisconnectWindowWin() override; // Allow dialog to auto-hide after a period of time. The dialog will be // reshown when local user input is detected. void EnableAutoHide(std::unique_ptr local_input_monitor); // HostWindow overrides. void Start( const base::WeakPtr& client_session_control) override; private: static INT_PTR CALLBACK DialogProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); BOOL OnDialogMessage(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); // Creates the dialog window and registers the disconnect hot key. bool BeginDialog(); // Closes the dialog, unregisters the hot key and invokes the disconnect // callback, if set. void EndDialog(); // Returns |control| rectangle in the dialog coordinates. bool GetControlRect(HWND control, RECT* rect); // Positions the dialog window based on the current auto-hide state. // If auto-hide is enabled, the window is displayed near the center of the // display, otherwise it is displayed just above the taskbar. void SetDialogPosition(); // Applies localization string and resizes the dialog. bool SetStrings(); // Draws the border around the dialog window. Can be used to draw the initial // border or to redraw if when the dialog is reshown. |hwnd| is the window to // have the border applied. |hdc| is the device context to draw to. void DrawBorder(HWND hwnd, HDC hdc); // Shows a previously hidden dialog using an animation. void ShowDialog(); // Hides the dialog using an animation. void HideDialog(); // Prevent the dialog from being hidden if local input monitoring fails. void StopAutoHideBehavior(); // Called when local mouse event is seen and shows the dialog (if hidden). void OnLocalMouseEvent(const webrtc::DesktopVector& mouse_position, ui::EventType type); // Called when local keyboard event is seen and shows the dialog (if hidden). void OnLocalKeyPressed(uint32_t usb_keycode); // Used to disconnect the client session. base::WeakPtr client_session_control_; // Used to watch for local input which will trigger the dialog to be reshown. std::unique_ptr local_input_monitor_; // Specifies the remote user name. std::string username_; bool was_auto_hidden_ = false; bool local_input_seen_ = false; base::OneShotTimer auto_hide_timer_; HWND hwnd_ = nullptr; bool has_hotkey_ = false; base::win::ScopedGDIObject border_pen_; webrtc::DesktopVector mouse_position_; base::WeakPtrFactory weak_factory_{this}; }; // Returns the text for the given dialog control window. bool GetControlText(HWND control, std::wstring* text) { // GetWindowText truncates the text if it is longer than can fit into // the buffer. WCHAR buffer[256]; int result = GetWindowText(control, buffer, std::size(buffer)); if (!result) return false; text->assign(buffer); return true; } // Returns width |text| rendered in |control| window. bool GetControlTextWidth(HWND control, const std::wstring& text, LONG* width) { RECT rect = {0, 0, 0, 0}; base::win::ScopedGetDC dc(control); base::win::ScopedSelectObject font( dc, (HFONT)SendMessage(control, WM_GETFONT, 0, 0)); if (!DrawText(dc, text.c_str(), -1, &rect, DT_CALCRECT | DT_SINGLELINE)) return false; *width = rect.right; return true; } DisconnectWindowWin::DisconnectWindowWin() : border_pen_( CreatePen(PS_SOLID, 5, RGB(0.13 * 255, 0.69 * 255, 0.11 * 255))) {} DisconnectWindowWin::~DisconnectWindowWin() { EndDialog(); } void DisconnectWindowWin::EnableAutoHide( std::unique_ptr local_input_monitor) { local_input_monitor_ = std::move(local_input_monitor); } void DisconnectWindowWin::Start( const base::WeakPtr& client_session_control) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); DCHECK(!client_session_control_); DCHECK(client_session_control); client_session_control_ = client_session_control; std::string client_jid = client_session_control_->client_jid(); username_ = client_jid.substr(0, client_jid.find('/')); if (!BeginDialog()) { EndDialog(); return; } if (local_input_monitor_) { local_input_monitor_->StartMonitoring( base::BindRepeating(&DisconnectWindowWin::OnLocalMouseEvent, weak_factory_.GetWeakPtr()), base::BindRepeating(&DisconnectWindowWin::OnLocalKeyPressed, weak_factory_.GetWeakPtr()), base::BindRepeating(&DisconnectWindowWin::StopAutoHideBehavior, weak_factory_.GetWeakPtr())); auto_hide_timer_.Start(FROM_HERE, kAutoHideTimeout, base::BindOnce(&DisconnectWindowWin::HideDialog, base::Unretained(this))); } } INT_PTR CALLBACK DisconnectWindowWin::DialogProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { LONG_PTR self = 0; if (message == WM_INITDIALOG) { self = lparam; // Store |this| to the window's user data. SetLastError(ERROR_SUCCESS); LONG_PTR result = SetWindowLongPtr(hwnd, DWLP_USER, self); if (result == 0 && GetLastError() != ERROR_SUCCESS) reinterpret_cast(self)->EndDialog(); } else { self = GetWindowLongPtr(hwnd, DWLP_USER); } if (self) { return reinterpret_cast(self)->OnDialogMessage( hwnd, message, wparam, lparam); } return FALSE; } BOOL DisconnectWindowWin::OnDialogMessage(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); switch (message) { // Ignore close messages. case WM_CLOSE: return TRUE; // Handle the Disconnect button. case WM_COMMAND: switch (LOWORD(wparam)) { case IDC_DISCONNECT: EndDialog(); return TRUE; } return FALSE; // Ensure we don't try to use the HWND anymore. case WM_DESTROY: hwnd_ = nullptr; // Ensure that the disconnect callback is invoked even if somehow our // window gets destroyed. EndDialog(); return TRUE; // Ensure the dialog stays visible if the work area dimensions change. case WM_SETTINGCHANGE: if (wparam == SPI_SETWORKAREA) SetDialogPosition(); return TRUE; // Ensure the dialog stays visible if the display dimensions change. case WM_DISPLAYCHANGE: SetDialogPosition(); return TRUE; // Handle the disconnect hot-key. case WM_HOTKEY: EndDialog(); return TRUE; // Let the window be draggable by its client area by responding // that the entire window is the title bar. case WM_NCHITTEST: SetWindowLongPtr(hwnd, DWLP_MSGRESULT, HTCAPTION); return TRUE; case WM_PAINT: { // Draw the client area after ShowWindow is used to make |hwnd_| visible. PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd_, &ps); DrawBorder(hwnd_, hdc); EndPaint(hwnd_, &ps); return TRUE; } case WM_PRINTCLIENT: { // Refresh the dialog client area. Called after AnimateWindow is used to // reshow the dialog. HDC hdc = reinterpret_cast(wparam); DrawBorder(hwnd_, hdc); return TRUE; } } return FALSE; } bool DisconnectWindowWin::BeginDialog() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); DCHECK(!hwnd_); hwnd_ = CreateDialogParam(CURRENT_MODULE(), MAKEINTRESOURCE(IDD_DISCONNECT), nullptr, DialogProc, reinterpret_cast(this)); if (!hwnd_) return false; // Set up handler for Ctrl-Alt-Esc shortcut. if (!has_hotkey_ && RegisterHotKey(hwnd_, DISCONNECT_HOTKEY_ID, MOD_ALT | MOD_CONTROL, VK_ESCAPE)) { has_hotkey_ = true; } if (!SetStrings()) return false; SetDialogPosition(); ShowWindow(hwnd_, SW_SHOW); return IsWindowVisible(hwnd_) != FALSE; } void DisconnectWindowWin::EndDialog() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); if (has_hotkey_) { UnregisterHotKey(hwnd_, DISCONNECT_HOTKEY_ID); has_hotkey_ = false; } if (hwnd_) { DestroyWindow(hwnd_); hwnd_ = nullptr; } // Disable auto-hide events since the window has been destroyed. auto_hide_timer_.Stop(); if (client_session_control_) client_session_control_->DisconnectSession(protocol::OK); } void DisconnectWindowWin::ShowDialog() { // Always reset the hide timer when this method is called. if (local_input_monitor_) { auto_hide_timer_.Start(FROM_HERE, kAutoHideTimeout, base::BindOnce(&DisconnectWindowWin::HideDialog, base::Unretained(this))); } if (!was_auto_hidden_) return; // Make sure the dialog is fully visible when it is reshown. if (!local_input_seen_) SetDialogPosition(); if (!AnimateWindow(hwnd_, kAnimationDurationMs, AW_BLEND)) { PLOG(ERROR) << "AnimateWindow() failed to show dialog: "; ShowWindow(hwnd_, SW_SHOW); // If the window still isn't visible, then disconnect the session. if (!IsWindowVisible(hwnd_)) client_session_control_->DisconnectSession(protocol::OK); } was_auto_hidden_ = false; } void DisconnectWindowWin::HideDialog() { if (was_auto_hidden_ || !local_input_monitor_ || !hwnd_) return; if (!AnimateWindow(hwnd_, kAnimationDurationMs, AW_BLEND | AW_HIDE)) PLOG(ERROR) << "AnimateWindow() failed to hide dialog: "; else was_auto_hidden_ = true; } void DisconnectWindowWin::StopAutoHideBehavior() { auto_hide_timer_.Stop(); local_input_monitor_.reset(); ShowDialog(); } void DisconnectWindowWin::OnLocalMouseEvent( const webrtc::DesktopVector& position, ui::EventType type) { // Don't show the dialog if the position changes by ~1px in any direction. // This will prevent the dialog from being reshown due to small movements // caused by hardware/software issues which cause cursor drift or small // vibrations in the environment around the remote host. if (std::abs(position.x() - mouse_position_.x()) > 1 || std::abs(position.y() - mouse_position_.y()) > 1) { // Show the dialog before setting |local_input_seen_|. That way the dialog // will be shown in the center position and subsequent reshows will honor // the new position (if any) the dialog is moved to. ShowDialog(); local_input_seen_ = true; } mouse_position_ = position; } void DisconnectWindowWin::OnLocalKeyPressed(uint32_t usb_keycode) { // Show the dialog before setting |local_input_seen_|. That way the dialog // will be shown in the center position and subsequent reshows will honor // the new position (if any) the dialog is moved to. ShowDialog(); local_input_seen_ = true; } void DisconnectWindowWin::DrawBorder(HWND hwnd, HDC hdc) { RECT rect; GetClientRect(hwnd, &rect); base::win::ScopedSelectObject border(hdc, border_pen_.get()); base::win::ScopedSelectObject brush(hdc, GetStockObject(NULL_BRUSH)); RoundRect(hdc, rect.left, rect.top, rect.right - 1, rect.bottom - 1, kWindowBorderRadius, kWindowBorderRadius); } // Returns |control| rectangle in the dialog coordinates. bool DisconnectWindowWin::GetControlRect(HWND control, RECT* rect) { if (!GetWindowRect(control, rect)) return false; SetLastError(ERROR_SUCCESS); int result = MapWindowPoints(HWND_DESKTOP, hwnd_, reinterpret_cast(rect), 2); if (!result && GetLastError() != ERROR_SUCCESS) return false; return true; } void DisconnectWindowWin::SetDialogPosition() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); // Try to center the window above the task-bar. If that fails, use the // primary monitor. If that fails (very unlikely), use the default position. HWND taskbar = FindWindow(kShellTrayWindowName, nullptr); HMONITOR monitor = MonitorFromWindow(taskbar, MONITOR_DEFAULTTOPRIMARY); MONITORINFO monitor_info = {sizeof(monitor_info)}; RECT window_rect; if (!GetMonitorInfo(monitor, &monitor_info) || !GetWindowRect(hwnd_, &window_rect)) { return; } int window_width = window_rect.right - window_rect.left; int window_height = window_rect.bottom - window_rect.top; // Default settings will display the window above the taskbar and centered // along the x axis. int top = monitor_info.rcWork.bottom - window_height; int left = (monitor_info.rcWork.right + monitor_info.rcWork.left - window_width) / 2; // Adjust the top value if the window is in auto-hide mode and we have not // seen local input yet. We adjust the position to make the dialog a bit more // obtrusive so that a local user will notice it before it auto-hides. if (local_input_monitor_ && !local_input_seen_) top = top * 0.7; SetWindowPos(hwnd_, nullptr, left, top, 0, 0, SWP_NOSIZE | SWP_NOZORDER); } bool DisconnectWindowWin::SetStrings() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); // Localize the disconnect button text and measure length of the old and new // labels. HWND hwnd_button = GetDlgItem(hwnd_, IDC_DISCONNECT); HWND hwnd_message = GetDlgItem(hwnd_, IDC_DISCONNECT_SHARINGWITH); if (!hwnd_button || !hwnd_message) return false; std::wstring button_text; std::wstring message_text; if (!GetControlText(hwnd_button, &button_text) || !GetControlText(hwnd_message, &message_text)) { return false; } // Format and truncate "Your desktop is shared with ..." message. message_text = base::AsWString(base::ReplaceStringPlaceholders( base::AsString16(message_text), base::UTF8ToUTF16(username_), nullptr)); if (message_text.length() > kMaxSharingWithTextLength) message_text.erase(kMaxSharingWithTextLength); if (!SetWindowText(hwnd_message, message_text.c_str())) return false; // Calculate the margin between controls in pixels. RECT rect = {0}; rect.right = kWindowTextMargin; if (!MapDialogRect(hwnd_, &rect)) return false; int margin = rect.right; // Resize |hwnd_message| so that the text is not clipped. RECT message_rect; if (!GetControlRect(hwnd_message, &message_rect)) return false; LONG control_width; if (!GetControlTextWidth(hwnd_message, message_text, &control_width)) return false; message_rect.right = message_rect.left + control_width + margin; if (!SetWindowPos(hwnd_message, nullptr, message_rect.left, message_rect.top, message_rect.right - message_rect.left, message_rect.bottom - message_rect.top, SWP_NOZORDER)) { return false; } // Reposition and resize |hwnd_button| as well. RECT button_rect; if (!GetControlRect(hwnd_button, &button_rect)) return false; if (!GetControlTextWidth(hwnd_button, button_text, &control_width)) return false; button_rect.left = message_rect.right; button_rect.right = button_rect.left + control_width + margin * 2; if (!SetWindowPos(hwnd_button, nullptr, button_rect.left, button_rect.top, button_rect.right - button_rect.left, button_rect.bottom - button_rect.top, SWP_NOZORDER)) { return false; } // Resize the whole window to fit the resized controls. RECT window_rect; if (!GetWindowRect(hwnd_, &window_rect)) return false; int width = button_rect.right + margin; int height = window_rect.bottom - window_rect.top; if (!SetWindowPos(hwnd_, nullptr, 0, 0, width, height, SWP_NOMOVE | SWP_NOZORDER)) { return false; } // Make the corners of the disconnect window rounded. HRGN rgn = CreateRoundRectRgn(0, 0, width, height, kWindowBorderRadius, kWindowBorderRadius); if (!rgn) return false; if (!SetWindowRgn(hwnd_, rgn, TRUE)) return false; return true; } } // namespace // static std::unique_ptr HostWindow::CreateDisconnectWindow() { return std::make_unique(); } std::unique_ptr HostWindow::CreateAutoHidingDisconnectWindow( std::unique_ptr local_input_monitor) { auto disconnect_window = std::make_unique(); disconnect_window->EnableAutoHide(std::move(local_input_monitor)); return disconnect_window; } } // namespace remoting