disconnect_window_win.cc 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. // Copyright (c) 2012 The Chromium Authors. 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. #include <stddef.h>
  5. #include <windows.h>
  6. #include <cstdlib>
  7. #include <memory>
  8. #include "base/bind.h"
  9. #include "base/compiler_specific.h"
  10. #include "base/location.h"
  11. #include "base/logging.h"
  12. #include "base/memory/weak_ptr.h"
  13. #include "base/strings/string_util.h"
  14. #include "base/strings/utf_string_conversions.h"
  15. #include "base/time/time.h"
  16. #include "base/timer/timer.h"
  17. #include "base/win/current_module.h"
  18. #include "base/win/scoped_gdi_object.h"
  19. #include "base/win/scoped_hdc.h"
  20. #include "base/win/scoped_select_object.h"
  21. #include "remoting/host/client_session_control.h"
  22. #include "remoting/host/host_window.h"
  23. #include "remoting/host/input_monitor/local_input_monitor.h"
  24. #include "remoting/host/win/core_resource.h"
  25. #include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h"
  26. #include "ui/events/event.h"
  27. namespace remoting {
  28. namespace {
  29. constexpr int DISCONNECT_HOTKEY_ID = 1000;
  30. // Maximum length of "Your desktop is shared with ..." message in UTF-16
  31. // characters.
  32. constexpr size_t kMaxSharingWithTextLength = 100;
  33. constexpr wchar_t kShellTrayWindowName[] = L"Shell_TrayWnd";
  34. constexpr int kWindowBorderRadius = 14;
  35. // Margin between dialog controls (in dialog units).
  36. constexpr int kWindowTextMargin = 8;
  37. // The amount of time to wait before hiding the disconnect window.
  38. constexpr base::TimeDelta kAutoHideTimeout = base::Seconds(10);
  39. // The length of the hide and show animations.
  40. constexpr DWORD kAnimationDurationMs = 200;
  41. class DisconnectWindowWin : public HostWindow {
  42. public:
  43. DisconnectWindowWin();
  44. DisconnectWindowWin(const DisconnectWindowWin&) = delete;
  45. DisconnectWindowWin& operator=(const DisconnectWindowWin&) = delete;
  46. ~DisconnectWindowWin() override;
  47. // Allow dialog to auto-hide after a period of time. The dialog will be
  48. // reshown when local user input is detected.
  49. void EnableAutoHide(std::unique_ptr<LocalInputMonitor> local_input_monitor);
  50. // HostWindow overrides.
  51. void Start(
  52. const base::WeakPtr<ClientSessionControl>& client_session_control)
  53. override;
  54. private:
  55. static INT_PTR CALLBACK DialogProc(HWND hwnd, UINT message, WPARAM wparam,
  56. LPARAM lparam);
  57. BOOL OnDialogMessage(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
  58. // Creates the dialog window and registers the disconnect hot key.
  59. bool BeginDialog();
  60. // Closes the dialog, unregisters the hot key and invokes the disconnect
  61. // callback, if set.
  62. void EndDialog();
  63. // Returns |control| rectangle in the dialog coordinates.
  64. bool GetControlRect(HWND control, RECT* rect);
  65. // Positions the dialog window based on the current auto-hide state.
  66. // If auto-hide is enabled, the window is displayed near the center of the
  67. // display, otherwise it is displayed just above the taskbar.
  68. void SetDialogPosition();
  69. // Applies localization string and resizes the dialog.
  70. bool SetStrings();
  71. // Draws the border around the dialog window. Can be used to draw the initial
  72. // border or to redraw if when the dialog is reshown. |hwnd| is the window to
  73. // have the border applied. |hdc| is the device context to draw to.
  74. void DrawBorder(HWND hwnd, HDC hdc);
  75. // Shows a previously hidden dialog using an animation.
  76. void ShowDialog();
  77. // Hides the dialog using an animation.
  78. void HideDialog();
  79. // Prevent the dialog from being hidden if local input monitoring fails.
  80. void StopAutoHideBehavior();
  81. // Called when local mouse event is seen and shows the dialog (if hidden).
  82. void OnLocalMouseEvent(const webrtc::DesktopVector& mouse_position,
  83. ui::EventType type);
  84. // Called when local keyboard event is seen and shows the dialog (if hidden).
  85. void OnLocalKeyPressed(uint32_t usb_keycode);
  86. // Used to disconnect the client session.
  87. base::WeakPtr<ClientSessionControl> client_session_control_;
  88. // Used to watch for local input which will trigger the dialog to be reshown.
  89. std::unique_ptr<LocalInputMonitor> local_input_monitor_;
  90. // Specifies the remote user name.
  91. std::string username_;
  92. bool was_auto_hidden_ = false;
  93. bool local_input_seen_ = false;
  94. base::OneShotTimer auto_hide_timer_;
  95. HWND hwnd_ = nullptr;
  96. bool has_hotkey_ = false;
  97. base::win::ScopedGDIObject<HPEN> border_pen_;
  98. webrtc::DesktopVector mouse_position_;
  99. base::WeakPtrFactory<DisconnectWindowWin> weak_factory_{this};
  100. };
  101. // Returns the text for the given dialog control window.
  102. bool GetControlText(HWND control, std::wstring* text) {
  103. // GetWindowText truncates the text if it is longer than can fit into
  104. // the buffer.
  105. WCHAR buffer[256];
  106. int result = GetWindowText(control, buffer, std::size(buffer));
  107. if (!result)
  108. return false;
  109. text->assign(buffer);
  110. return true;
  111. }
  112. // Returns width |text| rendered in |control| window.
  113. bool GetControlTextWidth(HWND control, const std::wstring& text, LONG* width) {
  114. RECT rect = {0, 0, 0, 0};
  115. base::win::ScopedGetDC dc(control);
  116. base::win::ScopedSelectObject font(
  117. dc, (HFONT)SendMessage(control, WM_GETFONT, 0, 0));
  118. if (!DrawText(dc, text.c_str(), -1, &rect, DT_CALCRECT | DT_SINGLELINE))
  119. return false;
  120. *width = rect.right;
  121. return true;
  122. }
  123. DisconnectWindowWin::DisconnectWindowWin()
  124. : border_pen_(
  125. CreatePen(PS_SOLID, 5, RGB(0.13 * 255, 0.69 * 255, 0.11 * 255))) {}
  126. DisconnectWindowWin::~DisconnectWindowWin() {
  127. EndDialog();
  128. }
  129. void DisconnectWindowWin::EnableAutoHide(
  130. std::unique_ptr<LocalInputMonitor> local_input_monitor) {
  131. local_input_monitor_ = std::move(local_input_monitor);
  132. }
  133. void DisconnectWindowWin::Start(
  134. const base::WeakPtr<ClientSessionControl>& client_session_control) {
  135. DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  136. DCHECK(!client_session_control_);
  137. DCHECK(client_session_control);
  138. client_session_control_ = client_session_control;
  139. std::string client_jid = client_session_control_->client_jid();
  140. username_ = client_jid.substr(0, client_jid.find('/'));
  141. if (!BeginDialog()) {
  142. EndDialog();
  143. return;
  144. }
  145. if (local_input_monitor_) {
  146. local_input_monitor_->StartMonitoring(
  147. base::BindRepeating(&DisconnectWindowWin::OnLocalMouseEvent,
  148. weak_factory_.GetWeakPtr()),
  149. base::BindRepeating(&DisconnectWindowWin::OnLocalKeyPressed,
  150. weak_factory_.GetWeakPtr()),
  151. base::BindRepeating(&DisconnectWindowWin::StopAutoHideBehavior,
  152. weak_factory_.GetWeakPtr()));
  153. auto_hide_timer_.Start(FROM_HERE, kAutoHideTimeout,
  154. base::BindOnce(&DisconnectWindowWin::HideDialog,
  155. base::Unretained(this)));
  156. }
  157. }
  158. INT_PTR CALLBACK DisconnectWindowWin::DialogProc(HWND hwnd,
  159. UINT message,
  160. WPARAM wparam,
  161. LPARAM lparam) {
  162. LONG_PTR self = 0;
  163. if (message == WM_INITDIALOG) {
  164. self = lparam;
  165. // Store |this| to the window's user data.
  166. SetLastError(ERROR_SUCCESS);
  167. LONG_PTR result = SetWindowLongPtr(hwnd, DWLP_USER, self);
  168. if (result == 0 && GetLastError() != ERROR_SUCCESS)
  169. reinterpret_cast<DisconnectWindowWin*>(self)->EndDialog();
  170. } else {
  171. self = GetWindowLongPtr(hwnd, DWLP_USER);
  172. }
  173. if (self) {
  174. return reinterpret_cast<DisconnectWindowWin*>(self)->OnDialogMessage(
  175. hwnd, message, wparam, lparam);
  176. }
  177. return FALSE;
  178. }
  179. BOOL DisconnectWindowWin::OnDialogMessage(HWND hwnd,
  180. UINT message,
  181. WPARAM wparam,
  182. LPARAM lparam) {
  183. DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  184. switch (message) {
  185. // Ignore close messages.
  186. case WM_CLOSE:
  187. return TRUE;
  188. // Handle the Disconnect button.
  189. case WM_COMMAND:
  190. switch (LOWORD(wparam)) {
  191. case IDC_DISCONNECT:
  192. EndDialog();
  193. return TRUE;
  194. }
  195. return FALSE;
  196. // Ensure we don't try to use the HWND anymore.
  197. case WM_DESTROY:
  198. hwnd_ = nullptr;
  199. // Ensure that the disconnect callback is invoked even if somehow our
  200. // window gets destroyed.
  201. EndDialog();
  202. return TRUE;
  203. // Ensure the dialog stays visible if the work area dimensions change.
  204. case WM_SETTINGCHANGE:
  205. if (wparam == SPI_SETWORKAREA)
  206. SetDialogPosition();
  207. return TRUE;
  208. // Ensure the dialog stays visible if the display dimensions change.
  209. case WM_DISPLAYCHANGE:
  210. SetDialogPosition();
  211. return TRUE;
  212. // Handle the disconnect hot-key.
  213. case WM_HOTKEY:
  214. EndDialog();
  215. return TRUE;
  216. // Let the window be draggable by its client area by responding
  217. // that the entire window is the title bar.
  218. case WM_NCHITTEST:
  219. SetWindowLongPtr(hwnd, DWLP_MSGRESULT, HTCAPTION);
  220. return TRUE;
  221. case WM_PAINT: {
  222. // Draw the client area after ShowWindow is used to make |hwnd_| visible.
  223. PAINTSTRUCT ps;
  224. HDC hdc = BeginPaint(hwnd_, &ps);
  225. DrawBorder(hwnd_, hdc);
  226. EndPaint(hwnd_, &ps);
  227. return TRUE;
  228. }
  229. case WM_PRINTCLIENT: {
  230. // Refresh the dialog client area. Called after AnimateWindow is used to
  231. // reshow the dialog.
  232. HDC hdc = reinterpret_cast<HDC>(wparam);
  233. DrawBorder(hwnd_, hdc);
  234. return TRUE;
  235. }
  236. }
  237. return FALSE;
  238. }
  239. bool DisconnectWindowWin::BeginDialog() {
  240. DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  241. DCHECK(!hwnd_);
  242. hwnd_ =
  243. CreateDialogParam(CURRENT_MODULE(), MAKEINTRESOURCE(IDD_DISCONNECT),
  244. nullptr, DialogProc, reinterpret_cast<LPARAM>(this));
  245. if (!hwnd_)
  246. return false;
  247. // Set up handler for Ctrl-Alt-Esc shortcut.
  248. if (!has_hotkey_ && RegisterHotKey(hwnd_, DISCONNECT_HOTKEY_ID,
  249. MOD_ALT | MOD_CONTROL, VK_ESCAPE)) {
  250. has_hotkey_ = true;
  251. }
  252. if (!SetStrings())
  253. return false;
  254. SetDialogPosition();
  255. ShowWindow(hwnd_, SW_SHOW);
  256. return IsWindowVisible(hwnd_) != FALSE;
  257. }
  258. void DisconnectWindowWin::EndDialog() {
  259. DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  260. if (has_hotkey_) {
  261. UnregisterHotKey(hwnd_, DISCONNECT_HOTKEY_ID);
  262. has_hotkey_ = false;
  263. }
  264. if (hwnd_) {
  265. DestroyWindow(hwnd_);
  266. hwnd_ = nullptr;
  267. }
  268. // Disable auto-hide events since the window has been destroyed.
  269. auto_hide_timer_.Stop();
  270. if (client_session_control_)
  271. client_session_control_->DisconnectSession(protocol::OK);
  272. }
  273. void DisconnectWindowWin::ShowDialog() {
  274. // Always reset the hide timer when this method is called.
  275. if (local_input_monitor_) {
  276. auto_hide_timer_.Start(FROM_HERE, kAutoHideTimeout,
  277. base::BindOnce(&DisconnectWindowWin::HideDialog,
  278. base::Unretained(this)));
  279. }
  280. if (!was_auto_hidden_)
  281. return;
  282. // Make sure the dialog is fully visible when it is reshown.
  283. if (!local_input_seen_)
  284. SetDialogPosition();
  285. if (!AnimateWindow(hwnd_, kAnimationDurationMs, AW_BLEND)) {
  286. PLOG(ERROR) << "AnimateWindow() failed to show dialog: ";
  287. ShowWindow(hwnd_, SW_SHOW);
  288. // If the window still isn't visible, then disconnect the session.
  289. if (!IsWindowVisible(hwnd_))
  290. client_session_control_->DisconnectSession(protocol::OK);
  291. }
  292. was_auto_hidden_ = false;
  293. }
  294. void DisconnectWindowWin::HideDialog() {
  295. if (was_auto_hidden_ || !local_input_monitor_ || !hwnd_)
  296. return;
  297. if (!AnimateWindow(hwnd_, kAnimationDurationMs, AW_BLEND | AW_HIDE))
  298. PLOG(ERROR) << "AnimateWindow() failed to hide dialog: ";
  299. else
  300. was_auto_hidden_ = true;
  301. }
  302. void DisconnectWindowWin::StopAutoHideBehavior() {
  303. auto_hide_timer_.Stop();
  304. local_input_monitor_.reset();
  305. ShowDialog();
  306. }
  307. void DisconnectWindowWin::OnLocalMouseEvent(
  308. const webrtc::DesktopVector& position,
  309. ui::EventType type) {
  310. // Don't show the dialog if the position changes by ~1px in any direction.
  311. // This will prevent the dialog from being reshown due to small movements
  312. // caused by hardware/software issues which cause cursor drift or small
  313. // vibrations in the environment around the remote host.
  314. if (std::abs(position.x() - mouse_position_.x()) > 1 ||
  315. std::abs(position.y() - mouse_position_.y()) > 1) {
  316. // Show the dialog before setting |local_input_seen_|. That way the dialog
  317. // will be shown in the center position and subsequent reshows will honor
  318. // the new position (if any) the dialog is moved to.
  319. ShowDialog();
  320. local_input_seen_ = true;
  321. }
  322. mouse_position_ = position;
  323. }
  324. void DisconnectWindowWin::OnLocalKeyPressed(uint32_t usb_keycode) {
  325. // Show the dialog before setting |local_input_seen_|. That way the dialog
  326. // will be shown in the center position and subsequent reshows will honor
  327. // the new position (if any) the dialog is moved to.
  328. ShowDialog();
  329. local_input_seen_ = true;
  330. }
  331. void DisconnectWindowWin::DrawBorder(HWND hwnd, HDC hdc) {
  332. RECT rect;
  333. GetClientRect(hwnd, &rect);
  334. base::win::ScopedSelectObject border(hdc, border_pen_.get());
  335. base::win::ScopedSelectObject brush(hdc, GetStockObject(NULL_BRUSH));
  336. RoundRect(hdc, rect.left, rect.top, rect.right - 1, rect.bottom - 1,
  337. kWindowBorderRadius, kWindowBorderRadius);
  338. }
  339. // Returns |control| rectangle in the dialog coordinates.
  340. bool DisconnectWindowWin::GetControlRect(HWND control, RECT* rect) {
  341. if (!GetWindowRect(control, rect))
  342. return false;
  343. SetLastError(ERROR_SUCCESS);
  344. int result = MapWindowPoints(HWND_DESKTOP, hwnd_,
  345. reinterpret_cast<LPPOINT>(rect), 2);
  346. if (!result && GetLastError() != ERROR_SUCCESS)
  347. return false;
  348. return true;
  349. }
  350. void DisconnectWindowWin::SetDialogPosition() {
  351. DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  352. // Try to center the window above the task-bar. If that fails, use the
  353. // primary monitor. If that fails (very unlikely), use the default position.
  354. HWND taskbar = FindWindow(kShellTrayWindowName, nullptr);
  355. HMONITOR monitor = MonitorFromWindow(taskbar, MONITOR_DEFAULTTOPRIMARY);
  356. MONITORINFO monitor_info = {sizeof(monitor_info)};
  357. RECT window_rect;
  358. if (!GetMonitorInfo(monitor, &monitor_info) ||
  359. !GetWindowRect(hwnd_, &window_rect)) {
  360. return;
  361. }
  362. int window_width = window_rect.right - window_rect.left;
  363. int window_height = window_rect.bottom - window_rect.top;
  364. // Default settings will display the window above the taskbar and centered
  365. // along the x axis.
  366. int top = monitor_info.rcWork.bottom - window_height;
  367. int left =
  368. (monitor_info.rcWork.right + monitor_info.rcWork.left - window_width) / 2;
  369. // Adjust the top value if the window is in auto-hide mode and we have not
  370. // seen local input yet. We adjust the position to make the dialog a bit more
  371. // obtrusive so that a local user will notice it before it auto-hides.
  372. if (local_input_monitor_ && !local_input_seen_)
  373. top = top * 0.7;
  374. SetWindowPos(hwnd_, nullptr, left, top, 0, 0, SWP_NOSIZE | SWP_NOZORDER);
  375. }
  376. bool DisconnectWindowWin::SetStrings() {
  377. DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  378. // Localize the disconnect button text and measure length of the old and new
  379. // labels.
  380. HWND hwnd_button = GetDlgItem(hwnd_, IDC_DISCONNECT);
  381. HWND hwnd_message = GetDlgItem(hwnd_, IDC_DISCONNECT_SHARINGWITH);
  382. if (!hwnd_button || !hwnd_message)
  383. return false;
  384. std::wstring button_text;
  385. std::wstring message_text;
  386. if (!GetControlText(hwnd_button, &button_text) ||
  387. !GetControlText(hwnd_message, &message_text)) {
  388. return false;
  389. }
  390. // Format and truncate "Your desktop is shared with ..." message.
  391. message_text = base::AsWString(base::ReplaceStringPlaceholders(
  392. base::AsString16(message_text), base::UTF8ToUTF16(username_), nullptr));
  393. if (message_text.length() > kMaxSharingWithTextLength)
  394. message_text.erase(kMaxSharingWithTextLength);
  395. if (!SetWindowText(hwnd_message, message_text.c_str()))
  396. return false;
  397. // Calculate the margin between controls in pixels.
  398. RECT rect = {0};
  399. rect.right = kWindowTextMargin;
  400. if (!MapDialogRect(hwnd_, &rect))
  401. return false;
  402. int margin = rect.right;
  403. // Resize |hwnd_message| so that the text is not clipped.
  404. RECT message_rect;
  405. if (!GetControlRect(hwnd_message, &message_rect))
  406. return false;
  407. LONG control_width;
  408. if (!GetControlTextWidth(hwnd_message, message_text, &control_width))
  409. return false;
  410. message_rect.right = message_rect.left + control_width + margin;
  411. if (!SetWindowPos(hwnd_message, nullptr,
  412. message_rect.left, message_rect.top,
  413. message_rect.right - message_rect.left,
  414. message_rect.bottom - message_rect.top,
  415. SWP_NOZORDER)) {
  416. return false;
  417. }
  418. // Reposition and resize |hwnd_button| as well.
  419. RECT button_rect;
  420. if (!GetControlRect(hwnd_button, &button_rect))
  421. return false;
  422. if (!GetControlTextWidth(hwnd_button, button_text, &control_width))
  423. return false;
  424. button_rect.left = message_rect.right;
  425. button_rect.right = button_rect.left + control_width + margin * 2;
  426. if (!SetWindowPos(hwnd_button, nullptr,
  427. button_rect.left, button_rect.top,
  428. button_rect.right - button_rect.left,
  429. button_rect.bottom - button_rect.top,
  430. SWP_NOZORDER)) {
  431. return false;
  432. }
  433. // Resize the whole window to fit the resized controls.
  434. RECT window_rect;
  435. if (!GetWindowRect(hwnd_, &window_rect))
  436. return false;
  437. int width = button_rect.right + margin;
  438. int height = window_rect.bottom - window_rect.top;
  439. if (!SetWindowPos(hwnd_, nullptr, 0, 0, width, height,
  440. SWP_NOMOVE | SWP_NOZORDER)) {
  441. return false;
  442. }
  443. // Make the corners of the disconnect window rounded.
  444. HRGN rgn = CreateRoundRectRgn(0, 0, width, height, kWindowBorderRadius,
  445. kWindowBorderRadius);
  446. if (!rgn)
  447. return false;
  448. if (!SetWindowRgn(hwnd_, rgn, TRUE))
  449. return false;
  450. return true;
  451. }
  452. } // namespace
  453. // static
  454. std::unique_ptr<HostWindow> HostWindow::CreateDisconnectWindow() {
  455. return std::make_unique<DisconnectWindowWin>();
  456. }
  457. std::unique_ptr<HostWindow> HostWindow::CreateAutoHidingDisconnectWindow(
  458. std::unique_ptr<LocalInputMonitor> local_input_monitor) {
  459. auto disconnect_window = std::make_unique<DisconnectWindowWin>();
  460. disconnect_window->EnableAutoHide(std::move(local_input_monitor));
  461. return disconnect_window;
  462. }
  463. } // namespace remoting