tab_contents_view_mac.mm revision ddb351dbec246cf1fab5ec20d2d5520909041de1
1// Copyright (c) 2011 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#include "chrome/browser/tab_contents/tab_contents_view_mac.h"
8
9#include <string>
10
11#include "chrome/browser/global_keyboard_shortcuts_mac.h"
12#include "chrome/browser/renderer_host/render_widget_host_view_mac.h"
13#include "chrome/browser/tab_contents/popup_menu_helper_mac.h"
14#include "chrome/browser/tab_contents/render_view_context_menu_mac.h"
15#import "chrome/browser/ui/cocoa/browser_window_controller.h"
16#import "chrome/browser/ui/cocoa/focus_tracker.h"
17#import "chrome/browser/ui/cocoa/tab_contents/sad_tab_controller.h"
18#import "chrome/browser/ui/cocoa/tab_contents/web_drag_source.h"
19#import "chrome/browser/ui/cocoa/tab_contents/web_drop_target.h"
20#import "chrome/browser/ui/cocoa/view_id_util.h"
21#include "chrome/common/render_messages.h"
22#include "content/browser/renderer_host/render_view_host.h"
23#include "content/browser/renderer_host/render_view_host_factory.h"
24#include "content/browser/renderer_host/render_widget_host.h"
25#include "content/browser/tab_contents/tab_contents.h"
26#include "content/browser/tab_contents/tab_contents_delegate.h"
27#import "content/common/chrome_application_mac.h"
28#include "content/common/notification_details.h"
29#include "content/common/notification_source.h"
30#include "content/common/notification_type.h"
31#include "content/common/view_messages.h"
32#include "skia/ext/skia_utils_mac.h"
33#import "third_party/mozilla/NSPasteboard+Utils.h"
34
35using WebKit::WebDragOperation;
36using WebKit::WebDragOperationsMask;
37
38// Ensure that the WebKit::WebDragOperation enum values stay in sync with
39// NSDragOperation constants, since the code below static_casts between 'em.
40#define COMPILE_ASSERT_MATCHING_ENUM(name) \
41  COMPILE_ASSERT(int(NS##name) == int(WebKit::Web##name), enum_mismatch_##name)
42COMPILE_ASSERT_MATCHING_ENUM(DragOperationNone);
43COMPILE_ASSERT_MATCHING_ENUM(DragOperationCopy);
44COMPILE_ASSERT_MATCHING_ENUM(DragOperationLink);
45COMPILE_ASSERT_MATCHING_ENUM(DragOperationGeneric);
46COMPILE_ASSERT_MATCHING_ENUM(DragOperationPrivate);
47COMPILE_ASSERT_MATCHING_ENUM(DragOperationMove);
48COMPILE_ASSERT_MATCHING_ENUM(DragOperationDelete);
49COMPILE_ASSERT_MATCHING_ENUM(DragOperationEvery);
50
51@interface TabContentsViewCocoa (Private)
52- (id)initWithTabContentsViewMac:(TabContentsViewMac*)w;
53- (void)registerDragTypes;
54- (void)setCurrentDragOperation:(NSDragOperation)operation;
55- (void)startDragWithDropData:(const WebDropData&)dropData
56            dragOperationMask:(NSDragOperation)operationMask
57                        image:(NSImage*)image
58                       offset:(NSPoint)offset;
59- (void)cancelDeferredClose;
60- (void)closeTabAfterEvent;
61- (void)viewDidBecomeFirstResponder:(NSNotification*)notification;
62@end
63
64// static
65TabContentsView* TabContentsView::Create(TabContents* tab_contents) {
66  return new TabContentsViewMac(tab_contents);
67}
68
69TabContentsViewMac::TabContentsViewMac(TabContents* tab_contents)
70    : TabContentsView(tab_contents),
71      preferred_width_(0) {
72  registrar_.Add(this, NotificationType::TAB_CONTENTS_CONNECTED,
73                 Source<TabContents>(tab_contents));
74}
75
76TabContentsViewMac::~TabContentsViewMac() {
77  // This handles the case where a renderer close call was deferred
78  // while the user was operating a UI control which resulted in a
79  // close.  In that case, the Cocoa view outlives the
80  // TabContentsViewMac instance due to Cocoa retain count.
81  [cocoa_view_ cancelDeferredClose];
82}
83
84void TabContentsViewMac::CreateView(const gfx::Size& initial_size) {
85  TabContentsViewCocoa* view =
86      [[TabContentsViewCocoa alloc] initWithTabContentsViewMac:this];
87  [view setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
88  cocoa_view_.reset(view);
89}
90
91RenderWidgetHostView* TabContentsViewMac::CreateViewForWidget(
92    RenderWidgetHost* render_widget_host) {
93  if (render_widget_host->view()) {
94    // During testing, the view will already be set up in most cases to the
95    // test view, so we don't want to clobber it with a real one. To verify that
96    // this actually is happening (and somebody isn't accidentally creating the
97    // view twice), we check for the RVH Factory, which will be set when we're
98    // making special ones (which go along with the special views).
99    DCHECK(RenderViewHostFactory::has_factory());
100    return render_widget_host->view();
101  }
102
103  RenderWidgetHostViewMac* view =
104      new RenderWidgetHostViewMac(render_widget_host);
105
106  // Fancy layout comes later; for now just make it our size and resize it
107  // with us. In case there are other siblings of the content area, we want
108  // to make sure the content area is on the bottom so other things draw over
109  // it.
110  NSView* view_view = view->native_view();
111  [view_view setFrame:[cocoa_view_.get() bounds]];
112  [view_view setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
113  [cocoa_view_.get() addSubview:view_view
114                     positioned:NSWindowBelow
115                     relativeTo:nil];
116  // For some reason known only to Cocoa, the autorecalculation of the key view
117  // loop set on the window doesn't set the next key view when the subview is
118  // added. On 10.6 things magically work fine; on 10.5 they fail
119  // <http://crbug.com/61493>. Digging into Cocoa key view loop code yielded
120  // madness; TODO(avi,rohit): look at this again and figure out what's really
121  // going on.
122  [cocoa_view_.get() setNextKeyView:view_view];
123  return view;
124}
125
126gfx::NativeView TabContentsViewMac::GetNativeView() const {
127  return cocoa_view_.get();
128}
129
130gfx::NativeView TabContentsViewMac::GetContentNativeView() const {
131  RenderWidgetHostView* rwhv = tab_contents()->GetRenderWidgetHostView();
132  if (!rwhv)
133    return NULL;
134  return rwhv->GetNativeView();
135}
136
137gfx::NativeWindow TabContentsViewMac::GetTopLevelNativeWindow() const {
138  return [cocoa_view_.get() window];
139}
140
141void TabContentsViewMac::GetContainerBounds(gfx::Rect* out) const {
142  *out = [cocoa_view_.get() flipNSRectToRect:[cocoa_view_.get() bounds]];
143}
144
145void TabContentsViewMac::StartDragging(
146    const WebDropData& drop_data,
147    WebDragOperationsMask allowed_operations,
148    const SkBitmap& image,
149    const gfx::Point& image_offset) {
150  // By allowing nested tasks, the code below also allows Close(),
151  // which would deallocate |this|.  The same problem can occur while
152  // processing -sendEvent:, so Close() is deferred in that case.
153  // Drags from web content do not come via -sendEvent:, this sets the
154  // same flag -sendEvent: would.
155  chrome_application_mac::ScopedSendingEvent sendingEventScoper;
156
157  // The drag invokes a nested event loop, arrange to continue
158  // processing events.
159  MessageLoop::ScopedNestableTaskAllower allow(MessageLoop::current());
160  NSDragOperation mask = static_cast<NSDragOperation>(allowed_operations);
161  NSPoint offset = NSPointFromCGPoint(image_offset.ToCGPoint());
162  [cocoa_view_ startDragWithDropData:drop_data
163                   dragOperationMask:mask
164                               image:gfx::SkBitmapToNSImage(image)
165                              offset:offset];
166}
167
168void TabContentsViewMac::RenderViewCreated(RenderViewHost* host) {
169  // We want updates whenever the intrinsic width of the webpage changes.
170  // Put the RenderView into that mode. The preferred width is used for example
171  // when the "zoom" button in the browser window is clicked.
172  host->EnablePreferredSizeChangedMode(kPreferredSizeWidth);
173}
174
175void TabContentsViewMac::SetPageTitle(const std::wstring& title) {
176  // Meaningless on the Mac; widgets don't have a "title" attribute
177}
178
179void TabContentsViewMac::OnTabCrashed(base::TerminationStatus /* status */,
180                                      int /* error_code */) {
181  if (!sad_tab_.get()) {
182    TabContents* contents = tab_contents();
183    DCHECK(contents);
184    if (contents) {
185      SadTabController* sad_tab =
186          [[SadTabController alloc] initWithTabContents:contents
187                                              superview:cocoa_view_];
188      sad_tab_.reset(sad_tab);
189    }
190  }
191}
192
193void TabContentsViewMac::SizeContents(const gfx::Size& size) {
194  // TODO(brettw | japhet) This is a hack and should be removed.
195  // See tab_contents_view.h.
196  gfx::Rect rect(gfx::Point(), size);
197  TabContentsViewCocoa* view = cocoa_view_.get();
198  [view setFrame:[view flipRectToNSRect:rect]];
199}
200
201void TabContentsViewMac::Focus() {
202  [[cocoa_view_.get() window] makeFirstResponder:GetContentNativeView()];
203  [[cocoa_view_.get() window] makeKeyAndOrderFront:GetContentNativeView()];
204}
205
206void TabContentsViewMac::SetInitialFocus() {
207  if (tab_contents()->FocusLocationBarByDefault())
208    tab_contents()->SetFocusToLocationBar(false);
209  else
210    [[cocoa_view_.get() window] makeFirstResponder:GetContentNativeView()];
211}
212
213void TabContentsViewMac::StoreFocus() {
214  // We're explicitly being asked to store focus, so don't worry if there's
215  // already a view saved.
216  focus_tracker_.reset(
217      [[FocusTracker alloc] initWithWindow:[cocoa_view_ window]]);
218}
219
220void TabContentsViewMac::RestoreFocus() {
221  // TODO(avi): Could we be restoring a view that's no longer in the key view
222  // chain?
223  if (!(focus_tracker_.get() &&
224        [focus_tracker_ restoreFocusInWindow:[cocoa_view_ window]])) {
225    // Fall back to the default focus behavior if we could not restore focus.
226    // TODO(shess): If location-bar gets focus by default, this will
227    // select-all in the field.  If there was a specific selection in
228    // the field when we navigated away from it, we should restore
229    // that selection.
230    SetInitialFocus();
231  }
232
233  focus_tracker_.reset(nil);
234}
235
236void TabContentsViewMac::UpdatePreferredSize(const gfx::Size& pref_size) {
237  preferred_width_ = pref_size.width();
238  TabContentsView::UpdatePreferredSize(pref_size);
239}
240
241void TabContentsViewMac::UpdateDragCursor(WebDragOperation operation) {
242  [cocoa_view_ setCurrentDragOperation: operation];
243}
244
245void TabContentsViewMac::GotFocus() {
246  // This is only used in the views FocusManager stuff but it bleeds through
247  // all subclasses. http://crbug.com/21875
248}
249
250// This is called when the renderer asks us to take focus back (i.e., it has
251// iterated past the last focusable element on the page).
252void TabContentsViewMac::TakeFocus(bool reverse) {
253  if (reverse) {
254    [[cocoa_view_ window] selectPreviousKeyView:cocoa_view_.get()];
255  } else {
256    [[cocoa_view_ window] selectNextKeyView:cocoa_view_.get()];
257  }
258}
259
260void TabContentsViewMac::ShowContextMenu(const ContextMenuParams& params) {
261  RenderViewContextMenuMac menu(tab_contents(),
262                                params,
263                                GetContentNativeView());
264  menu.Init();
265}
266
267// Display a popup menu for WebKit using Cocoa widgets.
268void TabContentsViewMac::ShowPopupMenu(
269    const gfx::Rect& bounds,
270    int item_height,
271    double item_font_size,
272    int selected_item,
273    const std::vector<WebMenuItem>& items,
274    bool right_aligned) {
275  PopupMenuHelper popup_menu_helper(tab_contents()->render_view_host());
276  popup_menu_helper.ShowPopupMenu(bounds, item_height, item_font_size,
277                                  selected_item, items, right_aligned);
278}
279
280RenderWidgetHostView* TabContentsViewMac::CreateNewWidgetInternal(
281    int route_id,
282    WebKit::WebPopupType popup_type) {
283  // A RenderWidgetHostViewMac has lifetime scoped to the view. We'll retain it
284  // to allow it to survive the trip without being hosted.
285  RenderWidgetHostView* widget_view =
286      TabContentsView::CreateNewWidgetInternal(route_id, popup_type);
287  RenderWidgetHostViewMac* widget_view_mac =
288      static_cast<RenderWidgetHostViewMac*>(widget_view);
289  [widget_view_mac->native_view() retain];
290
291  return widget_view;
292}
293
294void TabContentsViewMac::ShowCreatedWidgetInternal(
295    RenderWidgetHostView* widget_host_view,
296    const gfx::Rect& initial_pos) {
297  TabContentsView::ShowCreatedWidgetInternal(widget_host_view, initial_pos);
298
299  // A RenderWidgetHostViewMac has lifetime scoped to the view. Now that it's
300  // properly embedded (or purposefully ignored) we can release the retain we
301  // took in CreateNewWidgetInternal().
302  RenderWidgetHostViewMac* widget_view_mac =
303      static_cast<RenderWidgetHostViewMac*>(widget_host_view);
304  [widget_view_mac->native_view() release];
305}
306
307bool TabContentsViewMac::IsEventTracking() const {
308  if ([NSApp isKindOfClass:[CrApplication class]] &&
309      [static_cast<CrApplication*>(NSApp) isHandlingSendEvent]) {
310    return true;
311  }
312  return false;
313}
314
315// Arrange to call CloseTab() after we're back to the main event loop.
316// The obvious way to do this would be PostNonNestableTask(), but that
317// will fire when the event-tracking loop polls for events.  So we
318// need to bounce the message via Cocoa, instead.
319void TabContentsViewMac::CloseTabAfterEventTracking() {
320  [cocoa_view_ cancelDeferredClose];
321  [cocoa_view_ performSelector:@selector(closeTabAfterEvent)
322                    withObject:nil
323                    afterDelay:0.0];
324}
325
326void TabContentsViewMac::GetViewBounds(gfx::Rect* out) const {
327  // This method is noth currently used on mac.
328  NOTIMPLEMENTED();
329}
330
331void TabContentsViewMac::CloseTab() {
332  tab_contents()->Close(tab_contents()->render_view_host());
333}
334
335void TabContentsViewMac::Observe(NotificationType type,
336                                 const NotificationSource& source,
337                                 const NotificationDetails& details) {
338  switch (type.value) {
339    case NotificationType::TAB_CONTENTS_CONNECTED: {
340      sad_tab_.reset();
341      break;
342    }
343    default:
344      NOTREACHED() << "Got a notification we didn't register for.";
345  }
346}
347
348@implementation TabContentsViewCocoa
349
350- (id)initWithTabContentsViewMac:(TabContentsViewMac*)w {
351  self = [super initWithFrame:NSZeroRect];
352  if (self != nil) {
353    tabContentsView_ = w;
354    dropTarget_.reset(
355        [[WebDropTarget alloc] initWithTabContents:[self tabContents]]);
356    [self registerDragTypes];
357    // TabContentsViewCocoa's ViewID may be changed to VIEW_ID_DEV_TOOLS_DOCKED
358    // by TabContentsController, so we can't just override -viewID method to
359    // return it.
360    view_id_util::SetID(self, VIEW_ID_TAB_CONTAINER);
361
362    [[NSNotificationCenter defaultCenter]
363         addObserver:self
364            selector:@selector(viewDidBecomeFirstResponder:)
365                name:kViewDidBecomeFirstResponder
366              object:nil];
367  }
368  return self;
369}
370
371- (void)dealloc {
372  view_id_util::UnsetID(self);
373
374  // Cancel any deferred tab closes, just in case.
375  [self cancelDeferredClose];
376
377  // This probably isn't strictly necessary, but can't hurt.
378  [self unregisterDraggedTypes];
379
380  [[NSNotificationCenter defaultCenter] removeObserver:self];
381
382  [super dealloc];
383}
384
385// Registers for the view for the appropriate drag types.
386- (void)registerDragTypes {
387  NSArray* types = [NSArray arrayWithObjects:NSStringPboardType,
388      NSHTMLPboardType, NSURLPboardType, nil];
389  [self registerForDraggedTypes:types];
390}
391
392- (void)setCurrentDragOperation:(NSDragOperation)operation {
393  [dropTarget_ setCurrentOperation:operation];
394}
395
396- (TabContents*)tabContents {
397  return tabContentsView_->tab_contents();
398}
399
400- (void)mouseEvent:(NSEvent *)theEvent {
401  TabContents* tabContents = [self tabContents];
402  if (tabContents->delegate()) {
403    NSPoint location = [NSEvent mouseLocation];
404    if ([theEvent type] == NSMouseMoved)
405      tabContents->delegate()->ContentsMouseEvent(
406          tabContents, gfx::Point(location.x, location.y), true);
407    if ([theEvent type] == NSMouseExited)
408      tabContents->delegate()->ContentsMouseEvent(
409          tabContents, gfx::Point(location.x, location.y), false);
410  }
411}
412
413- (BOOL)mouseDownCanMoveWindow {
414  // This is needed to prevent mouseDowns from moving the window
415  // around.  The default implementation returns YES only for opaque
416  // views.  TabContentsViewCocoa does not draw itself in any way, but
417  // its subviews do paint their entire frames.  Returning NO here
418  // saves us the effort of overriding this method in every possible
419  // subview.
420  return NO;
421}
422
423- (void)pasteboard:(NSPasteboard*)sender provideDataForType:(NSString*)type {
424  [dragSource_ lazyWriteToPasteboard:sender
425                             forType:type];
426}
427
428- (void)startDragWithDropData:(const WebDropData&)dropData
429            dragOperationMask:(NSDragOperation)operationMask
430                        image:(NSImage*)image
431                       offset:(NSPoint)offset {
432  dragSource_.reset([[WebDragSource alloc]
433          initWithContentsView:self
434                      dropData:&dropData
435                         image:image
436                        offset:offset
437                    pasteboard:[NSPasteboard pasteboardWithName:NSDragPboard]
438             dragOperationMask:operationMask]);
439  [dragSource_ startDrag];
440}
441
442// NSDraggingSource methods
443
444// Returns what kind of drag operations are available. This is a required
445// method for NSDraggingSource.
446- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
447  return [dragSource_ draggingSourceOperationMaskForLocal:isLocal];
448}
449
450// Called when a drag initiated in our view ends.
451- (void)draggedImage:(NSImage*)anImage
452             endedAt:(NSPoint)screenPoint
453           operation:(NSDragOperation)operation {
454  [dragSource_ endDragAt:screenPoint operation:operation];
455
456  // Might as well throw out this object now.
457  dragSource_.reset();
458}
459
460// Called when a drag initiated in our view moves.
461- (void)draggedImage:(NSImage*)draggedImage movedTo:(NSPoint)screenPoint {
462  [dragSource_ moveDragTo:screenPoint];
463}
464
465// Called when we're informed where a file should be dropped.
466- (NSArray*)namesOfPromisedFilesDroppedAtDestination:(NSURL*)dropDest {
467  if (![dropDest isFileURL])
468    return nil;
469
470  NSString* file_name = [dragSource_ dragPromisedFileTo:[dropDest path]];
471  if (!file_name)
472    return nil;
473
474  return [NSArray arrayWithObject:file_name];
475}
476
477// NSDraggingDestination methods
478
479- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
480  return [dropTarget_ draggingEntered:sender view:self];
481}
482
483- (void)draggingExited:(id<NSDraggingInfo>)sender {
484  [dropTarget_ draggingExited:sender];
485}
486
487- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
488  return [dropTarget_ draggingUpdated:sender view:self];
489}
490
491- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
492  return [dropTarget_ performDragOperation:sender view:self];
493}
494
495- (void)cancelDeferredClose {
496  SEL aSel = @selector(closeTabAfterEvent);
497  [NSObject cancelPreviousPerformRequestsWithTarget:self
498                                           selector:aSel
499                                             object:nil];
500}
501
502- (void)closeTabAfterEvent {
503  tabContentsView_->CloseTab();
504}
505
506- (void)viewDidBecomeFirstResponder:(NSNotification*)notification {
507  NSView* view = [notification object];
508  if (![[self subviews] containsObject:view])
509    return;
510
511  NSSelectionDirection direction =
512      [[[notification userInfo] objectForKey:kSelectionDirection]
513        unsignedIntegerValue];
514  if (direction == NSDirectSelection)
515    return;
516
517  [self tabContents]->
518      FocusThroughTabTraversal(direction == NSSelectingPrevious);
519}
520
521@end
522