Index: chrome/browser/cocoa/tabpose_window.mm |
diff --git a/chrome/browser/cocoa/tabpose_window.mm b/chrome/browser/cocoa/tabpose_window.mm |
index f96d0402bb5a05d53fc23272536713a9ed94b1f2..b74f9caadef4c87b47c33795c7874ab46dd7c3aa 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 |