summaryrefslogtreecommitdiffstats
path: root/chrome
diff options
context:
space:
mode:
Diffstat (limited to 'chrome')
-rw-r--r--chrome/browser/cocoa/browser_window_controller.mm5
-rw-r--r--chrome/browser/cocoa/tabpose_window.h41
-rw-r--r--chrome/browser/cocoa/tabpose_window.mm474
-rw-r--r--chrome/browser/cocoa/tabpose_window_unittest.mm33
4 files changed, 531 insertions, 22 deletions
diff --git a/chrome/browser/cocoa/browser_window_controller.mm b/chrome/browser/cocoa/browser_window_controller.mm
index 19b584d..d9373a8 100644
--- a/chrome/browser/cocoa/browser_window_controller.mm
+++ b/chrome/browser/cocoa/browser_window_controller.mm
@@ -1856,7 +1856,10 @@ willAnimateFromState:(bookmarks::VisualState)oldState
activeArea.size.height += NSHeight([bookmarkBarView frame]);
}
- [TabposeWindow openTabposeFor:[self window] rect:activeArea slomo:slomo];
+ [TabposeWindow openTabposeFor:[self window]
+ rect:activeArea
+ slomo:slomo
+ tabStripModel:browser_->tabstrip_model()];
}
@end // @implementation BrowserWindowController(Fullscreen)
diff --git a/chrome/browser/cocoa/tabpose_window.h b/chrome/browser/cocoa/tabpose_window.h
index 0efb90c..3be0b32 100644
--- a/chrome/browser/cocoa/tabpose_window.h
+++ b/chrome/browser/cocoa/tabpose_window.h
@@ -10,12 +10,38 @@
#include "base/scoped_cftyperef.h"
+#include "base/ref_counted.h"
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "base/scoped_vector.h"
+
+namespace tabpose {
+
+class Tile;
+class TileSet;
+
+}
+
+namespace tabpose {
+
+enum WindowState {
+ kFadingIn,
+ kFadedIn,
+ kFadingOut,
+};
+
+} // namespace tabpose
+
+class TabStripModel;
+
// A TabposeWindow shows an overview of open tabs and lets the user select a new
// active tab. The window blocks clicks on the tab strip and the download
// shelf. Every open browser window has its own overlay, and they are
// independent of each other.
@interface TabposeWindow : NSWindow {
@private
+ tabpose::WindowState state_;
+
// The root layer added to the content view. Covers the whole window.
CALayer* rootLayer_; // weak
@@ -23,14 +49,25 @@
CALayer* bgLayer_; // weak
scoped_cftyperef<CGColorRef> gray_;
+
+ TabStripModel* tabStripModel_; // weak
+
+ // Stores all preview layers. The order in here matches the order in
+ // the tabstrip model.
+ scoped_nsobject<NSArray> allLayers_;
+
+ // Manages the state of all layers.
+ scoped_ptr<tabpose::TileSet> tileSet_;
}
// Shows a TabposeWindow on top of |parent|, with |rect| being the active area.
// If |slomo| is YES, then the appearance animation is shown in slow motion.
// The window blocks all keyboard and mouse events and releases itself when
// closed.
-+ (id)openTabposeFor:(NSWindow*)parent rect:(NSRect)rect slomo:(BOOL)slomo;
++ (id)openTabposeFor:(NSWindow*)parent
+ rect:(NSRect)rect
+ slomo:(BOOL)slomo
+ tabStripModel:(TabStripModel*)tabStripModel;
@end
#endif // CHROME_BROWSER_COCOA_TABPOSE_WINDOW_H_
-
diff --git a/chrome/browser/cocoa/tabpose_window.mm b/chrome/browser/cocoa/tabpose_window.mm
index c69c5c1..1cad47a 100644
--- a/chrome/browser/cocoa/tabpose_window.mm
+++ b/chrome/browser/cocoa/tabpose_window.mm
@@ -6,8 +6,18 @@
#import <QuartzCore/QuartzCore.h>
+#import "chrome/browser/cocoa/browser_window_controller.h"
+#import "chrome/browser/cocoa/tab_strip_controller.h"
+
const int kTopGradientHeight = 15;
+NSString* const kAnimationIdKey = @"AnimationId";
+NSString* const kAnimationIdFadeIn = @"FadeIn";
+NSString* const kAnimationIdFadeOut = @"FadeOut";
+
+const CGFloat kDefaultAnimationDuration = 0.25; // In seconds.
+const CGFloat kSlomoFactor = 4;
+
// CAGradientLayer is 10.6-only -- roll our own.
@interface DarkGradientLayer : CALayer
- (void)drawInContext:(CGContextRef)context;
@@ -26,35 +36,356 @@ const int kTopGradientHeight = 15;
}
@end
+namespace {
+
+class ScopedCAActionDisabler {
+ public:
+ ScopedCAActionDisabler() {
+ [CATransaction begin];
+ [CATransaction setValue:[NSNumber numberWithBool:YES]
+ forKey:kCATransactionDisableActions];
+ }
+
+ ~ScopedCAActionDisabler() {
+ [CATransaction commit];
+ }
+};
+
+class ScopedCAActionSetDuration {
+ public:
+ explicit ScopedCAActionSetDuration(CGFloat duration) {
+ [CATransaction begin];
+ [CATransaction setValue:[NSNumber numberWithFloat:duration]
+ forKey:kCATransactionAnimationDuration];
+ }
+
+ ~ScopedCAActionSetDuration() {
+ [CATransaction commit];
+ }
+};
+
+} // namespace
+
+// Given the number |n| of tiles with a desired aspect ratio of |a| and a
+// desired distance |dx|, |dy| between tiles, returns how many tiles fit
+// vertically into a rectangle with the dimensions |w_c|, |h_c|. This returns
+// an exact solution, which is usually a fractional number.
+static float FitNRectsWithAspectIntoBoundingSizeWithConstantPadding(
+ int n, double a, int w_c, int h_c, int dx, int dy) {
+ // We want to have the small rects have the same aspect ratio a as a full
+ // tab. Let w, h be the size of a small rect, and w_c, h_c the size of the
+ // container. dx, dy are the distances between small rects in x, y direction.
+
+ // Geometry yields:
+ // w_c = nx * (w + dx) - dx <=> w = (w_c + d_x) / nx - d_x
+ // h_c = ny * (h + dy) - dy <=> h = (h_c + d_y) / ny - d_t
+ // Plugging this into
+ // a := tab_width / tab_height = w / h
+ // yields
+ // a = ((w_c - (nx - 1)*d_x)*ny) / (nx*(h_c - (ny - 1)*d_y))
+ // Plugging in nx = n/ny and pen and paper (or wolfram alpha:
+ // http://www.wolframalpha.com/input/?i=(-sqrt((d+n-a+f+n)^2-4+(a+f%2Ba+h)+(-d+n-n+w))%2Ba+f+n-d+n)/(2+a+(f%2Bh)) , (solution for nx)
+ // http://www.wolframalpha.com/input/?i=+(-sqrt((a+f+n-d+n)^2-4+(d%2Bw)+(-a+f+n-a+h+n))-a+f+n%2Bd+n)/(2+(d%2Bw)) , (solution for ny)
+ // ) gives us nx and ny (but the wrong root -- s/-sqrt(FOO)/sqrt(FOO)/.
+
+ // This function returns ny.
+ return (sqrt(pow(n * (a * dy - dx), 2) +
+ 4 * n * a * (dx + w_c) * (dy + h_c)) -
+ n * (a * dy - dx))
+ /
+ (2 * (dx + w_c));
+}
+
+namespace tabpose {
+
+// A tile is what is shown for a single tab in tabpose mode. It consists of a
+// title, favicon, thumbnail image, and pre- and postanimation rects.
+// TODO(thakis): Right now, it only consists of a thumb rect.
+class Tile {
+ public:
+ // Returns the rectangle this thumbnail is at at the beginning of the zoom-in
+ // animation. |tile| is the rectangle that's covering the whole tab area when
+ // the animation starts.
+ NSRect GetStartRectRelativeTo(const Tile& tile) const;
+ NSRect thumb_rect() const { return thumb_rect_; }
+
+ private:
+ friend class TileSet;
+
+ // The thumb rect includes infobars, detached thumbnail bar, web contents,
+ // and devtools.
+ NSRect thumb_rect_;
+ NSRect start_thumb_rect_;
+};
+
+NSRect Tile::GetStartRectRelativeTo(const Tile& tile) const {
+ NSRect rect = start_thumb_rect_;
+ rect.origin.x -= tile.start_thumb_rect_.origin.x;
+ rect.origin.y -= tile.start_thumb_rect_.origin.y;
+ return rect;
+}
+
+// A tileset is responsible for owning and laying out all |Tile|s shown in a
+// tabpose window.
+class TileSet {
+ public:
+ // Fills in |tiles_|.
+ void Build(TabStripModel* source_model);
+
+ // Computes coordinates for |tiles_|.
+ void Layout(NSRect containing_rect);
+
+ int selected_index() const { return selected_index_; }
+ void set_selected_index(int index);
+ void ResetSelectedIndex() { selected_index_ = initial_index_; }
+
+ const Tile& selected_tile() const { return tiles_[selected_index()]; }
+ const Tile& tile_at(int index) const { return tiles_[index]; }
+
+ private:
+ std::vector<Tile> tiles_; // Doesn't change often, hence values are fine.
+
+ int selected_index_;
+ int initial_index_;
+};
+
+void TileSet::Build(TabStripModel* source_model) {
+ selected_index_ = initial_index_ = source_model->selected_index();
+ tiles_.resize(source_model->count());
+}
+
+void TileSet::Layout(NSRect containing_rect) {
+ int tile_count = tiles_.size();
+
+ // Room around the tiles insde of |containing_rect|.
+ const int kSmallPaddingTop = 30;
+ const int kSmallPaddingLeft = 30;
+ const int kSmallPaddingRight = 30;
+ const int kSmallPaddingBottom = 30;
+
+ // Room between the tiles.
+ const int kSmallPaddingX = 15;
+ const int kSmallPaddingY = 13;
+
+ // Aspect ratio of the containing rect.
+ CGFloat aspect = NSWidth(containing_rect) / NSHeight(containing_rect);
+
+ // Room left in container after the outer padding is removed.
+ double container_width =
+ NSWidth(containing_rect) - kSmallPaddingLeft - kSmallPaddingRight;
+ double container_height =
+ NSHeight(containing_rect) - kSmallPaddingTop - kSmallPaddingBottom;
+
+ // The tricky part is figuring out the size of a tab thumbnail, or since the
+ // size of the containing rect is known, the number of tiles in x and y
+ // direction.
+ // Given are the size of the containing rect, and the number of thumbnails
+ // that need to fit into that rect. The aspect ratio of the thumbnails needs
+ // to be the same as that of |containing_rect|, else they will look distorted.
+ // The thumbnails need to be distributed such that
+ // |count_x * count_y >= tile_count|, and such that wasted space is minimized.
+ // See the comments in
+ // |FitNRectsWithAspectIntoBoundingSizeWithConstantPadding()| for a more
+ // detailed discussion.
+ // TODO(thakis): It might be good enough to choose |count_x| and |count_y|
+ // such that count_x / count_y is roughly equal to |aspect|?
+ double fny = FitNRectsWithAspectIntoBoundingSizeWithConstantPadding(
+ tile_count, aspect, container_width, container_height,
+ kSmallPaddingX, kSmallPaddingY);
+ int count_y(roundf(fny));
+ int count_x(ceilf(tile_count / float(count_y)));
+ int last_row_count_x = tile_count - count_x * (count_y - 1);
+
+ // Now that |count_x| and |count_y| are known, it's straightforward to compute
+ // thumbnail width/height. See comment in
+ // |FitNRectsWithAspectIntoBoundingSizeWithConstantPadding| for the derivation
+ // of these two formulas.
+ int small_width =
+ floor((container_width + kSmallPaddingX) / float(count_x) -
+ kSmallPaddingX);
+ int small_height =
+ floor((container_height + kSmallPaddingY) / float(count_y) -
+ kSmallPaddingY);
+
+ // |small_width / small_height| has only roughly an aspect ratio of |aspect|.
+ // Shrink the thumbnail rect to make the aspect ratio fit exactly, and add
+ // the extra space won by shrinking to the outer padding.
+ int smallExtraPaddingLeft = 0;
+ int smallExtraPaddingTop = 0;
+ if (aspect > small_width/float(small_height)) {
+ small_height = small_width / aspect;
+ CGFloat all_tiles_height =
+ (small_height + kSmallPaddingY) * count_y - kSmallPaddingY;
+ smallExtraPaddingTop = (container_height - all_tiles_height)/2;
+ } else {
+ small_width = small_height * aspect;
+ CGFloat all_tiles_width =
+ (small_width + kSmallPaddingX) * count_x - kSmallPaddingX;
+ smallExtraPaddingLeft = (container_width - all_tiles_width)/2;
+ }
+
+ // Compute inter-tile padding in the zoomed-out view.
+ CGFloat scale_small_to_big = NSWidth(containing_rect) / float(small_width);
+ CGFloat big_padding_x = kSmallPaddingX * scale_small_to_big;
+ CGFloat big_padding_y = kSmallPaddingY * scale_small_to_big;
+
+ // Now all dimensions are known. Lay out all tiles on a regular grid:
+ // X X X X
+ // X X X X
+ // X X
+ for (int row = 0, i = 0; i < tile_count; ++row) {
+ for (int col = 0; col < count_x && i < tile_count; ++col, ++i) {
+ // Compute the smalled, zoomed-out thumbnail rect.
+ tiles_[i].thumb_rect_.size = NSMakeSize(small_width, small_height);
+
+ int small_x = col * (small_width + kSmallPaddingX) +
+ kSmallPaddingLeft + smallExtraPaddingLeft;
+ int small_y = row * (small_height + kSmallPaddingY) +
+ kSmallPaddingTop + smallExtraPaddingTop;
+
+ tiles_[i].thumb_rect_.origin = NSMakePoint(
+ small_x, NSHeight(containing_rect) - small_y - small_height);
+
+ // Compute the big, pre-zoom thumbnail rect.
+ tiles_[i].start_thumb_rect_.size = containing_rect.size;
+
+ int big_x = col * (NSWidth(containing_rect) + big_padding_x);
+ int big_y = row * (NSHeight(containing_rect) + big_padding_y);
+ tiles_[i].start_thumb_rect_.origin = NSMakePoint(big_x, -big_y);
+ }
+ }
+
+ // Go through last row and center it:
+ // X X X X
+ // X X X X
+ // X X
+ int last_row_empty_tiles_x = count_x - last_row_count_x;
+ CGFloat small_last_row_shift_x =
+ last_row_empty_tiles_x * (small_width + kSmallPaddingX) / 2;
+ CGFloat big_last_row_shift_x =
+ last_row_empty_tiles_x * (NSWidth(containing_rect) + big_padding_x) / 2;
+ for (int i = tile_count - last_row_count_x; i < tile_count; ++i) {
+ tiles_[i].thumb_rect_.origin.x += small_last_row_shift_x;
+ tiles_[i].start_thumb_rect_.origin.x += big_last_row_shift_x;
+ }
+}
+
+void TileSet::set_selected_index(int index) {
+ CHECK_GE(index, 0);
+ CHECK_LT(index, static_cast<int>(tiles_.size()));
+ selected_index_ = index;
+}
+
+} // namespace tabpose
+
+void AnimateCALayerFrameFromTo(
+ CALayer* layer, const NSRect& from, const NSRect& to,
+ NSTimeInterval duration, id boundsAnimationDelegate) {
+ // http://developer.apple.com/mac/library/qa/qa2008/qa1620.html
+ CABasicAnimation* animation;
+
+ animation = [CABasicAnimation animationWithKeyPath:@"bounds"];
+ animation.fromValue = [NSValue valueWithRect:from];
+ animation.toValue = [NSValue valueWithRect:to];
+ animation.duration = duration;
+ animation.timingFunction =
+ [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
+ animation.delegate = boundsAnimationDelegate;
+
+ // Update the layer's bounds so the layer doesn't snap back when the animation
+ // completes.
+ layer.bounds = NSRectToCGRect(to);
+
+ // Add the animation, overriding the implicit animation.
+ [layer addAnimation:animation forKey:@"bounds"];
+
+ // Prepare the animation from the current position to the new position.
+ NSPoint opoint = from.origin;
+ NSPoint point = to.origin;
+
+ // Adapt to anchorPoint.
+ opoint.x += NSWidth(from) * layer.anchorPoint.x;
+ opoint.y += NSHeight(from) * layer.anchorPoint.y;
+ point.x += NSWidth(to) * layer.anchorPoint.x;
+ point.y += NSHeight(to) * layer.anchorPoint.y;
+
+ animation = [CABasicAnimation animationWithKeyPath:@"position"];
+ animation.fromValue = [NSValue valueWithPoint:opoint];
+ animation.toValue = [NSValue valueWithPoint:point];
+ animation.duration = duration;
+ animation.timingFunction =
+ [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
+
+ // Update the layer's position so that the layer doesn't snap back when the
+ // animation completes.
+ layer.position = NSPointToCGPoint(point);
+
+ // Add the animation, overriding the implicit animation.
+ [layer addAnimation:animation forKey:@"position"];
+}
+
@interface TabposeWindow (Private)
-- (id)initForWindow:(NSWindow*)parent rect:(NSRect)rect slomo:(BOOL)slomo;
-- (void)setUpLayers:(NSRect)bgLayerRect;
+- (id)initForWindow:(NSWindow*)parent
+ rect:(NSRect)rect
+ slomo:(BOOL)slomo
+ tabStripModel:(TabStripModel*)tabStripModel;
+- (void)setUpLayers:(NSRect)bgLayerRect slomo:(BOOL)slomo;
+- (void)fadeAway:(BOOL)slomo;
+- (void)selectTileAtIndex:(int)newIndex;
@end
@implementation TabposeWindow
-+ (id)openTabposeFor:(NSWindow*)parent rect:(NSRect)rect slomo:(BOOL)slomo {
++ (id)openTabposeFor:(NSWindow*)parent
+ rect:(NSRect)rect
+ slomo:(BOOL)slomo
+ tabStripModel:(TabStripModel*)tabStripModel {
// Releases itself when closed.
- return [[TabposeWindow alloc] initForWindow:parent rect:rect slomo:slomo];
+ return [[TabposeWindow alloc]
+ initForWindow:parent rect:rect slomo:slomo tabStripModel:tabStripModel];
}
-- (id)initForWindow:(NSWindow*)parent rect:(NSRect)rect slomo:(BOOL)slomo {
+- (id)initForWindow:(NSWindow*)parent
+ rect:(NSRect)rect
+ slomo:(BOOL)slomo
+ tabStripModel:(TabStripModel*)tabStripModel {
NSRect frame = [parent frame];
if ((self = [super initWithContentRect:frame
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered
defer:NO])) {
+ // TODO(thakis): Add a TabStripModelObserver to |tabStripModel_|.
+ tabStripModel_ = tabStripModel;
+ state_ = tabpose::kFadingIn;
+ tileSet_.reset(new tabpose::TileSet);
[self setReleasedWhenClosed:YES];
[self setOpaque:NO];
[self setBackgroundColor:[NSColor clearColor]];
- [self setUpLayers:rect];
+ [self setUpLayers:rect slomo:slomo];
+ [self setAcceptsMouseMovedEvents:YES];
[parent addChildWindow:self ordered:NSWindowAbove];
[self makeKeyAndOrderFront:self];
}
return self;
}
-- (void)setUpLayers:(NSRect)bgLayerRect {
+- (CALayer*)selectedLayer {
+ return [allLayers_ objectAtIndex:tileSet_->selected_index()];
+}
+
+- (void)selectTileAtIndex:(int)newIndex {
+ // TODO(thakis): Have a nicer indicator for the current selection
+ // (a blue outline, probably).
+ int oldIndex = tileSet_->selected_index();
+ CALayer* oldSelectedLayer = [allLayers_ objectAtIndex:oldIndex];
+ oldSelectedLayer.backgroundColor = CGColorGetConstantColor(kCGColorBlack);
+ CALayer* newSelectedLayer = [allLayers_ objectAtIndex:newIndex];
+ newSelectedLayer.backgroundColor = CGColorGetConstantColor(kCGColorWhite);
+
+ tileSet_->set_selected_index(newIndex);
+}
+
+- (void)setUpLayers:(NSRect)bgLayerRect slomo:(BOOL)slomo {
// Root layer -- covers whole window.
rootLayer_ = [CALayer layer];
[[self contentView] setLayer:rootLayer_];
@@ -77,6 +408,41 @@ const int kTopGradientHeight = 15;
kTopGradientHeight);
[gradientLayer setNeedsDisplay]; // Draw once.
[bgLayer_ addSublayer:gradientLayer];
+
+ // Layers for the tab thumbnails.
+ tileSet_->Build(tabStripModel_);
+ tileSet_->Layout(bgLayerRect);
+
+ allLayers_.reset(
+ [[NSMutableArray alloc] initWithCapacity:tabStripModel_->count()]);
+ for (int i = 0; i < tabStripModel_->count(); ++i) {
+ CALayer* layer = [CALayer layer];
+
+ // Background color as placeholder for now.
+ layer.backgroundColor = CGColorGetConstantColor(kCGColorBlack);
+
+ AnimateCALayerFrameFromTo(
+ layer,
+ tileSet_->tile_at(i).GetStartRectRelativeTo(tileSet_->selected_tile()),
+ tileSet_->tile_at(i).thumb_rect(),
+ kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1),
+ i == tileSet_->selected_index() ? self : nil);
+
+ // Add a delegate to one of the animations to get a notification once the
+ // animations are done.
+ if (i == tileSet_->selected_index()) {
+ CAAnimation* animation = [layer animationForKey:@"bounds"];
+ DCHECK(animation);
+ [animation setValue:kAnimationIdFadeIn forKey:kAnimationIdKey];
+ }
+
+ layer.shadowRadius = 10;
+ layer.shadowOffset = CGSizeMake(0, -10);
+
+ [bgLayer_ addSublayer:layer];
+ [allLayers_ addObject:layer];
+ }
+ [self selectTileAtIndex:tileSet_->selected_index()];
}
- (BOOL)canBecomeKeyWindow {
@@ -88,6 +454,9 @@ const int kTopGradientHeight = 15;
}
- (void)keyUp:(NSEvent*)event {
+ if (state_ == tabpose::kFadingOut)
+ return;
+
NSString* characters = [event characters];
if ([characters length] < 1)
return;
@@ -98,19 +467,36 @@ const int kTopGradientHeight = 15;
case NSNewlineCharacter:
case NSCarriageReturnCharacter:
case ' ':
+ [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0];
+ break;
case '\e': // Escape
- [self close];
+ tileSet_->ResetSelectedIndex();
+ [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0];
break;
+ // TODO(thakis): Support moving the selection via arrow keys.
}
}
+- (void)mouseMoved:(NSEvent*)event {
+ int newIndex = -1;
+ CGPoint p = NSPointToCGPoint([event locationInWindow]);
+ for (NSUInteger i = 0; i < [allLayers_ count]; ++i) {
+ CALayer* layer = [allLayers_ objectAtIndex:i];
+ CGPoint lp = [layer convertPoint:p fromLayer:rootLayer_];
+ if ([static_cast<CALayer*>([layer presentationLayer]) containsPoint:lp])
+ newIndex = i;
+ }
+ if (newIndex >= 0)
+ [self selectTileAtIndex:newIndex];
+}
+
- (void)mouseDown:(NSEvent*)event {
- [self close];
+ [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0];
}
- (void)swipeWithEvent:(NSEvent*)event {
if ([event deltaY] > 0.5) // Swipe up
- [self close];
+ [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0];
}
- (void)close {
@@ -128,4 +514,72 @@ const int kTopGradientHeight = 15;
return NO;
}
+- (void)fadeAway:(BOOL)slomo {
+ if (state_ == tabpose::kFadingOut)
+ return;
+
+ state_ = tabpose::kFadingOut;
+ [self setAcceptsMouseMovedEvents:NO];
+
+ // Select chosen tab.
+ tabStripModel_->SelectTabContentsAt(tileSet_->selected_index(),
+ /*user_gesture=*/true);
+
+ {
+ ScopedCAActionDisabler disableCAActions;
+
+ // Move the selected layer on top of all other layers.
+ [self selectedLayer].zPosition = 1;
+
+ // Running animations with shadows is slow, so turn shadows off before
+ // running the exit animation.
+ for (CALayer* layer in allLayers_.get())
+ layer.shadowOpacity = 0.0;
+ }
+
+ // Animate layers out, all in one transaction.
+ CGFloat duration = kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1);
+ ScopedCAActionSetDuration durationSetter(duration);
+ for (NSUInteger i = 0; i < [allLayers_ count]; ++i) {
+ CALayer* layer = [allLayers_ objectAtIndex:i];
+ // |start_thumb_rect_| was relative to |initial_index_|, now this needs to
+ // be relative to |selectedIndex_| (whose start rect was relative to
+ // |initial_index_| too)
+ CGRect newFrame = NSRectToCGRect(
+ tileSet_->tile_at(i).GetStartRectRelativeTo(tileSet_->selected_tile()));
+
+ // Add a delegate to one of the implicit animations to get a notification
+ // once the animations are done.
+ if (static_cast<int>(i) == tileSet_->selected_index()) {
+ CAAnimation* animation = [CAAnimation animation];
+ animation.delegate = self;
+ [animation setValue:kAnimationIdFadeOut forKey:kAnimationIdKey];
+ [layer addAnimation:animation forKey:@"frame"];
+ }
+
+ layer.frame = newFrame;
+ }
+}
+
+- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
+ NSString* animationId = [animation valueForKey:kAnimationIdKey];
+ if ([animationId isEqualToString:kAnimationIdFadeIn]) {
+ if (finished) {
+ // If the user clicks while the fade in animation is still running,
+ // |state_| is already kFadingOut. In that case, don't do anything.
+ DCHECK_EQ(tabpose::kFadingIn, state_);
+ state_ = tabpose::kFadedIn;
+
+ // Running animations with shadows is slow, so turn shadows on only after
+ // the animation is done.
+ ScopedCAActionDisabler disableCAActions;
+ for (CALayer* layer in allLayers_.get())
+ layer.shadowOpacity = 0.5;
+ }
+ } else if ([animationId isEqualToString:kAnimationIdFadeOut]) {
+ DCHECK_EQ(tabpose::kFadingOut, state_);
+ [self close];
+ }
+}
+
@end
diff --git a/chrome/browser/cocoa/tabpose_window_unittest.mm b/chrome/browser/cocoa/tabpose_window_unittest.mm
index 8f6f239..ad10250 100644
--- a/chrome/browser/cocoa/tabpose_window_unittest.mm
+++ b/chrome/browser/cocoa/tabpose_window_unittest.mm
@@ -4,9 +4,11 @@
#import "chrome/browser/cocoa/tabpose_window.h"
-#include "base/scoped_nsobject.h"
+#import "chrome/browser/browser_window.h"
#import "chrome/browser/cocoa/browser_test_helper.h"
#import "chrome/browser/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/renderer_host/site_instance.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
#include "testing/gtest/include/gtest/gtest.h"
class TabposeWindowTest : public CocoaTest {
@@ -16,20 +18,33 @@ class TabposeWindowTest : public CocoaTest {
// Check that this doesn't leak.
TEST_F(TabposeWindowTest, TestShow) {
- scoped_nsobject<NSWindow> parent([[NSWindow alloc]
- initWithContentRect:NSMakeRect(100, 200, 300, 200)
- styleMask:NSTitledWindowMask
- backing:NSBackingStoreBuffered
- defer:NO]);
+ Browser* browser = browser_helper_.browser();
+ BrowserWindow* browser_window = browser_helper_.CreateBrowserWindow();
+ NSWindow* parent = browser_window->GetNativeHandle();
+
[parent orderFront:nil];
EXPECT_TRUE([parent isVisible]);
+ // Add a few tabs to the tab strip model.
+ TabStripModel* model = browser->tabstrip_model();
+ SiteInstance* instance =
+ SiteInstance::CreateSiteInstance(browser_helper_.profile());
+ for (int i = 0; i < 3; ++i) {
+ TabContents* tab_contents =
+ new TabContents(browser_helper_.profile(), instance, MSG_ROUTING_NONE,
+ NULL);
+ model->AppendTabContents(tab_contents, /*foreground=*/true);
+ }
+
base::ScopedNSAutoreleasePool pool;
TabposeWindow* window =
- [TabposeWindow openTabposeFor:parent.get()
- rect:NSMakeRect(10, 20, 50, 60)
- slomo:NO];
+ [TabposeWindow openTabposeFor:parent
+ rect:NSMakeRect(10, 20, 250, 160)
+ slomo:NO
+ tabStripModel:model];
// Should release the window.
[window mouseDown:nil];
+
+ browser_helper_.CloseBrowserWindow();
}