Home | History | Annotate | Download | only in cocoa
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #import "chrome/browser/ui/cocoa/tabpose_window.h"
      6 
      7 #import <QuartzCore/QuartzCore.h>
      8 
      9 #include <algorithm>
     10 
     11 #include "base/mac/mac_util.h"
     12 #include "base/mac/scoped_cftyperef.h"
     13 #include "base/memory/weak_ptr.h"
     14 #include "base/prefs/pref_service.h"
     15 #include "base/strings/sys_string_conversions.h"
     16 #include "chrome/app/chrome_command_ids.h"
     17 #include "chrome/browser/browser_process.h"
     18 #include "chrome/browser/devtools/devtools_window.h"
     19 #include "chrome/browser/extensions/tab_helper.h"
     20 #include "chrome/browser/profiles/profile.h"
     21 #include "chrome/browser/thumbnails/render_widget_snapshot_taker.h"
     22 #include "chrome/browser/ui/bookmarks/bookmark_tab_helper.h"
     23 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h"
     24 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
     25 #import "chrome/browser/ui/cocoa/infobars/infobar_container_controller.h"
     26 #import "chrome/browser/ui/cocoa/tab_contents/favicon_util_mac.h"
     27 #import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h"
     28 #import "chrome/browser/ui/cocoa/tabs/tab_strip_model_observer_bridge.h"
     29 #include "chrome/common/pref_names.h"
     30 #include "content/public/browser/browser_thread.h"
     31 #include "content/public/browser/render_view_host.h"
     32 #include "content/public/browser/render_widget_host_view.h"
     33 #include "content/public/browser/web_contents.h"
     34 #include "content/public/browser/web_contents_view.h"
     35 #include "grit/theme_resources.h"
     36 #include "grit/ui_resources.h"
     37 #include "skia/ext/skia_utils_mac.h"
     38 #include "third_party/skia/include/utils/mac/SkCGUtils.h"
     39 #include "ui/base/cocoa/animation_utils.h"
     40 #include "ui/base/resource/resource_bundle.h"
     41 #include "ui/gfx/image/image.h"
     42 #include "ui/gfx/scoped_cg_context_save_gstate_mac.h"
     43 
     44 using content::BrowserThread;
     45 using content::RenderWidgetHost;
     46 
     47 // Height of the bottom gradient, in pixels.
     48 const CGFloat kBottomGradientHeight = 50;
     49 
     50 // The shade of gray at the top of the window. There's a  gradient from
     51 // this to |kCentralGray| at the top of the window.
     52 const CGFloat kTopGray = 0.77;
     53 
     54 // The shade of gray at the center of the window. Most of the window background
     55 // has this color.
     56 const CGFloat kCentralGray = 0.6;
     57 
     58 // The shade of gray at the bottom of the window. There's a gradient from
     59 // |kCentralGray| to this at the bottom of the window, |kBottomGradientHeight|
     60 // high.
     61 const CGFloat kBottomGray = 0.5;
     62 
     63 NSString* const kAnimationIdKey = @"AnimationId";
     64 NSString* const kAnimationIdFadeIn = @"FadeIn";
     65 NSString* const kAnimationIdFadeOut = @"FadeOut";
     66 
     67 const CGFloat kDefaultAnimationDuration = 0.25;  // In seconds.
     68 const CGFloat kSlomoFactor = 4;
     69 const CGFloat kObserverChangeAnimationDuration = 0.25;  // In seconds.
     70 const CGFloat kSelectionInset = 5;
     71 
     72 // CAGradientLayer is 10.6-only -- roll our own.
     73 @interface GrayGradientLayer : CALayer {
     74  @private
     75   CGFloat startGray_;
     76   CGFloat endGray_;
     77 }
     78 - (id)initWithStartGray:(CGFloat)startGray endGray:(CGFloat)endGray;
     79 - (void)drawInContext:(CGContextRef)context;
     80 @end
     81 
     82 @implementation GrayGradientLayer
     83 - (id)initWithStartGray:(CGFloat)startGray endGray:(CGFloat)endGray {
     84   if ((self = [super init])) {
     85     startGray_ = startGray;
     86     endGray_ = endGray;
     87   }
     88   return self;
     89 }
     90 
     91 - (void)drawInContext:(CGContextRef)context {
     92   base::ScopedCFTypeRef<CGColorSpaceRef> grayColorSpace(
     93       CGColorSpaceCreateWithName(kCGColorSpaceGenericGray));
     94   CGFloat grays[] = { startGray_, 1.0, endGray_, 1.0 };
     95   CGFloat locations[] = { 0, 1 };
     96   base::ScopedCFTypeRef<CGGradientRef> gradient(
     97       CGGradientCreateWithColorComponents(
     98           grayColorSpace.get(), grays, locations, arraysize(locations)));
     99   CGPoint topLeft = CGPointMake(0.0, self.bounds.size.height);
    100   CGContextDrawLinearGradient(context, gradient.get(), topLeft, CGPointZero, 0);
    101 }
    102 @end
    103 
    104 namespace tabpose {
    105 class ThumbnailLoader;
    106 }
    107 
    108 // A CALayer that draws a thumbnail for a WebContents object. The layer
    109 // tries to draw the WebContents's backing store directly if possible, and
    110 // requests a thumbnail bitmap from the WebContents's renderer process if not.
    111 @interface ThumbnailLayer : CALayer {
    112   // The WebContents the thumbnail is for.
    113   content::WebContents* contents_;  // weak
    114 
    115   // The size the thumbnail is drawn at when zoomed in.
    116   NSSize fullSize_;
    117 
    118   // Used to load a thumbnail, if required.
    119   scoped_refptr<tabpose::ThumbnailLoader> loader_;
    120 
    121   // If the backing store couldn't be used and a thumbnail was returned from a
    122   // renderer process, it's stored in |thumbnail_|.
    123   base::ScopedCFTypeRef<CGImageRef> thumbnail_;
    124 
    125   // True if the layer already sent a thumbnail request to a renderer.
    126   BOOL didSendLoad_;
    127 }
    128 - (id)initWithWebContents:(content::WebContents*)contents
    129                  fullSize:(NSSize)fullSize;
    130 - (void)drawInContext:(CGContextRef)context;
    131 - (void)setThumbnail:(const SkBitmap&)bitmap;
    132 @end
    133 
    134 namespace tabpose {
    135 
    136 // ThumbnailLoader talks to the renderer process to load a thumbnail of a given
    137 // RenderWidgetHost, and sends the thumbnail back to a ThumbnailLayer once it
    138 // comes back from the renderer.
    139 class ThumbnailLoader : public base::RefCountedThreadSafe<ThumbnailLoader> {
    140  public:
    141   ThumbnailLoader(gfx::Size size, RenderWidgetHost* rwh, ThumbnailLayer* layer)
    142       : size_(size), rwh_(rwh), layer_(layer), weak_factory_(this) {}
    143 
    144   // Starts the fetch.
    145   void LoadThumbnail();
    146 
    147  private:
    148   friend class base::RefCountedThreadSafe<ThumbnailLoader>;
    149   ~ThumbnailLoader() {
    150   }
    151 
    152   void DidReceiveBitmap(const SkBitmap& bitmap) {
    153     DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    154     [layer_ setThumbnail:bitmap];
    155   }
    156 
    157   gfx::Size size_;
    158   RenderWidgetHost* rwh_;  // weak
    159   ThumbnailLayer* layer_;  // weak, owns us
    160   base::WeakPtrFactory<ThumbnailLoader> weak_factory_;
    161 
    162   DISALLOW_COPY_AND_ASSIGN(ThumbnailLoader);
    163 };
    164 
    165 void ThumbnailLoader::LoadThumbnail() {
    166   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    167 
    168   // As mentioned in ThumbnailLayer's -drawInContext:, it's sufficient to have
    169   // thumbnails at the zoomed-out pixel size for all but the thumbnail the user
    170   // clicks on in the end. But we don't don't which thumbnail that will be, so
    171   // keep it simple and request full thumbnails for everything.
    172   // TODO(thakis): Request smaller thumbnails for users with many tabs.
    173   gfx::Size page_size(size_);  // Logical size the renderer renders at.
    174   gfx::Size pixel_size(size_);  // Physical pixel size the image is rendered at.
    175 
    176   // Will send an IPC to the renderer on the IO thread.
    177   g_browser_process->GetRenderWidgetSnapshotTaker()->AskForSnapshot(
    178       rwh_,
    179       base::Bind(&ThumbnailLoader::DidReceiveBitmap,
    180                  weak_factory_.GetWeakPtr()),
    181       page_size,
    182       pixel_size);
    183 }
    184 
    185 }  // namespace tabpose
    186 
    187 @implementation ThumbnailLayer
    188 
    189 - (id)initWithWebContents:(content::WebContents*)contents
    190                  fullSize:(NSSize)fullSize {
    191   CHECK(contents);
    192   if ((self = [super init])) {
    193     contents_ = contents;
    194     fullSize_ = fullSize;
    195   }
    196   return self;
    197 }
    198 
    199 - (void)setWebContents:(content::WebContents*)contents {
    200   contents_ = contents;
    201 }
    202 
    203 - (void)setThumbnail:(const SkBitmap&)bitmap {
    204   // SkCreateCGImageRef() holds on to |bitmaps|'s memory, so this doesn't
    205   // create a copy. The renderer always draws data in the system colorspace.
    206   thumbnail_.reset(SkCreateCGImageRefWithColorspace(
    207       bitmap, base::mac::GetSystemColorSpace()));
    208   loader_ = NULL;
    209   [self setNeedsDisplay];
    210 }
    211 
    212 - (int)topOffset {
    213   int topOffset = 0;
    214 
    215   // Medium term, we want to show thumbs of the actual info bar views, which
    216   // means I need to create InfoBarControllers here.
    217   NSWindow* window = [contents_->GetView()->GetNativeView() window];
    218   NSWindowController* windowController = [window windowController];
    219   if ([windowController isKindOfClass:[BrowserWindowController class]]) {
    220     BrowserWindowController* bwc =
    221         static_cast<BrowserWindowController*>(windowController);
    222     InfoBarContainerController* infoBarContainer =
    223         [bwc infoBarContainerController];
    224     // TODO(thakis|rsesek): This is not correct for background tabs with
    225     // infobars as the aspect ratio will be wrong. Fix that.
    226     topOffset += NSHeight([[infoBarContainer view] frame]) -
    227         [infoBarContainer overlappingTipHeight];
    228   }
    229 
    230   BookmarkTabHelper* bookmark_tab_helper =
    231       BookmarkTabHelper::FromWebContents(contents_);
    232   Profile* profile =
    233       Profile::FromBrowserContext(contents_->GetBrowserContext());
    234   bool always_show_bookmark_bar =
    235       profile->GetPrefs()->GetBoolean(prefs::kShowBookmarkBar);
    236   bool has_detached_bookmark_bar =
    237       bookmark_tab_helper->ShouldShowBookmarkBar() &&
    238       !always_show_bookmark_bar;
    239   if (has_detached_bookmark_bar)
    240     topOffset += chrome::kNTPBookmarkBarHeight;
    241 
    242   return topOffset;
    243 }
    244 
    245 - (int)bottomOffset {
    246   int bottomOffset = 0;
    247   DevToolsWindow* devToolsWindow =
    248       DevToolsWindow::GetDockedInstanceForInspectedTab(contents_);
    249   content::WebContents* devToolsContents =
    250       devToolsWindow ? devToolsWindow->web_contents() : NULL;
    251   if (devToolsContents && devToolsContents->GetRenderViewHost() &&
    252       devToolsContents->GetRenderViewHost()->GetView()) {
    253     // The devtool's size might not be up-to-date, but since its height doesn't
    254     // change on window resize, and since most users don't use devtools, this is
    255     // good enough.
    256     bottomOffset += devToolsContents->GetRenderViewHost()->GetView()->
    257         GetViewBounds().height();
    258     bottomOffset += 1;  // :-( Divider line between web contents and devtools.
    259   }
    260   return bottomOffset;
    261 }
    262 
    263 - (void)drawInContext:(CGContextRef)context {
    264   RenderWidgetHost* rwh = contents_->GetRenderViewHost();
    265   // NULL if renderer crashed.
    266   content::RenderWidgetHostView* rwhv = rwh ? rwh->GetView() : NULL;
    267   if (!rwhv) {
    268     // TODO(thakis): Maybe draw a sad tab layer?
    269     [super drawInContext:context];
    270     return;
    271   }
    272 
    273   // The size of the WebContents's RenderWidgetHost might not fit to the
    274   // current browser window at all, for example if the window was resized while
    275   // this WebContents object was not an active tab.
    276   // Compute the required size ourselves. Leave room for eventual infobars and
    277   // a detached bookmarks bar on the top, and for the devtools on the bottom.
    278   // Download shelf is not included in the |fullSize| rect, so no need to
    279   // correct for it here.
    280   // TODO(thakis): This is not resolution-independent.
    281   int topOffset = [self topOffset];
    282   int bottomOffset = [self bottomOffset];
    283   gfx::Size desiredThumbSize(fullSize_.width,
    284                              fullSize_.height - topOffset - bottomOffset);
    285 
    286   // We need to ask the renderer for a thumbnail if
    287   // a) there's no backing store or
    288   // b) the backing store's size doesn't match our required size and
    289   // c) we didn't already send a thumbnail request to the renderer.
    290   bool draw_backing_store = rwh->GetBackingStoreSize() == desiredThumbSize;
    291 
    292   // Next weirdness: The destination rect. If the layer is |fullSize_| big, the
    293   // destination rect is (0, bottomOffset), (fullSize_.width, topOffset). But we
    294   // might be amidst an animation, so interpolate that rect.
    295   CGRect destRect = [self bounds];
    296   CGFloat scale = destRect.size.width / fullSize_.width;
    297   destRect.origin.y += bottomOffset * scale;
    298   destRect.size.height -= (bottomOffset + topOffset) * scale;
    299 
    300   // TODO(thakis): Draw infobars, detached bookmark bar as well.
    301 
    302   // If we haven't already, sent a thumbnail request to the renderer.
    303   if (!draw_backing_store && !didSendLoad_) {
    304     // Either the tab was never visible, or its backing store got evicted, or
    305     // the size of the backing store is wrong.
    306 
    307     // We only need a thumbnail the size of the zoomed-out layer for all
    308     // layers except the one the user clicks on. But since we can't know which
    309     // layer that is, request full-resolution layers for all tabs. This is
    310     // simple and seems to work in practice.
    311     loader_ = new tabpose::ThumbnailLoader(desiredThumbSize, rwh, self);
    312     loader_->LoadThumbnail();
    313     didSendLoad_ = YES;
    314 
    315     // Fill with bg color.
    316     [super drawInContext:context];
    317   }
    318 
    319   if (draw_backing_store) {
    320     // Backing store 'cache' hit!
    321     // TODO(thakis): Add a sublayer for each accelerated surface in the rwhv.
    322     // Until then, accelerated layers (CoreAnimation NPAPI plugins, compositor)
    323     // won't show up in tabpose.
    324     rwh->CopyFromBackingStoreToCGContext(destRect, context);
    325   } else if (thumbnail_) {
    326     // No cache hit, but the renderer returned a thumbnail to us.
    327     gfx::ScopedCGContextSaveGState save_gstate(context);
    328     CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
    329     CGContextDrawImage(context, destRect, thumbnail_.get());
    330   }
    331 }
    332 
    333 @end
    334 
    335 // Given the number |n| of tiles with a desired aspect ratio of |a| and a
    336 // desired distance |dx|, |dy| between tiles, returns how many tiles fit
    337 // vertically into a rectangle with the dimensions |w_c|, |h_c|. This returns
    338 // an exact solution, which is usually a fractional number.
    339 static float FitNRectsWithAspectIntoBoundingSizeWithConstantPadding(
    340     int n, double a, int w_c, int h_c, int dx, int dy) {
    341   // We want to have the small rects have the same aspect ratio a as a full
    342   // tab. Let w, h be the size of a small rect, and w_c, h_c the size of the
    343   // container. dx, dy are the distances between small rects in x, y direction.
    344 
    345   // Geometry yields:
    346   // w_c = nx * (w + dx) - dx <=> w = (w_c + d_x) / nx - d_x
    347   // h_c = ny * (h + dy) - dy <=> h = (h_c + d_y) / ny - d_t
    348   // Plugging this into
    349   // a := tab_width / tab_height = w / h
    350   // yields
    351   // a = ((w_c - (nx - 1)*d_x)*ny) / (nx*(h_c - (ny - 1)*d_y))
    352   // Plugging in nx = n/ny and pen and paper (or wolfram alpha:
    353   // 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)
    354   // 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)
    355   // ) gives us nx and ny (but the wrong root -- s/-sqrt(FOO)/sqrt(FOO)/.
    356 
    357   // This function returns ny.
    358   return (sqrt(pow(n * (a * dy - dx), 2) +
    359                4 * n * a * (dx + w_c) * (dy + h_c)) -
    360           n * (a * dy - dx))
    361       /
    362          (2 * (dx + w_c));
    363 }
    364 
    365 namespace tabpose {
    366 
    367 CGFloat ScaleWithOrigin(CGFloat x, CGFloat origin, CGFloat scale) {
    368   return (x - origin) * scale + origin;
    369 }
    370 
    371 NSRect ScaleRectWithOrigin(NSRect r, NSPoint p, CGFloat scale) {
    372   return NSMakeRect(ScaleWithOrigin(NSMinX(r), p.x, scale),
    373                     ScaleWithOrigin(NSMinY(r), p.y, scale),
    374                     NSWidth(r) * scale,
    375                     NSHeight(r) * scale);
    376 }
    377 
    378 // A tile is what is shown for a single tab in tabpose mode. It consists of a
    379 // title, favicon, thumbnail image, and pre- and postanimation rects.
    380 class Tile {
    381  public:
    382   Tile() {}
    383 
    384   // Returns the rectangle this thumbnail is at at the beginning of the zoom-in
    385   // animation. |tile| is the rectangle that's covering the whole tab area when
    386   // the animation starts.
    387   NSRect GetStartRectRelativeTo(const Tile& tile) const;
    388   NSRect thumb_rect() const { return thumb_rect_; }
    389 
    390   NSRect GetFaviconStartRectRelativeTo(const Tile& tile) const;
    391   NSRect favicon_rect() const { return NSIntegralRect(favicon_rect_); }
    392   NSImage* favicon() const;
    393 
    394   // This changes |title_rect| and |favicon_rect| such that the favicon is on
    395   // the font's baseline and that the minimum distance between thumb rect and
    396   // favicon and title rects doesn't change.
    397   // The view code
    398   // 1. queries desired font size by calling |title_font_size()|
    399   // 2. loads that font
    400   // 3. calls |set_font_metrics()| which updates the title rect
    401   // 4. receives the title rect and puts the title on it with the font from 2.
    402   void set_font_metrics(CGFloat ascender, CGFloat descender);
    403   CGFloat title_font_size() const { return title_font_size_; }
    404 
    405   NSRect GetTitleStartRectRelativeTo(const Tile& tile) const;
    406   NSRect title_rect() const { return NSIntegralRect(title_rect_); }
    407 
    408   // Returns an unelided title. The view logic is responsible for eliding.
    409   const string16& title() const {
    410     return contents_->GetTitle();
    411   }
    412 
    413   content::WebContents* web_contents() const { return contents_; }
    414   void set_tab_contents(content::WebContents* new_contents) {
    415     contents_ = new_contents;
    416   }
    417 
    418  private:
    419   friend class TileSet;
    420 
    421   // The thumb rect includes infobars, detached thumbnail bar, web contents,
    422   // and devtools.
    423   NSRect thumb_rect_;
    424   NSRect start_thumb_rect_;
    425 
    426   NSRect favicon_rect_;
    427 
    428   CGFloat title_font_size_;
    429   NSRect title_rect_;
    430 
    431   content::WebContents* contents_;  // weak
    432 
    433   DISALLOW_COPY_AND_ASSIGN(Tile);
    434 };
    435 
    436 NSRect Tile::GetStartRectRelativeTo(const Tile& tile) const {
    437   NSRect rect = start_thumb_rect_;
    438   rect.origin.x -= tile.start_thumb_rect_.origin.x;
    439   rect.origin.y -= tile.start_thumb_rect_.origin.y;
    440   return rect;
    441 }
    442 
    443 NSRect Tile::GetFaviconStartRectRelativeTo(const Tile& tile) const {
    444   NSRect thumb_start = GetStartRectRelativeTo(tile);
    445   CGFloat scale_to_start = NSWidth(thumb_start) / NSWidth(thumb_rect_);
    446   NSRect rect =
    447       ScaleRectWithOrigin(favicon_rect_, thumb_rect_.origin, scale_to_start);
    448   rect.origin.x += NSMinX(thumb_start) - NSMinX(thumb_rect_);
    449   rect.origin.y += NSMinY(thumb_start) - NSMinY(thumb_rect_);
    450   return rect;
    451 }
    452 
    453 NSImage* Tile::favicon() const {
    454   extensions::TabHelper* extensions_tab_helper =
    455       extensions::TabHelper::FromWebContents(contents_);
    456   if (extensions_tab_helper->is_app()) {
    457     SkBitmap* bitmap = extensions_tab_helper->GetExtensionAppIcon();
    458     if (bitmap)
    459       return gfx::SkBitmapToNSImage(*bitmap);
    460   }
    461   return mac::FaviconForWebContents(contents_);
    462 }
    463 
    464 NSRect Tile::GetTitleStartRectRelativeTo(const Tile& tile) const {
    465   NSRect thumb_start = GetStartRectRelativeTo(tile);
    466   CGFloat scale_to_start = NSWidth(thumb_start) / NSWidth(thumb_rect_);
    467   NSRect rect =
    468       ScaleRectWithOrigin(title_rect_, thumb_rect_.origin, scale_to_start);
    469   rect.origin.x += NSMinX(thumb_start) - NSMinX(thumb_rect_);
    470   rect.origin.y += NSMinY(thumb_start) - NSMinY(thumb_rect_);
    471   return rect;
    472 }
    473 
    474 // Changes |title_rect| and |favicon_rect| such that the favicon's and the
    475 // title's vertical center is aligned and that the minimum distance between
    476 // the thumb rect and favicon and title rects doesn't change.
    477 void Tile::set_font_metrics(CGFloat ascender, CGFloat descender) {
    478   // Make the title height big enough to fit the font, and adopt the title
    479   // position to keep its distance from the thumb rect.
    480   title_rect_.origin.y -= ascender + descender - NSHeight(title_rect_);
    481   title_rect_.size.height = ascender + descender;
    482 
    483   // Align vertical center. Both rects are currently aligned on their top edge.
    484   CGFloat delta_y = NSMidY(title_rect_) - NSMidY(favicon_rect_);
    485   if (delta_y > 0) {
    486     // Title is higher: Move favicon down to align the centers.
    487     favicon_rect_.origin.y += delta_y;
    488   } else {
    489     // Favicon is higher: Move title down to align the centers.
    490     title_rect_.origin.y -= delta_y;
    491   }
    492 }
    493 
    494 // A tileset is responsible for owning and laying out all |Tile|s shown in a
    495 // tabpose window.
    496 class TileSet {
    497  public:
    498   TileSet() {}
    499 
    500   // Fills in |tiles_|.
    501   void Build(TabStripModel* source_model);
    502 
    503   // Computes coordinates for |tiles_|.
    504   void Layout(NSRect containing_rect);
    505 
    506   int selected_index() const { return selected_index_; }
    507   void set_selected_index(int index);
    508 
    509   const Tile& selected_tile() const { return *tiles_[selected_index()]; }
    510   Tile& tile_at(int index) { return *tiles_[index]; }
    511   const Tile& tile_at(int index) const { return *tiles_[index]; }
    512 
    513   // These return which index needs to be selected when the user presses
    514   // up, down, left, or right respectively.
    515   int up_index() const;
    516   int down_index() const;
    517   int left_index() const;
    518   int right_index() const;
    519 
    520   // These return which index needs to be selected on tab / shift-tab.
    521   int next_index() const;
    522   int previous_index() const;
    523 
    524   // Inserts a new Tile object containing |contents| at |index|. Does no
    525   // relayout.
    526   void InsertTileAt(int index, content::WebContents* contents);
    527 
    528   // Removes the Tile object at |index|. Does no relayout.
    529   void RemoveTileAt(int index);
    530 
    531   // Moves the Tile object at |from_index| to |to_index|. Since this doesn't
    532   // change the number of tiles, relayout can be done just by swapping the
    533   // tile rectangles in the index interval [from_index, to_index], so this does
    534   // layout.
    535   void MoveTileFromTo(int from_index, int to_index);
    536 
    537  private:
    538   int count_x() const {
    539     return ceilf(tiles_.size() / static_cast<float>(count_y_));
    540   }
    541   int count_y() const {
    542     return count_y_;
    543   }
    544   int last_row_count_x() const {
    545     return tiles_.size() - count_x() * (count_y() - 1);
    546   }
    547   int tiles_in_row(int row) const {
    548     return row != count_y() - 1 ? count_x() : last_row_count_x();
    549   }
    550   void index_to_tile_xy(int index, int* tile_x, int* tile_y) const {
    551     *tile_x = index % count_x();
    552     *tile_y = index / count_x();
    553   }
    554   int tile_xy_to_index(int tile_x, int tile_y) const {
    555     return tile_y * count_x() + tile_x;
    556   }
    557 
    558   ScopedVector<Tile> tiles_;
    559   int selected_index_;
    560   int count_y_;
    561 
    562   DISALLOW_COPY_AND_ASSIGN(TileSet);
    563 };
    564 
    565 void TileSet::Build(TabStripModel* source_model) {
    566   selected_index_ =  source_model->active_index();
    567   tiles_.resize(source_model->count());
    568   for (size_t i = 0; i < tiles_.size(); ++i) {
    569     tiles_[i] = new Tile;
    570     tiles_[i]->contents_ = source_model->GetWebContentsAt(i);
    571   }
    572 }
    573 
    574 void TileSet::Layout(NSRect containing_rect) {
    575   int tile_count = tiles_.size();
    576   if (tile_count == 0)  // Happens e.g. during test shutdown.
    577     return;
    578 
    579   // Room around the tiles insde of |containing_rect|.
    580   const int kSmallPaddingTop = 30;
    581   const int kSmallPaddingLeft = 30;
    582   const int kSmallPaddingRight = 30;
    583   const int kSmallPaddingBottom = 30;
    584 
    585   // Favicon / title area.
    586   const int kThumbTitlePaddingY = 6;
    587   const int kFaviconSize = 16;
    588   const int kTitleHeight = 14;  // Font size.
    589   const int kTitleExtraHeight = kThumbTitlePaddingY + kTitleHeight;
    590   const int kFaviconExtraHeight = kThumbTitlePaddingY + kFaviconSize;
    591   const int kFaviconTitleDistanceX = 6;
    592   const int kFooterExtraHeight =
    593       std::max(kFaviconExtraHeight, kTitleExtraHeight);
    594 
    595   // Room between the tiles.
    596   const int kSmallPaddingX = 15;
    597   const int kSmallPaddingY = kFooterExtraHeight;
    598 
    599   // Aspect ratio of the containing rect.
    600   CGFloat aspect = NSWidth(containing_rect) / NSHeight(containing_rect);
    601 
    602   // Room left in container after the outer padding is removed.
    603   double container_width =
    604       NSWidth(containing_rect) - kSmallPaddingLeft - kSmallPaddingRight;
    605   double container_height =
    606       NSHeight(containing_rect) - kSmallPaddingTop - kSmallPaddingBottom;
    607 
    608   // The tricky part is figuring out the size of a tab thumbnail, or since the
    609   // size of the containing rect is known, the number of tiles in x and y
    610   // direction.
    611   // Given are the size of the containing rect, and the number of thumbnails
    612   // that need to fit into that rect. The aspect ratio of the thumbnails needs
    613   // to be the same as that of |containing_rect|, else they will look distorted.
    614   // The thumbnails need to be distributed such that
    615   // |count_x * count_y >= tile_count|, and such that wasted space is minimized.
    616   //  See the comments in
    617   // |FitNRectsWithAspectIntoBoundingSizeWithConstantPadding()| for a more
    618   // detailed discussion.
    619   // TODO(thakis): It might be good enough to choose |count_x| and |count_y|
    620   //   such that count_x / count_y is roughly equal to |aspect|?
    621   double fny = FitNRectsWithAspectIntoBoundingSizeWithConstantPadding(
    622       tile_count, aspect,
    623       container_width, container_height - kFooterExtraHeight,
    624       kSmallPaddingX, kSmallPaddingY + kFooterExtraHeight);
    625   count_y_ = roundf(fny);
    626 
    627   // Now that |count_x()| and |count_y_| are known, it's straightforward to
    628   // compute thumbnail width/height. See comment in
    629   // |FitNRectsWithAspectIntoBoundingSizeWithConstantPadding| for the derivation
    630   // of these two formulas.
    631   int small_width =
    632       floor((container_width + kSmallPaddingX) / static_cast<float>(count_x()) -
    633             kSmallPaddingX);
    634   int small_height =
    635       floor((container_height + kSmallPaddingY) / static_cast<float>(count_y_) -
    636             (kSmallPaddingY + kFooterExtraHeight));
    637 
    638   // |small_width / small_height| has only roughly an aspect ratio of |aspect|.
    639   // Shrink the thumbnail rect to make the aspect ratio fit exactly, and add
    640   // the extra space won by shrinking to the outer padding.
    641   int smallExtraPaddingLeft = 0;
    642   int smallExtraPaddingTop = 0;
    643   if (aspect > small_width/static_cast<float>(small_height)) {
    644     small_height = small_width / aspect;
    645     CGFloat all_tiles_height =
    646         (small_height + kSmallPaddingY + kFooterExtraHeight) * count_y() -
    647         (kSmallPaddingY + kFooterExtraHeight);
    648     smallExtraPaddingTop = (container_height - all_tiles_height)/2;
    649   } else {
    650     small_width = small_height * aspect;
    651     CGFloat all_tiles_width =
    652         (small_width + kSmallPaddingX) * count_x() - kSmallPaddingX;
    653     smallExtraPaddingLeft = (container_width - all_tiles_width)/2;
    654   }
    655 
    656   // Compute inter-tile padding in the zoomed-out view.
    657   CGFloat scale_small_to_big =
    658       NSWidth(containing_rect) / static_cast<float>(small_width);
    659   CGFloat big_padding_x = kSmallPaddingX * scale_small_to_big;
    660   CGFloat big_padding_y =
    661       (kSmallPaddingY + kFooterExtraHeight) * scale_small_to_big;
    662 
    663   // Now all dimensions are known. Lay out all tiles on a regular grid:
    664   // X X X X
    665   // X X X X
    666   // X X
    667   for (int row = 0, i = 0; i < tile_count; ++row) {
    668     for (int col = 0; col < count_x() && i < tile_count; ++col, ++i) {
    669       // Compute the smalled, zoomed-out thumbnail rect.
    670       tiles_[i]->thumb_rect_.size = NSMakeSize(small_width, small_height);
    671 
    672       int small_x = col * (small_width + kSmallPaddingX) +
    673                     kSmallPaddingLeft + smallExtraPaddingLeft;
    674       int small_y = row * (small_height + kSmallPaddingY + kFooterExtraHeight) +
    675                     kSmallPaddingTop + smallExtraPaddingTop;
    676 
    677       tiles_[i]->thumb_rect_.origin = NSMakePoint(
    678           small_x, NSHeight(containing_rect) - small_y - small_height);
    679 
    680       tiles_[i]->favicon_rect_.size = NSMakeSize(kFaviconSize, kFaviconSize);
    681       tiles_[i]->favicon_rect_.origin = NSMakePoint(
    682           small_x,
    683           NSHeight(containing_rect) -
    684               (small_y + small_height + kFaviconExtraHeight));
    685 
    686       // Align lower left corner of title rect with lower left corner of favicon
    687       // for now. The final position is computed later by
    688       // |Tile::set_font_metrics()|.
    689       tiles_[i]->title_font_size_ = kTitleHeight;
    690       tiles_[i]->title_rect_.origin = NSMakePoint(
    691           NSMaxX(tiles_[i]->favicon_rect()) + kFaviconTitleDistanceX,
    692           NSMinY(tiles_[i]->favicon_rect()));
    693       tiles_[i]->title_rect_.size = NSMakeSize(
    694           small_width -
    695               NSWidth(tiles_[i]->favicon_rect()) - kFaviconTitleDistanceX,
    696           kTitleHeight);
    697 
    698       // Compute the big, pre-zoom thumbnail rect.
    699       tiles_[i]->start_thumb_rect_.size = containing_rect.size;
    700 
    701       int big_x = col * (NSWidth(containing_rect) + big_padding_x);
    702       int big_y = row * (NSHeight(containing_rect) + big_padding_y);
    703       tiles_[i]->start_thumb_rect_.origin = NSMakePoint(big_x, -big_y);
    704     }
    705   }
    706 }
    707 
    708 void TileSet::set_selected_index(int index) {
    709   CHECK_GE(index, 0);
    710   CHECK_LT(index, static_cast<int>(tiles_.size()));
    711   selected_index_ = index;
    712 }
    713 
    714 // Given a |value| in [0, from_scale), map it into [0, to_scale) such that:
    715 // * [0, from_scale) ends up in the middle of [0, to_scale) if the latter is
    716 //   a bigger range
    717 // * The middle of [0, from_scale) is mapped to [0, to_scale), and the parts
    718 //   of the former that don't fit are mapped to 0 and to_scale - respectively
    719 //   if the former is a bigger range.
    720 static int rescale(int value, int from_scale, int to_scale) {
    721   int left = (to_scale - from_scale) / 2;
    722   int result = value + left;
    723   if (result < 0)
    724     return 0;
    725   if (result >= to_scale)
    726     return to_scale - 1;
    727   return result;
    728 }
    729 
    730 int TileSet::up_index() const {
    731   int tile_x, tile_y;
    732   index_to_tile_xy(selected_index(), &tile_x, &tile_y);
    733   tile_y -= 1;
    734   if (tile_y == count_y() - 2) {
    735     // Transition from last row to second-to-last row.
    736     tile_x = rescale(tile_x, last_row_count_x(), count_x());
    737   } else if (tile_y < 0) {
    738     // Transition from first row to last row.
    739     tile_x = rescale(tile_x, count_x(), last_row_count_x());
    740     tile_y = count_y() - 1;
    741   }
    742   return tile_xy_to_index(tile_x, tile_y);
    743 }
    744 
    745 int TileSet::down_index() const {
    746   int tile_x, tile_y;
    747   index_to_tile_xy(selected_index(), &tile_x, &tile_y);
    748   tile_y += 1;
    749   if (tile_y == count_y() - 1) {
    750     // Transition from second-to-last row to last row.
    751     tile_x = rescale(tile_x, count_x(), last_row_count_x());
    752   } else if (tile_y >= count_y()) {
    753     // Transition from last row to first row.
    754     tile_x = rescale(tile_x, last_row_count_x(), count_x());
    755     tile_y = 0;
    756   }
    757   return tile_xy_to_index(tile_x, tile_y);
    758 }
    759 
    760 int TileSet::left_index() const {
    761   int tile_x, tile_y;
    762   index_to_tile_xy(selected_index(), &tile_x, &tile_y);
    763   tile_x -= 1;
    764   if (tile_x < 0)
    765     tile_x = tiles_in_row(tile_y) - 1;
    766   return tile_xy_to_index(tile_x, tile_y);
    767 }
    768 
    769 int TileSet::right_index() const {
    770   int tile_x, tile_y;
    771   index_to_tile_xy(selected_index(), &tile_x, &tile_y);
    772   tile_x += 1;
    773   if (tile_x >= tiles_in_row(tile_y))
    774     tile_x = 0;
    775   return tile_xy_to_index(tile_x, tile_y);
    776 }
    777 
    778 int TileSet::next_index() const {
    779   int new_index = selected_index() + 1;
    780   if (new_index >= static_cast<int>(tiles_.size()))
    781     new_index = 0;
    782   return new_index;
    783 }
    784 
    785 int TileSet::previous_index() const {
    786   int new_index = selected_index() - 1;
    787   if (new_index < 0)
    788     new_index = tiles_.size() - 1;
    789   return new_index;
    790 }
    791 
    792 void TileSet::InsertTileAt(int index, content::WebContents* contents) {
    793   tiles_.insert(tiles_.begin() + index, new Tile);
    794   tiles_[index]->contents_ = contents;
    795 }
    796 
    797 void TileSet::RemoveTileAt(int index) {
    798   tiles_.erase(tiles_.begin() + index);
    799 }
    800 
    801 // Moves the Tile object at |from_index| to |to_index|. Also updates rectangles
    802 // so that the tiles stay in a left-to-right, top-to-bottom layout when walked
    803 // in sequential order.
    804 void TileSet::MoveTileFromTo(int from_index, int to_index) {
    805   NSRect thumb = tiles_[from_index]->thumb_rect_;
    806   NSRect start_thumb = tiles_[from_index]->start_thumb_rect_;
    807   NSRect favicon = tiles_[from_index]->favicon_rect_;
    808   NSRect title = tiles_[from_index]->title_rect_;
    809 
    810   scoped_ptr<Tile> tile(tiles_[from_index]);
    811   tiles_.weak_erase(tiles_.begin() + from_index);
    812   tiles_.insert(tiles_.begin() + to_index, tile.release());
    813 
    814   int step = from_index < to_index ? -1 : 1;
    815   for (int i = to_index; (i - from_index) * step < 0; i += step) {
    816     tiles_[i]->thumb_rect_ = tiles_[i + step]->thumb_rect_;
    817     tiles_[i]->start_thumb_rect_ = tiles_[i + step]->start_thumb_rect_;
    818     tiles_[i]->favicon_rect_ = tiles_[i + step]->favicon_rect_;
    819     tiles_[i]->title_rect_ = tiles_[i + step]->title_rect_;
    820   }
    821   tiles_[from_index]->thumb_rect_ = thumb;
    822   tiles_[from_index]->start_thumb_rect_ = start_thumb;
    823   tiles_[from_index]->favicon_rect_ = favicon;
    824   tiles_[from_index]->title_rect_ = title;
    825 }
    826 
    827 }  // namespace tabpose
    828 
    829 void AnimateScaledCALayerFrameFromTo(
    830     CALayer* layer,
    831     const NSRect& from, CGFloat from_scale,
    832     const NSRect& to, CGFloat to_scale,
    833     NSTimeInterval duration, id boundsAnimationDelegate) {
    834   // http://developer.apple.com/mac/library/qa/qa2008/qa1620.html
    835   CABasicAnimation* animation;
    836 
    837   animation = [CABasicAnimation animationWithKeyPath:@"bounds"];
    838   animation.fromValue = [NSValue valueWithRect:from];
    839   animation.toValue = [NSValue valueWithRect:to];
    840   animation.duration = duration;
    841   animation.timingFunction =
    842       [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    843   animation.delegate = boundsAnimationDelegate;
    844 
    845   // Update the layer's bounds so the layer doesn't snap back when the animation
    846   // completes.
    847   layer.bounds = NSRectToCGRect(to);
    848 
    849   // Add the animation, overriding the implicit animation.
    850   [layer addAnimation:animation forKey:@"bounds"];
    851 
    852   // Prepare the animation from the current position to the new position.
    853   NSPoint opoint = from.origin;
    854   NSPoint point = to.origin;
    855 
    856   // Adapt to anchorPoint.
    857   opoint.x += NSWidth(from) * from_scale * layer.anchorPoint.x;
    858   opoint.y += NSHeight(from) * from_scale * layer.anchorPoint.y;
    859   point.x += NSWidth(to) * to_scale * layer.anchorPoint.x;
    860   point.y += NSHeight(to) * to_scale * layer.anchorPoint.y;
    861 
    862   animation = [CABasicAnimation animationWithKeyPath:@"position"];
    863   animation.fromValue = [NSValue valueWithPoint:opoint];
    864   animation.toValue = [NSValue valueWithPoint:point];
    865   animation.duration = duration;
    866   animation.timingFunction =
    867       [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    868 
    869   // Update the layer's position so that the layer doesn't snap back when the
    870   // animation completes.
    871   layer.position = NSPointToCGPoint(point);
    872 
    873   // Add the animation, overriding the implicit animation.
    874   [layer addAnimation:animation forKey:@"position"];
    875 }
    876 
    877 void AnimateCALayerFrameFromTo(
    878     CALayer* layer, const NSRect& from, const NSRect& to,
    879     NSTimeInterval duration, id boundsAnimationDelegate) {
    880   AnimateScaledCALayerFrameFromTo(
    881       layer, from, 1.0, to, 1.0, duration, boundsAnimationDelegate);
    882 }
    883 
    884 void AnimateCALayerOpacityFromTo(
    885     CALayer* layer, double from, double to, NSTimeInterval duration) {
    886   CABasicAnimation* animation;
    887   animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
    888   animation.fromValue = [NSNumber numberWithFloat:from];
    889   animation.toValue = [NSNumber numberWithFloat:to];
    890   animation.duration = duration;
    891 
    892   layer.opacity = to;
    893   // Add the animation, overriding the implicit animation.
    894   [layer addAnimation:animation forKey:@"opacity"];
    895 }
    896 
    897 @interface TabposeWindow (Private)
    898 - (id)initForWindow:(NSWindow*)parent
    899                rect:(NSRect)rect
    900               slomo:(BOOL)slomo
    901       tabStripModel:(TabStripModel*)tabStripModel;
    902 
    903 // Creates and initializes the CALayer in the background and all the CALayers
    904 // for the thumbnails, favicons, and titles.
    905 - (void)setUpLayersInSlomo:(BOOL)slomo;
    906 
    907 // Tells the browser to make the tab corresponding to currently selected
    908 // thumbnail the current tab and starts the tabpose exit animmation.
    909 - (void)fadeAwayInSlomo:(BOOL)slomo;
    910 
    911 // Returns the CALayer for the close button belonging to the thumbnail at
    912 // index |index|.
    913 - (CALayer*)closebuttonLayerAtIndex:(NSUInteger)index;
    914 
    915 // Updates the visibility of all closebutton layers.
    916 - (void)updateClosebuttonLayersVisibility;
    917 @end
    918 
    919 @implementation TabposeWindow
    920 
    921 + (id)openTabposeFor:(NSWindow*)parent
    922                 rect:(NSRect)rect
    923                slomo:(BOOL)slomo
    924        tabStripModel:(TabStripModel*)tabStripModel {
    925   // Releases itself when closed.
    926   return [[TabposeWindow alloc]
    927       initForWindow:parent rect:rect slomo:slomo tabStripModel:tabStripModel];
    928 }
    929 
    930 - (id)initForWindow:(NSWindow*)parent
    931                rect:(NSRect)rect
    932               slomo:(BOOL)slomo
    933       tabStripModel:(TabStripModel*)tabStripModel {
    934   NSRect frame = [parent frame];
    935   if ((self = [super initWithContentRect:frame
    936                                styleMask:NSBorderlessWindowMask
    937                                  backing:NSBackingStoreBuffered
    938                                    defer:NO])) {
    939     containingRect_ = rect;
    940     tabStripModel_ = tabStripModel;
    941     state_ = tabpose::kFadingIn;
    942     tileSet_.reset(new tabpose::TileSet);
    943     tabStripModelObserverBridge_.reset(
    944         new TabStripModelObserverBridge(tabStripModel_, self));
    945     NSImage* nsCloseIcon =
    946         ResourceBundle::GetSharedInstance().GetNativeImageNamed(
    947             IDR_TABPOSE_CLOSE).ToNSImage();
    948     closeIcon_.reset(base::mac::CopyNSImageToCGImage(nsCloseIcon));
    949     [self setReleasedWhenClosed:YES];
    950     [self setOpaque:NO];
    951     [self setBackgroundColor:[NSColor clearColor]];
    952     [self setUpLayersInSlomo:slomo];
    953     [self setAcceptsMouseMovedEvents:YES];
    954     [parent addChildWindow:self ordered:NSWindowAbove];
    955     [self makeKeyAndOrderFront:self];
    956   }
    957   return self;
    958 }
    959 
    960 - (CALayer*)selectedLayer {
    961   return [allThumbnailLayers_ objectAtIndex:tileSet_->selected_index()];
    962 }
    963 
    964 - (void)selectTileAtIndexWithoutAnimation:(int)newIndex {
    965   ScopedCAActionDisabler disabler;
    966   const tabpose::Tile& tile = tileSet_->tile_at(newIndex);
    967   selectionHighlight_.frame =
    968       NSRectToCGRect(NSInsetRect(tile.thumb_rect(),
    969                      -kSelectionInset, -kSelectionInset));
    970   tileSet_->set_selected_index(newIndex);
    971 
    972   [self updateClosebuttonLayersVisibility];
    973 }
    974 
    975 - (void)addLayersForTile:(tabpose::Tile&)tile
    976                 showZoom:(BOOL)showZoom
    977                    slomo:(BOOL)slomo
    978        animationDelegate:(id)animationDelegate {
    979   base::scoped_nsobject<CALayer> layer(
    980       [[ThumbnailLayer alloc] initWithWebContents:tile.web_contents()
    981                                          fullSize:tile.GetStartRectRelativeTo(
    982                                              tileSet_->selected_tile()).size]);
    983   [layer setNeedsDisplay];
    984 
    985   NSTimeInterval interval =
    986       kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1);
    987 
    988   // Background color as placeholder for now.
    989   layer.get().backgroundColor = CGColorGetConstantColor(kCGColorWhite);
    990   if (showZoom) {
    991     AnimateCALayerFrameFromTo(
    992         layer,
    993         tile.GetStartRectRelativeTo(tileSet_->selected_tile()),
    994         tile.thumb_rect(),
    995         interval,
    996         animationDelegate);
    997   } else {
    998     layer.get().frame = NSRectToCGRect(tile.thumb_rect());
    999   }
   1000 
   1001   layer.get().shadowRadius = 10;
   1002   layer.get().shadowOffset = CGSizeMake(0, -10);
   1003   if (state_ == tabpose::kFadedIn)
   1004     layer.get().shadowOpacity = 0.5;
   1005 
   1006   // Add a close button to the thumb layer.
   1007   CALayer* closeLayer = [CALayer layer];
   1008   closeLayer.contents = reinterpret_cast<id>(closeIcon_.get());
   1009   CGRect closeBounds = {};
   1010   closeBounds.size.width = CGImageGetWidth(closeIcon_);
   1011   closeBounds.size.height = CGImageGetHeight(closeIcon_);
   1012   closeLayer.bounds = closeBounds;
   1013   closeLayer.hidden = YES;
   1014 
   1015   [closeLayer addConstraint:
   1016       [CAConstraint constraintWithAttribute:kCAConstraintMidX
   1017                                  relativeTo:@"superlayer"
   1018                                   attribute:kCAConstraintMinX]];
   1019   [closeLayer addConstraint:
   1020       [CAConstraint constraintWithAttribute:kCAConstraintMidY
   1021                                  relativeTo:@"superlayer"
   1022                                   attribute:kCAConstraintMaxY]];
   1023 
   1024   layer.get().layoutManager = [CAConstraintLayoutManager layoutManager];
   1025   [layer.get() addSublayer:closeLayer];
   1026 
   1027   [bgLayer_ addSublayer:layer];
   1028   [allThumbnailLayers_ addObject:layer];
   1029 
   1030   // Favicon and title.
   1031   NSFont* font = [NSFont systemFontOfSize:tile.title_font_size()];
   1032   tile.set_font_metrics([font ascender], -[font descender]);
   1033 
   1034   base::ScopedCFTypeRef<CGImageRef> favicon(
   1035       base::mac::CopyNSImageToCGImage(tile.favicon()));
   1036 
   1037   CALayer* faviconLayer = [CALayer layer];
   1038   if (showZoom) {
   1039     AnimateCALayerFrameFromTo(
   1040         faviconLayer,
   1041         tile.GetFaviconStartRectRelativeTo(tileSet_->selected_tile()),
   1042         tile.favicon_rect(),
   1043         interval,
   1044         nil);
   1045     AnimateCALayerOpacityFromTo(faviconLayer, 0.0, 1.0, interval);
   1046   } else {
   1047     faviconLayer.frame = NSRectToCGRect(tile.favicon_rect());
   1048   }
   1049   faviconLayer.contents = (id)favicon.get();
   1050   faviconLayer.zPosition = 1;  // On top of the thumb shadow.
   1051   [bgLayer_ addSublayer:faviconLayer];
   1052   [allFaviconLayers_ addObject:faviconLayer];
   1053 
   1054   // CATextLayers can't animate their fontSize property, at least on 10.5.
   1055   // Animate transform.scale instead.
   1056 
   1057   // The scaling should have its origin in the layer's upper left corner.
   1058   // This needs to be set before |AnimateCALayerFrameFromTo()| is called.
   1059   CATextLayer* titleLayer = [CATextLayer layer];
   1060   titleLayer.anchorPoint = CGPointMake(0, 1);
   1061   if (showZoom) {
   1062     NSRect fromRect =
   1063         tile.GetTitleStartRectRelativeTo(tileSet_->selected_tile());
   1064     NSRect toRect = tile.title_rect();
   1065     CGFloat scale = NSWidth(fromRect) / NSWidth(toRect);
   1066     fromRect.size = toRect.size;
   1067 
   1068     // Add scale animation.
   1069     CABasicAnimation* scaleAnimation =
   1070         [CABasicAnimation animationWithKeyPath:@"transform.scale"];
   1071     scaleAnimation.fromValue = [NSNumber numberWithDouble:scale];
   1072     scaleAnimation.toValue = [NSNumber numberWithDouble:1.0];
   1073     scaleAnimation.duration = interval;
   1074     scaleAnimation.timingFunction =
   1075         [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
   1076     [titleLayer addAnimation:scaleAnimation forKey:@"transform.scale"];
   1077 
   1078     // Add the position and opacity animations.
   1079     AnimateScaledCALayerFrameFromTo(
   1080         titleLayer, fromRect, scale, toRect, 1.0, interval, nil);
   1081     AnimateCALayerOpacityFromTo(faviconLayer, 0.0, 1.0, interval);
   1082   } else {
   1083     titleLayer.frame = NSRectToCGRect(tile.title_rect());
   1084   }
   1085   titleLayer.string = base::SysUTF16ToNSString(tile.title());
   1086   titleLayer.fontSize = [font pointSize];
   1087   titleLayer.truncationMode = kCATruncationEnd;
   1088   titleLayer.font = font;
   1089   titleLayer.zPosition = 1;  // On top of the thumb shadow.
   1090   [bgLayer_ addSublayer:titleLayer];
   1091   [allTitleLayers_ addObject:titleLayer];
   1092 }
   1093 
   1094 - (void)setUpLayersInSlomo:(BOOL)slomo {
   1095   // Root layer -- covers whole window.
   1096   rootLayer_ = [CALayer layer];
   1097 
   1098   // In a block so that the layers don't fade in.
   1099   {
   1100     ScopedCAActionDisabler disabler;
   1101     // Background layer -- the visible part of the window.
   1102     gray_.reset(CGColorCreateGenericGray(kCentralGray, 1.0));
   1103     bgLayer_ = [CALayer layer];
   1104     bgLayer_.backgroundColor = gray_;
   1105     bgLayer_.frame = NSRectToCGRect(containingRect_);
   1106     bgLayer_.masksToBounds = YES;
   1107     [rootLayer_ addSublayer:bgLayer_];
   1108 
   1109     // Selection highlight layer.
   1110     darkBlue_.reset(CGColorCreateGenericRGB(0.25, 0.34, 0.86, 1.0));
   1111     selectionHighlight_ = [CALayer layer];
   1112     selectionHighlight_.backgroundColor = darkBlue_;
   1113     selectionHighlight_.cornerRadius = 5.0;
   1114     selectionHighlight_.zPosition = -1;  // Behind other layers.
   1115     selectionHighlight_.hidden = YES;
   1116     [bgLayer_ addSublayer:selectionHighlight_];
   1117 
   1118     // Bottom gradient.
   1119     CALayer* gradientLayer = [[[GrayGradientLayer alloc]
   1120         initWithStartGray:kCentralGray endGray:kBottomGray] autorelease];
   1121     gradientLayer.frame = CGRectMake(
   1122         0,
   1123         0,
   1124         NSWidth(containingRect_),
   1125         kBottomGradientHeight);
   1126     [gradientLayer setNeedsDisplay];  // Draw once.
   1127     [bgLayer_ addSublayer:gradientLayer];
   1128   }
   1129   // Top gradient (fades in).
   1130   CGFloat toolbarHeight = NSHeight([self frame]) - NSHeight(containingRect_);
   1131   topGradient_ = [[[GrayGradientLayer alloc]
   1132       initWithStartGray:kTopGray endGray:kCentralGray] autorelease];
   1133   topGradient_.frame = CGRectMake(
   1134       0,
   1135       NSHeight([self frame]) - toolbarHeight,
   1136       NSWidth(containingRect_),
   1137       toolbarHeight);
   1138   [topGradient_ setNeedsDisplay];  // Draw once.
   1139   [rootLayer_ addSublayer:topGradient_];
   1140   NSTimeInterval interval =
   1141       kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1);
   1142   AnimateCALayerOpacityFromTo(topGradient_, 0, 1, interval);
   1143 
   1144   // Layers for the tab thumbnails.
   1145   tileSet_->Build(tabStripModel_);
   1146   tileSet_->Layout(containingRect_);
   1147   allThumbnailLayers_.reset(
   1148       [[NSMutableArray alloc] initWithCapacity:tabStripModel_->count()]);
   1149   allFaviconLayers_.reset(
   1150       [[NSMutableArray alloc] initWithCapacity:tabStripModel_->count()]);
   1151   allTitleLayers_.reset(
   1152       [[NSMutableArray alloc] initWithCapacity:tabStripModel_->count()]);
   1153 
   1154   for (int i = 0; i < tabStripModel_->count(); ++i) {
   1155     // Add a delegate to one of the animations to get a notification once the
   1156     // animations are done.
   1157     [self  addLayersForTile:tileSet_->tile_at(i)
   1158                  showZoom:YES
   1159                     slomo:slomo
   1160         animationDelegate:i == tileSet_->selected_index() ? self : nil];
   1161     if (i == tileSet_->selected_index()) {
   1162       CALayer* layer = [allThumbnailLayers_ objectAtIndex:i];
   1163       CAAnimation* animation = [layer animationForKey:@"bounds"];
   1164       DCHECK(animation);
   1165       [animation setValue:kAnimationIdFadeIn forKey:kAnimationIdKey];
   1166     }
   1167   }
   1168   [self selectTileAtIndexWithoutAnimation:tileSet_->selected_index()];
   1169 
   1170   // Needs to happen after all layers have been added to |rootLayer_|, else
   1171   // there's a one frame flash of grey at the beginning of the animation
   1172   // (|bgLayer_| showing through with none of its children visible yet).
   1173   [[self contentView] setLayer:rootLayer_];
   1174   [[self contentView] setWantsLayer:YES];
   1175 }
   1176 
   1177 - (BOOL)canBecomeKeyWindow {
   1178  return YES;
   1179 }
   1180 
   1181 // Lets the traffic light buttons on the browser window keep their "active"
   1182 // state while an info bubble is open. Only has an effect on 10.7.
   1183 - (BOOL)_sharesParentKeyState {
   1184   return YES;
   1185 }
   1186 
   1187 // Handle key events that should be executed repeatedly while the key is down.
   1188 - (void)keyDown:(NSEvent*)event {
   1189   if (state_ == tabpose::kFadingOut)
   1190     return;
   1191   NSString* characters = [event characters];
   1192   if ([characters length] < 1)
   1193     return;
   1194 
   1195   unichar character = [characters characterAtIndex:0];
   1196   int newIndex = -1;
   1197   switch (character) {
   1198     case NSUpArrowFunctionKey:
   1199       newIndex = tileSet_->up_index();
   1200       break;
   1201     case NSDownArrowFunctionKey:
   1202       newIndex = tileSet_->down_index();
   1203       break;
   1204     case NSLeftArrowFunctionKey:
   1205       newIndex = tileSet_->left_index();
   1206       break;
   1207     case NSRightArrowFunctionKey:
   1208       newIndex = tileSet_->right_index();
   1209       break;
   1210     case NSTabCharacter:
   1211       newIndex = tileSet_->next_index();
   1212       break;
   1213     case NSBackTabCharacter:
   1214       newIndex = tileSet_->previous_index();
   1215       break;
   1216   }
   1217   if (newIndex != -1)
   1218     [self selectTileAtIndexWithoutAnimation:newIndex];
   1219 }
   1220 
   1221 // Handle keyboard events that should be executed once when the key is released.
   1222 - (void)keyUp:(NSEvent*)event {
   1223   if (state_ == tabpose::kFadingOut)
   1224     return;
   1225   NSString* characters = [event characters];
   1226   if ([characters length] < 1)
   1227     return;
   1228 
   1229   unichar character = [characters characterAtIndex:0];
   1230   switch (character) {
   1231     case NSEnterCharacter:
   1232     case NSNewlineCharacter:
   1233     case NSCarriageReturnCharacter:
   1234     case ' ':
   1235       [self fadeAwayInSlomo:([event modifierFlags] & NSShiftKeyMask) != 0];
   1236       break;
   1237     case '\e':  // Escape
   1238       tileSet_->set_selected_index(tabStripModel_->active_index());
   1239       [self fadeAwayInSlomo:([event modifierFlags] & NSShiftKeyMask) != 0];
   1240       break;
   1241   }
   1242 }
   1243 
   1244 // Handle keyboard events that contain cmd or ctrl.
   1245 - (BOOL)performKeyEquivalent:(NSEvent*)event {
   1246   if (state_ == tabpose::kFadingOut)
   1247     return NO;
   1248   NSString* characters = [event characters];
   1249   if ([characters length] < 1)
   1250     return NO;
   1251   unichar character = [characters characterAtIndex:0];
   1252   if ([event modifierFlags] & NSCommandKeyMask) {
   1253     if (character >= '1' && character <= '9') {
   1254       int index =
   1255           character == '9' ? tabStripModel_->count() - 1 : character - '1';
   1256       if (index < tabStripModel_->count()) {
   1257         tileSet_->set_selected_index(index);
   1258         [self fadeAwayInSlomo:([event modifierFlags] & NSShiftKeyMask) != 0];
   1259         return YES;
   1260       }
   1261     }
   1262   }
   1263   return NO;
   1264 }
   1265 
   1266 - (void)flagsChanged:(NSEvent*)event {
   1267   showAllCloseLayers_ = ([event modifierFlags] & NSAlternateKeyMask) != 0;
   1268   [self updateClosebuttonLayersVisibility];
   1269 }
   1270 
   1271 - (void)selectTileFromMouseEvent:(NSEvent*)event {
   1272   int newIndex = -1;
   1273   CGPoint p = NSPointToCGPoint([event locationInWindow]);
   1274   for (NSUInteger i = 0; i < [allThumbnailLayers_ count]; ++i) {
   1275     CALayer* layer = [allThumbnailLayers_ objectAtIndex:i];
   1276     CGPoint lp = [layer convertPoint:p fromLayer:rootLayer_];
   1277     if ([static_cast<CALayer*>([layer presentationLayer]) containsPoint:lp])
   1278       newIndex = i;
   1279   }
   1280   if (newIndex >= 0)
   1281     [self selectTileAtIndexWithoutAnimation:newIndex];
   1282 }
   1283 
   1284 - (void)mouseMoved:(NSEvent*)event {
   1285   [self selectTileFromMouseEvent:event];
   1286 }
   1287 
   1288 - (CALayer*)closebuttonLayerAtIndex:(NSUInteger)index {
   1289   CALayer* layer = [allThumbnailLayers_ objectAtIndex:index];
   1290   return [[layer sublayers] objectAtIndex:0];
   1291 }
   1292 
   1293 - (void)updateClosebuttonLayersVisibility {
   1294   for (NSUInteger i = 0; i < [allThumbnailLayers_ count]; ++i) {
   1295     CALayer* layer = [self closebuttonLayerAtIndex:i];
   1296     BOOL isSelectedTile = static_cast<int>(i) == tileSet_->selected_index();
   1297     BOOL isVisible = state_ == tabpose::kFadedIn &&
   1298                      (isSelectedTile || showAllCloseLayers_);
   1299     layer.hidden = !isVisible;
   1300   }
   1301 }
   1302 
   1303 - (void)mouseDown:(NSEvent*)event {
   1304   // Just in case the user clicked without ever moving the mouse.
   1305   [self selectTileFromMouseEvent:event];
   1306 
   1307   // If the click occurred in a close box, close that tab and don't do anything
   1308   // else.
   1309   CGPoint p = NSPointToCGPoint([event locationInWindow]);
   1310   for (NSUInteger i = 0; i < [allThumbnailLayers_ count]; ++i) {
   1311     CALayer* layer = [self closebuttonLayerAtIndex:i];
   1312     CGPoint lp = [layer convertPoint:p fromLayer:rootLayer_];
   1313     if ([static_cast<CALayer*>([layer presentationLayer]) containsPoint:lp] &&
   1314         !layer.hidden) {
   1315       tabStripModel_->CloseWebContentsAt(i,
   1316           TabStripModel::CLOSE_USER_GESTURE |
   1317           TabStripModel::CLOSE_CREATE_HISTORICAL_TAB);
   1318       return;
   1319     }
   1320   }
   1321 
   1322   [self fadeAwayInSlomo:([event modifierFlags] & NSShiftKeyMask) != 0];
   1323 }
   1324 
   1325 - (void)swipeWithEvent:(NSEvent*)event {
   1326   if (abs([event deltaY]) > 0.5)  // Swipe up or down.
   1327     [self fadeAwayInSlomo:([event modifierFlags] & NSShiftKeyMask) != 0];
   1328 }
   1329 
   1330 - (void)close {
   1331   // Prevent parent window from disappearing.
   1332   [[self parentWindow] removeChildWindow:self];
   1333 
   1334   // We're dealloc'd in an autorelease pool by then the observer registry
   1335   // might be dead, so explicitly reset the observer now.
   1336   tabStripModelObserverBridge_.reset();
   1337 
   1338   [super close];
   1339 }
   1340 
   1341 - (void)commandDispatch:(id)sender {
   1342   if ([sender tag] == IDC_TABPOSE)
   1343     [self fadeAwayInSlomo:NO];
   1344 }
   1345 
   1346 - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
   1347   // Disable all browser-related menu items except the tab overview toggle.
   1348   SEL action = [item action];
   1349   NSInteger tag = [item tag];
   1350   return action == @selector(commandDispatch:) && tag == IDC_TABPOSE;
   1351 }
   1352 
   1353 - (void)fadeAwayTileAtIndex:(int)index {
   1354   const tabpose::Tile& tile = tileSet_->tile_at(index);
   1355   CALayer* layer = [allThumbnailLayers_ objectAtIndex:index];
   1356   // Add a delegate to one of the implicit animations to get a notification
   1357   // once the animations are done.
   1358   if (static_cast<int>(index) == tileSet_->selected_index()) {
   1359     CAAnimation* animation = [CAAnimation animation];
   1360     animation.delegate = self;
   1361     [animation setValue:kAnimationIdFadeOut forKey:kAnimationIdKey];
   1362     [layer addAnimation:animation forKey:@"frame"];
   1363   }
   1364 
   1365   // Thumbnail.
   1366   layer.frame = NSRectToCGRect(
   1367       tile.GetStartRectRelativeTo(tileSet_->selected_tile()));
   1368 
   1369   if (static_cast<int>(index) == tileSet_->selected_index()) {
   1370     // Redraw layer at big resolution, so that zoom-in isn't blocky.
   1371     [layer setNeedsDisplay];
   1372   }
   1373 
   1374   // Title.
   1375   CALayer* faviconLayer = [allFaviconLayers_ objectAtIndex:index];
   1376   faviconLayer.frame = NSRectToCGRect(
   1377       tile.GetFaviconStartRectRelativeTo(tileSet_->selected_tile()));
   1378   faviconLayer.opacity = 0;
   1379 
   1380   // Favicon.
   1381   // The |fontSize| cannot be animated directly, animate the layer's scale
   1382   // instead. |transform.scale| affects the rendered width, so keep the small
   1383   // bounds.
   1384   CALayer* titleLayer = [allTitleLayers_ objectAtIndex:index];
   1385   NSRect titleRect = tile.title_rect();
   1386   NSRect titleToRect =
   1387       tile.GetTitleStartRectRelativeTo(tileSet_->selected_tile());
   1388   CGFloat scale = NSWidth(titleToRect) / NSWidth(titleRect);
   1389   titleToRect.origin.x +=
   1390       NSWidth(titleRect) * scale * titleLayer.anchorPoint.x;
   1391   titleToRect.origin.y +=
   1392       NSHeight(titleRect) * scale * titleLayer.anchorPoint.y;
   1393   titleLayer.position = NSPointToCGPoint(titleToRect.origin);
   1394   [titleLayer setValue:[NSNumber numberWithDouble:scale]
   1395             forKeyPath:@"transform.scale"];
   1396   titleLayer.opacity = 0;
   1397 }
   1398 
   1399 - (void)fadeAwayInSlomo:(BOOL)slomo {
   1400   if (state_ == tabpose::kFadingOut)
   1401     return;
   1402 
   1403   state_ = tabpose::kFadingOut;
   1404   [self setAcceptsMouseMovedEvents:NO];
   1405 
   1406   // Select chosen tab.
   1407   if (tileSet_->selected_index() < tabStripModel_->count()) {
   1408     tabStripModel_->ActivateTabAt(tileSet_->selected_index(),
   1409                                   /*user_gesture=*/true);
   1410   } else {
   1411     DCHECK_EQ(tileSet_->selected_index(), 0);
   1412   }
   1413 
   1414   {
   1415     ScopedCAActionDisabler disableCAActions;
   1416 
   1417     // Move the selected layer on top of all other layers.
   1418     [self selectedLayer].zPosition = 1;
   1419 
   1420     selectionHighlight_.hidden = YES;
   1421     // Running animations with shadows is slow, so turn shadows off before
   1422     // running the exit animation.
   1423     for (CALayer* layer in allThumbnailLayers_.get())
   1424       layer.shadowOpacity = 0.0;
   1425 
   1426     [self updateClosebuttonLayersVisibility];
   1427   }
   1428 
   1429   // Animate layers out, all in one transaction.
   1430   CGFloat duration =
   1431       1.3 * kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1);
   1432   ScopedCAActionSetDuration durationSetter(duration);
   1433   for (int i = 0; i < tabStripModel_->count(); ++i)
   1434     [self fadeAwayTileAtIndex:i];
   1435   AnimateCALayerOpacityFromTo(topGradient_, 1, 0, duration);
   1436 }
   1437 
   1438 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
   1439   NSString* animationId = [animation valueForKey:kAnimationIdKey];
   1440   if ([animationId isEqualToString:kAnimationIdFadeIn]) {
   1441     if (finished && state_ == tabpose::kFadingIn) {
   1442       // If the user clicks while the fade in animation is still running,
   1443       // |state_| is already kFadingOut. In that case, don't do anything.
   1444       state_ = tabpose::kFadedIn;
   1445 
   1446       selectionHighlight_.hidden = NO;
   1447 
   1448       // Running animations with shadows is slow, so turn shadows on only after
   1449       // the animation is done.
   1450       ScopedCAActionDisabler disableCAActions;
   1451       for (CALayer* layer in allThumbnailLayers_.get())
   1452         layer.shadowOpacity = 0.5;
   1453 
   1454       [self updateClosebuttonLayersVisibility];
   1455     }
   1456   } else if ([animationId isEqualToString:kAnimationIdFadeOut]) {
   1457     DCHECK_EQ(tabpose::kFadingOut, state_);
   1458     [self close];
   1459   }
   1460 }
   1461 
   1462 - (NSUInteger)thumbnailLayerCount {
   1463   return [allThumbnailLayers_ count];
   1464 }
   1465 
   1466 - (int)selectedIndex {
   1467   return tileSet_->selected_index();
   1468 }
   1469 
   1470 #pragma mark TabStripModelBridge
   1471 
   1472 - (void)refreshLayerFramesAtIndex:(int)i {
   1473   const tabpose::Tile& tile = tileSet_->tile_at(i);
   1474 
   1475   CALayer* thumbLayer = [allThumbnailLayers_ objectAtIndex:i];
   1476 
   1477   if (i == tileSet_->selected_index()) {
   1478     AnimateCALayerFrameFromTo(
   1479         selectionHighlight_,
   1480         NSInsetRect(NSRectFromCGRect(thumbLayer.frame),
   1481                     -kSelectionInset, -kSelectionInset),
   1482         NSInsetRect(tile.thumb_rect(),
   1483                     -kSelectionInset, -kSelectionInset),
   1484         kObserverChangeAnimationDuration,
   1485         nil);
   1486   }
   1487 
   1488   // Repaint layer if necessary.
   1489   if (!NSEqualSizes(NSRectFromCGRect(thumbLayer.frame).size,
   1490                     tile.thumb_rect().size)) {
   1491     [thumbLayer setNeedsDisplay];
   1492   }
   1493 
   1494   // Use AnimateCALayerFrameFromTo() instead of just setting |frame| to let
   1495   // the animation match the selection animation --
   1496   // |kCAMediaTimingFunctionDefault| is 10.6-only.
   1497   AnimateCALayerFrameFromTo(
   1498       thumbLayer,
   1499       NSRectFromCGRect(thumbLayer.frame),
   1500       tile.thumb_rect(),
   1501       kObserverChangeAnimationDuration,
   1502       nil);
   1503 
   1504   CALayer* faviconLayer = [allFaviconLayers_ objectAtIndex:i];
   1505   AnimateCALayerFrameFromTo(
   1506       faviconLayer,
   1507       NSRectFromCGRect(faviconLayer.frame),
   1508       tile.favicon_rect(),
   1509       kObserverChangeAnimationDuration,
   1510       nil);
   1511 
   1512   CALayer* titleLayer = [allTitleLayers_ objectAtIndex:i];
   1513   AnimateCALayerFrameFromTo(
   1514       titleLayer,
   1515       NSRectFromCGRect(titleLayer.frame),
   1516       tile.title_rect(),
   1517       kObserverChangeAnimationDuration,
   1518       nil);
   1519 }
   1520 
   1521 - (void)insertTabWithContents:(content::WebContents*)contents
   1522                       atIndex:(NSInteger)index
   1523                  inForeground:(bool)inForeground {
   1524   // This happens if you cmd-click a link and then immediately open tabpose
   1525   // on a slowish machine.
   1526   ScopedCAActionSetDuration durationSetter(kObserverChangeAnimationDuration);
   1527 
   1528   // Insert new layer and relayout.
   1529   tileSet_->InsertTileAt(index, contents);
   1530   tileSet_->Layout(containingRect_);
   1531   [self  addLayersForTile:tileSet_->tile_at(index)
   1532                  showZoom:NO
   1533                     slomo:NO
   1534         animationDelegate:nil];
   1535 
   1536   // Update old layers.
   1537   DCHECK_EQ(tabStripModel_->count(),
   1538             static_cast<int>([allThumbnailLayers_ count]));
   1539   DCHECK_EQ(tabStripModel_->count(),
   1540             static_cast<int>([allTitleLayers_ count]));
   1541   DCHECK_EQ(tabStripModel_->count(),
   1542             static_cast<int>([allFaviconLayers_ count]));
   1543 
   1544   // Update selection.
   1545   int selectedIndex = tileSet_->selected_index();
   1546   if (selectedIndex >= index)
   1547     selectedIndex++;
   1548   [self selectTileAtIndexWithoutAnimation:selectedIndex];
   1549 
   1550   // Animate everything into its new place.
   1551   for (int i = 0; i < tabStripModel_->count(); ++i) {
   1552     if (i == index)  // The new layer.
   1553       continue;
   1554     [self refreshLayerFramesAtIndex:i];
   1555   }
   1556 }
   1557 
   1558 - (void)tabClosingWithContents:(content::WebContents*)contents
   1559                        atIndex:(NSInteger)index {
   1560   // We will also get a -tabDetachedWithContents:atIndex: notification for
   1561   // closing tabs, so do nothing here.
   1562 }
   1563 
   1564 - (void)tabDetachedWithContents:(content::WebContents*)contents
   1565                         atIndex:(NSInteger)index {
   1566   ScopedCAActionSetDuration durationSetter(kObserverChangeAnimationDuration);
   1567 
   1568   // Remove layer and relayout.
   1569   tileSet_->RemoveTileAt(index);
   1570   tileSet_->Layout(containingRect_);
   1571 
   1572   {
   1573     ScopedCAActionDisabler disabler;
   1574     [[allThumbnailLayers_ objectAtIndex:index] removeFromSuperlayer];
   1575     [allThumbnailLayers_ removeObjectAtIndex:index];
   1576     [[allTitleLayers_ objectAtIndex:index] removeFromSuperlayer];
   1577     [allTitleLayers_ removeObjectAtIndex:index];
   1578     [[allFaviconLayers_ objectAtIndex:index] removeFromSuperlayer];
   1579     [allFaviconLayers_ removeObjectAtIndex:index];
   1580   }
   1581 
   1582   // Update old layers.
   1583   DCHECK_EQ(tabStripModel_->count(),
   1584             static_cast<int>([allThumbnailLayers_ count]));
   1585   DCHECK_EQ(tabStripModel_->count(),
   1586             static_cast<int>([allTitleLayers_ count]));
   1587   DCHECK_EQ(tabStripModel_->count(),
   1588             static_cast<int>([allFaviconLayers_ count]));
   1589 
   1590   if (tabStripModel_->count() == 0)
   1591     [self close];
   1592 
   1593   // Update selection.
   1594   int selectedIndex = tileSet_->selected_index();
   1595   if (selectedIndex > index || selectedIndex >= tabStripModel_->count())
   1596     selectedIndex--;
   1597   if (selectedIndex >= 0)
   1598     [self selectTileAtIndexWithoutAnimation:selectedIndex];
   1599 
   1600   // Animate everything into its new place.
   1601   for (int i = 0; i < tabStripModel_->count(); ++i)
   1602     [self refreshLayerFramesAtIndex:i];
   1603 }
   1604 
   1605 - (void)tabMovedWithContents:(content::WebContents*)contents
   1606                     fromIndex:(NSInteger)from
   1607                       toIndex:(NSInteger)to {
   1608   ScopedCAActionSetDuration durationSetter(kObserverChangeAnimationDuration);
   1609 
   1610   // Move tile from |from| to |to|.
   1611   tileSet_->MoveTileFromTo(from, to);
   1612 
   1613   // Move corresponding layers from |from| to |to|.
   1614   base::scoped_nsobject<CALayer> thumbLayer(
   1615       [[allThumbnailLayers_ objectAtIndex:from] retain]);
   1616   [allThumbnailLayers_ removeObjectAtIndex:from];
   1617   [allThumbnailLayers_ insertObject:thumbLayer.get() atIndex:to];
   1618   base::scoped_nsobject<CALayer> faviconLayer(
   1619       [[allFaviconLayers_ objectAtIndex:from] retain]);
   1620   [allFaviconLayers_ removeObjectAtIndex:from];
   1621   [allFaviconLayers_ insertObject:faviconLayer.get() atIndex:to];
   1622   base::scoped_nsobject<CALayer> titleLayer(
   1623       [[allTitleLayers_ objectAtIndex:from] retain]);
   1624   [allTitleLayers_ removeObjectAtIndex:from];
   1625   [allTitleLayers_ insertObject:titleLayer.get() atIndex:to];
   1626 
   1627   // Update selection.
   1628   int selectedIndex = tileSet_->selected_index();
   1629   if (from == selectedIndex)
   1630     selectedIndex = to;
   1631   else if (from < selectedIndex && selectedIndex <= to)
   1632     selectedIndex--;
   1633   else if (to <= selectedIndex && selectedIndex < from)
   1634     selectedIndex++;
   1635   [self selectTileAtIndexWithoutAnimation:selectedIndex];
   1636 
   1637   // Update frames of the layers.
   1638   for (int i = std::min(from, to); i <= std::max(from, to); ++i)
   1639     [self refreshLayerFramesAtIndex:i];
   1640 }
   1641 
   1642 - (void)tabChangedWithContents:(content::WebContents*)contents
   1643                        atIndex:(NSInteger)index
   1644                     changeType:(TabStripModelObserver::TabChangeType)change {
   1645   // Tell the window to update text, title, and thumb layers at |index| to get
   1646   // their data from |contents|. |contents| can be different from the old
   1647   // contents at that index!
   1648   // While a tab is loading, this is unfortunately called quite often for
   1649   // both the "loading" and the "all" change types, so we don't really want to
   1650   // send thumb requests to the corresponding renderer when this is called.
   1651   // For now, just make sure that we don't hold on to an invalid WebContents
   1652   // object.
   1653   tabpose::Tile& tile = tileSet_->tile_at(index);
   1654   if (contents == tile.web_contents()) {
   1655     // TODO(thakis): Install a timer to send a thumb request/update title/update
   1656     // favicon after 20ms or so, and reset the timer every time this is called
   1657     // to make sure we get an updated thumb, without requesting them all over.
   1658     return;
   1659   }
   1660 
   1661   tile.set_tab_contents(contents);
   1662   ThumbnailLayer* thumbLayer = [allThumbnailLayers_ objectAtIndex:index];
   1663   [thumbLayer setWebContents:contents];
   1664 }
   1665 
   1666 - (void)tabStripModelDeleted {
   1667   [self close];
   1668 }
   1669 
   1670 @end
   1671