summaryrefslogtreecommitdiffstats
path: root/chrome/browser/cocoa
diff options
context:
space:
mode:
authorpinkerton@chromium.org <pinkerton@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2009-11-04 15:18:22 +0000
committerpinkerton@chromium.org <pinkerton@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2009-11-04 15:18:22 +0000
commitcaf2f4e2142246201ad7589e8629ab9c47df022f (patch)
tree7bf8269dccb49d2cfce22a79b5ad787c36e6fcac /chrome/browser/cocoa
parenta5d28334fcea7a34558d3c2afe6155dcd8379edc (diff)
downloadchromium_src-caf2f4e2142246201ad7589e8629ab9c47df022f.zip
chromium_src-caf2f4e2142246201ad7589e8629ab9c47df022f.tar.gz
chromium_src-caf2f4e2142246201ad7589e8629ab9c47df022f.tar.bz2
Implement tab closing animations.
BUG=14919 TEST=tab opening and closing, tab dragging, trying to drag tabs while animating closed, http auth, tab modal sheets, anything involving tabs. Review URL: http://codereview.chromium.org/348061 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@30959 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/cocoa')
-rw-r--r--chrome/browser/cocoa/browser_window_controller.mm12
-rw-r--r--chrome/browser/cocoa/tab_strip_controller.h20
-rw-r--r--chrome/browser/cocoa/tab_strip_controller.mm406
-rw-r--r--chrome/browser/cocoa/tab_view.h5
-rw-r--r--chrome/browser/cocoa/tab_view.mm20
-rw-r--r--chrome/browser/cocoa/tab_window_controller.h5
-rw-r--r--chrome/browser/cocoa/tab_window_controller.mm4
7 files changed, 376 insertions, 96 deletions
diff --git a/chrome/browser/cocoa/browser_window_controller.mm b/chrome/browser/cocoa/browser_window_controller.mm
index 42d35dd..f8d0b75 100644
--- a/chrome/browser/cocoa/browser_window_controller.mm
+++ b/chrome/browser/cocoa/browser_window_controller.mm
@@ -718,7 +718,7 @@ willPositionSheet:(NSWindow*)sheet
DCHECK(isBrowser);
if (!isBrowser) return;
BrowserWindowController* dragBWC = (BrowserWindowController*)dragController;
- int index = [dragBWC->tabStripController_ indexForTabView:view];
+ int index = [dragBWC->tabStripController_ modelIndexForTabView:view];
TabContents* contents =
dragBWC->browser_->tabstrip_model()->GetTabContentsAt(index);
// The tab contents may have gone away if given a window.close() while it
@@ -750,7 +750,7 @@ willPositionSheet:(NSWindow*)sheet
[tabStripController_ dropTabContents:contents withFrame:destinationFrame];
} else {
// Moving within a window.
- int index = [tabStripController_ indexForTabView:view];
+ int index = [tabStripController_ modelIndexForTabView:view];
[tabStripController_ moveTabFromIndex:index];
}
@@ -761,7 +761,7 @@ willPositionSheet:(NSWindow*)sheet
// Tells the tab strip to forget about this tab in preparation for it being
// put into a different tab strip, such as during a drop on another window.
- (void)detachTabView:(NSView*)view {
- int index = [tabStripController_ indexForTabView:view];
+ int index = [tabStripController_ modelIndexForTabView:view];
browser_->tabstrip_model()->DetachTabContentsAt(index);
}
@@ -791,7 +791,7 @@ willPositionSheet:(NSWindow*)sheet
base::ScopedNSDisableScreenUpdates disabler;
// Fetch the tab contents for the tab being dragged
- int index = [tabStripController_ indexForTabView:tabView];
+ int index = [tabStripController_ modelIndexForTabView:tabView];
TabContents* contents = browser_->tabstrip_model()->GetTabContentsAt(index);
// Set the window size. Need to do this before we detach the tab so it's
@@ -853,6 +853,10 @@ willPositionSheet:(NSWindow*)sheet
yStretchiness:0];
}
+- (BOOL)tabDraggingAllowed {
+ return [tabStripController_ tabDraggingAllowed];
+}
+
- (BOOL)isTabFullyVisible:(TabView*)tab {
return [tabStripController_ isTabFullyVisible:tab];
}
diff --git a/chrome/browser/cocoa/tab_strip_controller.h b/chrome/browser/cocoa/tab_strip_controller.h
index 521be75..3a3f5d06 100644
--- a/chrome/browser/cocoa/tab_strip_controller.h
+++ b/chrome/browser/cocoa/tab_strip_controller.h
@@ -45,15 +45,18 @@ class ToolbarModel;
scoped_ptr<TabStripModelObserverBridge> bridge_;
Browser* browser_; // weak
TabStripModel* tabModel_; // weak
- // access to the TabContentsControllers (which own the parent view
+ // Access to the TabContentsControllers (which own the parent view
// for the toolbar and associated tab contents) given an index. This needs
// to be kept in the same order as the tab strip's model as we will be
// using its index from the TabStripModelObserver calls.
scoped_nsobject<NSMutableArray> tabContentsArray_;
- // an array of TabControllers which manage the actual tab views. As above,
+ // An array of TabControllers which manage the actual tab views. As above,
// this is kept in the same order as the tab strip model.
scoped_nsobject<NSMutableArray> tabArray_;
+ // Set of TabControllers that are currently animating closed.
+ scoped_nsobject<NSMutableSet> closingControllers_;
+
// These values are only used during a drag, and override tab positioning.
TabView* placeholderTab_; // weak. Tab being dragged
NSRect placeholderFrame_; // Frame to use
@@ -116,10 +119,13 @@ class ToolbarModel;
// location when the tab is added to the model.
- (void)dropTabContents:(TabContents*)contents withFrame:(NSRect)frame;
-// Given a tab view in the strip, return its index. Returns -1 if not present.
-- (NSInteger)indexForTabView:(NSView*)view;
+// Returns the index of the subview |view|. Returns -1 if not present. Takes
+// closing tabs into account such that this index will correctly match the tab
+// model. If |view| is in the process of closing, returns -1, as closing tabs
+// are no longer in the model.
+- (NSInteger)modelIndexForTabView:(NSView*)view;
-// return the view at a given index
+// Return the view at a given index.
- (NSView*)viewAtIndex:(NSUInteger)index;
// Set the placeholder for a dragged tab, allowing the |frame| and |strechiness|
@@ -146,6 +152,10 @@ class ToolbarModel;
// closure.
- (BOOL)inRapidClosureMode;
+// Returns YES if the user is allowed to drag tabs on the strip at this moment.
+// For example, this returns NO if there are any pending tab close animtations.
+- (BOOL)tabDraggingAllowed;
+
// Default height for tabs.
+ (CGFloat)defaultTabHeight;
diff --git a/chrome/browser/cocoa/tab_strip_controller.mm b/chrome/browser/cocoa/tab_strip_controller.mm
index 75b7f45..916e659 100644
--- a/chrome/browser/cocoa/tab_strip_controller.mm
+++ b/chrome/browser/cocoa/tab_strip_controller.mm
@@ -4,6 +4,8 @@
#import "chrome/browser/cocoa/tab_strip_controller.h"
+#import <QuartzCore/QuartzCore.h>
+
#include <limits>
#include "app/l10n_util.h"
@@ -45,6 +47,23 @@ static const float kUseFullAvailableWidth = -1.0;
// controls.
static const float kIndentLeavingSpaceForControls = 64.0;
+// Time (in seconds) in which tabs animate to their final position.
+static const NSTimeInterval kAnimationDuration = 0.2;
+
+@interface TabStripController(Private)
+- (void)installTrackingArea;
+- (void)addSubviewToPermanentList:(NSView*)aView;
+- (void)regenerateSubviewList;
+- (NSInteger)indexForContentsView:(NSView*)view;
+- (void)updateFavIconForContents:(TabContents*)contents
+ atIndex:(NSInteger)index;
+- (void)layoutTabsWithAnimation:(BOOL)animate
+ regenerateSubviews:(BOOL)doUpdate;
+- (void)animationDidStopForController:(TabController*)controller
+ finished:(BOOL)finished;
+- (NSInteger)indexFromModelIndex:(NSInteger)index;
+@end
+
// A simple view class that prevents the Window Server from dragging the area
// behind tabs. Sometimes core animation confuses it. Unfortunately, it can also
// falsely pick up clicks during rapid tab closure, so we have to account for
@@ -56,6 +75,7 @@ static const float kIndentLeavingSpaceForControls = 64.0;
- (id)initWithFrame:(NSRect)frameRect
controller:(TabStripController*)controller;
@end
+
@implementation TabStripControllerDragBlockingView
- (BOOL)mouseDownCanMoveWindow {return NO;}
- (void)drawRect:(NSRect)rect {}
@@ -89,17 +109,98 @@ static const float kIndentLeavingSpaceForControls = 64.0;
}
@end
-@interface TabStripController(Private)
-- (void)installTrackingArea;
-- (void)addSubviewToPermanentList:(NSView*)aView;
-- (void)regenerateSubviewList;
-- (NSInteger)indexForContentsView:(NSView*)view;
-- (void)updateFavIconForContents:(TabContents*)contents
- atIndex:(NSInteger)index;
-- (void)layoutTabsWithAnimation:(BOOL)animate
- regenerateSubviews:(BOOL)doUpdate;
+#pragma mark -
+
+// A delegate, owned by the CAAnimation system, that is alerted when the
+// animation to close a tab is completed. Calls back to the given tab strip
+// to let it know that |controller_| is ready to be removed from the model.
+// Since we only maintain weak references, the tab strip must call -invalidate:
+// to prevent the use of dangling pointers.
+@interface TabCloseAnimationDelegate : NSObject {
+ @private
+ TabStripController* strip_; // weak; owns us indirectly
+ TabController* controller_; // weak
+}
+
+// Will tell |strip| when the animation for |controller|'s view has completed.
+// These should not be nil, and will not be retained.
+- (id)initWithTabStrip:(TabStripController*)strip
+ tabController:(TabController*)controller;
+
+// Invalidates this object so that no further calls will be made to
+// |strip_|. This should be called when |strip_| is released, to
+// prevent attempts to call into the released object.
+- (void)invalidate;
+
+// CAAnimation delegate method
+- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished;
+
@end
+@implementation TabCloseAnimationDelegate
+
+- (id)initWithTabStrip:(TabStripController*)strip
+ tabController:(TabController*)controller {
+ if ((self == [super init])) {
+ DCHECK(strip && controller);
+ strip_ = strip;
+ controller_ = controller;
+ }
+ return self;
+}
+
+- (void)invalidate {
+ strip_ = nil;
+ controller_ = nil;
+}
+
+- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
+ [strip_ animationDidStopForController:controller_ finished:finished];
+}
+
+@end
+
+#pragma mark -
+
+// TODO(pinkerton): document tab layout, placeholders, tab dragging on
+// dev.chromium.org
+
+// In general, there is a one-to-one correspondence between TabControllers,
+// TabViews, TabContentsControllers, and the TabContents in the TabStripModel.
+// In the steady-state, the indices line up so an index coming from the model
+// is directly mapped to the same index in the parallel arrays holding our
+// views and controllers. This is also true when new tabs are created (even
+// though there is a small period of animation) because the tab is present
+// in the model while the TabView is animating into place. As a result, nothing
+// special need be done to handle "new tab" animation.
+//
+// This all goes out the window with the "close tab" animation. The animation
+// kicks off in |-tabDetachedWithContents:atIndex:| with the notifiation that
+// the tab has been removed from the model. The simplest solution at this
+// point would be to remove the views and controllers as well, however once
+// the TabView is removed from the view list, the tab z-order code takes care of
+// removing it from the tab strip and we'll get no animation. That means if
+// there is to be any visible animation, the TabView needs to stay around until
+// its animation is complete. In order to maintain consistency among the
+// internal parallel arrays, this means all structures are kept around until
+// the animation completes. At this point, though, the model and our internal
+// structures are out of sync: the indices no longer line up. As a result,
+// there is a concept of a "model index" which represents an index valid in
+// the TabStripModel. During steady-state, the "model index" is just the same
+// index as our parallel arrays (as above), but during tab close animations,
+// it is different, offset by the number of tabs preceding the index which
+// are undergoing tab closing animation. As a result, the caller needs to be
+// careful to use the available conversion routines when accessing the internal
+// parallel arrays (e.g., -indexFromModelIndex:). Care also needs to be taken
+// during tab layout to ignore closing tabs in the total width calculations and
+// in individual tab positioning (to avoid moving them right back to where they
+// were).
+//
+// In order to prevent actions being taken on tabs which are closing, the tab
+// itself gets marked as such so it no longer will send back its select action
+// or allow itself to be dragged. In addition, drags on the tab strip as a
+// whole are disabled while there are tabs closing.
+
@implementation TabStripController
- (id)initWithView:(TabStripView*)view
@@ -134,6 +235,8 @@ static const float kIndentLeavingSpaceForControls = 64.0;
newTabTargetFrame_ = NSMakeRect(0, 0, 0, 0);
availableResizeWidth_ = kUseFullAvailableWidth;
+ closingControllers_.reset([[NSMutableSet alloc] init]);
+
// Install the permanent subviews.
[self regenerateSubviewList];
@@ -161,6 +264,12 @@ static const float kIndentLeavingSpaceForControls = 64.0;
- (void)dealloc {
if (trackingArea_.get())
[tabView_ removeTrackingArea:trackingArea_.get()];
+ // Invalidate all closing animations so they don't call back to us after
+ // we're gone.
+ for (TabController* controller in closingControllers_.get()) {
+ NSView* view = [controller view];
+ [[[view animationForKey:@"frameOrigin"] delegate] invalidate];
+ }
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}
@@ -169,9 +278,12 @@ static const float kIndentLeavingSpaceForControls = 64.0;
return 24.0;
}
-// Finds the associated TabContentsController at the given |index| and swaps
-// out the sole child of the contentArea to display its contents.
-- (void)swapInTabAtIndex:(NSInteger)index {
+// Finds the TabContentsController associated with the given index into the tab
+// model and swaps out the sole child of the contentArea to display its
+// contents.
+- (void)swapInTabAtIndex:(NSInteger)modelIndex {
+ DCHECK(modelIndex >= 0 && modelIndex < tabModel_->count());
+ NSInteger index = [self indexFromModelIndex:modelIndex];
TabContentsController* controller = [tabContentsArray_ objectAtIndex:index];
// Resize the new view to fit the window. Calling |view| may lazily
@@ -198,7 +310,7 @@ static const float kIndentLeavingSpaceForControls = 64.0;
// Make sure the new tabs's sheets are visible (necessary when a background
// tab opened a sheet while it was in the background and now becomes active).
- TabContents* newTab = tabModel_->GetTabContentsAt(index);
+ TabContents* newTab = tabModel_->GetTabContentsAt(modelIndex);
DCHECK(newTab);
if (newTab) {
TabContents::ConstrainedWindowList::iterator it, end;
@@ -232,18 +344,45 @@ static const float kIndentLeavingSpaceForControls = 64.0;
return controller;
}
-// Returns the number of tabs in the tab strip. This is just the number
-// of TabControllers we know about as there's a 1-to-1 mapping from these
-// controllers to a tab.
+// Returns the number of open tabs in the tab strip. This is the number
+// of TabControllers we know about (as there's a 1-to-1 mapping from these
+// controllers to a tab) less the number of closing tabs.
- (NSInteger)numberOfTabViews {
- return [tabArray_ count];
+ NSInteger number = [tabArray_ count] - [closingControllers_ count];
+ DCHECK(number >= 0);
+ return number;
+}
+
+// Given an index into the tab model, returns the index into the tab controller
+// array accounting for tabs that are currently closing. For example, if there
+// are two tabs in the process of closing before |index|, this returns
+// |index| + 2. If there are no closing tabs, this will return |index|.
+- (NSInteger)indexFromModelIndex:(NSInteger)index {
+ NSInteger i = 0;
+ for (TabController* controller in tabArray_.get()) {
+ if ([closingControllers_ containsObject:controller]) {
+ DCHECK([(TabView*)[controller view] isClosing]);
+ ++index;
+ }
+ if (i == index) // No need to check anything after, it has no effect.
+ break;
+ ++i;
+ }
+ return index;
}
-// Returns the index of the subview |view|. Returns -1 if not present.
-- (NSInteger)indexForTabView:(NSView*)view {
+
+// Returns the index of the subview |view|. Returns -1 if not present. Takes
+// closing tabs into account such that this index will correctly match the tab
+// model. If |view| is in the process of closing, returns -1, as closing tabs
+// are no longer in the model.
+- (NSInteger)modelIndexForTabView:(NSView*)view {
NSInteger index = 0;
for (TabController* current in tabArray_.get()) {
- if ([current view] == view)
+ // If |current| is closing, skip it.
+ if ([closingControllers_ containsObject:current])
+ continue;
+ else if ([current view] == view)
return index;
++index;
}
@@ -251,12 +390,23 @@ static const float kIndentLeavingSpaceForControls = 64.0;
}
// Returns the index of the contents subview |view|. Returns -1 if not present.
-- (NSInteger)indexForContentsView:(NSView*)view {
+// Takes closing tabs into account such that this index will correctly match the
+// tab model. If |view| is in the process of closing, returns -1, as closing
+// tabs are no longer in the model.
+- (NSInteger)modelIndexForContentsView:(NSView*)view {
NSInteger index = 0;
+ NSInteger i = 0;
for (TabContentsController* current in tabContentsArray_.get()) {
- if ([current view] == view)
+ // If the TabController corresponding to |current| is closing, skip it.
+ TabController* controller = [tabArray_ objectAtIndex:i];
+ if ([closingControllers_ containsObject:controller]) {
+ ++i;
+ continue;
+ } else if ([current view] == view) {
return index;
+ }
++index;
+ ++i;
}
return -1;
}
@@ -274,61 +424,66 @@ static const float kIndentLeavingSpaceForControls = 64.0;
// which feeds back into us via a notification.
- (void)selectTab:(id)sender {
DCHECK([sender isKindOfClass:[NSView class]]);
- int index = [self indexForTabView:sender];
- if (index >= 0 && tabModel_->ContainsIndex(index))
+ int index = [self modelIndexForTabView:sender];
+ if (tabModel_->ContainsIndex(index))
tabModel_->SelectTabContentsAt(index, true);
}
// Called when the user closes a tab. Asks the model to close the tab. |sender|
// is the TabView that is potentially going away.
- (void)closeTab:(id)sender {
- DCHECK([sender isKindOfClass:[NSView class]]);
+ DCHECK([sender isKindOfClass:[TabView class]]);
if ([hoveredTab_ isEqual:sender]) {
hoveredTab_ = nil;
}
- int index = [self indexForTabView:sender];
- if (tabModel_->ContainsIndex(index)) {
- TabContents* contents = tabModel_->GetTabContentsAt(index);
- if (contents)
- UserMetrics::RecordAction(L"CloseTab_Mouse", contents->profile());
- if ([self numberOfTabViews] > 1) {
- bool isClosingLastTab =
- static_cast<size_t>(index) == [tabArray_ count] - 1;
- if (!isClosingLastTab) {
- // Limit the width available for laying out tabs so that tabs are not
- // resized until a later time (when the mouse leaves the tab strip).
- // TODO(pinkerton): re-visit when handling tab overflow.
- NSView* penultimateTab = [self viewAtIndex:[tabArray_ count] - 2];
- availableResizeWidth_ = NSMaxX([penultimateTab frame]);
- } else {
- // If the rightmost tab is closed, change the available width so that
- // another tab's close button lands below the cursor (assuming the tabs
- // are currently below their maximum width and can grow).
- NSView* lastTab = [self viewAtIndex:[tabArray_ count] - 1];
- availableResizeWidth_ = NSMaxX([lastTab frame]);
- }
- tabModel_->CloseTabContentsAt(index);
+
+ NSInteger index = [self modelIndexForTabView:sender];
+ if (!tabModel_->ContainsIndex(index))
+ return;
+
+ TabContents* contents = tabModel_->GetTabContentsAt(index);
+ if (contents)
+ UserMetrics::RecordAction(L"CloseTab_Mouse", contents->profile());
+ const NSInteger numberOfTabViews = [self numberOfTabViews];
+ if (numberOfTabViews > 1) {
+ bool isClosingLastTab = index == numberOfTabViews - 1;
+ if (!isClosingLastTab) {
+ // Limit the width available for laying out tabs so that tabs are not
+ // resized until a later time (when the mouse leaves the tab strip).
+ // TODO(pinkerton): re-visit when handling tab overflow.
+ NSView* penultimateTab = [self viewAtIndex:numberOfTabViews - 2];
+ availableResizeWidth_ = NSMaxX([penultimateTab frame]);
} else {
- // Use the standard window close if this is the last tab
- // this prevents the tab from being removed from the model until after
- // the window dissapears
- [[tabView_ window] performClose:nil];
+ // If the rightmost tab is closed, change the available width so that
+ // another tab's close button lands below the cursor (assuming the tabs
+ // are currently below their maximum width and can grow).
+ NSView* lastTab = [self viewAtIndex:numberOfTabViews - 1];
+ availableResizeWidth_ = NSMaxX([lastTab frame]);
}
+ tabModel_->CloseTabContentsAt(index);
+ } else {
+ // Use the standard window close if this is the last tab
+ // this prevents the tab from being removed from the model until after
+ // the window dissapears
+ [[tabView_ window] performClose:nil];
}
}
// Dispatch context menu commands for the given tab controller.
- (void)commandDispatch:(TabStripModel::ContextMenuCommand)command
forController:(TabController*)controller {
- int index = [self indexForTabView:[controller view]];
- tabModel_->ExecuteContextMenuCommand(index, command);
+ int index = [self modelIndexForTabView:[controller view]];
+ if (tabModel_->ContainsIndex(index))
+ tabModel_->ExecuteContextMenuCommand(index, command);
}
// Returns YES if the specificed command should be enabled for the given
// controller.
- (BOOL)isCommandEnabled:(TabStripModel::ContextMenuCommand)command
forController:(TabController*)controller {
- int index = [self indexForTabView:[controller view]];
+ int index = [self modelIndexForTabView:[controller view]];
+ if (!tabModel_->ContainsIndex(index))
+ return NO;
return tabModel_->IsContextMenuCommandEnabled(index, command) ? YES : NO;
}
@@ -380,7 +535,7 @@ static const float kIndentLeavingSpaceForControls = 64.0;
NSRect enclosingRect = NSZeroRect;
[NSAnimationContext beginGrouping];
- [[NSAnimationContext currentContext] setDuration:0.2];
+ [[NSAnimationContext currentContext] setDuration:kAnimationDuration];
// Update the current subviews and their z-order if requested.
if (doUpdate)
@@ -398,10 +553,14 @@ static const float kIndentLeavingSpaceForControls = 64.0;
}
availableWidth -= kIndentLeavingSpaceForControls;
+ // Only count the number of tabs that are not in the process of closing.
+ const NSUInteger numberOfOpenTabs =
+ [tabContentsArray_ count] - [closingControllers_ count];
+
// Add back in the amount we "get back" from the tabs overlapping.
- availableWidth += ([tabContentsArray_ count] - 1) * kTabOverlap;
+ availableWidth += (numberOfOpenTabs - 1) * kTabOverlap;
const float baseTabWidth =
- MAX(MIN(availableWidth / [tabContentsArray_ count],
+ MAX(MIN(availableWidth / numberOfOpenTabs,
kMaxTabWidth),
kMinTabWidth);
@@ -412,6 +571,10 @@ static const float kIndentLeavingSpaceForControls = 64.0;
NSUInteger i = 0;
bool hasPlaceholderGap = false;
for (TabController* tab in tabArray_.get()) {
+ // Ignore a tab that is going through a close animation.
+ if ([closingControllers_ containsObject:tab])
+ continue;
+
BOOL isPlaceholder = [[tab view] isEqual:placeholderTab_];
NSRect tabFrame = [[tab view] frame];
tabFrame.size.height = [[self class] defaultTabHeight] + 1;
@@ -558,12 +721,14 @@ static const float kIndentLeavingSpaceForControls = 64.0;
// Called when a notification is received from the model to insert a new tab
// at |index|.
- (void)insertTabWithContents:(TabContents*)contents
- atIndex:(NSInteger)index
+ atIndex:(NSInteger)modelIndex
inForeground:(bool)inForeground {
DCHECK(contents);
- DCHECK(index == TabStripModel::kNoTab || tabModel_->ContainsIndex(index));
+ DCHECK(modelIndex == TabStripModel::kNoTab ||
+ tabModel_->ContainsIndex(modelIndex));
- // TODO(pinkerton): handle tab dragging in here
+ // Take closing tabs into account.
+ NSInteger index = [self indexFromModelIndex:modelIndex];
// Make a new tab. Load the contents of this tab from the nib and associate
// the new controller with |contents| so it can be looked up later.
@@ -616,8 +781,11 @@ static const float kIndentLeavingSpaceForControls = 64.0;
// tab. Swaps in the toolbar and content area associated with |newContents|.
- (void)selectTabWithContents:(TabContents*)newContents
previousContents:(TabContents*)oldContents
- atIndex:(NSInteger)index
+ atIndex:(NSInteger)modelIndex
userGesture:(bool)wasUserGesture {
+ // Take closing tabs into account.
+ NSInteger index = [self indexFromModelIndex:modelIndex];
+
// De-select all other tabs and select the new tab.
int i = 0;
for (TabController* current in tabArray_.get()) {
@@ -641,8 +809,8 @@ static const float kIndentLeavingSpaceForControls = 64.0;
oldContents->WasHidden();
}
- // Swap in the contents for the new tab
- [self swapInTabAtIndex:index];
+ // Swap in the contents for the new tab.
+ [self swapInTabAtIndex:modelIndex];
if (newContents) {
newContents->DidBecomeSelected();
@@ -653,43 +821,99 @@ static const float kIndentLeavingSpaceForControls = 64.0;
}
}
-// Called when a notification is received from the model that the given tab
-// has gone away. Remove all knowledge about this tab and it's associated
+// Remove all knowledge about this tab and it's associated
// controller and remove the view from the strip.
-- (void)tabDetachedWithContents:(TabContents*)contents
- atIndex:(NSInteger)index {
+- (void)removeTab:(TabController*)controller {
+ NSUInteger index = [tabArray_ indexOfObject:controller];
+
// Release the tab contents controller so those views get destroyed. This
// will remove all the tab content Cocoa views from the hierarchy. A
// subsequent "select tab" notification will follow from the model. To
// tell us what to swap in in its absence.
[tabContentsArray_ removeObjectAtIndex:index];
- // Remove the |index|th view from the tab strip
- NSView* tab = [self viewAtIndex:index];
+ // Remove the view from the tab strip.
+ NSView* tab = [controller view];
[tab removeFromSuperview];
// Clear the tab controller's target.
// TODO(viettrungluu): [crbug.com/23829] Find a better way to handle the tab
// controller's target.
- TabController* tabController = [tabArray_ objectAtIndex:index];
- [tabController setTarget:nil];
+ [controller setTarget:nil];
- if ([hoveredTab_ isEqual:tab]) {
+ if ([hoveredTab_ isEqual:tab])
hoveredTab_ = nil;
- }
NSValue* identifier = [NSValue valueWithPointer:tab];
[targetFrames_ removeObjectForKey:identifier];
// Once we're totally done with the tab, delete its controller
[tabArray_ removeObjectAtIndex:index];
+}
+
+// Called by the CAAnimation delegate when the tab completes the closing
+// animation.
+- (void)animationDidStopForController:(TabController*)controller
+ finished:(BOOL)finished {
+ [closingControllers_ removeObject:controller];
+ [self removeTab:controller];
+}
+
+// Save off which TabController is closing and tell its view's animator
+// where to move the tab to. Registers a delegate to call back when the
+// animation is complete in order to remove the tab from the model.
+- (void)startClosingTabWithAnimation:(TabController*)closingTab {
+ // Save off the controller into the set of animating tabs. This alerts
+ // the layout method to not do anything with it and allows us to correctly
+ // calculate offsets when working with indices into the model.
+ [closingControllers_ addObject:closingTab];
+
+ // Mark the tab as closing. This prevents it from generating any drags or
+ // selections while it's animating closed.
+ [(TabView*)[closingTab view] setIsClosing:YES];
+
+ // Register delegate (owned by the animation system).
+ NSView* tabView = [closingTab view];
+ CAAnimation* animation = [[tabView animationForKey:@"frameOrigin"] copy];
+ [animation autorelease];
+ scoped_nsobject<TabCloseAnimationDelegate> delegate(
+ [[TabCloseAnimationDelegate alloc] initWithTabStrip:self
+ tabController:closingTab]);
+ [animation setDelegate:delegate.get()]; // Retains delegate.
+ NSMutableDictionary* animationDictionary =
+ [NSMutableDictionary dictionaryWithDictionary:[tabView animations]];
+ [animationDictionary setObject:animation forKey:@"frameOrigin"];
+ [tabView setAnimations:animationDictionary];
+
+ // Periscope down! Animate the tab.
+ NSRect newFrame = [tabView frame];
+ newFrame = NSOffsetRect(newFrame, 0, -newFrame.size.height);
+ [NSAnimationContext beginGrouping];
+ [[NSAnimationContext currentContext] setDuration:kAnimationDuration];
+ [[tabView animator] setFrame:newFrame];
+ [NSAnimationContext endGrouping];
+}
+
+// Called when a notification is received from the model that the given tab
+// has gone away. Start an animation then force a layout to put everything
+// in motion.
+- (void)tabDetachedWithContents:(TabContents*)contents
+ atIndex:(NSInteger)modelIndex {
+ // Take closing tabs into account.
+ NSInteger index = [self indexFromModelIndex:modelIndex];
+
+ TabController* tab = [tabArray_ objectAtIndex:index];
+ if (tabModel_->count() > 0) {
+ [self startClosingTabWithAnimation:tab];
+ [self layoutTabs];
+ } else {
+ [self removeTab:tab];
+ }
// Send a broadcast that the number of tabs have changed.
[[NSNotificationCenter defaultCenter]
postNotificationName:kTabStripNumberOfTabsChanged
object:self];
-
- [self layoutTabs];
}
// A helper routine for creating an NSImageView to hold the fav icon for
@@ -720,7 +944,8 @@ static const float kIndentLeavingSpaceForControls = 64.0;
}
// Updates the current loading state, replacing the icon view with a favicon,
-// a throbber, the default icon, or nothing at all.
+// a throbber, the default icon, or nothing at all. |index| is a controller
+// array index, not a model index.
- (void)updateFavIconForContents:(TabContents*)contents
atIndex:(NSInteger)index {
if (!contents)
@@ -789,8 +1014,11 @@ static const float kIndentLeavingSpaceForControls = 64.0;
// has been updated. |loading| will be YES when we only want to update the
// throbber state, not anything else about the (partially) loading tab.
- (void)tabChangedWithContents:(TabContents*)contents
- atIndex:(NSInteger)index
+ atIndex:(NSInteger)modelIndex
loadingOnly:(BOOL)loading {
+ // Take closing tabs into account.
+ NSInteger index = [self indexFromModelIndex:modelIndex];
+
if (!loading)
[self setTabTitle:[tabArray_ objectAtIndex:index] withContents:contents];
@@ -804,8 +1032,12 @@ static const float kIndentLeavingSpaceForControls = 64.0;
// Called when a tab is moved (usually by drag&drop). Keep our parallel arrays
// in sync with the tab strip model.
- (void)tabMovedWithContents:(TabContents*)contents
- fromIndex:(NSInteger)from
- toIndex:(NSInteger)to {
+ fromIndex:(NSInteger)modelFrom
+ toIndex:(NSInteger)modelTo {
+ // Take closing tabs into account.
+ NSInteger from = [self indexFromModelIndex:modelFrom];
+ NSInteger to = [self indexFromModelIndex:modelTo];
+
scoped_nsobject<TabContentsController> movedController(
[[tabContentsArray_ objectAtIndex:from] retain]);
[tabContentsArray_ removeObjectAtIndex:from];
@@ -828,6 +1060,8 @@ static const float kIndentLeavingSpaceForControls = 64.0;
- (NSView*)selectedTabView {
int selectedIndex = tabModel_->selected_index();
+ // Take closing tabs into account. They can't ever be selected.
+ selectedIndex = [self indexFromModelIndex:selectedIndex];
return [self viewAtIndex:selectedIndex];
}
@@ -894,6 +1128,11 @@ static const float kIndentLeavingSpaceForControls = 64.0;
return availableResizeWidth_ != kUseFullAvailableWidth;
}
+// Disable tab dragging when there are any pending animations.
+- (BOOL)tabDraggingAllowed {
+ return [closingControllers_ count] == 0;
+}
+
- (void)mouseMoved:(NSEvent*)event {
// Use hit test to figure out what view we are hovering over.
TabView* targetView = (TabView*)[tabView_ hitTest:[event locationInWindow]];
@@ -978,7 +1217,7 @@ static const float kIndentLeavingSpaceForControls = 64.0;
[[switchView_ window] makeKeyAndOrderFront:self];
// ...and raise a tab with a sheet.
- NSInteger index = [self indexForContentsView:view];
+ NSInteger index = [self modelIndexForContentsView:view];
DCHECK(index >= 0);
if (index >= 0)
tabModel_->SelectTabContentsAt(index, false /* not a user gesture */);
@@ -1002,7 +1241,8 @@ static const float kIndentLeavingSpaceForControls = 64.0;
// TODO(avi, thakis): GTMWindowSheetController has no api to move tabsheets
// between windows. Until then, we have to prevent having to move a tabsheet
// between windows, e.g. no tearing off of tabs.
- NSInteger index = [self indexForContentsView:tabContentsView];
+ NSInteger modelIndex = [self modelIndexForContentsView:tabContentsView];
+ NSInteger index = [self indexFromModelIndex:modelIndex];
BrowserWindowController* controller =
(BrowserWindowController*)[[switchView_ window] windowController];
DCHECK(controller != nil);
@@ -1019,7 +1259,8 @@ static const float kIndentLeavingSpaceForControls = 64.0;
// TODO(avi, thakis): GTMWindowSheetController has no api to move tabsheets
// between windows. Until then, we have to prevent having to move a tabsheet
// between windows, e.g. no tearing off of tabs.
- NSInteger index = [self indexForContentsView:tabContentsView];
+ NSInteger modelIndex = [self modelIndexForContentsView:tabContentsView];
+ NSInteger index = [self indexFromModelIndex:modelIndex];
BrowserWindowController* controller =
(BrowserWindowController*)[[switchView_ window] windowController];
DCHECK(index >= 0);
@@ -1028,5 +1269,4 @@ static const float kIndentLeavingSpaceForControls = 64.0;
}
}
-
@end
diff --git a/chrome/browser/cocoa/tab_view.h b/chrome/browser/cocoa/tab_view.h
index 2595ea2..f36512e 100644
--- a/chrome/browser/cocoa/tab_view.h
+++ b/chrome/browser/cocoa/tab_view.h
@@ -22,6 +22,7 @@
// TODO(rohitrao): Add this button to a CoreAnimation layer so we can fade it
// in and out on mouseovers.
IBOutlet NSButton* closeButton_;
+ BOOL isClosing_;
// Tracking area for close button mouseover images.
scoped_nsobject<NSTrackingArea> closeTrackingArea_;
@@ -56,6 +57,10 @@
}
@property(assign) NSCellStateValue state;
@property(assign, nonatomic)CGFloat hoverAlpha;
+
+// Determines if the tab is in the process of animating closed. It may still
+// be visible on-screen, but should not respond to/initiate any events.
+@property(assign, nonatomic)BOOL isClosing;
@end
#endif // CHROME_BROWSER_COCOA_TAB_VIEW_H_
diff --git a/chrome/browser/cocoa/tab_view.mm b/chrome/browser/cocoa/tab_view.mm
index fd8c737..fb97299 100644
--- a/chrome/browser/cocoa/tab_view.mm
+++ b/chrome/browser/cocoa/tab_view.mm
@@ -21,6 +21,7 @@ static const NSTimeInterval kAnimationHideDuration = 0.4;
@synthesize state = state_;
@synthesize hoverAlpha = hoverAlpha_;
+@synthesize isClosing = isClosing_;
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
@@ -136,6 +137,8 @@ static const NSTimeInterval kAnimationHideDuration = 0.4;
// Returns |YES| if this tab can be torn away into a new window.
- (BOOL)canBeDragged {
+ if ([self isClosing])
+ return NO;
NSWindowController* controller = [sourceWindow_ windowController];
if ([controller isKindOfClass:[TabWindowController class]]) {
TabWindowController* realController =
@@ -189,6 +192,9 @@ static const NSTimeInterval kTearDuration = 0.333;
static const CGFloat kRapidCloseDist = 2.5;
- (void)mouseDown:(NSEvent*)theEvent {
+ if ([self isClosing])
+ return;
+
NSPoint downLocation = [theEvent locationInWindow];
// During the tab closure animation (in particular, during rapid tab closure),
@@ -232,7 +238,8 @@ static const CGFloat kRapidCloseDist = 2.5;
NSArray* targets = [self dropTargetsForController:sourceController_];
moveWindowOnDrag_ =
([sourceController_ numberOfTabs] < 2 && ![targets count]) ||
- ![self canBeDragged];
+ ![self canBeDragged] ||
+ ![sourceController_ tabDraggingAllowed];
// If we are dragging a tab, a window with a single tab should immediately
// snap off and not drag within the tab strip.
if (!moveWindowOnDrag_)
@@ -244,9 +251,9 @@ static const CGFloat kRapidCloseDist = 2.5;
// ourselves. Ideally we should use the standard event loop.
while (1) {
theEvent =
- [NSApp nextEventMatchingMask:NSLeftMouseUpMask | NSLeftMouseDraggedMask
- untilDate:[NSDate distantFuture]
- inMode:NSDefaultRunLoopMode dequeue:YES];
+ [NSApp nextEventMatchingMask:NSLeftMouseUpMask | NSLeftMouseDraggedMask
+ untilDate:[NSDate distantFuture]
+ inMode:NSDefaultRunLoopMode dequeue:YES];
NSPoint thisPoint = [NSEvent mouseLocation];
NSEventType type = [theEvent type];
@@ -550,6 +557,9 @@ static const CGFloat kRapidCloseDist = 2.5;
}
- (void)otherMouseUp:(NSEvent*)theEvent {
+ if ([self isClosing])
+ return;
+
// Support middle-click-to-close.
if ([theEvent buttonNumber] == 2) {
// |-hitTest:| takes a location in the superview's coordinates.
@@ -718,6 +728,8 @@ static const CGFloat kRapidCloseDist = 2.5;
// Called when the user hits the right mouse button (or control-clicks) to
// show a context menu.
- (void)rightMouseDown:(NSEvent*)theEvent {
+ if ([self isClosing])
+ return;
[NSMenu popUpContextMenu:[self menu] withEvent:theEvent forView:self];
}
diff --git a/chrome/browser/cocoa/tab_window_controller.h b/chrome/browser/cocoa/tab_window_controller.h
index ed3c23b..ba11bb9 100644
--- a/chrome/browser/cocoa/tab_window_controller.h
+++ b/chrome/browser/cocoa/tab_window_controller.h
@@ -72,6 +72,11 @@
// implementation.
- (void)removePlaceholder;
+// Returns YES if tab dragging is currently allowed. Any number of things
+// can choose to disable it, such as pending animations. The default is to
+// always return YES. Subclasses should override as appropriate.
+- (BOOL)tabDraggingAllowed;
+
// Show or hide the new tab button. The button is hidden immediately, but
// waits until the next call to |-layoutTabs| to show it again.
- (void)showNewTabButton:(BOOL)show;
diff --git a/chrome/browser/cocoa/tab_window_controller.mm b/chrome/browser/cocoa/tab_window_controller.mm
index 20267c8..b627214 100644
--- a/chrome/browser/cocoa/tab_window_controller.mm
+++ b/chrome/browser/cocoa/tab_window_controller.mm
@@ -162,6 +162,10 @@
[self showNewTabButton:YES];
}
+- (BOOL)tabDraggingAllowed {
+ return YES;
+}
+
- (BOOL)isTabFullyVisible:(TabView*)tab {
// Subclasses should implement this, but it's not necessary.
return YES;