1// Copyright (c) 2011 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h" 6 7#import <QuartzCore/QuartzCore.h> 8 9#include <limits> 10#include <string> 11 12#include "app/mac/nsimage_cache.h" 13#include "base/command_line.h" 14#include "base/mac/mac_util.h" 15#include "base/sys_string_conversions.h" 16#include "chrome/app/chrome_command_ids.h" 17#include "chrome/browser/autocomplete/autocomplete.h" 18#include "chrome/browser/autocomplete/autocomplete_classifier.h" 19#include "chrome/browser/autocomplete/autocomplete_match.h" 20#include "chrome/browser/extensions/extension_tab_helper.h" 21#include "chrome/browser/metrics/user_metrics.h" 22#include "chrome/browser/prefs/pref_service.h" 23#include "chrome/browser/profiles/profile.h" 24#include "chrome/browser/debugger/devtools_window.h" 25#include "chrome/browser/net/url_fixer_upper.h" 26#include "chrome/browser/sidebar/sidebar_container.h" 27#include "chrome/browser/sidebar/sidebar_manager.h" 28#include "chrome/browser/tabs/tab_strip_model.h" 29#include "chrome/browser/ui/browser.h" 30#include "chrome/browser/ui/browser_navigator.h" 31#include "chrome/browser/ui/find_bar/find_tab_helper.h" 32#import "chrome/browser/ui/cocoa/browser_window_controller.h" 33#import "chrome/browser/ui/cocoa/constrained_window_mac.h" 34#import "chrome/browser/ui/cocoa/new_tab_button.h" 35#import "chrome/browser/ui/cocoa/profile_menu_button.h" 36#import "chrome/browser/ui/cocoa/tab_contents/favicon_util.h" 37#import "chrome/browser/ui/cocoa/tabs/tab_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#import "chrome/browser/ui/cocoa/tabs/throbber_view.h" 42#import "chrome/browser/ui/cocoa/tracking_area.h" 43#include "chrome/browser/ui/find_bar/find_bar.h" 44#include "chrome/browser/ui/find_bar/find_bar_controller.h" 45#include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h" 46#include "chrome/common/chrome_switches.h" 47#include "chrome/common/pref_names.h" 48#include "content/browser/tab_contents/navigation_controller.h" 49#include "content/browser/tab_contents/navigation_entry.h" 50#include "content/browser/tab_contents/tab_contents.h" 51#include "content/browser/tab_contents/tab_contents_view.h" 52#include "content/common/notification_service.h" 53#include "grit/app_resources.h" 54#include "grit/generated_resources.h" 55#include "grit/theme_resources.h" 56#include "skia/ext/skia_utils_mac.h" 57#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h" 58#include "ui/base/l10n/l10n_util.h" 59#include "ui/base/resource/resource_bundle.h" 60#include "ui/gfx/image.h" 61 62NSString* const kTabStripNumberOfTabsChanged = @"kTabStripNumberOfTabsChanged"; 63 64// 10.7 adds public APIs for full-screen support. Provide the declaration so it 65// can be called below when building with the 10.5 SDK. 66#if !defined(MAC_OS_X_VERSION_10_7) || \ 67MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_7 68 69@interface NSWindow (LionSDKDeclarations) 70- (void)toggleFullScreen:(id)sender; 71@end 72 73enum { 74 NSWindowFullScreenButton = 7 75}; 76 77#endif // MAC_OS_X_VERSION_10_7 78 79namespace { 80 81// The images names used for different states of the new tab button. 82NSString* const kNewTabHoverImage = @"newtab_h.pdf"; 83NSString* const kNewTabImage = @"newtab.pdf"; 84NSString* const kNewTabPressedImage = @"newtab_p.pdf"; 85 86// A value to indicate tab layout should use the full available width of the 87// view. 88const CGFloat kUseFullAvailableWidth = -1.0; 89 90// The amount by which tabs overlap. 91const CGFloat kTabOverlap = 20.0; 92 93// The width and height for a tab's icon. 94const CGFloat kIconWidthAndHeight = 16.0; 95 96// The amount by which the new tab button is offset (from the tabs). 97const CGFloat kNewTabButtonOffset = 8.0; 98 99// The amount by which to shrink the tab strip (on the right) when the 100// incognito badge is present. 101const CGFloat kIncognitoBadgeTabStripShrink = 18; 102 103// Time (in seconds) in which tabs animate to their final position. 104const NSTimeInterval kAnimationDuration = 0.125; 105 106// The amount by wich the profile menu button is offset (from tab tabs or new 107// tab button). 108const CGFloat kProfileMenuButtonOffset = 6.0; 109 110// Helper class for doing NSAnimationContext calls that takes a bool to disable 111// all the work. Useful for code that wants to conditionally animate. 112class ScopedNSAnimationContextGroup { 113 public: 114 explicit ScopedNSAnimationContextGroup(bool animate) 115 : animate_(animate) { 116 if (animate_) { 117 [NSAnimationContext beginGrouping]; 118 } 119 } 120 121 ~ScopedNSAnimationContextGroup() { 122 if (animate_) { 123 [NSAnimationContext endGrouping]; 124 } 125 } 126 127 void SetCurrentContextDuration(NSTimeInterval duration) { 128 if (animate_) { 129 [[NSAnimationContext currentContext] gtm_setDuration:duration 130 eventMask:NSLeftMouseUpMask]; 131 } 132 } 133 134 void SetCurrentContextShortestDuration() { 135 if (animate_) { 136 // The minimum representable time interval. This used to stop an 137 // in-progress animation as quickly as possible. 138 const NSTimeInterval kMinimumTimeInterval = 139 std::numeric_limits<NSTimeInterval>::min(); 140 // Directly set the duration to be short, avoiding the Steve slowmotion 141 // ettect the gtm_setDuration: provides. 142 [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval]; 143 } 144 } 145 146private: 147 bool animate_; 148 DISALLOW_COPY_AND_ASSIGN(ScopedNSAnimationContextGroup); 149}; 150 151} // namespace 152 153@interface TabStripController (Private) 154- (void)addSubviewToPermanentList:(NSView*)aView; 155- (void)regenerateSubviewList; 156- (NSInteger)indexForContentsView:(NSView*)view; 157- (void)updateFaviconForContents:(TabContents*)contents 158 atIndex:(NSInteger)modelIndex; 159- (void)layoutTabsWithAnimation:(BOOL)animate 160 regenerateSubviews:(BOOL)doUpdate; 161- (void)animationDidStopForController:(TabController*)controller 162 finished:(BOOL)finished; 163- (NSInteger)indexFromModelIndex:(NSInteger)index; 164- (NSInteger)numberOfOpenTabs; 165- (NSInteger)numberOfOpenMiniTabs; 166- (NSInteger)numberOfOpenNonMiniTabs; 167- (void)mouseMoved:(NSEvent*)event; 168- (void)setTabTrackingAreasEnabled:(BOOL)enabled; 169- (void)droppingURLsAt:(NSPoint)point 170 givesIndex:(NSInteger*)index 171 disposition:(WindowOpenDisposition*)disposition; 172- (void)setNewTabButtonHoverState:(BOOL)showHover; 173- (BOOL)shouldShowProfileMenuButton; 174- (void)updateProfileMenuButton; 175@end 176 177// A simple view class that prevents the Window Server from dragging the area 178// behind tabs. Sometimes core animation confuses it. Unfortunately, it can also 179// falsely pick up clicks during rapid tab closure, so we have to account for 180// that. 181@interface TabStripControllerDragBlockingView : NSView { 182 TabStripController* controller_; // weak; owns us 183} 184 185- (id)initWithFrame:(NSRect)frameRect 186 controller:(TabStripController*)controller; 187@end 188 189@implementation TabStripControllerDragBlockingView 190- (BOOL)mouseDownCanMoveWindow {return NO;} 191- (void)drawRect:(NSRect)rect {} 192 193- (id)initWithFrame:(NSRect)frameRect 194 controller:(TabStripController*)controller { 195 if ((self = [super initWithFrame:frameRect])) 196 controller_ = controller; 197 return self; 198} 199 200// In "rapid tab closure" mode (i.e., the user is clicking close tab buttons in 201// rapid succession), the animations confuse Cocoa's hit testing (which appears 202// to use cached results, among other tricks), so this view can somehow end up 203// getting a mouse down event. Thus we do an explicit hit test during rapid tab 204// closure, and if we find that we got a mouse down we shouldn't have, we send 205// it off to the appropriate view. 206- (void)mouseDown:(NSEvent*)event { 207 if ([controller_ inRapidClosureMode]) { 208 NSView* superview = [self superview]; 209 NSPoint hitLocation = 210 [[superview superview] convertPoint:[event locationInWindow] 211 fromView:nil]; 212 NSView* hitView = [superview hitTest:hitLocation]; 213 if (hitView != self) { 214 [hitView mouseDown:event]; 215 return; 216 } 217 } 218 [super mouseDown:event]; 219} 220@end 221 222#pragma mark - 223 224// A delegate, owned by the CAAnimation system, that is alerted when the 225// animation to close a tab is completed. Calls back to the given tab strip 226// to let it know that |controller_| is ready to be removed from the model. 227// Since we only maintain weak references, the tab strip must call -invalidate: 228// to prevent the use of dangling pointers. 229@interface TabCloseAnimationDelegate : NSObject { 230 @private 231 TabStripController* strip_; // weak; owns us indirectly 232 TabController* controller_; // weak 233} 234 235// Will tell |strip| when the animation for |controller|'s view has completed. 236// These should not be nil, and will not be retained. 237- (id)initWithTabStrip:(TabStripController*)strip 238 tabController:(TabController*)controller; 239 240// Invalidates this object so that no further calls will be made to 241// |strip_|. This should be called when |strip_| is released, to 242// prevent attempts to call into the released object. 243- (void)invalidate; 244 245// CAAnimation delegate method 246- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished; 247 248@end 249 250@implementation TabCloseAnimationDelegate 251 252- (id)initWithTabStrip:(TabStripController*)strip 253 tabController:(TabController*)controller { 254 if ((self = [super init])) { 255 DCHECK(strip && controller); 256 strip_ = strip; 257 controller_ = controller; 258 } 259 return self; 260} 261 262- (void)invalidate { 263 strip_ = nil; 264 controller_ = nil; 265} 266 267- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished { 268 [strip_ animationDidStopForController:controller_ finished:finished]; 269} 270 271@end 272 273namespace TabStripControllerInternal { 274 275// Bridges C++ notifications back to the TabStripController. 276class NotificationBridge : public NotificationObserver { 277 public: 278 explicit NotificationBridge(TabStripController* controller, 279 PrefService* prefService) 280 : controller_(controller) { 281 DCHECK(prefService); 282 usernamePref_.Init(prefs::kGoogleServicesUsername, prefService, this); 283 } 284 285 // Overridden from NotificationObserver: 286 virtual void Observe(NotificationType type, 287 const NotificationSource& source, 288 const NotificationDetails& details) { 289 DCHECK_EQ(NotificationType::PREF_CHANGED, type.value); 290 std::string* name = Details<std::string>(details).ptr(); 291 if (prefs::kGoogleServicesUsername == *name) { 292 [controller_ updateProfileMenuButton]; 293 [controller_ layoutTabsWithAnimation:NO regenerateSubviews:NO]; 294 } 295 } 296 297 private: 298 TabStripController* controller_; // weak, owns us 299 300 // The Google services user name associated with this BrowserView's profile. 301 StringPrefMember usernamePref_; 302}; 303 304} // namespace TabStripControllerInternal 305 306#pragma mark - 307 308// In general, there is a one-to-one correspondence between TabControllers, 309// TabViews, TabContentsControllers, and the TabContents in the TabStripModel. 310// In the steady-state, the indices line up so an index coming from the model 311// is directly mapped to the same index in the parallel arrays holding our 312// views and controllers. This is also true when new tabs are created (even 313// though there is a small period of animation) because the tab is present 314// in the model while the TabView is animating into place. As a result, nothing 315// special need be done to handle "new tab" animation. 316// 317// This all goes out the window with the "close tab" animation. The animation 318// kicks off in |-tabDetachedWithContents:atIndex:| with the notification that 319// the tab has been removed from the model. The simplest solution at this 320// point would be to remove the views and controllers as well, however once 321// the TabView is removed from the view list, the tab z-order code takes care of 322// removing it from the tab strip and we'll get no animation. That means if 323// there is to be any visible animation, the TabView needs to stay around until 324// its animation is complete. In order to maintain consistency among the 325// internal parallel arrays, this means all structures are kept around until 326// the animation completes. At this point, though, the model and our internal 327// structures are out of sync: the indices no longer line up. As a result, 328// there is a concept of a "model index" which represents an index valid in 329// the TabStripModel. During steady-state, the "model index" is just the same 330// index as our parallel arrays (as above), but during tab close animations, 331// it is different, offset by the number of tabs preceding the index which 332// are undergoing tab closing animation. As a result, the caller needs to be 333// careful to use the available conversion routines when accessing the internal 334// parallel arrays (e.g., -indexFromModelIndex:). Care also needs to be taken 335// during tab layout to ignore closing tabs in the total width calculations and 336// in individual tab positioning (to avoid moving them right back to where they 337// were). 338// 339// In order to prevent actions being taken on tabs which are closing, the tab 340// itself gets marked as such so it no longer will send back its select action 341// or allow itself to be dragged. In addition, drags on the tab strip as a 342// whole are disabled while there are tabs closing. 343 344@implementation TabStripController 345 346@synthesize indentForControls = indentForControls_; 347 348- (id)initWithView:(TabStripView*)view 349 switchView:(NSView*)switchView 350 browser:(Browser*)browser 351 delegate:(id<TabStripControllerDelegate>)delegate { 352 DCHECK(view && switchView && browser && delegate); 353 if ((self = [super init])) { 354 tabStripView_.reset([view retain]); 355 switchView_ = switchView; 356 browser_ = browser; 357 tabStripModel_ = browser_->tabstrip_model(); 358 delegate_ = delegate; 359 bridge_.reset(new TabStripModelObserverBridge(tabStripModel_, self)); 360 tabContentsArray_.reset([[NSMutableArray alloc] init]); 361 tabArray_.reset([[NSMutableArray alloc] init]); 362 NSWindow* browserWindow = [view window]; 363 364 // Important note: any non-tab subviews not added to |permanentSubviews_| 365 // (see |-addSubviewToPermanentList:|) will be wiped out. 366 permanentSubviews_.reset([[NSMutableArray alloc] init]); 367 368 defaultFavicon_.reset( 369 [app::mac::GetCachedImageWithName(@"nav.pdf") retain]); 370 371 [self setIndentForControls:[[self class] defaultIndentForControls]]; 372 373 // TODO(viettrungluu): WTF? "For some reason, if the view is present in the 374 // nib a priori, it draws correctly. If we create it in code and add it to 375 // the tab view, it draws with all sorts of crazy artifacts." 376 newTabButton_ = [view newTabButton]; 377 [self addSubviewToPermanentList:newTabButton_]; 378 [newTabButton_ setTarget:nil]; 379 [newTabButton_ setAction:@selector(commandDispatch:)]; 380 [newTabButton_ setTag:IDC_NEW_TAB]; 381 382 profileMenuButton_ = [view profileMenuButton]; 383 [self addSubviewToPermanentList:profileMenuButton_]; 384 [self updateProfileMenuButton]; 385 // Register pref observers for profile name. 386 notificationBridge_.reset( 387 new TabStripControllerInternal::NotificationBridge( 388 self, browser_->profile()->GetPrefs())); 389 390 // Set the images from code because Cocoa fails to find them in our sub 391 // bundle during tests. 392 [newTabButton_ setImage:app::mac::GetCachedImageWithName(kNewTabImage)]; 393 [newTabButton_ setAlternateImage: 394 app::mac::GetCachedImageWithName(kNewTabPressedImage)]; 395 newTabButtonShowingHoverImage_ = NO; 396 newTabTrackingArea_.reset( 397 [[CrTrackingArea alloc] initWithRect:[newTabButton_ bounds] 398 options:(NSTrackingMouseEnteredAndExited | 399 NSTrackingActiveAlways) 400 proxiedOwner:self 401 userInfo:nil]); 402 if (browserWindow) // Nil for Browsers without a tab strip (e.g. popups). 403 [newTabTrackingArea_ clearOwnerWhenWindowWillClose:browserWindow]; 404 [newTabButton_ addTrackingArea:newTabTrackingArea_.get()]; 405 targetFrames_.reset([[NSMutableDictionary alloc] init]); 406 407 dragBlockingView_.reset( 408 [[TabStripControllerDragBlockingView alloc] initWithFrame:NSZeroRect 409 controller:self]); 410 [self addSubviewToPermanentList:dragBlockingView_]; 411 412 newTabTargetFrame_ = NSMakeRect(0, 0, 0, 0); 413 availableResizeWidth_ = kUseFullAvailableWidth; 414 415 closingControllers_.reset([[NSMutableSet alloc] init]); 416 417 // Install the permanent subviews. 418 [self regenerateSubviewList]; 419 420 // Watch for notifications that the tab strip view has changed size so 421 // we can tell it to layout for the new size. 422 [[NSNotificationCenter defaultCenter] 423 addObserver:self 424 selector:@selector(tabViewFrameChanged:) 425 name:NSViewFrameDidChangeNotification 426 object:tabStripView_]; 427 428 trackingArea_.reset([[CrTrackingArea alloc] 429 initWithRect:NSZeroRect // Ignored by NSTrackingInVisibleRect 430 options:NSTrackingMouseEnteredAndExited | 431 NSTrackingMouseMoved | 432 NSTrackingActiveAlways | 433 NSTrackingInVisibleRect 434 proxiedOwner:self 435 userInfo:nil]); 436 if (browserWindow) // Nil for Browsers without a tab strip (e.g. popups). 437 [trackingArea_ clearOwnerWhenWindowWillClose:browserWindow]; 438 [tabStripView_ addTrackingArea:trackingArea_.get()]; 439 440 // Check to see if the mouse is currently in our bounds so we can 441 // enable the tracking areas. Otherwise we won't get hover states 442 // or tab gradients if we load the window up under the mouse. 443 NSPoint mouseLoc = [[view window] mouseLocationOutsideOfEventStream]; 444 mouseLoc = [view convertPoint:mouseLoc fromView:nil]; 445 if (NSPointInRect(mouseLoc, [view bounds])) { 446 [self setTabTrackingAreasEnabled:YES]; 447 mouseInside_ = YES; 448 } 449 450 // Set accessibility descriptions. http://openradar.appspot.com/7496255 451 NSString* description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_NEWTAB); 452 [[newTabButton_ cell] 453 accessibilitySetOverrideValue:description 454 forAttribute:NSAccessibilityDescriptionAttribute]; 455 456 // Controller may have been (re-)created by switching layout modes, which 457 // means the tab model is already fully formed with tabs. Need to walk the 458 // list and create the UI for each. 459 const int existingTabCount = tabStripModel_->count(); 460 const TabContentsWrapper* selection = 461 tabStripModel_->GetSelectedTabContents(); 462 for (int i = 0; i < existingTabCount; ++i) { 463 TabContentsWrapper* currentContents = tabStripModel_->GetTabContentsAt(i); 464 [self insertTabWithContents:currentContents 465 atIndex:i 466 inForeground:NO]; 467 if (selection == currentContents) { 468 // Must manually force a selection since the model won't send 469 // selection messages in this scenario. 470 [self selectTabWithContents:currentContents 471 previousContents:NULL 472 atIndex:i 473 userGesture:NO]; 474 } 475 } 476 // Don't lay out the tabs until after the controller has been fully 477 // constructed. The |verticalLayout_| flag has not been initialized by 478 // subclasses at this point, which would cause layout to potentially use 479 // the wrong mode. 480 if (existingTabCount) { 481 [self performSelectorOnMainThread:@selector(layoutTabs) 482 withObject:nil 483 waitUntilDone:NO]; 484 } 485 } 486 return self; 487} 488 489- (void)dealloc { 490 if (trackingArea_.get()) 491 [tabStripView_ removeTrackingArea:trackingArea_.get()]; 492 493 [newTabButton_ removeTrackingArea:newTabTrackingArea_.get()]; 494 // Invalidate all closing animations so they don't call back to us after 495 // we're gone. 496 for (TabController* controller in closingControllers_.get()) { 497 NSView* view = [controller view]; 498 [[[view animationForKey:@"frameOrigin"] delegate] invalidate]; 499 } 500 [[NSNotificationCenter defaultCenter] removeObserver:self]; 501 [super dealloc]; 502} 503 504+ (CGFloat)defaultTabHeight { 505 return 25.0; 506} 507 508+ (CGFloat)defaultIndentForControls { 509 // Default indentation leaves enough room so tabs don't overlap with the 510 // window controls. 511 return 70.0; 512} 513 514// Finds the TabContentsController associated with the given index into the tab 515// model and swaps out the sole child of the contentArea to display its 516// contents. 517- (void)swapInTabAtIndex:(NSInteger)modelIndex { 518 DCHECK(modelIndex >= 0 && modelIndex < tabStripModel_->count()); 519 NSInteger index = [self indexFromModelIndex:modelIndex]; 520 TabContentsController* controller = [tabContentsArray_ objectAtIndex:index]; 521 522 // Resize the new view to fit the window. Calling |view| may lazily 523 // instantiate the TabContentsController from the nib. Until we call 524 // |-ensureContentsVisible|, the controller doesn't install the RWHVMac into 525 // the view hierarchy. This is in order to avoid sending the renderer a 526 // spurious default size loaded from the nib during the call to |-view|. 527 NSView* newView = [controller view]; 528 529 // Turns content autoresizing off, so removing and inserting views won't 530 // trigger unnecessary content relayout. 531 [controller ensureContentsSizeDoesNotChange]; 532 533 // Remove the old view from the view hierarchy. We know there's only one 534 // child of |switchView_| because we're the one who put it there. There 535 // may not be any children in the case of a tab that's been closed, in 536 // which case there's no swapping going on. 537 NSArray* subviews = [switchView_ subviews]; 538 if ([subviews count]) { 539 NSView* oldView = [subviews objectAtIndex:0]; 540 // Set newView frame to the oldVew frame to prevent NSSplitView hosting 541 // sidebar and tab content from resizing sidebar's content view. 542 // ensureContentsVisible (see below) sets content size and autoresizing 543 // properties. 544 [newView setFrame:[oldView frame]]; 545 [switchView_ replaceSubview:oldView with:newView]; 546 } else { 547 [newView setFrame:[switchView_ bounds]]; 548 [switchView_ addSubview:newView]; 549 } 550 551 // New content is in place, delegate should adjust itself accordingly. 552 [delegate_ onSelectTabWithContents:[controller tabContents]]; 553 554 // It also restores content autoresizing properties. 555 [controller ensureContentsVisible]; 556 557 // Tell per-tab sheet manager about currently selected tab. 558 if (sheetController_.get()) { 559 [sheetController_ setActiveView:newView]; 560 } 561 562 // Make sure the new tabs's sheets are visible (necessary when a background 563 // tab opened a sheet while it was in the background and now becomes active). 564 TabContentsWrapper* newTab = tabStripModel_->GetTabContentsAt(modelIndex); 565 DCHECK(newTab); 566 if (newTab) { 567 TabContents::ConstrainedWindowList::iterator it, end; 568 end = newTab->tab_contents()->constrained_window_end(); 569 NSWindowController* controller = [[newView window] windowController]; 570 DCHECK([controller isKindOfClass:[BrowserWindowController class]]); 571 572 for (it = newTab->tab_contents()->constrained_window_begin(); 573 it != end; 574 ++it) { 575 ConstrainedWindow* constrainedWindow = *it; 576 static_cast<ConstrainedWindowMac*>(constrainedWindow)->Realize( 577 static_cast<BrowserWindowController*>(controller)); 578 } 579 } 580} 581 582// Create a new tab view and set its cell correctly so it draws the way we want 583// it to. It will be sized and positioned by |-layoutTabs| so there's no need to 584// set the frame here. This also creates the view as hidden, it will be 585// shown during layout. 586- (TabController*)newTab { 587 TabController* controller = [[[TabController alloc] init] autorelease]; 588 [controller setTarget:self]; 589 [controller setAction:@selector(selectTab:)]; 590 [[controller view] setHidden:YES]; 591 592 return controller; 593} 594 595// (Private) Returns the number of open tabs in the tab strip. This is the 596// number of TabControllers we know about (as there's a 1-to-1 mapping from 597// these controllers to a tab) less the number of closing tabs. 598- (NSInteger)numberOfOpenTabs { 599 return static_cast<NSInteger>(tabStripModel_->count()); 600} 601 602// (Private) Returns the number of open, mini-tabs. 603- (NSInteger)numberOfOpenMiniTabs { 604 // Ask the model for the number of mini tabs. Note that tabs which are in 605 // the process of closing (i.e., whose controllers are in 606 // |closingControllers_|) have already been removed from the model. 607 return tabStripModel_->IndexOfFirstNonMiniTab(); 608} 609 610// (Private) Returns the number of open, non-mini tabs. 611- (NSInteger)numberOfOpenNonMiniTabs { 612 NSInteger number = [self numberOfOpenTabs] - [self numberOfOpenMiniTabs]; 613 DCHECK_GE(number, 0); 614 return number; 615} 616 617// Given an index into the tab model, returns the index into the tab controller 618// or tab contents controller array accounting for tabs that are currently 619// closing. For example, if there are two tabs in the process of closing before 620// |index|, this returns |index| + 2. If there are no closing tabs, this will 621// return |index|. 622- (NSInteger)indexFromModelIndex:(NSInteger)index { 623 DCHECK(index >= 0); 624 if (index < 0) 625 return index; 626 627 NSInteger i = 0; 628 for (TabController* controller in tabArray_.get()) { 629 if ([closingControllers_ containsObject:controller]) { 630 DCHECK([(TabView*)[controller view] isClosing]); 631 ++index; 632 } 633 if (i == index) // No need to check anything after, it has no effect. 634 break; 635 ++i; 636 } 637 return index; 638} 639 640 641// Returns the index of the subview |view|. Returns -1 if not present. Takes 642// closing tabs into account such that this index will correctly match the tab 643// model. If |view| is in the process of closing, returns -1, as closing tabs 644// are no longer in the model. 645- (NSInteger)modelIndexForTabView:(NSView*)view { 646 NSInteger index = 0; 647 for (TabController* current in tabArray_.get()) { 648 // If |current| is closing, skip it. 649 if ([closingControllers_ containsObject:current]) 650 continue; 651 else if ([current view] == view) 652 return index; 653 ++index; 654 } 655 return -1; 656} 657 658// Returns the index of the contents subview |view|. Returns -1 if not present. 659// Takes closing tabs into account such that this index will correctly match the 660// tab model. If |view| is in the process of closing, returns -1, as closing 661// tabs are no longer in the model. 662- (NSInteger)modelIndexForContentsView:(NSView*)view { 663 NSInteger index = 0; 664 NSInteger i = 0; 665 for (TabContentsController* current in tabContentsArray_.get()) { 666 // If the TabController corresponding to |current| is closing, skip it. 667 TabController* controller = [tabArray_ objectAtIndex:i]; 668 if ([closingControllers_ containsObject:controller]) { 669 ++i; 670 continue; 671 } else if ([current view] == view) { 672 return index; 673 } 674 ++index; 675 ++i; 676 } 677 return -1; 678} 679 680 681// Returns the view at the given index, using the array of TabControllers to 682// get the associated view. Returns nil if out of range. 683- (NSView*)viewAtIndex:(NSUInteger)index { 684 if (index >= [tabArray_ count]) 685 return NULL; 686 return [[tabArray_ objectAtIndex:index] view]; 687} 688 689- (NSUInteger)viewsCount { 690 return [tabArray_ count]; 691} 692 693// Called when the user clicks a tab. Tell the model the selection has changed, 694// which feeds back into us via a notification. 695- (void)selectTab:(id)sender { 696 DCHECK([sender isKindOfClass:[NSView class]]); 697 int index = [self modelIndexForTabView:sender]; 698 if (tabStripModel_->ContainsIndex(index)) 699 tabStripModel_->ActivateTabAt(index, true); 700} 701 702// Called when the user closes a tab. Asks the model to close the tab. |sender| 703// is the TabView that is potentially going away. 704- (void)closeTab:(id)sender { 705 DCHECK([sender isKindOfClass:[TabView class]]); 706 if ([hoveredTab_ isEqual:sender]) { 707 hoveredTab_ = nil; 708 } 709 710 NSInteger index = [self modelIndexForTabView:sender]; 711 if (!tabStripModel_->ContainsIndex(index)) 712 return; 713 714 TabContentsWrapper* contents = tabStripModel_->GetTabContentsAt(index); 715 if (contents) 716 UserMetrics::RecordAction(UserMetricsAction("CloseTab_Mouse"), 717 contents->tab_contents()->profile()); 718 const NSInteger numberOfOpenTabs = [self numberOfOpenTabs]; 719 if (numberOfOpenTabs > 1) { 720 bool isClosingLastTab = index == numberOfOpenTabs - 1; 721 if (!isClosingLastTab) { 722 // Limit the width available for laying out tabs so that tabs are not 723 // resized until a later time (when the mouse leaves the tab strip). 724 // However, if the tab being closed is a pinned tab, break out of 725 // rapid-closure mode since the mouse is almost guaranteed not to be over 726 // the closebox of the adjacent tab (due to the difference in widths). 727 // TODO(pinkerton): re-visit when handling tab overflow. 728 // http://crbug.com/188 729 if (tabStripModel_->IsTabPinned(index)) { 730 availableResizeWidth_ = kUseFullAvailableWidth; 731 } else { 732 NSView* penultimateTab = [self viewAtIndex:numberOfOpenTabs - 2]; 733 availableResizeWidth_ = NSMaxX([penultimateTab frame]); 734 } 735 } else { 736 // If the rightmost tab is closed, change the available width so that 737 // another tab's close button lands below the cursor (assuming the tabs 738 // are currently below their maximum width and can grow). 739 NSView* lastTab = [self viewAtIndex:numberOfOpenTabs - 1]; 740 availableResizeWidth_ = NSMaxX([lastTab frame]); 741 } 742 tabStripModel_->CloseTabContentsAt( 743 index, 744 TabStripModel::CLOSE_USER_GESTURE | 745 TabStripModel::CLOSE_CREATE_HISTORICAL_TAB); 746 } else { 747 // Use the standard window close if this is the last tab 748 // this prevents the tab from being removed from the model until after 749 // the window dissapears 750 [[tabStripView_ window] performClose:nil]; 751 } 752} 753 754// Dispatch context menu commands for the given tab controller. 755- (void)commandDispatch:(TabStripModel::ContextMenuCommand)command 756 forController:(TabController*)controller { 757 int index = [self modelIndexForTabView:[controller view]]; 758 if (tabStripModel_->ContainsIndex(index)) 759 tabStripModel_->ExecuteContextMenuCommand(index, command); 760} 761 762// Returns YES if the specificed command should be enabled for the given 763// controller. 764- (BOOL)isCommandEnabled:(TabStripModel::ContextMenuCommand)command 765 forController:(TabController*)controller { 766 int index = [self modelIndexForTabView:[controller view]]; 767 if (!tabStripModel_->ContainsIndex(index)) 768 return NO; 769 return tabStripModel_->IsContextMenuCommandEnabled(index, command) ? YES : NO; 770} 771 772- (void)insertPlaceholderForTab:(TabView*)tab 773 frame:(NSRect)frame 774 yStretchiness:(CGFloat)yStretchiness { 775 placeholderTab_ = tab; 776 placeholderFrame_ = frame; 777 placeholderStretchiness_ = yStretchiness; 778 [self layoutTabsWithAnimation:initialLayoutComplete_ regenerateSubviews:NO]; 779} 780 781- (BOOL)isDragSessionActive { 782 return placeholderTab_ != nil; 783} 784 785- (BOOL)isTabFullyVisible:(TabView*)tab { 786 NSRect frame = [tab frame]; 787 return NSMinX(frame) >= [self indentForControls] && 788 NSMaxX(frame) <= NSMaxX([tabStripView_ frame]); 789} 790 791- (void)showNewTabButton:(BOOL)show { 792 forceNewTabButtonHidden_ = show ? NO : YES; 793 if (forceNewTabButtonHidden_) 794 [newTabButton_ setHidden:YES]; 795} 796 797// Lay out all tabs in the order of their TabContentsControllers, which matches 798// the ordering in the TabStripModel. This call isn't that expensive, though 799// it is O(n) in the number of tabs. Tabs will animate to their new position 800// if the window is visible and |animate| is YES. 801// TODO(pinkerton): Note this doesn't do too well when the number of min-sized 802// tabs would cause an overflow. http://crbug.com/188 803- (void)layoutTabsWithAnimation:(BOOL)animate 804 regenerateSubviews:(BOOL)doUpdate { 805 DCHECK([NSThread isMainThread]); 806 if (![tabArray_ count]) 807 return; 808 809 const CGFloat kMaxTabWidth = [TabController maxTabWidth]; 810 const CGFloat kMinTabWidth = [TabController minTabWidth]; 811 const CGFloat kMinSelectedTabWidth = [TabController minSelectedTabWidth]; 812 const CGFloat kMiniTabWidth = [TabController miniTabWidth]; 813 const CGFloat kAppTabWidth = [TabController appTabWidth]; 814 815 NSRect enclosingRect = NSZeroRect; 816 ScopedNSAnimationContextGroup mainAnimationGroup(animate); 817 mainAnimationGroup.SetCurrentContextDuration(kAnimationDuration); 818 819 // Update the current subviews and their z-order if requested. 820 if (doUpdate) 821 [self regenerateSubviewList]; 822 823 // Compute the base width of tabs given how much room we're allowed. Note that 824 // mini-tabs have a fixed width. We may not be able to use the entire width 825 // if the user is quickly closing tabs. This may be negative, but that's okay 826 // (taken care of by |MAX()| when calculating tab sizes). 827 CGFloat availableSpace = 0; 828 if (verticalLayout_) { 829 availableSpace = NSHeight([tabStripView_ bounds]); 830 } else { 831 if ([self inRapidClosureMode]) { 832 availableSpace = availableResizeWidth_; 833 } else { 834 availableSpace = NSWidth([tabStripView_ frame]); 835 836 // Account for the widths of the new tab button, the incognito badge, and 837 // the fullscreen button if any/all are present. 838 availableSpace -= NSWidth([newTabButton_ frame]) + kNewTabButtonOffset; 839 if (browser_->profile()->IsOffTheRecord()) 840 availableSpace -= kIncognitoBadgeTabStripShrink; 841 if ([[tabStripView_ window] 842 respondsToSelector:@selector(toggleFullScreen:)]) { 843 NSButton* fullscreenButton = [[tabStripView_ window] 844 standardWindowButton:NSWindowFullScreenButton]; 845 if (fullscreenButton) 846 availableSpace -= [fullscreenButton frame].size.width; 847 } 848 } 849 availableSpace -= [self indentForControls]; 850 } 851 852 // This may be negative, but that's okay (taken care of by |MAX()| when 853 // calculating tab sizes). "mini" tabs in horizontal mode just get a special 854 // section, they don't change size. 855 CGFloat availableSpaceForNonMini = availableSpace; 856 if (!verticalLayout_) { 857 availableSpaceForNonMini -= 858 [self numberOfOpenMiniTabs] * (kMiniTabWidth - kTabOverlap); 859 } 860 861 // Initialize |nonMiniTabWidth| in case there aren't any non-mini-tabs; this 862 // value shouldn't actually be used. 863 CGFloat nonMiniTabWidth = kMaxTabWidth; 864 const NSInteger numberOfOpenNonMiniTabs = [self numberOfOpenNonMiniTabs]; 865 if (!verticalLayout_ && numberOfOpenNonMiniTabs) { 866 // Find the width of a non-mini-tab. This only applies to horizontal 867 // mode. Add in the amount we "get back" from the tabs overlapping. 868 availableSpaceForNonMini += (numberOfOpenNonMiniTabs - 1) * kTabOverlap; 869 870 // Divide up the space between the non-mini-tabs. 871 nonMiniTabWidth = availableSpaceForNonMini / numberOfOpenNonMiniTabs; 872 873 // Clamp the width between the max and min. 874 nonMiniTabWidth = MAX(MIN(nonMiniTabWidth, kMaxTabWidth), kMinTabWidth); 875 } 876 877 BOOL visible = [[tabStripView_ window] isVisible]; 878 879 CGFloat offset = [self indentForControls]; 880 bool hasPlaceholderGap = false; 881 for (TabController* tab in tabArray_.get()) { 882 // Ignore a tab that is going through a close animation. 883 if ([closingControllers_ containsObject:tab]) 884 continue; 885 886 BOOL isPlaceholder = [[tab view] isEqual:placeholderTab_]; 887 NSRect tabFrame = [[tab view] frame]; 888 tabFrame.size.height = [[self class] defaultTabHeight] + 1; 889 if (verticalLayout_) { 890 tabFrame.origin.y = availableSpace - tabFrame.size.height - offset; 891 tabFrame.origin.x = 0; 892 } else { 893 tabFrame.origin.y = 0; 894 tabFrame.origin.x = offset; 895 } 896 // If the tab is hidden, we consider it a new tab. We make it visible 897 // and animate it in. 898 BOOL newTab = [[tab view] isHidden]; 899 if (newTab) 900 [[tab view] setHidden:NO]; 901 902 if (isPlaceholder) { 903 // Move the current tab to the correct location instantly. 904 // We need a duration or else it doesn't cancel an inflight animation. 905 ScopedNSAnimationContextGroup localAnimationGroup(animate); 906 localAnimationGroup.SetCurrentContextShortestDuration(); 907 if (verticalLayout_) 908 tabFrame.origin.y = availableSpace - tabFrame.size.height - offset; 909 else 910 tabFrame.origin.x = placeholderFrame_.origin.x; 911 // TODO(alcor): reenable this 912 //tabFrame.size.height += 10.0 * placeholderStretchiness_; 913 id target = animate ? [[tab view] animator] : [tab view]; 914 [target setFrame:tabFrame]; 915 916 // Store the frame by identifier to aviod redundant calls to animator. 917 NSValue* identifier = [NSValue valueWithPointer:[tab view]]; 918 [targetFrames_ setObject:[NSValue valueWithRect:tabFrame] 919 forKey:identifier]; 920 continue; 921 } 922 923 if (placeholderTab_ && !hasPlaceholderGap) { 924 const CGFloat placeholderMin = 925 verticalLayout_ ? NSMinY(placeholderFrame_) : 926 NSMinX(placeholderFrame_); 927 if (verticalLayout_) { 928 if (NSMidY(tabFrame) > placeholderMin) { 929 hasPlaceholderGap = true; 930 offset += NSHeight(placeholderFrame_); 931 tabFrame.origin.y = availableSpace - tabFrame.size.height - offset; 932 } 933 } else { 934 // If the left edge is to the left of the placeholder's left, but the 935 // mid is to the right of it slide over to make space for it. 936 if (NSMidX(tabFrame) > placeholderMin) { 937 hasPlaceholderGap = true; 938 offset += NSWidth(placeholderFrame_); 939 offset -= kTabOverlap; 940 tabFrame.origin.x = offset; 941 } 942 } 943 } 944 945 // Set the width. Selected tabs are slightly wider when things get really 946 // small and thus we enforce a different minimum width. 947 tabFrame.size.width = [tab mini] ? 948 ([tab app] ? kAppTabWidth : kMiniTabWidth) : nonMiniTabWidth; 949 if ([tab selected]) 950 tabFrame.size.width = MAX(tabFrame.size.width, kMinSelectedTabWidth); 951 952 // Animate a new tab in by putting it below the horizon unless told to put 953 // it in a specific location (i.e., from a drop). 954 // TODO(pinkerton): figure out vertical tab animations. 955 if (newTab && visible && animate) { 956 if (NSEqualRects(droppedTabFrame_, NSZeroRect)) { 957 [[tab view] setFrame:NSOffsetRect(tabFrame, 0, -NSHeight(tabFrame))]; 958 } else { 959 [[tab view] setFrame:droppedTabFrame_]; 960 droppedTabFrame_ = NSZeroRect; 961 } 962 } 963 964 // Check the frame by identifier to avoid redundant calls to animator. 965 id frameTarget = visible && animate ? [[tab view] animator] : [tab view]; 966 NSValue* identifier = [NSValue valueWithPointer:[tab view]]; 967 NSValue* oldTargetValue = [targetFrames_ objectForKey:identifier]; 968 if (!oldTargetValue || 969 !NSEqualRects([oldTargetValue rectValue], tabFrame)) { 970 [frameTarget setFrame:tabFrame]; 971 [targetFrames_ setObject:[NSValue valueWithRect:tabFrame] 972 forKey:identifier]; 973 } 974 975 enclosingRect = NSUnionRect(tabFrame, enclosingRect); 976 977 if (verticalLayout_) { 978 offset += NSHeight(tabFrame); 979 } else { 980 offset += NSWidth(tabFrame); 981 offset -= kTabOverlap; 982 } 983 } 984 985 // Hide the new tab button if we're explicitly told to. It may already 986 // be hidden, doing it again doesn't hurt. Otherwise position it 987 // appropriately, showing it if necessary. 988 if (forceNewTabButtonHidden_) { 989 [newTabButton_ setHidden:YES]; 990 } else { 991 NSRect newTabNewFrame = [newTabButton_ frame]; 992 // We've already ensured there's enough space for the new tab button 993 // so we don't have to check it against the available space. We do need 994 // to make sure we put it after any placeholder. 995 CGFloat maxTabX = MAX(offset, NSMaxX(placeholderFrame_) - kTabOverlap); 996 newTabNewFrame.origin = NSMakePoint(maxTabX + kNewTabButtonOffset, 0); 997 if ([tabContentsArray_ count]) 998 [newTabButton_ setHidden:NO]; 999 1000 if (!NSEqualRects(newTabTargetFrame_, newTabNewFrame)) { 1001 // Set the new tab button image correctly based on where the cursor is. 1002 NSWindow* window = [tabStripView_ window]; 1003 NSPoint currentMouse = [window mouseLocationOutsideOfEventStream]; 1004 currentMouse = [tabStripView_ convertPoint:currentMouse fromView:nil]; 1005 1006 BOOL shouldShowHover = [newTabButton_ pointIsOverButton:currentMouse]; 1007 [self setNewTabButtonHoverState:shouldShowHover]; 1008 1009 // Move the new tab button into place. We want to animate the new tab 1010 // button if it's moving to the left (closing a tab), but not when it's 1011 // moving to the right (inserting a new tab). If moving right, we need 1012 // to use a very small duration to make sure we cancel any in-flight 1013 // animation to the left. 1014 if (visible && animate) { 1015 ScopedNSAnimationContextGroup localAnimationGroup(true); 1016 BOOL movingLeft = NSMinX(newTabNewFrame) < NSMinX(newTabTargetFrame_); 1017 if (!movingLeft) { 1018 localAnimationGroup.SetCurrentContextShortestDuration(); 1019 } 1020 [[newTabButton_ animator] setFrame:newTabNewFrame]; 1021 newTabTargetFrame_ = newTabNewFrame; 1022 } else { 1023 [newTabButton_ setFrame:newTabNewFrame]; 1024 newTabTargetFrame_ = newTabNewFrame; 1025 } 1026 } 1027 } 1028 1029 if (profileMenuButton_ && ![profileMenuButton_ isHidden]) { 1030 CGFloat maxX; 1031 if ([newTabButton_ isHidden]) { 1032 maxX = std::max(offset, NSMaxX(placeholderFrame_) - kTabOverlap); 1033 } else { 1034 maxX = NSMaxX(newTabTargetFrame_); 1035 } 1036 NSRect profileMenuButtonFrame = [profileMenuButton_ frame]; 1037 NSSize minSize = [profileMenuButton_ minControlSize]; 1038 1039 // Make room for the full screen button if necessary. 1040 if (!hasUpdatedProfileMenuButtonXOffset_) { 1041 hasUpdatedProfileMenuButtonXOffset_ = YES; 1042 if ([[profileMenuButton_ window] 1043 respondsToSelector:@selector(toggleFullScreen:)]) { 1044 NSButton* fullscreenButton = [[profileMenuButton_ window] 1045 standardWindowButton:NSWindowFullScreenButton]; 1046 if (fullscreenButton) { 1047 profileMenuButtonFrame.origin.x = NSMinX([fullscreenButton frame]) - 1048 NSWidth(profileMenuButtonFrame) - kProfileMenuButtonOffset; 1049 } 1050 } 1051 } 1052 1053 // TODO(sail): Animate this. 1054 CGFloat availableWidth = NSMaxX(profileMenuButtonFrame) - maxX - 1055 kProfileMenuButtonOffset; 1056 if (availableWidth > minSize.width) { 1057 [profileMenuButton_ setShouldShowProfileDisplayName:YES]; 1058 } else { 1059 [profileMenuButton_ setShouldShowProfileDisplayName:NO]; 1060 } 1061 1062 NSSize desiredSize = [profileMenuButton_ desiredControlSize]; 1063 NSRect rect; 1064 rect.size.width = std::min(desiredSize.width, 1065 std::max(availableWidth, minSize.width)); 1066 rect.size.height = desiredSize.height; 1067 rect.origin.y = NSMaxY(profileMenuButtonFrame) - rect.size.height; 1068 rect.origin.x = NSMaxX(profileMenuButtonFrame) - rect.size.width; 1069 [profileMenuButton_ setFrame:rect]; 1070 } 1071 1072 [dragBlockingView_ setFrame:enclosingRect]; 1073 1074 // Mark that we've successfully completed layout of at least one tab. 1075 initialLayoutComplete_ = YES; 1076} 1077 1078// When we're told to layout from the public API we usually want to animate, 1079// except when it's the first time. 1080- (void)layoutTabs { 1081 [self layoutTabsWithAnimation:initialLayoutComplete_ regenerateSubviews:YES]; 1082} 1083 1084// Handles setting the title of the tab based on the given |contents|. Uses 1085// a canned string if |contents| is NULL. 1086- (void)setTabTitle:(NSViewController*)tab withContents:(TabContents*)contents { 1087 NSString* titleString = nil; 1088 if (contents) 1089 titleString = base::SysUTF16ToNSString(contents->GetTitle()); 1090 if (![titleString length]) { 1091 titleString = l10n_util::GetNSString(IDS_BROWSER_WINDOW_MAC_TAB_UNTITLED); 1092 } 1093 [tab setTitle:titleString]; 1094} 1095 1096// Called when a notification is received from the model to insert a new tab 1097// at |modelIndex|. 1098- (void)insertTabWithContents:(TabContentsWrapper*)contents 1099 atIndex:(NSInteger)modelIndex 1100 inForeground:(bool)inForeground { 1101 DCHECK(contents); 1102 DCHECK(modelIndex == TabStripModel::kNoTab || 1103 tabStripModel_->ContainsIndex(modelIndex)); 1104 1105 // Take closing tabs into account. 1106 NSInteger index = [self indexFromModelIndex:modelIndex]; 1107 1108 // Make a new tab. Load the contents of this tab from the nib and associate 1109 // the new controller with |contents| so it can be looked up later. 1110 scoped_nsobject<TabContentsController> contentsController( 1111 [[TabContentsController alloc] initWithContents:contents->tab_contents() 1112 delegate:self]); 1113 [tabContentsArray_ insertObject:contentsController atIndex:index]; 1114 1115 // Make a new tab and add it to the strip. Keep track of its controller. 1116 TabController* newController = [self newTab]; 1117 [newController setMini:tabStripModel_->IsMiniTab(modelIndex)]; 1118 [newController setPinned:tabStripModel_->IsTabPinned(modelIndex)]; 1119 [newController setApp:tabStripModel_->IsAppTab(modelIndex)]; 1120 [newController setUrl:contents->tab_contents()->GetURL()]; 1121 [tabArray_ insertObject:newController atIndex:index]; 1122 NSView* newView = [newController view]; 1123 1124 // Set the originating frame to just below the strip so that it animates 1125 // upwards as it's being initially layed out. Oddly, this works while doing 1126 // something similar in |-layoutTabs| confuses the window server. 1127 [newView setFrame:NSOffsetRect([newView frame], 1128 0, -[[self class] defaultTabHeight])]; 1129 1130 [self setTabTitle:newController withContents:contents->tab_contents()]; 1131 1132 // If a tab is being inserted, we can again use the entire tab strip width 1133 // for layout. 1134 availableResizeWidth_ = kUseFullAvailableWidth; 1135 1136 // We don't need to call |-layoutTabs| if the tab will be in the foreground 1137 // because it will get called when the new tab is selected by the tab model. 1138 // Whenever |-layoutTabs| is called, it'll also add the new subview. 1139 if (!inForeground) { 1140 [self layoutTabs]; 1141 } 1142 1143 // During normal loading, we won't yet have a favicon and we'll get 1144 // subsequent state change notifications to show the throbber, but when we're 1145 // dragging a tab out into a new window, we have to put the tab's favicon 1146 // into the right state up front as we won't be told to do it from anywhere 1147 // else. 1148 [self updateFaviconForContents:contents->tab_contents() atIndex:modelIndex]; 1149 1150 // Send a broadcast that the number of tabs have changed. 1151 [[NSNotificationCenter defaultCenter] 1152 postNotificationName:kTabStripNumberOfTabsChanged 1153 object:self]; 1154} 1155 1156// Called when a notification is received from the model to select a particular 1157// tab. Swaps in the toolbar and content area associated with |newContents|. 1158- (void)selectTabWithContents:(TabContentsWrapper*)newContents 1159 previousContents:(TabContentsWrapper*)oldContents 1160 atIndex:(NSInteger)modelIndex 1161 userGesture:(bool)wasUserGesture { 1162 // Take closing tabs into account. 1163 NSInteger index = [self indexFromModelIndex:modelIndex]; 1164 1165 if (oldContents && oldContents != newContents) { 1166 int oldModelIndex = 1167 browser_->GetIndexOfController(&(oldContents->controller())); 1168 if (oldModelIndex != -1) { // When closing a tab, the old tab may be gone. 1169 NSInteger oldIndex = [self indexFromModelIndex:oldModelIndex]; 1170 TabContentsController* oldController = 1171 [tabContentsArray_ objectAtIndex:oldIndex]; 1172 [oldController willBecomeUnselectedTab]; 1173 oldContents->view()->StoreFocus(); 1174 oldContents->tab_contents()->WasHidden(); 1175 } 1176 } 1177 1178 // De-select all other tabs and select the new tab. 1179 int i = 0; 1180 for (TabController* current in tabArray_.get()) { 1181 [current setSelected:(i == index) ? YES : NO]; 1182 ++i; 1183 } 1184 1185 // Tell the new tab contents it is about to become the selected tab. Here it 1186 // can do things like make sure the toolbar is up to date. 1187 TabContentsController* newController = 1188 [tabContentsArray_ objectAtIndex:index]; 1189 [newController willBecomeSelectedTab]; 1190 1191 // Relayout for new tabs and to let the selected tab grow to be larger in 1192 // size than surrounding tabs if the user has many. This also raises the 1193 // selected tab to the top. 1194 [self layoutTabs]; 1195 1196 // Swap in the contents for the new tab. 1197 [self swapInTabAtIndex:modelIndex]; 1198 1199 if (newContents) { 1200 newContents->tab_contents()->DidBecomeSelected(); 1201 newContents->view()->RestoreFocus(); 1202 1203 if (newContents->find_tab_helper()->find_ui_active()) 1204 browser_->GetFindBarController()->find_bar()->SetFocusAndSelection(); 1205 } 1206} 1207 1208- (void)tabReplacedWithContents:(TabContentsWrapper*)newContents 1209 previousContents:(TabContentsWrapper*)oldContents 1210 atIndex:(NSInteger)modelIndex { 1211 NSInteger index = [self indexFromModelIndex:modelIndex]; 1212 TabContentsController* oldController = 1213 [tabContentsArray_ objectAtIndex:index]; 1214 DCHECK_EQ(oldContents->tab_contents(), [oldController tabContents]); 1215 1216 // Simply create a new TabContentsController for |newContents| and place it 1217 // into the array, replacing |oldContents|. A TabSelectedAt notification will 1218 // follow, at which point we will install the new view. 1219 scoped_nsobject<TabContentsController> newController( 1220 [[TabContentsController alloc] 1221 initWithContents:newContents->tab_contents() 1222 delegate:self]); 1223 1224 // Bye bye, |oldController|. 1225 [tabContentsArray_ replaceObjectAtIndex:index withObject:newController]; 1226 1227 [delegate_ onReplaceTabWithContents:newContents->tab_contents()]; 1228 1229 // Fake a tab changed notification to force tab titles and favicons to update. 1230 [self tabChangedWithContents:newContents 1231 atIndex:modelIndex 1232 changeType:TabStripModelObserver::ALL]; 1233} 1234 1235// Remove all knowledge about this tab and its associated controller, and remove 1236// the view from the strip. 1237- (void)removeTab:(TabController*)controller { 1238 NSUInteger index = [tabArray_ indexOfObject:controller]; 1239 1240 // Release the tab contents controller so those views get destroyed. This 1241 // will remove all the tab content Cocoa views from the hierarchy. A 1242 // subsequent "select tab" notification will follow from the model. To 1243 // tell us what to swap in in its absence. 1244 [tabContentsArray_ removeObjectAtIndex:index]; 1245 1246 // Remove the view from the tab strip. 1247 NSView* tab = [controller view]; 1248 [tab removeFromSuperview]; 1249 1250 // Remove ourself as an observer. 1251 [[NSNotificationCenter defaultCenter] 1252 removeObserver:self 1253 name:NSViewDidUpdateTrackingAreasNotification 1254 object:tab]; 1255 1256 // Clear the tab controller's target. 1257 // TODO(viettrungluu): [crbug.com/23829] Find a better way to handle the tab 1258 // controller's target. 1259 [controller setTarget:nil]; 1260 1261 if ([hoveredTab_ isEqual:tab]) 1262 hoveredTab_ = nil; 1263 1264 NSValue* identifier = [NSValue valueWithPointer:tab]; 1265 [targetFrames_ removeObjectForKey:identifier]; 1266 1267 // Once we're totally done with the tab, delete its controller 1268 [tabArray_ removeObjectAtIndex:index]; 1269} 1270 1271// Called by the CAAnimation delegate when the tab completes the closing 1272// animation. 1273- (void)animationDidStopForController:(TabController*)controller 1274 finished:(BOOL)finished { 1275 [closingControllers_ removeObject:controller]; 1276 [self removeTab:controller]; 1277} 1278 1279// Save off which TabController is closing and tell its view's animator 1280// where to move the tab to. Registers a delegate to call back when the 1281// animation is complete in order to remove the tab from the model. 1282- (void)startClosingTabWithAnimation:(TabController*)closingTab { 1283 DCHECK([NSThread isMainThread]); 1284 // Save off the controller into the set of animating tabs. This alerts 1285 // the layout method to not do anything with it and allows us to correctly 1286 // calculate offsets when working with indices into the model. 1287 [closingControllers_ addObject:closingTab]; 1288 1289 // Mark the tab as closing. This prevents it from generating any drags or 1290 // selections while it's animating closed. 1291 [(TabView*)[closingTab view] setClosing:YES]; 1292 1293 // Register delegate (owned by the animation system). 1294 NSView* tabView = [closingTab view]; 1295 CAAnimation* animation = [[tabView animationForKey:@"frameOrigin"] copy]; 1296 [animation autorelease]; 1297 scoped_nsobject<TabCloseAnimationDelegate> delegate( 1298 [[TabCloseAnimationDelegate alloc] initWithTabStrip:self 1299 tabController:closingTab]); 1300 [animation setDelegate:delegate.get()]; // Retains delegate. 1301 NSMutableDictionary* animationDictionary = 1302 [NSMutableDictionary dictionaryWithDictionary:[tabView animations]]; 1303 [animationDictionary setObject:animation forKey:@"frameOrigin"]; 1304 [tabView setAnimations:animationDictionary]; 1305 1306 // Periscope down! Animate the tab. 1307 NSRect newFrame = [tabView frame]; 1308 newFrame = NSOffsetRect(newFrame, 0, -newFrame.size.height); 1309 ScopedNSAnimationContextGroup animationGroup(true); 1310 animationGroup.SetCurrentContextDuration(kAnimationDuration); 1311 [[tabView animator] setFrame:newFrame]; 1312} 1313 1314// Called when a notification is received from the model that the given tab 1315// has gone away. Start an animation then force a layout to put everything 1316// in motion. 1317- (void)tabDetachedWithContents:(TabContentsWrapper*)contents 1318 atIndex:(NSInteger)modelIndex { 1319 // Take closing tabs into account. 1320 NSInteger index = [self indexFromModelIndex:modelIndex]; 1321 1322 TabController* tab = [tabArray_ objectAtIndex:index]; 1323 if (tabStripModel_->count() > 0) { 1324 [self startClosingTabWithAnimation:tab]; 1325 [self layoutTabs]; 1326 } else { 1327 [self removeTab:tab]; 1328 } 1329 1330 // Send a broadcast that the number of tabs have changed. 1331 [[NSNotificationCenter defaultCenter] 1332 postNotificationName:kTabStripNumberOfTabsChanged 1333 object:self]; 1334 1335 [delegate_ onTabDetachedWithContents:contents->tab_contents()]; 1336} 1337 1338// A helper routine for creating an NSImageView to hold the favicon or app icon 1339// for |contents|. 1340- (NSImageView*)iconImageViewForContents:(TabContents*)contents { 1341 TabContentsWrapper* wrapper = 1342 TabContentsWrapper::GetCurrentWrapperForContents(contents); 1343 BOOL isApp = wrapper->extension_tab_helper()->is_app(); 1344 NSImage* image = nil; 1345 // Favicons come from the renderer, and the renderer draws everything in the 1346 // system color space. 1347 CGColorSpaceRef colorSpace = base::mac::GetSystemColorSpace(); 1348 if (isApp) { 1349 SkBitmap* icon = wrapper->extension_tab_helper()->GetExtensionAppIcon(); 1350 if (icon) 1351 image = gfx::SkBitmapToNSImageWithColorSpace(*icon, colorSpace); 1352 } else { 1353 image = mac::FaviconForTabContents(contents); 1354 } 1355 1356 // Either we don't have a valid favicon or there was some issue converting it 1357 // from an SkBitmap. Either way, just show the default. 1358 if (!image) 1359 image = defaultFavicon_.get(); 1360 NSRect frame = NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight); 1361 NSImageView* view = [[[NSImageView alloc] initWithFrame:frame] autorelease]; 1362 [view setImage:image]; 1363 return view; 1364} 1365 1366// Updates the current loading state, replacing the icon view with a favicon, 1367// a throbber, the default icon, or nothing at all. 1368- (void)updateFaviconForContents:(TabContents*)contents 1369 atIndex:(NSInteger)modelIndex { 1370 if (!contents) 1371 return; 1372 1373 static NSImage* throbberWaitingImage = 1374 [ResourceBundle::GetSharedInstance().GetNativeImageNamed( 1375 IDR_THROBBER_WAITING) retain]; 1376 static NSImage* throbberLoadingImage = 1377 [ResourceBundle::GetSharedInstance().GetNativeImageNamed(IDR_THROBBER) 1378 retain]; 1379 static NSImage* sadFaviconImage = 1380 [ResourceBundle::GetSharedInstance().GetNativeImageNamed(IDR_SAD_FAVICON) 1381 retain]; 1382 1383 // Take closing tabs into account. 1384 NSInteger index = [self indexFromModelIndex:modelIndex]; 1385 TabController* tabController = [tabArray_ objectAtIndex:index]; 1386 1387 bool oldHasIcon = [tabController iconView] != nil; 1388 bool newHasIcon = contents->ShouldDisplayFavicon() || 1389 tabStripModel_->IsMiniTab(modelIndex); // Always show icon if mini. 1390 1391 TabLoadingState oldState = [tabController loadingState]; 1392 TabLoadingState newState = kTabDone; 1393 NSImage* throbberImage = nil; 1394 if (contents->is_crashed()) { 1395 newState = kTabCrashed; 1396 newHasIcon = true; 1397 } else if (contents->waiting_for_response()) { 1398 newState = kTabWaiting; 1399 throbberImage = throbberWaitingImage; 1400 } else if (contents->is_loading()) { 1401 newState = kTabLoading; 1402 throbberImage = throbberLoadingImage; 1403 } 1404 1405 if (oldState != newState) 1406 [tabController setLoadingState:newState]; 1407 1408 // While loading, this function is called repeatedly with the same state. 1409 // To avoid expensive unnecessary view manipulation, only make changes when 1410 // the state is actually changing. When loading is complete (kTabDone), 1411 // every call to this function is significant. 1412 if (newState == kTabDone || oldState != newState || 1413 oldHasIcon != newHasIcon) { 1414 NSView* iconView = nil; 1415 if (newHasIcon) { 1416 if (newState == kTabDone) { 1417 iconView = [self iconImageViewForContents:contents]; 1418 } else if (newState == kTabCrashed) { 1419 NSImage* oldImage = [[self iconImageViewForContents:contents] image]; 1420 NSRect frame = 1421 NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight); 1422 iconView = [ThrobberView toastThrobberViewWithFrame:frame 1423 beforeImage:oldImage 1424 afterImage:sadFaviconImage]; 1425 } else { 1426 NSRect frame = 1427 NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight); 1428 iconView = [ThrobberView filmstripThrobberViewWithFrame:frame 1429 image:throbberImage]; 1430 } 1431 } 1432 1433 [tabController setIconView:iconView]; 1434 } 1435} 1436 1437// Called when a notification is received from the model that the given tab 1438// has been updated. |loading| will be YES when we only want to update the 1439// throbber state, not anything else about the (partially) loading tab. 1440- (void)tabChangedWithContents:(TabContentsWrapper*)contents 1441 atIndex:(NSInteger)modelIndex 1442 changeType:(TabStripModelObserver::TabChangeType)change { 1443 // Take closing tabs into account. 1444 NSInteger index = [self indexFromModelIndex:modelIndex]; 1445 1446 if (modelIndex == tabStripModel_->active_index()) 1447 [delegate_ onSelectedTabChange:change]; 1448 1449 if (change == TabStripModelObserver::TITLE_NOT_LOADING) { 1450 // TODO(sky): make this work. 1451 // We'll receive another notification of the change asynchronously. 1452 return; 1453 } 1454 1455 TabController* tabController = [tabArray_ objectAtIndex:index]; 1456 1457 if (change != TabStripModelObserver::LOADING_ONLY) 1458 [self setTabTitle:tabController withContents:contents->tab_contents()]; 1459 1460 [self updateFaviconForContents:contents->tab_contents() atIndex:modelIndex]; 1461 1462 TabContentsController* updatedController = 1463 [tabContentsArray_ objectAtIndex:index]; 1464 [updatedController tabDidChange:contents->tab_contents()]; 1465} 1466 1467// Called when a tab is moved (usually by drag&drop). Keep our parallel arrays 1468// in sync with the tab strip model. It can also be pinned/unpinned 1469// simultaneously, so we need to take care of that. 1470- (void)tabMovedWithContents:(TabContentsWrapper*)contents 1471 fromIndex:(NSInteger)modelFrom 1472 toIndex:(NSInteger)modelTo { 1473 // Take closing tabs into account. 1474 NSInteger from = [self indexFromModelIndex:modelFrom]; 1475 NSInteger to = [self indexFromModelIndex:modelTo]; 1476 1477 scoped_nsobject<TabContentsController> movedTabContentsController( 1478 [[tabContentsArray_ objectAtIndex:from] retain]); 1479 [tabContentsArray_ removeObjectAtIndex:from]; 1480 [tabContentsArray_ insertObject:movedTabContentsController.get() 1481 atIndex:to]; 1482 scoped_nsobject<TabController> movedTabController( 1483 [[tabArray_ objectAtIndex:from] retain]); 1484 DCHECK([movedTabController isKindOfClass:[TabController class]]); 1485 [tabArray_ removeObjectAtIndex:from]; 1486 [tabArray_ insertObject:movedTabController.get() atIndex:to]; 1487 1488 // The tab moved, which means that the mini-tab state may have changed. 1489 if (tabStripModel_->IsMiniTab(modelTo) != [movedTabController mini]) 1490 [self tabMiniStateChangedWithContents:contents atIndex:modelTo]; 1491 1492 [self layoutTabs]; 1493} 1494 1495// Called when a tab is pinned or unpinned without moving. 1496- (void)tabMiniStateChangedWithContents:(TabContentsWrapper*)contents 1497 atIndex:(NSInteger)modelIndex { 1498 // Take closing tabs into account. 1499 NSInteger index = [self indexFromModelIndex:modelIndex]; 1500 1501 TabController* tabController = [tabArray_ objectAtIndex:index]; 1502 DCHECK([tabController isKindOfClass:[TabController class]]); 1503 1504 // Don't do anything if the change was already picked up by the move event. 1505 if (tabStripModel_->IsMiniTab(modelIndex) == [tabController mini]) 1506 return; 1507 1508 [tabController setMini:tabStripModel_->IsMiniTab(modelIndex)]; 1509 [tabController setPinned:tabStripModel_->IsTabPinned(modelIndex)]; 1510 [tabController setApp:tabStripModel_->IsAppTab(modelIndex)]; 1511 [tabController setUrl:contents->tab_contents()->GetURL()]; 1512 [self updateFaviconForContents:contents->tab_contents() atIndex:modelIndex]; 1513 // If the tab is being restored and it's pinned, the mini state is set after 1514 // the tab has already been rendered, so re-layout the tabstrip. In all other 1515 // cases, the state is set before the tab is rendered so this isn't needed. 1516 [self layoutTabs]; 1517} 1518 1519- (void)setFrameOfSelectedTab:(NSRect)frame { 1520 NSView* view = [self selectedTabView]; 1521 NSValue* identifier = [NSValue valueWithPointer:view]; 1522 [targetFrames_ setObject:[NSValue valueWithRect:frame] 1523 forKey:identifier]; 1524 [view setFrame:frame]; 1525} 1526 1527- (NSView*)selectedTabView { 1528 int selectedIndex = tabStripModel_->active_index(); 1529 // Take closing tabs into account. They can't ever be selected. 1530 selectedIndex = [self indexFromModelIndex:selectedIndex]; 1531 return [self viewAtIndex:selectedIndex]; 1532} 1533 1534// Find the model index based on the x coordinate of the placeholder. If there 1535// is no placeholder, this returns the end of the tab strip. Closing tabs are 1536// not considered in computing the index. 1537- (int)indexOfPlaceholder { 1538 double placeholderX = placeholderFrame_.origin.x; 1539 int index = 0; 1540 int location = 0; 1541 // Use |tabArray_| here instead of the tab strip count in order to get the 1542 // correct index when there are closing tabs to the left of the placeholder. 1543 const int count = [tabArray_ count]; 1544 while (index < count) { 1545 // Ignore closing tabs for simplicity. The only drawback of this is that 1546 // if the placeholder is placed right before one or several contiguous 1547 // currently closing tabs, the associated TabController will start at the 1548 // end of the closing tabs. 1549 if ([closingControllers_ containsObject:[tabArray_ objectAtIndex:index]]) { 1550 index++; 1551 continue; 1552 } 1553 NSView* curr = [self viewAtIndex:index]; 1554 // The placeholder tab works by changing the frame of the tab being dragged 1555 // to be the bounds of the placeholder, so we need to skip it while we're 1556 // iterating, otherwise we'll end up off by one. Note This only effects 1557 // dragging to the right, not to the left. 1558 if (curr == placeholderTab_) { 1559 index++; 1560 continue; 1561 } 1562 if (placeholderX <= NSMinX([curr frame])) 1563 break; 1564 index++; 1565 location++; 1566 } 1567 return location; 1568} 1569 1570// Move the given tab at index |from| in this window to the location of the 1571// current placeholder. 1572- (void)moveTabFromIndex:(NSInteger)from { 1573 int toIndex = [self indexOfPlaceholder]; 1574 tabStripModel_->MoveTabContentsAt(from, toIndex, true); 1575} 1576 1577// Drop a given TabContents at the location of the current placeholder. If there 1578// is no placeholder, it will go at the end. Used when dragging from another 1579// window when we don't have access to the TabContents as part of our strip. 1580// |frame| is in the coordinate system of the tab strip view and represents 1581// where the user dropped the new tab so it can be animated into its correct 1582// location when the tab is added to the model. If the tab was pinned in its 1583// previous window, setting |pinned| to YES will propagate that state to the 1584// new window. Mini-tabs are either app or pinned tabs; the app state is stored 1585// by the |contents|, but the |pinned| state is the caller's responsibility. 1586- (void)dropTabContents:(TabContentsWrapper*)contents 1587 withFrame:(NSRect)frame 1588 asPinnedTab:(BOOL)pinned { 1589 int modelIndex = [self indexOfPlaceholder]; 1590 1591 // Mark that the new tab being created should start at |frame|. It will be 1592 // reset as soon as the tab has been positioned. 1593 droppedTabFrame_ = frame; 1594 1595 // Insert it into this tab strip. We want it in the foreground and to not 1596 // inherit the current tab's group. 1597 tabStripModel_->InsertTabContentsAt( 1598 modelIndex, contents, 1599 TabStripModel::ADD_ACTIVE | (pinned ? TabStripModel::ADD_PINNED : 0)); 1600} 1601 1602// Called when the tab strip view changes size. As we only registered for 1603// changes on our view, we know it's only for our view. Layout w/out 1604// animations since they are blocked by the resize nested runloop. We need 1605// the views to adjust immediately. Neither the tabs nor their z-order are 1606// changed, so we don't need to update the subviews. 1607- (void)tabViewFrameChanged:(NSNotification*)info { 1608 [self layoutTabsWithAnimation:NO regenerateSubviews:NO]; 1609} 1610 1611// Called when the tracking areas for any given tab are updated. This allows 1612// the individual tabs to update their hover states correctly. 1613// Only generates the event if the cursor is in the tab strip. 1614- (void)tabUpdateTracking:(NSNotification*)notification { 1615 DCHECK([[notification object] isKindOfClass:[TabView class]]); 1616 DCHECK(mouseInside_); 1617 NSWindow* window = [tabStripView_ window]; 1618 NSPoint location = [window mouseLocationOutsideOfEventStream]; 1619 if (NSPointInRect(location, [tabStripView_ frame])) { 1620 NSEvent* mouseEvent = [NSEvent mouseEventWithType:NSMouseMoved 1621 location:location 1622 modifierFlags:0 1623 timestamp:0 1624 windowNumber:[window windowNumber] 1625 context:nil 1626 eventNumber:0 1627 clickCount:0 1628 pressure:0]; 1629 [self mouseMoved:mouseEvent]; 1630 } 1631} 1632 1633- (BOOL)inRapidClosureMode { 1634 return availableResizeWidth_ != kUseFullAvailableWidth; 1635} 1636 1637// Disable tab dragging when there are any pending animations. 1638- (BOOL)tabDraggingAllowed { 1639 return [closingControllers_ count] == 0; 1640} 1641 1642- (void)mouseMoved:(NSEvent*)event { 1643 // Use hit test to figure out what view we are hovering over. 1644 NSView* targetView = [tabStripView_ hitTest:[event locationInWindow]]; 1645 1646 // Set the new tab button hover state iff the mouse is over the button. 1647 BOOL shouldShowHoverImage = [targetView isKindOfClass:[NewTabButton class]]; 1648 [self setNewTabButtonHoverState:shouldShowHoverImage]; 1649 1650 TabView* tabView = (TabView*)targetView; 1651 if (![tabView isKindOfClass:[TabView class]]) { 1652 if ([[tabView superview] isKindOfClass:[TabView class]]) { 1653 tabView = (TabView*)[targetView superview]; 1654 } else { 1655 tabView = nil; 1656 } 1657 } 1658 1659 if (hoveredTab_ != tabView) { 1660 [hoveredTab_ mouseExited:nil]; // We don't pass event because moved events 1661 [tabView mouseEntered:nil]; // don't have valid tracking areas 1662 hoveredTab_ = tabView; 1663 } else { 1664 [hoveredTab_ mouseMoved:event]; 1665 } 1666} 1667 1668- (void)mouseEntered:(NSEvent*)event { 1669 NSTrackingArea* area = [event trackingArea]; 1670 if ([area isEqual:trackingArea_]) { 1671 mouseInside_ = YES; 1672 [self setTabTrackingAreasEnabled:YES]; 1673 [self mouseMoved:event]; 1674 } 1675} 1676 1677// Called when the tracking area is in effect which means we're tracking to 1678// see if the user leaves the tab strip with their mouse. When they do, 1679// reset layout to use all available width. 1680- (void)mouseExited:(NSEvent*)event { 1681 NSTrackingArea* area = [event trackingArea]; 1682 if ([area isEqual:trackingArea_]) { 1683 mouseInside_ = NO; 1684 [self setTabTrackingAreasEnabled:NO]; 1685 availableResizeWidth_ = kUseFullAvailableWidth; 1686 [hoveredTab_ mouseExited:event]; 1687 hoveredTab_ = nil; 1688 [self layoutTabs]; 1689 } else if ([area isEqual:newTabTrackingArea_]) { 1690 // If the mouse is moved quickly enough, it is possible for the mouse to 1691 // leave the tabstrip without sending any mouseMoved: messages at all. 1692 // Since this would result in the new tab button incorrectly staying in the 1693 // hover state, disable the hover image on every mouse exit. 1694 [self setNewTabButtonHoverState:NO]; 1695 } 1696} 1697 1698// Enable/Disable the tracking areas for the tabs. They are only enabled 1699// when the mouse is in the tabstrip. 1700- (void)setTabTrackingAreasEnabled:(BOOL)enabled { 1701 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; 1702 for (TabController* controller in tabArray_.get()) { 1703 TabView* tabView = [controller tabView]; 1704 if (enabled) { 1705 // Set self up to observe tabs so hover states will be correct. 1706 [defaultCenter addObserver:self 1707 selector:@selector(tabUpdateTracking:) 1708 name:NSViewDidUpdateTrackingAreasNotification 1709 object:tabView]; 1710 } else { 1711 [defaultCenter removeObserver:self 1712 name:NSViewDidUpdateTrackingAreasNotification 1713 object:tabView]; 1714 } 1715 [tabView setTrackingEnabled:enabled]; 1716 } 1717} 1718 1719// Sets the new tab button's image based on the current hover state. Does 1720// nothing if the hover state is already correct. 1721- (void)setNewTabButtonHoverState:(BOOL)shouldShowHover { 1722 if (shouldShowHover && !newTabButtonShowingHoverImage_) { 1723 newTabButtonShowingHoverImage_ = YES; 1724 [newTabButton_ setImage: 1725 app::mac::GetCachedImageWithName(kNewTabHoverImage)]; 1726 } else if (!shouldShowHover && newTabButtonShowingHoverImage_) { 1727 newTabButtonShowingHoverImage_ = NO; 1728 [newTabButton_ setImage:app::mac::GetCachedImageWithName(kNewTabImage)]; 1729 } 1730} 1731 1732// Adds the given subview to (the end of) the list of permanent subviews 1733// (specified from bottom up). These subviews will always be below the 1734// transitory subviews (tabs). |-regenerateSubviewList| must be called to 1735// effectuate the addition. 1736- (void)addSubviewToPermanentList:(NSView*)aView { 1737 if (aView) 1738 [permanentSubviews_ addObject:aView]; 1739} 1740 1741// Update the subviews, keeping the permanent ones (or, more correctly, putting 1742// in the ones listed in permanentSubviews_), and putting in the current tabs in 1743// the correct z-order. Any current subviews which is neither in the permanent 1744// list nor a (current) tab will be removed. So if you add such a subview, you 1745// should call |-addSubviewToPermanentList:| (or better yet, call that and then 1746// |-regenerateSubviewList| to actually add it). 1747- (void)regenerateSubviewList { 1748 // Remove self as an observer from all the old tabs before a new set of 1749 // potentially different tabs is put in place. 1750 [self setTabTrackingAreasEnabled:NO]; 1751 1752 // Subviews to put in (in bottom-to-top order), beginning with the permanent 1753 // ones. 1754 NSMutableArray* subviews = [NSMutableArray arrayWithArray:permanentSubviews_]; 1755 1756 NSView* selectedTabView = nil; 1757 // Go through tabs in reverse order, since |subviews| is bottom-to-top. 1758 for (TabController* tab in [tabArray_ reverseObjectEnumerator]) { 1759 NSView* tabView = [tab view]; 1760 if ([tab selected]) { 1761 DCHECK(!selectedTabView); 1762 selectedTabView = tabView; 1763 } else { 1764 [subviews addObject:tabView]; 1765 } 1766 } 1767 if (selectedTabView) { 1768 [subviews addObject:selectedTabView]; 1769 } 1770 [tabStripView_ setSubviews:subviews]; 1771 [self setTabTrackingAreasEnabled:mouseInside_]; 1772} 1773 1774// Get the index and disposition for a potential URL(s) drop given a point (in 1775// the |TabStripView|'s coordinates). It considers only the x-coordinate of the 1776// given point. If it's in the "middle" of a tab, it drops on that tab. If it's 1777// to the left, it inserts to the left, and similarly for the right. 1778- (void)droppingURLsAt:(NSPoint)point 1779 givesIndex:(NSInteger*)index 1780 disposition:(WindowOpenDisposition*)disposition { 1781 // Proportion of the tab which is considered the "middle" (and causes things 1782 // to drop on that tab). 1783 const double kMiddleProportion = 0.5; 1784 const double kLRProportion = (1.0 - kMiddleProportion) / 2.0; 1785 1786 DCHECK(index && disposition); 1787 NSInteger i = 0; 1788 for (TabController* tab in tabArray_.get()) { 1789 NSView* view = [tab view]; 1790 DCHECK([view isKindOfClass:[TabView class]]); 1791 1792 // Recall that |-[NSView frame]| is in its superview's coordinates, so a 1793 // |TabView|'s frame is in the coordinates of the |TabStripView| (which 1794 // matches the coordinate system of |point|). 1795 NSRect frame = [view frame]; 1796 1797 // Modify the frame to make it "unoverlapped". 1798 frame.origin.x += kTabOverlap / 2.0; 1799 frame.size.width -= kTabOverlap; 1800 if (frame.size.width < 1.0) 1801 frame.size.width = 1.0; // try to avoid complete failure 1802 1803 // Drop in a new tab to the left of tab |i|? 1804 if (point.x < (frame.origin.x + kLRProportion * frame.size.width)) { 1805 *index = i; 1806 *disposition = NEW_FOREGROUND_TAB; 1807 return; 1808 } 1809 1810 // Drop on tab |i|? 1811 if (point.x <= (frame.origin.x + 1812 (1.0 - kLRProportion) * frame.size.width)) { 1813 *index = i; 1814 *disposition = CURRENT_TAB; 1815 return; 1816 } 1817 1818 // (Dropping in a new tab to the right of tab |i| will be taken care of in 1819 // the next iteration.) 1820 i++; 1821 } 1822 1823 // If we've made it here, we want to append a new tab to the end. 1824 *index = -1; 1825 *disposition = NEW_FOREGROUND_TAB; 1826} 1827 1828- (void)openURL:(GURL*)url inView:(NSView*)view at:(NSPoint)point { 1829 // Get the index and disposition. 1830 NSInteger index; 1831 WindowOpenDisposition disposition; 1832 [self droppingURLsAt:point 1833 givesIndex:&index 1834 disposition:&disposition]; 1835 1836 // Either insert a new tab or open in a current tab. 1837 switch (disposition) { 1838 case NEW_FOREGROUND_TAB: { 1839 UserMetrics::RecordAction(UserMetricsAction("Tab_DropURLBetweenTabs"), 1840 browser_->profile()); 1841 browser::NavigateParams params(browser_, *url, PageTransition::TYPED); 1842 params.disposition = disposition; 1843 params.tabstrip_index = index; 1844 params.tabstrip_add_types = 1845 TabStripModel::ADD_ACTIVE | TabStripModel::ADD_FORCE_INDEX; 1846 browser::Navigate(¶ms); 1847 break; 1848 } 1849 case CURRENT_TAB: 1850 UserMetrics::RecordAction(UserMetricsAction("Tab_DropURLOnTab"), 1851 browser_->profile()); 1852 tabStripModel_->GetTabContentsAt(index) 1853 ->tab_contents()->OpenURL(*url, GURL(), CURRENT_TAB, 1854 PageTransition::TYPED); 1855 tabStripModel_->ActivateTabAt(index, true); 1856 break; 1857 default: 1858 NOTIMPLEMENTED(); 1859 } 1860} 1861 1862// (URLDropTargetController protocol) 1863- (void)dropURLs:(NSArray*)urls inView:(NSView*)view at:(NSPoint)point { 1864 DCHECK_EQ(view, tabStripView_.get()); 1865 1866 if ([urls count] < 1) { 1867 NOTREACHED(); 1868 return; 1869 } 1870 1871 //TODO(viettrungluu): dropping multiple URLs. 1872 if ([urls count] > 1) 1873 NOTIMPLEMENTED(); 1874 1875 // Get the first URL and fix it up. 1876 GURL url(GURL(URLFixerUpper::FixupURL( 1877 base::SysNSStringToUTF8([urls objectAtIndex:0]), std::string()))); 1878 1879 [self openURL:&url inView:view at:point]; 1880} 1881 1882// (URLDropTargetController protocol) 1883- (void)dropText:(NSString*)text inView:(NSView*)view at:(NSPoint)point { 1884 DCHECK_EQ(view, tabStripView_.get()); 1885 1886 // If the input is plain text, classify the input and make the URL. 1887 AutocompleteMatch match; 1888 browser_->profile()->GetAutocompleteClassifier()->Classify( 1889 base::SysNSStringToUTF16(text), string16(), false, &match, NULL); 1890 GURL url(match.destination_url); 1891 1892 [self openURL:&url inView:view at:point]; 1893} 1894 1895// (URLDropTargetController protocol) 1896- (void)indicateDropURLsInView:(NSView*)view at:(NSPoint)point { 1897 DCHECK_EQ(view, tabStripView_.get()); 1898 1899 // The minimum y-coordinate at which one should consider place the arrow. 1900 const CGFloat arrowBaseY = 25; 1901 1902 NSInteger index; 1903 WindowOpenDisposition disposition; 1904 [self droppingURLsAt:point 1905 givesIndex:&index 1906 disposition:&disposition]; 1907 1908 NSPoint arrowPos = NSMakePoint(0, arrowBaseY); 1909 if (index == -1) { 1910 // Append a tab at the end. 1911 DCHECK(disposition == NEW_FOREGROUND_TAB); 1912 NSInteger lastIndex = [tabArray_ count] - 1; 1913 NSRect overRect = [[[tabArray_ objectAtIndex:lastIndex] view] frame]; 1914 arrowPos.x = overRect.origin.x + overRect.size.width - kTabOverlap / 2.0; 1915 } else { 1916 NSRect overRect = [[[tabArray_ objectAtIndex:index] view] frame]; 1917 switch (disposition) { 1918 case NEW_FOREGROUND_TAB: 1919 // Insert tab (to the left of the given tab). 1920 arrowPos.x = overRect.origin.x + kTabOverlap / 2.0; 1921 break; 1922 case CURRENT_TAB: 1923 // Overwrite the given tab. 1924 arrowPos.x = overRect.origin.x + overRect.size.width / 2.0; 1925 break; 1926 default: 1927 NOTREACHED(); 1928 } 1929 } 1930 1931 [tabStripView_ setDropArrowPosition:arrowPos]; 1932 [tabStripView_ setDropArrowShown:YES]; 1933 [tabStripView_ setNeedsDisplay:YES]; 1934} 1935 1936// (URLDropTargetController protocol) 1937- (void)hideDropURLsIndicatorInView:(NSView*)view { 1938 DCHECK_EQ(view, tabStripView_.get()); 1939 1940 if ([tabStripView_ dropArrowShown]) { 1941 [tabStripView_ setDropArrowShown:NO]; 1942 [tabStripView_ setNeedsDisplay:YES]; 1943 } 1944} 1945 1946- (GTMWindowSheetController*)sheetController { 1947 if (!sheetController_.get()) 1948 sheetController_.reset([[GTMWindowSheetController alloc] 1949 initWithWindow:[switchView_ window] delegate:self]); 1950 return sheetController_.get(); 1951} 1952 1953- (void)destroySheetController { 1954 // Make sure there are no open sheets. 1955 DCHECK_EQ(0U, [[sheetController_ viewsWithAttachedSheets] count]); 1956 sheetController_.reset(); 1957} 1958 1959// TabContentsControllerDelegate protocol. 1960- (void)tabContentsViewFrameWillChange:(TabContentsController*)source 1961 frameRect:(NSRect)frameRect { 1962 id<TabContentsControllerDelegate> controller = 1963 [[switchView_ window] windowController]; 1964 [controller tabContentsViewFrameWillChange:source frameRect:frameRect]; 1965} 1966 1967- (TabContentsController*)activeTabContentsController { 1968 int modelIndex = tabStripModel_->active_index(); 1969 if (modelIndex < 0) 1970 return nil; 1971 NSInteger index = [self indexFromModelIndex:modelIndex]; 1972 if (index < 0 || 1973 index >= (NSInteger)[tabContentsArray_ count]) 1974 return nil; 1975 return [tabContentsArray_ objectAtIndex:index]; 1976} 1977 1978- (void)gtm_systemRequestsVisibilityForView:(NSView*)view { 1979 // This implementation is required by GTMWindowSheetController. 1980 1981 // Raise window... 1982 [[switchView_ window] makeKeyAndOrderFront:self]; 1983 1984 // ...and raise a tab with a sheet. 1985 NSInteger index = [self modelIndexForContentsView:view]; 1986 DCHECK(index >= 0); 1987 if (index >= 0) 1988 tabStripModel_->ActivateTabAt(index, false /* not a user gesture */); 1989} 1990 1991- (void)attachConstrainedWindow:(ConstrainedWindowMac*)window { 1992 // TODO(thakis, avi): Figure out how to make this work when tabs are dragged 1993 // out or if fullscreen mode is toggled. 1994 1995 // View hierarchy of the contents view: 1996 // NSView -- switchView, same for all tabs 1997 // +- NSView -- TabContentsController's view 1998 // +- TabContentsViewCocoa 1999 // Changing it? Do not forget to modify removeConstrainedWindow too. 2000 // We use the TabContentsController's view in |swapInTabAtIndex|, so we have 2001 // to pass it to the sheet controller here. 2002 NSView* tabContentsView = [window->owner()->GetNativeView() superview]; 2003 window->delegate()->RunSheet([self sheetController], tabContentsView); 2004 2005 // TODO(avi, thakis): GTMWindowSheetController has no api to move tabsheets 2006 // between windows. Until then, we have to prevent having to move a tabsheet 2007 // between windows, e.g. no tearing off of tabs. 2008 NSInteger modelIndex = [self modelIndexForContentsView:tabContentsView]; 2009 NSInteger index = [self indexFromModelIndex:modelIndex]; 2010 BrowserWindowController* controller = 2011 (BrowserWindowController*)[[switchView_ window] windowController]; 2012 DCHECK(controller != nil); 2013 DCHECK(index >= 0); 2014 if (index >= 0) { 2015 [controller setTab:[self viewAtIndex:index] isDraggable:NO]; 2016 } 2017} 2018 2019- (void)removeConstrainedWindow:(ConstrainedWindowMac*)window { 2020 NSView* tabContentsView = [window->owner()->GetNativeView() superview]; 2021 2022 // TODO(avi, thakis): GTMWindowSheetController has no api to move tabsheets 2023 // between windows. Until then, we have to prevent having to move a tabsheet 2024 // between windows, e.g. no tearing off of tabs. 2025 NSInteger modelIndex = [self modelIndexForContentsView:tabContentsView]; 2026 NSInteger index = [self indexFromModelIndex:modelIndex]; 2027 BrowserWindowController* controller = 2028 (BrowserWindowController*)[[switchView_ window] windowController]; 2029 DCHECK(index >= 0); 2030 if (index >= 0) { 2031 [controller setTab:[self viewAtIndex:index] isDraggable:YES]; 2032 } 2033} 2034 2035- (BOOL)shouldShowProfileMenuButton { 2036 if (!CommandLine::ForCurrentProcess()->HasSwitch(switches::kMultiProfiles)) 2037 return NO; 2038 if (browser_->profile()->IsOffTheRecord()) 2039 return NO; 2040 return (!browser_->profile()->GetPrefs()->GetString( 2041 prefs::kGoogleServicesUsername).empty()); 2042} 2043 2044- (void)updateProfileMenuButton { 2045 if (![self shouldShowProfileMenuButton]) { 2046 [profileMenuButton_ setHidden:YES]; 2047 return; 2048 } 2049 2050 std::string profileName = browser_->profile()->GetPrefs()->GetString( 2051 prefs::kGoogleServicesUsername); 2052 [profileMenuButton_ setProfileDisplayName: 2053 [NSString stringWithUTF8String:profileName.c_str()]]; 2054 [profileMenuButton_ setHidden:NO]; 2055 2056 NSMenu* menu = [profileMenuButton_ menu]; 2057 while ([menu numberOfItems] > 0) { 2058 [menu removeItemAtIndex:0]; 2059 } 2060 2061 NSString* menuTitle = 2062 l10n_util::GetNSStringWithFixup(IDS_PROFILES_CREATE_NEW_PROFILE_OPTION); 2063 [menu addItemWithTitle:menuTitle 2064 action:NULL 2065 keyEquivalent:@""]; 2066} 2067 2068@end 2069