| // 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 <stddef.h> |
| #include <windows.h> |
| |
| #include <cstdlib> |
| #include <memory> |
| |
| #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/stl_util.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" |
| |
| 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::TimeDelta::FromSeconds(10); |
| |
| // The length of the hide and show animations. |
| constexpr DWORD kAnimationDurationMs = 200; |
| |
| class DisconnectWindowWin : public HostWindow { |
| public: |
| DisconnectWindowWin(); |
| ~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<LocalInputMonitor> local_input_monitor); |
| |
| // HostWindow overrides. |
| void Start( |
| const base::WeakPtr<ClientSessionControl>& 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); |
| |
| // Called when local keyboard event is seen and shows the dialog (if hidden). |
| void OnLocalKeyboardEvent(); |
| |
| // Used to disconnect the client session. |
| base::WeakPtr<ClientSessionControl> client_session_control_; |
| |
| // Used to watch for local input which will trigger the dialog to be reshown. |
| std::unique_ptr<LocalInputMonitor> 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<HPEN> border_pen_; |
| |
| webrtc::DesktopVector mouse_position_; |
| |
| base::WeakPtrFactory<DisconnectWindowWin> weak_factory_; |
| |
| DISALLOW_COPY_AND_ASSIGN(DisconnectWindowWin); |
| }; |
| |
| // Returns the text for the given dialog control window. |
| bool GetControlText(HWND control, base::string16* text) { |
| // GetWindowText truncates the text if it is longer than can fit into |
| // the buffer. |
| WCHAR buffer[256]; |
| int result = GetWindowText(control, buffer, base::size(buffer)); |
| if (!result) |
| return false; |
| |
| text->assign(buffer); |
| return true; |
| } |
| |
| // Returns width |text| rendered in |control| window. |
| bool GetControlTextWidth(HWND control, |
| const base::string16& 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))), |
| weak_factory_(this) {} |
| |
| DisconnectWindowWin::~DisconnectWindowWin() { |
| EndDialog(); |
| } |
| |
| void DisconnectWindowWin::EnableAutoHide( |
| std::unique_ptr<LocalInputMonitor> local_input_monitor) { |
| local_input_monitor_ = std::move(local_input_monitor); |
| } |
| |
| void DisconnectWindowWin::Start( |
| const base::WeakPtr<ClientSessionControl>& 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::OnLocalKeyboardEvent, |
| 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<DisconnectWindowWin*>(self)->EndDialog(); |
| } else { |
| self = GetWindowLongPtr(hwnd, DWLP_USER); |
| } |
| |
| if (self) { |
| return reinterpret_cast<DisconnectWindowWin*>(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<HDC>(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<LPARAM>(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; |
| } |
| |
| 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 windows 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_) |
| return; |
| |
| if (!AnimateWindow(hwnd_, kAnimationDurationMs, AW_BLEND | AW_HIDE)) |
| PLOG(ERROR) << "AnimateWindow() failed to show 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) { |
| // 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::OnLocalKeyboardEvent() { |
| // 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<LPPOINT>(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; |
| |
| base::string16 button_text; |
| base::string16 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::ReplaceStringPlaceholders(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> HostWindow::CreateDisconnectWindow() { |
| return std::make_unique<DisconnectWindowWin>(); |
| } |
| |
| std::unique_ptr<HostWindow> HostWindow::CreateAutoHidingDisconnectWindow( |
| std::unique_ptr<LocalInputMonitor> local_input_monitor) { |
| auto disconnect_window = std::make_unique<DisconnectWindowWin>(); |
| disconnect_window->EnableAutoHide(std::move(local_input_monitor)); |
| |
| return disconnect_window; |
| } |
| |
| } // namespace remoting |