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_factory.h"
21 #include "chrome/browser/extensions/tab_helper.h"
22 #include "chrome/browser/favicon/favicon_utils.h"
23 #include "chrome/browser/profiles/profile.h"
24 #include "chrome/browser/profiles/profile_manager.h"
25 #include "chrome/browser/themes/theme_service.h"
26 #include "chrome/browser/ui/browser.h"
27 #include "chrome/browser/ui/browser_navigator.h"
28 #include "chrome/browser/ui/browser_tabstrip.h"
29 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
30 #include "chrome/browser/ui/cocoa/drag_util.h"
31 #import "chrome/browser/ui/cocoa/image_button_cell.h"
32 #import "chrome/browser/ui/cocoa/new_tab_button.h"
33 #import "chrome/browser/ui/cocoa/tab_contents/favicon_util_mac.h"
34 #import "chrome/browser/ui/cocoa/tab_contents/tab_contents_controller.h"
35 #import "chrome/browser/ui/cocoa/tabs/media_indicator_button_cocoa.h"
36 #import "chrome/browser/ui/cocoa/tabs/tab_controller.h"
37 #import "chrome/browser/ui/cocoa/tabs/tab_strip_drag_controller.h"
38 #import "chrome/browser/ui/cocoa/tabs/tab_strip_model_observer_bridge.h"
39 #import "chrome/browser/ui/cocoa/tabs/tab_strip_view.h"
40 #import "chrome/browser/ui/cocoa/tabs/tab_view.h"
41 #include "chrome/browser/ui/find_bar/find_bar.h"
42 #include "chrome/browser/ui/find_bar/find_bar_controller.h"
43 #include "chrome/browser/ui/find_bar/find_tab_helper.h"
44 #include "chrome/browser/ui/tabs/tab_menu_model.h"
45 #include "chrome/browser/ui/tabs/tab_strip_model.h"
46 #include "chrome/browser/ui/tabs/tab_strip_model_delegate.h"
47 #include "chrome/browser/ui/tabs/tab_utils.h"
48 #include "chrome/common/chrome_switches.h"
49 #include "chrome/grit/generated_resources.h"
50 #include "components/metrics/proto/omnibox_event.pb.h"
51 #include "components/omnibox/browser/autocomplete_classifier.h"
52 #include "components/omnibox/browser/autocomplete_match.h"
53 #include "components/url_formatter/url_fixer.h"
54 #include "components/web_modal/web_contents_modal_dialog_manager.h"
55 #include "content/public/browser/navigation_controller.h"
56 #include "content/public/browser/user_metrics.h"
57 #include "content/public/browser/web_contents.h"
58 #include "grit/theme_resources.h"
59 #include "skia/ext/skia_utils_mac.h"
60 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
61 #include "ui/base/cocoa/animation_utils.h"
62 #import "ui/base/cocoa/tracking_area.h"
63 #include "ui/base/l10n/l10n_util.h"
64 #include "ui/base/models/list_selection_model.h"
65 #include "ui/base/resource/resource_bundle.h"
66 #include "ui/base/theme_provider.h"
67 #include "ui/gfx/image/image.h"
68 #include "ui/gfx/mac/scoped_cocoa_disable_screen_updates.h"
69 #include "ui/resources/grit/ui_resources.h"
71 using base::UserMetricsAction;
72 using content::OpenURLParams;
73 using content::Referrer;
74 using content::WebContents;
78 // A value to indicate tab layout should use the full available width of the
80 const CGFloat kUseFullAvailableWidth = -1.0;
82 // The amount by which tabs overlap.
83 // Needs to be <= the x position of the favicon within a tab. Else, every time
84 // the throbber is painted, the throbber's invalidation will also invalidate
85 // parts of the tab to the left, and two tabs's backgrounds need to be painted
86 // on each throbber frame instead of one.
87 const CGFloat kTabOverlap = 19.0;
89 // The amount by which pinned tabs are separated from normal tabs.
90 const CGFloat kLastPinnedTabSpacing = 2.0;
92 // The amount by which the new tab button is offset (from the tabs).
93 const CGFloat kNewTabButtonOffset = 8.0;
95 // Time (in seconds) in which tabs animate to their final position.
96 const NSTimeInterval kAnimationDuration = 0.125;
98 // Helper class for doing NSAnimationContext calls that takes a bool to disable
99 // all the work. Useful for code that wants to conditionally animate.
100 class ScopedNSAnimationContextGroup {
102 explicit ScopedNSAnimationContextGroup(bool animate)
103 : animate_(animate) {
105 [NSAnimationContext beginGrouping];
109 ~ScopedNSAnimationContextGroup() {
111 [NSAnimationContext endGrouping];
115 void SetCurrentContextDuration(NSTimeInterval duration) {
117 [[NSAnimationContext currentContext] gtm_setDuration:duration
118 eventMask:NSLeftMouseUpMask];
122 void SetCurrentContextShortestDuration() {
124 // The minimum representable time interval. This used to stop an
125 // in-progress animation as quickly as possible.
126 const NSTimeInterval kMinimumTimeInterval =
127 std::numeric_limits<NSTimeInterval>::min();
128 // Directly set the duration to be short, avoiding the Steve slowmotion
129 // ettect the gtm_setDuration: provides.
130 [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval];
136 DISALLOW_COPY_AND_ASSIGN(ScopedNSAnimationContextGroup);
139 // Creates an NSImage with size |size| and bitmap image representations for both
140 // 1x and 2x scale factors. |drawingHandler| is called once for every scale
141 // factor. This is similar to -[NSImage imageWithSize:flipped:drawingHandler:],
142 // but this function always evaluates drawingHandler eagerly, and it works on
144 NSImage* CreateImageWithSize(NSSize size,
145 void (^drawingHandler)(NSSize)) {
146 base::scoped_nsobject<NSImage> result([[NSImage alloc] initWithSize:size]);
147 [NSGraphicsContext saveGraphicsState];
148 for (ui::ScaleFactor scale_factor : ui::GetSupportedScaleFactors()) {
149 float scale = GetScaleForScaleFactor(scale_factor);
150 NSBitmapImageRep *bmpImageRep = [[[NSBitmapImageRep alloc]
151 initWithBitmapDataPlanes:NULL
152 pixelsWide:size.width * scale
153 pixelsHigh:size.height * scale
158 colorSpaceName:NSDeviceRGBColorSpace
160 bitsPerPixel:0] autorelease];
161 [bmpImageRep setSize:size];
162 [NSGraphicsContext setCurrentContext:
163 [NSGraphicsContext graphicsContextWithBitmapImageRep:bmpImageRep]];
164 drawingHandler(size);
165 [result addRepresentation:bmpImageRep];
167 [NSGraphicsContext restoreGraphicsState];
169 return result.release();
172 // Takes a normal bitmap and a mask image and returns an image the size of the
173 // mask that has pixels from |image| but alpha information from |mask|.
174 NSImage* ApplyMask(NSImage* image, NSImage* mask) {
175 return [CreateImageWithSize([mask size], ^(NSSize size) {
176 // Skip a few pixels from the top of the tab background gradient, because
177 // the new tab button is not drawn at the very top of the browser window.
178 const int kYOffset = 10;
179 CGFloat width = size.width;
180 CGFloat height = size.height;
182 // In some themes, the tab background image is narrower than the
183 // new tab button, so tile the background image.
185 // The floor() is to make sure images with odd widths don't draw to the
186 // same pixel twice on retina displays. (Using NSDrawThreePartImage()
187 // caused a startup perf regression, so that cannot be used.)
188 CGFloat tileWidth = floor(std::min(width, [image size].width));
190 [image drawAtPoint:NSMakePoint(x, 0)
191 fromRect:NSMakeRect(0,
192 [image size].height - height - kYOffset,
195 operation:NSCompositeCopy
200 [mask drawAtPoint:NSZeroPoint
201 fromRect:NSMakeRect(0, 0, width, height)
202 operation:NSCompositeDestinationIn
207 // Paints |overlay| on top of |ground|.
208 NSImage* Overlay(NSImage* ground, NSImage* overlay, CGFloat alpha) {
209 DCHECK_EQ([ground size].width, [overlay size].width);
210 DCHECK_EQ([ground size].height, [overlay size].height);
212 return [CreateImageWithSize([ground size], ^(NSSize size) {
213 CGFloat width = size.width;
214 CGFloat height = size.height;
215 [ground drawAtPoint:NSZeroPoint
216 fromRect:NSMakeRect(0, 0, width, height)
217 operation:NSCompositeCopy
219 [overlay drawAtPoint:NSZeroPoint
220 fromRect:NSMakeRect(0, 0, width, height)
221 operation:NSCompositeSourceOver
228 @interface TabStripController (Private)
229 - (void)addSubviewToPermanentList:(NSView*)aView;
230 - (void)regenerateSubviewList;
231 - (NSInteger)indexForContentsView:(NSView*)view;
232 - (NSImage*)iconImageForContents:(content::WebContents*)contents;
233 - (void)updateIconsForContents:(content::WebContents*)contents
234 atIndex:(NSInteger)modelIndex;
235 - (void)layoutTabsWithAnimation:(BOOL)animate
236 regenerateSubviews:(BOOL)doUpdate;
237 - (void)animationDidStop:(CAAnimation*)animation
238 forController:(TabController*)controller
239 finished:(BOOL)finished;
240 - (NSInteger)indexFromModelIndex:(NSInteger)index;
241 - (void)clickNewTabButton:(id)sender;
242 - (NSInteger)numberOfOpenTabs;
243 - (NSInteger)numberOfOpenPinnedTabs;
244 - (NSInteger)numberOfOpenNonPinnedTabs;
245 - (void)mouseMoved:(NSEvent*)event;
246 - (void)setTabTrackingAreasEnabled:(BOOL)enabled;
247 - (void)droppingURLsAt:(NSPoint)point
248 givesIndex:(NSInteger*)index
249 disposition:(WindowOpenDisposition*)disposition;
250 - (void)setNewTabButtonHoverState:(BOOL)showHover;
251 - (void)themeDidChangeNotification:(NSNotification*)notification;
252 - (void)setNewTabImages;
255 // A simple view class that prevents the Window Server from dragging the area
256 // behind tabs. Sometimes core animation confuses it. Unfortunately, it can also
257 // falsely pick up clicks during rapid tab closure, so we have to account for
259 @interface TabStripControllerDragBlockingView : NSView {
260 TabStripController* controller_; // weak; owns us
263 - (id)initWithFrame:(NSRect)frameRect
264 controller:(TabStripController*)controller;
266 // Runs a nested runloop to do window move tracking. Overriding
267 // -mouseDownCanMoveWindow with a dynamic result instead doesn't work:
268 // http://www.cocoabuilder.com/archive/cocoa/219261-conditional-mousedowncanmovewindow-for-nsview.html
269 // http://www.cocoabuilder.com/archive/cocoa/92973-brushed-metal-window-dragging.html
270 - (void)trackClickForWindowMove:(NSEvent*)event;
273 @implementation TabStripControllerDragBlockingView
274 - (BOOL)mouseDownCanMoveWindow {
278 - (id)initWithFrame:(NSRect)frameRect
279 controller:(TabStripController*)controller {
280 if ((self = [super initWithFrame:frameRect])) {
281 controller_ = controller;
286 // In "rapid tab closure" mode (i.e., the user is clicking close tab buttons in
287 // rapid succession), the animations confuse Cocoa's hit testing (which appears
288 // to use cached results, among other tricks), so this view can somehow end up
289 // getting a mouse down event. Thus we do an explicit hit test during rapid tab
290 // closure, and if we find that we got a mouse down we shouldn't have, we send
291 // it off to the appropriate view.
292 - (void)mouseDown:(NSEvent*)event {
293 NSView* superview = [self superview];
294 NSPoint hitLocation =
295 [[superview superview] convertPoint:[event locationInWindow]
297 NSView* hitView = [superview hitTest:hitLocation];
299 if ([controller_ inRapidClosureMode]) {
300 if (hitView != self) {
301 [hitView mouseDown:event];
306 if (hitView == self) {
307 BrowserWindowController* windowController =
308 [BrowserWindowController browserWindowControllerForView:self];
309 if (![windowController isInAnyFullscreenMode]) {
310 [self trackClickForWindowMove:event];
314 [super mouseDown:event];
317 - (void)trackClickForWindowMove:(NSEvent*)event {
318 NSWindow* window = [self window];
319 NSPoint frameOrigin = [window frame].origin;
320 NSPoint lastEventLoc = [window convertBaseToScreen:[event locationInWindow]];
321 while ((event = [NSApp nextEventMatchingMask:
322 NSLeftMouseDownMask|NSLeftMouseDraggedMask|NSLeftMouseUpMask
323 untilDate:[NSDate distantFuture]
324 inMode:NSEventTrackingRunLoopMode
326 [event type] != NSLeftMouseUp) {
327 base::mac::ScopedNSAutoreleasePool pool;
329 NSPoint now = [window convertBaseToScreen:[event locationInWindow]];
330 frameOrigin.x += now.x - lastEventLoc.x;
331 frameOrigin.y += now.y - lastEventLoc.y;
332 [window setFrameOrigin:frameOrigin];
341 // A delegate, owned by the CAAnimation system, that is alerted when the
342 // animation to close a tab is completed. Calls back to the given tab strip
343 // to let it know that |controller_| is ready to be removed from the model.
344 // Since we only maintain weak references, the tab strip must call -invalidate:
345 // to prevent the use of dangling pointers.
346 @interface TabCloseAnimationDelegate : NSObject {
348 TabStripController* strip_; // weak; owns us indirectly
349 TabController* controller_; // weak
352 // Will tell |strip| when the animation for |controller|'s view has completed.
353 // These should not be nil, and will not be retained.
354 - (id)initWithTabStrip:(TabStripController*)strip
355 tabController:(TabController*)controller;
357 // Invalidates this object so that no further calls will be made to
358 // |strip_|. This should be called when |strip_| is released, to
359 // prevent attempts to call into the released object.
362 // CAAnimation delegate method
363 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished;
367 @implementation TabCloseAnimationDelegate
369 - (id)initWithTabStrip:(TabStripController*)strip
370 tabController:(TabController*)controller {
371 if ((self = [super init])) {
372 DCHECK(strip && controller);
374 controller_ = controller;
384 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
385 [strip_ animationDidStop:animation
386 forController:controller_
394 // In general, there is a one-to-one correspondence between TabControllers,
395 // TabViews, TabContentsControllers, and the WebContents in the
396 // TabStripModel. In the steady-state, the indices line up so an index coming
397 // from the model is directly mapped to the same index in the parallel arrays
398 // holding our views and controllers. This is also true when new tabs are
399 // created (even though there is a small period of animation) because the tab is
400 // present in the model while the TabView is animating into place. As a result,
401 // nothing special need be done to handle "new tab" animation.
403 // This all goes out the window with the "close tab" animation. The animation
404 // kicks off in |-tabDetachedWithContents:atIndex:| with the notification that
405 // the tab has been removed from the model. The simplest solution at this
406 // point would be to remove the views and controllers as well, however once
407 // the TabView is removed from the view list, the tab z-order code takes care of
408 // removing it from the tab strip and we'll get no animation. That means if
409 // there is to be any visible animation, the TabView needs to stay around until
410 // its animation is complete. In order to maintain consistency among the
411 // internal parallel arrays, this means all structures are kept around until
412 // the animation completes. At this point, though, the model and our internal
413 // structures are out of sync: the indices no longer line up. As a result,
414 // there is a concept of a "model index" which represents an index valid in
415 // the TabStripModel. During steady-state, the "model index" is just the same
416 // index as our parallel arrays (as above), but during tab close animations,
417 // it is different, offset by the number of tabs preceding the index which
418 // are undergoing tab closing animation. As a result, the caller needs to be
419 // careful to use the available conversion routines when accessing the internal
420 // parallel arrays (e.g., -indexFromModelIndex:). Care also needs to be taken
421 // during tab layout to ignore closing tabs in the total width calculations and
422 // in individual tab positioning (to avoid moving them right back to where they
425 // In order to prevent actions being taken on tabs which are closing, the tab
426 // itself gets marked as such so it no longer will send back its select action
427 // or allow itself to be dragged. In addition, drags on the tab strip as a
428 // whole are disabled while there are tabs closing.
430 @implementation TabStripController
432 @synthesize leftIndentForControls = leftIndentForControls_;
433 @synthesize rightIndentForControls = rightIndentForControls_;
435 - (id)initWithView:(TabStripView*)view
436 switchView:(NSView*)switchView
437 browser:(Browser*)browser
438 delegate:(id<TabStripControllerDelegate>)delegate {
439 DCHECK(view && switchView && browser && delegate);
440 if ((self = [super init])) {
441 tabStripView_.reset([view retain]);
442 [tabStripView_ setController:self];
443 switchView_ = switchView;
445 tabStripModel_ = browser_->tab_strip_model();
446 hoverTabSelector_.reset(new HoverTabSelector(tabStripModel_));
447 delegate_ = delegate;
448 bridge_.reset(new TabStripModelObserverBridge(tabStripModel_, self));
449 dragController_.reset(
450 [[TabStripDragController alloc] initWithTabStripController:self]);
451 tabContentsArray_.reset([[NSMutableArray alloc] init]);
452 tabArray_.reset([[NSMutableArray alloc] init]);
453 NSWindow* browserWindow = [view window];
455 // Important note: any non-tab subviews not added to |permanentSubviews_|
456 // (see |-addSubviewToPermanentList:|) will be wiped out.
457 permanentSubviews_.reset([[NSMutableArray alloc] init]);
459 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
460 defaultFavicon_.reset(
461 rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).CopyNSImage());
463 [self setLeftIndentForControls:[[self class] defaultLeftIndentForControls]];
464 [self setRightIndentForControls:0];
466 // Add this invisible view first so that it is ordered below other views.
467 dragBlockingView_.reset(
468 [[TabStripControllerDragBlockingView alloc] initWithFrame:NSZeroRect
470 [self addSubviewToPermanentList:dragBlockingView_];
472 newTabButton_ = [view getNewTabButton];
473 [newTabButton_ setWantsLayer:YES];
474 [self addSubviewToPermanentList:newTabButton_];
475 [newTabButton_ setTarget:self];
476 [newTabButton_ setAction:@selector(clickNewTabButton:)];
478 [self setNewTabImages];
479 newTabButtonShowingHoverImage_ = NO;
480 newTabTrackingArea_.reset(
481 [[CrTrackingArea alloc] initWithRect:[newTabButton_ bounds]
482 options:(NSTrackingMouseEnteredAndExited |
483 NSTrackingActiveAlways)
486 if (browserWindow) // Nil for Browsers without a tab strip (e.g. popups).
487 [newTabTrackingArea_ clearOwnerWhenWindowWillClose:browserWindow];
488 [newTabButton_ addTrackingArea:newTabTrackingArea_.get()];
489 targetFrames_.reset([[NSMutableDictionary alloc] init]);
491 newTabTargetFrame_ = NSZeroRect;
492 availableResizeWidth_ = kUseFullAvailableWidth;
494 closingControllers_.reset([[NSMutableSet alloc] init]);
496 // Install the permanent subviews.
497 [self regenerateSubviewList];
499 // Watch for notifications that the tab strip view has changed size so
500 // we can tell it to layout for the new size.
501 [[NSNotificationCenter defaultCenter]
503 selector:@selector(tabViewFrameChanged:)
504 name:NSViewFrameDidChangeNotification
505 object:tabStripView_];
507 [[NSNotificationCenter defaultCenter]
509 selector:@selector(themeDidChangeNotification:)
510 name:kBrowserThemeDidChangeNotification
513 trackingArea_.reset([[CrTrackingArea alloc]
514 initWithRect:NSZeroRect // Ignored by NSTrackingInVisibleRect
515 options:NSTrackingMouseEnteredAndExited |
516 NSTrackingMouseMoved |
517 NSTrackingActiveAlways |
518 NSTrackingInVisibleRect
521 if (browserWindow) // Nil for Browsers without a tab strip (e.g. popups).
522 [trackingArea_ clearOwnerWhenWindowWillClose:browserWindow];
523 [tabStripView_ addTrackingArea:trackingArea_.get()];
525 // Check to see if the mouse is currently in our bounds so we can
526 // enable the tracking areas. Otherwise we won't get hover states
527 // or tab gradients if we load the window up under the mouse.
528 NSPoint mouseLoc = [[view window] mouseLocationOutsideOfEventStream];
529 mouseLoc = [view convertPoint:mouseLoc fromView:nil];
530 if (NSPointInRect(mouseLoc, [view bounds])) {
531 [self setTabTrackingAreasEnabled:YES];
535 // Set accessibility descriptions. http://openradar.appspot.com/7496255
536 NSString* description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_NEWTAB);
537 [[newTabButton_ cell]
538 accessibilitySetOverrideValue:description
539 forAttribute:NSAccessibilityDescriptionAttribute];
541 // Controller may have been (re-)created by switching layout modes, which
542 // means the tab model is already fully formed with tabs. Need to walk the
543 // list and create the UI for each.
544 const int existingTabCount = tabStripModel_->count();
545 const content::WebContents* selection =
546 tabStripModel_->GetActiveWebContents();
547 for (int i = 0; i < existingTabCount; ++i) {
548 content::WebContents* currentContents =
549 tabStripModel_->GetWebContentsAt(i);
550 [self insertTabWithContents:currentContents
553 if (selection == currentContents) {
554 // Must manually force a selection since the model won't send
555 // selection messages in this scenario.
557 activateTabWithContents:currentContents
558 previousContents:NULL
560 reason:TabStripModelObserver::CHANGE_REASON_NONE];
563 // Don't lay out the tabs until after the controller has been fully
565 if (existingTabCount) {
566 [self performSelectorOnMainThread:@selector(layoutTabs)
575 [self browserWillBeDestroyed];
579 - (void)browserWillBeDestroyed {
580 [tabStripView_ setController:nil];
582 if (trackingArea_.get())
583 [tabStripView_ removeTrackingArea:trackingArea_.get()];
585 [newTabButton_ removeTrackingArea:newTabTrackingArea_.get()];
586 // Invalidate all closing animations so they don't call back to us after
588 for (TabController* controller in closingControllers_.get()) {
589 NSView* view = [controller view];
590 [[[view animationForKey:@"frameOrigin"] delegate] invalidate];
592 [[NSNotificationCenter defaultCenter] removeObserver:self];
597 + (CGFloat)defaultTabHeight {
598 return [TabController defaultTabHeight];
601 + (CGFloat)defaultLeftIndentForControls {
602 // Default indentation leaves enough room so tabs don't overlap with the
607 // Finds the TabContentsController associated with the given index into the tab
608 // model and swaps out the sole child of the contentArea to display its
610 - (void)swapInTabAtIndex:(NSInteger)modelIndex {
611 DCHECK(modelIndex >= 0 && modelIndex < tabStripModel_->count());
612 NSInteger index = [self indexFromModelIndex:modelIndex];
613 TabContentsController* controller = [tabContentsArray_ objectAtIndex:index];
615 // Make sure we do not draw any transient arrangements of views.
616 gfx::ScopedCocoaDisableScreenUpdates cocoa_disabler;
617 // Make sure that any layers that move are not animated to their new
619 ScopedCAActionDisabler ca_disabler;
621 // Resize the new view to fit the window. Calling |view| may lazily
622 // instantiate the TabContentsController from the nib. Until we call
623 // |-ensureContentsVisible|, the controller doesn't install the RWHVMac into
624 // the view hierarchy. This is in order to avoid sending the renderer a
625 // spurious default size loaded from the nib during the call to |-view|.
626 NSView* newView = [controller view];
628 // Turns content autoresizing off, so removing and inserting views won't
629 // trigger unnecessary content relayout.
630 [controller ensureContentsSizeDoesNotChange];
632 // Remove the old view from the view hierarchy. We know there's only one
633 // child of |switchView_| because we're the one who put it there. There
634 // may not be any children in the case of a tab that's been closed, in
635 // which case there's no swapping going on.
636 NSArray* subviews = [switchView_ subviews];
637 if ([subviews count]) {
638 NSView* oldView = [subviews objectAtIndex:0];
639 // Set newView frame to the oldVew frame to prevent NSSplitView hosting
640 // sidebar and tab content from resizing sidebar's content view.
641 // ensureContentsVisible (see below) sets content size and autoresizing
643 [newView setFrame:[oldView frame]];
644 [switchView_ replaceSubview:oldView with:newView];
646 [newView setFrame:[switchView_ bounds]];
647 [switchView_ addSubview:newView];
650 // New content is in place, delegate should adjust itself accordingly.
651 [delegate_ onActivateTabWithContents:[controller webContents]];
653 // It also restores content autoresizing properties.
654 [controller ensureContentsVisible];
657 // Create a new tab view and set its cell correctly so it draws the way we want
658 // it to. It will be sized and positioned by |-layoutTabs| so there's no need to
659 // set the frame here. This also creates the view as hidden, it will be
660 // shown during layout.
661 - (TabController*)newTab {
662 TabController* controller = [[[TabController alloc] init] autorelease];
663 [controller setTarget:self];
664 [controller setAction:@selector(selectTab:)];
665 [[controller view] setHidden:YES];
670 // (Private) Handles a click on the new tab button.
671 - (void)clickNewTabButton:(id)sender {
672 content::RecordAction(UserMetricsAction("NewTab_Button"));
673 UMA_HISTOGRAM_ENUMERATION("Tab.NewTab", TabStripModel::NEW_TAB_BUTTON,
674 TabStripModel::NEW_TAB_ENUM_COUNT);
675 tabStripModel_->delegate()->AddTabAt(GURL(), -1, true);
678 // (Private) Returns the number of open tabs in the tab strip. This is the
679 // number of TabControllers we know about (as there's a 1-to-1 mapping from
680 // these controllers to a tab) less the number of closing tabs.
681 - (NSInteger)numberOfOpenTabs {
682 return static_cast<NSInteger>(tabStripModel_->count());
685 // (Private) Returns the number of open, pinned tabs.
686 - (NSInteger)numberOfOpenPinnedTabs {
687 // Ask the model for the number of pinned tabs. Note that tabs which are in
688 // the process of closing (i.e., whose controllers are in
689 // |closingControllers_|) have already been removed from the model.
690 return tabStripModel_->IndexOfFirstNonPinnedTab();
693 // (Private) Returns the number of open, non-pinned tabs.
694 - (NSInteger)numberOfOpenNonPinnedTabs {
695 NSInteger number = [self numberOfOpenTabs] - [self numberOfOpenPinnedTabs];
696 DCHECK_GE(number, 0);
700 // Given an index into the tab model, returns the index into the tab controller
701 // or tab contents controller array accounting for tabs that are currently
702 // closing. For example, if there are two tabs in the process of closing before
703 // |index|, this returns |index| + 2. If there are no closing tabs, this will
705 - (NSInteger)indexFromModelIndex:(NSInteger)index {
711 for (TabController* controller in tabArray_.get()) {
712 if ([closingControllers_ containsObject:controller]) {
713 DCHECK([[controller tabView] isClosing]);
716 if (i == index) // No need to check anything after, it has no effect.
723 // Given an index into |tabArray_|, return the corresponding index into
724 // |tabStripModel_| or NSNotFound if the specified tab does not exist in
725 // the model (if it's closing, for example).
726 - (NSInteger)modelIndexFromIndex:(NSInteger)index {
727 NSInteger modelIndex = 0;
728 NSInteger arrayIndex = 0;
729 for (TabController* controller in tabArray_.get()) {
730 if (![closingControllers_ containsObject:controller]) {
731 if (arrayIndex == index)
734 } else if (arrayIndex == index) {
735 // Tab is closing - no model index.
743 // Returns the index of the subview |view|. Returns -1 if not present. Takes
744 // closing tabs into account such that this index will correctly match the tab
745 // model. If |view| is in the process of closing, returns -1, as closing tabs
746 // are no longer in the model.
747 - (NSInteger)modelIndexForTabView:(NSView*)view {
749 for (TabController* current in tabArray_.get()) {
750 // If |current| is closing, skip it.
751 if ([closingControllers_ containsObject:current])
753 else if ([current view] == view)
760 // Returns the index of the contents subview |view|. Returns -1 if not present.
761 // Takes closing tabs into account such that this index will correctly match the
762 // tab model. If |view| is in the process of closing, returns -1, as closing
763 // tabs are no longer in the model.
764 - (NSInteger)modelIndexForContentsView:(NSView*)view {
767 for (TabContentsController* current in tabContentsArray_.get()) {
768 // If the TabController corresponding to |current| is closing, skip it.
769 TabController* controller = [tabArray_ objectAtIndex:i];
770 if ([closingControllers_ containsObject:controller]) {
773 } else if ([current view] == view) {
782 - (NSArray*)selectedViews {
783 NSMutableArray* views = [NSMutableArray arrayWithCapacity:[tabArray_ count]];
784 for (TabController* tab in tabArray_.get()) {
786 [views addObject:[tab tabView]];
791 // Returns the view at the given index, using the array of TabControllers to
792 // get the associated view. Returns nil if out of range.
793 - (NSView*)viewAtIndex:(NSUInteger)index {
794 if (index >= [tabArray_ count])
796 return [[tabArray_ objectAtIndex:index] view];
799 - (NSUInteger)viewsCount {
800 return [tabArray_ count];
803 // Called when the user clicks a tab. Tell the model the selection has changed,
804 // which feeds back into us via a notification.
805 - (void)selectTab:(id)sender {
806 DCHECK([sender isKindOfClass:[NSView class]]);
807 int index = [self modelIndexForTabView:sender];
808 NSUInteger modifiers = [[NSApp currentEvent] modifierFlags];
809 if (tabStripModel_->ContainsIndex(index)) {
810 if (modifiers & NSCommandKeyMask && modifiers & NSShiftKeyMask) {
811 tabStripModel_->AddSelectionFromAnchorTo(index);
812 } else if (modifiers & NSShiftKeyMask) {
813 tabStripModel_->ExtendSelectionTo(index);
814 } else if (modifiers & NSCommandKeyMask) {
815 tabStripModel_->ToggleSelectionAt(index);
817 tabStripModel_->ActivateTabAt(index, true);
822 // Called when the user clicks the tab audio indicator to mute the tab.
823 - (void)toggleMute:(id)sender {
824 DCHECK([sender isKindOfClass:[TabView class]]);
825 NSInteger index = [self modelIndexForTabView:sender];
826 if (!tabStripModel_->ContainsIndex(index))
828 WebContents* contents = tabStripModel_->GetWebContentsAt(index);
829 chrome::SetTabAudioMuted(contents, !contents->IsAudioMuted(),
830 TAB_MUTED_REASON_AUDIO_INDICATOR, std::string());
833 // Called when the user closes a tab. Asks the model to close the tab. |sender|
834 // is the TabView that is potentially going away.
835 - (void)closeTab:(id)sender {
836 DCHECK([sender isKindOfClass:[TabView class]]);
838 // Cancel any pending tab transition.
839 hoverTabSelector_->CancelTabTransition();
841 if ([hoveredTab_ isEqual:sender])
842 [self setHoveredTab:nil];
844 NSInteger index = [self modelIndexForTabView:sender];
845 if (!tabStripModel_->ContainsIndex(index))
848 content::RecordAction(UserMetricsAction("CloseTab_Mouse"));
849 const NSInteger numberOfOpenTabs = [self numberOfOpenTabs];
850 if (numberOfOpenTabs > 1) {
851 bool isClosingLastTab = index == numberOfOpenTabs - 1;
852 if (!isClosingLastTab) {
853 // Limit the width available for laying out tabs so that tabs are not
854 // resized until a later time (when the mouse leaves the tab strip).
855 // However, if the tab being closed is a pinned tab, break out of
856 // rapid-closure mode since the mouse is almost guaranteed not to be over
857 // the closebox of the adjacent tab (due to the difference in widths).
858 // TODO(pinkerton): re-visit when handling tab overflow.
859 // http://crbug.com/188
860 if (tabStripModel_->IsTabPinned(index)) {
861 availableResizeWidth_ = kUseFullAvailableWidth;
863 NSView* penultimateTab = [self viewAtIndex:numberOfOpenTabs - 2];
864 availableResizeWidth_ = NSMaxX([penultimateTab frame]);
867 // If the rightmost tab is closed, change the available width so that
868 // another tab's close button lands below the cursor (assuming the tabs
869 // are currently below their maximum width and can grow).
870 NSView* lastTab = [self viewAtIndex:numberOfOpenTabs - 1];
871 availableResizeWidth_ = NSMaxX([lastTab frame]);
873 tabStripModel_->CloseWebContentsAt(
875 TabStripModel::CLOSE_USER_GESTURE |
876 TabStripModel::CLOSE_CREATE_HISTORICAL_TAB);
878 // Use the standard window close if this is the last tab
879 // this prevents the tab from being removed from the model until after
880 // the window dissapears
881 [[tabStripView_ window] performClose:nil];
885 // Dispatch context menu commands for the given tab controller.
886 - (void)commandDispatch:(TabStripModel::ContextMenuCommand)command
887 forController:(TabController*)controller {
888 int index = [self modelIndexForTabView:[controller view]];
889 if (tabStripModel_->ContainsIndex(index))
890 tabStripModel_->ExecuteContextMenuCommand(index, command);
893 // Returns YES if the specificed command should be enabled for the given
895 - (BOOL)isCommandEnabled:(TabStripModel::ContextMenuCommand)command
896 forController:(TabController*)controller {
897 int index = [self modelIndexForTabView:[controller view]];
898 if (!tabStripModel_->ContainsIndex(index))
900 return tabStripModel_->IsContextMenuCommandEnabled(index, command) ? YES : NO;
903 // Returns a context menu model for a given controller. Caller owns the result.
904 - (ui::SimpleMenuModel*)contextMenuModelForController:(TabController*)controller
905 menuDelegate:(ui::SimpleMenuModel::Delegate*)delegate {
906 int index = [self modelIndexForTabView:[controller view]];
907 return new TabMenuModel(delegate, tabStripModel_, index);
910 // Returns a weak reference to the controller that manages dragging of tabs.
911 - (id<TabDraggingEventTarget>)dragController {
912 return dragController_.get();
915 - (void)insertPlaceholderForTab:(TabView*)tab frame:(NSRect)frame {
916 placeholderTab_ = tab;
917 placeholderFrame_ = frame;
918 [self layoutTabsWithAnimation:initialLayoutComplete_ regenerateSubviews:NO];
921 - (BOOL)isDragSessionActive {
922 return placeholderTab_ != nil;
925 - (BOOL)isTabFullyVisible:(TabView*)tab {
926 NSRect frame = [tab frame];
927 return NSMinX(frame) >= [self leftIndentForControls] &&
928 NSMaxX(frame) <= (NSMaxX([tabStripView_ frame]) -
929 [self rightIndentForControls]);
932 - (void)showNewTabButton:(BOOL)show {
933 forceNewTabButtonHidden_ = show ? NO : YES;
934 if (forceNewTabButtonHidden_)
935 [newTabButton_ setHidden:YES];
938 // Lay out all tabs in the order of their TabContentsControllers, which matches
939 // the ordering in the TabStripModel. This call isn't that expensive, though
940 // it is O(n) in the number of tabs. Tabs will animate to their new position
941 // if the window is visible and |animate| is YES.
942 // TODO(pinkerton): Note this doesn't do too well when the number of min-sized
943 // tabs would cause an overflow. http://crbug.com/188
944 - (void)layoutTabsWithAnimation:(BOOL)animate
945 regenerateSubviews:(BOOL)doUpdate {
946 DCHECK([NSThread isMainThread]);
947 if (![tabArray_ count])
950 const CGFloat kMaxTabWidth = [TabController maxTabWidth];
951 const CGFloat kMinTabWidth = [TabController minTabWidth];
952 const CGFloat kMinActiveTabWidth = [TabController minActiveTabWidth];
953 const CGFloat kPinnedTabWidth = [TabController pinnedTabWidth];
955 NSRect enclosingRect = NSZeroRect;
956 ScopedNSAnimationContextGroup mainAnimationGroup(animate);
957 mainAnimationGroup.SetCurrentContextDuration(kAnimationDuration);
959 // Update the current subviews and their z-order if requested.
961 [self regenerateSubviewList];
963 // Compute the base width of tabs given how much room we're allowed. Note that
964 // pinned tabs have a fixed width. We may not be able to use the entire width
965 // if the user is quickly closing tabs. This may be negative, but that's okay
966 // (taken care of by |MAX()| when calculating tab sizes).
967 CGFloat availableSpace = 0;
968 if ([self inRapidClosureMode]) {
969 availableSpace = availableResizeWidth_;
971 availableSpace = NSWidth([tabStripView_ frame]);
973 // Account for the width of the new tab button.
975 NSWidth([newTabButton_ frame]) + kNewTabButtonOffset - kTabOverlap;
977 // Account for the right-side controls if not in rapid closure mode.
978 // (In rapid closure mode, the available width is set based on the
979 // position of the rightmost tab, not based on the width of the tab strip,
980 // so the right controls have already been accounted for.)
981 availableSpace -= [self rightIndentForControls];
984 // Need to leave room for the left-side controls even in rapid closure mode.
985 availableSpace -= [self leftIndentForControls];
987 // This may be negative, but that's okay (taken care of by |MAX()| when
988 // calculating tab sizes). "pinned" tabs in horizontal mode just get a special
989 // section, they don't change size.
990 CGFloat availableSpaceForNonPinned = availableSpace;
991 if ([self numberOfOpenPinnedTabs]) {
992 availableSpaceForNonPinned -=
993 [self numberOfOpenPinnedTabs] * (kPinnedTabWidth - kTabOverlap);
994 availableSpaceForNonPinned -= kLastPinnedTabSpacing;
997 // Initialize |nonPinnedTabWidth| in case there aren't any non-pinned tabs;
998 // this value shouldn't actually be used.
999 CGFloat nonPinnedTabWidth = kMaxTabWidth;
1000 CGFloat nonPinnedTabWidthFraction = 0;
1001 NSInteger numberOfNonPinnedTabs = MIN(
1002 [self numberOfOpenNonPinnedTabs],
1003 (availableSpaceForNonPinned - kTabOverlap) / (kMinTabWidth -
1006 if (numberOfNonPinnedTabs) {
1007 // Find the width of a non-pinned tab. This only applies to horizontal
1008 // mode. Add in the amount we "get back" from the tabs overlapping.
1010 ((availableSpaceForNonPinned - kTabOverlap) / numberOfNonPinnedTabs) +
1013 // Clamp the width between the max and min.
1014 nonPinnedTabWidth = MAX(MIN(nonPinnedTabWidth, kMaxTabWidth), kMinTabWidth);
1016 // When there are multiple tabs, we'll have one active and some inactive
1017 // tabs. If the desired width was between the minimum sizes of these types,
1018 // try to shrink the tabs with the smaller minimum. For example, if we have
1019 // a strip of width 10 with 4 tabs, the desired width per tab will be 2.5.
1020 // If selected tabs have a minimum width of 4 and unselected tabs have
1021 // minimum width of 1, the above code would set *unselected_width = 2.5,
1022 // *selected_width = 4, which results in a total width of 11.5. Instead, we
1023 // want to set *unselected_width = 2, *selected_width = 4, for a total width
1025 if (numberOfNonPinnedTabs > 1 && nonPinnedTabWidth < kMinActiveTabWidth) {
1026 nonPinnedTabWidth = (availableSpaceForNonPinned - kMinActiveTabWidth) /
1027 (numberOfNonPinnedTabs - 1) +
1029 if (nonPinnedTabWidth < kMinTabWidth) {
1030 // The above adjustment caused the tabs to not fit, show 1 less tab.
1031 --numberOfNonPinnedTabs;
1032 nonPinnedTabWidth = ((availableSpaceForNonPinned - kTabOverlap) /
1033 numberOfNonPinnedTabs) +
1038 // Separate integral and fractional parts.
1039 CGFloat integralPart = std::floor(nonPinnedTabWidth);
1040 nonPinnedTabWidthFraction = nonPinnedTabWidth - integralPart;
1041 nonPinnedTabWidth = integralPart;
1044 BOOL visible = [[tabStripView_ window] isVisible];
1046 CGFloat offset = [self leftIndentForControls];
1047 bool hasPlaceholderGap = false;
1048 // Whether or not the last tab processed by the loop was a pinned tab.
1049 BOOL isLastTabPinned = NO;
1050 CGFloat tabWidthAccumulatedFraction = 0;
1051 NSInteger laidOutNonPinnedTabs = 0;
1053 for (TabController* tab in tabArray_.get()) {
1054 // Ignore a tab that is going through a close animation.
1055 if ([closingControllers_ containsObject:tab])
1058 BOOL isPlaceholder = [[tab view] isEqual:placeholderTab_];
1059 NSRect tabFrame = [[tab view] frame];
1060 tabFrame.size.height = [[self class] defaultTabHeight];
1061 tabFrame.origin.y = 0;
1062 tabFrame.origin.x = offset;
1064 // If the tab is hidden, we consider it a new tab. We make it visible
1065 // and animate it in.
1066 BOOL newTab = [[tab view] isHidden];
1068 [[tab view] setHidden:NO];
1070 if (isPlaceholder) {
1071 // Move the current tab to the correct location instantly.
1072 // We need a duration or else it doesn't cancel an inflight animation.
1073 ScopedNSAnimationContextGroup localAnimationGroup(animate);
1074 localAnimationGroup.SetCurrentContextShortestDuration();
1075 tabFrame.origin.x = placeholderFrame_.origin.x;
1076 id target = animate ? [[tab view] animator] : [tab view];
1077 [target setFrame:tabFrame];
1079 // Store the frame by identifier to avoid redundant calls to animator.
1080 NSValue* identifier = [NSValue valueWithPointer:[tab view]];
1081 [targetFrames_ setObject:[NSValue valueWithRect:tabFrame]
1086 if (placeholderTab_ && !hasPlaceholderGap) {
1087 const CGFloat placeholderMin = NSMinX(placeholderFrame_);
1088 // If the left edge is to the left of the placeholder's left, but the
1089 // mid is to the right of it slide over to make space for it.
1090 if (NSMidX(tabFrame) > placeholderMin) {
1091 hasPlaceholderGap = true;
1092 offset += NSWidth(placeholderFrame_);
1093 offset -= kTabOverlap;
1094 tabFrame.origin.x = offset;
1098 // Set the width. Selected tabs are slightly wider when things get really
1099 // small and thus we enforce a different minimum width.
1100 BOOL isPinned = [tab pinned];
1102 tabFrame.size.width = kPinnedTabWidth;
1104 // Tabs have non-integer widths. Assign the integer part to the tab, and
1105 // keep an accumulation of the fractional parts. When the fractional
1106 // accumulation gets to be more than one pixel, assign that to the current
1107 // tab being laid out. This is vaguely inspired by Bresenham's line
1109 tabFrame.size.width = nonPinnedTabWidth;
1110 tabWidthAccumulatedFraction += nonPinnedTabWidthFraction;
1112 if (tabWidthAccumulatedFraction >= 1.0) {
1113 ++tabFrame.size.width;
1114 --tabWidthAccumulatedFraction;
1117 // In case of rounding error, give any left over pixels to the last tab.
1118 if (laidOutNonPinnedTabs == numberOfNonPinnedTabs - 1 &&
1119 tabWidthAccumulatedFraction > 0.5) {
1120 ++tabFrame.size.width;
1123 ++laidOutNonPinnedTabs;
1127 tabFrame.size.width = MAX(tabFrame.size.width, kMinActiveTabWidth);
1129 // If this is the first non-pinned tab, then add a bit of spacing between
1130 // this and the last pinned tab.
1131 if (!isPinned && isLastTabPinned) {
1132 offset += kLastPinnedTabSpacing;
1133 tabFrame.origin.x = offset;
1135 isLastTabPinned = isPinned;
1137 if (laidOutNonPinnedTabs > numberOfNonPinnedTabs) {
1138 // There is not enough space to fit this tab.
1139 tabFrame.size.width = 0;
1140 [self setFrame:tabFrame ofTabView:[tab view]];
1144 // Animate a new tab in by putting it below the horizon unless told to put
1145 // it in a specific location (i.e., from a drop).
1146 if (newTab && visible && animate) {
1147 if (NSEqualRects(droppedTabFrame_, NSZeroRect)) {
1148 [[tab view] setFrame:NSOffsetRect(tabFrame, 0, -NSHeight(tabFrame))];
1150 [[tab view] setFrame:droppedTabFrame_];
1151 droppedTabFrame_ = NSZeroRect;
1155 // Check the frame by identifier to avoid redundant calls to animator.
1156 id frameTarget = visible && animate ? [[tab view] animator] : [tab view];
1157 NSValue* identifier = [NSValue valueWithPointer:[tab view]];
1158 NSValue* oldTargetValue = [targetFrames_ objectForKey:identifier];
1159 if (!oldTargetValue ||
1160 !NSEqualRects([oldTargetValue rectValue], tabFrame)) {
1161 [frameTarget setFrame:tabFrame];
1162 [targetFrames_ setObject:[NSValue valueWithRect:tabFrame]
1166 enclosingRect = NSUnionRect(tabFrame, enclosingRect);
1168 offset += NSWidth(tabFrame);
1169 offset -= kTabOverlap;
1172 // Hide the new tab button if we're explicitly told to. It may already
1173 // be hidden, doing it again doesn't hurt. Otherwise position it
1174 // appropriately, showing it if necessary.
1175 if (forceNewTabButtonHidden_) {
1176 [newTabButton_ setHidden:YES];
1178 NSRect newTabNewFrame = [newTabButton_ frame];
1179 // We've already ensured there's enough space for the new tab button
1180 // so we don't have to check it against the available space. We do need
1181 // to make sure we put it after any placeholder.
1182 CGFloat maxTabX = MAX(offset, NSMaxX(placeholderFrame_) - kTabOverlap);
1183 newTabNewFrame.origin = NSMakePoint(maxTabX + kNewTabButtonOffset, 0);
1184 if ([tabContentsArray_ count])
1185 [newTabButton_ setHidden:NO];
1187 if (!NSEqualRects(newTabTargetFrame_, newTabNewFrame)) {
1188 // Set the new tab button image correctly based on where the cursor is.
1189 NSWindow* window = [tabStripView_ window];
1190 NSPoint currentMouse = [window mouseLocationOutsideOfEventStream];
1191 currentMouse = [tabStripView_ convertPoint:currentMouse fromView:nil];
1193 BOOL shouldShowHover = [newTabButton_ pointIsOverButton:currentMouse];
1194 [self setNewTabButtonHoverState:shouldShowHover];
1196 // Move the new tab button into place. We want to animate the new tab
1197 // button if it's moving to the left (closing a tab), but not when it's
1198 // moving to the right (inserting a new tab). If moving right, we need
1199 // to use a very small duration to make sure we cancel any in-flight
1200 // animation to the left.
1201 if (visible && animate) {
1202 ScopedNSAnimationContextGroup localAnimationGroup(true);
1203 BOOL movingLeft = NSMinX(newTabNewFrame) < NSMinX(newTabTargetFrame_);
1205 localAnimationGroup.SetCurrentContextShortestDuration();
1207 [[newTabButton_ animator] setFrame:newTabNewFrame];
1208 newTabTargetFrame_ = newTabNewFrame;
1210 [newTabButton_ setFrame:newTabNewFrame];
1211 newTabTargetFrame_ = newTabNewFrame;
1216 [dragBlockingView_ setFrame:enclosingRect];
1218 // Mark that we've successfully completed layout of at least one tab.
1219 initialLayoutComplete_ = YES;
1222 // When we're told to layout from the public API we usually want to animate,
1223 // except when it's the first time.
1224 - (void)layoutTabs {
1225 [self layoutTabsWithAnimation:initialLayoutComplete_ regenerateSubviews:YES];
1228 - (void)layoutTabsWithoutAnimation {
1229 [self layoutTabsWithAnimation:NO regenerateSubviews:YES];
1232 // Handles setting the title of the tab based on the given |contents|. Uses
1233 // a canned string if |contents| is NULL.
1234 - (void)setTabTitle:(TabController*)tab withContents:(WebContents*)contents {
1235 base::string16 title;
1237 title = contents->GetTitle();
1239 title = l10n_util::GetStringUTF16(IDS_BROWSER_WINDOW_MAC_TAB_UNTITLED);
1240 [tab setTitle:base::SysUTF16ToNSString(title)];
1242 const base::string16& toolTip = chrome::AssembleTabTooltipText(
1243 title, chrome::GetTabMediaStateForContents(contents));
1244 [tab setToolTip:base::SysUTF16ToNSString(toolTip)];
1247 // Called when a notification is received from the model to insert a new tab
1249 - (void)insertTabWithContents:(content::WebContents*)contents
1250 atIndex:(NSInteger)modelIndex
1251 inForeground:(bool)inForeground {
1253 DCHECK(modelIndex == TabStripModel::kNoTab ||
1254 tabStripModel_->ContainsIndex(modelIndex));
1256 // Cancel any pending tab transition.
1257 hoverTabSelector_->CancelTabTransition();
1259 // Take closing tabs into account.
1260 NSInteger index = [self indexFromModelIndex:modelIndex];
1262 // Make a new tab. Load the contents of this tab from the nib and associate
1263 // the new controller with |contents| so it can be looked up later.
1264 base::scoped_nsobject<TabContentsController> contentsController(
1265 [[TabContentsController alloc] initWithContents:contents]);
1266 [tabContentsArray_ insertObject:contentsController atIndex:index];
1268 // Make a new tab and add it to the strip. Keep track of its controller.
1269 TabController* newController = [self newTab];
1270 [newController setPinned:tabStripModel_->IsTabPinned(modelIndex)];
1271 [newController setUrl:contents->GetURL()];
1272 [tabArray_ insertObject:newController atIndex:index];
1273 NSView* newView = [newController view];
1275 // Set the originating frame to just below the strip so that it animates
1276 // upwards as it's being initially layed out. Oddly, this works while doing
1277 // something similar in |-layoutTabs| confuses the window server.
1278 [newView setFrame:NSOffsetRect([newView frame],
1279 0, -[[self class] defaultTabHeight])];
1281 [self setTabTitle:newController withContents:contents];
1283 // If a tab is being inserted, we can again use the entire tab strip width
1285 availableResizeWidth_ = kUseFullAvailableWidth;
1287 // We don't need to call |-layoutTabs| if the tab will be in the foreground
1288 // because it will get called when the new tab is selected by the tab model.
1289 // Whenever |-layoutTabs| is called, it'll also add the new subview.
1290 if (!inForeground) {
1294 // During normal loading, we won't yet have a favicon and we'll get
1295 // subsequent state change notifications to show the throbber, but when we're
1296 // dragging a tab out into a new window, we have to put the tab's favicon
1297 // into the right state up front as we won't be told to do it from anywhere
1299 [self updateIconsForContents:contents atIndex:modelIndex];
1302 // Called before |contents| is deactivated.
1303 - (void)tabDeactivatedWithContents:(content::WebContents*)contents {
1304 contents->StoreFocus();
1307 // Called when a notification is received from the model to select a particular
1308 // tab. Swaps in the toolbar and content area associated with |newContents|.
1309 - (void)activateTabWithContents:(content::WebContents*)newContents
1310 previousContents:(content::WebContents*)oldContents
1311 atIndex:(NSInteger)modelIndex
1312 reason:(int)reason {
1313 // Take closing tabs into account.
1316 browser_->tab_strip_model()->GetIndexOfWebContents(oldContents);
1317 if (oldModelIndex != -1) { // When closing a tab, the old tab may be gone.
1318 NSInteger oldIndex = [self indexFromModelIndex:oldModelIndex];
1319 TabContentsController* oldController =
1320 [tabContentsArray_ objectAtIndex:oldIndex];
1321 [oldController willBecomeUnselectedTab];
1322 oldContents->WasHidden();
1326 NSUInteger activeIndex = [self indexFromModelIndex:modelIndex];
1328 [tabArray_ enumerateObjectsUsingBlock:^(TabController* current,
1331 [current setActive:index == activeIndex];
1334 // Tell the new tab contents it is about to become the selected tab. Here it
1335 // can do things like make sure the toolbar is up to date.
1336 TabContentsController* newController =
1337 [tabContentsArray_ objectAtIndex:activeIndex];
1338 [newController willBecomeSelectedTab];
1340 // Relayout for new tabs and to let the selected tab grow to be larger in
1341 // size than surrounding tabs if the user has many. This also raises the
1342 // selected tab to the top.
1345 // Swap in the contents for the new tab.
1346 [self swapInTabAtIndex:modelIndex];
1349 newContents->WasShown();
1350 newContents->RestoreFocus();
1354 - (void)tabSelectionChanged {
1355 // First get the vector of indices, which is allays sorted in ascending order.
1356 ui::ListSelectionModel::SelectedIndices selection(
1357 tabStripModel_->selection_model().selected_indices());
1358 // Iterate through all of the tabs, selecting each as necessary.
1359 ui::ListSelectionModel::SelectedIndices::iterator iter = selection.begin();
1361 for (TabController* current in tabArray_.get()) {
1362 BOOL selected = iter != selection.end() &&
1363 [self indexFromModelIndex:*iter] == i;
1364 [current setSelected:selected];
1371 - (void)tabReplacedWithContents:(content::WebContents*)newContents
1372 previousContents:(content::WebContents*)oldContents
1373 atIndex:(NSInteger)modelIndex {
1374 NSInteger index = [self indexFromModelIndex:modelIndex];
1375 TabContentsController* oldController =
1376 [tabContentsArray_ objectAtIndex:index];
1377 DCHECK_EQ(oldContents, [oldController webContents]);
1379 // Simply create a new TabContentsController for |newContents| and place it
1380 // into the array, replacing |oldContents|. An ActiveTabChanged notification
1381 // will follow, at which point we will install the new view.
1382 base::scoped_nsobject<TabContentsController> newController(
1383 [[TabContentsController alloc] initWithContents:newContents]);
1385 // Bye bye, |oldController|.
1386 [tabContentsArray_ replaceObjectAtIndex:index withObject:newController];
1388 // Fake a tab changed notification to force tab titles and favicons to update.
1389 [self tabChangedWithContents:newContents
1391 changeType:TabStripModelObserver::ALL];
1394 // Remove all knowledge about this tab and its associated controller, and remove
1395 // the view from the strip.
1396 - (void)removeTab:(TabController*)controller {
1397 // Cancel any pending tab transition.
1398 hoverTabSelector_->CancelTabTransition();
1400 NSUInteger index = [tabArray_ indexOfObject:controller];
1402 // Release the tab contents controller so those views get destroyed. This
1403 // will remove all the tab content Cocoa views from the hierarchy. A
1404 // subsequent "select tab" notification will follow from the model. To
1405 // tell us what to swap in in its absence.
1406 [tabContentsArray_ removeObjectAtIndex:index];
1408 // Remove the view from the tab strip.
1409 NSView* tab = [controller view];
1410 [tab removeFromSuperview];
1412 // Remove ourself as an observer.
1413 [[NSNotificationCenter defaultCenter]
1415 name:NSViewDidUpdateTrackingAreasNotification
1418 // Clear the tab controller's target.
1419 // TODO(viettrungluu): [crbug.com/23829] Find a better way to handle the tab
1420 // controller's target.
1421 [controller setTarget:nil];
1423 if ([hoveredTab_ isEqual:tab])
1424 [self setHoveredTab:nil];
1426 NSValue* identifier = [NSValue valueWithPointer:tab];
1427 [targetFrames_ removeObjectForKey:identifier];
1429 // Once we're totally done with the tab, delete its controller
1430 [tabArray_ removeObjectAtIndex:index];
1433 // Called by the CAAnimation delegate when the tab completes the closing
1435 - (void)animationDidStop:(CAAnimation*)animation
1436 forController:(TabController*)controller
1437 finished:(BOOL)finished{
1438 [[animation delegate] invalidate];
1439 [closingControllers_ removeObject:controller];
1440 [self removeTab:controller];
1443 // Save off which TabController is closing and tell its view's animator
1444 // where to move the tab to. Registers a delegate to call back when the
1445 // animation is complete in order to remove the tab from the model.
1446 - (void)startClosingTabWithAnimation:(TabController*)closingTab {
1447 DCHECK([NSThread isMainThread]);
1449 // Cancel any pending tab transition.
1450 hoverTabSelector_->CancelTabTransition();
1452 // Save off the controller into the set of animating tabs. This alerts
1453 // the layout method to not do anything with it and allows us to correctly
1454 // calculate offsets when working with indices into the model.
1455 [closingControllers_ addObject:closingTab];
1457 // Mark the tab as closing. This prevents it from generating any drags or
1458 // selections while it's animating closed.
1459 [[closingTab tabView] setClosing:YES];
1461 // Register delegate (owned by the animation system).
1462 NSView* tabView = [closingTab view];
1463 CAAnimation* animation = [[tabView animationForKey:@"frameOrigin"] copy];
1464 [animation autorelease];
1465 base::scoped_nsobject<TabCloseAnimationDelegate> delegate(
1466 [[TabCloseAnimationDelegate alloc] initWithTabStrip:self
1467 tabController:closingTab]);
1468 [animation setDelegate:delegate.get()]; // Retains delegate.
1469 NSMutableDictionary* animationDictionary =
1470 [NSMutableDictionary dictionaryWithDictionary:[tabView animations]];
1471 [animationDictionary setObject:animation forKey:@"frameOrigin"];
1472 [tabView setAnimations:animationDictionary];
1474 // Periscope down! Animate the tab.
1475 NSRect newFrame = [tabView frame];
1476 newFrame = NSOffsetRect(newFrame, 0, -newFrame.size.height);
1477 ScopedNSAnimationContextGroup animationGroup(true);
1478 animationGroup.SetCurrentContextDuration(kAnimationDuration);
1479 [[tabView animator] setFrame:newFrame];
1482 // Called when a notification is received from the model that the given tab
1483 // has gone away. Start an animation then force a layout to put everything
1485 - (void)tabDetachedWithContents:(content::WebContents*)contents
1486 atIndex:(NSInteger)modelIndex {
1487 // Take closing tabs into account.
1488 NSInteger index = [self indexFromModelIndex:modelIndex];
1490 // Cancel any pending tab transition.
1491 hoverTabSelector_->CancelTabTransition();
1493 TabController* tab = [tabArray_ objectAtIndex:index];
1494 if (tabStripModel_->count() > 0) {
1495 [self startClosingTabWithAnimation:tab];
1498 // Don't remove the tab, as that makes the window look jarring without any
1499 // tabs. Instead, simply mark it as closing to prevent the tab from
1500 // generating any drags or selections.
1501 [[tab tabView] setClosing:YES];
1504 [delegate_ onTabDetachedWithContents:contents];
1507 // A helper routine for creating an NSImageView to hold the favicon or app icon
1509 - (NSImage*)iconImageForContents:(content::WebContents*)contents {
1510 extensions::TabHelper* extensions_tab_helper =
1511 extensions::TabHelper::FromWebContents(contents);
1512 BOOL isApp = extensions_tab_helper->is_app();
1513 NSImage* image = nil;
1514 // Favicons come from the renderer, and the renderer draws everything in the
1515 // system color space.
1516 CGColorSpaceRef colorSpace = base::mac::GetSystemColorSpace();
1518 SkBitmap* icon = extensions_tab_helper->GetExtensionAppIcon();
1520 image = gfx::SkBitmapToNSImageWithColorSpace(*icon, colorSpace);
1522 image = mac::FaviconForWebContents(contents);
1525 // Either we don't have a valid favicon or there was some issue converting it
1526 // from an SkBitmap. Either way, just show the default.
1528 image = defaultFavicon_.get();
1533 // Updates the current loading state, replacing the icon view with a favicon,
1534 // a throbber, the default icon, or nothing at all.
1535 - (void)updateIconsForContents:(content::WebContents*)contents
1536 atIndex:(NSInteger)modelIndex {
1540 static NSImage* throbberWaitingImage =
1541 ResourceBundle::GetSharedInstance().GetNativeImageNamed(
1542 IDR_THROBBER_WAITING).CopyNSImage();
1543 static NSImage* throbberLoadingImage =
1544 ResourceBundle::GetSharedInstance().GetNativeImageNamed(
1545 IDR_THROBBER).CopyNSImage();
1546 static NSImage* sadFaviconImage =
1547 ResourceBundle::GetSharedInstance().GetNativeImageNamed(
1548 IDR_SAD_FAVICON).CopyNSImage();
1550 // Take closing tabs into account.
1551 NSInteger index = [self indexFromModelIndex:modelIndex];
1552 TabController* tabController = [tabArray_ objectAtIndex:index];
1554 bool oldHasIcon = [tabController iconView] != nil;
1556 favicon::ShouldDisplayFavicon(contents) ||
1557 tabStripModel_->IsTabPinned(modelIndex); // Always show icon if pinned.
1559 TabLoadingState oldState = [tabController loadingState];
1560 TabLoadingState newState = kTabDone;
1561 NSImage* throbberImage = nil;
1562 if (contents->IsCrashed()) {
1563 newState = kTabCrashed;
1565 } else if (contents->IsWaitingForResponse()) {
1566 newState = kTabWaiting;
1567 throbberImage = throbberWaitingImage;
1568 } else if (contents->IsLoadingToDifferentDocument()) {
1569 newState = kTabLoading;
1570 throbberImage = throbberLoadingImage;
1573 if (oldState != newState)
1574 [tabController setLoadingState:newState];
1576 // While loading, this function is called repeatedly with the same state.
1577 // To avoid expensive unnecessary view manipulation, only make changes when
1578 // the state is actually changing. When loading is complete (kTabDone),
1579 // every call to this function is significant.
1580 if (newState == kTabDone || oldState != newState ||
1581 oldHasIcon != newHasIcon) {
1583 if (newState == kTabDone) {
1584 [tabController setIconImage:[self iconImageForContents:contents]];
1585 } else if (newState == kTabCrashed) {
1586 [tabController setIconImage:sadFaviconImage withToastAnimation:YES];
1588 [tabController setIconImage:throbberImage];
1591 [tabController setIconImage:nil];
1595 [tabController setMediaState:chrome::GetTabMediaStateForContents(contents)];
1597 [tabController updateVisibility];
1600 // Called when a notification is received from the model that the given tab
1601 // has been updated. |loading| will be YES when we only want to update the
1602 // throbber state, not anything else about the (partially) loading tab.
1603 - (void)tabChangedWithContents:(content::WebContents*)contents
1604 atIndex:(NSInteger)modelIndex
1605 changeType:(TabStripModelObserver::TabChangeType)change {
1606 // Take closing tabs into account.
1607 NSInteger index = [self indexFromModelIndex:modelIndex];
1609 if (modelIndex == tabStripModel_->active_index())
1610 [delegate_ onTabChanged:change withContents:contents];
1612 if (change == TabStripModelObserver::TITLE_NOT_LOADING) {
1613 // TODO(sky): make this work.
1614 // We'll receive another notification of the change asynchronously.
1618 TabController* tabController = [tabArray_ objectAtIndex:index];
1620 if (change != TabStripModelObserver::LOADING_ONLY)
1621 [self setTabTitle:tabController withContents:contents];
1623 [self updateIconsForContents:contents atIndex:modelIndex];
1625 TabContentsController* updatedController =
1626 [tabContentsArray_ objectAtIndex:index];
1627 [updatedController tabDidChange:contents];
1630 // Called when a tab is moved (usually by drag&drop). Keep our parallel arrays
1631 // in sync with the tab strip model. It can also be pinned/unpinned
1632 // simultaneously, so we need to take care of that.
1633 - (void)tabMovedWithContents:(content::WebContents*)contents
1634 fromIndex:(NSInteger)modelFrom
1635 toIndex:(NSInteger)modelTo {
1636 // Take closing tabs into account.
1637 NSInteger from = [self indexFromModelIndex:modelFrom];
1638 NSInteger to = [self indexFromModelIndex:modelTo];
1640 // Cancel any pending tab transition.
1641 hoverTabSelector_->CancelTabTransition();
1643 base::scoped_nsobject<TabContentsController> movedTabContentsController(
1644 [[tabContentsArray_ objectAtIndex:from] retain]);
1645 [tabContentsArray_ removeObjectAtIndex:from];
1646 [tabContentsArray_ insertObject:movedTabContentsController.get()
1648 base::scoped_nsobject<TabController> movedTabController(
1649 [[tabArray_ objectAtIndex:from] retain]);
1650 DCHECK([movedTabController isKindOfClass:[TabController class]]);
1651 [tabArray_ removeObjectAtIndex:from];
1652 [tabArray_ insertObject:movedTabController.get() atIndex:to];
1654 // The tab moved, which means that the pinned tab state may have changed.
1655 if (tabStripModel_->IsTabPinned(modelTo) != [movedTabController pinned])
1656 [self tabPinnedStateChangedWithContents:contents atIndex:modelTo];
1661 // Called when a tab is pinned or unpinned without moving.
1662 - (void)tabPinnedStateChangedWithContents:(content::WebContents*)contents
1663 atIndex:(NSInteger)modelIndex {
1664 // Take closing tabs into account.
1665 NSInteger index = [self indexFromModelIndex:modelIndex];
1667 TabController* tabController = [tabArray_ objectAtIndex:index];
1668 DCHECK([tabController isKindOfClass:[TabController class]]);
1670 // Don't do anything if the change was already picked up by the move event.
1671 if (tabStripModel_->IsTabPinned(modelIndex) == [tabController pinned])
1674 [tabController setPinned:tabStripModel_->IsTabPinned(modelIndex)];
1675 [tabController setUrl:contents->GetURL()];
1676 [self updateIconsForContents:contents atIndex:modelIndex];
1677 // If the tab is being restored and it's pinned, the pinned state is set after
1678 // the tab has already been rendered, so re-layout the tabstrip. In all other
1679 // cases, the state is set before the tab is rendered so this isn't needed.
1683 - (void)setFrame:(NSRect)frame ofTabView:(NSView*)view {
1684 NSValue* identifier = [NSValue valueWithPointer:view];
1685 [targetFrames_ setObject:[NSValue valueWithRect:frame]
1687 [view setFrame:frame];
1690 - (TabStripModel*)tabStripModel {
1691 return tabStripModel_;
1694 - (NSArray*)tabViews {
1695 NSMutableArray* views = [NSMutableArray arrayWithCapacity:[tabArray_ count]];
1696 for (TabController* tab in tabArray_.get()) {
1697 [views addObject:[tab tabView]];
1702 - (NSView*)activeTabView {
1703 int activeIndex = tabStripModel_->active_index();
1704 // Take closing tabs into account. They can't ever be selected.
1705 activeIndex = [self indexFromModelIndex:activeIndex];
1706 return [self viewAtIndex:activeIndex];
1709 - (int)indexOfPlaceholder {
1710 // Use |tabArray_| here instead of the tab strip count in order to get the
1711 // correct index when there are closing tabs to the left of the placeholder.
1712 const int count = [tabArray_ count];
1714 // No placeholder, return the end of the strip.
1715 if (placeholderTab_ == nil)
1718 double placeholderX = placeholderFrame_.origin.x;
1721 while (index < count) {
1722 // Ignore closing tabs for simplicity. The only drawback of this is that
1723 // if the placeholder is placed right before one or several contiguous
1724 // currently closing tabs, the associated TabController will start at the
1725 // end of the closing tabs.
1726 if ([closingControllers_ containsObject:[tabArray_ objectAtIndex:index]]) {
1730 NSView* curr = [self viewAtIndex:index];
1731 // The placeholder tab works by changing the frame of the tab being dragged
1732 // to be the bounds of the placeholder, so we need to skip it while we're
1733 // iterating, otherwise we'll end up off by one. Note This only effects
1734 // dragging to the right, not to the left.
1735 if (curr == placeholderTab_) {
1739 if (placeholderX <= NSMinX([curr frame]))
1747 // Move the given tab at index |from| in this window to the location of the
1748 // current placeholder.
1749 - (void)moveTabFromIndex:(NSInteger)from {
1750 int toIndex = [self indexOfPlaceholder];
1751 // Cancel any pending tab transition.
1752 hoverTabSelector_->CancelTabTransition();
1753 tabStripModel_->MoveWebContentsAt(from, toIndex, true);
1756 // Drop a given WebContents at the location of the current placeholder.
1757 // If there is no placeholder, it will go at the end. Used when dragging from
1758 // another window when we don't have access to the WebContents as part of our
1759 // strip. |frame| is in the coordinate system of the tab strip view and
1760 // represents where the user dropped the new tab so it can be animated into its
1761 // correct location when the tab is added to the model. If the tab was pinned in
1762 // its previous window, setting |pinned| to YES will propagate that state to the
1763 // new window. Pinned tabs are pinned tabs; the |pinned| state is the caller's
1765 - (void)dropWebContents:(WebContents*)contents
1766 atIndex:(int)modelIndex
1767 withFrame:(NSRect)frame
1768 asPinnedTab:(BOOL)pinned
1769 activate:(BOOL)activate {
1770 // Mark that the new tab being created should start at |frame|. It will be
1771 // reset as soon as the tab has been positioned.
1772 droppedTabFrame_ = frame;
1774 // Insert it into this tab strip. We want it in the foreground and to not
1775 // inherit the current tab's group.
1776 tabStripModel_->InsertWebContentsAt(
1779 (activate ? TabStripModel::ADD_ACTIVE : TabStripModel::ADD_NONE) |
1780 (pinned ? TabStripModel::ADD_PINNED : TabStripModel::ADD_NONE));
1783 // Called when the tab strip view changes size. As we only registered for
1784 // changes on our view, we know it's only for our view. Layout w/out
1785 // animations since they are blocked by the resize nested runloop. We need
1786 // the views to adjust immediately. Neither the tabs nor their z-order are
1787 // changed, so we don't need to update the subviews.
1788 - (void)tabViewFrameChanged:(NSNotification*)info {
1789 [self layoutTabsWithAnimation:NO regenerateSubviews:NO];
1792 // Called when the tracking areas for any given tab are updated. This allows
1793 // the individual tabs to update their hover states correctly.
1794 // Only generates the event if the cursor is in the tab strip.
1795 - (void)tabUpdateTracking:(NSNotification*)notification {
1796 DCHECK([[notification object] isKindOfClass:[TabView class]]);
1797 DCHECK(mouseInside_);
1798 NSWindow* window = [tabStripView_ window];
1799 NSPoint location = [window mouseLocationOutsideOfEventStream];
1800 if (NSPointInRect(location, [tabStripView_ frame])) {
1801 NSEvent* mouseEvent = [NSEvent mouseEventWithType:NSMouseMoved
1805 windowNumber:[window windowNumber]
1810 [self mouseMoved:mouseEvent];
1814 - (BOOL)inRapidClosureMode {
1815 return availableResizeWidth_ != kUseFullAvailableWidth;
1818 // Disable tab dragging when there are any pending animations.
1819 - (BOOL)tabDraggingAllowed {
1820 return [closingControllers_ count] == 0;
1823 - (void)mouseMoved:(NSEvent*)event {
1824 // We don't want the draggged tab to repeatedly redraw its glow unnecessarily.
1825 // We also want the dragged tab to keep the glow even when it slides behind
1827 if ([dragController_ draggedTab])
1830 // Use hit test to figure out what view we are hovering over.
1831 NSView* targetView = [tabStripView_ hitTest:[event locationInWindow]];
1833 // Set the new tab button hover state iff the mouse is over the button.
1834 BOOL shouldShowHoverImage = [targetView isKindOfClass:[NewTabButton class]];
1835 [self setNewTabButtonHoverState:shouldShowHoverImage];
1837 TabView* tabView = (TabView*)targetView;
1838 if (![tabView isKindOfClass:[TabView class]]) {
1839 if ([[tabView superview] isKindOfClass:[TabView class]]) {
1840 tabView = (TabView*)[targetView superview];
1846 if (hoveredTab_ != tabView) {
1847 [self setHoveredTab:tabView];
1849 [hoveredTab_ mouseMoved:event];
1853 - (void)mouseEntered:(NSEvent*)event {
1854 NSTrackingArea* area = [event trackingArea];
1855 if ([area isEqual:trackingArea_]) {
1857 [self setTabTrackingAreasEnabled:YES];
1858 [self mouseMoved:event];
1862 // Called when the tracking area is in effect which means we're tracking to
1863 // see if the user leaves the tab strip with their mouse. When they do,
1864 // reset layout to use all available width.
1865 - (void)mouseExited:(NSEvent*)event {
1866 NSTrackingArea* area = [event trackingArea];
1867 if ([area isEqual:trackingArea_]) {
1869 [self setTabTrackingAreasEnabled:NO];
1870 availableResizeWidth_ = kUseFullAvailableWidth;
1871 [self setHoveredTab:nil];
1873 } else if ([area isEqual:newTabTrackingArea_]) {
1874 // If the mouse is moved quickly enough, it is possible for the mouse to
1875 // leave the tabstrip without sending any mouseMoved: messages at all.
1876 // Since this would result in the new tab button incorrectly staying in the
1877 // hover state, disable the hover image on every mouse exit.
1878 [self setNewTabButtonHoverState:NO];
1882 - (TabView*)hoveredTab {
1886 - (void)setHoveredTab:(TabView*)newHoveredTab {
1888 [hoveredTab_ mouseExited:nil];
1889 [toolTipView_ setFrame:NSZeroRect];
1892 hoveredTab_ = newHoveredTab;
1894 if (newHoveredTab) {
1895 [newHoveredTab mouseEntered:nil];
1897 // Use a transparent subview to show the hovered tab's tooltip while the
1898 // mouse pointer is inside the tab's custom shape.
1900 toolTipView_.reset([[NSView alloc] init]);
1901 [toolTipView_ setToolTip:[newHoveredTab toolTipText]];
1902 [toolTipView_ setFrame:[newHoveredTab frame]];
1903 if (![toolTipView_ superview]) {
1904 [tabStripView_ addSubview:toolTipView_
1905 positioned:NSWindowBelow
1911 // Enable/Disable the tracking areas for the tabs. They are only enabled
1912 // when the mouse is in the tabstrip.
1913 - (void)setTabTrackingAreasEnabled:(BOOL)enabled {
1914 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
1915 for (TabController* controller in tabArray_.get()) {
1916 TabView* tabView = [controller tabView];
1918 // Set self up to observe tabs so hover states will be correct.
1919 [defaultCenter addObserver:self
1920 selector:@selector(tabUpdateTracking:)
1921 name:NSViewDidUpdateTrackingAreasNotification
1924 [defaultCenter removeObserver:self
1925 name:NSViewDidUpdateTrackingAreasNotification
1928 [tabView setTrackingEnabled:enabled];
1932 // Sets the new tab button's image based on the current hover state. Does
1933 // nothing if the hover state is already correct.
1934 - (void)setNewTabButtonHoverState:(BOOL)shouldShowHover {
1935 if (shouldShowHover && !newTabButtonShowingHoverImage_) {
1936 newTabButtonShowingHoverImage_ = YES;
1937 [[newTabButton_ cell] setIsMouseInside:YES];
1938 } else if (!shouldShowHover && newTabButtonShowingHoverImage_) {
1939 newTabButtonShowingHoverImage_ = NO;
1940 [[newTabButton_ cell] setIsMouseInside:NO];
1944 // Adds the given subview to (the end of) the list of permanent subviews
1945 // (specified from bottom up). These subviews will always be below the
1946 // transitory subviews (tabs). |-regenerateSubviewList| must be called to
1947 // effectuate the addition.
1948 - (void)addSubviewToPermanentList:(NSView*)aView {
1950 [permanentSubviews_ addObject:aView];
1953 // Update the subviews, keeping the permanent ones (or, more correctly, putting
1954 // in the ones listed in permanentSubviews_), and putting in the current tabs in
1955 // the correct z-order. Any current subviews which is neither in the permanent
1956 // list nor a (current) tab will be removed. So if you add such a subview, you
1957 // should call |-addSubviewToPermanentList:| (or better yet, call that and then
1958 // |-regenerateSubviewList| to actually add it).
1959 - (void)regenerateSubviewList {
1960 // Remove self as an observer from all the old tabs before a new set of
1961 // potentially different tabs is put in place.
1962 [self setTabTrackingAreasEnabled:NO];
1964 // Subviews to put in (in bottom-to-top order), beginning with the permanent
1966 NSMutableArray* subviews = [NSMutableArray arrayWithArray:permanentSubviews_];
1968 NSView* activeTabView = nil;
1969 // Go through tabs in reverse order, since |subviews| is bottom-to-top.
1970 for (TabController* tab in [tabArray_ reverseObjectEnumerator]) {
1971 NSView* tabView = [tab view];
1973 DCHECK(!activeTabView);
1974 activeTabView = tabView;
1976 [subviews addObject:tabView];
1979 if (activeTabView) {
1980 [subviews addObject:activeTabView];
1982 WithNoAnimation noAnimation;
1983 [tabStripView_ setSubviews:subviews];
1984 [self setTabTrackingAreasEnabled:mouseInside_];
1987 // Get the index and disposition for a potential URL(s) drop given a point (in
1988 // the |TabStripView|'s coordinates). It considers only the x-coordinate of the
1989 // given point. If it's in the "middle" of a tab, it drops on that tab. If it's
1990 // to the left, it inserts to the left, and similarly for the right.
1991 - (void)droppingURLsAt:(NSPoint)point
1992 givesIndex:(NSInteger*)index
1993 disposition:(WindowOpenDisposition*)disposition {
1994 // Proportion of the tab which is considered the "middle" (and causes things
1995 // to drop on that tab).
1996 const double kMiddleProportion = 0.5;
1997 const double kLRProportion = (1.0 - kMiddleProportion) / 2.0;
1999 DCHECK(index && disposition);
2001 for (TabController* tab in tabArray_.get()) {
2002 NSView* view = [tab view];
2003 DCHECK([view isKindOfClass:[TabView class]]);
2005 // Recall that |-[NSView frame]| is in its superview's coordinates, so a
2006 // |TabView|'s frame is in the coordinates of the |TabStripView| (which
2007 // matches the coordinate system of |point|).
2008 NSRect frame = [view frame];
2010 // Modify the frame to make it "unoverlapped".
2011 frame.origin.x += kTabOverlap / 2.0;
2012 frame.size.width -= kTabOverlap;
2013 if (frame.size.width < 1.0)
2014 frame.size.width = 1.0; // try to avoid complete failure
2016 // Drop in a new tab to the left of tab |i|?
2017 if (point.x < (frame.origin.x + kLRProportion * frame.size.width)) {
2019 *disposition = NEW_FOREGROUND_TAB;
2024 if (point.x <= (frame.origin.x +
2025 (1.0 - kLRProportion) * frame.size.width)) {
2027 *disposition = CURRENT_TAB;
2031 // (Dropping in a new tab to the right of tab |i| will be taken care of in
2032 // the next iteration.)
2036 // If we've made it here, we want to append a new tab to the end.
2038 *disposition = NEW_FOREGROUND_TAB;
2041 - (void)openURL:(GURL*)url inView:(NSView*)view at:(NSPoint)point {
2042 // Get the index and disposition.
2044 WindowOpenDisposition disposition;
2045 [self droppingURLsAt:point
2047 disposition:&disposition];
2049 // Either insert a new tab or open in a current tab.
2050 switch (disposition) {
2051 case NEW_FOREGROUND_TAB: {
2052 content::RecordAction(UserMetricsAction("Tab_DropURLBetweenTabs"));
2053 chrome::NavigateParams params(browser_, *url,
2054 ui::PAGE_TRANSITION_TYPED);
2055 params.disposition = disposition;
2056 params.tabstrip_index = index;
2057 params.tabstrip_add_types =
2058 TabStripModel::ADD_ACTIVE | TabStripModel::ADD_FORCE_INDEX;
2059 chrome::Navigate(¶ms);
2063 content::RecordAction(UserMetricsAction("Tab_DropURLOnTab"));
2064 OpenURLParams params(
2065 *url, Referrer(), CURRENT_TAB, ui::PAGE_TRANSITION_TYPED, false);
2066 tabStripModel_->GetWebContentsAt(index)->OpenURL(params);
2067 tabStripModel_->ActivateTabAt(index, true);
2075 // (URLDropTargetController protocol)
2076 - (void)dropURLs:(NSArray*)urls inView:(NSView*)view at:(NSPoint)point {
2077 DCHECK_EQ(view, tabStripView_.get());
2079 if ([urls count] < 1) {
2084 //TODO(viettrungluu): dropping multiple URLs.
2085 if ([urls count] > 1)
2088 // Get the first URL and fix it up.
2089 GURL url(GURL(url_formatter::FixupURL(
2090 base::SysNSStringToUTF8([urls objectAtIndex:0]), std::string())));
2092 [self openURL:&url inView:view at:point];
2095 // (URLDropTargetController protocol)
2096 - (void)dropText:(NSString*)text inView:(NSView*)view at:(NSPoint)point {
2097 DCHECK_EQ(view, tabStripView_.get());
2099 // If the input is plain text, classify the input and make the URL.
2100 AutocompleteMatch match;
2101 AutocompleteClassifierFactory::GetForProfile(browser_->profile())->Classify(
2102 base::SysNSStringToUTF16(text), false, false,
2103 metrics::OmniboxEventProto::BLANK, &match, NULL);
2104 GURL url(match.destination_url);
2106 [self openURL:&url inView:view at:point];
2109 // (URLDropTargetController protocol)
2110 - (void)indicateDropURLsInView:(NSView*)view at:(NSPoint)point {
2111 DCHECK_EQ(view, tabStripView_.get());
2113 // The minimum y-coordinate at which one should consider place the arrow.
2114 const CGFloat arrowBaseY = 25;
2117 WindowOpenDisposition disposition;
2118 [self droppingURLsAt:point
2120 disposition:&disposition];
2122 NSPoint arrowPos = NSMakePoint(0, arrowBaseY);
2124 // Append a tab at the end.
2125 DCHECK(disposition == NEW_FOREGROUND_TAB);
2126 NSInteger lastIndex = [tabArray_ count] - 1;
2127 NSRect overRect = [[[tabArray_ objectAtIndex:lastIndex] view] frame];
2128 arrowPos.x = overRect.origin.x + overRect.size.width - kTabOverlap / 2.0;
2130 NSRect overRect = [[[tabArray_ objectAtIndex:index] view] frame];
2131 switch (disposition) {
2132 case NEW_FOREGROUND_TAB:
2133 // Insert tab (to the left of the given tab).
2134 arrowPos.x = overRect.origin.x + kTabOverlap / 2.0;
2137 // Overwrite the given tab.
2138 arrowPos.x = overRect.origin.x + overRect.size.width / 2.0;
2145 [tabStripView_ setDropArrowPosition:arrowPos];
2146 [tabStripView_ setDropArrowShown:YES];
2147 [tabStripView_ setNeedsDisplay:YES];
2149 // Perform a delayed tab transition if hovering directly over a tab.
2150 if (index != -1 && disposition == CURRENT_TAB) {
2151 NSInteger modelIndex = [self modelIndexFromIndex:index];
2152 // Only start the transition if it has a valid model index (i.e. it's not
2153 // in the middle of closing).
2154 if (modelIndex != NSNotFound) {
2155 hoverTabSelector_->StartTabTransition(modelIndex);
2159 // If a tab transition was not started, cancel the pending one.
2160 hoverTabSelector_->CancelTabTransition();
2163 // (URLDropTargetController protocol)
2164 - (void)hideDropURLsIndicatorInView:(NSView*)view {
2165 DCHECK_EQ(view, tabStripView_.get());
2167 // Cancel any pending tab transition.
2168 hoverTabSelector_->CancelTabTransition();
2170 if ([tabStripView_ dropArrowShown]) {
2171 [tabStripView_ setDropArrowShown:NO];
2172 [tabStripView_ setNeedsDisplay:YES];
2176 // (URLDropTargetController protocol)
2177 - (BOOL)isUnsupportedDropData:(id<NSDraggingInfo>)info {
2178 return drag_util::IsUnsupportedDropData(browser_->profile(), info);
2181 - (TabContentsController*)activeTabContentsController {
2182 int modelIndex = tabStripModel_->active_index();
2185 NSInteger index = [self indexFromModelIndex:modelIndex];
2187 index >= (NSInteger)[tabContentsArray_ count])
2189 return [tabContentsArray_ objectAtIndex:index];
2192 - (void)addCustomWindowControls {
2193 if (!customWindowControls_) {
2194 // Make the container view.
2195 CGFloat height = NSHeight([tabStripView_ frame]);
2196 NSRect frame = NSMakeRect(0, 0, [self leftIndentForControls], height);
2197 customWindowControls_.reset([[NSView alloc] initWithFrame:frame]);
2198 [customWindowControls_
2199 setAutoresizingMask:NSViewMaxXMargin | NSViewHeightSizable];
2201 // Add the traffic light buttons. The horizontal layout was determined by
2202 // manual inspection on Yosemite.
2203 CGFloat closeButtonX = 11;
2204 CGFloat pinnedButtonX = 31;
2205 CGFloat zoomButtonX = 51;
2207 NSUInteger styleMask = [[tabStripView_ window] styleMask];
2208 NSButton* closeButton = [NSWindow standardWindowButton:NSWindowCloseButton
2209 forStyleMask:styleMask];
2211 // Vertically center the buttons in the tab strip.
2212 CGFloat buttonY = floor((height - NSHeight([closeButton bounds])) / 2);
2213 [closeButton setFrameOrigin:NSMakePoint(closeButtonX, buttonY)];
2214 [customWindowControls_ addSubview:closeButton];
2216 NSButton* miniaturizeButton =
2217 [NSWindow standardWindowButton:NSWindowMiniaturizeButton
2218 forStyleMask:styleMask];
2219 [miniaturizeButton setFrameOrigin:NSMakePoint(pinnedButtonX, buttonY)];
2220 [miniaturizeButton setEnabled:NO];
2221 [customWindowControls_ addSubview:miniaturizeButton];
2223 NSButton* zoomButton =
2224 [NSWindow standardWindowButton:NSWindowZoomButton
2225 forStyleMask:styleMask];
2226 [customWindowControls_ addSubview:zoomButton];
2227 [zoomButton setFrameOrigin:NSMakePoint(zoomButtonX, buttonY)];
2230 if (![permanentSubviews_ containsObject:customWindowControls_]) {
2231 [self addSubviewToPermanentList:customWindowControls_];
2232 [self regenerateSubviewList];
2236 - (void)removeCustomWindowControls {
2237 if (customWindowControls_)
2238 [permanentSubviews_ removeObject:customWindowControls_];
2239 [self regenerateSubviewList];
2242 - (void)themeDidChangeNotification:(NSNotification*)notification {
2243 [self setNewTabImages];
2246 - (void)setNewTabImages {
2247 ThemeService *theme =
2248 static_cast<ThemeService*>([[tabStripView_ window] themeProvider]);
2252 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
2253 NSImage* mask = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON_MASK).ToNSImage();
2254 NSImage* normal = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON).ToNSImage();
2255 NSImage* hover = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON_H).ToNSImage();
2256 NSImage* pressed = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON_P).ToNSImage();
2258 NSImage* foreground = ApplyMask(
2259 theme->GetNSImageNamed(IDR_THEME_TAB_BACKGROUND), mask);
2261 [[newTabButton_ cell] setImage:Overlay(foreground, normal, 1.0)
2262 forButtonState:image_button_cell::kDefaultState];
2263 [[newTabButton_ cell] setImage:Overlay(foreground, hover, 1.0)
2264 forButtonState:image_button_cell::kHoverState];
2265 [[newTabButton_ cell] setImage:Overlay(foreground, pressed, 1.0)
2266 forButtonState:image_button_cell::kPressedState];
2268 // IDR_THEME_TAB_BACKGROUND_INACTIVE is only used with the default theme.
2269 if (theme->UsingDefaultTheme()) {
2270 const CGFloat alpha = tabs::kImageNoFocusAlpha;
2271 NSImage* background = ApplyMask(
2272 theme->GetNSImageNamed(IDR_THEME_TAB_BACKGROUND_INACTIVE), mask);
2273 [[newTabButton_ cell] setImage:Overlay(background, normal, alpha)
2274 forButtonState:image_button_cell::kDefaultStateBackground];
2275 [[newTabButton_ cell] setImage:Overlay(background, hover, alpha)
2276 forButtonState:image_button_cell::kHoverStateBackground];
2278 [[newTabButton_ cell] setImage:nil
2279 forButtonState:image_button_cell::kDefaultStateBackground];
2280 [[newTabButton_ cell] setImage:nil
2281 forButtonState:image_button_cell::kHoverStateBackground];