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.
5 #import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h"
7 #import <QuartzCore/QuartzCore.h>
13 #include "base/command_line.h"
14 #include "base/mac/mac_util.h"
15 #include "base/mac/scoped_nsautorelease_pool.h"
16 #include "base/metrics/histogram.h"
17 #include "base/prefs/pref_service.h"
18 #include "base/strings/sys_string_conversions.h"
19 #include "chrome/app/chrome_command_ids.h"
20 #include "chrome/browser/autocomplete/autocomplete_classifier.h"
21 #include "chrome/browser/autocomplete/autocomplete_classifier_factory.h"
22 #include "chrome/browser/autocomplete/autocomplete_input.h"
23 #include "chrome/browser/autocomplete/autocomplete_match.h"
24 #include "chrome/browser/devtools/devtools_window.h"
25 #include "chrome/browser/extensions/tab_helper.h"
26 #include "chrome/browser/favicon/favicon_tab_helper.h"
27 #include "chrome/browser/profiles/profile.h"
28 #include "chrome/browser/profiles/profile_manager.h"
29 #include "chrome/browser/themes/theme_service.h"
30 #include "chrome/browser/ui/browser.h"
31 #include "chrome/browser/ui/browser_navigator.h"
32 #include "chrome/browser/ui/browser_tabstrip.h"
33 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
34 #import "chrome/browser/ui/cocoa/constrained_window/constrained_window_sheet_controller.h"
35 #include "chrome/browser/ui/cocoa/drag_util.h"
36 #import "chrome/browser/ui/cocoa/image_button_cell.h"
37 #import "chrome/browser/ui/cocoa/new_tab_button.h"
38 #import "chrome/browser/ui/cocoa/tab_contents/favicon_util_mac.h"
39 #import "chrome/browser/ui/cocoa/tab_contents/tab_contents_controller.h"
40 #import "chrome/browser/ui/cocoa/tabs/media_indicator_view.h"
41 #import "chrome/browser/ui/cocoa/tabs/tab_controller.h"
42 #import "chrome/browser/ui/cocoa/tabs/tab_strip_drag_controller.h"
43 #import "chrome/browser/ui/cocoa/tabs/tab_strip_model_observer_bridge.h"
44 #import "chrome/browser/ui/cocoa/tabs/tab_strip_view.h"
45 #import "chrome/browser/ui/cocoa/tabs/tab_view.h"
46 #include "chrome/browser/ui/find_bar/find_bar.h"
47 #include "chrome/browser/ui/find_bar/find_bar_controller.h"
48 #include "chrome/browser/ui/find_bar/find_tab_helper.h"
49 #include "chrome/browser/ui/tabs/tab_menu_model.h"
50 #include "chrome/browser/ui/tabs/tab_strip_model.h"
51 #include "chrome/browser/ui/tabs/tab_strip_model_delegate.h"
52 #include "chrome/browser/ui/tabs/tab_utils.h"
53 #include "chrome/common/chrome_switches.h"
54 #include "chrome/common/net/url_fixer_upper.h"
55 #include "chrome/common/pref_names.h"
56 #include "components/web_modal/web_contents_modal_dialog_manager.h"
57 #include "content/public/browser/navigation_controller.h"
58 #include "content/public/browser/user_metrics.h"
59 #include "content/public/browser/web_contents.h"
60 #include "grit/generated_resources.h"
61 #include "grit/theme_resources.h"
62 #include "grit/ui_resources.h"
63 #include "skia/ext/skia_utils_mac.h"
64 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
65 #include "ui/base/cocoa/animation_utils.h"
66 #import "ui/base/cocoa/tracking_area.h"
67 #include "ui/base/l10n/l10n_util.h"
68 #include "ui/base/models/list_selection_model.h"
69 #include "ui/base/resource/resource_bundle.h"
70 #include "ui/base/theme_provider.h"
71 #include "ui/gfx/image/image.h"
72 #include "ui/gfx/mac/scoped_ns_disable_screen_updates.h"
74 using base::UserMetricsAction;
75 using content::OpenURLParams;
76 using content::Referrer;
77 using content::WebContents;
81 // A value to indicate tab layout should use the full available width of the
83 const CGFloat kUseFullAvailableWidth = -1.0;
85 // The amount by which tabs overlap.
86 // Needs to be <= the x position of the favicon within a tab. Else, every time
87 // the throbber is painted, the throbber's invalidation will also invalidate
88 // parts of the tab to the left, and two tabs's backgrounds need to be painted
89 // on each throbber frame instead of one.
90 const CGFloat kTabOverlap = 19.0;
92 // The amount by which mini tabs are separated from normal tabs.
93 const CGFloat kLastMiniTabSpacing = 2.0;
95 // The amount by which the new tab button is offset (from the tabs).
96 const CGFloat kNewTabButtonOffset = 8.0;
98 // Time (in seconds) in which tabs animate to their final position.
99 const NSTimeInterval kAnimationDuration = 0.125;
101 // Helper class for doing NSAnimationContext calls that takes a bool to disable
102 // all the work. Useful for code that wants to conditionally animate.
103 class ScopedNSAnimationContextGroup {
105 explicit ScopedNSAnimationContextGroup(bool animate)
106 : animate_(animate) {
108 [NSAnimationContext beginGrouping];
112 ~ScopedNSAnimationContextGroup() {
114 [NSAnimationContext endGrouping];
118 void SetCurrentContextDuration(NSTimeInterval duration) {
120 [[NSAnimationContext currentContext] gtm_setDuration:duration
121 eventMask:NSLeftMouseUpMask];
125 void SetCurrentContextShortestDuration() {
127 // The minimum representable time interval. This used to stop an
128 // in-progress animation as quickly as possible.
129 const NSTimeInterval kMinimumTimeInterval =
130 std::numeric_limits<NSTimeInterval>::min();
131 // Directly set the duration to be short, avoiding the Steve slowmotion
132 // ettect the gtm_setDuration: provides.
133 [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval];
139 DISALLOW_COPY_AND_ASSIGN(ScopedNSAnimationContextGroup);
142 // Creates an NSImage with size |size| and bitmap image representations for both
143 // 1x and 2x scale factors. |drawingHandler| is called once for every scale
144 // factor. This is similar to -[NSImage imageWithSize:flipped:drawingHandler:],
145 // but this function always evaluates drawingHandler eagerly, and it works on
147 NSImage* CreateImageWithSize(NSSize size,
148 void (^drawingHandler)(NSSize)) {
149 base::scoped_nsobject<NSImage> result([[NSImage alloc] initWithSize:size]);
150 [NSGraphicsContext saveGraphicsState];
151 for (ui::ScaleFactor scale_factor : ui::GetSupportedScaleFactors()) {
152 float scale = GetImageScale(scale_factor);
153 NSBitmapImageRep *bmpImageRep = [[[NSBitmapImageRep alloc]
154 initWithBitmapDataPlanes:NULL
155 pixelsWide:size.width * scale
156 pixelsHigh:size.height * scale
161 colorSpaceName:NSDeviceRGBColorSpace
163 bitsPerPixel:0] autorelease];
164 [bmpImageRep setSize:size];
165 [NSGraphicsContext setCurrentContext:
166 [NSGraphicsContext graphicsContextWithBitmapImageRep:bmpImageRep]];
167 drawingHandler(size);
168 [result addRepresentation:bmpImageRep];
170 [NSGraphicsContext restoreGraphicsState];
172 return result.release();
175 // Takes a normal bitmap and a mask image and returns an image the size of the
176 // mask that has pixels from |image| but alpha information from |mask|.
177 NSImage* ApplyMask(NSImage* image, NSImage* mask) {
178 return [CreateImageWithSize([mask size], ^(NSSize size) {
179 // Skip a few pixels from the top of the tab background gradient, because
180 // the new tab button is not drawn at the very top of the browser window.
181 const int kYOffset = 10;
182 CGFloat width = size.width;
183 CGFloat height = size.height;
185 // In some themes, the tab background image is narrower than the
186 // new tab button, so tile the background image.
188 // The floor() is to make sure images with odd widths don't draw to the
189 // same pixel twice on retina displays. (Using NSDrawThreePartImage()
190 // caused a startup perf regression, so that cannot be used.)
191 CGFloat tileWidth = floor(std::min(width, [image size].width));
193 [image drawAtPoint:NSMakePoint(x, 0)
194 fromRect:NSMakeRect(0,
195 [image size].height - height - kYOffset,
198 operation:NSCompositeCopy
203 [mask drawAtPoint:NSZeroPoint
204 fromRect:NSMakeRect(0, 0, width, height)
205 operation:NSCompositeDestinationIn
210 // Paints |overlay| on top of |ground|.
211 NSImage* Overlay(NSImage* ground, NSImage* overlay, CGFloat alpha) {
212 DCHECK_EQ([ground size].width, [overlay size].width);
213 DCHECK_EQ([ground size].height, [overlay size].height);
215 return [CreateImageWithSize([ground size], ^(NSSize size) {
216 CGFloat width = size.width;
217 CGFloat height = size.height;
218 [ground drawAtPoint:NSZeroPoint
219 fromRect:NSMakeRect(0, 0, width, height)
220 operation:NSCompositeCopy
222 [overlay drawAtPoint:NSZeroPoint
223 fromRect:NSMakeRect(0, 0, width, height)
224 operation:NSCompositeSourceOver
231 @interface TabStripController (Private)
232 - (void)addSubviewToPermanentList:(NSView*)aView;
233 - (void)regenerateSubviewList;
234 - (NSInteger)indexForContentsView:(NSView*)view;
235 - (NSImage*)iconImageForContents:(content::WebContents*)contents;
236 - (void)updateIconsForContents:(content::WebContents*)contents
237 atIndex:(NSInteger)modelIndex;
238 - (void)layoutTabsWithAnimation:(BOOL)animate
239 regenerateSubviews:(BOOL)doUpdate;
240 - (void)animationDidStop:(CAAnimation*)animation
241 forController:(TabController*)controller
242 finished:(BOOL)finished;
243 - (NSInteger)indexFromModelIndex:(NSInteger)index;
244 - (void)clickNewTabButton:(id)sender;
245 - (NSInteger)numberOfOpenTabs;
246 - (NSInteger)numberOfOpenMiniTabs;
247 - (NSInteger)numberOfOpenNonMiniTabs;
248 - (void)mouseMoved:(NSEvent*)event;
249 - (void)setTabTrackingAreasEnabled:(BOOL)enabled;
250 - (void)droppingURLsAt:(NSPoint)point
251 givesIndex:(NSInteger*)index
252 disposition:(WindowOpenDisposition*)disposition;
253 - (void)setNewTabButtonHoverState:(BOOL)showHover;
254 - (void)themeDidChangeNotification:(NSNotification*)notification;
255 - (void)setNewTabImages;
258 // A simple view class that prevents the Window Server from dragging the area
259 // behind tabs. Sometimes core animation confuses it. Unfortunately, it can also
260 // falsely pick up clicks during rapid tab closure, so we have to account for
262 @interface TabStripControllerDragBlockingView : NSView {
263 TabStripController* controller_; // weak; owns us
266 - (id)initWithFrame:(NSRect)frameRect
267 controller:(TabStripController*)controller;
269 // Runs a nested runloop to do window move tracking. Overriding
270 // -mouseDownCanMoveWindow with a dynamic result instead doesn't work:
271 // http://www.cocoabuilder.com/archive/cocoa/219261-conditional-mousedowncanmovewindow-for-nsview.html
272 // http://www.cocoabuilder.com/archive/cocoa/92973-brushed-metal-window-dragging.html
273 - (void)trackClickForWindowMove:(NSEvent*)event;
276 @implementation TabStripControllerDragBlockingView
277 - (BOOL)mouseDownCanMoveWindow {
281 - (void)drawRect:(NSRect)rect {
284 - (id)initWithFrame:(NSRect)frameRect
285 controller:(TabStripController*)controller {
286 if ((self = [super initWithFrame:frameRect])) {
287 controller_ = controller;
292 // In "rapid tab closure" mode (i.e., the user is clicking close tab buttons in
293 // rapid succession), the animations confuse Cocoa's hit testing (which appears
294 // to use cached results, among other tricks), so this view can somehow end up
295 // getting a mouse down event. Thus we do an explicit hit test during rapid tab
296 // closure, and if we find that we got a mouse down we shouldn't have, we send
297 // it off to the appropriate view.
298 - (void)mouseDown:(NSEvent*)event {
299 NSView* superview = [self superview];
300 NSPoint hitLocation =
301 [[superview superview] convertPoint:[event locationInWindow]
303 NSView* hitView = [superview hitTest:hitLocation];
305 if ([controller_ inRapidClosureMode]) {
306 if (hitView != self) {
307 [hitView mouseDown:event];
312 if (hitView == self) {
313 BrowserWindowController* windowController =
314 [BrowserWindowController browserWindowControllerForView:self];
315 if (![windowController isFullscreen]) {
316 [self trackClickForWindowMove:event];
320 [super mouseDown:event];
323 - (void)trackClickForWindowMove:(NSEvent*)event {
324 NSWindow* window = [self window];
325 NSPoint frameOrigin = [window frame].origin;
326 NSPoint lastEventLoc = [window convertBaseToScreen:[event locationInWindow]];
327 while ((event = [NSApp nextEventMatchingMask:
328 NSLeftMouseDownMask|NSLeftMouseDraggedMask|NSLeftMouseUpMask
329 untilDate:[NSDate distantFuture]
330 inMode:NSEventTrackingRunLoopMode
332 [event type] != NSLeftMouseUp) {
333 base::mac::ScopedNSAutoreleasePool pool;
335 NSPoint now = [window convertBaseToScreen:[event locationInWindow]];
336 frameOrigin.x += now.x - lastEventLoc.x;
337 frameOrigin.y += now.y - lastEventLoc.y;
338 [window setFrameOrigin:frameOrigin];
347 // A delegate, owned by the CAAnimation system, that is alerted when the
348 // animation to close a tab is completed. Calls back to the given tab strip
349 // to let it know that |controller_| is ready to be removed from the model.
350 // Since we only maintain weak references, the tab strip must call -invalidate:
351 // to prevent the use of dangling pointers.
352 @interface TabCloseAnimationDelegate : NSObject {
354 TabStripController* strip_; // weak; owns us indirectly
355 TabController* controller_; // weak
358 // Will tell |strip| when the animation for |controller|'s view has completed.
359 // These should not be nil, and will not be retained.
360 - (id)initWithTabStrip:(TabStripController*)strip
361 tabController:(TabController*)controller;
363 // Invalidates this object so that no further calls will be made to
364 // |strip_|. This should be called when |strip_| is released, to
365 // prevent attempts to call into the released object.
368 // CAAnimation delegate method
369 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished;
373 @implementation TabCloseAnimationDelegate
375 - (id)initWithTabStrip:(TabStripController*)strip
376 tabController:(TabController*)controller {
377 if ((self = [super init])) {
378 DCHECK(strip && controller);
380 controller_ = controller;
390 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
391 [strip_ animationDidStop:animation
392 forController:controller_
400 // In general, there is a one-to-one correspondence between TabControllers,
401 // TabViews, TabContentsControllers, and the WebContents in the
402 // TabStripModel. In the steady-state, the indices line up so an index coming
403 // from the model is directly mapped to the same index in the parallel arrays
404 // holding our views and controllers. This is also true when new tabs are
405 // created (even though there is a small period of animation) because the tab is
406 // present in the model while the TabView is animating into place. As a result,
407 // nothing special need be done to handle "new tab" animation.
409 // This all goes out the window with the "close tab" animation. The animation
410 // kicks off in |-tabDetachedWithContents:atIndex:| with the notification that
411 // the tab has been removed from the model. The simplest solution at this
412 // point would be to remove the views and controllers as well, however once
413 // the TabView is removed from the view list, the tab z-order code takes care of
414 // removing it from the tab strip and we'll get no animation. That means if
415 // there is to be any visible animation, the TabView needs to stay around until
416 // its animation is complete. In order to maintain consistency among the
417 // internal parallel arrays, this means all structures are kept around until
418 // the animation completes. At this point, though, the model and our internal
419 // structures are out of sync: the indices no longer line up. As a result,
420 // there is a concept of a "model index" which represents an index valid in
421 // the TabStripModel. During steady-state, the "model index" is just the same
422 // index as our parallel arrays (as above), but during tab close animations,
423 // it is different, offset by the number of tabs preceding the index which
424 // are undergoing tab closing animation. As a result, the caller needs to be
425 // careful to use the available conversion routines when accessing the internal
426 // parallel arrays (e.g., -indexFromModelIndex:). Care also needs to be taken
427 // during tab layout to ignore closing tabs in the total width calculations and
428 // in individual tab positioning (to avoid moving them right back to where they
431 // In order to prevent actions being taken on tabs which are closing, the tab
432 // itself gets marked as such so it no longer will send back its select action
433 // or allow itself to be dragged. In addition, drags on the tab strip as a
434 // whole are disabled while there are tabs closing.
436 @implementation TabStripController
438 @synthesize leftIndentForControls = leftIndentForControls_;
439 @synthesize rightIndentForControls = rightIndentForControls_;
441 - (id)initWithView:(TabStripView*)view
442 switchView:(NSView*)switchView
443 browser:(Browser*)browser
444 delegate:(id<TabStripControllerDelegate>)delegate {
445 DCHECK(view && switchView && browser && delegate);
446 if ((self = [super init])) {
447 tabStripView_.reset([view retain]);
448 [tabStripView_ setController:self];
449 switchView_ = switchView;
451 tabStripModel_ = browser_->tab_strip_model();
452 hoverTabSelector_.reset(new HoverTabSelector(tabStripModel_));
453 delegate_ = delegate;
454 bridge_.reset(new TabStripModelObserverBridge(tabStripModel_, self));
455 dragController_.reset(
456 [[TabStripDragController alloc] initWithTabStripController:self]);
457 tabContentsArray_.reset([[NSMutableArray alloc] init]);
458 tabArray_.reset([[NSMutableArray alloc] init]);
459 NSWindow* browserWindow = [view window];
461 // Important note: any non-tab subviews not added to |permanentSubviews_|
462 // (see |-addSubviewToPermanentList:|) will be wiped out.
463 permanentSubviews_.reset([[NSMutableArray alloc] init]);
465 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
466 defaultFavicon_.reset(
467 rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).CopyNSImage());
469 [self setLeftIndentForControls:[[self class] defaultLeftIndentForControls]];
470 [self setRightIndentForControls:0];
472 // Add this invisible view first so that it is ordered below other views.
473 dragBlockingView_.reset(
474 [[TabStripControllerDragBlockingView alloc] initWithFrame:NSZeroRect
476 [self addSubviewToPermanentList:dragBlockingView_];
478 newTabButton_ = [view getNewTabButton];
479 [newTabButton_ setWantsLayer:YES];
480 [self addSubviewToPermanentList:newTabButton_];
481 [newTabButton_ setTarget:self];
482 [newTabButton_ setAction:@selector(clickNewTabButton:)];
484 [self setNewTabImages];
485 newTabButtonShowingHoverImage_ = NO;
486 newTabTrackingArea_.reset(
487 [[CrTrackingArea alloc] initWithRect:[newTabButton_ bounds]
488 options:(NSTrackingMouseEnteredAndExited |
489 NSTrackingActiveAlways)
492 if (browserWindow) // Nil for Browsers without a tab strip (e.g. popups).
493 [newTabTrackingArea_ clearOwnerWhenWindowWillClose:browserWindow];
494 [newTabButton_ addTrackingArea:newTabTrackingArea_.get()];
495 targetFrames_.reset([[NSMutableDictionary alloc] init]);
497 newTabTargetFrame_ = NSZeroRect;
498 availableResizeWidth_ = kUseFullAvailableWidth;
500 closingControllers_.reset([[NSMutableSet alloc] init]);
502 // Install the permanent subviews.
503 [self regenerateSubviewList];
505 // Watch for notifications that the tab strip view has changed size so
506 // we can tell it to layout for the new size.
507 [[NSNotificationCenter defaultCenter]
509 selector:@selector(tabViewFrameChanged:)
510 name:NSViewFrameDidChangeNotification
511 object:tabStripView_];
513 [[NSNotificationCenter defaultCenter]
515 selector:@selector(themeDidChangeNotification:)
516 name:kBrowserThemeDidChangeNotification
519 trackingArea_.reset([[CrTrackingArea alloc]
520 initWithRect:NSZeroRect // Ignored by NSTrackingInVisibleRect
521 options:NSTrackingMouseEnteredAndExited |
522 NSTrackingMouseMoved |
523 NSTrackingActiveAlways |
524 NSTrackingInVisibleRect
527 if (browserWindow) // Nil for Browsers without a tab strip (e.g. popups).
528 [trackingArea_ clearOwnerWhenWindowWillClose:browserWindow];
529 [tabStripView_ addTrackingArea:trackingArea_.get()];
531 // Check to see if the mouse is currently in our bounds so we can
532 // enable the tracking areas. Otherwise we won't get hover states
533 // or tab gradients if we load the window up under the mouse.
534 NSPoint mouseLoc = [[view window] mouseLocationOutsideOfEventStream];
535 mouseLoc = [view convertPoint:mouseLoc fromView:nil];
536 if (NSPointInRect(mouseLoc, [view bounds])) {
537 [self setTabTrackingAreasEnabled:YES];
541 // Set accessibility descriptions. http://openradar.appspot.com/7496255
542 NSString* description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_NEWTAB);
543 [[newTabButton_ cell]
544 accessibilitySetOverrideValue:description
545 forAttribute:NSAccessibilityDescriptionAttribute];
547 // Controller may have been (re-)created by switching layout modes, which
548 // means the tab model is already fully formed with tabs. Need to walk the
549 // list and create the UI for each.
550 const int existingTabCount = tabStripModel_->count();
551 const content::WebContents* selection =
552 tabStripModel_->GetActiveWebContents();
553 for (int i = 0; i < existingTabCount; ++i) {
554 content::WebContents* currentContents =
555 tabStripModel_->GetWebContentsAt(i);
556 [self insertTabWithContents:currentContents
559 if (selection == currentContents) {
560 // Must manually force a selection since the model won't send
561 // selection messages in this scenario.
563 activateTabWithContents:currentContents
564 previousContents:NULL
566 reason:TabStripModelObserver::CHANGE_REASON_NONE];
569 // Don't lay out the tabs until after the controller has been fully
571 if (existingTabCount) {
572 [self performSelectorOnMainThread:@selector(layoutTabs)
581 [tabStripView_ setController:nil];
583 if (trackingArea_.get())
584 [tabStripView_ removeTrackingArea:trackingArea_.get()];
586 [newTabButton_ removeTrackingArea:newTabTrackingArea_.get()];
587 // Invalidate all closing animations so they don't call back to us after
589 for (TabController* controller in closingControllers_.get()) {
590 NSView* view = [controller view];
591 [[[view animationForKey:@"frameOrigin"] delegate] invalidate];
593 [[NSNotificationCenter defaultCenter] removeObserver:self];
594 [tabStripView_ removeAllToolTips];
598 + (CGFloat)defaultTabHeight {
602 + (CGFloat)defaultLeftIndentForControls {
603 // Default indentation leaves enough room so tabs don't overlap with the
608 // Finds the TabContentsController associated with the given index into the tab
609 // model and swaps out the sole child of the contentArea to display its
611 - (void)swapInTabAtIndex:(NSInteger)modelIndex {
612 DCHECK(modelIndex >= 0 && modelIndex < tabStripModel_->count());
613 NSInteger index = [self indexFromModelIndex:modelIndex];
614 TabContentsController* controller = [tabContentsArray_ objectAtIndex:index];
616 // Make sure we do not draw any transient arrangements of views.
617 gfx::ScopedNSDisableScreenUpdates ns_disabler;
618 // Make sure that any layers that move are not animated to their new
620 ScopedCAActionDisabler ca_disabler;
622 // Resize the new view to fit the window. Calling |view| may lazily
623 // instantiate the TabContentsController from the nib. Until we call
624 // |-ensureContentsVisible|, the controller doesn't install the RWHVMac into
625 // the view hierarchy. This is in order to avoid sending the renderer a
626 // spurious default size loaded from the nib during the call to |-view|.
627 NSView* newView = [controller view];
629 // Turns content autoresizing off, so removing and inserting views won't
630 // trigger unnecessary content relayout.
631 [controller ensureContentsSizeDoesNotChange];
633 // Remove the old view from the view hierarchy. We know there's only one
634 // child of |switchView_| because we're the one who put it there. There
635 // may not be any children in the case of a tab that's been closed, in
636 // which case there's no swapping going on.
637 NSArray* subviews = [switchView_ subviews];
638 if ([subviews count]) {
639 NSView* oldView = [subviews objectAtIndex:0];
640 // Set newView frame to the oldVew frame to prevent NSSplitView hosting
641 // sidebar and tab content from resizing sidebar's content view.
642 // ensureContentsVisible (see below) sets content size and autoresizing
644 [newView setFrame:[oldView frame]];
645 [switchView_ replaceSubview:oldView with:newView];
647 [newView setFrame:[switchView_ bounds]];
648 [switchView_ addSubview:newView];
651 // New content is in place, delegate should adjust itself accordingly.
652 [delegate_ onActivateTabWithContents:[controller webContents]];
654 // It also restores content autoresizing properties.
655 [controller ensureContentsVisible];
657 NSWindow* parentWindow = [switchView_ window];
658 ConstrainedWindowSheetController* sheetController =
659 [ConstrainedWindowSheetController
660 controllerForParentWindow:parentWindow];
661 [sheetController parentViewDidBecomeActive:newView];
664 // Create a new tab view and set its cell correctly so it draws the way we want
665 // it to. It will be sized and positioned by |-layoutTabs| so there's no need to
666 // set the frame here. This also creates the view as hidden, it will be
667 // shown during layout.
668 - (TabController*)newTab {
669 TabController* controller = [[[TabController alloc] init] autorelease];
670 [controller setTarget:self];
671 [controller setAction:@selector(selectTab:)];
672 [[controller view] setHidden:YES];
677 // (Private) Handles a click on the new tab button.
678 - (void)clickNewTabButton:(id)sender {
679 content::RecordAction(UserMetricsAction("NewTab_Button"));
680 UMA_HISTOGRAM_ENUMERATION("Tab.NewTab", TabStripModel::NEW_TAB_BUTTON,
681 TabStripModel::NEW_TAB_ENUM_COUNT);
682 tabStripModel_->delegate()->AddTabAt(GURL(), -1, true);
685 // (Private) Returns the number of open tabs in the tab strip. This is the
686 // number of TabControllers we know about (as there's a 1-to-1 mapping from
687 // these controllers to a tab) less the number of closing tabs.
688 - (NSInteger)numberOfOpenTabs {
689 return static_cast<NSInteger>(tabStripModel_->count());
692 // (Private) Returns the number of open, mini-tabs.
693 - (NSInteger)numberOfOpenMiniTabs {
694 // Ask the model for the number of mini tabs. Note that tabs which are in
695 // the process of closing (i.e., whose controllers are in
696 // |closingControllers_|) have already been removed from the model.
697 return tabStripModel_->IndexOfFirstNonMiniTab();
700 // (Private) Returns the number of open, non-mini tabs.
701 - (NSInteger)numberOfOpenNonMiniTabs {
702 NSInteger number = [self numberOfOpenTabs] - [self numberOfOpenMiniTabs];
703 DCHECK_GE(number, 0);
707 // Given an index into the tab model, returns the index into the tab controller
708 // or tab contents controller array accounting for tabs that are currently
709 // closing. For example, if there are two tabs in the process of closing before
710 // |index|, this returns |index| + 2. If there are no closing tabs, this will
712 - (NSInteger)indexFromModelIndex:(NSInteger)index {
718 for (TabController* controller in tabArray_.get()) {
719 if ([closingControllers_ containsObject:controller]) {
720 DCHECK([[controller tabView] isClosing]);
723 if (i == index) // No need to check anything after, it has no effect.
730 // Given an index into |tabArray_|, return the corresponding index into
731 // |tabStripModel_| or NSNotFound if the specified tab does not exist in
732 // the model (if it's closing, for example).
733 - (NSInteger)modelIndexFromIndex:(NSInteger)index {
734 NSInteger modelIndex = 0;
735 NSInteger arrayIndex = 0;
736 for (TabController* controller in tabArray_.get()) {
737 if (![closingControllers_ containsObject:controller]) {
738 if (arrayIndex == index)
741 } else if (arrayIndex == index) {
742 // Tab is closing - no model index.
750 // Returns the index of the subview |view|. Returns -1 if not present. Takes
751 // closing tabs into account such that this index will correctly match the tab
752 // model. If |view| is in the process of closing, returns -1, as closing tabs
753 // are no longer in the model.
754 - (NSInteger)modelIndexForTabView:(NSView*)view {
756 for (TabController* current in tabArray_.get()) {
757 // If |current| is closing, skip it.
758 if ([closingControllers_ containsObject:current])
760 else if ([current view] == view)
767 // Returns the index of the contents subview |view|. Returns -1 if not present.
768 // Takes closing tabs into account such that this index will correctly match the
769 // tab model. If |view| is in the process of closing, returns -1, as closing
770 // tabs are no longer in the model.
771 - (NSInteger)modelIndexForContentsView:(NSView*)view {
774 for (TabContentsController* current in tabContentsArray_.get()) {
775 // If the TabController corresponding to |current| is closing, skip it.
776 TabController* controller = [tabArray_ objectAtIndex:i];
777 if ([closingControllers_ containsObject:controller]) {
780 } else if ([current view] == view) {
789 - (NSArray*)selectedViews {
790 NSMutableArray* views = [NSMutableArray arrayWithCapacity:[tabArray_ count]];
791 for (TabController* tab in tabArray_.get()) {
793 [views addObject:[tab tabView]];
798 // Returns the view at the given index, using the array of TabControllers to
799 // get the associated view. Returns nil if out of range.
800 - (NSView*)viewAtIndex:(NSUInteger)index {
801 if (index >= [tabArray_ count])
803 return [[tabArray_ objectAtIndex:index] view];
806 - (NSUInteger)viewsCount {
807 return [tabArray_ count];
810 // Called when the user clicks a tab. Tell the model the selection has changed,
811 // which feeds back into us via a notification.
812 - (void)selectTab:(id)sender {
813 DCHECK([sender isKindOfClass:[NSView class]]);
814 int index = [self modelIndexForTabView:sender];
815 NSUInteger modifiers = [[NSApp currentEvent] modifierFlags];
816 if (tabStripModel_->ContainsIndex(index)) {
817 if (modifiers & NSCommandKeyMask && modifiers & NSShiftKeyMask) {
818 tabStripModel_->AddSelectionFromAnchorTo(index);
819 } else if (modifiers & NSShiftKeyMask) {
820 tabStripModel_->ExtendSelectionTo(index);
821 } else if (modifiers & NSCommandKeyMask) {
822 tabStripModel_->ToggleSelectionAt(index);
824 tabStripModel_->ActivateTabAt(index, true);
829 // Called when the user closes a tab. Asks the model to close the tab. |sender|
830 // is the TabView that is potentially going away.
831 - (void)closeTab:(id)sender {
832 DCHECK([sender isKindOfClass:[TabView class]]);
834 // Cancel any pending tab transition.
835 hoverTabSelector_->CancelTabTransition();
837 if ([hoveredTab_ isEqual:sender]) {
841 NSInteger index = [self modelIndexForTabView:sender];
842 if (!tabStripModel_->ContainsIndex(index))
845 content::RecordAction(UserMetricsAction("CloseTab_Mouse"));
846 const NSInteger numberOfOpenTabs = [self numberOfOpenTabs];
847 if (numberOfOpenTabs > 1) {
848 bool isClosingLastTab = index == numberOfOpenTabs - 1;
849 if (!isClosingLastTab) {
850 // Limit the width available for laying out tabs so that tabs are not
851 // resized until a later time (when the mouse leaves the tab strip).
852 // However, if the tab being closed is a pinned tab, break out of
853 // rapid-closure mode since the mouse is almost guaranteed not to be over
854 // the closebox of the adjacent tab (due to the difference in widths).
855 // TODO(pinkerton): re-visit when handling tab overflow.
856 // http://crbug.com/188
857 if (tabStripModel_->IsTabPinned(index)) {
858 availableResizeWidth_ = kUseFullAvailableWidth;
860 NSView* penultimateTab = [self viewAtIndex:numberOfOpenTabs - 2];
861 availableResizeWidth_ = NSMaxX([penultimateTab frame]);
864 // If the rightmost tab is closed, change the available width so that
865 // another tab's close button lands below the cursor (assuming the tabs
866 // are currently below their maximum width and can grow).
867 NSView* lastTab = [self viewAtIndex:numberOfOpenTabs - 1];
868 availableResizeWidth_ = NSMaxX([lastTab frame]);
870 tabStripModel_->CloseWebContentsAt(
872 TabStripModel::CLOSE_USER_GESTURE |
873 TabStripModel::CLOSE_CREATE_HISTORICAL_TAB);
875 // Use the standard window close if this is the last tab
876 // this prevents the tab from being removed from the model until after
877 // the window dissapears
878 [[tabStripView_ window] performClose:nil];
882 // Dispatch context menu commands for the given tab controller.
883 - (void)commandDispatch:(TabStripModel::ContextMenuCommand)command
884 forController:(TabController*)controller {
885 int index = [self modelIndexForTabView:[controller view]];
886 if (tabStripModel_->ContainsIndex(index))
887 tabStripModel_->ExecuteContextMenuCommand(index, command);
890 // Returns YES if the specificed command should be enabled for the given
892 - (BOOL)isCommandEnabled:(TabStripModel::ContextMenuCommand)command
893 forController:(TabController*)controller {
894 int index = [self modelIndexForTabView:[controller view]];
895 if (!tabStripModel_->ContainsIndex(index))
897 return tabStripModel_->IsContextMenuCommandEnabled(index, command) ? YES : NO;
900 // Returns a context menu model for a given controller. Caller owns the result.
901 - (ui::SimpleMenuModel*)contextMenuModelForController:(TabController*)controller
902 menuDelegate:(ui::SimpleMenuModel::Delegate*)delegate {
903 int index = [self modelIndexForTabView:[controller view]];
904 return new TabMenuModel(delegate, tabStripModel_, index);
907 // Returns a weak reference to the controller that manages dragging of tabs.
908 - (id<TabDraggingEventTarget>)dragController {
909 return dragController_.get();
912 - (void)insertPlaceholderForTab:(TabView*)tab frame:(NSRect)frame {
913 placeholderTab_ = tab;
914 placeholderFrame_ = frame;
915 [self layoutTabsWithAnimation:initialLayoutComplete_ regenerateSubviews:NO];
918 - (BOOL)isDragSessionActive {
919 return placeholderTab_ != nil;
922 - (BOOL)isTabFullyVisible:(TabView*)tab {
923 NSRect frame = [tab frame];
924 return NSMinX(frame) >= [self leftIndentForControls] &&
925 NSMaxX(frame) <= (NSMaxX([tabStripView_ frame]) -
926 [self rightIndentForControls]);
929 - (void)showNewTabButton:(BOOL)show {
930 forceNewTabButtonHidden_ = show ? NO : YES;
931 if (forceNewTabButtonHidden_)
932 [newTabButton_ setHidden:YES];
935 // Lay out all tabs in the order of their TabContentsControllers, which matches
936 // the ordering in the TabStripModel. This call isn't that expensive, though
937 // it is O(n) in the number of tabs. Tabs will animate to their new position
938 // if the window is visible and |animate| is YES.
939 // TODO(pinkerton): Note this doesn't do too well when the number of min-sized
940 // tabs would cause an overflow. http://crbug.com/188
941 - (void)layoutTabsWithAnimation:(BOOL)animate
942 regenerateSubviews:(BOOL)doUpdate {
943 DCHECK([NSThread isMainThread]);
944 if (![tabArray_ count])
947 const CGFloat kMaxTabWidth = [TabController maxTabWidth];
948 const CGFloat kMinTabWidth = [TabController minTabWidth];
949 const CGFloat kMinSelectedTabWidth = [TabController minSelectedTabWidth];
950 const CGFloat kMiniTabWidth = [TabController miniTabWidth];
951 const CGFloat kAppTabWidth = [TabController appTabWidth];
953 NSRect enclosingRect = NSZeroRect;
954 ScopedNSAnimationContextGroup mainAnimationGroup(animate);
955 mainAnimationGroup.SetCurrentContextDuration(kAnimationDuration);
957 // Update the current subviews and their z-order if requested.
959 [self regenerateSubviewList];
961 // Compute the base width of tabs given how much room we're allowed. Note that
962 // mini-tabs have a fixed width. We may not be able to use the entire width
963 // if the user is quickly closing tabs. This may be negative, but that's okay
964 // (taken care of by |MAX()| when calculating tab sizes).
965 CGFloat availableSpace = 0;
966 if ([self inRapidClosureMode]) {
967 availableSpace = availableResizeWidth_;
969 availableSpace = NSWidth([tabStripView_ frame]);
971 // Account for the width of the new tab button.
972 availableSpace -= NSWidth([newTabButton_ frame]) + kNewTabButtonOffset;
974 // Account for the right-side controls if not in rapid closure mode.
975 // (In rapid closure mode, the available width is set based on the
976 // position of the rightmost tab, not based on the width of the tab strip,
977 // so the right controls have already been accounted for.)
978 availableSpace -= [self rightIndentForControls];
981 // Need to leave room for the left-side controls even in rapid closure mode.
982 availableSpace -= [self leftIndentForControls];
984 // If there are any mini tabs, account for the extra spacing between the last
985 // mini tab and the first regular tab.
986 if ([self numberOfOpenMiniTabs])
987 availableSpace -= kLastMiniTabSpacing;
989 // This may be negative, but that's okay (taken care of by |MAX()| when
990 // calculating tab sizes). "mini" tabs in horizontal mode just get a special
991 // section, they don't change size.
992 CGFloat availableSpaceForNonMini = availableSpace;
993 availableSpaceForNonMini -=
994 [self numberOfOpenMiniTabs] * (kMiniTabWidth - kTabOverlap);
996 // Initialize |nonMiniTabWidth| in case there aren't any non-mini-tabs; this
997 // value shouldn't actually be used.
998 CGFloat nonMiniTabWidth = kMaxTabWidth;
999 CGFloat nonMiniTabWidthFraction = 0;
1000 const NSInteger numberOfOpenNonMiniTabs = [self numberOfOpenNonMiniTabs];
1001 if (numberOfOpenNonMiniTabs) {
1002 // Find the width of a non-mini-tab. This only applies to horizontal
1003 // mode. Add in the amount we "get back" from the tabs overlapping.
1004 availableSpaceForNonMini += (numberOfOpenNonMiniTabs - 1) * kTabOverlap;
1006 // Divide up the space between the non-mini-tabs.
1007 nonMiniTabWidth = availableSpaceForNonMini / numberOfOpenNonMiniTabs;
1009 // Clamp the width between the max and min.
1010 nonMiniTabWidth = MAX(MIN(nonMiniTabWidth, kMaxTabWidth), kMinTabWidth);
1012 // Separate integral and fractional parts.
1013 CGFloat integralPart = std::floor(nonMiniTabWidth);
1014 nonMiniTabWidthFraction = nonMiniTabWidth - integralPart;
1015 nonMiniTabWidth = integralPart;
1018 BOOL visible = [[tabStripView_ window] isVisible];
1020 CGFloat offset = [self leftIndentForControls];
1021 bool hasPlaceholderGap = false;
1022 // Whether or not the last tab processed by the loop was a mini tab.
1023 BOOL isLastTabMini = NO;
1024 CGFloat tabWidthAccumulatedFraction = 0;
1025 NSInteger laidOutNonMiniTabs = 0;
1027 // Remove all the tooltip rects on the tab strip so that we can re-apply
1028 // them to correspond with the new tab positions.
1029 [tabStripView_ removeAllToolTips];
1031 for (TabController* tab in tabArray_.get()) {
1032 // Ignore a tab that is going through a close animation.
1033 if ([closingControllers_ containsObject:tab])
1036 BOOL isPlaceholder = [[tab view] isEqual:placeholderTab_];
1037 NSRect tabFrame = [[tab view] frame];
1038 tabFrame.size.height = [[self class] defaultTabHeight];
1039 tabFrame.origin.y = 0;
1040 tabFrame.origin.x = offset;
1042 // If the tab is hidden, we consider it a new tab. We make it visible
1043 // and animate it in.
1044 BOOL newTab = [[tab view] isHidden];
1046 [[tab view] setHidden:NO];
1048 if (isPlaceholder) {
1049 // Move the current tab to the correct location instantly.
1050 // We need a duration or else it doesn't cancel an inflight animation.
1051 ScopedNSAnimationContextGroup localAnimationGroup(animate);
1052 localAnimationGroup.SetCurrentContextShortestDuration();
1053 tabFrame.origin.x = placeholderFrame_.origin.x;
1054 id target = animate ? [[tab view] animator] : [tab view];
1055 [target setFrame:tabFrame];
1057 // Store the frame by identifier to avoid redundant calls to animator.
1058 NSValue* identifier = [NSValue valueWithPointer:[tab view]];
1059 [targetFrames_ setObject:[NSValue valueWithRect:tabFrame]
1064 if (placeholderTab_ && !hasPlaceholderGap) {
1065 const CGFloat placeholderMin = NSMinX(placeholderFrame_);
1066 // If the left edge is to the left of the placeholder's left, but the
1067 // mid is to the right of it slide over to make space for it.
1068 if (NSMidX(tabFrame) > placeholderMin) {
1069 hasPlaceholderGap = true;
1070 offset += NSWidth(placeholderFrame_);
1071 offset -= kTabOverlap;
1072 tabFrame.origin.x = offset;
1076 // Set the width. Selected tabs are slightly wider when things get really
1077 // small and thus we enforce a different minimum width.
1078 BOOL isMini = [tab mini];
1080 tabFrame.size.width = [tab app] ? kAppTabWidth : kMiniTabWidth;
1082 // Tabs have non-integer widths. Assign the integer part to the tab, and
1083 // keep an accumulation of the fractional parts. When the fractional
1084 // accumulation gets to be more than one pixel, assign that to the current
1085 // tab being laid out. This is vaguely inspired by Bresenham's line
1087 tabFrame.size.width = nonMiniTabWidth;
1088 tabWidthAccumulatedFraction += nonMiniTabWidthFraction;
1090 if (tabWidthAccumulatedFraction >= 1.0) {
1091 ++tabFrame.size.width;
1092 --tabWidthAccumulatedFraction;
1095 // In case of rounding error, give any left over pixels to the last tab.
1096 if (laidOutNonMiniTabs == numberOfOpenNonMiniTabs - 1 &&
1097 tabWidthAccumulatedFraction > 0.5) {
1098 ++tabFrame.size.width;
1101 ++laidOutNonMiniTabs;
1105 tabFrame.size.width = MAX(tabFrame.size.width, kMinSelectedTabWidth);
1107 // If this is the first non-mini tab, then add a bit of spacing between this
1108 // and the last mini tab.
1109 if (!isMini && isLastTabMini) {
1110 offset += kLastMiniTabSpacing;
1111 tabFrame.origin.x = offset;
1113 isLastTabMini = isMini;
1115 // Animate a new tab in by putting it below the horizon unless told to put
1116 // it in a specific location (i.e., from a drop).
1117 if (newTab && visible && animate) {
1118 if (NSEqualRects(droppedTabFrame_, NSZeroRect)) {
1119 [[tab view] setFrame:NSOffsetRect(tabFrame, 0, -NSHeight(tabFrame))];
1121 [[tab view] setFrame:droppedTabFrame_];
1122 droppedTabFrame_ = NSZeroRect;
1126 // Check the frame by identifier to avoid redundant calls to animator.
1127 id frameTarget = visible && animate ? [[tab view] animator] : [tab view];
1128 NSValue* identifier = [NSValue valueWithPointer:[tab view]];
1129 NSValue* oldTargetValue = [targetFrames_ objectForKey:identifier];
1130 if (!oldTargetValue ||
1131 !NSEqualRects([oldTargetValue rectValue], tabFrame)) {
1132 [frameTarget setFrame:tabFrame];
1133 [targetFrames_ setObject:[NSValue valueWithRect:tabFrame]
1137 enclosingRect = NSUnionRect(tabFrame, enclosingRect);
1139 offset += NSWidth(tabFrame);
1140 offset -= kTabOverlap;
1142 // Create a rect which starts at the point where the tab overlap will end so
1143 // that as the mouse cursor crosses over the boundary it will get updated.
1144 // The inset is based on a multiplier of the height.
1145 float insetWidth = NSHeight(tabFrame) * [TabView insetMultiplier];
1146 // NSInsetRect will also expose the "insetWidth" at the right of the tab.
1147 NSRect tabToolTipRect = NSInsetRect(tabFrame, insetWidth, 0);
1148 [tabStripView_ addToolTipRect:tabToolTipRect owner:self userData:nil];
1150 // Also create two more rects in the remaining space so that the tooltip
1151 // is more likely to get updated crossing tabs.
1152 // These rects "cover" the right edge of the previous tab that was exposed
1153 // since the tabs overlap.
1154 tabToolTipRect = tabFrame;
1155 tabToolTipRect.size.width = insetWidth / 2.0;
1156 [tabStripView_ addToolTipRect:tabToolTipRect owner:self userData:nil];
1158 tabToolTipRect = NSOffsetRect(tabToolTipRect, insetWidth / 2.0, 0);
1159 [tabStripView_ addToolTipRect:tabToolTipRect owner:self userData:nil];
1162 // Hide the new tab button if we're explicitly told to. It may already
1163 // be hidden, doing it again doesn't hurt. Otherwise position it
1164 // appropriately, showing it if necessary.
1165 if (forceNewTabButtonHidden_) {
1166 [newTabButton_ setHidden:YES];
1168 NSRect newTabNewFrame = [newTabButton_ frame];
1169 // We've already ensured there's enough space for the new tab button
1170 // so we don't have to check it against the available space. We do need
1171 // to make sure we put it after any placeholder.
1172 CGFloat maxTabX = MAX(offset, NSMaxX(placeholderFrame_) - kTabOverlap);
1173 newTabNewFrame.origin = NSMakePoint(maxTabX + kNewTabButtonOffset, 0);
1174 if ([tabContentsArray_ count])
1175 [newTabButton_ setHidden:NO];
1177 if (!NSEqualRects(newTabTargetFrame_, newTabNewFrame)) {
1178 // Set the new tab button image correctly based on where the cursor is.
1179 NSWindow* window = [tabStripView_ window];
1180 NSPoint currentMouse = [window mouseLocationOutsideOfEventStream];
1181 currentMouse = [tabStripView_ convertPoint:currentMouse fromView:nil];
1183 BOOL shouldShowHover = [newTabButton_ pointIsOverButton:currentMouse];
1184 [self setNewTabButtonHoverState:shouldShowHover];
1186 // Move the new tab button into place. We want to animate the new tab
1187 // button if it's moving to the left (closing a tab), but not when it's
1188 // moving to the right (inserting a new tab). If moving right, we need
1189 // to use a very small duration to make sure we cancel any in-flight
1190 // animation to the left.
1191 if (visible && animate) {
1192 ScopedNSAnimationContextGroup localAnimationGroup(true);
1193 BOOL movingLeft = NSMinX(newTabNewFrame) < NSMinX(newTabTargetFrame_);
1195 localAnimationGroup.SetCurrentContextShortestDuration();
1197 [[newTabButton_ animator] setFrame:newTabNewFrame];
1198 newTabTargetFrame_ = newTabNewFrame;
1200 [newTabButton_ setFrame:newTabNewFrame];
1201 newTabTargetFrame_ = newTabNewFrame;
1206 [dragBlockingView_ setFrame:enclosingRect];
1208 // Add a catch-all tooltip rect which will handle any remaining tab strip
1209 // region not covered by tab-specific rects.
1210 [tabStripView_ addToolTipRect:enclosingRect owner:self userData:nil];
1212 // Mark that we've successfully completed layout of at least one tab.
1213 initialLayoutComplete_ = YES;
1216 // Return the current hovered tab's tooltip when requested by the tooltip
1218 - (NSString*) view:(NSView*)view
1219 stringForToolTip:(NSToolTipTag)tag
1220 point:(NSPoint)point
1221 userData:(void*)data {
1222 return [hoveredTab_ toolTipText];
1225 // When we're told to layout from the public API we usually want to animate,
1226 // except when it's the first time.
1227 - (void)layoutTabs {
1228 [self layoutTabsWithAnimation:initialLayoutComplete_ regenerateSubviews:YES];
1231 - (void)layoutTabsWithoutAnimation {
1232 [self layoutTabsWithAnimation:NO regenerateSubviews:YES];
1235 // Handles setting the title of the tab based on the given |contents|. Uses
1236 // a canned string if |contents| is NULL.
1237 - (void)setTabTitle:(TabController*)tab withContents:(WebContents*)contents {
1238 base::string16 title;
1240 title = contents->GetTitle();
1242 title = l10n_util::GetStringUTF16(IDS_BROWSER_WINDOW_MAC_TAB_UNTITLED);
1243 [tab setTitle:base::SysUTF16ToNSString(title)];
1245 const base::string16& toolTip = chrome::AssembleTabTooltipText(
1246 title, chrome::GetTabMediaStateForContents(contents));
1247 [tab setToolTip:base::SysUTF16ToNSString(toolTip)];
1250 // Called when a notification is received from the model to insert a new tab
1252 - (void)insertTabWithContents:(content::WebContents*)contents
1253 atIndex:(NSInteger)modelIndex
1254 inForeground:(bool)inForeground {
1256 DCHECK(modelIndex == TabStripModel::kNoTab ||
1257 tabStripModel_->ContainsIndex(modelIndex));
1259 // Cancel any pending tab transition.
1260 hoverTabSelector_->CancelTabTransition();
1262 // Take closing tabs into account.
1263 NSInteger index = [self indexFromModelIndex:modelIndex];
1265 // Make a new tab. Load the contents of this tab from the nib and associate
1266 // the new controller with |contents| so it can be looked up later.
1267 const BOOL autoEmbedFullscreen =
1268 implicit_cast<content::WebContentsDelegate*>(browser_)->
1269 EmbedsFullscreenWidget();
1270 base::scoped_nsobject<TabContentsController> contentsController(
1271 [[TabContentsController alloc] initWithContents:contents
1272 andAutoEmbedFullscreen:autoEmbedFullscreen]);
1273 [tabContentsArray_ insertObject:contentsController atIndex:index];
1275 // Make a new tab and add it to the strip. Keep track of its controller.
1276 TabController* newController = [self newTab];
1277 [newController setMini:tabStripModel_->IsMiniTab(modelIndex)];
1278 [newController setPinned:tabStripModel_->IsTabPinned(modelIndex)];
1279 [newController setApp:tabStripModel_->IsAppTab(modelIndex)];
1280 [newController setUrl:contents->GetURL()];
1281 [tabArray_ insertObject:newController atIndex:index];
1282 NSView* newView = [newController view];
1284 // Set the originating frame to just below the strip so that it animates
1285 // upwards as it's being initially layed out. Oddly, this works while doing
1286 // something similar in |-layoutTabs| confuses the window server.
1287 [newView setFrame:NSOffsetRect([newView frame],
1288 0, -[[self class] defaultTabHeight])];
1290 [self setTabTitle:newController withContents:contents];
1292 // If a tab is being inserted, we can again use the entire tab strip width
1294 availableResizeWidth_ = kUseFullAvailableWidth;
1296 // We don't need to call |-layoutTabs| if the tab will be in the foreground
1297 // because it will get called when the new tab is selected by the tab model.
1298 // Whenever |-layoutTabs| is called, it'll also add the new subview.
1299 if (!inForeground) {
1303 // During normal loading, we won't yet have a favicon and we'll get
1304 // subsequent state change notifications to show the throbber, but when we're
1305 // dragging a tab out into a new window, we have to put the tab's favicon
1306 // into the right state up front as we won't be told to do it from anywhere
1308 [self updateIconsForContents:contents atIndex:modelIndex];
1311 // Called before |contents| is deactivated.
1312 - (void)tabDeactivatedWithContents:(content::WebContents*)contents {
1313 contents->StoreFocus();
1316 // Called when a notification is received from the model to select a particular
1317 // tab. Swaps in the toolbar and content area associated with |newContents|.
1318 - (void)activateTabWithContents:(content::WebContents*)newContents
1319 previousContents:(content::WebContents*)oldContents
1320 atIndex:(NSInteger)modelIndex
1321 reason:(int)reason {
1322 // Take closing tabs into account.
1325 browser_->tab_strip_model()->GetIndexOfWebContents(oldContents);
1326 if (oldModelIndex != -1) { // When closing a tab, the old tab may be gone.
1327 NSInteger oldIndex = [self indexFromModelIndex:oldModelIndex];
1328 TabContentsController* oldController =
1329 [tabContentsArray_ objectAtIndex:oldIndex];
1330 [oldController willBecomeUnselectedTab];
1331 oldContents->WasHidden();
1335 NSUInteger activeIndex = [self indexFromModelIndex:modelIndex];
1337 [tabArray_ enumerateObjectsUsingBlock:^(TabController* current,
1340 [current setActive:index == activeIndex];
1343 // Tell the new tab contents it is about to become the selected tab. Here it
1344 // can do things like make sure the toolbar is up to date.
1345 TabContentsController* newController =
1346 [tabContentsArray_ objectAtIndex:activeIndex];
1347 [newController willBecomeSelectedTab];
1349 // Relayout for new tabs and to let the selected tab grow to be larger in
1350 // size than surrounding tabs if the user has many. This also raises the
1351 // selected tab to the top.
1354 // Swap in the contents for the new tab.
1355 [self swapInTabAtIndex:modelIndex];
1358 newContents->WasShown();
1359 newContents->RestoreFocus();
1363 - (void)tabSelectionChanged {
1364 // First get the vector of indices, which is allays sorted in ascending order.
1365 ui::ListSelectionModel::SelectedIndices selection(
1366 tabStripModel_->selection_model().selected_indices());
1367 // Iterate through all of the tabs, selecting each as necessary.
1368 ui::ListSelectionModel::SelectedIndices::iterator iter = selection.begin();
1370 for (TabController* current in tabArray_.get()) {
1371 BOOL selected = iter != selection.end() &&
1372 [self indexFromModelIndex:*iter] == i;
1373 [current setSelected:selected];
1380 - (void)tabReplacedWithContents:(content::WebContents*)newContents
1381 previousContents:(content::WebContents*)oldContents
1382 atIndex:(NSInteger)modelIndex {
1383 NSInteger index = [self indexFromModelIndex:modelIndex];
1384 TabContentsController* oldController =
1385 [tabContentsArray_ objectAtIndex:index];
1386 DCHECK_EQ(oldContents, [oldController webContents]);
1388 // Simply create a new TabContentsController for |newContents| and place it
1389 // into the array, replacing |oldContents|. An ActiveTabChanged notification
1390 // will follow, at which point we will install the new view.
1391 const BOOL autoEmbedFullscreen =
1392 implicit_cast<content::WebContentsDelegate*>(browser_)->
1393 EmbedsFullscreenWidget();
1394 base::scoped_nsobject<TabContentsController> newController(
1395 [[TabContentsController alloc] initWithContents:newContents
1396 andAutoEmbedFullscreen:autoEmbedFullscreen]);
1398 // Bye bye, |oldController|.
1399 [tabContentsArray_ replaceObjectAtIndex:index withObject:newController];
1401 // Fake a tab changed notification to force tab titles and favicons to update.
1402 [self tabChangedWithContents:newContents
1404 changeType:TabStripModelObserver::ALL];
1407 // Remove all knowledge about this tab and its associated controller, and remove
1408 // the view from the strip.
1409 - (void)removeTab:(TabController*)controller {
1410 // Cancel any pending tab transition.
1411 hoverTabSelector_->CancelTabTransition();
1413 NSUInteger index = [tabArray_ indexOfObject:controller];
1415 // Release the tab contents controller so those views get destroyed. This
1416 // will remove all the tab content Cocoa views from the hierarchy. A
1417 // subsequent "select tab" notification will follow from the model. To
1418 // tell us what to swap in in its absence.
1419 [tabContentsArray_ removeObjectAtIndex:index];
1421 // Remove the view from the tab strip.
1422 NSView* tab = [controller view];
1423 [tab removeFromSuperview];
1425 // Remove ourself as an observer.
1426 [[NSNotificationCenter defaultCenter]
1428 name:NSViewDidUpdateTrackingAreasNotification
1431 // Clear the tab controller's target.
1432 // TODO(viettrungluu): [crbug.com/23829] Find a better way to handle the tab
1433 // controller's target.
1434 [controller setTarget:nil];
1436 if ([hoveredTab_ isEqual:tab])
1439 NSValue* identifier = [NSValue valueWithPointer:tab];
1440 [targetFrames_ removeObjectForKey:identifier];
1442 // Once we're totally done with the tab, delete its controller
1443 [tabArray_ removeObjectAtIndex:index];
1446 // Called by the CAAnimation delegate when the tab completes the closing
1448 - (void)animationDidStop:(CAAnimation*)animation
1449 forController:(TabController*)controller
1450 finished:(BOOL)finished{
1451 [[animation delegate] invalidate];
1452 [closingControllers_ removeObject:controller];
1453 [self removeTab:controller];
1456 // Save off which TabController is closing and tell its view's animator
1457 // where to move the tab to. Registers a delegate to call back when the
1458 // animation is complete in order to remove the tab from the model.
1459 - (void)startClosingTabWithAnimation:(TabController*)closingTab {
1460 DCHECK([NSThread isMainThread]);
1462 // Cancel any pending tab transition.
1463 hoverTabSelector_->CancelTabTransition();
1465 // Save off the controller into the set of animating tabs. This alerts
1466 // the layout method to not do anything with it and allows us to correctly
1467 // calculate offsets when working with indices into the model.
1468 [closingControllers_ addObject:closingTab];
1470 // Mark the tab as closing. This prevents it from generating any drags or
1471 // selections while it's animating closed.
1472 [[closingTab tabView] setClosing:YES];
1474 // Register delegate (owned by the animation system).
1475 NSView* tabView = [closingTab view];
1476 CAAnimation* animation = [[tabView animationForKey:@"frameOrigin"] copy];
1477 [animation autorelease];
1478 base::scoped_nsobject<TabCloseAnimationDelegate> delegate(
1479 [[TabCloseAnimationDelegate alloc] initWithTabStrip:self
1480 tabController:closingTab]);
1481 [animation setDelegate:delegate.get()]; // Retains delegate.
1482 NSMutableDictionary* animationDictionary =
1483 [NSMutableDictionary dictionaryWithDictionary:[tabView animations]];
1484 [animationDictionary setObject:animation forKey:@"frameOrigin"];
1485 [tabView setAnimations:animationDictionary];
1487 // Periscope down! Animate the tab.
1488 NSRect newFrame = [tabView frame];
1489 newFrame = NSOffsetRect(newFrame, 0, -newFrame.size.height);
1490 ScopedNSAnimationContextGroup animationGroup(true);
1491 animationGroup.SetCurrentContextDuration(kAnimationDuration);
1492 [[tabView animator] setFrame:newFrame];
1495 // Called when a notification is received from the model that the given tab
1496 // has gone away. Start an animation then force a layout to put everything
1498 - (void)tabDetachedWithContents:(content::WebContents*)contents
1499 atIndex:(NSInteger)modelIndex {
1500 // Take closing tabs into account.
1501 NSInteger index = [self indexFromModelIndex:modelIndex];
1503 // Cancel any pending tab transition.
1504 hoverTabSelector_->CancelTabTransition();
1506 TabController* tab = [tabArray_ objectAtIndex:index];
1507 if (tabStripModel_->count() > 0) {
1508 [self startClosingTabWithAnimation:tab];
1511 // Don't remove the tab, as that makes the window look jarring without any
1512 // tabs. Instead, simply mark it as closing to prevent the tab from
1513 // generating any drags or selections.
1514 [[tab tabView] setClosing:YES];
1517 [delegate_ onTabDetachedWithContents:contents];
1520 // A helper routine for creating an NSImageView to hold the favicon or app icon
1522 - (NSImage*)iconImageForContents:(content::WebContents*)contents {
1523 extensions::TabHelper* extensions_tab_helper =
1524 extensions::TabHelper::FromWebContents(contents);
1525 BOOL isApp = extensions_tab_helper->is_app();
1526 NSImage* image = nil;
1527 // Favicons come from the renderer, and the renderer draws everything in the
1528 // system color space.
1529 CGColorSpaceRef colorSpace = base::mac::GetSystemColorSpace();
1531 SkBitmap* icon = extensions_tab_helper->GetExtensionAppIcon();
1533 image = gfx::SkBitmapToNSImageWithColorSpace(*icon, colorSpace);
1535 image = mac::FaviconForWebContents(contents);
1538 // Either we don't have a valid favicon or there was some issue converting it
1539 // from an SkBitmap. Either way, just show the default.
1541 image = defaultFavicon_.get();
1546 // Updates the current loading state, replacing the icon view with a favicon,
1547 // a throbber, the default icon, or nothing at all.
1548 - (void)updateIconsForContents:(content::WebContents*)contents
1549 atIndex:(NSInteger)modelIndex {
1553 static NSImage* throbberWaitingImage =
1554 ResourceBundle::GetSharedInstance().GetNativeImageNamed(
1555 IDR_THROBBER_WAITING).CopyNSImage();
1556 static NSImage* throbberLoadingImage =
1557 ResourceBundle::GetSharedInstance().GetNativeImageNamed(
1558 IDR_THROBBER).CopyNSImage();
1559 static NSImage* sadFaviconImage =
1560 ResourceBundle::GetSharedInstance().GetNativeImageNamed(
1561 IDR_SAD_FAVICON).CopyNSImage();
1563 // Take closing tabs into account.
1564 NSInteger index = [self indexFromModelIndex:modelIndex];
1565 TabController* tabController = [tabArray_ objectAtIndex:index];
1567 FaviconTabHelper* favicon_tab_helper =
1568 FaviconTabHelper::FromWebContents(contents);
1569 bool oldHasIcon = [tabController iconView] != nil;
1570 bool newHasIcon = favicon_tab_helper->ShouldDisplayFavicon() ||
1571 tabStripModel_->IsMiniTab(modelIndex); // Always show icon if mini.
1573 TabLoadingState oldState = [tabController loadingState];
1574 TabLoadingState newState = kTabDone;
1575 NSImage* throbberImage = nil;
1576 if (contents->IsCrashed()) {
1577 newState = kTabCrashed;
1579 } else if (contents->IsWaitingForResponse()) {
1580 newState = kTabWaiting;
1581 throbberImage = throbberWaitingImage;
1582 } else if (contents->IsLoading()) {
1583 newState = kTabLoading;
1584 throbberImage = throbberLoadingImage;
1587 if (oldState != newState)
1588 [tabController setLoadingState:newState];
1590 // While loading, this function is called repeatedly with the same state.
1591 // To avoid expensive unnecessary view manipulation, only make changes when
1592 // the state is actually changing. When loading is complete (kTabDone),
1593 // every call to this function is significant.
1594 if (newState == kTabDone || oldState != newState ||
1595 oldHasIcon != newHasIcon) {
1597 if (newState == kTabDone) {
1598 [tabController setIconImage:[self iconImageForContents:contents]];
1599 const TabMediaState mediaState =
1600 chrome::GetTabMediaStateForContents(contents);
1601 // Create MediaIndicatorView upon first use.
1602 if (mediaState != TAB_MEDIA_STATE_NONE &&
1603 ![tabController mediaIndicatorView]) {
1604 MediaIndicatorView* const mediaIndicatorView =
1605 [[[MediaIndicatorView alloc] init] autorelease];
1606 [tabController setMediaIndicatorView:mediaIndicatorView];
1608 [[tabController mediaIndicatorView] updateIndicator:mediaState];
1609 } else if (newState == kTabCrashed) {
1610 [tabController setIconImage:sadFaviconImage withToastAnimation:YES];
1611 [[tabController mediaIndicatorView]
1612 updateIndicator:TAB_MEDIA_STATE_NONE];
1614 [tabController setIconImage:throbberImage];
1617 [tabController setIconImage:nil];
1622 // Called when a notification is received from the model that the given tab
1623 // has been updated. |loading| will be YES when we only want to update the
1624 // throbber state, not anything else about the (partially) loading tab.
1625 - (void)tabChangedWithContents:(content::WebContents*)contents
1626 atIndex:(NSInteger)modelIndex
1627 changeType:(TabStripModelObserver::TabChangeType)change {
1628 // Take closing tabs into account.
1629 NSInteger index = [self indexFromModelIndex:modelIndex];
1631 if (modelIndex == tabStripModel_->active_index())
1632 [delegate_ onTabChanged:change withContents:contents];
1634 if (change == TabStripModelObserver::TITLE_NOT_LOADING) {
1635 // TODO(sky): make this work.
1636 // We'll receive another notification of the change asynchronously.
1640 TabController* tabController = [tabArray_ objectAtIndex:index];
1642 if (change != TabStripModelObserver::LOADING_ONLY)
1643 [self setTabTitle:tabController withContents:contents];
1645 [self updateIconsForContents:contents atIndex:modelIndex];
1647 TabContentsController* updatedController =
1648 [tabContentsArray_ objectAtIndex:index];
1649 [updatedController tabDidChange:contents];
1652 // Called when a tab is moved (usually by drag&drop). Keep our parallel arrays
1653 // in sync with the tab strip model. It can also be pinned/unpinned
1654 // simultaneously, so we need to take care of that.
1655 - (void)tabMovedWithContents:(content::WebContents*)contents
1656 fromIndex:(NSInteger)modelFrom
1657 toIndex:(NSInteger)modelTo {
1658 // Take closing tabs into account.
1659 NSInteger from = [self indexFromModelIndex:modelFrom];
1660 NSInteger to = [self indexFromModelIndex:modelTo];
1662 // Cancel any pending tab transition.
1663 hoverTabSelector_->CancelTabTransition();
1665 base::scoped_nsobject<TabContentsController> movedTabContentsController(
1666 [[tabContentsArray_ objectAtIndex:from] retain]);
1667 [tabContentsArray_ removeObjectAtIndex:from];
1668 [tabContentsArray_ insertObject:movedTabContentsController.get()
1670 base::scoped_nsobject<TabController> movedTabController(
1671 [[tabArray_ objectAtIndex:from] retain]);
1672 DCHECK([movedTabController isKindOfClass:[TabController class]]);
1673 [tabArray_ removeObjectAtIndex:from];
1674 [tabArray_ insertObject:movedTabController.get() atIndex:to];
1676 // The tab moved, which means that the mini-tab state may have changed.
1677 if (tabStripModel_->IsMiniTab(modelTo) != [movedTabController mini])
1678 [self tabMiniStateChangedWithContents:contents atIndex:modelTo];
1683 // Called when a tab is pinned or unpinned without moving.
1684 - (void)tabMiniStateChangedWithContents:(content::WebContents*)contents
1685 atIndex:(NSInteger)modelIndex {
1686 // Take closing tabs into account.
1687 NSInteger index = [self indexFromModelIndex:modelIndex];
1689 TabController* tabController = [tabArray_ objectAtIndex:index];
1690 DCHECK([tabController isKindOfClass:[TabController class]]);
1692 // Don't do anything if the change was already picked up by the move event.
1693 if (tabStripModel_->IsMiniTab(modelIndex) == [tabController mini])
1696 [tabController setMini:tabStripModel_->IsMiniTab(modelIndex)];
1697 [tabController setPinned:tabStripModel_->IsTabPinned(modelIndex)];
1698 [tabController setApp:tabStripModel_->IsAppTab(modelIndex)];
1699 [tabController setUrl:contents->GetURL()];
1700 [self updateIconsForContents:contents atIndex:modelIndex];
1701 // If the tab is being restored and it's pinned, the mini state is set after
1702 // the tab has already been rendered, so re-layout the tabstrip. In all other
1703 // cases, the state is set before the tab is rendered so this isn't needed.
1707 - (void)setFrame:(NSRect)frame ofTabView:(NSView*)view {
1708 NSValue* identifier = [NSValue valueWithPointer:view];
1709 [targetFrames_ setObject:[NSValue valueWithRect:frame]
1711 [view setFrame:frame];
1714 - (TabStripModel*)tabStripModel {
1715 return tabStripModel_;
1718 - (NSArray*)tabViews {
1719 NSMutableArray* views = [NSMutableArray arrayWithCapacity:[tabArray_ count]];
1720 for (TabController* tab in tabArray_.get()) {
1721 [views addObject:[tab tabView]];
1726 - (NSView*)activeTabView {
1727 int activeIndex = tabStripModel_->active_index();
1728 // Take closing tabs into account. They can't ever be selected.
1729 activeIndex = [self indexFromModelIndex:activeIndex];
1730 return [self viewAtIndex:activeIndex];
1733 - (int)indexOfPlaceholder {
1734 // Use |tabArray_| here instead of the tab strip count in order to get the
1735 // correct index when there are closing tabs to the left of the placeholder.
1736 const int count = [tabArray_ count];
1738 // No placeholder, return the end of the strip.
1739 if (placeholderTab_ == nil)
1742 double placeholderX = placeholderFrame_.origin.x;
1745 while (index < count) {
1746 // Ignore closing tabs for simplicity. The only drawback of this is that
1747 // if the placeholder is placed right before one or several contiguous
1748 // currently closing tabs, the associated TabController will start at the
1749 // end of the closing tabs.
1750 if ([closingControllers_ containsObject:[tabArray_ objectAtIndex:index]]) {
1754 NSView* curr = [self viewAtIndex:index];
1755 // The placeholder tab works by changing the frame of the tab being dragged
1756 // to be the bounds of the placeholder, so we need to skip it while we're
1757 // iterating, otherwise we'll end up off by one. Note This only effects
1758 // dragging to the right, not to the left.
1759 if (curr == placeholderTab_) {
1763 if (placeholderX <= NSMinX([curr frame]))
1771 // Move the given tab at index |from| in this window to the location of the
1772 // current placeholder.
1773 - (void)moveTabFromIndex:(NSInteger)from {
1774 int toIndex = [self indexOfPlaceholder];
1775 // Cancel any pending tab transition.
1776 hoverTabSelector_->CancelTabTransition();
1777 tabStripModel_->MoveWebContentsAt(from, toIndex, true);
1780 // Drop a given WebContents at the location of the current placeholder.
1781 // If there is no placeholder, it will go at the end. Used when dragging from
1782 // another window when we don't have access to the WebContents as part of our
1783 // strip. |frame| is in the coordinate system of the tab strip view and
1784 // represents where the user dropped the new tab so it can be animated into its
1785 // correct location when the tab is added to the model. If the tab was pinned in
1786 // its previous window, setting |pinned| to YES will propagate that state to the
1787 // new window. Mini-tabs are either app or pinned tabs; the app state is stored
1788 // by the |contents|, but the |pinned| state is the caller's responsibility.
1789 - (void)dropWebContents:(WebContents*)contents
1790 atIndex:(int)modelIndex
1791 withFrame:(NSRect)frame
1792 asPinnedTab:(BOOL)pinned
1793 activate:(BOOL)activate {
1794 // Mark that the new tab being created should start at |frame|. It will be
1795 // reset as soon as the tab has been positioned.
1796 droppedTabFrame_ = frame;
1798 // Insert it into this tab strip. We want it in the foreground and to not
1799 // inherit the current tab's group.
1800 tabStripModel_->InsertWebContentsAt(
1803 (activate ? TabStripModel::ADD_ACTIVE : TabStripModel::ADD_NONE) |
1804 (pinned ? TabStripModel::ADD_PINNED : TabStripModel::ADD_NONE));
1807 // Called when the tab strip view changes size. As we only registered for
1808 // changes on our view, we know it's only for our view. Layout w/out
1809 // animations since they are blocked by the resize nested runloop. We need
1810 // the views to adjust immediately. Neither the tabs nor their z-order are
1811 // changed, so we don't need to update the subviews.
1812 - (void)tabViewFrameChanged:(NSNotification*)info {
1813 [self layoutTabsWithAnimation:NO regenerateSubviews:NO];
1816 // Called when the tracking areas for any given tab are updated. This allows
1817 // the individual tabs to update their hover states correctly.
1818 // Only generates the event if the cursor is in the tab strip.
1819 - (void)tabUpdateTracking:(NSNotification*)notification {
1820 DCHECK([[notification object] isKindOfClass:[TabView class]]);
1821 DCHECK(mouseInside_);
1822 NSWindow* window = [tabStripView_ window];
1823 NSPoint location = [window mouseLocationOutsideOfEventStream];
1824 if (NSPointInRect(location, [tabStripView_ frame])) {
1825 NSEvent* mouseEvent = [NSEvent mouseEventWithType:NSMouseMoved
1829 windowNumber:[window windowNumber]
1834 [self mouseMoved:mouseEvent];
1838 - (BOOL)inRapidClosureMode {
1839 return availableResizeWidth_ != kUseFullAvailableWidth;
1842 // Disable tab dragging when there are any pending animations.
1843 - (BOOL)tabDraggingAllowed {
1844 return [closingControllers_ count] == 0;
1847 - (void)mouseMoved:(NSEvent*)event {
1848 // Use hit test to figure out what view we are hovering over.
1849 NSView* targetView = [tabStripView_ hitTest:[event locationInWindow]];
1851 // Set the new tab button hover state iff the mouse is over the button.
1852 BOOL shouldShowHoverImage = [targetView isKindOfClass:[NewTabButton class]];
1853 [self setNewTabButtonHoverState:shouldShowHoverImage];
1855 TabView* tabView = (TabView*)targetView;
1856 if (![tabView isKindOfClass:[TabView class]]) {
1857 if ([[tabView superview] isKindOfClass:[TabView class]]) {
1858 tabView = (TabView*)[targetView superview];
1864 if (hoveredTab_ != tabView) {
1865 [hoveredTab_ mouseExited:nil]; // We don't pass event because moved events
1866 [tabView mouseEntered:nil]; // don't have valid tracking areas
1867 hoveredTab_ = tabView;
1869 [hoveredTab_ mouseMoved:event];
1873 - (void)mouseEntered:(NSEvent*)event {
1874 NSTrackingArea* area = [event trackingArea];
1875 if ([area isEqual:trackingArea_]) {
1877 [self setTabTrackingAreasEnabled:YES];
1878 [self mouseMoved:event];
1882 // Called when the tracking area is in effect which means we're tracking to
1883 // see if the user leaves the tab strip with their mouse. When they do,
1884 // reset layout to use all available width.
1885 - (void)mouseExited:(NSEvent*)event {
1886 NSTrackingArea* area = [event trackingArea];
1887 if ([area isEqual:trackingArea_]) {
1889 [self setTabTrackingAreasEnabled:NO];
1890 availableResizeWidth_ = kUseFullAvailableWidth;
1891 [hoveredTab_ mouseExited:event];
1894 } else if ([area isEqual:newTabTrackingArea_]) {
1895 // If the mouse is moved quickly enough, it is possible for the mouse to
1896 // leave the tabstrip without sending any mouseMoved: messages at all.
1897 // Since this would result in the new tab button incorrectly staying in the
1898 // hover state, disable the hover image on every mouse exit.
1899 [self setNewTabButtonHoverState:NO];
1903 // Enable/Disable the tracking areas for the tabs. They are only enabled
1904 // when the mouse is in the tabstrip.
1905 - (void)setTabTrackingAreasEnabled:(BOOL)enabled {
1906 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
1907 for (TabController* controller in tabArray_.get()) {
1908 TabView* tabView = [controller tabView];
1910 // Set self up to observe tabs so hover states will be correct.
1911 [defaultCenter addObserver:self
1912 selector:@selector(tabUpdateTracking:)
1913 name:NSViewDidUpdateTrackingAreasNotification
1916 [defaultCenter removeObserver:self
1917 name:NSViewDidUpdateTrackingAreasNotification
1920 [tabView setTrackingEnabled:enabled];
1924 // Sets the new tab button's image based on the current hover state. Does
1925 // nothing if the hover state is already correct.
1926 - (void)setNewTabButtonHoverState:(BOOL)shouldShowHover {
1927 if (shouldShowHover && !newTabButtonShowingHoverImage_) {
1928 newTabButtonShowingHoverImage_ = YES;
1929 [[newTabButton_ cell] setIsMouseInside:YES];
1930 } else if (!shouldShowHover && newTabButtonShowingHoverImage_) {
1931 newTabButtonShowingHoverImage_ = NO;
1932 [[newTabButton_ cell] setIsMouseInside:NO];
1936 // Adds the given subview to (the end of) the list of permanent subviews
1937 // (specified from bottom up). These subviews will always be below the
1938 // transitory subviews (tabs). |-regenerateSubviewList| must be called to
1939 // effectuate the addition.
1940 - (void)addSubviewToPermanentList:(NSView*)aView {
1942 [permanentSubviews_ addObject:aView];
1945 // Update the subviews, keeping the permanent ones (or, more correctly, putting
1946 // in the ones listed in permanentSubviews_), and putting in the current tabs in
1947 // the correct z-order. Any current subviews which is neither in the permanent
1948 // list nor a (current) tab will be removed. So if you add such a subview, you
1949 // should call |-addSubviewToPermanentList:| (or better yet, call that and then
1950 // |-regenerateSubviewList| to actually add it).
1951 - (void)regenerateSubviewList {
1952 // Remove self as an observer from all the old tabs before a new set of
1953 // potentially different tabs is put in place.
1954 [self setTabTrackingAreasEnabled:NO];
1956 // Subviews to put in (in bottom-to-top order), beginning with the permanent
1958 NSMutableArray* subviews = [NSMutableArray arrayWithArray:permanentSubviews_];
1960 NSView* activeTabView = nil;
1961 // Go through tabs in reverse order, since |subviews| is bottom-to-top.
1962 for (TabController* tab in [tabArray_ reverseObjectEnumerator]) {
1963 NSView* tabView = [tab view];
1965 DCHECK(!activeTabView);
1966 activeTabView = tabView;
1968 [subviews addObject:tabView];
1971 if (activeTabView) {
1972 [subviews addObject:activeTabView];
1974 WithNoAnimation noAnimation;
1975 [tabStripView_ setSubviews:subviews];
1976 [self setTabTrackingAreasEnabled:mouseInside_];
1979 // Get the index and disposition for a potential URL(s) drop given a point (in
1980 // the |TabStripView|'s coordinates). It considers only the x-coordinate of the
1981 // given point. If it's in the "middle" of a tab, it drops on that tab. If it's
1982 // to the left, it inserts to the left, and similarly for the right.
1983 - (void)droppingURLsAt:(NSPoint)point
1984 givesIndex:(NSInteger*)index
1985 disposition:(WindowOpenDisposition*)disposition {
1986 // Proportion of the tab which is considered the "middle" (and causes things
1987 // to drop on that tab).
1988 const double kMiddleProportion = 0.5;
1989 const double kLRProportion = (1.0 - kMiddleProportion) / 2.0;
1991 DCHECK(index && disposition);
1993 for (TabController* tab in tabArray_.get()) {
1994 NSView* view = [tab view];
1995 DCHECK([view isKindOfClass:[TabView class]]);
1997 // Recall that |-[NSView frame]| is in its superview's coordinates, so a
1998 // |TabView|'s frame is in the coordinates of the |TabStripView| (which
1999 // matches the coordinate system of |point|).
2000 NSRect frame = [view frame];
2002 // Modify the frame to make it "unoverlapped".
2003 frame.origin.x += kTabOverlap / 2.0;
2004 frame.size.width -= kTabOverlap;
2005 if (frame.size.width < 1.0)
2006 frame.size.width = 1.0; // try to avoid complete failure
2008 // Drop in a new tab to the left of tab |i|?
2009 if (point.x < (frame.origin.x + kLRProportion * frame.size.width)) {
2011 *disposition = NEW_FOREGROUND_TAB;
2016 if (point.x <= (frame.origin.x +
2017 (1.0 - kLRProportion) * frame.size.width)) {
2019 *disposition = CURRENT_TAB;
2023 // (Dropping in a new tab to the right of tab |i| will be taken care of in
2024 // the next iteration.)
2028 // If we've made it here, we want to append a new tab to the end.
2030 *disposition = NEW_FOREGROUND_TAB;
2033 - (void)openURL:(GURL*)url inView:(NSView*)view at:(NSPoint)point {
2034 // Get the index and disposition.
2036 WindowOpenDisposition disposition;
2037 [self droppingURLsAt:point
2039 disposition:&disposition];
2041 // Either insert a new tab or open in a current tab.
2042 switch (disposition) {
2043 case NEW_FOREGROUND_TAB: {
2044 content::RecordAction(UserMetricsAction("Tab_DropURLBetweenTabs"));
2045 chrome::NavigateParams params(browser_, *url,
2046 content::PAGE_TRANSITION_TYPED);
2047 params.disposition = disposition;
2048 params.tabstrip_index = index;
2049 params.tabstrip_add_types =
2050 TabStripModel::ADD_ACTIVE | TabStripModel::ADD_FORCE_INDEX;
2051 chrome::Navigate(¶ms);
2055 content::RecordAction(UserMetricsAction("Tab_DropURLOnTab"));
2056 OpenURLParams params(
2057 *url, Referrer(), CURRENT_TAB, content::PAGE_TRANSITION_TYPED, false);
2058 tabStripModel_->GetWebContentsAt(index)->OpenURL(params);
2059 tabStripModel_->ActivateTabAt(index, true);
2067 // (URLDropTargetController protocol)
2068 - (void)dropURLs:(NSArray*)urls inView:(NSView*)view at:(NSPoint)point {
2069 DCHECK_EQ(view, tabStripView_.get());
2071 if ([urls count] < 1) {
2076 //TODO(viettrungluu): dropping multiple URLs.
2077 if ([urls count] > 1)
2080 // Get the first URL and fix it up.
2081 GURL url(GURL(URLFixerUpper::FixupURL(
2082 base::SysNSStringToUTF8([urls objectAtIndex:0]), std::string())));
2084 [self openURL:&url inView:view at:point];
2087 // (URLDropTargetController protocol)
2088 - (void)dropText:(NSString*)text inView:(NSView*)view at:(NSPoint)point {
2089 DCHECK_EQ(view, tabStripView_.get());
2091 // If the input is plain text, classify the input and make the URL.
2092 AutocompleteMatch match;
2093 AutocompleteClassifierFactory::GetForProfile(browser_->profile())->Classify(
2094 base::SysNSStringToUTF16(text), false, false, AutocompleteInput::BLANK,
2096 GURL url(match.destination_url);
2098 [self openURL:&url inView:view at:point];
2101 // (URLDropTargetController protocol)
2102 - (void)indicateDropURLsInView:(NSView*)view at:(NSPoint)point {
2103 DCHECK_EQ(view, tabStripView_.get());
2105 // The minimum y-coordinate at which one should consider place the arrow.
2106 const CGFloat arrowBaseY = 25;
2109 WindowOpenDisposition disposition;
2110 [self droppingURLsAt:point
2112 disposition:&disposition];
2114 NSPoint arrowPos = NSMakePoint(0, arrowBaseY);
2116 // Append a tab at the end.
2117 DCHECK(disposition == NEW_FOREGROUND_TAB);
2118 NSInteger lastIndex = [tabArray_ count] - 1;
2119 NSRect overRect = [[[tabArray_ objectAtIndex:lastIndex] view] frame];
2120 arrowPos.x = overRect.origin.x + overRect.size.width - kTabOverlap / 2.0;
2122 NSRect overRect = [[[tabArray_ objectAtIndex:index] view] frame];
2123 switch (disposition) {
2124 case NEW_FOREGROUND_TAB:
2125 // Insert tab (to the left of the given tab).
2126 arrowPos.x = overRect.origin.x + kTabOverlap / 2.0;
2129 // Overwrite the given tab.
2130 arrowPos.x = overRect.origin.x + overRect.size.width / 2.0;
2137 [tabStripView_ setDropArrowPosition:arrowPos];
2138 [tabStripView_ setDropArrowShown:YES];
2139 [tabStripView_ setNeedsDisplay:YES];
2141 // Perform a delayed tab transition if hovering directly over a tab.
2142 if (index != -1 && disposition == CURRENT_TAB) {
2143 NSInteger modelIndex = [self modelIndexFromIndex:index];
2144 // Only start the transition if it has a valid model index (i.e. it's not
2145 // in the middle of closing).
2146 if (modelIndex != NSNotFound) {
2147 hoverTabSelector_->StartTabTransition(modelIndex);
2151 // If a tab transition was not started, cancel the pending one.
2152 hoverTabSelector_->CancelTabTransition();
2155 // (URLDropTargetController protocol)
2156 - (void)hideDropURLsIndicatorInView:(NSView*)view {
2157 DCHECK_EQ(view, tabStripView_.get());
2159 // Cancel any pending tab transition.
2160 hoverTabSelector_->CancelTabTransition();
2162 if ([tabStripView_ dropArrowShown]) {
2163 [tabStripView_ setDropArrowShown:NO];
2164 [tabStripView_ setNeedsDisplay:YES];
2168 // (URLDropTargetController protocol)
2169 - (BOOL)isUnsupportedDropData:(id<NSDraggingInfo>)info {
2170 return drag_util::IsUnsupportedDropData(browser_->profile(), info);
2173 - (TabContentsController*)activeTabContentsController {
2174 int modelIndex = tabStripModel_->active_index();
2177 NSInteger index = [self indexFromModelIndex:modelIndex];
2179 index >= (NSInteger)[tabContentsArray_ count])
2181 return [tabContentsArray_ objectAtIndex:index];
2184 - (void)themeDidChangeNotification:(NSNotification*)notification {
2185 [self setNewTabImages];
2188 - (void)setNewTabImages {
2189 ThemeService *theme =
2190 static_cast<ThemeService*>([[tabStripView_ window] themeProvider]);
2194 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
2195 NSImage* mask = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON_MASK).ToNSImage();
2196 NSImage* normal = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON).ToNSImage();
2197 NSImage* hover = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON_H).ToNSImage();
2198 NSImage* pressed = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON_P).ToNSImage();
2200 NSImage* foreground = ApplyMask(
2201 theme->GetNSImageNamed(IDR_THEME_TAB_BACKGROUND), mask);
2203 [[newTabButton_ cell] setImage:Overlay(foreground, normal, 1.0)
2204 forButtonState:image_button_cell::kDefaultState];
2205 [[newTabButton_ cell] setImage:Overlay(foreground, hover, 1.0)
2206 forButtonState:image_button_cell::kHoverState];
2207 [[newTabButton_ cell] setImage:Overlay(foreground, pressed, 1.0)
2208 forButtonState:image_button_cell::kPressedState];
2210 // IDR_THEME_TAB_BACKGROUND_INACTIVE is only used with the default theme.
2211 if (theme->UsingDefaultTheme()) {
2212 const CGFloat alpha = tabs::kImageNoFocusAlpha;
2213 NSImage* background = ApplyMask(
2214 theme->GetNSImageNamed(IDR_THEME_TAB_BACKGROUND_INACTIVE), mask);
2215 [[newTabButton_ cell] setImage:Overlay(background, normal, alpha)
2216 forButtonState:image_button_cell::kDefaultStateBackground];
2217 [[newTabButton_ cell] setImage:Overlay(background, hover, alpha)
2218 forButtonState:image_button_cell::kHoverStateBackground];
2220 [[newTabButton_ cell] setImage:nil
2221 forButtonState:image_button_cell::kDefaultStateBackground];
2222 [[newTabButton_ cell] setImage:nil
2223 forButtonState:image_button_cell::kHoverStateBackground];
2229 NSView* GetSheetParentViewForWebContents(WebContents* web_contents) {
2230 // View hierarchy of the contents view:
2231 // NSView -- switchView, same for all tabs
2232 // +- NSView -- TabContentsController's view
2233 // +- TabContentsViewCocoa
2235 // Changing it? Do not forget to modify
2236 // -[TabStripController swapInTabAtIndex:] too.
2237 return [web_contents->GetNativeView() superview];