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