// Copyright (c) 2010 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import "chrome/browser/cocoa/tabpose_window.h" #import #include "app/resource_bundle.h" #include "base/mac_util.h" #include "base/mac/scoped_cftyperef.h" #include "base/scoped_callback_factory.h" #include "base/sys_string_conversions.h" #include "chrome/app/chrome_command_ids.h" #include "chrome/browser/browser_process.h" #import "chrome/browser/cocoa/bookmarks/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_wrapper.h" #include "chrome/browser/tab_contents/thumbnail_generator.h" #include "chrome/browser/tab_contents_wrapper.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; NSString* const kAnimationIdKey = @"AnimationId"; NSString* const kAnimationIdFadeIn = @"FadeIn"; 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 - (void)drawInContext:(CGContextRef)context; @end @implementation DarkGradientLayer - (void)drawInContext:(CGContextRef)context { base::mac::ScopedCFTypeRef grayColorSpace( CGColorSpaceCreateWithName(kCGColorSpaceGenericGray)); CGFloat grays[] = { 0.277, 1.0, 0.39, 1.0 }; CGFloat locations[] = { 0, 1 }; base::mac::ScopedCFTypeRef gradient( CGGradientCreateWithColorComponents( grayColorSpace.get(), grays, locations, arraysize(locations))); CGPoint topLeft = CGPointMake(0.0, kTopGradientHeight); CGContextDrawLinearGradient(context, gradient.get(), topLeft, CGPointZero, 0); } @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 loader_; // If the backing store couldn't be used and a thumbnail was returned from a // renderer process, it's stored in |thumbnail_|. base::mac::ScopedCFTypeRef 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 { 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() { ResetPaintingObserver(); } void DidReceiveBitmap(const SkBitmap& bitmap) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::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 factory_; DISALLOW_COPY_AND_ASSIGN(ThumbnailLoader); }; void ThumbnailLoader::LoadThumbnail() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::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 { base::mac::ScopedCFTypeRef image( CGBitmapContextCreateImage(backing_store->cg_bitmap())); CGContextDrawImage(context, destRect, image); } } - (void)drawInContext:(CGContextRef)context { RenderWidgetHost* rwh = contents_->render_view_host(); // NULL if renderer crashed. RenderWidgetHostView* rwhv = rwh ? rwh->view() : NULL; 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 { 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. 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. NSRect GetStartRectRelativeTo(const Tile& tile) const; NSRect thumb_rect() const { return thumb_rect_; } NSRect favicon_rect() const { return favicon_rect_; } SkBitmap favicon() const; // This changes |title_rect| and |favicon_rect| such that the favicon is on // the font's baseline and that the minimum distance between thumb rect and // favicon and title rects doesn't change. // The view code // 1. queries desired font size by calling |title_font_size()| // 2. loads that font // 3. calls |set_font_metrics()| which updates the title rect // 4. receives the title rect and puts the title on it with the font from 2. void set_font_metrics(CGFloat ascender, CGFloat descender); CGFloat title_font_size() const { return title_font_size_; } NSRect title_rect() const { return title_rect_; } // 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; // The thumb rect includes infobars, detached thumbnail bar, web contents, // and devtools. NSRect thumb_rect_; NSRect start_thumb_rect_; NSRect favicon_rect_; CGFloat title_font_size_; NSRect title_rect_; TabContents* contents_; // weak DISALLOW_COPY_AND_ASSIGN(Tile); }; 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; } SkBitmap Tile::favicon() const { if (contents_->is_app()) { SkBitmap* icon = contents_->GetExtensionAppIcon(); if (icon) return *icon; } return contents_->GetFavIcon(); } // Changes |title_rect| and |favicon_rect| such that the favicon is on the // font's baseline and that the minimum distance between thumb rect and // favicon and title rects doesn't change. void Tile::set_font_metrics(CGFloat ascender, CGFloat descender) { title_rect_.origin.y -= ascender + descender - NSHeight(title_rect_); title_rect_.size.height = ascender + descender; if (NSHeight(favicon_rect_) < ascender) { // Move favicon down. favicon_rect_.origin.y = title_rect_.origin.y + descender; } else { // Move title down. title_rect_.origin.y = favicon_rect_.origin.y - descender; } } // A tileset is responsible for owning and laying out all |Tile|s shown in a // tabpose window. class TileSet { public: TileSet() {} // 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); 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]; } // These return which index needs to be selected when the user presses // up, down, left, or right respectively. int up_index() const; int down_index() const; int left_index() const; int right_index() const; // These return which index needs to be selected on tab / shift-tab. int next_index() const; int previous_index() const; // 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: int count_x() const { return ceilf(tiles_.size() / static_cast(count_y_)); } int count_y() const { return count_y_; } int last_row_count_x() const { return tiles_.size() - count_x() * (count_y() - 1); } int tiles_in_row(int row) const { return row != count_y() - 1 ? count_x() : last_row_count_x(); } void index_to_tile_xy(int index, int* tile_x, int* tile_y) const { *tile_x = index % count_x(); *tile_y = index / count_x(); } int tile_xy_to_index(int tile_x, int tile_y) const { return tile_y * count_x() + tile_x; } ScopedVector tiles_; int selected_index_; int count_y_; DISALLOW_COPY_AND_ASSIGN(TileSet); }; void TileSet::Build(TabStripModel* source_model) { selected_index_ = source_model->selected_index(); tiles_.resize(source_model->count()); for (size_t i = 0; i < tiles_.size(); ++i) { tiles_[i] = new Tile; tiles_[i]->contents_ = source_model->GetTabContentsAt(i)->tab_contents(); } } 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; const int kSmallPaddingLeft = 30; const int kSmallPaddingRight = 30; const int kSmallPaddingBottom = 30; // Favicon / title area. const int kThumbTitlePaddingY = 6; const int kFaviconSize = 16; const int kTitleHeight = 14; // Font size. const int kTitleExtraHeight = kThumbTitlePaddingY + kTitleHeight; const int kFaviconExtraHeight = kThumbTitlePaddingY + kFaviconSize; const int kFaviconTitleDistanceX = 6; const int kFooterExtraHeight = std::max(kFaviconExtraHeight, kTitleExtraHeight); // Room between the tiles. const int kSmallPaddingX = 15; const int kSmallPaddingY = kFooterExtraHeight; // 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 - kFooterExtraHeight, kSmallPaddingX, kSmallPaddingY + kFooterExtraHeight); count_y_ = roundf(fny); // 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) / static_cast(count_x()) - kSmallPaddingX); int small_height = floor((container_height + kSmallPaddingY) / static_cast(count_y_) - (kSmallPaddingY + kFooterExtraHeight)); // |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/static_cast(small_height)) { small_height = small_width / aspect; CGFloat all_tiles_height = (small_height + kSmallPaddingY + kFooterExtraHeight) * count_y() - (kSmallPaddingY + kFooterExtraHeight); 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) / static_cast(small_width); CGFloat big_padding_x = kSmallPaddingX * scale_small_to_big; CGFloat big_padding_y = (kSmallPaddingY + kFooterExtraHeight) * 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 + kFooterExtraHeight) + kSmallPaddingTop + smallExtraPaddingTop; tiles_[i]->thumb_rect_.origin = NSMakePoint( small_x, NSHeight(containing_rect) - small_y - small_height); tiles_[i]->favicon_rect_.size = NSMakeSize(kFaviconSize, kFaviconSize); tiles_[i]->favicon_rect_.origin = NSMakePoint( small_x, NSHeight(containing_rect) - (small_y + small_height + kFaviconExtraHeight)); // Align lower left corner of title rect with lower left corner of favicon // for now. The final position is computed later by // |Tile::set_font_metrics()|. tiles_[i]->title_font_size_ = kTitleHeight; tiles_[i]->title_rect_.origin = NSMakePoint( NSMaxX(tiles_[i]->favicon_rect()) + kFaviconTitleDistanceX, NSMinY(tiles_[i]->favicon_rect())); tiles_[i]->title_rect_.size = NSMakeSize( small_width - NSWidth(tiles_[i]->favicon_rect()) - kFaviconTitleDistanceX, kTitleHeight); // 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; tiles_[i]->favicon_rect_.origin.x += small_last_row_shift_x; tiles_[i]->title_rect_.origin.x += small_last_row_shift_x; } } void TileSet::set_selected_index(int index) { CHECK_GE(index, 0); CHECK_LT(index, static_cast(tiles_.size())); selected_index_ = index; } // Given a |value| in [0, from_scale), map it into [0, to_scale) such that: // * [0, from_scale) ends up in the middle of [0, to_scale) if the latter is // a bigger range // * The middle of [0, from_scale) is mapped to [0, to_scale), and the parts // of the former that don't fit are mapped to 0 and to_scale - respectively // if the former is a bigger range. static int rescale(int value, int from_scale, int to_scale) { int left = (to_scale - from_scale) / 2; int result = value + left; if (result < 0) return 0; if (result >= to_scale) return to_scale - 1; return result; } int TileSet::up_index() const { int tile_x, tile_y; index_to_tile_xy(selected_index(), &tile_x, &tile_y); tile_y -= 1; if (tile_y == count_y() - 2) { // Transition from last row to second-to-last row. tile_x = rescale(tile_x, last_row_count_x(), count_x()); } else if (tile_y < 0) { // Transition from first row to last row. tile_x = rescale(tile_x, count_x(), last_row_count_x()); tile_y = count_y() - 1; } return tile_xy_to_index(tile_x, tile_y); } int TileSet::down_index() const { int tile_x, tile_y; index_to_tile_xy(selected_index(), &tile_x, &tile_y); tile_y += 1; if (tile_y == count_y() - 1) { // Transition from second-to-last row to last row. tile_x = rescale(tile_x, count_x(), last_row_count_x()); } else if (tile_y >= count_y()) { // Transition from last row to first row. tile_x = rescale(tile_x, last_row_count_x(), count_x()); tile_y = 0; } return tile_xy_to_index(tile_x, tile_y); } int TileSet::left_index() const { int tile_x, tile_y; index_to_tile_xy(selected_index(), &tile_x, &tile_y); tile_x -= 1; if (tile_x < 0) tile_x = tiles_in_row(tile_y) - 1; return tile_xy_to_index(tile_x, tile_y); } int TileSet::right_index() const { int tile_x, tile_y; index_to_tile_xy(selected_index(), &tile_x, &tile_y); tile_x += 1; if (tile_x >= tiles_in_row(tile_y)) tile_x = 0; return tile_xy_to_index(tile_x, tile_y); } int TileSet::next_index() const { int new_index = selected_index() + 1; if (new_index >= static_cast(tiles_.size())) new_index = 0; return new_index; } int TileSet::previous_index() const { int new_index = selected_index() - 1; if (new_index < 0) new_index = tiles_.size() - 1; return new_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(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( 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 tabStripModel:(TabStripModel*)tabStripModel; - (void)setUpLayersInSlomo:(BOOL)slomo; - (void)fadeAway:(BOOL)slomo; - (void)selectTileAtIndex:(int)newIndex; @end @implementation TabposeWindow + (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 tabStripModel:tabStripModel]; } - (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])) { 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 setUpLayersInSlomo:slomo]; [self setAcceptsMouseMovedEvents:YES]; [parent addChildWindow:self ordered:NSWindowAbove]; [self makeKeyAndOrderFront:self]; } return self; } - (CALayer*)selectedLayer { return [allThumbnailLayers_ objectAtIndex:tileSet_->selected_index()]; } - (void)selectTileAtIndex:(int)newIndex { const tabpose::Tile& tile = tileSet_->tile_at(newIndex); selectionHighlight_.frame = NSRectToCGRect(NSInsetRect(tile.thumb_rect(), -5, -5)); tileSet_->set_selected_index(newIndex); } - (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 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().GetNativeImageNamed( IDR_DEFAULT_FAVICON); nsFavicon = defaultFavIcon; } base::mac::ScopedCFTypeRef 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]; // 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(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) { // 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]; } } [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 { return YES; } // Handle key events that should be executed repeatedly while the key is down. - (void)keyDown:(NSEvent*)event { if (state_ == tabpose::kFadingOut) return; NSString* characters = [event characters]; if ([characters length] < 1) return; unichar character = [characters characterAtIndex:0]; int newIndex = -1; switch (character) { case NSUpArrowFunctionKey: newIndex = tileSet_->up_index(); break; case NSDownArrowFunctionKey: newIndex = tileSet_->down_index(); break; case NSLeftArrowFunctionKey: newIndex = tileSet_->left_index(); break; case NSRightArrowFunctionKey: newIndex = tileSet_->right_index(); break; case NSTabCharacter: newIndex = tileSet_->next_index(); break; case NSBackTabCharacter: newIndex = tileSet_->previous_index(); break; } if (newIndex != -1) [self selectTileAtIndexWithoutAnimation:newIndex]; } // Handle keyboard events that should be executed once when the key is released. - (void)keyUp:(NSEvent*)event { if (state_ == tabpose::kFadingOut) return; NSString* characters = [event characters]; if ([characters length] < 1) return; unichar character = [characters characterAtIndex:0]; switch (character) { case NSEnterCharacter: case NSNewlineCharacter: case NSCarriageReturnCharacter: case ' ': [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0]; break; case '\e': // Escape tileSet_->set_selected_index(tabStripModel_->selected_index()); [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0]; break; } } // Handle keyboard events that contain cmd or ctrl. - (BOOL)performKeyEquivalent:(NSEvent*)event { if (state_ == tabpose::kFadingOut) return NO; NSString* characters = [event characters]; if ([characters length] < 1) return NO; unichar character = [characters characterAtIndex:0]; if ([event modifierFlags] & NSCommandKeyMask) { if (character >= '1' && character <= '9') { int index = character == '9' ? tabStripModel_->count() - 1 : character - '1'; if (index < tabStripModel_->count()) { tileSet_->set_selected_index(index); [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0]; return YES; } } } return NO; } -(void)selectTileFromMouseEvent:(NSEvent*)event { int newIndex = -1; CGPoint p = NSPointToCGPoint([event locationInWindow]); for (NSUInteger i = 0; i < [allThumbnailLayers_ count]; ++i) { CALayer* layer = [allThumbnailLayers_ objectAtIndex:i]; CGPoint lp = [layer convertPoint:p fromLayer:rootLayer_]; if ([static_cast([layer presentationLayer]) containsPoint:lp]) newIndex = i; } if (newIndex >= 0) [self selectTileAtIndexWithoutAnimation:newIndex]; } - (void)mouseMoved:(NSEvent*)event { [self selectTileFromMouseEvent:event]; } - (void)mouseDown:(NSEvent*)event { // Just in case the user clicked without ever moving the mouse. [self selectTileFromMouseEvent:event]; [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0]; } - (void)swipeWithEvent:(NSEvent*)event { if (abs([event deltaY]) > 0.5) // Swipe up or down. [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0]; } - (void)close { // Prevent parent window from disappearing. [[self parentWindow] removeChildWindow:self]; // We're dealloc'd in an autorelease pool – by then the observer registry // might be dead, so explicitly reset the observer now. tabStripModelObserverBridge_.reset(); [super close]; } - (void)commandDispatch:(id)sender { if ([sender tag] == IDC_TABPOSE) [self fadeAway:NO]; } - (BOOL)validateUserInterfaceItem:(id)item { // Disable all browser-related menu items except the tab overview toggle. SEL action = [item action]; NSInteger tag = [item tag]; return action == @selector(commandDispatch:) && tag == IDC_TABPOSE; } - (void)fadeAway:(BOOL)slomo { if (state_ == tabpose::kFadingOut) return; state_ = tabpose::kFadingOut; [self setAcceptsMouseMovedEvents:NO]; // Select chosen tab. if (tileSet_->selected_index() < tabStripModel_->count()) { tabStripModel_->SelectTabContentsAt(tileSet_->selected_index(), /*user_gesture=*/true); } else { DCHECK_EQ(tileSet_->selected_index(), 0); } { ScopedCAActionDisabler disableCAActions; // Move the selected layer on top of all other layers. [self selectedLayer].zPosition = 1; selectionHighlight_.hidden = YES; for (CALayer* layer in allFaviconLayers_.get()) layer.hidden = YES; for (CALayer* layer in allTitleLayers_.get()) layer.hidden = YES; // Running animations with shadows is slow, so turn shadows off before // running the exit animation. for (CALayer* layer in allThumbnailLayers_.get()) layer.shadowOpacity = 0.0; } // Animate layers out, all in one transaction. CGFloat duration = 1.3 * 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 the initial index, now this needs to // be relative to |selectedIndex_| (whose start rect was relative to // the 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(i) == tileSet_->selected_index()) { CAAnimation* animation = [CAAnimation animation]; animation.delegate = self; [animation setValue:kAnimationIdFadeOut forKey:kAnimationIdKey]; [layer addAnimation:animation forKey:@"frame"]; } layer.frame = newFrame; if (static_cast(i) == tileSet_->selected_index()) { // Redraw layer at big resolution, so that zoom-in isn't blocky. [layer setNeedsDisplay]; } } } - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished { NSString* animationId = [animation valueForKey:kAnimationIdKey]; if ([animationId isEqualToString:kAnimationIdFadeIn]) { if (finished && state_ == tabpose::kFadingIn) { // If the user clicks while the fade in animation is still running, // |state_| is already kFadingOut. In that case, don't do anything. state_ = tabpose::kFadedIn; selectionHighlight_.hidden = NO; for (CALayer* layer in allFaviconLayers_.get()) layer.hidden = NO; for (CALayer* layer in allTitleLayers_.get()) layer.hidden = NO; // Running animations with shadows is slow, so turn shadows on only after // the animation is done. ScopedCAActionDisabler disableCAActions; for (CALayer* layer in allThumbnailLayers_.get()) layer.shadowOpacity = 0.5; } } else if ([animationId isEqualToString:kAnimationIdFadeOut]) { DCHECK_EQ(tabpose::kFadingOut, state_); [self close]; } } - (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:(TabContentsWrapper*)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->tab_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([allThumbnailLayers_ count])); DCHECK_EQ(tabStripModel_->count(), static_cast([allTitleLayers_ count])); DCHECK_EQ(tabStripModel_->count(), static_cast([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:(TabContentsWrapper*)contents atIndex:(NSInteger)index { // We will also get a -tabDetachedWithContents:atIndex: notification for // closing tabs, so do nothing here. } - (void)tabDetachedWithContents:(TabContentsWrapper*)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([allThumbnailLayers_ count])); DCHECK_EQ(tabStripModel_->count(), static_cast([allTitleLayers_ count])); DCHECK_EQ(tabStripModel_->count(), static_cast([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:(TabContentsWrapper*)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 thumbLayer( [[allThumbnailLayers_ objectAtIndex:from] retain]); [allThumbnailLayers_ removeObjectAtIndex:from]; [allThumbnailLayers_ insertObject:thumbLayer.get() atIndex:to]; scoped_nsobject faviconLayer( [[allFaviconLayers_ objectAtIndex:from] retain]); [allFaviconLayers_ removeObjectAtIndex:from]; [allFaviconLayers_ insertObject:faviconLayer.get() atIndex:to]; scoped_nsobject 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:(TabContentsWrapper*)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->tab_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->tab_contents()); ThumbnailLayer* thumbLayer = [allThumbnailLayers_ objectAtIndex:index]; [thumbLayer setTabContents:contents->tab_contents()]; } - (void)tabStripModelDeleted { [self close]; } @end