web_contents_view_mac.mm revision 116680a4aac90f2aa7413d9095a592090648e557
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/renderer_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 {
71WebContentsView* CreateWebContentsView(
72    WebContentsImpl* web_contents,
73    WebContentsViewDelegate* delegate,
74    RenderViewHostDelegateView** render_view_host_delegate_view) {
75  WebContentsViewMac* rv = new WebContentsViewMac(web_contents, delegate);
76  *render_view_host_delegate_view = rv;
77  return rv;
78}
79
80WebContentsViewMac::WebContentsViewMac(WebContentsImpl* web_contents,
81                                       WebContentsViewDelegate* delegate)
82    : web_contents_(web_contents),
83      delegate_(delegate),
84      allow_overlapping_views_(false),
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    content::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
237// Display a popup menu for WebKit using Cocoa widgets.
238void WebContentsViewMac::ShowPopupMenu(
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(
247      new PopupMenuHelper(web_contents_->GetRenderViewHost()));
248  popup_menu_helper_->ShowPopupMenu(bounds, item_height, item_font_size,
249                                    selected_item, items, right_aligned,
250                                    allow_multiple_selection);
251  popup_menu_helper_.reset();
252}
253
254void WebContentsViewMac::HidePopupMenu() {
255  if (popup_menu_helper_)
256    popup_menu_helper_->Hide();
257}
258
259gfx::Rect WebContentsViewMac::GetViewBounds() const {
260  // This method is not currently used on mac.
261  NOTIMPLEMENTED();
262  return gfx::Rect();
263}
264
265void WebContentsViewMac::SetAllowOverlappingViews(bool overlapping) {
266  if (allow_overlapping_views_ == overlapping)
267    return;
268
269  allow_overlapping_views_ = overlapping;
270  RenderWidgetHostViewMac* view = static_cast<RenderWidgetHostViewMac*>(
271      web_contents_->GetRenderWidgetHostView());
272  if (view)
273    view->SetAllowOverlappingViews(allow_overlapping_views_);
274}
275
276bool WebContentsViewMac::GetAllowOverlappingViews() const {
277  return allow_overlapping_views_;
278}
279
280void WebContentsViewMac::SetAllowOtherViews(bool allow) {
281  if (allow_other_views_ == allow)
282    return;
283
284  allow_other_views_ = allow;
285  RenderWidgetHostViewMac* view = static_cast<RenderWidgetHostViewMac*>(
286      web_contents_->GetRenderWidgetHostView());
287  if (view)
288    view->SetAllowPauseForResizeOrRepaint(!allow_other_views_);
289}
290
291bool WebContentsViewMac::GetAllowOtherViews() const {
292  return allow_other_views_;
293}
294
295void WebContentsViewMac::CreateView(
296    const gfx::Size& initial_size, gfx::NativeView context) {
297  WebContentsViewCocoa* view =
298      [[WebContentsViewCocoa alloc] initWithWebContentsViewMac:this];
299  cocoa_view_.reset(view);
300}
301
302RenderWidgetHostViewBase* WebContentsViewMac::CreateViewForWidget(
303    RenderWidgetHost* render_widget_host) {
304  if (render_widget_host->GetView()) {
305    // During testing, the view will already be set up in most cases to the
306    // test view, so we don't want to clobber it with a real one. To verify that
307    // this actually is happening (and somebody isn't accidentally creating the
308    // view twice), we check for the RVH Factory, which will be set when we're
309    // making special ones (which go along with the special views).
310    DCHECK(RenderViewHostFactory::has_factory());
311    return static_cast<RenderWidgetHostViewBase*>(
312        render_widget_host->GetView());
313  }
314
315  RenderWidgetHostViewMac* view = new RenderWidgetHostViewMac(
316      render_widget_host);
317  if (delegate()) {
318    base::scoped_nsobject<NSObject<RenderWidgetHostViewMacDelegate> >
319        rw_delegate(
320            delegate()->CreateRenderWidgetHostViewDelegate(render_widget_host));
321
322    view->SetDelegate(rw_delegate.get());
323  }
324  view->SetAllowOverlappingViews(allow_overlapping_views_);
325  view->SetAllowPauseForResizeOrRepaint(!allow_other_views_);
326
327  // Fancy layout comes later; for now just make it our size and resize it
328  // with us. In case there are other siblings of the content area, we want
329  // to make sure the content area is on the bottom so other things draw over
330  // it.
331  NSView* view_view = view->GetNativeView();
332  [view_view setFrame:[cocoa_view_.get() bounds]];
333  [view_view setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
334  // Add the new view below all other views; this also keeps it below any
335  // overlay view installed.
336  [cocoa_view_.get() addSubview:view_view
337                     positioned:NSWindowBelow
338                     relativeTo:nil];
339  // For some reason known only to Cocoa, the autorecalculation of the key view
340  // loop set on the window doesn't set the next key view when the subview is
341  // added. On 10.6 things magically work fine; on 10.5 they fail
342  // <http://crbug.com/61493>. Digging into Cocoa key view loop code yielded
343  // madness; TODO(avi,rohit): look at this again and figure out what's really
344  // going on.
345  [cocoa_view_.get() setNextKeyView:view_view];
346  return view;
347}
348
349RenderWidgetHostViewBase* WebContentsViewMac::CreateViewForPopupWidget(
350    RenderWidgetHost* render_widget_host) {
351  return new RenderWidgetHostViewMac(render_widget_host);
352}
353
354void WebContentsViewMac::SetPageTitle(const base::string16& title) {
355  // Meaningless on the Mac; widgets don't have a "title" attribute
356}
357
358
359void WebContentsViewMac::RenderViewCreated(RenderViewHost* host) {
360  // We want updates whenever the intrinsic width of the webpage changes.
361  // Put the RenderView into that mode. The preferred width is used for example
362  // when the "zoom" button in the browser window is clicked.
363  host->EnablePreferredSizeMode();
364}
365
366void WebContentsViewMac::RenderViewSwappedIn(RenderViewHost* host) {
367}
368
369void WebContentsViewMac::SetOverscrollControllerEnabled(bool enabled) {
370}
371
372bool WebContentsViewMac::IsEventTracking() const {
373  return base::MessagePumpMac::IsHandlingSendEvent();
374}
375
376// Arrange to call CloseTab() after we're back to the main event loop.
377// The obvious way to do this would be PostNonNestableTask(), but that
378// will fire when the event-tracking loop polls for events.  So we
379// need to bounce the message via Cocoa, instead.
380void WebContentsViewMac::CloseTabAfterEventTracking() {
381  [cocoa_view_ cancelDeferredClose];
382  [cocoa_view_ performSelector:@selector(closeTabAfterEvent)
383                    withObject:nil
384                    afterDelay:0.0];
385}
386
387void WebContentsViewMac::CloseTab() {
388  web_contents_->Close(web_contents_->GetRenderViewHost());
389}
390
391}  // namespace content
392
393@implementation WebContentsViewCocoa
394
395- (id)initWithWebContentsViewMac:(WebContentsViewMac*)w {
396  self = [super initWithFrame:NSZeroRect];
397  if (self != nil) {
398    webContentsView_ = w;
399    dragDest_.reset(
400        [[WebDragDest alloc] initWithWebContentsImpl:[self webContents]]);
401    [self registerDragTypes];
402
403    [[NSNotificationCenter defaultCenter]
404         addObserver:self
405            selector:@selector(viewDidBecomeFirstResponder:)
406                name:kViewDidBecomeFirstResponder
407              object:nil];
408
409    if (webContentsView_->delegate()) {
410      [dragDest_ setDragDelegate:webContentsView_->delegate()->
411          GetDragDestDelegate()];
412    }
413  }
414  return self;
415}
416
417- (void)dealloc {
418  // Cancel any deferred tab closes, just in case.
419  [self cancelDeferredClose];
420
421  // This probably isn't strictly necessary, but can't hurt.
422  [self unregisterDraggedTypes];
423
424  [[NSNotificationCenter defaultCenter] removeObserver:self];
425
426  [super dealloc];
427}
428
429// Registers for the view for the appropriate drag types.
430- (void)registerDragTypes {
431  NSArray* types = [NSArray arrayWithObjects:
432      ui::kChromeDragDummyPboardType,
433      kWebURLsWithTitlesPboardType,
434      NSURLPboardType,
435      NSStringPboardType,
436      NSHTMLPboardType,
437      NSRTFPboardType,
438      NSFilenamesPboardType,
439      ui::kWebCustomDataPboardType,
440      nil];
441  [self registerForDraggedTypes:types];
442}
443
444- (void)setCurrentDragOperation:(NSDragOperation)operation {
445  [dragDest_ setCurrentOperation:operation];
446}
447
448- (DropData*)dropData {
449  return [dragDest_ currentDropData];
450}
451
452- (WebContentsImpl*)webContents {
453  if (webContentsView_ == NULL)
454    return NULL;
455  return webContentsView_->web_contents();
456}
457
458- (void)mouseEvent:(NSEvent*)theEvent {
459  WebContentsImpl* webContents = [self webContents];
460  if (webContents && webContents->GetDelegate()) {
461    NSPoint location = [NSEvent mouseLocation];
462    if ([theEvent type] == NSMouseMoved)
463      webContents->GetDelegate()->ContentsMouseEvent(
464          webContents, gfx::Point(location.x, location.y), true);
465    if ([theEvent type] == NSMouseExited)
466      webContents->GetDelegate()->ContentsMouseEvent(
467          webContents, gfx::Point(location.x, location.y), false);
468  }
469}
470
471- (void)setMouseDownCanMoveWindow:(BOOL)canMove {
472  mouseDownCanMoveWindow_ = canMove;
473}
474
475- (BOOL)mouseDownCanMoveWindow {
476  // This is needed to prevent mouseDowns from moving the window
477  // around.  The default implementation returns YES only for opaque
478  // views.  WebContentsViewCocoa does not draw itself in any way, but
479  // its subviews do paint their entire frames.  Returning NO here
480  // saves us the effort of overriding this method in every possible
481  // subview.
482  return mouseDownCanMoveWindow_;
483}
484
485- (void)pasteboard:(NSPasteboard*)sender provideDataForType:(NSString*)type {
486  [dragSource_ lazyWriteToPasteboard:sender
487                             forType:type];
488}
489
490- (void)startDragWithDropData:(const DropData&)dropData
491            dragOperationMask:(NSDragOperation)operationMask
492                        image:(NSImage*)image
493                       offset:(NSPoint)offset {
494  dragSource_.reset([[WebDragSource alloc]
495      initWithContents:[self webContents]
496                  view:self
497              dropData:&dropData
498                 image:image
499                offset:offset
500            pasteboard:[NSPasteboard pasteboardWithName:NSDragPboard]
501     dragOperationMask:operationMask]);
502  [dragSource_ startDrag];
503}
504
505// NSDraggingSource methods
506
507// Returns what kind of drag operations are available. This is a required
508// method for NSDraggingSource.
509- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
510  if (dragSource_)
511    return [dragSource_ draggingSourceOperationMaskForLocal:isLocal];
512  // No web drag source - this is the case for dragging a file from the
513  // downloads manager. Default to copy operation. Note: It is desirable to
514  // allow the user to either move or copy, but this requires additional
515  // plumbing to update the download item's path once its moved.
516  return NSDragOperationCopy;
517}
518
519// Called when a drag initiated in our view ends.
520- (void)draggedImage:(NSImage*)anImage
521             endedAt:(NSPoint)screenPoint
522           operation:(NSDragOperation)operation {
523  [dragSource_ endDragAt:screenPoint operation:operation];
524
525  // Might as well throw out this object now.
526  dragSource_.reset();
527}
528
529// Called when a drag initiated in our view moves.
530- (void)draggedImage:(NSImage*)draggedImage movedTo:(NSPoint)screenPoint {
531}
532
533// Called when a file drag is dropped and the promised files need to be written.
534- (NSArray*)namesOfPromisedFilesDroppedAtDestination:(NSURL*)dropDest {
535  if (![dropDest isFileURL])
536    return nil;
537
538  NSString* fileName = [dragSource_ dragPromisedFileTo:[dropDest path]];
539  if (!fileName)
540    return nil;
541
542  return @[ fileName ];
543}
544
545// NSDraggingDestination methods
546
547- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
548  return [dragDest_ draggingEntered:sender view:self];
549}
550
551- (void)draggingExited:(id<NSDraggingInfo>)sender {
552  [dragDest_ draggingExited:sender];
553}
554
555- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
556  return [dragDest_ draggingUpdated:sender view:self];
557}
558
559- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
560  return [dragDest_ performDragOperation:sender view:self];
561}
562
563- (void)cancelDeferredClose {
564  SEL aSel = @selector(closeTabAfterEvent);
565  [NSObject cancelPreviousPerformRequestsWithTarget:self
566                                           selector:aSel
567                                             object:nil];
568}
569
570- (void)clearWebContentsView {
571  webContentsView_ = NULL;
572  [dragSource_ clearWebContentsView];
573}
574
575- (void)closeTabAfterEvent {
576  webContentsView_->CloseTab();
577}
578
579- (void)viewDidBecomeFirstResponder:(NSNotification*)notification {
580  NSView* view = [notification object];
581  if (![[self subviews] containsObject:view])
582    return;
583
584  NSSelectionDirection direction =
585      [[[notification userInfo] objectForKey:kSelectionDirection]
586        unsignedIntegerValue];
587  if (direction == NSDirectSelection)
588    return;
589
590  [self webContents]->
591      FocusThroughTabTraversal(direction == NSSelectingPrevious);
592}
593
594// When the subviews require a layout, their size should be reset to the size
595// of this view. (It is possible for the size to get out of sync as an
596// optimization in preparation for an upcoming WebContentsView resize.
597// http://crbug.com/264207)
598- (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize {
599  for (NSView* subview in self.subviews)
600    [subview setFrame:self.bounds];
601}
602
603@end
604