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 "chrome/browser/ui/cocoa/web_dialog_window_controller.h"
6
7#include "base/logging.h"
8#include "base/mac/scoped_nsobject.h"
9#include "base/strings/sys_string_conversions.h"
10#import "chrome/browser/ui/browser_dialogs.h"
11#import "chrome/browser/ui/cocoa/browser_command_executor.h"
12#import "chrome/browser/ui/cocoa/chrome_event_processing_window.h"
13#include "chrome/browser/ui/webui/chrome_web_contents_handler.h"
14#include "content/public/browser/native_web_keyboard_event.h"
15#include "content/public/browser/web_contents.h"
16#include "content/public/browser/web_ui_message_handler.h"
17#include "ui/events/keycodes/keyboard_codes.h"
18#include "ui/gfx/size.h"
19#include "ui/web_dialogs/web_dialog_delegate.h"
20#include "ui/web_dialogs/web_dialog_web_contents_delegate.h"
21
22using content::NativeWebKeyboardEvent;
23using content::WebContents;
24using content::WebUIMessageHandler;
25using ui::WebDialogDelegate;
26using ui::WebDialogUI;
27using ui::WebDialogWebContentsDelegate;
28
29// Thin bridge that routes notifications to
30// WebDialogWindowController's member variables.
31class WebDialogWindowDelegateBridge
32    : public WebDialogDelegate,
33      public WebDialogWebContentsDelegate {
34public:
35  // All parameters must be non-NULL/non-nil.
36  WebDialogWindowDelegateBridge(WebDialogWindowController* controller,
37                                content::BrowserContext* context,
38                                WebDialogDelegate* delegate);
39
40  virtual ~WebDialogWindowDelegateBridge();
41
42  // Called when the window is directly closed, e.g. from the close
43  // button or from an accelerator.
44  void WindowControllerClosed();
45
46  // WebDialogDelegate declarations.
47  virtual ui::ModalType GetDialogModalType() const OVERRIDE;
48  virtual base::string16 GetDialogTitle() const OVERRIDE;
49  virtual GURL GetDialogContentURL() const OVERRIDE;
50  virtual void GetWebUIMessageHandlers(
51      std::vector<WebUIMessageHandler*>* handlers) const OVERRIDE;
52  virtual void GetDialogSize(gfx::Size* size) const OVERRIDE;
53  virtual void GetMinimumDialogSize(gfx::Size* size) const OVERRIDE;
54  virtual std::string GetDialogArgs() const OVERRIDE;
55  virtual void OnDialogClosed(const std::string& json_retval) OVERRIDE;
56  virtual void OnCloseContents(WebContents* source,
57                               bool* out_close_dialog) OVERRIDE;
58  virtual bool ShouldShowDialogTitle() const OVERRIDE { return true; }
59
60  // WebDialogWebContentsDelegate declarations.
61  virtual void MoveContents(WebContents* source, const gfx::Rect& pos) OVERRIDE;
62  virtual void HandleKeyboardEvent(
63      content::WebContents* source,
64      const NativeWebKeyboardEvent& event) OVERRIDE;
65  virtual void CloseContents(WebContents* source) OVERRIDE;
66  virtual content::WebContents* OpenURLFromTab(
67      content::WebContents* source,
68      const content::OpenURLParams& params) OVERRIDE;
69  virtual void AddNewContents(content::WebContents* source,
70                              content::WebContents* new_contents,
71                              WindowOpenDisposition disposition,
72                              const gfx::Rect& initial_pos,
73                              bool user_gesture,
74                              bool* was_blocked) OVERRIDE;
75  virtual void LoadingStateChanged(content::WebContents* source,
76                                   bool to_different_document) OVERRIDE;
77
78private:
79  WebDialogWindowController* controller_;  // weak
80  WebDialogDelegate* delegate_;  // weak, owned by controller_
81
82  // Calls delegate_'s OnDialogClosed() exactly once, nulling it out afterwards
83  // so that no other WebDialogDelegate calls are sent to it. Returns whether or
84  // not the OnDialogClosed() was actually called on the delegate.
85  bool DelegateOnDialogClosed(const std::string& json_retval);
86
87  DISALLOW_COPY_AND_ASSIGN(WebDialogWindowDelegateBridge);
88};
89
90// ChromeEventProcessingWindow expects its controller to implement the
91// BrowserCommandExecutor protocol.
92@interface WebDialogWindowController (InternalAPI) <BrowserCommandExecutor>
93
94// BrowserCommandExecutor methods.
95- (void)executeCommand:(int)command;
96
97@end
98
99namespace chrome {
100
101gfx::NativeWindow ShowWebDialog(gfx::NativeWindow parent,
102                                content::BrowserContext* context,
103                                WebDialogDelegate* delegate) {
104  return [WebDialogWindowController showWebDialog:delegate
105                                          context:context];
106}
107
108}  // namespace chrome
109
110WebDialogWindowDelegateBridge::WebDialogWindowDelegateBridge(
111    WebDialogWindowController* controller,
112    content::BrowserContext* context,
113    WebDialogDelegate* delegate)
114    : WebDialogWebContentsDelegate(context, new ChromeWebContentsHandler),
115      controller_(controller),
116      delegate_(delegate) {
117  DCHECK(controller_);
118  DCHECK(delegate_);
119}
120
121WebDialogWindowDelegateBridge::~WebDialogWindowDelegateBridge() {}
122
123void WebDialogWindowDelegateBridge::WindowControllerClosed() {
124  Detach();
125  controller_ = nil;
126  DelegateOnDialogClosed("");
127}
128
129bool WebDialogWindowDelegateBridge::DelegateOnDialogClosed(
130    const std::string& json_retval) {
131  if (delegate_) {
132    WebDialogDelegate* real_delegate = delegate_;
133    delegate_ = NULL;
134    real_delegate->OnDialogClosed(json_retval);
135    return true;
136  }
137  return false;
138}
139
140// WebDialogDelegate definitions.
141
142// All of these functions check for NULL first since delegate_ is set
143// to NULL when the window is closed.
144
145ui::ModalType WebDialogWindowDelegateBridge::GetDialogModalType() const {
146  // TODO(akalin): Support modal dialog boxes.
147  if (delegate_ && delegate_->GetDialogModalType() != ui::MODAL_TYPE_NONE) {
148    LOG(WARNING) << "Modal Web dialogs are not supported yet";
149  }
150  return ui::MODAL_TYPE_NONE;
151}
152
153base::string16 WebDialogWindowDelegateBridge::GetDialogTitle() const {
154  return delegate_ ? delegate_->GetDialogTitle() : base::string16();
155}
156
157GURL WebDialogWindowDelegateBridge::GetDialogContentURL() const {
158  return delegate_ ? delegate_->GetDialogContentURL() : GURL();
159}
160
161void WebDialogWindowDelegateBridge::GetWebUIMessageHandlers(
162    std::vector<WebUIMessageHandler*>* handlers) const {
163  if (delegate_) {
164    delegate_->GetWebUIMessageHandlers(handlers);
165  } else {
166    // TODO(akalin): Add this clause in the windows version.  Also
167    // make sure that everything expects handlers to be non-NULL and
168    // document it.
169    handlers->clear();
170  }
171}
172
173void WebDialogWindowDelegateBridge::GetDialogSize(gfx::Size* size) const {
174  if (delegate_)
175    delegate_->GetDialogSize(size);
176  else
177    *size = gfx::Size();
178}
179
180void WebDialogWindowDelegateBridge::GetMinimumDialogSize(
181    gfx::Size* size) const {
182  if (delegate_)
183    delegate_->GetMinimumDialogSize(size);
184  else
185    *size = gfx::Size();
186}
187
188std::string WebDialogWindowDelegateBridge::GetDialogArgs() const {
189  return delegate_ ? delegate_->GetDialogArgs() : "";
190}
191
192void WebDialogWindowDelegateBridge::OnDialogClosed(
193    const std::string& json_retval) {
194  Detach();
195  // [controller_ close] should be called at most once, too.
196  if (DelegateOnDialogClosed(json_retval)) {
197    [controller_ close];
198  }
199  controller_ = nil;
200}
201
202void WebDialogWindowDelegateBridge::OnCloseContents(WebContents* source,
203                                                    bool* out_close_dialog) {
204  if (out_close_dialog)
205    *out_close_dialog = true;
206}
207
208void WebDialogWindowDelegateBridge::CloseContents(WebContents* source) {
209  bool close_dialog = false;
210  OnCloseContents(source, &close_dialog);
211  if (close_dialog)
212    OnDialogClosed(std::string());
213}
214
215content::WebContents* WebDialogWindowDelegateBridge::OpenURLFromTab(
216    content::WebContents* source,
217    const content::OpenURLParams& params) {
218  content::WebContents* new_contents = NULL;
219  if (delegate_ &&
220      delegate_->HandleOpenURLFromTab(source, params, &new_contents)) {
221    return new_contents;
222  }
223  return WebDialogWebContentsDelegate::OpenURLFromTab(source, params);
224}
225
226void WebDialogWindowDelegateBridge::AddNewContents(
227    content::WebContents* source,
228    content::WebContents* new_contents,
229    WindowOpenDisposition disposition,
230    const gfx::Rect& initial_pos,
231    bool user_gesture,
232    bool* was_blocked) {
233  if (delegate_ && delegate_->HandleAddNewContents(
234          source, new_contents, disposition, initial_pos, user_gesture)) {
235    return;
236  }
237  WebDialogWebContentsDelegate::AddNewContents(
238      source, new_contents, disposition, initial_pos, user_gesture,
239      was_blocked);
240}
241
242void WebDialogWindowDelegateBridge::LoadingStateChanged(
243    content::WebContents* source, bool to_different_document) {
244  if (delegate_)
245    delegate_->OnLoadingStateChanged(source);
246}
247
248void WebDialogWindowDelegateBridge::MoveContents(WebContents* source,
249                                                 const gfx::Rect& pos) {
250  // TODO(akalin): Actually set the window bounds.
251}
252
253// A simplified version of BrowserWindowCocoa::HandleKeyboardEvent().
254// We don't handle global keyboard shortcuts here, but that's fine since
255// they're all browser-specific. (This may change in the future.)
256void WebDialogWindowDelegateBridge::HandleKeyboardEvent(
257    content::WebContents* source,
258    const NativeWebKeyboardEvent& event) {
259  if (event.skip_in_browser || event.type == NativeWebKeyboardEvent::Char)
260    return;
261
262  // Close ourselves if the user hits Esc or Command-. .  The normal
263  // way to do this is to implement (void)cancel:(int)sender, but
264  // since we handle keyboard events ourselves we can't do that.
265  //
266  // According to experiments, hitting Esc works regardless of the
267  // presence of other modifiers (as long as it's not an app-level
268  // shortcut, e.g. Commmand-Esc for Front Row) but no other modifiers
269  // can be present for Command-. to work.
270  //
271  // TODO(thakis): It would be nice to get cancel: to work somehow.
272  // Bug: http://code.google.com/p/chromium/issues/detail?id=32828 .
273  if (event.type == NativeWebKeyboardEvent::RawKeyDown &&
274      ((event.windowsKeyCode == ui::VKEY_ESCAPE) ||
275       (event.windowsKeyCode == ui::VKEY_OEM_PERIOD &&
276        event.modifiers == NativeWebKeyboardEvent::MetaKey))) {
277    [controller_ close];
278    return;
279  }
280
281  ChromeEventProcessingWindow* event_window =
282      static_cast<ChromeEventProcessingWindow*>([controller_ window]);
283  DCHECK([event_window isKindOfClass:[ChromeEventProcessingWindow class]]);
284  [event_window redispatchKeyEvent:event.os_event];
285}
286
287@implementation WebDialogWindowController (InternalAPI)
288
289// This gets called whenever a chrome-specific keyboard shortcut is performed
290// in the Web dialog window.  We simply swallow all those events.
291- (void)executeCommand:(int)command {}
292
293@end
294
295@implementation WebDialogWindowController
296
297// NOTE(akalin): We'll probably have to add the parentWindow parameter back
298// in once we implement modal dialogs.
299
300+ (NSWindow*)showWebDialog:(WebDialogDelegate*)delegate
301                   context:(content::BrowserContext*)context {
302  WebDialogWindowController* webDialogWindowController =
303    [[WebDialogWindowController alloc] initWithDelegate:delegate
304                                                context:context];
305  [webDialogWindowController loadDialogContents];
306  [webDialogWindowController showWindow:nil];
307  return [webDialogWindowController window];
308}
309
310- (id)initWithDelegate:(WebDialogDelegate*)delegate
311               context:(content::BrowserContext*)context {
312  DCHECK(delegate);
313  DCHECK(context);
314
315  gfx::Size dialogSize;
316  delegate->GetDialogSize(&dialogSize);
317  NSRect dialogRect = NSMakeRect(0, 0, dialogSize.width(), dialogSize.height());
318  NSUInteger style = NSTitledWindowMask | NSClosableWindowMask |
319      NSResizableWindowMask;
320  base::scoped_nsobject<ChromeEventProcessingWindow> window(
321      [[ChromeEventProcessingWindow alloc]
322          initWithContentRect:dialogRect
323                    styleMask:style
324                      backing:NSBackingStoreBuffered
325                        defer:YES]);
326  if (!window.get()) {
327    return nil;
328  }
329  self = [super initWithWindow:window];
330  if (!self) {
331    return nil;
332  }
333  [window setWindowController:self];
334  [window setDelegate:self];
335  [window setTitle:base::SysUTF16ToNSString(delegate->GetDialogTitle())];
336  [window setMinSize:dialogRect.size];
337  [window center];
338  delegate_.reset(
339      new WebDialogWindowDelegateBridge(self, context, delegate));
340  return self;
341}
342
343- (void)loadDialogContents {
344  webContents_.reset(WebContents::Create(
345      WebContents::CreateParams(delegate_->browser_context())));
346  [[self window] setContentView:webContents_->GetNativeView()];
347  webContents_->SetDelegate(delegate_.get());
348
349  // This must be done before loading the page; see the comments in
350  // WebDialogUI.
351  WebDialogUI::SetDelegate(webContents_.get(), delegate_.get());
352
353  webContents_->GetController().LoadURL(
354      delegate_->GetDialogContentURL(),
355      content::Referrer(),
356      ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
357      std::string());
358
359  // TODO(akalin): add accelerator for ESC to close the dialog box.
360  //
361  // TODO(akalin): Figure out why implementing (void)cancel:(id)sender
362  // to do the above doesn't work.
363}
364
365- (void)windowWillClose:(NSNotification*)notification {
366  delegate_->WindowControllerClosed();
367  [self autorelease];
368}
369
370@end
371