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
5#include <windows.h>
6
7#include "base/compiler_specific.h"
8#include "base/logging.h"
9#include "base/process/memory.h"
10#include "base/strings/string_util.h"
11#include "base/strings/utf_string_conversions.h"
12#include "base/win/scoped_gdi_object.h"
13#include "base/win/scoped_hdc.h"
14#include "base/win/scoped_select_object.h"
15#include "remoting/host/client_session_control.h"
16#include "remoting/host/host_window.h"
17#include "remoting/host/win/core_resource.h"
18
19namespace remoting {
20
21namespace {
22
23const int DISCONNECT_HOTKEY_ID = 1000;
24
25// Maximum length of "Your desktop is shared with ..." message in UTF-16
26// characters.
27const size_t kMaxSharingWithTextLength = 100;
28
29const wchar_t kShellTrayWindowName[] = L"Shell_TrayWnd";
30const int kWindowBorderRadius = 14;
31
32// Margin between dialog controls (in dialog units).
33const int kWindowTextMargin = 8;
34
35class DisconnectWindowWin : public HostWindow {
36 public:
37  DisconnectWindowWin();
38  virtual ~DisconnectWindowWin();
39
40  // HostWindow overrides.
41  virtual void Start(
42      const base::WeakPtr<ClientSessionControl>& client_session_control)
43      OVERRIDE;
44
45 protected:
46  static INT_PTR CALLBACK DialogProc(HWND hwnd, UINT message, WPARAM wparam,
47                                     LPARAM lparam);
48
49  BOOL OnDialogMessage(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
50
51  // Creates the dialog window and registers the disconnect hot key.
52  bool BeginDialog();
53
54  // Closes the dialog, unregisters the hot key and invokes the disconnect
55  // callback, if set.
56  void EndDialog();
57
58  // Returns |control| rectangle in the dialog coordinates.
59  bool GetControlRect(HWND control, RECT* rect);
60
61  // Trys to position the dialog window above the taskbar.
62  void SetDialogPosition();
63
64  // Applies localization string and resizes the dialog.
65  bool SetStrings();
66
67  // Used to disconnect the client session.
68  base::WeakPtr<ClientSessionControl> client_session_control_;
69
70  // Specifies the remote user name.
71  std::string username_;
72
73  HWND hwnd_;
74  bool has_hotkey_;
75  base::win::ScopedGDIObject<HPEN> border_pen_;
76
77  DISALLOW_COPY_AND_ASSIGN(DisconnectWindowWin);
78};
79
80// Returns the text for the given dialog control window.
81bool GetControlText(HWND control, base::string16* text) {
82  // GetWindowText truncates the text if it is longer than can fit into
83  // the buffer.
84  WCHAR buffer[256];
85  int result = GetWindowText(control, buffer, arraysize(buffer));
86  if (!result)
87    return false;
88
89  text->assign(buffer);
90  return true;
91}
92
93// Returns width |text| rendered in |control| window.
94bool GetControlTextWidth(HWND control,
95                         const base::string16& text,
96                         LONG* width) {
97  RECT rect = {0, 0, 0, 0};
98  base::win::ScopedGetDC dc(control);
99  base::win::ScopedSelectObject font(
100      dc, (HFONT)SendMessage(control, WM_GETFONT, 0, 0));
101  if (!DrawText(dc, text.c_str(), -1, &rect, DT_CALCRECT | DT_SINGLELINE))
102    return false;
103
104  *width = rect.right;
105  return true;
106}
107
108DisconnectWindowWin::DisconnectWindowWin()
109    : hwnd_(NULL),
110      has_hotkey_(false),
111      border_pen_(CreatePen(PS_SOLID, 5,
112                            RGB(0.13 * 255, 0.69 * 255, 0.11 * 255))) {
113}
114
115DisconnectWindowWin::~DisconnectWindowWin() {
116  EndDialog();
117}
118
119void DisconnectWindowWin::Start(
120    const base::WeakPtr<ClientSessionControl>& client_session_control) {
121  DCHECK(CalledOnValidThread());
122  DCHECK(!client_session_control_);
123  DCHECK(client_session_control);
124
125  client_session_control_ = client_session_control;
126
127  std::string client_jid = client_session_control_->client_jid();
128  username_ = client_jid.substr(0, client_jid.find('/'));
129  if (!BeginDialog())
130    EndDialog();
131}
132
133INT_PTR CALLBACK DisconnectWindowWin::DialogProc(HWND hwnd,
134                                                 UINT message,
135                                                 WPARAM wparam,
136                                                 LPARAM lparam) {
137  LONG_PTR self = NULL;
138  if (message == WM_INITDIALOG) {
139    self = lparam;
140
141    // Store |this| to the window's user data.
142    SetLastError(ERROR_SUCCESS);
143    LONG_PTR result = SetWindowLongPtr(hwnd, DWLP_USER, self);
144    if (result == 0 && GetLastError() != ERROR_SUCCESS)
145      reinterpret_cast<DisconnectWindowWin*>(self)->EndDialog();
146  } else {
147    self = GetWindowLongPtr(hwnd, DWLP_USER);
148  }
149
150  if (self) {
151    return reinterpret_cast<DisconnectWindowWin*>(self)->OnDialogMessage(
152        hwnd, message, wparam, lparam);
153  }
154  return FALSE;
155}
156
157BOOL DisconnectWindowWin::OnDialogMessage(HWND hwnd,
158                                          UINT message,
159                                          WPARAM wparam,
160                                          LPARAM lparam) {
161  DCHECK(CalledOnValidThread());
162
163  switch (message) {
164    // Ignore close messages.
165    case WM_CLOSE:
166      return TRUE;
167
168    // Handle the Disconnect button.
169    case WM_COMMAND:
170      switch (LOWORD(wparam)) {
171        case IDC_DISCONNECT:
172          EndDialog();
173          return TRUE;
174      }
175      return FALSE;
176
177    // Ensure we don't try to use the HWND anymore.
178    case WM_DESTROY:
179      hwnd_ = NULL;
180
181      // Ensure that the disconnect callback is invoked even if somehow our
182      // window gets destroyed.
183      EndDialog();
184
185      return TRUE;
186
187    // Ensure the dialog stays visible if the work area dimensions change.
188    case WM_SETTINGCHANGE:
189      if (wparam == SPI_SETWORKAREA)
190        SetDialogPosition();
191      return TRUE;
192
193    // Ensure the dialog stays visible if the display dimensions change.
194    case WM_DISPLAYCHANGE:
195      SetDialogPosition();
196      return TRUE;
197
198    // Handle the disconnect hot-key.
199    case WM_HOTKEY:
200      EndDialog();
201      return TRUE;
202
203    // Let the window be draggable by its client area by responding
204    // that the entire window is the title bar.
205    case WM_NCHITTEST:
206      SetWindowLongPtr(hwnd, DWLP_MSGRESULT, HTCAPTION);
207      return TRUE;
208
209    case WM_PAINT: {
210      PAINTSTRUCT ps;
211      HDC hdc = BeginPaint(hwnd_, &ps);
212      RECT rect;
213      GetClientRect(hwnd_, &rect);
214      {
215        base::win::ScopedSelectObject border(hdc, border_pen_);
216        base::win::ScopedSelectObject brush(hdc, GetStockObject(NULL_BRUSH));
217        RoundRect(hdc, rect.left, rect.top, rect.right - 1, rect.bottom - 1,
218                  kWindowBorderRadius, kWindowBorderRadius);
219      }
220      EndPaint(hwnd_, &ps);
221      return TRUE;
222    }
223  }
224  return FALSE;
225}
226
227bool DisconnectWindowWin::BeginDialog() {
228  DCHECK(CalledOnValidThread());
229  DCHECK(!hwnd_);
230
231  HMODULE module = base::GetModuleFromAddress(&DialogProc);
232  hwnd_ = CreateDialogParam(module, MAKEINTRESOURCE(IDD_DISCONNECT), NULL,
233                            DialogProc, reinterpret_cast<LPARAM>(this));
234  if (!hwnd_)
235    return false;
236
237  // Set up handler for Ctrl-Alt-Esc shortcut.
238  if (!has_hotkey_ && RegisterHotKey(hwnd_, DISCONNECT_HOTKEY_ID,
239                                     MOD_ALT | MOD_CONTROL, VK_ESCAPE)) {
240    has_hotkey_ = true;
241  }
242
243  if (!SetStrings())
244    return false;
245
246  SetDialogPosition();
247  ShowWindow(hwnd_, SW_SHOW);
248  return IsWindowVisible(hwnd_) != FALSE;
249}
250
251void DisconnectWindowWin::EndDialog() {
252  DCHECK(CalledOnValidThread());
253
254  if (has_hotkey_) {
255    UnregisterHotKey(hwnd_, DISCONNECT_HOTKEY_ID);
256    has_hotkey_ = false;
257  }
258
259  if (hwnd_) {
260    DestroyWindow(hwnd_);
261    hwnd_ = NULL;
262  }
263
264  if (client_session_control_)
265    client_session_control_->DisconnectSession();
266}
267
268// Returns |control| rectangle in the dialog coordinates.
269bool DisconnectWindowWin::GetControlRect(HWND control, RECT* rect) {
270  if (!GetWindowRect(control, rect))
271    return false;
272  SetLastError(ERROR_SUCCESS);
273  int result = MapWindowPoints(HWND_DESKTOP, hwnd_,
274                               reinterpret_cast<LPPOINT>(rect), 2);
275  if (!result && GetLastError() != ERROR_SUCCESS)
276    return false;
277
278  return true;
279}
280
281void DisconnectWindowWin::SetDialogPosition() {
282  DCHECK(CalledOnValidThread());
283
284  // Try to center the window above the task-bar. If that fails, use the
285  // primary monitor. If that fails (very unlikely), use the default position.
286  HWND taskbar = FindWindow(kShellTrayWindowName, NULL);
287  HMONITOR monitor = MonitorFromWindow(taskbar, MONITOR_DEFAULTTOPRIMARY);
288  MONITORINFO monitor_info = {sizeof(monitor_info)};
289  RECT window_rect;
290  if (GetMonitorInfo(monitor, &monitor_info) &&
291      GetWindowRect(hwnd_, &window_rect)) {
292    int window_width = window_rect.right - window_rect.left;
293    int window_height = window_rect.bottom - window_rect.top;
294    int top = monitor_info.rcWork.bottom - window_height;
295    int left = (monitor_info.rcWork.right + monitor_info.rcWork.left -
296        window_width) / 2;
297    SetWindowPos(hwnd_, NULL, left, top, 0, 0, SWP_NOSIZE | SWP_NOZORDER);
298  }
299}
300
301bool DisconnectWindowWin::SetStrings() {
302  DCHECK(CalledOnValidThread());
303
304  // Localize the disconnect button text and measure length of the old and new
305  // labels.
306  HWND hwnd_button = GetDlgItem(hwnd_, IDC_DISCONNECT);
307  HWND hwnd_message = GetDlgItem(hwnd_, IDC_DISCONNECT_SHARINGWITH);
308  if (!hwnd_button || !hwnd_message)
309    return false;
310
311  base::string16 button_text;
312  base::string16 message_text;
313  if (!GetControlText(hwnd_button, &button_text) ||
314      !GetControlText(hwnd_message, &message_text)) {
315    return false;
316  }
317
318  // Format and truncate "Your desktop is shared with ..." message.
319  message_text = ReplaceStringPlaceholders(message_text,
320                                           base::UTF8ToUTF16(username_),
321                                           NULL);
322  if (message_text.length() > kMaxSharingWithTextLength)
323    message_text.erase(kMaxSharingWithTextLength);
324
325  if (!SetWindowText(hwnd_message, message_text.c_str()))
326    return false;
327
328  // Calculate the margin between controls in pixels.
329  RECT rect = {0};
330  rect.right = kWindowTextMargin;
331  if (!MapDialogRect(hwnd_, &rect))
332    return false;
333  int margin = rect.right;
334
335  // Resize |hwnd_message| so that the text is not clipped.
336  RECT message_rect;
337  if (!GetControlRect(hwnd_message, &message_rect))
338    return false;
339
340  LONG control_width;
341  if (!GetControlTextWidth(hwnd_message, message_text, &control_width))
342    return false;
343  message_rect.right = message_rect.left + control_width + margin;
344
345  if (!SetWindowPos(hwnd_message, NULL,
346                    message_rect.left, message_rect.top,
347                    message_rect.right - message_rect.left,
348                    message_rect.bottom - message_rect.top,
349                    SWP_NOZORDER)) {
350    return false;
351  }
352
353  // Reposition and resize |hwnd_button| as well.
354  RECT button_rect;
355  if (!GetControlRect(hwnd_button, &button_rect))
356    return false;
357
358  if (!GetControlTextWidth(hwnd_button, button_text, &control_width))
359    return false;
360
361  button_rect.left = message_rect.right;
362  button_rect.right = button_rect.left + control_width + margin * 2;
363  if (!SetWindowPos(hwnd_button, NULL,
364                    button_rect.left, button_rect.top,
365                    button_rect.right - button_rect.left,
366                    button_rect.bottom - button_rect.top,
367                    SWP_NOZORDER)) {
368    return false;
369  }
370
371  // Resize the whole window to fit the resized controls.
372  RECT window_rect;
373  if (!GetWindowRect(hwnd_, &window_rect))
374    return false;
375  int width = button_rect.right + margin;
376  int height = window_rect.bottom - window_rect.top;
377  if (!SetWindowPos(hwnd_, NULL, 0, 0, width, height,
378                    SWP_NOMOVE | SWP_NOZORDER)) {
379    return false;
380  }
381
382  // Make the corners of the disconnect window rounded.
383  HRGN rgn = CreateRoundRectRgn(0, 0, width, height, kWindowBorderRadius,
384                                kWindowBorderRadius);
385  if (!rgn)
386    return false;
387  if (!SetWindowRgn(hwnd_, rgn, TRUE))
388    return false;
389
390  return true;
391}
392
393} // namespace
394
395// static
396scoped_ptr<HostWindow> HostWindow::CreateDisconnectWindow() {
397  return scoped_ptr<HostWindow>(new DisconnectWindowWin());
398}
399
400}  // namespace remoting
401