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 <Carbon/Carbon.h>
6
7#import "content/browser/web_contents/web_contents_view_mac.h"
8
9#include <string>
10
11#import "base/mac/scoped_sending_event.h"
12#include "base/message_loop/message_loop.h"
13#import "base/message_loop/message_pump_mac.h"
14#include "content/browser/frame_host/popup_menu_helper_mac.h"
15#include "content/browser/renderer_host/render_view_host_factory.h"
16#include "content/browser/renderer_host/render_view_host_impl.h"
17#include "content/browser/renderer_host/render_widget_host_view_mac.h"
18#include "content/browser/web_contents/web_contents_impl.h"
19#import "content/browser/web_contents/web_drag_dest_mac.h"
20#import "content/browser/web_contents/web_drag_source_mac.h"
21#include "content/common/view_messages.h"
22#include "content/public/browser/web_contents_delegate.h"
23#include "content/public/browser/web_contents_view_delegate.h"
24#include "skia/ext/skia_utils_mac.h"
25#import "third_party/mozilla/NSPasteboard+Utils.h"
26#include "ui/base/clipboard/custom_data_helper.h"
27#import "ui/base/cocoa/focus_tracker.h"
28#include "ui/base/dragdrop/cocoa_dnd_util.h"
29#include "ui/gfx/image/image_skia_util_mac.h"
30
31using blink::WebDragOperation;
32using blink::WebDragOperationsMask;
33using content::DropData;
34using content::PopupMenuHelper;
35using content::RenderViewHostFactory;
36using content::RenderWidgetHostView;
37using content::RenderWidgetHostViewMac;
38using content::WebContents;
39using content::WebContentsImpl;
40using content::WebContentsViewMac;
41
42// Ensure that the blink::WebDragOperation enum values stay in sync with
43// NSDragOperation constants, since the code below static_casts between 'em.
44#define COMPILE_ASSERT_MATCHING_ENUM(name) \
45  COMPILE_ASSERT(int(NS##name) == int(blink::Web##name), enum_mismatch_##name)
46COMPILE_ASSERT_MATCHING_ENUM(DragOperationNone);
47COMPILE_ASSERT_MATCHING_ENUM(DragOperationCopy);
48COMPILE_ASSERT_MATCHING_ENUM(DragOperationLink);
49COMPILE_ASSERT_MATCHING_ENUM(DragOperationGeneric);
50COMPILE_ASSERT_MATCHING_ENUM(DragOperationPrivate);
51COMPILE_ASSERT_MATCHING_ENUM(DragOperationMove);
52COMPILE_ASSERT_MATCHING_ENUM(DragOperationDelete);
53COMPILE_ASSERT_MATCHING_ENUM(DragOperationEvery);
54
55@interface WebContentsViewCocoa (Private)
56- (id)initWithWebContentsViewMac:(WebContentsViewMac*)w;
57- (void)registerDragTypes;
58- (void)setCurrentDragOperation:(NSDragOperation)operation;
59- (DropData*)dropData;
60- (void)startDragWithDropData:(const DropData&)dropData
61            dragOperationMask:(NSDragOperation)operationMask
62                        image:(NSImage*)image
63                       offset:(NSPoint)offset;
64- (void)cancelDeferredClose;
65- (void)clearWebContentsView;
66- (void)closeTabAfterEvent;
67- (void)viewDidBecomeFirstResponder:(NSNotification*)notification;
68@end
69
70namespace content {
71
72WebContentsView* CreateWebContentsView(
73    WebContentsImpl* web_contents,
74    WebContentsViewDelegate* delegate,
75    RenderViewHostDelegateView** render_view_host_delegate_view) {
76  WebContentsViewMac* rv = new WebContentsViewMac(web_contents, delegate);
77  *render_view_host_delegate_view = rv;
78  return rv;
79}
80
81WebContentsViewMac::WebContentsViewMac(WebContentsImpl* web_contents,
82                                       WebContentsViewDelegate* delegate)
83    : web_contents_(web_contents),
84      delegate_(delegate),
85      allow_other_views_(false) {
86}
87
88WebContentsViewMac::~WebContentsViewMac() {
89  // This handles the case where a renderer close call was deferred
90  // while the user was operating a UI control which resulted in a
91  // close.  In that case, the Cocoa view outlives the
92  // WebContentsViewMac instance due to Cocoa retain count.
93  [cocoa_view_ cancelDeferredClose];
94  [cocoa_view_ clearWebContentsView];
95}
96
97gfx::NativeView WebContentsViewMac::GetNativeView() const {
98  return cocoa_view_.get();
99}
100
101gfx::NativeView WebContentsViewMac::GetContentNativeView() const {
102  RenderWidgetHostView* rwhv = web_contents_->GetRenderWidgetHostView();
103  if (!rwhv)
104    return NULL;
105  return rwhv->GetNativeView();
106}
107
108gfx::NativeWindow WebContentsViewMac::GetTopLevelNativeWindow() const {
109  return [cocoa_view_.get() window];
110}
111
112void WebContentsViewMac::GetContainerBounds(gfx::Rect* out) const {
113  // Convert bounds to window coordinate space.
114  NSRect bounds =
115      [cocoa_view_.get() convertRect:[cocoa_view_.get() bounds] toView:nil];
116
117  // Convert bounds to screen coordinate space.
118  NSWindow* window = [cocoa_view_.get() window];
119  bounds.origin = [window convertBaseToScreen:bounds.origin];
120
121  // Flip y to account for screen flip.
122  NSScreen* screen = [[NSScreen screens] objectAtIndex:0];
123  bounds.origin.y = [screen frame].size.height - bounds.origin.y
124      - bounds.size.height;
125  *out = gfx::Rect(NSRectToCGRect(bounds));
126}
127
128void WebContentsViewMac::StartDragging(
129    const DropData& drop_data,
130    WebDragOperationsMask allowed_operations,
131    const gfx::ImageSkia& image,
132    const gfx::Vector2d& image_offset,
133    const DragEventSourceInfo& event_info) {
134  // By allowing nested tasks, the code below also allows Close(),
135  // which would deallocate |this|.  The same problem can occur while
136  // processing -sendEvent:, so Close() is deferred in that case.
137  // Drags from web content do not come via -sendEvent:, this sets the
138  // same flag -sendEvent: would.
139  base::mac::ScopedSendingEvent sending_event_scoper;
140
141  // The drag invokes a nested event loop, arrange to continue
142  // processing events.
143  base::MessageLoop::ScopedNestableTaskAllower allow(
144      base::MessageLoop::current());
145  NSDragOperation mask = static_cast<NSDragOperation>(allowed_operations);
146  NSPoint offset = NSPointFromCGPoint(
147      gfx::PointAtOffsetFromOrigin(image_offset).ToCGPoint());
148  [cocoa_view_ startDragWithDropData:drop_data
149                   dragOperationMask:mask
150                               image:gfx::NSImageFromImageSkia(image)
151                              offset:offset];
152}
153
154void WebContentsViewMac::SizeContents(const gfx::Size& size) {
155  // TODO(brettw | japhet) This is a hack and should be removed.
156  // See web_contents_view.h.
157  // Note(erikchen): This method has /never/ worked correctly. I've removed the
158  // previous implementation.
159}
160
161void WebContentsViewMac::Focus() {
162  NSWindow* window = [cocoa_view_.get() window];
163  [window makeFirstResponder:GetContentNativeView()];
164  if (![window isVisible])
165    return;
166  [window makeKeyAndOrderFront:nil];
167}
168
169void WebContentsViewMac::SetInitialFocus() {
170  if (web_contents_->FocusLocationBarByDefault())
171    web_contents_->SetFocusToLocationBar(false);
172  else
173    [[cocoa_view_.get() window] makeFirstResponder:GetContentNativeView()];
174}
175
176void WebContentsViewMac::StoreFocus() {
177  // We're explicitly being asked to store focus, so don't worry if there's
178  // already a view saved.
179  focus_tracker_.reset(
180      [[FocusTracker alloc] initWithWindow:[cocoa_view_ window]]);
181}
182
183void WebContentsViewMac::RestoreFocus() {
184  // TODO(avi): Could we be restoring a view that's no longer in the key view
185  // chain?
186  if (!(focus_tracker_.get() &&
187        [focus_tracker_ restoreFocusInWindow:[cocoa_view_ window]])) {
188    // Fall back to the default focus behavior if we could not restore focus.
189    // TODO(shess): If location-bar gets focus by default, this will
190    // select-all in the field.  If there was a specific selection in
191    // the field when we navigated away from it, we should restore
192    // that selection.
193    SetInitialFocus();
194  }
195
196  focus_tracker_.reset(nil);
197}
198
199DropData* WebContentsViewMac::GetDropData() const {
200  return [cocoa_view_ dropData];
201}
202
203void WebContentsViewMac::UpdateDragCursor(WebDragOperation operation) {
204  [cocoa_view_ setCurrentDragOperation: operation];
205}
206
207void WebContentsViewMac::GotFocus() {
208  // This is only used in the views FocusManager stuff but it bleeds through
209  // all subclasses. http://crbug.com/21875
210}
211
212// This is called when the renderer asks us to take focus back (i.e., it has
213// iterated past the last focusable element on the page).
214void WebContentsViewMac::TakeFocus(bool reverse) {
215  if (reverse) {
216    [[cocoa_view_ window] selectPreviousKeyView:cocoa_view_.get()];
217  } else {
218    [[cocoa_view_ window] selectNextKeyView:cocoa_view_.get()];
219  }
220}
221
222void WebContentsViewMac::ShowContextMenu(
223    RenderFrameHost* render_frame_host,
224    const ContextMenuParams& params) {
225  // Allow delegates to handle the context menu operation first.
226  if (web_contents_->GetDelegate() &&
227      web_contents_->GetDelegate()->HandleContextMenu(params)) {
228    return;
229  }
230
231  if (delegate())
232    delegate()->ShowContextMenu(render_frame_host, params);
233  else
234    DLOG(ERROR) << "Cannot show context menus without a delegate.";
235}
236
237void WebContentsViewMac::ShowPopupMenu(
238    RenderFrameHost* render_frame_host,
239    const gfx::Rect& bounds,
240    int item_height,
241    double item_font_size,
242    int selected_item,
243    const std::vector<MenuItem>& items,
244    bool right_aligned,
245    bool allow_multiple_selection) {
246  popup_menu_helper_.reset(new PopupMenuHelper(render_frame_host));
247  popup_menu_helper_->ShowPopupMenu(bounds, item_height, item_font_size,
248                                    selected_item, items, right_aligned,
249                                    allow_multiple_selection);
250  popup_menu_helper_.reset();
251}
252
253void WebContentsViewMac::HidePopupMenu() {
254  if (popup_menu_helper_)
255    popup_menu_helper_->Hide();
256}
257
258gfx::Rect WebContentsViewMac::GetViewBounds() const {
259  // This method is not currently used on mac.
260  NOTIMPLEMENTED();
261  return gfx::Rect();
262}
263
264void WebContentsViewMac::SetAllowOtherViews(bool allow) {
265  if (allow_other_views_ == allow)
266    return;
267
268  allow_other_views_ = allow;
269  RenderWidgetHostViewMac* view = static_cast<RenderWidgetHostViewMac*>(
270      web_contents_->GetRenderWidgetHostView());
271  if (view)
272    view->SetAllowPauseForResizeOrRepaint(!allow_other_views_);
273}
274
275bool WebContentsViewMac::GetAllowOtherViews() const {
276  return allow_other_views_;
277}
278
279void WebContentsViewMac::CreateView(
280    const gfx::Size& initial_size, gfx::NativeView context) {
281  WebContentsViewCocoa* view =
282      [[WebContentsViewCocoa alloc] initWithWebContentsViewMac:this];
283  cocoa_view_.reset(view);
284}
285
286RenderWidgetHostViewBase* WebContentsViewMac::CreateViewForWidget(
287    RenderWidgetHost* render_widget_host) {
288  if (render_widget_host->GetView()) {
289    // During testing, the view will already be set up in most cases to the
290    // test view, so we don't want to clobber it with a real one. To verify that
291    // this actually is happening (and somebody isn't accidentally creating the
292    // view twice), we check for the RVH Factory, which will be set when we're
293    // making special ones (which go along with the special views).
294    DCHECK(RenderViewHostFactory::has_factory());
295    return static_cast<RenderWidgetHostViewBase*>(
296        render_widget_host->GetView());
297  }
298
299  RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(
300      render_widget_host);
301  if (delegate()) {
302    base::scoped_nsobject<NSObject<RenderWidgetHostViewMacDelegate> >
303        rw_delegate(
304            delegate()->CreateRenderWidgetHostViewDelegate(render_widget_host));
305
306    view->SetDelegate(rw_delegate.get());
307  }
308  view->SetAllowPauseForResizeOrRepaint(!allow_other_views_);
309
310  // Fancy layout comes later; for now just make it our size and resize it
311  // with us. In case there are other siblings of the content area, we want
312  // to make sure the content area is on the bottom so other things draw over
313  // it.
314  NSView* view_view = view->GetNativeView();
315  [view_view setFrame:[cocoa_view_.get() bounds]];
316  [view_view setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
317  // Add the new view below all other views; this also keeps it below any
318  // overlay view installed.
319  [cocoa_view_.get() addSubview:view_view
320                     positioned:NSWindowBelow
321                     relativeTo:nil];
322  // For some reason known only to Cocoa, the autorecalculation of the key view
323  // loop set on the window doesn't set the next key view when the subview is
324  // added. On 10.6 things magically work fine; on 10.5 they fail
325  // <http://crbug.com/61493>. Digging into Cocoa key view loop code yielded
326  // madness; TODO(avi,rohit): look at this again and figure out what's really
327  // going on.
328  [cocoa_view_.get() setNextKeyView:view_view];
329  return view;
330}
331
332RenderWidgetHostViewBase* WebContentsViewMac::CreateViewForPopupWidget(
333    RenderWidgetHost* render_widget_host) {
334  return new RenderWidgetHostViewMac(render_widget_host);
335}
336
337void WebContentsViewMac::SetPageTitle(const base::string16& title) {
338  // Meaningless on the Mac; widgets don't have a "title" attribute
339}
340
341
342void WebContentsViewMac::RenderViewCreated(RenderViewHost* host) {
343  // We want updates whenever the intrinsic width of the webpage changes.
344  // Put the RenderView into that mode. The preferred width is used for example
345  // when the "zoom" button in the browser window is clicked.
346  host->EnablePreferredSizeMode();
347}
348
349void WebContentsViewMac::RenderViewSwappedIn(RenderViewHost* host) {
350}
351
352void WebContentsViewMac::SetOverscrollControllerEnabled(bool enabled) {
353}
354
355bool WebContentsViewMac::IsEventTracking() const {
356  return base::MessagePumpMac::IsHandlingSendEvent();
357}
358
359// Arrange to call CloseTab() after we're back to the main event loop.
360// The obvious way to do this would be PostNonNestableTask(), but that
361// will fire when the event-tracking loop polls for events.  So we
362// need to bounce the message via Cocoa, instead.
363void WebContentsViewMac::CloseTabAfterEventTracking() {
364  [cocoa_view_ cancelDeferredClose];
365  [cocoa_view_ performSelector:@selector(closeTabAfterEvent)
366                    withObject:nil
367                    afterDelay:0.0];
368}
369
370void WebContentsViewMac::CloseTab() {
371  web_contents_->Close(web_contents_->GetRenderViewHost());
372}
373
374}  // namespace content
375
376@implementation WebContentsViewCocoa
377
378- (id)initWithWebContentsViewMac:(WebContentsViewMac*)w {
379  self = [super initWithFrame:NSZeroRect];
380  if (self != nil) {
381    webContentsView_ = w;
382    dragDest_.reset(
383        [[WebDragDest alloc] initWithWebContentsImpl:[self webContents]]);
384    [self registerDragTypes];
385
386    [[NSNotificationCenter defaultCenter]
387         addObserver:self
388            selector:@selector(viewDidBecomeFirstResponder:)
389                name:kViewDidBecomeFirstResponder
390              object:nil];
391
392    if (webContentsView_->delegate()) {
393      [dragDest_ setDragDelegate:webContentsView_->delegate()->
394          GetDragDestDelegate()];
395    }
396  }
397  return self;
398}
399
400- (void)dealloc {
401  // Cancel any deferred tab closes, just in case.
402  [self cancelDeferredClose];
403
404  // This probably isn't strictly necessary, but can't hurt.
405  [self unregisterDraggedTypes];
406
407  [[NSNotificationCenter defaultCenter] removeObserver:self];
408
409  [super dealloc];
410}
411
412// Registers for the view for the appropriate drag types.
413- (void)registerDragTypes {
414  NSArray* types = [NSArray arrayWithObjects:
415      ui::kChromeDragDummyPboardType,
416      kWebURLsWithTitlesPboardType,
417      NSURLPboardType,
418      NSStringPboardType,
419      NSHTMLPboardType,
420      NSRTFPboardType,
421      NSFilenamesPboardType,
422      ui::kWebCustomDataPboardType,
423      nil];
424  [self registerForDraggedTypes:types];
425}
426
427- (void)setCurrentDragOperation:(NSDragOperation)operation {
428  [dragDest_ setCurrentOperation:operation];
429}
430
431- (DropData*)dropData {
432  return [dragDest_ currentDropData];
433}
434
435- (WebContentsImpl*)webContents {
436  if (webContentsView_ == NULL)
437    return NULL;
438  return webContentsView_->web_contents();
439}
440
441- (void)mouseEvent:(NSEvent*)theEvent {
442  WebContentsImpl* webContents = [self webContents];
443  if (webContents && webContents->GetDelegate()) {
444    NSPoint location = [NSEvent mouseLocation];
445    if ([theEvent type] == NSMouseMoved)
446      webContents->GetDelegate()->ContentsMouseEvent(
447          webContents, gfx::Point(location.x, location.y), true);
448    if ([theEvent type] == NSMouseExited)
449      webContents->GetDelegate()->ContentsMouseEvent(
450          webContents, gfx::Point(location.x, location.y), false);
451  }
452}
453
454- (void)setMouseDownCanMoveWindow:(BOOL)canMove {
455  mouseDownCanMoveWindow_ = canMove;
456}
457
458- (BOOL)mouseDownCanMoveWindow {
459  // This is needed to prevent mouseDowns from moving the window
460  // around.  The default implementation returns YES only for opaque
461  // views.  WebContentsViewCocoa does not draw itself in any way, but
462  // its subviews do paint their entire frames.  Returning NO here
463  // saves us the effort of overriding this method in every possible
464  // subview.
465  return mouseDownCanMoveWindow_;
466}
467
468- (void)pasteboard:(NSPasteboard*)sender provideDataForType:(NSString*)type {
469  [dragSource_ lazyWriteToPasteboard:sender
470                             forType:type];
471}
472
473- (void)startDragWithDropData:(const DropData&)dropData
474            dragOperationMask:(NSDragOperation)operationMask
475                        image:(NSImage*)image
476                       offset:(NSPoint)offset {
477  dragSource_.reset([[WebDragSource alloc]
478      initWithContents:[self webContents]
479                  view:self
480              dropData:&dropData
481                 image:image
482                offset:offset
483            pasteboard:[NSPasteboard pasteboardWithName:NSDragPboard]
484     dragOperationMask:operationMask]);
485  [dragSource_ startDrag];
486}
487
488// NSDraggingSource methods
489
490// Returns what kind of drag operations are available. This is a required
491// method for NSDraggingSource.
492- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
493  if (dragSource_)
494    return [dragSource_ draggingSourceOperationMaskForLocal:isLocal];
495  // No web drag source - this is the case for dragging a file from the
496  // downloads manager. Default to copy operation. Note: It is desirable to
497  // allow the user to either move or copy, but this requires additional
498  // plumbing to update the download item's path once its moved.
499  return NSDragOperationCopy;
500}
501
502// Called when a drag initiated in our view ends.
503- (void)draggedImage:(NSImage*)anImage
504             endedAt:(NSPoint)screenPoint
505           operation:(NSDragOperation)operation {
506  [dragSource_ endDragAt:screenPoint operation:operation];
507
508  // Might as well throw out this object now.
509  dragSource_.reset();
510}
511
512// Called when a drag initiated in our view moves.
513- (void)draggedImage:(NSImage*)draggedImage movedTo:(NSPoint)screenPoint {
514}
515
516// Called when a file drag is dropped and the promised files need to be written.
517- (NSArray*)namesOfPromisedFilesDroppedAtDestination:(NSURL*)dropDest {
518  if (![dropDest isFileURL])
519    return nil;
520
521  NSString* fileName = [dragSource_ dragPromisedFileTo:[dropDest path]];
522  if (!fileName)
523    return nil;
524
525  return @[ fileName ];
526}
527
528// NSDraggingDestination methods
529
530- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
531  return [dragDest_ draggingEntered:sender view:self];
532}
533
534- (void)draggingExited:(id<NSDraggingInfo>)sender {
535  [dragDest_ draggingExited:sender];
536}
537
538- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
539  return [dragDest_ draggingUpdated:sender view:self];
540}
541
542- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
543  return [dragDest_ performDragOperation:sender view:self];
544}
545
546- (void)cancelDeferredClose {
547  SEL aSel = @selector(closeTabAfterEvent);
548  [NSObject cancelPreviousPerformRequestsWithTarget:self
549                                           selector:aSel
550                                             object:nil];
551}
552
553- (void)clearWebContentsView {
554  webContentsView_ = NULL;
555  [dragSource_ clearWebContentsView];
556}
557
558- (void)closeTabAfterEvent {
559  webContentsView_->CloseTab();
560}
561
562- (void)viewDidBecomeFirstResponder:(NSNotification*)notification {
563  NSView* view = [notification object];
564  if (![[self subviews] containsObject:view])
565    return;
566
567  NSSelectionDirection direction =
568      [[[notification userInfo] objectForKey:kSelectionDirection]
569        unsignedIntegerValue];
570  if (direction == NSDirectSelection)
571    return;
572
573  [self webContents]->
574      FocusThroughTabTraversal(direction == NSSelectingPrevious);
575}
576
577// When the subviews require a layout, their size should be reset to the size
578// of this view. (It is possible for the size to get out of sync as an
579// optimization in preparation for an upcoming WebContentsView resize.
580// http://crbug.com/264207)
581- (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize {
582  for (NSView* subview in self.subviews)
583    [subview setFrame:self.bounds];
584}
585
586@end
587