1// Copyright 2013 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 "content/shell/browser/shell.h"
6
7#include <algorithm>
8
9#include "base/logging.h"
10#import "base/mac/scoped_nsobject.h"
11#include "base/mac/sdk_forward_declarations.h"
12#include "base/strings/string_piece.h"
13#include "base/strings/sys_string_conversions.h"
14#include "content/public/browser/native_web_keyboard_event.h"
15#include "content/public/browser/web_contents.h"
16#include "content/shell/app/resource.h"
17#import "ui/base/cocoa/underlay_opengl_hosting_window.h"
18#include "url/gurl.h"
19
20// Receives notification that the window is closing so that it can start the
21// tear-down process. Is responsible for deleting itself when done.
22@interface ContentShellWindowDelegate : NSObject<NSWindowDelegate> {
23 @private
24  content::Shell* shell_;
25}
26- (id)initWithShell:(content::Shell*)shell;
27@end
28
29@implementation ContentShellWindowDelegate
30
31- (id)initWithShell:(content::Shell*)shell {
32  if ((self = [super init])) {
33    shell_ = shell;
34  }
35  return self;
36}
37
38// Called when the window is about to close. Perform the self-destruction
39// sequence by getting rid of the shell and removing it and the window from
40// the various global lists. By returning YES, we allow the window to be
41// removed from the screen.
42- (BOOL)windowShouldClose:(id)window {
43  [window autorelease];
44  delete shell_;
45  [self release];
46
47  return YES;
48}
49
50- (void)performAction:(id)sender {
51  shell_->ActionPerformed([sender tag]);
52}
53
54- (void)takeURLStringValueFrom:(id)sender {
55  shell_->URLEntered(base::SysNSStringToUTF8([sender stringValue]));
56}
57
58@end
59
60@interface CrShellWindow : UnderlayOpenGLHostingWindow {
61 @private
62  content::Shell* shell_;
63}
64- (void)setShell:(content::Shell*)shell;
65- (void)showDevTools:(id)sender;
66@end
67
68@implementation CrShellWindow
69
70- (void)setShell:(content::Shell*)shell {
71  shell_ = shell;
72}
73
74- (void)showDevTools:(id)sender {
75  shell_->ShowDevTools();
76}
77
78@end
79
80namespace {
81
82NSString* kWindowTitle = @"Content Shell";
83
84// Layout constants (in view coordinates)
85const CGFloat kButtonWidth = 72;
86const CGFloat kURLBarHeight = 24;
87
88// The minimum size of the window's content (in view coordinates)
89const CGFloat kMinimumWindowWidth = 400;
90const CGFloat kMinimumWindowHeight = 300;
91
92void MakeShellButton(NSRect* rect,
93                     NSString* title,
94                     NSView* parent,
95                     int control,
96                     NSView* target,
97                     NSString* key,
98                     NSUInteger modifier) {
99  base::scoped_nsobject<NSButton> button(
100      [[NSButton alloc] initWithFrame:*rect]);
101  [button setTitle:title];
102  [button setBezelStyle:NSSmallSquareBezelStyle];
103  [button setAutoresizingMask:(NSViewMaxXMargin | NSViewMinYMargin)];
104  [button setTarget:target];
105  [button setAction:@selector(performAction:)];
106  [button setTag:control];
107  [button setKeyEquivalent:key];
108  [button setKeyEquivalentModifierMask:modifier];
109  [parent addSubview:button];
110  rect->origin.x += kButtonWidth;
111}
112
113}  // namespace
114
115namespace content {
116
117void Shell::PlatformInitialize(const gfx::Size& default_window_size) {
118}
119
120void Shell::PlatformExit() {
121}
122
123void Shell::PlatformCleanUp() {
124}
125
126void Shell::PlatformEnableUIControl(UIControl control, bool is_enabled) {
127  if (headless_)
128    return;
129
130  int id;
131  switch (control) {
132    case BACK_BUTTON:
133      id = IDC_NAV_BACK;
134      break;
135    case FORWARD_BUTTON:
136      id = IDC_NAV_FORWARD;
137      break;
138    case STOP_BUTTON:
139      id = IDC_NAV_STOP;
140      break;
141    default:
142      NOTREACHED() << "Unknown UI control";
143      return;
144  }
145  [[[window_ contentView] viewWithTag:id] setEnabled:is_enabled];
146}
147
148void Shell::PlatformSetAddressBarURL(const GURL& url) {
149  if (headless_)
150    return;
151
152  NSString* url_string = base::SysUTF8ToNSString(url.spec());
153  [url_edit_view_ setStringValue:url_string];
154}
155
156void Shell::PlatformSetIsLoading(bool loading) {
157}
158
159void Shell::PlatformCreateWindow(int width, int height) {
160  if (headless_) {
161    content_size_ = gfx::Size(width, height);
162    return;
163  }
164
165  NSRect initial_window_bounds =
166      NSMakeRect(0, 0, width, height + kURLBarHeight);
167  NSRect content_rect = initial_window_bounds;
168  NSUInteger style_mask = NSTitledWindowMask |
169                          NSClosableWindowMask |
170                          NSMiniaturizableWindowMask |
171                          NSResizableWindowMask;
172  CrShellWindow* window =
173      [[CrShellWindow alloc] initWithContentRect:content_rect
174                                       styleMask:style_mask
175                                         backing:NSBackingStoreBuffered
176                                           defer:NO];
177  window_ = window;
178  [window setShell:this];
179  [window_ setTitle:kWindowTitle];
180  NSView* content = [window_ contentView];
181
182  // If the window is allowed to get too small, it will wreck the view bindings.
183  NSSize min_size = NSMakeSize(kMinimumWindowWidth, kMinimumWindowHeight);
184  min_size = [content convertSize:min_size toView:nil];
185  // Note that this takes window coordinates.
186  [window_ setContentMinSize:min_size];
187
188  // Set the shell window to participate in Lion Fullscreen mode. Set
189  // Setting this flag has no effect on Snow Leopard or earlier.
190  NSUInteger collectionBehavior = [window_ collectionBehavior];
191  collectionBehavior |= NSWindowCollectionBehaviorFullScreenPrimary;
192  [window_ setCollectionBehavior:collectionBehavior];
193
194  // Rely on the window delegate to clean us up rather than immediately
195  // releasing when the window gets closed. We use the delegate to do
196  // everything from the autorelease pool so the shell isn't on the stack
197  // during cleanup (ie, a window close from javascript).
198  [window_ setReleasedWhenClosed:NO];
199
200  // Create a window delegate to watch for when it's asked to go away. It will
201  // clean itself up so we don't need to hold a reference.
202  ContentShellWindowDelegate* delegate =
203      [[ContentShellWindowDelegate alloc] initWithShell:this];
204  [window_ setDelegate:delegate];
205
206  NSRect button_frame =
207      NSMakeRect(0, NSMaxY(initial_window_bounds) - kURLBarHeight,
208                 kButtonWidth, kURLBarHeight);
209
210  MakeShellButton(&button_frame, @"Back", content, IDC_NAV_BACK,
211                  (NSView*)delegate, @"[", NSCommandKeyMask);
212  MakeShellButton(&button_frame, @"Forward", content, IDC_NAV_FORWARD,
213                  (NSView*)delegate, @"]", NSCommandKeyMask);
214  MakeShellButton(&button_frame, @"Reload", content, IDC_NAV_RELOAD,
215                  (NSView*)delegate, @"r", NSCommandKeyMask);
216  MakeShellButton(&button_frame, @"Stop", content, IDC_NAV_STOP,
217                  (NSView*)delegate, @".", NSCommandKeyMask);
218
219  button_frame.size.width =
220      NSWidth(initial_window_bounds) - NSMinX(button_frame);
221  base::scoped_nsobject<NSTextField> url_edit_view(
222      [[NSTextField alloc] initWithFrame:button_frame]);
223  [content addSubview:url_edit_view];
224  [url_edit_view setAutoresizingMask:(NSViewWidthSizable | NSViewMinYMargin)];
225  [url_edit_view setTarget:delegate];
226  [url_edit_view setAction:@selector(takeURLStringValueFrom:)];
227  [[url_edit_view cell] setWraps:NO];
228  [[url_edit_view cell] setScrollable:YES];
229  url_edit_view_ = url_edit_view.get();
230
231  // show the window
232  [window_ makeKeyAndOrderFront:nil];
233}
234
235void Shell::PlatformSetContents() {
236  NSView* web_view = web_contents_->GetNativeView();
237  [web_view setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
238
239  if (headless_) {
240    SizeTo(content_size_);
241    return;
242  }
243
244  NSView* content = [window_ contentView];
245  [content addSubview:web_view];
246
247  NSRect frame = [content bounds];
248  frame.size.height -= kURLBarHeight;
249  [web_view setFrame:frame];
250  [web_view setNeedsDisplay:YES];
251}
252
253void Shell::SizeTo(const gfx::Size& content_size) {
254  if (!headless_) {
255    NSRect frame = NSMakeRect(
256        0, 0, content_size.width(), content_size.height() + kURLBarHeight);
257    [window().contentView setFrame:frame];
258    return;
259  }
260  NSView* web_view = web_contents_->GetNativeView();
261  NSRect frame = NSMakeRect(0, 0, content_size.width(), content_size.height());
262  [web_view setFrame:frame];
263}
264
265void Shell::PlatformResizeSubViews() {
266  // Not needed; subviews are bound.
267}
268
269void Shell::PlatformSetTitle(const base::string16& title) {
270  if (headless_)
271    return;
272
273  NSString* title_string = base::SysUTF16ToNSString(title);
274  [window_ setTitle:title_string];
275}
276
277bool Shell::PlatformHandleContextMenu(
278    const content::ContextMenuParams& params) {
279  return false;
280}
281
282#if defined(TOOLKIT_VIEWS)
283void Shell::PlatformWebContentsFocused(WebContents* contents) {
284  if (headless_)
285    return;
286
287  NOTIMPLEMENTED();
288  return;
289}
290#endif
291
292void Shell::Close() {
293  if (headless_)
294    delete this;
295  else
296    [window_ performClose:nil];
297}
298
299void Shell::ActionPerformed(int control) {
300  switch (control) {
301    case IDC_NAV_BACK:
302      GoBackOrForward(-1);
303      break;
304    case IDC_NAV_FORWARD:
305      GoBackOrForward(1);
306      break;
307    case IDC_NAV_RELOAD:
308      Reload();
309      break;
310    case IDC_NAV_STOP:
311      Stop();
312      break;
313  }
314}
315
316void Shell::URLEntered(std::string url_string) {
317  if (!url_string.empty()) {
318    GURL url(url_string);
319    if (!url.has_scheme())
320      url = GURL("http://" + url_string);
321    LoadURL(url);
322  }
323}
324
325void Shell::HandleKeyboardEvent(WebContents* source,
326                                const NativeWebKeyboardEvent& event) {
327  if (event.skip_in_browser)
328    return;
329
330  // The event handling to get this strictly right is a tangle; cheat here a bit
331  // by just letting the menus have a chance at it.
332  if ([event.os_event type] == NSKeyDown) {
333    if (([event.os_event modifierFlags] & NSCommandKeyMask) &&
334        [[event.os_event characters] isEqual:@"l"]) {
335      [window_ makeFirstResponder:url_edit_view_];
336      return;
337    }
338
339    [[NSApp mainMenu] performKeyEquivalent:event.os_event];
340  }
341}
342
343}  // namespace content
344