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