diff options
-rw-r--r-- | base/scoped_vector.h | 24 | ||||
-rw-r--r-- | chrome/browser/cocoa/tab_strip_controller.h | 1 | ||||
-rw-r--r-- | chrome/browser/cocoa/tabpose_window.h | 14 | ||||
-rw-r--r-- | chrome/browser/cocoa/tabpose_window.mm | 720 | ||||
-rw-r--r-- | chrome/browser/cocoa/tabpose_window_unittest.mm | 88 |
5 files changed, 733 insertions, 114 deletions
diff --git a/base/scoped_vector.h b/base/scoped_vector.h index 34620e6..ec152c9 100644 --- a/base/scoped_vector.h +++ b/base/scoped_vector.h @@ -56,6 +56,30 @@ class ScopedVector { void reset() { STLDeleteElements(&v); } void resize(size_t new_size) { v.resize(new_size); } + // Lets the ScopedVector take ownership of |x|. + iterator insert(iterator position, T* x) { + return v.insert(position, x); + } + + iterator erase(iterator position) { + delete *position; + return v.erase(position); + } + + iterator erase(iterator first, iterator last) { + STLDeleteContainerPointers(first, last); + return v.erase(first, last); + } + + // Like |erase()|, but doesn't delete the element at |position|. + iterator weak_erase(iterator position) { + return v.erase(position); + } + + // Like |erase()|, but doesn't delete the elements in [first, last). + iterator weak_erase(iterator first, iterator last) { + return v.erase(first, last); + } private: std::vector<T*> v; diff --git a/chrome/browser/cocoa/tab_strip_controller.h b/chrome/browser/cocoa/tab_strip_controller.h index 1751a15..e03267b 100644 --- a/chrome/browser/cocoa/tab_strip_controller.h +++ b/chrome/browser/cocoa/tab_strip_controller.h @@ -42,7 +42,6 @@ class ToolbarModel; BOOL verticalLayout_; @private - TabContents* currentTab_; // weak, tab for which we're showing state scoped_nsobject<TabStripView> tabStripView_; NSView* switchView_; // weak scoped_nsobject<NSView> dragBlockingView_; // avoid bad window server drags diff --git a/chrome/browser/cocoa/tabpose_window.h b/chrome/browser/cocoa/tabpose_window.h index 72acced..39958a2 100644 --- a/chrome/browser/cocoa/tabpose_window.h +++ b/chrome/browser/cocoa/tabpose_window.h @@ -32,6 +32,7 @@ enum WindowState { } // namespace tabpose class TabStripModel; +class TabStripModelObserverBridge; // 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 @@ -65,6 +66,13 @@ class TabStripModel; // Manages the state of all layers. scoped_ptr<tabpose::TileSet> tileSet_; + + // The rectangle of the window that contains all layers. This is the + // rectangle occupied by |bgLayer_|. + NSRect containingRect_; + + // Informs us of added/removed/updated tabs. + scoped_ptr<TabStripModelObserverBridge> tabStripModelObserverBridge_; } // Shows a TabposeWindow on top of |parent|, with |rect| being the active area. @@ -77,4 +85,10 @@ class TabStripModel; tabStripModel:(TabStripModel*)tabStripModel; @end +@interface TabposeWindow (TestingAPI) +- (void)selectTileAtIndexWithoutAnimation:(int)newIndex; +- (NSUInteger)thumbnailLayerCount; +- (int)selectedIndex; +@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 f96d040..b74f9ca 100644 --- a/chrome/browser/cocoa/tabpose_window.mm +++ b/chrome/browser/cocoa/tabpose_window.mm @@ -8,12 +8,24 @@ #include "app/resource_bundle.h" #include "base/mac_util.h" +#include "base/scoped_callback_factory.h" #include "base/sys_string_conversions.h" +#include "chrome/browser/browser_process.h" +#import "chrome/browser/cocoa/bookmark_bar_constants.h" #import "chrome/browser/cocoa/browser_window_controller.h" #import "chrome/browser/cocoa/tab_strip_controller.h" +#import "chrome/browser/cocoa/tab_strip_model_observer_bridge.h" +#import "chrome/browser/debugger/devtools_window.h" +#include "chrome/browser/prefs/pref_service.h" +#include "chrome/browser/renderer_host/backing_store_mac.h" +#include "chrome/browser/renderer_host/render_view_host.h" +#include "chrome/browser/renderer_host/render_widget_host_view_mac.h" #include "chrome/browser/tab_contents/tab_contents.h" +#include "chrome/browser/tab_contents/thumbnail_generator.h" +#include "chrome/common/pref_names.h" #include "grit/app_resources.h" #include "skia/ext/skia_utils_mac.h" +#include "third_party/skia/include/utils/mac/SkCGUtils.h" const int kTopGradientHeight = 15; @@ -23,6 +35,7 @@ NSString* const kAnimationIdFadeOut = @"FadeOut"; const CGFloat kDefaultAnimationDuration = 0.25; // In seconds. const CGFloat kSlomoFactor = 4; +const CGFloat kObserverChangeAnimationDuration = 0.75; // In seconds. // CAGradientLayer is 10.6-only -- roll our own. @interface DarkGradientLayer : CALayer @@ -42,6 +55,247 @@ const CGFloat kSlomoFactor = 4; } @end +namespace tabpose { +class ThumbnailLoader; +} + +// A CALayer that draws a thumbnail for a TabContents object. The layer tries +// to draw the TabContents's backing store directly if possible, and requests +// a thumbnail bitmap from the TabContents's renderer process if not. +@interface ThumbnailLayer : CALayer { + // The TabContents the thumbnail is for. + TabContents* contents_; // weak + + // The size the thumbnail is drawn at when zoomed in. + NSSize fullSize_; + + // Used to load a thumbnail, if required. + scoped_refptr<tabpose::ThumbnailLoader> loader_; + + // If the backing store couldn't be used and a thumbnail was returned from a + // renderer process, it's stored in |thumbnail_|. + scoped_cftyperef<CGImageRef> thumbnail_; + + // True if the layer already sent a thumbnail request to a renderer. + BOOL didSendLoad_; +} +- (id)initWithTabContents:(TabContents*)contents fullSize:(NSSize)fullSize; +- (void)drawInContext:(CGContextRef)context; +- (void)setThumbnail:(const SkBitmap&)bitmap; +@end + +namespace tabpose { + +// ThumbnailLoader talks to the renderer process to load a thumbnail of a given +// RenderWidgetHost, and sends the thumbnail back to a ThumbnailLayer once it +// comes back from the renderer. +class ThumbnailLoader : public base::RefCountedThreadSafe<ThumbnailLoader> { + public: + ThumbnailLoader(gfx::Size size, RenderWidgetHost* rwh, ThumbnailLayer* layer) + : size_(size), rwh_(rwh), layer_(layer), factory_(this) {} + + // Starts the fetch. + void LoadThumbnail(); + + private: + friend class base::RefCountedThreadSafe<ThumbnailLoader>; + ~ThumbnailLoader() { + ResetPaintingObserver(); + } + + void DidReceiveBitmap(const SkBitmap& bitmap) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + ResetPaintingObserver(); + [layer_ setThumbnail:bitmap]; + } + + void ResetPaintingObserver() { + if (rwh_->painting_observer() != NULL) { + DCHECK(rwh_->painting_observer() == + g_browser_process->GetThumbnailGenerator()); + rwh_->set_painting_observer(NULL); + } + } + + gfx::Size size_; + RenderWidgetHost* rwh_; // weak + ThumbnailLayer* layer_; // weak, owns us + base::ScopedCallbackFactory<ThumbnailLoader> factory_; + + DISALLOW_COPY_AND_ASSIGN(ThumbnailLoader); +}; + +void ThumbnailLoader::LoadThumbnail() { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + ThumbnailGenerator* generator = g_browser_process->GetThumbnailGenerator(); + if (!generator) // In unit tests. + return; + + // As mentioned in ThumbnailLayer's -drawInContext:, it's sufficient to have + // thumbnails at the zoomed-out pixel size for all but the thumbnail the user + // clicks on in the end. But we don't don't which thumbnail that will be, so + // keep it simple and request full thumbnails for everything. + // TODO(thakis): Request smaller thumbnails for users with many tabs. + gfx::Size page_size(size_); // Logical size the renderer renders at. + gfx::Size pixel_size(size_); // Physical pixel size the image is rendered at. + + DCHECK(rwh_->painting_observer() == NULL || + rwh_->painting_observer() == generator); + rwh_->set_painting_observer(generator); + + // Will send an IPC to the renderer on the IO thread. + generator->AskForSnapshot( + rwh_, + /*prefer_backing_store=*/false, + factory_.NewCallback(&ThumbnailLoader::DidReceiveBitmap), + page_size, + pixel_size); +} + +} // namespace tabpose + +@implementation ThumbnailLayer + +- (id)initWithTabContents:(TabContents*)contents fullSize:(NSSize)fullSize { + CHECK(contents); + if ((self = [super init])) { + contents_ = contents; + fullSize_ = fullSize; + } + return self; +} + +- (void)setTabContents:(TabContents*)contents { + contents_ = contents; +} + +- (void)setThumbnail:(const SkBitmap&)bitmap { + // SkCreateCGImageRef() holds on to |bitmaps|'s memory, so this doesn't + // create a copy. + thumbnail_.reset(SkCreateCGImageRef(bitmap)); + loader_ = NULL; + [self setNeedsDisplay]; +} + +- (int)topOffset { + int topOffset = 0; + + // Medium term, we want to show thumbs of the actual info bar views, which + // means I need to create InfoBarControllers here. At that point, we can get + // the height from that controller. Until then, hardcode. :-/ + const int kInfoBarHeight = 31; + topOffset += contents_->infobar_delegate_count() * kInfoBarHeight; + + bool always_show_bookmark_bar = + contents_->profile()->GetPrefs()->GetBoolean(prefs::kShowBookmarkBar); + bool has_detached_bookmark_bar = + contents_->ShouldShowBookmarkBar() && !always_show_bookmark_bar; + if (has_detached_bookmark_bar) + topOffset += bookmarks::kNTPBookmarkBarHeight; + + return topOffset; +} + +- (int)bottomOffset { + int bottomOffset = 0; + TabContents* devToolsContents = + DevToolsWindow::GetDevToolsContents(contents_); + if (devToolsContents && devToolsContents->render_view_host() && + devToolsContents->render_view_host()->view()) { + // The devtool's size might not be up-to-date, but since its height doesn't + // change on window resize, and since most users don't use devtools, this is + // good enough. + bottomOffset += + devToolsContents->render_view_host()->view()->GetViewBounds().height(); + bottomOffset += 1; // :-( Divider line between web contents and devtools. + } + return bottomOffset; +} + +- (void)drawBackingStore:(BackingStoreMac*)backing_store + inRect:(CGRect)destRect + context:(CGContextRef)context { + // TODO(thakis): Add a sublayer for each accelerated surface in the rwhv. + // Until then, accelerated layers (CoreAnimation NPAPI plugins, compositor) + // won't show up in tabpose. + if (backing_store->cg_layer()) { + CGContextDrawLayerInRect(context, destRect, backing_store->cg_layer()); + } else { + scoped_cftyperef<CGImageRef> image( + CGBitmapContextCreateImage(backing_store->cg_bitmap())); + CGContextDrawImage(context, destRect, image); + } +} + +- (void)drawInContext:(CGContextRef)context { + RenderWidgetHost* rwh = contents_->render_view_host(); + RenderWidgetHostView* rwhv = rwh->view(); // NULL if renderer crashed. + if (!rwhv) { + // TODO(thakis): Maybe draw a sad tab layer? + [super drawInContext:context]; + return; + } + + // The size of the TabContent's RenderWidgetHost might not fit to the + // current browser window at all, for example if the window was resized while + // this TabContents object was not an active tab. + // Compute the required size ourselves. Leave room for eventual infobars and + // a detached bookmarks bar on the top, and for the devtools on the bottom. + // Download shelf is not included in the |fullSize| rect, so no need to + // correct for it here. + // TODO(thakis): This is not resolution-independent. + int topOffset = [self topOffset]; + int bottomOffset = [self bottomOffset]; + gfx::Size desiredThumbSize(fullSize_.width, + fullSize_.height - topOffset - bottomOffset); + + // We need to ask the renderer for a thumbnail if + // a) there's no backing store or + // b) the backing store's size doesn't match our required size and + // c) we didn't already send a thumbnail request to the renderer. + BackingStoreMac* backing_store = + (BackingStoreMac*)rwh->GetBackingStore(/*force_create=*/false); + bool draw_backing_store = + backing_store && backing_store->size() == desiredThumbSize; + + // Next weirdness: The destination rect. If the layer is |fullSize_| big, the + // destination rect is (0, bottomOffset), (fullSize_.width, topOffset). But we + // might be amidst an animation, so interpolate that rect. + CGRect destRect = [self bounds]; + CGFloat scale = destRect.size.width / fullSize_.width; + destRect.origin.y += bottomOffset * scale; + destRect.size.height -= (bottomOffset + topOffset) * scale; + + // TODO(thakis): Draw infobars, detached bookmark bar as well. + + // If we haven't already, sent a thumbnail request to the renderer. + if (!draw_backing_store && !didSendLoad_) { + // Either the tab was never visible, or its backing store got evicted, or + // the size of the backing store is wrong. + + // We only need a thumbnail the size of the zoomed-out layer for all + // layers except the one the user clicks on. But since we can't know which + // layer that is, request full-resolution layers for all tabs. This is + // simple and seems to work in practice. + loader_ = new tabpose::ThumbnailLoader(desiredThumbSize, rwh, self); + loader_->LoadThumbnail(); + didSendLoad_ = YES; + + // Fill with bg color. + [super drawInContext:context]; + } + + if (draw_backing_store) { + // Backing store 'cache' hit! + [self drawBackingStore:backing_store inRect:destRect context:context]; + } else if (thumbnail_) { + // No cache hit, but the renderer returned a thumbnail to us. + CGContextDrawImage(context, destRect, thumbnail_.get()); + } +} + +@end + namespace { class ScopedCAActionDisabler { @@ -106,9 +360,10 @@ 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: + Tile() {} + // 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. @@ -133,6 +388,10 @@ class Tile { // Returns an unelided title. The view logic is responsible for eliding. const string16& title() const { return contents_->GetTitle(); } + + TabContents* tab_contents() const { return contents_; } + void set_tab_contents(TabContents* new_contents) { contents_ = new_contents; } + private: friend class TileSet; @@ -147,6 +406,8 @@ class Tile { NSRect title_rect_; TabContents* contents_; // weak + + DISALLOW_COPY_AND_ASSIGN(Tile); }; NSRect Tile::GetStartRectRelativeTo(const Tile& tile) const { @@ -185,6 +446,8 @@ void Tile::set_font_metrics(CGFloat ascender, CGFloat descender) { // tabpose window. class TileSet { public: + TileSet() {} + // Fills in |tiles_|. void Build(TabStripModel* source_model); @@ -193,21 +456,33 @@ class TileSet { 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()]; } Tile& tile_at(int index) { return *tiles_[index]; } const Tile& tile_at(int index) const { return *tiles_[index]; } + // Inserts a new Tile object containing |contents| at |index|. Does no + // relayout. + void InsertTileAt(int index, TabContents* contents); + + // Removes the Tile object at |index|. Does no relayout. + void RemoveTileAt(int index); + + // Moves the Tile object at |from_index| to |to_index|. Since this doesn't + // change the number of tiles, relayout can be done just by swapping the + // tile rectangles in the index interval [from_index, to_index], so this does + // layout. + void MoveTileFromTo(int from_index, int to_index); + private: ScopedVector<Tile> tiles_; - int selected_index_; - int initial_index_; + + DISALLOW_COPY_AND_ASSIGN(TileSet); }; void TileSet::Build(TabStripModel* source_model) { - selected_index_ = initial_index_ = source_model->selected_index(); + selected_index_ = source_model->selected_index(); tiles_.resize(source_model->count()); for (size_t i = 0; i < tiles_.size(); ++i) { tiles_[i] = new Tile; @@ -217,6 +492,8 @@ void TileSet::Build(TabStripModel* source_model) { void TileSet::Layout(NSRect containing_rect) { int tile_count = tiles_.size(); + if (tile_count == 0) // Happens e.g. during test shutdown. + return; // Room around the tiles insde of |containing_rect|. const int kSmallPaddingTop = 30; @@ -370,6 +647,41 @@ void TileSet::set_selected_index(int index) { selected_index_ = index; } +void TileSet::InsertTileAt(int index, TabContents* contents) { + tiles_.insert(tiles_.begin() + index, new Tile); + tiles_[index]->contents_ = contents; +} + +void TileSet::RemoveTileAt(int index) { + tiles_.erase(tiles_.begin() + index); +} + +// Moves the Tile object at |from_index| to |to_index|. Also updates rectangles +// so that the tiles stay in a left-to-right, top-to-bottom layout when walked +// in sequential order. +void TileSet::MoveTileFromTo(int from_index, int to_index) { + NSRect thumb = tiles_[from_index]->thumb_rect_; + NSRect start_thumb = tiles_[from_index]->start_thumb_rect_; + NSRect favicon = tiles_[from_index]->favicon_rect_; + NSRect title = tiles_[from_index]->title_rect_; + + scoped_ptr<Tile> tile(tiles_[from_index]); + tiles_.weak_erase(tiles_.begin() + from_index); + tiles_.insert(tiles_.begin() + to_index, tile.release()); + + int step = from_index < to_index ? -1 : 1; + for (int i = to_index; (i - from_index) * step < 0; i += step) { + tiles_[i]->thumb_rect_ = tiles_[i + step]->thumb_rect_; + tiles_[i]->start_thumb_rect_ = tiles_[i + step]->start_thumb_rect_; + tiles_[i]->favicon_rect_ = tiles_[i + step]->favicon_rect_; + tiles_[i]->title_rect_ = tiles_[i + step]->title_rect_; + } + tiles_[from_index]->thumb_rect_ = thumb; + tiles_[from_index]->start_thumb_rect_ = start_thumb; + tiles_[from_index]->favicon_rect_ = favicon; + tiles_[from_index]->title_rect_ = title; +} + } // namespace tabpose void AnimateCALayerFrameFromTo( @@ -423,7 +735,7 @@ void AnimateCALayerFrameFromTo( rect:(NSRect)rect slomo:(BOOL)slomo tabStripModel:(TabStripModel*)tabStripModel; -- (void)setUpLayers:(NSRect)bgLayerRect slomo:(BOOL)slomo; +- (void)setUpLayersInSlomo:(BOOL)slomo; - (void)fadeAway:(BOOL)slomo; - (void)selectTileAtIndex:(int)newIndex; @end @@ -448,14 +760,16 @@ void AnimateCALayerFrameFromTo( styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO])) { - // TODO(thakis): Add a TabStripModelObserver to |tabStripModel_|. + containingRect_ = rect; tabStripModel_ = tabStripModel; state_ = tabpose::kFadingIn; tileSet_.reset(new tabpose::TileSet); + tabStripModelObserverBridge_.reset( + new TabStripModelObserverBridge(tabStripModel_, self)); [self setReleasedWhenClosed:YES]; [self setOpaque:NO]; [self setBackgroundColor:[NSColor clearColor]]; - [self setUpLayers:rect slomo:slomo]; + [self setUpLayersInSlomo:slomo]; [self setAcceptsMouseMovedEvents:YES]; [parent addChildWindow:self ordered:NSWindowAbove]; [self makeKeyAndOrderFront:self]; @@ -468,120 +782,152 @@ void AnimateCALayerFrameFromTo( } - (void)selectTileAtIndex:(int)newIndex { - ScopedCAActionDisabler disabler; const tabpose::Tile& tile = tileSet_->tile_at(newIndex); selectionHighlight_.frame = NSRectToCGRect(NSInsetRect(tile.thumb_rect(), -5, -5)); - tileSet_->set_selected_index(newIndex); } -- (void)setUpLayers:(NSRect)bgLayerRect slomo:(BOOL)slomo { +- (void)selectTileAtIndexWithoutAnimation:(int)newIndex { + ScopedCAActionDisabler disabler; + [self selectTileAtIndex:newIndex]; +} + +- (void)addLayersForTile:(tabpose::Tile&)tile + showZoom:(BOOL)showZoom + slomo:(BOOL)slomo + animationDelegate:(id)animationDelegate { + scoped_nsobject<CALayer> layer([[ThumbnailLayer alloc] + initWithTabContents:tile.tab_contents() + fullSize:tile.GetStartRectRelativeTo( + tileSet_->selected_tile()).size]); + [layer setNeedsDisplay]; + + // Background color as placeholder for now. + layer.get().backgroundColor = CGColorGetConstantColor(kCGColorWhite); + if (showZoom) { + AnimateCALayerFrameFromTo( + layer, + tile.GetStartRectRelativeTo(tileSet_->selected_tile()), + tile.thumb_rect(), + kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1), + animationDelegate); + } else { + layer.get().frame = NSRectToCGRect(tile.thumb_rect()); + } + + layer.get().shadowRadius = 10; + layer.get().shadowOffset = CGSizeMake(0, -10); + if (state_ == tabpose::kFadedIn) + layer.get().shadowOpacity = 0.5; + + [bgLayer_ addSublayer:layer]; + [allThumbnailLayers_ addObject:layer]; + + // Favicon and title. + NSFont* font = [NSFont systemFontOfSize:tile.title_font_size()]; + tile.set_font_metrics([font ascender], -[font descender]); + + NSImage* nsFavicon = gfx::SkBitmapToNSImage(tile.favicon()); + // Either we don't have a valid favicon or there was some issue converting + // it from an SkBitmap. Either way, just show the default. + if (!nsFavicon) { + NSImage* defaultFavIcon = + ResourceBundle::GetSharedInstance().GetNSImageNamed( + IDR_DEFAULT_FAVICON); + nsFavicon = defaultFavIcon; + } + scoped_cftyperef<CGImageRef> favicon( + mac_util::CopyNSImageToCGImage(nsFavicon)); + + CALayer* faviconLayer = [CALayer layer]; + faviconLayer.frame = NSRectToCGRect(tile.favicon_rect()); + faviconLayer.contents = (id)favicon.get(); + faviconLayer.zPosition = 1; // On top of the thumb shadow. + if (state_ == tabpose::kFadingIn) + faviconLayer.hidden = YES; + [bgLayer_ addSublayer:faviconLayer]; + [allFaviconLayers_ addObject:faviconLayer]; + + CATextLayer* titleLayer = [CATextLayer layer]; + titleLayer.frame = NSRectToCGRect(tile.title_rect()); + titleLayer.string = base::SysUTF16ToNSString(tile.title()); + titleLayer.fontSize = [font pointSize]; + titleLayer.truncationMode = kCATruncationEnd; + titleLayer.font = font; + titleLayer.zPosition = 1; // On top of the thumb shadow. + if (state_ == tabpose::kFadingIn) + titleLayer.hidden = YES; + [bgLayer_ addSublayer:titleLayer]; + [allTitleLayers_ addObject:titleLayer]; +} + +- (void)setUpLayersInSlomo:(BOOL)slomo { // Root layer -- covers whole window. rootLayer_ = [CALayer layer]; - [[self contentView] setLayer:rootLayer_]; - [[self contentView] setWantsLayer:YES]; - // Background layer -- the visible part of the window. - gray_.reset(CGColorCreateGenericGray(0.39, 1.0)); - bgLayer_ = [CALayer layer]; - bgLayer_.backgroundColor = gray_; - bgLayer_.frame = NSRectToCGRect(bgLayerRect); - bgLayer_.masksToBounds = YES; - [rootLayer_ addSublayer:bgLayer_]; - - // Selection highlight layer. - darkBlue_.reset(CGColorCreateGenericRGB(0.25, 0.34, 0.86, 1.0)); - selectionHighlight_ = [CALayer layer]; - selectionHighlight_.backgroundColor = darkBlue_; - selectionHighlight_.cornerRadius = 5.0; - selectionHighlight_.zPosition = -1; // Behind other layers. - selectionHighlight_.hidden = YES; - [bgLayer_ addSublayer:selectionHighlight_]; - - // Top gradient. - CALayer* gradientLayer = [DarkGradientLayer layer]; - gradientLayer.frame = CGRectMake( - 0, - NSHeight(bgLayerRect) - kTopGradientHeight, - NSWidth(bgLayerRect), - kTopGradientHeight); - [gradientLayer setNeedsDisplay]; // Draw once. - [bgLayer_ addSublayer:gradientLayer]; + // In a block so that the layers don't fade in. + { + ScopedCAActionDisabler disabler; + // Background layer -- the visible part of the window. + gray_.reset(CGColorCreateGenericGray(0.39, 1.0)); + bgLayer_ = [CALayer layer]; + bgLayer_.backgroundColor = gray_; + bgLayer_.frame = NSRectToCGRect(containingRect_); + bgLayer_.masksToBounds = YES; + [rootLayer_ addSublayer:bgLayer_]; + + // Selection highlight layer. + darkBlue_.reset(CGColorCreateGenericRGB(0.25, 0.34, 0.86, 1.0)); + selectionHighlight_ = [CALayer layer]; + selectionHighlight_.backgroundColor = darkBlue_; + selectionHighlight_.cornerRadius = 5.0; + selectionHighlight_.zPosition = -1; // Behind other layers. + selectionHighlight_.hidden = YES; + [bgLayer_ addSublayer:selectionHighlight_]; + + // Top gradient. + CALayer* gradientLayer = [DarkGradientLayer layer]; + gradientLayer.frame = CGRectMake( + 0, + NSHeight(containingRect_) - kTopGradientHeight, + NSWidth(containingRect_), + kTopGradientHeight); + [gradientLayer setNeedsDisplay]; // Draw once. + [bgLayer_ addSublayer:gradientLayer]; + } // Layers for the tab thumbnails. tileSet_->Build(tabStripModel_); - tileSet_->Layout(bgLayerRect); - - NSImage* defaultFavIcon = ResourceBundle::GetSharedInstance().GetNSImageNamed( - IDR_DEFAULT_FAVICON); - + tileSet_->Layout(containingRect_); allThumbnailLayers_.reset( [[NSMutableArray alloc] initWithCapacity:tabStripModel_->count()]); allFaviconLayers_.reset( [[NSMutableArray alloc] initWithCapacity:tabStripModel_->count()]); allTitleLayers_.reset( [[NSMutableArray alloc] initWithCapacity:tabStripModel_->count()]); - for (int i = 0; i < tabStripModel_->count(); ++i) { - const tabpose::Tile& tile = tileSet_->tile_at(i); - CALayer* layer = [CALayer layer]; - - // Background color as placeholder for now. - layer.backgroundColor = CGColorGetConstantColor(kCGColorWhite); - - AnimateCALayerFrameFromTo( - layer, - tile.GetStartRectRelativeTo(tileSet_->selected_tile()), - tile.thumb_rect(), - kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1), - i == tileSet_->selected_index() ? self : nil); + for (int i = 0; i < tabStripModel_->count(); ++i) { // Add a delegate to one of the animations to get a notification once the // animations are done. + [self addLayersForTile:tileSet_->tile_at(i) + showZoom:YES + slomo:slomo + animationDelegate:i == tileSet_->selected_index() ? self : nil]; if (i == tileSet_->selected_index()) { + CALayer* layer = [allThumbnailLayers_ objectAtIndex:i]; CAAnimation* animation = [layer animationForKey:@"bounds"]; DCHECK(animation); [animation setValue:kAnimationIdFadeIn forKey:kAnimationIdKey]; } - - layer.shadowRadius = 10; - layer.shadowOffset = CGSizeMake(0, -10); - - [bgLayer_ addSublayer:layer]; - [allThumbnailLayers_ addObject:layer]; - - // Favicon and title. - NSFont* font = [NSFont systemFontOfSize:tile.title_font_size()]; - tileSet_->tile_at(i).set_font_metrics([font ascender], -[font descender]); - - NSImage* ns_favicon = gfx::SkBitmapToNSImage(tile.favicon()); - // Either we don't have a valid favicon or there was some issue converting - // it from an SkBitmap. Either way, just show the default. - if (!ns_favicon) - ns_favicon = defaultFavIcon; - scoped_cftyperef<CGImageRef> favicon( - mac_util::CopyNSImageToCGImage(ns_favicon)); - - CALayer* faviconLayer = [CALayer layer]; - faviconLayer.frame = NSRectToCGRect(tile.favicon_rect()); - faviconLayer.contents = (id)favicon.get(); - faviconLayer.zPosition = 1; // On top of the thumb shadow. - faviconLayer.hidden = YES; - [bgLayer_ addSublayer:faviconLayer]; - [allFaviconLayers_ addObject:faviconLayer]; - - CATextLayer* titleLayer = [CATextLayer layer]; - titleLayer.frame = NSRectToCGRect(tile.title_rect()); - titleLayer.string = base::SysUTF16ToNSString(tile.title()); - titleLayer.fontSize = [font pointSize]; - titleLayer.truncationMode = kCATruncationEnd; - titleLayer.font = font; - titleLayer.zPosition = 1; // On top of the thumb shadow. - titleLayer.hidden = YES; - [bgLayer_ addSublayer:titleLayer]; - [allTitleLayers_ addObject:titleLayer]; } - [self selectTileAtIndex:tileSet_->selected_index()]; + [self selectTileAtIndexWithoutAnimation:tileSet_->selected_index()]; + + // Needs to happen after all layers have been added to |rootLayer_|, else + // there's a one frame flash of grey at the beginning of the animation + // (|bgLayer_| showing through with none of its children visible yet). + [[self contentView] setLayer:rootLayer_]; + [[self contentView] setWantsLayer:YES]; } - (BOOL)canBecomeKeyWindow { @@ -609,7 +955,7 @@ void AnimateCALayerFrameFromTo( [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0]; break; case '\e': // Escape - tileSet_->ResetSelectedIndex(); + tileSet_->set_selected_index(tabStripModel_->selected_index()); [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0]; break; // TODO(thakis): Support moving the selection via arrow keys. @@ -626,7 +972,7 @@ void AnimateCALayerFrameFromTo( newIndex = i; } if (newIndex >= 0) - [self selectTileAtIndex:newIndex]; + [self selectTileAtIndexWithoutAnimation:newIndex]; } - (void)mouseDown:(NSEvent*)event { @@ -661,8 +1007,12 @@ void AnimateCALayerFrameFromTo( [self setAcceptsMouseMovedEvents:NO]; // Select chosen tab. - tabStripModel_->SelectTabContentsAt(tileSet_->selected_index(), - /*user_gesture=*/true); + if (tileSet_->selected_index() < tabStripModel_->count()) { + tabStripModel_->SelectTabContentsAt(tileSet_->selected_index(), + /*user_gesture=*/true); + } else { + DCHECK_EQ(tileSet_->selected_index(), 0); + } { ScopedCAActionDisabler disableCAActions; @@ -683,13 +1033,13 @@ void AnimateCALayerFrameFromTo( } // Animate layers out, all in one transaction. - CGFloat duration = kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1); + CGFloat duration = 2 * kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1); ScopedCAActionSetDuration durationSetter(duration); for (NSUInteger i = 0; i < [allThumbnailLayers_ count]; ++i) { CALayer* layer = [allThumbnailLayers_ objectAtIndex:i]; - // |start_thumb_rect_| was relative to |initial_index_|, now this needs to + // |start_thumb_rect_| was relative to the initial index, now this needs to // be relative to |selectedIndex_| (whose start rect was relative to - // |initial_index_| too) + // the initial index, too). CGRect newFrame = NSRectToCGRect( tileSet_->tile_at(i).GetStartRectRelativeTo(tileSet_->selected_tile())); @@ -703,6 +1053,11 @@ void AnimateCALayerFrameFromTo( } layer.frame = newFrame; + + if (static_cast<int>(i) == tileSet_->selected_index()) { + // Redraw layer at big resolution, so that zoom-in isn't blocky. + [layer setNeedsDisplay]; + } } } @@ -733,4 +1088,165 @@ void AnimateCALayerFrameFromTo( } } +- (NSUInteger)thumbnailLayerCount { + return [allThumbnailLayers_ count]; +} + +- (int)selectedIndex { + return tileSet_->selected_index(); +} + +#pragma mark TabStripModelBridge + +- (void)refreshLayerFramesAtIndex:(int)i { + const tabpose::Tile& tile = tileSet_->tile_at(i); + + CALayer* faviconLayer = [allFaviconLayers_ objectAtIndex:i]; + faviconLayer.frame = NSRectToCGRect(tile.favicon_rect()); + CALayer* titleLayer = [allTitleLayers_ objectAtIndex:i]; + titleLayer.frame = NSRectToCGRect(tile.title_rect()); + CALayer* thumbLayer = [allThumbnailLayers_ objectAtIndex:i]; + thumbLayer.frame = NSRectToCGRect(tile.thumb_rect()); +} + +- (void)insertTabWithContents:(TabContents*)contents + atIndex:(NSInteger)index + inForeground:(bool)inForeground { + // This happens if you cmd-click a link and then immediately open tabpose + // on a slowish machine. + ScopedCAActionSetDuration durationSetter(kObserverChangeAnimationDuration); + + // Insert new layer and relayout. + tileSet_->InsertTileAt(index, contents); + tileSet_->Layout(containingRect_); + [self addLayersForTile:tileSet_->tile_at(index) + showZoom:NO + slomo:NO + animationDelegate:nil]; + + // Update old layers. + DCHECK_EQ(tabStripModel_->count(), + static_cast<int>([allThumbnailLayers_ count])); + DCHECK_EQ(tabStripModel_->count(), + static_cast<int>([allTitleLayers_ count])); + DCHECK_EQ(tabStripModel_->count(), + static_cast<int>([allFaviconLayers_ count])); + + for (int i = 0; i < tabStripModel_->count(); ++i) { + if (i == index) // The new layer. + continue; + [self refreshLayerFramesAtIndex:i]; + } + + // Update selection. + int selectedIndex = tileSet_->selected_index(); + if (selectedIndex >= index) + selectedIndex++; + [self selectTileAtIndex:selectedIndex]; +} + +- (void)tabClosingWithContents:(TabContents*)contents + atIndex:(NSInteger)index { + // We will also get a -tabDetachedWithContents:atIndex: notification for + // closing tabs, so do nothing here. +} + +- (void)tabDetachedWithContents:(TabContents*)contents + atIndex:(NSInteger)index { + ScopedCAActionSetDuration durationSetter(kObserverChangeAnimationDuration); + + // Remove layer and relayout. + tileSet_->RemoveTileAt(index); + tileSet_->Layout(containingRect_); + + [[allThumbnailLayers_ objectAtIndex:index] removeFromSuperlayer]; + [allThumbnailLayers_ removeObjectAtIndex:index]; + [[allTitleLayers_ objectAtIndex:index] removeFromSuperlayer]; + [allTitleLayers_ removeObjectAtIndex:index]; + [[allFaviconLayers_ objectAtIndex:index] removeFromSuperlayer]; + [allFaviconLayers_ removeObjectAtIndex:index]; + + // Update old layers. + DCHECK_EQ(tabStripModel_->count(), + static_cast<int>([allThumbnailLayers_ count])); + DCHECK_EQ(tabStripModel_->count(), + static_cast<int>([allTitleLayers_ count])); + DCHECK_EQ(tabStripModel_->count(), + static_cast<int>([allFaviconLayers_ count])); + + if (tabStripModel_->count() == 0) + [self close]; + + for (int i = 0; i < tabStripModel_->count(); ++i) + [self refreshLayerFramesAtIndex:i]; + + // Update selection. + int selectedIndex = tileSet_->selected_index(); + if (selectedIndex >= index) + selectedIndex--; + if (selectedIndex >= 0) + [self selectTileAtIndex:selectedIndex]; +} + +- (void)tabMovedWithContents:(TabContents*)contents + fromIndex:(NSInteger)from + toIndex:(NSInteger)to { + ScopedCAActionSetDuration durationSetter(kObserverChangeAnimationDuration); + + // Move tile from |from| to |to|. + tileSet_->MoveTileFromTo(from, to); + + // Move corresponding layers from |from| to |to|. + scoped_nsobject<CALayer> thumbLayer( + [[allThumbnailLayers_ objectAtIndex:from] retain]); + [allThumbnailLayers_ removeObjectAtIndex:from]; + [allThumbnailLayers_ insertObject:thumbLayer.get() atIndex:to]; + scoped_nsobject<CALayer> faviconLayer( + [[allFaviconLayers_ objectAtIndex:from] retain]); + [allFaviconLayers_ removeObjectAtIndex:from]; + [allFaviconLayers_ insertObject:faviconLayer.get() atIndex:to]; + scoped_nsobject<CALayer> titleLayer( + [[allTitleLayers_ objectAtIndex:from] retain]); + [allTitleLayers_ removeObjectAtIndex:from]; + [allTitleLayers_ insertObject:titleLayer.get() atIndex:to]; + + // Update frames of the layers. + for (int i = std::min(from, to); i <= std::max(from, to); ++i) + [self refreshLayerFramesAtIndex:i]; + + // Update selection. + int selectedIndex = tileSet_->selected_index(); + if (from == selectedIndex) + selectedIndex = to; + else if (from < selectedIndex && selectedIndex <= to) + selectedIndex--; + else if (to <= selectedIndex && selectedIndex < from) + selectedIndex++; + [self selectTileAtIndex:selectedIndex]; +} + +- (void)tabChangedWithContents:(TabContents*)contents + atIndex:(NSInteger)index + changeType:(TabStripModelObserver::TabChangeType)change { + // Tell the window to update text, title, and thumb layers at |index| to get + // their data from |contents|. |contents| can be different from the old + // contents at that index! + // While a tab is loading, this is unfortunately called quite often for + // both the "loading" and the "all" change types, so we don't really want to + // send thumb requests to the corresponding renderer when this is called. + // For now, just make sure that we don't hold on to an invalid TabContents + // object. + tabpose::Tile& tile = tileSet_->tile_at(index); + if (contents == tile.tab_contents()) { + // TODO(thakis): Install a timer to send a thumb request/update title/update + // favicon after 20ms or so, and reset the timer every time this is called + // to make sure we get an updated thumb, without requesting them all over. + return; + } + + tile.set_tab_contents(contents); + ThumbnailLayer* thumbLayer = [allThumbnailLayers_ objectAtIndex:index]; + [thumbLayer setTabContents:contents]; +} + @end diff --git a/chrome/browser/cocoa/tabpose_window_unittest.mm b/chrome/browser/cocoa/tabpose_window_unittest.mm index ad10250..e9ed441 100644 --- a/chrome/browser/cocoa/tabpose_window_unittest.mm +++ b/chrome/browser/cocoa/tabpose_window_unittest.mm @@ -13,12 +13,24 @@ class TabposeWindowTest : public CocoaTest { public: + TabposeWindowTest() { + site_instance_ = + SiteInstance::CreateSiteInstance(browser_helper_.profile()); + } + + void AppendTabToStrip() { + TabContents* tab_contents = new TabContents( + browser_helper_.profile(), site_instance_, MSG_ROUTING_NONE, NULL); + browser_helper_.browser()->tabstrip_model()->AppendTabContents( + tab_contents, /*foreground=*/true); + } + BrowserTestHelper browser_helper_; + scoped_refptr<SiteInstance> site_instance_; }; // Check that this doesn't leak. TEST_F(TabposeWindowTest, TestShow) { - Browser* browser = browser_helper_.browser(); BrowserWindow* browser_window = browser_helper_.CreateBrowserWindow(); NSWindow* parent = browser_window->GetNativeHandle(); @@ -26,22 +38,76 @@ TEST_F(TabposeWindowTest, TestShow) { 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); - } + for (int i = 0; i < 3; ++i) + AppendTabToStrip(); base::ScopedNSAutoreleasePool pool; TabposeWindow* window = [TabposeWindow openTabposeFor:parent rect:NSMakeRect(10, 20, 250, 160) slomo:NO - tabStripModel:model]; + tabStripModel:browser_helper_.browser()->tabstrip_model()]; + + // Should release the window. + [window mouseDown:nil]; + + browser_helper_.CloseBrowserWindow(); +} + +TEST_F(TabposeWindowTest, TestModelObserver) { + BrowserWindow* browser_window = browser_helper_.CreateBrowserWindow(); + NSWindow* parent = browser_window->GetNativeHandle(); + [parent orderFront:nil]; + + // Add a few tabs to the tab strip model. + for (int i = 0; i < 3; ++i) + AppendTabToStrip(); + + base::ScopedNSAutoreleasePool pool; + TabposeWindow* window = + [TabposeWindow openTabposeFor:parent + rect:NSMakeRect(10, 20, 250, 160) + slomo:NO + tabStripModel:browser_helper_.browser()->tabstrip_model()]; + + // Exercise all the model change events. + TabStripModel* model = browser_helper_.browser()->tabstrip_model(); + DCHECK_EQ([window thumbnailLayerCount], 3u); + DCHECK_EQ([window selectedIndex], 2); + + model->MoveTabContentsAt(0, 2, /*select_after_move=*/false); + DCHECK_EQ([window thumbnailLayerCount], 3u); + DCHECK_EQ([window selectedIndex], 1); + + model->MoveTabContentsAt(2, 0, /*select_after_move=*/false); + DCHECK_EQ([window thumbnailLayerCount], 3u); + DCHECK_EQ([window selectedIndex], 2); + + [window selectTileAtIndexWithoutAnimation:0]; + DCHECK_EQ([window selectedIndex], 0); + + model->MoveTabContentsAt(0, 2, /*select_after_move=*/false); + DCHECK_EQ([window selectedIndex], 2); + + model->MoveTabContentsAt(2, 0, /*select_after_move=*/false); + DCHECK_EQ([window selectedIndex], 0); + + delete model->DetachTabContentsAt(0); + DCHECK_EQ([window thumbnailLayerCount], 2u); + DCHECK_EQ([window selectedIndex], 0); + + AppendTabToStrip(); + DCHECK_EQ([window thumbnailLayerCount], 3u); + DCHECK_EQ([window selectedIndex], 0); + + model->CloseTabContentsAt(0, TabStripModel::CLOSE_NONE); + DCHECK_EQ([window thumbnailLayerCount], 2u); + DCHECK_EQ([window selectedIndex], 0); + + [window selectTileAtIndexWithoutAnimation:1]; + model->CloseTabContentsAt(0, TabStripModel::CLOSE_NONE); + DCHECK_EQ([window thumbnailLayerCount], 1u); + DCHECK_EQ([window selectedIndex], 0); // Should release the window. [window mouseDown:nil]; |