tab_strip_controller.mm revision 010d83a9304c5a91596085d917d248abff47903a
1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h" 6 7#import <QuartzCore/QuartzCore.h> 8 9#include <cmath> 10#include <limits> 11#include <string> 12 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" 73 74using base::UserMetricsAction; 75using content::OpenURLParams; 76using content::Referrer; 77using content::WebContents; 78 79namespace { 80 81// A value to indicate tab layout should use the full available width of the 82// view. 83const CGFloat kUseFullAvailableWidth = -1.0; 84 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. 90const CGFloat kTabOverlap = 19.0; 91 92// The amount by which mini tabs are separated from normal tabs. 93const CGFloat kLastMiniTabSpacing = 2.0; 94 95// The amount by which the new tab button is offset (from the tabs). 96const CGFloat kNewTabButtonOffset = 8.0; 97 98// Time (in seconds) in which tabs animate to their final position. 99const NSTimeInterval kAnimationDuration = 0.125; 100 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. 103class ScopedNSAnimationContextGroup { 104 public: 105 explicit ScopedNSAnimationContextGroup(bool animate) 106 : animate_(animate) { 107 if (animate_) { 108 [NSAnimationContext beginGrouping]; 109 } 110 } 111 112 ~ScopedNSAnimationContextGroup() { 113 if (animate_) { 114 [NSAnimationContext endGrouping]; 115 } 116 } 117 118 void SetCurrentContextDuration(NSTimeInterval duration) { 119 if (animate_) { 120 [[NSAnimationContext currentContext] gtm_setDuration:duration 121 eventMask:NSLeftMouseUpMask]; 122 } 123 } 124 125 void SetCurrentContextShortestDuration() { 126 if (animate_) { 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]; 134 } 135 } 136 137private: 138 bool animate_; 139 DISALLOW_COPY_AND_ASSIGN(ScopedNSAnimationContextGroup); 140}; 141 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 146// 10.6 and 10.7. 147NSImage* 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 157 bitsPerSample:8 158 samplesPerPixel:4 159 hasAlpha:YES 160 isPlanar:NO 161 colorSpaceName:NSDeviceRGBColorSpace 162 bytesPerRow:0 163 bitsPerPixel:0] autorelease]; 164 [bmpImageRep setSize:size]; 165 [NSGraphicsContext setCurrentContext: 166 [NSGraphicsContext graphicsContextWithBitmapImageRep:bmpImageRep]]; 167 drawingHandler(size); 168 [result addRepresentation:bmpImageRep]; 169 } 170 [NSGraphicsContext restoreGraphicsState]; 171 172 return result.release(); 173} 174 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|. 177NSImage* 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; 184 185 // In some themes, the tab background image is narrower than the 186 // new tab button, so tile the background image. 187 CGFloat x = 0; 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)); 192 while (x < width) { 193 [image drawAtPoint:NSMakePoint(x, 0) 194 fromRect:NSMakeRect(0, 195 [image size].height - height - kYOffset, 196 tileWidth, 197 height) 198 operation:NSCompositeCopy 199 fraction:1.0]; 200 x += tileWidth; 201 } 202 203 [mask drawAtPoint:NSZeroPoint 204 fromRect:NSMakeRect(0, 0, width, height) 205 operation:NSCompositeDestinationIn 206 fraction:1.0]; 207 }) autorelease]; 208} 209 210// Paints |overlay| on top of |ground|. 211NSImage* 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); 214 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 221 fraction:1.0]; 222 [overlay drawAtPoint:NSZeroPoint 223 fromRect:NSMakeRect(0, 0, width, height) 224 operation:NSCompositeSourceOver 225 fraction:alpha]; 226 }) autorelease]; 227} 228 229} // namespace 230 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; 256@end 257 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 261// that. 262@interface TabStripControllerDragBlockingView : NSView { 263 TabStripController* controller_; // weak; owns us 264} 265 266- (id)initWithFrame:(NSRect)frameRect 267 controller:(TabStripController*)controller; 268 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; 274@end 275 276@implementation TabStripControllerDragBlockingView 277- (BOOL)mouseDownCanMoveWindow { 278 return NO; 279} 280 281- (void)drawRect:(NSRect)rect { 282} 283 284- (id)initWithFrame:(NSRect)frameRect 285 controller:(TabStripController*)controller { 286 if ((self = [super initWithFrame:frameRect])) { 287 controller_ = controller; 288 } 289 return self; 290} 291 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] 302 fromView:nil]; 303 NSView* hitView = [superview hitTest:hitLocation]; 304 305 if ([controller_ inRapidClosureMode]) { 306 if (hitView != self) { 307 [hitView mouseDown:event]; 308 return; 309 } 310 } 311 312 if (hitView == self) { 313 BrowserWindowController* windowController = 314 [BrowserWindowController browserWindowControllerForView:self]; 315 if (![windowController isFullscreen]) { 316 [self trackClickForWindowMove:event]; 317 return; 318 } 319 } 320 [super mouseDown:event]; 321} 322 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 331 dequeue:YES]) && 332 [event type] != NSLeftMouseUp) { 333 base::mac::ScopedNSAutoreleasePool pool; 334 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]; 339 lastEventLoc = now; 340 } 341} 342 343@end 344 345#pragma mark - 346 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 { 353 @private 354 TabStripController* strip_; // weak; owns us indirectly 355 TabController* controller_; // weak 356} 357 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; 362 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. 366- (void)invalidate; 367 368// CAAnimation delegate method 369- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished; 370 371@end 372 373@implementation TabCloseAnimationDelegate 374 375- (id)initWithTabStrip:(TabStripController*)strip 376 tabController:(TabController*)controller { 377 if ((self = [super init])) { 378 DCHECK(strip && controller); 379 strip_ = strip; 380 controller_ = controller; 381 } 382 return self; 383} 384 385- (void)invalidate { 386 strip_ = nil; 387 controller_ = nil; 388} 389 390- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished { 391 [strip_ animationDidStop:animation 392 forController:controller_ 393 finished:finished]; 394} 395 396@end 397 398#pragma mark - 399 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. 408// 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 429// were). 430// 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. 435 436@implementation TabStripController 437 438@synthesize leftIndentForControls = leftIndentForControls_; 439@synthesize rightIndentForControls = rightIndentForControls_; 440 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; 450 browser_ = browser; 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]; 460 461 // Important note: any non-tab subviews not added to |permanentSubviews_| 462 // (see |-addSubviewToPermanentList:|) will be wiped out. 463 permanentSubviews_.reset([[NSMutableArray alloc] init]); 464 465 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 466 defaultFavicon_.reset( 467 rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).CopyNSImage()); 468 469 [self setLeftIndentForControls:[[self class] defaultLeftIndentForControls]]; 470 [self setRightIndentForControls:0]; 471 472 // Add this invisible view first so that it is ordered below other views. 473 dragBlockingView_.reset( 474 [[TabStripControllerDragBlockingView alloc] initWithFrame:NSZeroRect 475 controller:self]); 476 [self addSubviewToPermanentList:dragBlockingView_]; 477 478 newTabButton_ = [view getNewTabButton]; 479 [newTabButton_ setWantsLayer:YES]; 480 [self addSubviewToPermanentList:newTabButton_]; 481 [newTabButton_ setTarget:self]; 482 [newTabButton_ setAction:@selector(clickNewTabButton:)]; 483 484 [self setNewTabImages]; 485 newTabButtonShowingHoverImage_ = NO; 486 newTabTrackingArea_.reset( 487 [[CrTrackingArea alloc] initWithRect:[newTabButton_ bounds] 488 options:(NSTrackingMouseEnteredAndExited | 489 NSTrackingActiveAlways) 490 owner:self 491 userInfo:nil]); 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]); 496 497 newTabTargetFrame_ = NSZeroRect; 498 availableResizeWidth_ = kUseFullAvailableWidth; 499 500 closingControllers_.reset([[NSMutableSet alloc] init]); 501 502 // Install the permanent subviews. 503 [self regenerateSubviewList]; 504 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] 508 addObserver:self 509 selector:@selector(tabViewFrameChanged:) 510 name:NSViewFrameDidChangeNotification 511 object:tabStripView_]; 512 513 [[NSNotificationCenter defaultCenter] 514 addObserver:self 515 selector:@selector(themeDidChangeNotification:) 516 name:kBrowserThemeDidChangeNotification 517 object:nil]; 518 519 trackingArea_.reset([[CrTrackingArea alloc] 520 initWithRect:NSZeroRect // Ignored by NSTrackingInVisibleRect 521 options:NSTrackingMouseEnteredAndExited | 522 NSTrackingMouseMoved | 523 NSTrackingActiveAlways | 524 NSTrackingInVisibleRect 525 owner:self 526 userInfo:nil]); 527 if (browserWindow) // Nil for Browsers without a tab strip (e.g. popups). 528 [trackingArea_ clearOwnerWhenWindowWillClose:browserWindow]; 529 [tabStripView_ addTrackingArea:trackingArea_.get()]; 530 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]; 538 mouseInside_ = YES; 539 } 540 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]; 546 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 557 atIndex:i 558 inForeground:NO]; 559 if (selection == currentContents) { 560 // Must manually force a selection since the model won't send 561 // selection messages in this scenario. 562 [self 563 activateTabWithContents:currentContents 564 previousContents:NULL 565 atIndex:i 566 reason:TabStripModelObserver::CHANGE_REASON_NONE]; 567 } 568 } 569 // Don't lay out the tabs until after the controller has been fully 570 // constructed. 571 if (existingTabCount) { 572 [self performSelectorOnMainThread:@selector(layoutTabs) 573 withObject:nil 574 waitUntilDone:NO]; 575 } 576 } 577 return self; 578} 579 580- (void)dealloc { 581 [tabStripView_ setController:nil]; 582 583 if (trackingArea_.get()) 584 [tabStripView_ removeTrackingArea:trackingArea_.get()]; 585 586 [newTabButton_ removeTrackingArea:newTabTrackingArea_.get()]; 587 // Invalidate all closing animations so they don't call back to us after 588 // we're gone. 589 for (TabController* controller in closingControllers_.get()) { 590 NSView* view = [controller view]; 591 [[[view animationForKey:@"frameOrigin"] delegate] invalidate]; 592 } 593 [[NSNotificationCenter defaultCenter] removeObserver:self]; 594 [tabStripView_ removeAllToolTips]; 595 [super dealloc]; 596} 597 598+ (CGFloat)defaultTabHeight { 599 return 26.0; 600} 601 602+ (CGFloat)defaultLeftIndentForControls { 603 // Default indentation leaves enough room so tabs don't overlap with the 604 // window controls. 605 return 70.0; 606} 607 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 610// contents. 611- (void)swapInTabAtIndex:(NSInteger)modelIndex { 612 DCHECK(modelIndex >= 0 && modelIndex < tabStripModel_->count()); 613 NSInteger index = [self indexFromModelIndex:modelIndex]; 614 TabContentsController* controller = [tabContentsArray_ objectAtIndex:index]; 615 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 619 // positions. 620 ScopedCAActionDisabler ca_disabler; 621 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]; 628 629 // Turns content autoresizing off, so removing and inserting views won't 630 // trigger unnecessary content relayout. 631 [controller ensureContentsSizeDoesNotChange]; 632 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 643 // properties. 644 [newView setFrame:[oldView frame]]; 645 [switchView_ replaceSubview:oldView with:newView]; 646 } else { 647 [newView setFrame:[switchView_ bounds]]; 648 [switchView_ addSubview:newView]; 649 } 650 651 // New content is in place, delegate should adjust itself accordingly. 652 [delegate_ onActivateTabWithContents:[controller webContents]]; 653 654 // It also restores content autoresizing properties. 655 [controller ensureContentsVisible]; 656 657 NSWindow* parentWindow = [switchView_ window]; 658 ConstrainedWindowSheetController* sheetController = 659 [ConstrainedWindowSheetController 660 controllerForParentWindow:parentWindow]; 661 [sheetController parentViewDidBecomeActive:newView]; 662} 663 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]; 673 674 return controller; 675} 676 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); 683} 684 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()); 690} 691 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(); 698} 699 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); 704 return number; 705} 706 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 711// return |index|. 712- (NSInteger)indexFromModelIndex:(NSInteger)index { 713 DCHECK_GE(index, 0); 714 if (index < 0) 715 return index; 716 717 NSInteger i = 0; 718 for (TabController* controller in tabArray_.get()) { 719 if ([closingControllers_ containsObject:controller]) { 720 DCHECK([[controller tabView] isClosing]); 721 ++index; 722 } 723 if (i == index) // No need to check anything after, it has no effect. 724 break; 725 ++i; 726 } 727 return index; 728} 729 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) 739 return modelIndex; 740 ++modelIndex; 741 } else if (arrayIndex == index) { 742 // Tab is closing - no model index. 743 return NSNotFound; 744 } 745 ++arrayIndex; 746 } 747 return NSNotFound; 748} 749 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 { 755 NSInteger index = 0; 756 for (TabController* current in tabArray_.get()) { 757 // If |current| is closing, skip it. 758 if ([closingControllers_ containsObject:current]) 759 continue; 760 else if ([current view] == view) 761 return index; 762 ++index; 763 } 764 return -1; 765} 766 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 { 772 NSInteger index = 0; 773 NSInteger i = 0; 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]) { 778 ++i; 779 continue; 780 } else if ([current view] == view) { 781 return index; 782 } 783 ++index; 784 ++i; 785 } 786 return -1; 787} 788 789- (NSArray*)selectedViews { 790 NSMutableArray* views = [NSMutableArray arrayWithCapacity:[tabArray_ count]]; 791 for (TabController* tab in tabArray_.get()) { 792 if ([tab selected]) 793 [views addObject:[tab tabView]]; 794 } 795 return views; 796} 797 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]) 802 return NULL; 803 return [[tabArray_ objectAtIndex:index] view]; 804} 805 806- (NSUInteger)viewsCount { 807 return [tabArray_ count]; 808} 809 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); 823 } else { 824 tabStripModel_->ActivateTabAt(index, true); 825 } 826 } 827} 828 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]]); 833 834 // Cancel any pending tab transition. 835 hoverTabSelector_->CancelTabTransition(); 836 837 if ([hoveredTab_ isEqual:sender]) { 838 hoveredTab_ = nil; 839 } 840 841 NSInteger index = [self modelIndexForTabView:sender]; 842 if (!tabStripModel_->ContainsIndex(index)) 843 return; 844 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; 859 } else { 860 NSView* penultimateTab = [self viewAtIndex:numberOfOpenTabs - 2]; 861 availableResizeWidth_ = NSMaxX([penultimateTab frame]); 862 } 863 } else { 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]); 869 } 870 tabStripModel_->CloseWebContentsAt( 871 index, 872 TabStripModel::CLOSE_USER_GESTURE | 873 TabStripModel::CLOSE_CREATE_HISTORICAL_TAB); 874 } else { 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]; 879 } 880} 881 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); 888} 889 890// Returns YES if the specificed command should be enabled for the given 891// controller. 892- (BOOL)isCommandEnabled:(TabStripModel::ContextMenuCommand)command 893 forController:(TabController*)controller { 894 int index = [self modelIndexForTabView:[controller view]]; 895 if (!tabStripModel_->ContainsIndex(index)) 896 return NO; 897 return tabStripModel_->IsContextMenuCommandEnabled(index, command) ? YES : NO; 898} 899 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); 905} 906 907// Returns a weak reference to the controller that manages dragging of tabs. 908- (id<TabDraggingEventTarget>)dragController { 909 return dragController_.get(); 910} 911 912- (void)insertPlaceholderForTab:(TabView*)tab frame:(NSRect)frame { 913 placeholderTab_ = tab; 914 placeholderFrame_ = frame; 915 [self layoutTabsWithAnimation:initialLayoutComplete_ regenerateSubviews:NO]; 916} 917 918- (BOOL)isDragSessionActive { 919 return placeholderTab_ != nil; 920} 921 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]); 927} 928 929- (void)showNewTabButton:(BOOL)show { 930 forceNewTabButtonHidden_ = show ? NO : YES; 931 if (forceNewTabButtonHidden_) 932 [newTabButton_ setHidden:YES]; 933} 934 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]) 945 return; 946 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]; 952 953 NSRect enclosingRect = NSZeroRect; 954 ScopedNSAnimationContextGroup mainAnimationGroup(animate); 955 mainAnimationGroup.SetCurrentContextDuration(kAnimationDuration); 956 957 // Update the current subviews and their z-order if requested. 958 if (doUpdate) 959 [self regenerateSubviewList]; 960 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_; 968 } else { 969 availableSpace = NSWidth([tabStripView_ frame]); 970 971 // Account for the width of the new tab button. 972 availableSpace -= NSWidth([newTabButton_ frame]) + kNewTabButtonOffset; 973 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]; 979 } 980 981 // Need to leave room for the left-side controls even in rapid closure mode. 982 availableSpace -= [self leftIndentForControls]; 983 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; 988 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); 995 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; 1005 1006 // Divide up the space between the non-mini-tabs. 1007 nonMiniTabWidth = availableSpaceForNonMini / numberOfOpenNonMiniTabs; 1008 1009 // Clamp the width between the max and min. 1010 nonMiniTabWidth = MAX(MIN(nonMiniTabWidth, kMaxTabWidth), kMinTabWidth); 1011 1012 // Separate integral and fractional parts. 1013 CGFloat integralPart = std::floor(nonMiniTabWidth); 1014 nonMiniTabWidthFraction = nonMiniTabWidth - integralPart; 1015 nonMiniTabWidth = integralPart; 1016 } 1017 1018 BOOL visible = [[tabStripView_ window] isVisible]; 1019 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; 1026 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]; 1030 1031 for (TabController* tab in tabArray_.get()) { 1032 // Ignore a tab that is going through a close animation. 1033 if ([closingControllers_ containsObject:tab]) 1034 continue; 1035 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; 1041 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]; 1045 if (newTab) 1046 [[tab view] setHidden:NO]; 1047 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]; 1056 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] 1060 forKey:identifier]; 1061 continue; 1062 } 1063 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; 1073 } 1074 } 1075 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]; 1079 if (isMini) { 1080 tabFrame.size.width = [tab app] ? kAppTabWidth : kMiniTabWidth; 1081 } else { 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 1086 // algorithm. 1087 tabFrame.size.width = nonMiniTabWidth; 1088 tabWidthAccumulatedFraction += nonMiniTabWidthFraction; 1089 1090 if (tabWidthAccumulatedFraction >= 1.0) { 1091 ++tabFrame.size.width; 1092 --tabWidthAccumulatedFraction; 1093 } 1094 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; 1099 } 1100 1101 ++laidOutNonMiniTabs; 1102 } 1103 1104 if ([tab selected]) 1105 tabFrame.size.width = MAX(tabFrame.size.width, kMinSelectedTabWidth); 1106 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; 1112 } 1113 isLastTabMini = isMini; 1114 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))]; 1120 } else { 1121 [[tab view] setFrame:droppedTabFrame_]; 1122 droppedTabFrame_ = NSZeroRect; 1123 } 1124 } 1125 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] 1134 forKey:identifier]; 1135 } 1136 1137 enclosingRect = NSUnionRect(tabFrame, enclosingRect); 1138 1139 offset += NSWidth(tabFrame); 1140 offset -= kTabOverlap; 1141 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]; 1149 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]; 1157 1158 tabToolTipRect = NSOffsetRect(tabToolTipRect, insetWidth / 2.0, 0); 1159 [tabStripView_ addToolTipRect:tabToolTipRect owner:self userData:nil]; 1160 } 1161 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]; 1167 } else { 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]; 1176 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]; 1182 1183 BOOL shouldShowHover = [newTabButton_ pointIsOverButton:currentMouse]; 1184 [self setNewTabButtonHoverState:shouldShowHover]; 1185 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_); 1194 if (!movingLeft) { 1195 localAnimationGroup.SetCurrentContextShortestDuration(); 1196 } 1197 [[newTabButton_ animator] setFrame:newTabNewFrame]; 1198 newTabTargetFrame_ = newTabNewFrame; 1199 } else { 1200 [newTabButton_ setFrame:newTabNewFrame]; 1201 newTabTargetFrame_ = newTabNewFrame; 1202 } 1203 } 1204 } 1205 1206 [dragBlockingView_ setFrame:enclosingRect]; 1207 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]; 1211 1212 // Mark that we've successfully completed layout of at least one tab. 1213 initialLayoutComplete_ = YES; 1214} 1215 1216// Return the current hovered tab's tooltip when requested by the tooltip 1217// manager. 1218- (NSString*) view:(NSView*)view 1219 stringForToolTip:(NSToolTipTag)tag 1220 point:(NSPoint)point 1221 userData:(void*)data { 1222 return [hoveredTab_ toolTipText]; 1223} 1224 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]; 1229} 1230 1231- (void)layoutTabsWithoutAnimation { 1232 [self layoutTabsWithAnimation:NO regenerateSubviews:YES]; 1233} 1234 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; 1239 if (contents) 1240 title = contents->GetTitle(); 1241 if (title.empty()) 1242 title = l10n_util::GetStringUTF16(IDS_BROWSER_WINDOW_MAC_TAB_UNTITLED); 1243 [tab setTitle:base::SysUTF16ToNSString(title)]; 1244 1245 const base::string16& toolTip = chrome::AssembleTabTooltipText( 1246 title, chrome::GetTabMediaStateForContents(contents)); 1247 [tab setToolTip:base::SysUTF16ToNSString(toolTip)]; 1248} 1249 1250// Called when a notification is received from the model to insert a new tab 1251// at |modelIndex|. 1252- (void)insertTabWithContents:(content::WebContents*)contents 1253 atIndex:(NSInteger)modelIndex 1254 inForeground:(bool)inForeground { 1255 DCHECK(contents); 1256 DCHECK(modelIndex == TabStripModel::kNoTab || 1257 tabStripModel_->ContainsIndex(modelIndex)); 1258 1259 // Cancel any pending tab transition. 1260 hoverTabSelector_->CancelTabTransition(); 1261 1262 // Take closing tabs into account. 1263 NSInteger index = [self indexFromModelIndex:modelIndex]; 1264 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]; 1274 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]; 1283 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])]; 1289 1290 [self setTabTitle:newController withContents:contents]; 1291 1292 // If a tab is being inserted, we can again use the entire tab strip width 1293 // for layout. 1294 availableResizeWidth_ = kUseFullAvailableWidth; 1295 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) { 1300 [self layoutTabs]; 1301 } 1302 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 1307 // else. 1308 [self updateIconsForContents:contents atIndex:modelIndex]; 1309} 1310 1311// Called before |contents| is deactivated. 1312- (void)tabDeactivatedWithContents:(content::WebContents*)contents { 1313 contents->StoreFocus(); 1314} 1315 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. 1323 if (oldContents) { 1324 int oldModelIndex = 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(); 1332 } 1333 } 1334 1335 NSUInteger activeIndex = [self indexFromModelIndex:modelIndex]; 1336 1337 [tabArray_ enumerateObjectsUsingBlock:^(TabController* current, 1338 NSUInteger index, 1339 BOOL* stop) { 1340 [current setActive:index == activeIndex]; 1341 }]; 1342 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]; 1348 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. 1352 [self layoutTabs]; 1353 1354 // Swap in the contents for the new tab. 1355 [self swapInTabAtIndex:modelIndex]; 1356 1357 if (newContents) { 1358 newContents->WasShown(); 1359 newContents->RestoreFocus(); 1360 } 1361} 1362 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(); 1369 int i = 0; 1370 for (TabController* current in tabArray_.get()) { 1371 BOOL selected = iter != selection.end() && 1372 [self indexFromModelIndex:*iter] == i; 1373 [current setSelected:selected]; 1374 if (selected) 1375 ++iter; 1376 ++i; 1377 } 1378} 1379 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]); 1387 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]); 1397 1398 // Bye bye, |oldController|. 1399 [tabContentsArray_ replaceObjectAtIndex:index withObject:newController]; 1400 1401 // Fake a tab changed notification to force tab titles and favicons to update. 1402 [self tabChangedWithContents:newContents 1403 atIndex:modelIndex 1404 changeType:TabStripModelObserver::ALL]; 1405} 1406 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(); 1412 1413 NSUInteger index = [tabArray_ indexOfObject:controller]; 1414 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]; 1420 1421 // Remove the view from the tab strip. 1422 NSView* tab = [controller view]; 1423 [tab removeFromSuperview]; 1424 1425 // Remove ourself as an observer. 1426 [[NSNotificationCenter defaultCenter] 1427 removeObserver:self 1428 name:NSViewDidUpdateTrackingAreasNotification 1429 object:tab]; 1430 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]; 1435 1436 if ([hoveredTab_ isEqual:tab]) 1437 hoveredTab_ = nil; 1438 1439 NSValue* identifier = [NSValue valueWithPointer:tab]; 1440 [targetFrames_ removeObjectForKey:identifier]; 1441 1442 // Once we're totally done with the tab, delete its controller 1443 [tabArray_ removeObjectAtIndex:index]; 1444} 1445 1446// Called by the CAAnimation delegate when the tab completes the closing 1447// animation. 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]; 1454} 1455 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]); 1461 1462 // Cancel any pending tab transition. 1463 hoverTabSelector_->CancelTabTransition(); 1464 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]; 1469 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]; 1473 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]; 1486 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]; 1493} 1494 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 1497// in motion. 1498- (void)tabDetachedWithContents:(content::WebContents*)contents 1499 atIndex:(NSInteger)modelIndex { 1500 // Take closing tabs into account. 1501 NSInteger index = [self indexFromModelIndex:modelIndex]; 1502 1503 // Cancel any pending tab transition. 1504 hoverTabSelector_->CancelTabTransition(); 1505 1506 TabController* tab = [tabArray_ objectAtIndex:index]; 1507 if (tabStripModel_->count() > 0) { 1508 [self startClosingTabWithAnimation:tab]; 1509 [self layoutTabs]; 1510 } else { 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]; 1515 } 1516 1517 [delegate_ onTabDetachedWithContents:contents]; 1518} 1519 1520// A helper routine for creating an NSImageView to hold the favicon or app icon 1521// for |contents|. 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(); 1530 if (isApp) { 1531 SkBitmap* icon = extensions_tab_helper->GetExtensionAppIcon(); 1532 if (icon) 1533 image = gfx::SkBitmapToNSImageWithColorSpace(*icon, colorSpace); 1534 } else { 1535 image = mac::FaviconForWebContents(contents); 1536 } 1537 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. 1540 if (!image) 1541 image = defaultFavicon_.get(); 1542 1543 return image; 1544} 1545 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 { 1550 if (!contents) 1551 return; 1552 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(); 1562 1563 // Take closing tabs into account. 1564 NSInteger index = [self indexFromModelIndex:modelIndex]; 1565 TabController* tabController = [tabArray_ objectAtIndex:index]; 1566 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. 1572 1573 TabLoadingState oldState = [tabController loadingState]; 1574 TabLoadingState newState = kTabDone; 1575 NSImage* throbberImage = nil; 1576 if (contents->IsCrashed()) { 1577 newState = kTabCrashed; 1578 newHasIcon = true; 1579 } else if (contents->IsWaitingForResponse()) { 1580 newState = kTabWaiting; 1581 throbberImage = throbberWaitingImage; 1582 } else if (contents->IsLoading()) { 1583 newState = kTabLoading; 1584 throbberImage = throbberLoadingImage; 1585 } 1586 1587 if (oldState != newState) 1588 [tabController setLoadingState:newState]; 1589 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) { 1596 if (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]; 1607 } 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]; 1613 } else { 1614 [tabController setIconImage:throbberImage]; 1615 } 1616 } else { 1617 [tabController setIconImage:nil]; 1618 } 1619 } 1620} 1621 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]; 1630 1631 if (modelIndex == tabStripModel_->active_index()) 1632 [delegate_ onTabChanged:change withContents:contents]; 1633 1634 if (change == TabStripModelObserver::TITLE_NOT_LOADING) { 1635 // TODO(sky): make this work. 1636 // We'll receive another notification of the change asynchronously. 1637 return; 1638 } 1639 1640 TabController* tabController = [tabArray_ objectAtIndex:index]; 1641 1642 if (change != TabStripModelObserver::LOADING_ONLY) 1643 [self setTabTitle:tabController withContents:contents]; 1644 1645 [self updateIconsForContents:contents atIndex:modelIndex]; 1646 1647 TabContentsController* updatedController = 1648 [tabContentsArray_ objectAtIndex:index]; 1649 [updatedController tabDidChange:contents]; 1650} 1651 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]; 1661 1662 // Cancel any pending tab transition. 1663 hoverTabSelector_->CancelTabTransition(); 1664 1665 base::scoped_nsobject<TabContentsController> movedTabContentsController( 1666 [[tabContentsArray_ objectAtIndex:from] retain]); 1667 [tabContentsArray_ removeObjectAtIndex:from]; 1668 [tabContentsArray_ insertObject:movedTabContentsController.get() 1669 atIndex:to]; 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]; 1675 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]; 1679 1680 [self layoutTabs]; 1681} 1682 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]; 1688 1689 TabController* tabController = [tabArray_ objectAtIndex:index]; 1690 DCHECK([tabController isKindOfClass:[TabController class]]); 1691 1692 // Don't do anything if the change was already picked up by the move event. 1693 if (tabStripModel_->IsMiniTab(modelIndex) == [tabController mini]) 1694 return; 1695 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. 1704 [self layoutTabs]; 1705} 1706 1707- (void)setFrame:(NSRect)frame ofTabView:(NSView*)view { 1708 NSValue* identifier = [NSValue valueWithPointer:view]; 1709 [targetFrames_ setObject:[NSValue valueWithRect:frame] 1710 forKey:identifier]; 1711 [view setFrame:frame]; 1712} 1713 1714- (TabStripModel*)tabStripModel { 1715 return tabStripModel_; 1716} 1717 1718- (NSArray*)tabViews { 1719 NSMutableArray* views = [NSMutableArray arrayWithCapacity:[tabArray_ count]]; 1720 for (TabController* tab in tabArray_.get()) { 1721 [views addObject:[tab tabView]]; 1722 } 1723 return views; 1724} 1725 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]; 1731} 1732 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]; 1737 1738 // No placeholder, return the end of the strip. 1739 if (placeholderTab_ == nil) 1740 return count; 1741 1742 double placeholderX = placeholderFrame_.origin.x; 1743 int index = 0; 1744 int location = 0; 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]]) { 1751 index++; 1752 continue; 1753 } 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_) { 1760 index++; 1761 continue; 1762 } 1763 if (placeholderX <= NSMinX([curr frame])) 1764 break; 1765 index++; 1766 location++; 1767 } 1768 return location; 1769} 1770 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); 1778} 1779 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; 1797 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( 1801 modelIndex, 1802 contents, 1803 (activate ? TabStripModel::ADD_ACTIVE : TabStripModel::ADD_NONE) | 1804 (pinned ? TabStripModel::ADD_PINNED : TabStripModel::ADD_NONE)); 1805} 1806 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]; 1814} 1815 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 1826 location:location 1827 modifierFlags:0 1828 timestamp:0 1829 windowNumber:[window windowNumber] 1830 context:nil 1831 eventNumber:0 1832 clickCount:0 1833 pressure:0]; 1834 [self mouseMoved:mouseEvent]; 1835 } 1836} 1837 1838- (BOOL)inRapidClosureMode { 1839 return availableResizeWidth_ != kUseFullAvailableWidth; 1840} 1841 1842// Disable tab dragging when there are any pending animations. 1843- (BOOL)tabDraggingAllowed { 1844 return [closingControllers_ count] == 0; 1845} 1846 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]]; 1850 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]; 1854 1855 TabView* tabView = (TabView*)targetView; 1856 if (![tabView isKindOfClass:[TabView class]]) { 1857 if ([[tabView superview] isKindOfClass:[TabView class]]) { 1858 tabView = (TabView*)[targetView superview]; 1859 } else { 1860 tabView = nil; 1861 } 1862 } 1863 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; 1868 } else { 1869 [hoveredTab_ mouseMoved:event]; 1870 } 1871} 1872 1873- (void)mouseEntered:(NSEvent*)event { 1874 NSTrackingArea* area = [event trackingArea]; 1875 if ([area isEqual:trackingArea_]) { 1876 mouseInside_ = YES; 1877 [self setTabTrackingAreasEnabled:YES]; 1878 [self mouseMoved:event]; 1879 } 1880} 1881 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_]) { 1888 mouseInside_ = NO; 1889 [self setTabTrackingAreasEnabled:NO]; 1890 availableResizeWidth_ = kUseFullAvailableWidth; 1891 [hoveredTab_ mouseExited:event]; 1892 hoveredTab_ = nil; 1893 [self layoutTabs]; 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]; 1900 } 1901} 1902 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]; 1909 if (enabled) { 1910 // Set self up to observe tabs so hover states will be correct. 1911 [defaultCenter addObserver:self 1912 selector:@selector(tabUpdateTracking:) 1913 name:NSViewDidUpdateTrackingAreasNotification 1914 object:tabView]; 1915 } else { 1916 [defaultCenter removeObserver:self 1917 name:NSViewDidUpdateTrackingAreasNotification 1918 object:tabView]; 1919 } 1920 [tabView setTrackingEnabled:enabled]; 1921 } 1922} 1923 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]; 1933 } 1934} 1935 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 { 1941 if (aView) 1942 [permanentSubviews_ addObject:aView]; 1943} 1944 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]; 1955 1956 // Subviews to put in (in bottom-to-top order), beginning with the permanent 1957 // ones. 1958 NSMutableArray* subviews = [NSMutableArray arrayWithArray:permanentSubviews_]; 1959 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]; 1964 if ([tab active]) { 1965 DCHECK(!activeTabView); 1966 activeTabView = tabView; 1967 } else { 1968 [subviews addObject:tabView]; 1969 } 1970 } 1971 if (activeTabView) { 1972 [subviews addObject:activeTabView]; 1973 } 1974 WithNoAnimation noAnimation; 1975 [tabStripView_ setSubviews:subviews]; 1976 [self setTabTrackingAreasEnabled:mouseInside_]; 1977} 1978 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; 1990 1991 DCHECK(index && disposition); 1992 NSInteger i = 0; 1993 for (TabController* tab in tabArray_.get()) { 1994 NSView* view = [tab view]; 1995 DCHECK([view isKindOfClass:[TabView class]]); 1996 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]; 2001 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 2007 2008 // Drop in a new tab to the left of tab |i|? 2009 if (point.x < (frame.origin.x + kLRProportion * frame.size.width)) { 2010 *index = i; 2011 *disposition = NEW_FOREGROUND_TAB; 2012 return; 2013 } 2014 2015 // Drop on tab |i|? 2016 if (point.x <= (frame.origin.x + 2017 (1.0 - kLRProportion) * frame.size.width)) { 2018 *index = i; 2019 *disposition = CURRENT_TAB; 2020 return; 2021 } 2022 2023 // (Dropping in a new tab to the right of tab |i| will be taken care of in 2024 // the next iteration.) 2025 i++; 2026 } 2027 2028 // If we've made it here, we want to append a new tab to the end. 2029 *index = -1; 2030 *disposition = NEW_FOREGROUND_TAB; 2031} 2032 2033- (void)openURL:(GURL*)url inView:(NSView*)view at:(NSPoint)point { 2034 // Get the index and disposition. 2035 NSInteger index; 2036 WindowOpenDisposition disposition; 2037 [self droppingURLsAt:point 2038 givesIndex:&index 2039 disposition:&disposition]; 2040 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); 2052 break; 2053 } 2054 case CURRENT_TAB: { 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); 2060 break; 2061 } 2062 default: 2063 NOTIMPLEMENTED(); 2064 } 2065} 2066 2067// (URLDropTargetController protocol) 2068- (void)dropURLs:(NSArray*)urls inView:(NSView*)view at:(NSPoint)point { 2069 DCHECK_EQ(view, tabStripView_.get()); 2070 2071 if ([urls count] < 1) { 2072 NOTREACHED(); 2073 return; 2074 } 2075 2076 //TODO(viettrungluu): dropping multiple URLs. 2077 if ([urls count] > 1) 2078 NOTIMPLEMENTED(); 2079 2080 // Get the first URL and fix it up. 2081 GURL url(GURL(URLFixerUpper::FixupURL( 2082 base::SysNSStringToUTF8([urls objectAtIndex:0]), std::string()))); 2083 2084 [self openURL:&url inView:view at:point]; 2085} 2086 2087// (URLDropTargetController protocol) 2088- (void)dropText:(NSString*)text inView:(NSView*)view at:(NSPoint)point { 2089 DCHECK_EQ(view, tabStripView_.get()); 2090 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, 2095 &match, NULL); 2096 GURL url(match.destination_url); 2097 2098 [self openURL:&url inView:view at:point]; 2099} 2100 2101// (URLDropTargetController protocol) 2102- (void)indicateDropURLsInView:(NSView*)view at:(NSPoint)point { 2103 DCHECK_EQ(view, tabStripView_.get()); 2104 2105 // The minimum y-coordinate at which one should consider place the arrow. 2106 const CGFloat arrowBaseY = 25; 2107 2108 NSInteger index; 2109 WindowOpenDisposition disposition; 2110 [self droppingURLsAt:point 2111 givesIndex:&index 2112 disposition:&disposition]; 2113 2114 NSPoint arrowPos = NSMakePoint(0, arrowBaseY); 2115 if (index == -1) { 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; 2121 } else { 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; 2127 break; 2128 case CURRENT_TAB: 2129 // Overwrite the given tab. 2130 arrowPos.x = overRect.origin.x + overRect.size.width / 2.0; 2131 break; 2132 default: 2133 NOTREACHED(); 2134 } 2135 } 2136 2137 [tabStripView_ setDropArrowPosition:arrowPos]; 2138 [tabStripView_ setDropArrowShown:YES]; 2139 [tabStripView_ setNeedsDisplay:YES]; 2140 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); 2148 return; 2149 } 2150 } 2151 // If a tab transition was not started, cancel the pending one. 2152 hoverTabSelector_->CancelTabTransition(); 2153} 2154 2155// (URLDropTargetController protocol) 2156- (void)hideDropURLsIndicatorInView:(NSView*)view { 2157 DCHECK_EQ(view, tabStripView_.get()); 2158 2159 // Cancel any pending tab transition. 2160 hoverTabSelector_->CancelTabTransition(); 2161 2162 if ([tabStripView_ dropArrowShown]) { 2163 [tabStripView_ setDropArrowShown:NO]; 2164 [tabStripView_ setNeedsDisplay:YES]; 2165 } 2166} 2167 2168// (URLDropTargetController protocol) 2169- (BOOL)isUnsupportedDropData:(id<NSDraggingInfo>)info { 2170 return drag_util::IsUnsupportedDropData(browser_->profile(), info); 2171} 2172 2173- (TabContentsController*)activeTabContentsController { 2174 int modelIndex = tabStripModel_->active_index(); 2175 if (modelIndex < 0) 2176 return nil; 2177 NSInteger index = [self indexFromModelIndex:modelIndex]; 2178 if (index < 0 || 2179 index >= (NSInteger)[tabContentsArray_ count]) 2180 return nil; 2181 return [tabContentsArray_ objectAtIndex:index]; 2182} 2183 2184- (void)themeDidChangeNotification:(NSNotification*)notification { 2185 [self setNewTabImages]; 2186} 2187 2188- (void)setNewTabImages { 2189 ThemeService *theme = 2190 static_cast<ThemeService*>([[tabStripView_ window] themeProvider]); 2191 if (!theme) 2192 return; 2193 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(); 2199 2200 NSImage* foreground = ApplyMask( 2201 theme->GetNSImageNamed(IDR_THEME_TAB_BACKGROUND), mask); 2202 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]; 2209 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]; 2219 } else { 2220 [[newTabButton_ cell] setImage:nil 2221 forButtonState:image_button_cell::kDefaultStateBackground]; 2222 [[newTabButton_ cell] setImage:nil 2223 forButtonState:image_button_cell::kHoverStateBackground]; 2224 } 2225} 2226 2227@end 2228 2229NSView* 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 2234 // 2235 // Changing it? Do not forget to modify 2236 // -[TabStripController swapInTabAtIndex:] too. 2237 return [web_contents->GetNativeView() superview]; 2238} 2239