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#import <Cocoa/Cocoa.h>
6
7#import "remoting/host/disconnect_window_mac.h"
8
9#include "base/bind.h"
10#include "base/compiler_specific.h"
11#include "base/i18n/rtl.h"
12#include "base/memory/weak_ptr.h"
13#include "base/strings/string_util.h"
14#include "base/strings/sys_string_conversions.h"
15#include "remoting/base/string_resources.h"
16#include "remoting/host/client_session_control.h"
17#include "remoting/host/host_window.h"
18#include "ui/base/l10n/l10n_util_mac.h"
19
20@interface DisconnectWindowController()
21- (BOOL)isRToL;
22- (void)Hide;
23@end
24
25const int kMaximumConnectedNameWidthInPixels = 600;
26
27namespace remoting {
28
29class DisconnectWindowMac : public HostWindow {
30 public:
31  DisconnectWindowMac();
32  virtual ~DisconnectWindowMac();
33
34  // HostWindow overrides.
35  virtual void Start(
36      const base::WeakPtr<ClientSessionControl>& client_session_control)
37      OVERRIDE;
38
39 private:
40  DisconnectWindowController* window_controller_;
41
42  DISALLOW_COPY_AND_ASSIGN(DisconnectWindowMac);
43};
44
45DisconnectWindowMac::DisconnectWindowMac()
46    : window_controller_(nil) {
47}
48
49DisconnectWindowMac::~DisconnectWindowMac() {
50  DCHECK(CalledOnValidThread());
51
52  // DisconnectWindowController is responsible for releasing itself in its
53  // windowWillClose: method.
54  [window_controller_ Hide];
55  window_controller_ = nil;
56}
57
58void DisconnectWindowMac::Start(
59    const base::WeakPtr<ClientSessionControl>& client_session_control) {
60  DCHECK(CalledOnValidThread());
61  DCHECK(client_session_control);
62  DCHECK(window_controller_ == nil);
63
64  // Create the window.
65  base::Closure disconnect_callback =
66      base::Bind(&ClientSessionControl::DisconnectSession,
67                 client_session_control);
68  std::string client_jid = client_session_control->client_jid();
69  std::string username = client_jid.substr(0, client_jid.find('/'));
70  window_controller_ =
71      [[DisconnectWindowController alloc] initWithCallback:disconnect_callback
72                                                  username:username];
73  [window_controller_ showWindow:nil];
74}
75
76// static
77scoped_ptr<HostWindow> HostWindow::CreateDisconnectWindow() {
78  return scoped_ptr<HostWindow>(new DisconnectWindowMac());
79}
80
81}  // namespace remoting
82
83@implementation DisconnectWindowController
84- (id)initWithCallback:(const base::Closure&)disconnect_callback
85              username:(const std::string&)username {
86  self = [super initWithWindowNibName:@"disconnect_window"];
87  if (self) {
88    disconnect_callback_ = disconnect_callback;
89    username_ = base::UTF8ToUTF16(username);
90  }
91  return self;
92}
93
94- (void)dealloc {
95  [super dealloc];
96}
97
98- (IBAction)stopSharing:(id)sender {
99  if (!disconnect_callback_.is_null()) {
100    disconnect_callback_.Run();
101  }
102}
103
104- (BOOL)isRToL {
105  return base::i18n::IsRTL();
106}
107
108- (void)Hide {
109  disconnect_callback_.Reset();
110  [self close];
111}
112
113- (void)windowDidLoad {
114  [connectedToField_ setStringValue:l10n_util::GetNSStringF(IDS_MESSAGE_SHARED,
115                                                            username_)];
116  [disconnectButton_ setTitle:l10n_util::GetNSString(IDS_STOP_SHARING_BUTTON)];
117
118  // Resize the window dynamically based on the content.
119  CGFloat oldConnectedWidth = NSWidth([connectedToField_ bounds]);
120  [connectedToField_ sizeToFit];
121  NSRect connectedToFrame = [connectedToField_ frame];
122  CGFloat newConnectedWidth = NSWidth(connectedToFrame);
123
124  // Set a max width for the connected to text field.
125  if (newConnectedWidth > kMaximumConnectedNameWidthInPixels) {
126    newConnectedWidth = kMaximumConnectedNameWidthInPixels;
127    connectedToFrame.size.width = newConnectedWidth;
128    [connectedToField_ setFrame:connectedToFrame];
129  }
130
131  CGFloat oldDisconnectWidth = NSWidth([disconnectButton_ bounds]);
132  [disconnectButton_ sizeToFit];
133  NSRect disconnectFrame = [disconnectButton_ frame];
134  CGFloat newDisconnectWidth = NSWidth(disconnectFrame);
135
136  // Move the disconnect button appropriately.
137  disconnectFrame.origin.x += newConnectedWidth - oldConnectedWidth;
138  [disconnectButton_ setFrame:disconnectFrame];
139
140  // Then resize the window appropriately
141  NSWindow *window = [self window];
142  NSRect windowFrame = [window frame];
143  windowFrame.size.width += (newConnectedWidth - oldConnectedWidth +
144                             newDisconnectWidth - oldDisconnectWidth);
145  [window setFrame:windowFrame display:NO];
146
147  if ([self isRToL]) {
148    // Handle right to left case
149    CGFloat buttonInset = NSWidth(windowFrame) - NSMaxX(disconnectFrame);
150    CGFloat buttonTextSpacing
151        = NSMinX(disconnectFrame) - NSMaxX(connectedToFrame);
152    disconnectFrame.origin.x = buttonInset;
153    connectedToFrame.origin.x = NSMaxX(disconnectFrame) + buttonTextSpacing;
154    [connectedToField_ setFrame:connectedToFrame];
155    [disconnectButton_ setFrame:disconnectFrame];
156  }
157
158  // Center the window at the bottom of the screen, above the dock (if present).
159  NSRect desktopRect = [[NSScreen mainScreen] visibleFrame];
160  NSRect windowRect = [[self window] frame];
161  CGFloat x = (NSWidth(desktopRect) - NSWidth(windowRect)) / 2;
162  CGFloat y = NSMinY(desktopRect);
163  [[self window] setFrameOrigin:NSMakePoint(x, y)];
164}
165
166- (void)windowWillClose:(NSNotification*)notification {
167  [self stopSharing:self];
168  [self autorelease];
169}
170
171@end
172
173
174@interface DisconnectWindow()
175- (BOOL)isRToL;
176@end
177
178@implementation DisconnectWindow
179
180- (id)initWithContentRect:(NSRect)contentRect
181                styleMask:(NSUInteger)aStyle
182                  backing:(NSBackingStoreType)bufferingType
183                  defer:(BOOL)flag {
184  // Pass NSBorderlessWindowMask for the styleMask to remove the title bar.
185  self = [super initWithContentRect:contentRect
186                          styleMask:NSBorderlessWindowMask
187                            backing:bufferingType
188                              defer:flag];
189
190  if (self) {
191    // Set window to be clear and non-opaque so we can see through it.
192    [self setBackgroundColor:[NSColor clearColor]];
193    [self setOpaque:NO];
194    [self setMovableByWindowBackground:YES];
195
196    // Pull the window up to Status Level so that it always displays.
197    [self setLevel:NSStatusWindowLevel];
198  }
199  return self;
200}
201
202- (BOOL)isRToL {
203  DCHECK([[self windowController] respondsToSelector:@selector(isRToL)]);
204  return [[self windowController] isRToL];
205}
206
207@end
208
209
210@interface DisconnectView()
211- (BOOL)isRToL;
212@end
213
214@implementation DisconnectView
215
216- (BOOL)isRToL {
217  DCHECK([[self window] isKindOfClass:[DisconnectWindow class]]);
218  return [static_cast<DisconnectWindow*>([self window]) isRToL];
219}
220
221- (void)drawRect:(NSRect)rect {
222  // All magic numbers taken from screen shots provided by UX.
223  NSRect bounds = NSInsetRect([self bounds], 1, 1);
224
225  NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:bounds
226                                                       xRadius:5
227                                                       yRadius:5];
228  NSColor *gray = [NSColor colorWithCalibratedWhite:0.91 alpha:1.0];
229  [gray setFill];
230  [path fill];
231  [path setLineWidth:4];
232  NSColor *green = [NSColor colorWithCalibratedRed:0.13
233                                             green:0.69
234                                              blue:0.11
235                                             alpha:1.0];
236  [green setStroke];
237  [path stroke];
238
239
240  // Draw drag handle on proper side
241  const CGFloat kHeight = 21.0;
242  const CGFloat kBaseInset = 12.0;
243  const CGFloat kDragHandleWidth = 5.0;
244
245  NSColor *dark = [NSColor colorWithCalibratedWhite:0.70 alpha:1.0];
246  NSColor *light = [NSColor colorWithCalibratedWhite:0.97 alpha:1.0];
247
248  // Turn off aliasing so it's nice and crisp.
249  NSGraphicsContext *context = [NSGraphicsContext currentContext];
250  BOOL alias = [context shouldAntialias];
251  [context setShouldAntialias:NO];
252
253  // Handle bidirectional locales properly.
254  CGFloat inset = [self isRToL] ? NSMaxX(bounds) - kBaseInset - kDragHandleWidth
255                                : kBaseInset;
256
257  NSPoint top = NSMakePoint(inset, NSMidY(bounds) - kHeight / 2.0);
258  NSPoint bottom = NSMakePoint(inset, top.y + kHeight);
259
260  path = [NSBezierPath bezierPath];
261  [path moveToPoint:top];
262  [path lineToPoint:bottom];
263  [dark setStroke];
264  [path stroke];
265
266  top.x += 1;
267  bottom.x += 1;
268  path = [NSBezierPath bezierPath];
269  [path moveToPoint:top];
270  [path lineToPoint:bottom];
271  [light setStroke];
272  [path stroke];
273
274  top.x += 2;
275  bottom.x += 2;
276  path = [NSBezierPath bezierPath];
277  [path moveToPoint:top];
278  [path lineToPoint:bottom];
279  [dark setStroke];
280  [path stroke];
281
282  top.x += 1;
283  bottom.x += 1;
284  path = [NSBezierPath bezierPath];
285  [path moveToPoint:top];
286  [path lineToPoint:bottom];
287  [light setStroke];
288  [path stroke];
289
290  [context setShouldAntialias:alias];
291}
292
293@end
294