disconnect_window_win.cc revision 5e3f23d412006dc4db4e659864679f29341e113f
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_util.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/ui_strings.h"
18#include "remoting/host/win/core_resource.h"
19
20namespace remoting {
21
22namespace {
23
24const int DISCONNECT_HOTKEY_ID = 1000;
25
26// Maximum length of "Your desktop is shared with ..." message in UTF-16
27// characters.
28const size_t kMaxSharingWithTextLength = 100;
29
30const wchar_t kShellTrayWindowName[] = L"Shell_TrayWnd";
31const int kWindowBorderRadius = 14;
32
33class DisconnectWindowWin : public HostWindow {
34 public:
35  explicit DisconnectWindowWin(const UiStrings& ui_strings);
36  virtual ~DisconnectWindowWin();
37
38  // HostWindow overrides.
39  virtual void Start(
40      const base::WeakPtr<ClientSessionControl>& client_session_control)
41      OVERRIDE;
42
43 protected:
44  static INT_PTR CALLBACK DialogProc(HWND hwnd, UINT message, WPARAM wparam,
45                                     LPARAM lparam);
46
47  BOOL OnDialogMessage(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
48
49  // Creates the dialog window and registers the disconnect hot key.
50  bool BeginDialog();
51
52  // Closes the dialog, unregisters the hot key and invokes the disconnect
53  // callback, if set.
54  void EndDialog();
55
56  // Trys to position the dialog window above the taskbar.
57  void SetDialogPosition();
58
59  // Applies localization string and resizes the dialog.
60  bool SetStrings();
61
62  // Used to disconnect the client session.
63  base::WeakPtr<ClientSessionControl> client_session_control_;
64
65  // Localized UI strings.
66  UiStrings ui_strings_;
67
68  // Specifies the remote user name.
69  std::string username_;
70
71  HWND hwnd_;
72  bool has_hotkey_;
73  base::win::ScopedGDIObject<HPEN> border_pen_;
74
75  DISALLOW_COPY_AND_ASSIGN(DisconnectWindowWin);
76};
77
78int GetControlTextWidth(HWND control) {
79  RECT rect = {0, 0, 0, 0};
80  WCHAR text[256];
81  int result = GetWindowText(control, text, arraysize(text));
82  if (result) {
83    base::win::ScopedGetDC dc(control);
84    base::win::ScopedSelectObject font(
85        dc, (HFONT)SendMessage(control, WM_GETFONT, 0, 0));
86    DrawText(dc, text, -1, &rect, DT_CALCRECT | DT_SINGLELINE);
87  }
88  return rect.right;
89}
90
91DisconnectWindowWin::DisconnectWindowWin(const UiStrings& ui_strings)
92    : ui_strings_(ui_strings),
93      hwnd_(NULL),
94      has_hotkey_(false),
95      border_pen_(CreatePen(PS_SOLID, 5,
96                            RGB(0.13 * 255, 0.69 * 255, 0.11 * 255))) {
97}
98
99DisconnectWindowWin::~DisconnectWindowWin() {
100  EndDialog();
101}
102
103void DisconnectWindowWin::Start(
104    const base::WeakPtr<ClientSessionControl>& client_session_control) {
105  DCHECK(CalledOnValidThread());
106  DCHECK(!client_session_control_);
107  DCHECK(client_session_control);
108
109  client_session_control_ = client_session_control;
110
111  std::string client_jid = client_session_control_->client_jid();
112  username_ = client_jid.substr(0, client_jid.find('/'));
113  if (!BeginDialog())
114    EndDialog();
115}
116
117INT_PTR CALLBACK DisconnectWindowWin::DialogProc(HWND hwnd,
118                                                 UINT message,
119                                                 WPARAM wparam,
120                                                 LPARAM lparam) {
121  LONG_PTR self = NULL;
122  if (message == WM_INITDIALOG) {
123    self = lparam;
124
125    // Store |this| to the window's user data.
126    SetLastError(ERROR_SUCCESS);
127    LONG_PTR result = SetWindowLongPtr(hwnd, DWLP_USER, self);
128    if (result == 0 && GetLastError() != ERROR_SUCCESS)
129      reinterpret_cast<DisconnectWindowWin*>(self)->EndDialog();
130  } else {
131    self = GetWindowLongPtr(hwnd, DWLP_USER);
132  }
133
134  if (self) {
135    return reinterpret_cast<DisconnectWindowWin*>(self)->OnDialogMessage(
136        hwnd, message, wparam, lparam);
137  }
138  return FALSE;
139}
140
141BOOL DisconnectWindowWin::OnDialogMessage(HWND hwnd,
142                                          UINT message,
143                                          WPARAM wparam,
144                                          LPARAM lparam) {
145  DCHECK(CalledOnValidThread());
146
147  switch (message) {
148    // Ignore close messages.
149    case WM_CLOSE:
150      return TRUE;
151
152    // Handle the Disconnect button.
153    case WM_COMMAND:
154      switch (LOWORD(wparam)) {
155        case IDC_DISCONNECT:
156          EndDialog();
157          return TRUE;
158      }
159      return FALSE;
160
161    // Ensure we don't try to use the HWND anymore.
162    case WM_DESTROY:
163      hwnd_ = NULL;
164
165      // Ensure that the disconnect callback is invoked even if somehow our
166      // window gets destroyed.
167      EndDialog();
168
169      return TRUE;
170
171    // Ensure the dialog stays visible if the work area dimensions change.
172    case WM_SETTINGCHANGE:
173      if (wparam == SPI_SETWORKAREA)
174        SetDialogPosition();
175      return TRUE;
176
177    // Ensure the dialog stays visible if the display dimensions change.
178    case WM_DISPLAYCHANGE:
179      SetDialogPosition();
180      return TRUE;
181
182    // Handle the disconnect hot-key.
183    case WM_HOTKEY:
184      EndDialog();
185      return TRUE;
186
187    // Let the window be draggable by its client area by responding
188    // that the entire window is the title bar.
189    case WM_NCHITTEST:
190      SetWindowLongPtr(hwnd, DWLP_MSGRESULT, HTCAPTION);
191      return TRUE;
192
193    case WM_PAINT: {
194      PAINTSTRUCT ps;
195      HDC hdc = BeginPaint(hwnd_, &ps);
196      RECT rect;
197      GetClientRect(hwnd_, &rect);
198      {
199        base::win::ScopedSelectObject border(hdc, border_pen_);
200        base::win::ScopedSelectObject brush(hdc, GetStockObject(NULL_BRUSH));
201        RoundRect(hdc, rect.left, rect.top, rect.right - 1, rect.bottom - 1,
202                  kWindowBorderRadius, kWindowBorderRadius);
203      }
204      EndPaint(hwnd_, &ps);
205      return TRUE;
206    }
207  }
208  return FALSE;
209}
210
211bool DisconnectWindowWin::BeginDialog() {
212  DCHECK(CalledOnValidThread());
213  DCHECK(!hwnd_);
214
215  // Load the dialog resource so that we can modify the RTL flags if necessary.
216  HMODULE module = base::GetModuleFromAddress(&DialogProc);
217  HRSRC dialog_resource =
218      FindResource(module, MAKEINTRESOURCE(IDD_DISCONNECT), RT_DIALOG);
219  if (!dialog_resource)
220    return false;
221
222  HGLOBAL dialog_template = LoadResource(module, dialog_resource);
223  if (!dialog_template)
224    return false;
225
226  DLGTEMPLATE* dialog_pointer =
227      reinterpret_cast<DLGTEMPLATE*>(LockResource(dialog_template));
228  if (!dialog_pointer)
229    return false;
230
231  // The actual resource type is DLGTEMPLATEEX, but this is not defined in any
232  // standard headers, so we treat it as a generic pointer and manipulate the
233  // correct offsets explicitly.
234  scoped_ptr<unsigned char[]> rtl_dialog_template;
235  if (ui_strings_.direction == UiStrings::RTL) {
236    unsigned long dialog_template_size =
237        SizeofResource(module, dialog_resource);
238    rtl_dialog_template.reset(new unsigned char[dialog_template_size]);
239    memcpy(rtl_dialog_template.get(), dialog_pointer, dialog_template_size);
240    DWORD* rtl_dwords = reinterpret_cast<DWORD*>(rtl_dialog_template.get());
241    rtl_dwords[2] |= (WS_EX_LAYOUTRTL | WS_EX_RTLREADING);
242    dialog_pointer = reinterpret_cast<DLGTEMPLATE*>(rtl_dwords);
243  }
244
245  hwnd_ = CreateDialogIndirectParam(module, dialog_pointer, NULL,
246                                    DialogProc, reinterpret_cast<LPARAM>(this));
247  if (!hwnd_)
248    return false;
249
250  // Set up handler for Ctrl-Alt-Esc shortcut.
251  if (!has_hotkey_ && RegisterHotKey(hwnd_, DISCONNECT_HOTKEY_ID,
252                                     MOD_ALT | MOD_CONTROL, VK_ESCAPE)) {
253    has_hotkey_ = true;
254  }
255
256  if (!SetStrings())
257    return false;
258
259  SetDialogPosition();
260  ShowWindow(hwnd_, SW_SHOW);
261  return IsWindowVisible(hwnd_) != FALSE;
262}
263
264void DisconnectWindowWin::EndDialog() {
265  DCHECK(CalledOnValidThread());
266
267  if (has_hotkey_) {
268    UnregisterHotKey(hwnd_, DISCONNECT_HOTKEY_ID);
269    has_hotkey_ = false;
270  }
271
272  if (hwnd_) {
273    DestroyWindow(hwnd_);
274    hwnd_ = NULL;
275  }
276
277  if (client_session_control_)
278    client_session_control_->DisconnectSession();
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  if (!SetWindowText(hwnd_, ui_strings_.product_name.c_str()))
305    return false;
306
307  // Localize the disconnect button text and measure length of the old and new
308  // labels.
309  HWND disconnect_button = GetDlgItem(hwnd_, IDC_DISCONNECT);
310  if (!disconnect_button)
311    return false;
312  int button_old_required_width = GetControlTextWidth(disconnect_button);
313  if (!SetWindowText(disconnect_button,
314                     ui_strings_.disconnect_button_text.c_str())) {
315    return false;
316  }
317  int button_new_required_width = GetControlTextWidth(disconnect_button);
318  int button_width_delta =
319      button_new_required_width - button_old_required_width;
320
321  // Format and truncate "Your desktop is shared with ..." message.
322  string16 text = ReplaceStringPlaceholders(ui_strings_.disconnect_message,
323                                            UTF8ToUTF16(username_), NULL);
324  if (text.length() > kMaxSharingWithTextLength)
325    text.erase(kMaxSharingWithTextLength);
326
327  // Set localized and truncated "Your desktop is shared with ..." message and
328  // measure length of the old and new text.
329  HWND sharing_with_label = GetDlgItem(hwnd_, IDC_DISCONNECT_SHARINGWITH);
330  if (!sharing_with_label)
331    return false;
332  int label_old_required_width = GetControlTextWidth(sharing_with_label);
333  if (!SetWindowText(sharing_with_label, text.c_str()))
334    return false;
335  int label_new_required_width = GetControlTextWidth(sharing_with_label);
336  int label_width_delta = label_new_required_width - label_old_required_width;
337
338  // Reposition the controls such that the label lies to the left of the
339  // disconnect button (assuming LTR layout). The dialog template determines
340  // the controls' spacing; update their size to fit the localized content.
341  RECT label_rect;
342  if (!GetClientRect(sharing_with_label, &label_rect))
343    return false;
344  if (!SetWindowPos(sharing_with_label, NULL, 0, 0,
345                    label_rect.right + label_width_delta, label_rect.bottom,
346                    SWP_NOMOVE | SWP_NOZORDER)) {
347    return false;
348  }
349
350  // Reposition the disconnect button as well.
351  RECT button_rect;
352  if (!GetWindowRect(disconnect_button, &button_rect))
353    return false;
354  int button_width = button_rect.right - button_rect.left;
355  int button_height = button_rect.bottom - button_rect.top;
356  SetLastError(ERROR_SUCCESS);
357  int result = MapWindowPoints(HWND_DESKTOP, hwnd_,
358                               reinterpret_cast<LPPOINT>(&button_rect), 2);
359  if (!result && GetLastError() != ERROR_SUCCESS)
360    return false;
361  if (!SetWindowPos(disconnect_button, NULL,
362                    button_rect.left + label_width_delta, button_rect.top,
363                    button_width + button_width_delta, button_height,
364                    SWP_NOZORDER)) {
365    return false;
366  }
367
368  // Resize the whole window to fit the resized controls.
369  RECT window_rect;
370  if (!GetWindowRect(hwnd_, &window_rect))
371    return false;
372  int width = (window_rect.right - window_rect.left) +
373      button_width_delta + label_width_delta;
374  int height = window_rect.bottom - window_rect.top;
375  if (!SetWindowPos(hwnd_, NULL, 0, 0, width, height,
376                    SWP_NOMOVE | SWP_NOZORDER)) {
377    return false;
378  }
379
380  // Make the corners of the disconnect window rounded.
381  HRGN rgn = CreateRoundRectRgn(0, 0, width, height, kWindowBorderRadius,
382                                kWindowBorderRadius);
383  if (!rgn)
384    return false;
385  if (!SetWindowRgn(hwnd_, rgn, TRUE))
386    return false;
387
388  return true;
389}
390
391} // namespace
392
393// static
394scoped_ptr<HostWindow> HostWindow::CreateDisconnectWindow(
395    const UiStrings& ui_strings) {
396  return scoped_ptr<HostWindow>(new DisconnectWindowWin(ui_strings));
397}
398
399}  // namespace remoting
400