Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(156)

Side by Side Diff: chrome/browser/ui/cocoa/tabpose_window.mm

Issue 154083008: Remove Tabpose feature on mac, and supporting infrastructure (PaintAtSize) (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: rebase, merge Created 6 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
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 base::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 closeIcon_.reset([ResourceBundle::GetSharedInstance().GetNativeImageNamed(
946 IDR_TABPOSE_CLOSE).ToNSImage() retain]);
947 [self setReleasedWhenClosed:YES];
948 [self setOpaque:NO];
949 [self setBackgroundColor:[NSColor clearColor]];
950 [self setUpLayersInSlomo:slomo];
951 [self setAcceptsMouseMovedEvents:YES];
952 [parent addChildWindow:self ordered:NSWindowAbove];
953 [self makeKeyAndOrderFront:self];
954 }
955 return self;
956 }
957
958 - (CALayer*)selectedLayer {
959 return [allThumbnailLayers_ objectAtIndex:tileSet_->selected_index()];
960 }
961
962 - (void)selectTileAtIndexWithoutAnimation:(int)newIndex {
963 ScopedCAActionDisabler disabler;
964 const tabpose::Tile& tile = tileSet_->tile_at(newIndex);
965 selectionHighlight_.frame =
966 NSRectToCGRect(NSInsetRect(tile.thumb_rect(),
967 -kSelectionInset, -kSelectionInset));
968 tileSet_->set_selected_index(newIndex);
969
970 [self updateClosebuttonLayersVisibility];
971 }
972
973 - (void)addLayersForTile:(tabpose::Tile&)tile
974 showZoom:(BOOL)showZoom
975 slomo:(BOOL)slomo
976 animationDelegate:(id)animationDelegate {
977 base::scoped_nsobject<CALayer> layer(
978 [[ThumbnailLayer alloc] initWithWebContents:tile.web_contents()
979 fullSize:tile.GetStartRectRelativeTo(
980 tileSet_->selected_tile()).size]);
981 [layer setNeedsDisplay];
982
983 NSTimeInterval interval =
984 kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1);
985
986 // Background color as placeholder for now.
987 layer.get().backgroundColor = CGColorGetConstantColor(kCGColorWhite);
988 if (showZoom) {
989 AnimateCALayerFrameFromTo(
990 layer,
991 tile.GetStartRectRelativeTo(tileSet_->selected_tile()),
992 tile.thumb_rect(),
993 interval,
994 animationDelegate);
995 } else {
996 layer.get().frame = NSRectToCGRect(tile.thumb_rect());
997 }
998
999 layer.get().shadowRadius = 10;
1000 layer.get().shadowOffset = CGSizeMake(0, -10);
1001 if (state_ == tabpose::kFadedIn)
1002 layer.get().shadowOpacity = 0.5;
1003
1004 // Add a close button to the thumb layer.
1005 CALayer* closeLayer = [CALayer layer];
1006 closeLayer.contents = closeIcon_.get();
1007 CGRect closeBounds = {};
1008 closeBounds.size = NSSizeToCGSize([closeIcon_ size]);
1009 closeLayer.bounds = closeBounds;
1010 closeLayer.hidden = YES;
1011
1012 [closeLayer addConstraint:
1013 [CAConstraint constraintWithAttribute:kCAConstraintMidX
1014 relativeTo:@"superlayer"
1015 attribute:kCAConstraintMinX]];
1016 [closeLayer addConstraint:
1017 [CAConstraint constraintWithAttribute:kCAConstraintMidY
1018 relativeTo:@"superlayer"
1019 attribute:kCAConstraintMaxY]];
1020
1021 layer.get().layoutManager = [CAConstraintLayoutManager layoutManager];
1022 [layer.get() addSublayer:closeLayer];
1023
1024 [bgLayer_ addSublayer:layer];
1025 [allThumbnailLayers_ addObject:layer];
1026
1027 // Favicon and title.
1028 NSFont* font = [NSFont systemFontOfSize:tile.title_font_size()];
1029 tile.set_font_metrics([font ascender], -[font descender]);
1030
1031 CALayer* faviconLayer = [CALayer layer];
1032 if (showZoom) {
1033 AnimateCALayerFrameFromTo(
1034 faviconLayer,
1035 tile.GetFaviconStartRectRelativeTo(tileSet_->selected_tile()),
1036 tile.favicon_rect(),
1037 interval,
1038 nil);
1039 AnimateCALayerOpacityFromTo(faviconLayer, 0.0, 1.0, interval);
1040 } else {
1041 faviconLayer.frame = NSRectToCGRect(tile.favicon_rect());
1042 }
1043 faviconLayer.contents = tile.favicon();
1044 faviconLayer.zPosition = 1; // On top of the thumb shadow.
1045 [bgLayer_ addSublayer:faviconLayer];
1046 [allFaviconLayers_ addObject:faviconLayer];
1047
1048 // CATextLayers can't animate their fontSize property, at least on 10.5.
1049 // Animate transform.scale instead.
1050
1051 // The scaling should have its origin in the layer's upper left corner.
1052 // This needs to be set before |AnimateCALayerFrameFromTo()| is called.
1053 CATextLayer* titleLayer = [CATextLayer layer];
1054 titleLayer.anchorPoint = CGPointMake(0, 1);
1055 if (showZoom) {
1056 NSRect fromRect =
1057 tile.GetTitleStartRectRelativeTo(tileSet_->selected_tile());
1058 NSRect toRect = tile.title_rect();
1059 CGFloat scale = NSWidth(fromRect) / NSWidth(toRect);
1060 fromRect.size = toRect.size;
1061
1062 // Add scale animation.
1063 CABasicAnimation* scaleAnimation =
1064 [CABasicAnimation animationWithKeyPath:@"transform.scale"];
1065 scaleAnimation.fromValue = [NSNumber numberWithDouble:scale];
1066 scaleAnimation.toValue = [NSNumber numberWithDouble:1.0];
1067 scaleAnimation.duration = interval;
1068 scaleAnimation.timingFunction =
1069 [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
1070 [titleLayer addAnimation:scaleAnimation forKey:@"transform.scale"];
1071
1072 // Add the position and opacity animations.
1073 AnimateScaledCALayerFrameFromTo(
1074 titleLayer, fromRect, scale, toRect, 1.0, interval, nil);
1075 AnimateCALayerOpacityFromTo(faviconLayer, 0.0, 1.0, interval);
1076 } else {
1077 titleLayer.frame = NSRectToCGRect(tile.title_rect());
1078 }
1079 titleLayer.string = base::SysUTF16ToNSString(tile.title());
1080 titleLayer.fontSize = [font pointSize];
1081 titleLayer.truncationMode = kCATruncationEnd;
1082 titleLayer.font = font;
1083 titleLayer.zPosition = 1; // On top of the thumb shadow.
1084 [bgLayer_ addSublayer:titleLayer];
1085 [allTitleLayers_ addObject:titleLayer];
1086 }
1087
1088 - (void)setUpLayersInSlomo:(BOOL)slomo {
1089 // Root layer -- covers whole window.
1090 rootLayer_ = [CALayer layer];
1091
1092 // In a block so that the layers don't fade in.
1093 {
1094 ScopedCAActionDisabler disabler;
1095 // Background layer -- the visible part of the window.
1096 gray_.reset(CGColorCreateGenericGray(kCentralGray, 1.0));
1097 bgLayer_ = [CALayer layer];
1098 bgLayer_.backgroundColor = gray_;
1099 bgLayer_.frame = NSRectToCGRect(containingRect_);
1100 bgLayer_.masksToBounds = YES;
1101 [rootLayer_ addSublayer:bgLayer_];
1102
1103 // Selection highlight layer.
1104 darkBlue_.reset(CGColorCreateGenericRGB(0.25, 0.34, 0.86, 1.0));
1105 selectionHighlight_ = [CALayer layer];
1106 selectionHighlight_.backgroundColor = darkBlue_;
1107 selectionHighlight_.cornerRadius = 5.0;
1108 selectionHighlight_.zPosition = -1; // Behind other layers.
1109 selectionHighlight_.hidden = YES;
1110 [bgLayer_ addSublayer:selectionHighlight_];
1111
1112 // Bottom gradient.
1113 CALayer* gradientLayer = [[[GrayGradientLayer alloc]
1114 initWithStartGray:kCentralGray endGray:kBottomGray] autorelease];
1115 gradientLayer.frame = CGRectMake(
1116 0,
1117 0,
1118 NSWidth(containingRect_),
1119 kBottomGradientHeight);
1120 [gradientLayer setNeedsDisplay]; // Draw once.
1121 [bgLayer_ addSublayer:gradientLayer];
1122 }
1123 // Top gradient (fades in).
1124 CGFloat toolbarHeight = NSHeight([self frame]) - NSHeight(containingRect_);
1125 topGradient_ = [[[GrayGradientLayer alloc]
1126 initWithStartGray:kTopGray endGray:kCentralGray] autorelease];
1127 topGradient_.frame = CGRectMake(
1128 0,
1129 NSHeight([self frame]) - toolbarHeight,
1130 NSWidth(containingRect_),
1131 toolbarHeight);
1132 [topGradient_ setNeedsDisplay]; // Draw once.
1133 [rootLayer_ addSublayer:topGradient_];
1134 NSTimeInterval interval =
1135 kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1);
1136 AnimateCALayerOpacityFromTo(topGradient_, 0, 1, interval);
1137
1138 // Layers for the tab thumbnails.
1139 tileSet_->Build(tabStripModel_);
1140 tileSet_->Layout(containingRect_);
1141 allThumbnailLayers_.reset(
1142 [[NSMutableArray alloc] initWithCapacity:tabStripModel_->count()]);
1143 allFaviconLayers_.reset(
1144 [[NSMutableArray alloc] initWithCapacity:tabStripModel_->count()]);
1145 allTitleLayers_.reset(
1146 [[NSMutableArray alloc] initWithCapacity:tabStripModel_->count()]);
1147
1148 for (int i = 0; i < tabStripModel_->count(); ++i) {
1149 // Add a delegate to one of the animations to get a notification once the
1150 // animations are done.
1151 [self addLayersForTile:tileSet_->tile_at(i)
1152 showZoom:YES
1153 slomo:slomo
1154 animationDelegate:i == tileSet_->selected_index() ? self : nil];
1155 if (i == tileSet_->selected_index()) {
1156 CALayer* layer = [allThumbnailLayers_ objectAtIndex:i];
1157 CAAnimation* animation = [layer animationForKey:@"bounds"];
1158 DCHECK(animation);
1159 [animation setValue:kAnimationIdFadeIn forKey:kAnimationIdKey];
1160 }
1161 }
1162 [self selectTileAtIndexWithoutAnimation:tileSet_->selected_index()];
1163
1164 // Needs to happen after all layers have been added to |rootLayer_|, else
1165 // there's a one frame flash of grey at the beginning of the animation
1166 // (|bgLayer_| showing through with none of its children visible yet).
1167 [[self contentView] setLayer:rootLayer_];
1168 [[self contentView] setWantsLayer:YES];
1169 }
1170
1171 - (BOOL)canBecomeKeyWindow {
1172 return YES;
1173 }
1174
1175 // Lets the traffic light buttons on the browser window keep their "active"
1176 // state while an info bubble is open. Only has an effect on 10.7.
1177 - (BOOL)_sharesParentKeyState {
1178 return YES;
1179 }
1180
1181 // Handle key events that should be executed repeatedly while the key is down.
1182 - (void)keyDown:(NSEvent*)event {
1183 if (state_ == tabpose::kFadingOut)
1184 return;
1185 NSString* characters = [event characters];
1186 if ([characters length] < 1)
1187 return;
1188
1189 unichar character = [characters characterAtIndex:0];
1190 int newIndex = -1;
1191 switch (character) {
1192 case NSUpArrowFunctionKey:
1193 newIndex = tileSet_->up_index();
1194 break;
1195 case NSDownArrowFunctionKey:
1196 newIndex = tileSet_->down_index();
1197 break;
1198 case NSLeftArrowFunctionKey:
1199 newIndex = tileSet_->left_index();
1200 break;
1201 case NSRightArrowFunctionKey:
1202 newIndex = tileSet_->right_index();
1203 break;
1204 case NSTabCharacter:
1205 newIndex = tileSet_->next_index();
1206 break;
1207 case NSBackTabCharacter:
1208 newIndex = tileSet_->previous_index();
1209 break;
1210 }
1211 if (newIndex != -1)
1212 [self selectTileAtIndexWithoutAnimation:newIndex];
1213 }
1214
1215 // Handle keyboard events that should be executed once when the key is released.
1216 - (void)keyUp:(NSEvent*)event {
1217 if (state_ == tabpose::kFadingOut)
1218 return;
1219 NSString* characters = [event characters];
1220 if ([characters length] < 1)
1221 return;
1222
1223 unichar character = [characters characterAtIndex:0];
1224 switch (character) {
1225 case NSEnterCharacter:
1226 case NSNewlineCharacter:
1227 case NSCarriageReturnCharacter:
1228 case ' ':
1229 [self fadeAwayInSlomo:([event modifierFlags] & NSShiftKeyMask) != 0];
1230 break;
1231 case '\e': // Escape
1232 tileSet_->set_selected_index(tabStripModel_->active_index());
1233 [self fadeAwayInSlomo:([event modifierFlags] & NSShiftKeyMask) != 0];
1234 break;
1235 }
1236 }
1237
1238 // Handle keyboard events that contain cmd or ctrl.
1239 - (BOOL)performKeyEquivalent:(NSEvent*)event {
1240 if (state_ == tabpose::kFadingOut)
1241 return NO;
1242 NSString* characters = [event characters];
1243 if ([characters length] < 1)
1244 return NO;
1245 unichar character = [characters characterAtIndex:0];
1246 if ([event modifierFlags] & NSCommandKeyMask) {
1247 if (character >= '1' && character <= '9') {
1248 int index =
1249 character == '9' ? tabStripModel_->count() - 1 : character - '1';
1250 if (index < tabStripModel_->count()) {
1251 tileSet_->set_selected_index(index);
1252 [self fadeAwayInSlomo:([event modifierFlags] & NSShiftKeyMask) != 0];
1253 return YES;
1254 }
1255 }
1256 }
1257 return NO;
1258 }
1259
1260 - (void)flagsChanged:(NSEvent*)event {
1261 showAllCloseLayers_ = ([event modifierFlags] & NSAlternateKeyMask) != 0;
1262 [self updateClosebuttonLayersVisibility];
1263 }
1264
1265 - (void)selectTileFromMouseEvent:(NSEvent*)event {
1266 int newIndex = -1;
1267 CGPoint p = NSPointToCGPoint([event locationInWindow]);
1268 for (NSUInteger i = 0; i < [allThumbnailLayers_ count]; ++i) {
1269 CALayer* layer = [allThumbnailLayers_ objectAtIndex:i];
1270 CGPoint lp = [layer convertPoint:p fromLayer:rootLayer_];
1271 if ([static_cast<CALayer*>([layer presentationLayer]) containsPoint:lp])
1272 newIndex = i;
1273 }
1274 if (newIndex >= 0)
1275 [self selectTileAtIndexWithoutAnimation:newIndex];
1276 }
1277
1278 - (void)mouseMoved:(NSEvent*)event {
1279 [self selectTileFromMouseEvent:event];
1280 }
1281
1282 - (CALayer*)closebuttonLayerAtIndex:(NSUInteger)index {
1283 CALayer* layer = [allThumbnailLayers_ objectAtIndex:index];
1284 return [[layer sublayers] objectAtIndex:0];
1285 }
1286
1287 - (void)updateClosebuttonLayersVisibility {
1288 for (NSUInteger i = 0; i < [allThumbnailLayers_ count]; ++i) {
1289 CALayer* layer = [self closebuttonLayerAtIndex:i];
1290 BOOL isSelectedTile = static_cast<int>(i) == tileSet_->selected_index();
1291 BOOL isVisible = state_ == tabpose::kFadedIn &&
1292 (isSelectedTile || showAllCloseLayers_);
1293 layer.hidden = !isVisible;
1294 }
1295 }
1296
1297 - (void)mouseDown:(NSEvent*)event {
1298 // Just in case the user clicked without ever moving the mouse.
1299 [self selectTileFromMouseEvent:event];
1300
1301 // If the click occurred in a close box, close that tab and don't do anything
1302 // else.
1303 CGPoint p = NSPointToCGPoint([event locationInWindow]);
1304 for (NSUInteger i = 0; i < [allThumbnailLayers_ count]; ++i) {
1305 CALayer* layer = [self closebuttonLayerAtIndex:i];
1306 CGPoint lp = [layer convertPoint:p fromLayer:rootLayer_];
1307 if ([static_cast<CALayer*>([layer presentationLayer]) containsPoint:lp] &&
1308 !layer.hidden) {
1309 tabStripModel_->CloseWebContentsAt(i,
1310 TabStripModel::CLOSE_USER_GESTURE |
1311 TabStripModel::CLOSE_CREATE_HISTORICAL_TAB);
1312 return;
1313 }
1314 }
1315
1316 [self fadeAwayInSlomo:([event modifierFlags] & NSShiftKeyMask) != 0];
1317 }
1318
1319 - (void)swipeWithEvent:(NSEvent*)event {
1320 if (abs([event deltaY]) > 0.5) // Swipe up or down.
1321 [self fadeAwayInSlomo:([event modifierFlags] & NSShiftKeyMask) != 0];
1322 }
1323
1324 - (void)close {
1325 // Prevent parent window from disappearing.
1326 [[self parentWindow] removeChildWindow:self];
1327
1328 // We're dealloc'd in an autorelease pool – by then the observer registry
1329 // might be dead, so explicitly reset the observer now.
1330 tabStripModelObserverBridge_.reset();
1331
1332 [super close];
1333 }
1334
1335 - (void)commandDispatch:(id)sender {
1336 if ([sender tag] == IDC_TABPOSE)
1337 [self fadeAwayInSlomo:NO];
1338 }
1339
1340 - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
1341 // Disable all browser-related menu items except the tab overview toggle.
1342 SEL action = [item action];
1343 NSInteger tag = [item tag];
1344 return action == @selector(commandDispatch:) && tag == IDC_TABPOSE;
1345 }
1346
1347 - (void)fadeAwayTileAtIndex:(int)index {
1348 const tabpose::Tile& tile = tileSet_->tile_at(index);
1349 CALayer* layer = [allThumbnailLayers_ objectAtIndex:index];
1350 // Add a delegate to one of the implicit animations to get a notification
1351 // once the animations are done.
1352 if (static_cast<int>(index) == tileSet_->selected_index()) {
1353 CAAnimation* animation = [CAAnimation animation];
1354 animation.delegate = self;
1355 [animation setValue:kAnimationIdFadeOut forKey:kAnimationIdKey];
1356 [layer addAnimation:animation forKey:@"frame"];
1357 }
1358
1359 // Thumbnail.
1360 layer.frame = NSRectToCGRect(
1361 tile.GetStartRectRelativeTo(tileSet_->selected_tile()));
1362
1363 if (static_cast<int>(index) == tileSet_->selected_index()) {
1364 // Redraw layer at big resolution, so that zoom-in isn't blocky.
1365 [layer setNeedsDisplay];
1366 }
1367
1368 // Title.
1369 CALayer* faviconLayer = [allFaviconLayers_ objectAtIndex:index];
1370 faviconLayer.frame = NSRectToCGRect(
1371 tile.GetFaviconStartRectRelativeTo(tileSet_->selected_tile()));
1372 faviconLayer.opacity = 0;
1373
1374 // Favicon.
1375 // The |fontSize| cannot be animated directly, animate the layer's scale
1376 // instead. |transform.scale| affects the rendered width, so keep the small
1377 // bounds.
1378 CALayer* titleLayer = [allTitleLayers_ objectAtIndex:index];
1379 NSRect titleRect = tile.title_rect();
1380 NSRect titleToRect =
1381 tile.GetTitleStartRectRelativeTo(tileSet_->selected_tile());
1382 CGFloat scale = NSWidth(titleToRect) / NSWidth(titleRect);
1383 titleToRect.origin.x +=
1384 NSWidth(titleRect) * scale * titleLayer.anchorPoint.x;
1385 titleToRect.origin.y +=
1386 NSHeight(titleRect) * scale * titleLayer.anchorPoint.y;
1387 titleLayer.position = NSPointToCGPoint(titleToRect.origin);
1388 [titleLayer setValue:[NSNumber numberWithDouble:scale]
1389 forKeyPath:@"transform.scale"];
1390 titleLayer.opacity = 0;
1391 }
1392
1393 - (void)fadeAwayInSlomo:(BOOL)slomo {
1394 if (state_ == tabpose::kFadingOut)
1395 return;
1396
1397 state_ = tabpose::kFadingOut;
1398 [self setAcceptsMouseMovedEvents:NO];
1399
1400 // Select chosen tab.
1401 if (tileSet_->selected_index() < tabStripModel_->count()) {
1402 tabStripModel_->ActivateTabAt(tileSet_->selected_index(),
1403 /*user_gesture=*/true);
1404 } else {
1405 DCHECK_EQ(tileSet_->selected_index(), 0);
1406 }
1407
1408 {
1409 ScopedCAActionDisabler disableCAActions;
1410
1411 // Move the selected layer on top of all other layers.
1412 [self selectedLayer].zPosition = 1;
1413
1414 selectionHighlight_.hidden = YES;
1415 // Running animations with shadows is slow, so turn shadows off before
1416 // running the exit animation.
1417 for (CALayer* layer in allThumbnailLayers_.get())
1418 layer.shadowOpacity = 0.0;
1419
1420 [self updateClosebuttonLayersVisibility];
1421 }
1422
1423 // Animate layers out, all in one transaction.
1424 CGFloat duration =
1425 1.3 * kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1);
1426 ScopedCAActionSetDuration durationSetter(duration);
1427 for (int i = 0; i < tabStripModel_->count(); ++i)
1428 [self fadeAwayTileAtIndex:i];
1429 AnimateCALayerOpacityFromTo(topGradient_, 1, 0, duration);
1430 }
1431
1432 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
1433 NSString* animationId = [animation valueForKey:kAnimationIdKey];
1434 if ([animationId isEqualToString:kAnimationIdFadeIn]) {
1435 if (finished && state_ == tabpose::kFadingIn) {
1436 // If the user clicks while the fade in animation is still running,
1437 // |state_| is already kFadingOut. In that case, don't do anything.
1438 state_ = tabpose::kFadedIn;
1439
1440 selectionHighlight_.hidden = NO;
1441
1442 // Running animations with shadows is slow, so turn shadows on only after
1443 // the animation is done.
1444 ScopedCAActionDisabler disableCAActions;
1445 for (CALayer* layer in allThumbnailLayers_.get())
1446 layer.shadowOpacity = 0.5;
1447
1448 [self updateClosebuttonLayersVisibility];
1449 }
1450 } else if ([animationId isEqualToString:kAnimationIdFadeOut]) {
1451 DCHECK_EQ(tabpose::kFadingOut, state_);
1452 [self close];
1453 }
1454 }
1455
1456 - (NSUInteger)thumbnailLayerCount {
1457 return [allThumbnailLayers_ count];
1458 }
1459
1460 - (int)selectedIndex {
1461 return tileSet_->selected_index();
1462 }
1463
1464 #pragma mark TabStripModelBridge
1465
1466 - (void)refreshLayerFramesAtIndex:(int)i {
1467 const tabpose::Tile& tile = tileSet_->tile_at(i);
1468
1469 CALayer* thumbLayer = [allThumbnailLayers_ objectAtIndex:i];
1470
1471 if (i == tileSet_->selected_index()) {
1472 AnimateCALayerFrameFromTo(
1473 selectionHighlight_,
1474 NSInsetRect(NSRectFromCGRect(thumbLayer.frame),
1475 -kSelectionInset, -kSelectionInset),
1476 NSInsetRect(tile.thumb_rect(),
1477 -kSelectionInset, -kSelectionInset),
1478 kObserverChangeAnimationDuration,
1479 nil);
1480 }
1481
1482 // Repaint layer if necessary.
1483 if (!NSEqualSizes(NSRectFromCGRect(thumbLayer.frame).size,
1484 tile.thumb_rect().size)) {
1485 [thumbLayer setNeedsDisplay];
1486 }
1487
1488 // Use AnimateCALayerFrameFromTo() instead of just setting |frame| to let
1489 // the animation match the selection animation --
1490 // |kCAMediaTimingFunctionDefault| is 10.6-only.
1491 AnimateCALayerFrameFromTo(
1492 thumbLayer,
1493 NSRectFromCGRect(thumbLayer.frame),
1494 tile.thumb_rect(),
1495 kObserverChangeAnimationDuration,
1496 nil);
1497
1498 CALayer* faviconLayer = [allFaviconLayers_ objectAtIndex:i];
1499 AnimateCALayerFrameFromTo(
1500 faviconLayer,
1501 NSRectFromCGRect(faviconLayer.frame),
1502 tile.favicon_rect(),
1503 kObserverChangeAnimationDuration,
1504 nil);
1505
1506 CALayer* titleLayer = [allTitleLayers_ objectAtIndex:i];
1507 AnimateCALayerFrameFromTo(
1508 titleLayer,
1509 NSRectFromCGRect(titleLayer.frame),
1510 tile.title_rect(),
1511 kObserverChangeAnimationDuration,
1512 nil);
1513 }
1514
1515 - (void)insertTabWithContents:(content::WebContents*)contents
1516 atIndex:(NSInteger)index
1517 inForeground:(bool)inForeground {
1518 // This happens if you cmd-click a link and then immediately open tabpose
1519 // on a slowish machine.
1520 ScopedCAActionSetDuration durationSetter(kObserverChangeAnimationDuration);
1521
1522 // Insert new layer and relayout.
1523 tileSet_->InsertTileAt(index, contents);
1524 tileSet_->Layout(containingRect_);
1525 [self addLayersForTile:tileSet_->tile_at(index)
1526 showZoom:NO
1527 slomo:NO
1528 animationDelegate:nil];
1529
1530 // Update old layers.
1531 DCHECK_EQ(tabStripModel_->count(),
1532 static_cast<int>([allThumbnailLayers_ count]));
1533 DCHECK_EQ(tabStripModel_->count(),
1534 static_cast<int>([allTitleLayers_ count]));
1535 DCHECK_EQ(tabStripModel_->count(),
1536 static_cast<int>([allFaviconLayers_ count]));
1537
1538 // Update selection.
1539 int selectedIndex = tileSet_->selected_index();
1540 if (selectedIndex >= index)
1541 selectedIndex++;
1542 [self selectTileAtIndexWithoutAnimation:selectedIndex];
1543
1544 // Animate everything into its new place.
1545 for (int i = 0; i < tabStripModel_->count(); ++i) {
1546 if (i == index) // The new layer.
1547 continue;
1548 [self refreshLayerFramesAtIndex:i];
1549 }
1550 }
1551
1552 - (void)tabClosingWithContents:(content::WebContents*)contents
1553 atIndex:(NSInteger)index {
1554 // We will also get a -tabDetachedWithContents:atIndex: notification for
1555 // closing tabs, so do nothing here.
1556 }
1557
1558 - (void)tabDetachedWithContents:(content::WebContents*)contents
1559 atIndex:(NSInteger)index {
1560 ScopedCAActionSetDuration durationSetter(kObserverChangeAnimationDuration);
1561
1562 // Remove layer and relayout.
1563 tileSet_->RemoveTileAt(index);
1564 tileSet_->Layout(containingRect_);
1565
1566 {
1567 ScopedCAActionDisabler disabler;
1568 [[allThumbnailLayers_ objectAtIndex:index] removeFromSuperlayer];
1569 [allThumbnailLayers_ removeObjectAtIndex:index];
1570 [[allTitleLayers_ objectAtIndex:index] removeFromSuperlayer];
1571 [allTitleLayers_ removeObjectAtIndex:index];
1572 [[allFaviconLayers_ objectAtIndex:index] removeFromSuperlayer];
1573 [allFaviconLayers_ removeObjectAtIndex:index];
1574 }
1575
1576 // Update old layers.
1577 DCHECK_EQ(tabStripModel_->count(),
1578 static_cast<int>([allThumbnailLayers_ count]));
1579 DCHECK_EQ(tabStripModel_->count(),
1580 static_cast<int>([allTitleLayers_ count]));
1581 DCHECK_EQ(tabStripModel_->count(),
1582 static_cast<int>([allFaviconLayers_ count]));
1583
1584 if (tabStripModel_->count() == 0)
1585 [self close];
1586
1587 // Update selection.
1588 int selectedIndex = tileSet_->selected_index();
1589 if (selectedIndex > index || selectedIndex >= tabStripModel_->count())
1590 selectedIndex--;
1591 if (selectedIndex >= 0)
1592 [self selectTileAtIndexWithoutAnimation:selectedIndex];
1593
1594 // Animate everything into its new place.
1595 for (int i = 0; i < tabStripModel_->count(); ++i)
1596 [self refreshLayerFramesAtIndex:i];
1597 }
1598
1599 - (void)tabMovedWithContents:(content::WebContents*)contents
1600 fromIndex:(NSInteger)from
1601 toIndex:(NSInteger)to {
1602 ScopedCAActionSetDuration durationSetter(kObserverChangeAnimationDuration);
1603
1604 // Move tile from |from| to |to|.
1605 tileSet_->MoveTileFromTo(from, to);
1606
1607 // Move corresponding layers from |from| to |to|.
1608 base::scoped_nsobject<CALayer> thumbLayer(
1609 [[allThumbnailLayers_ objectAtIndex:from] retain]);
1610 [allThumbnailLayers_ removeObjectAtIndex:from];
1611 [allThumbnailLayers_ insertObject:thumbLayer.get() atIndex:to];
1612 base::scoped_nsobject<CALayer> faviconLayer(
1613 [[allFaviconLayers_ objectAtIndex:from] retain]);
1614 [allFaviconLayers_ removeObjectAtIndex:from];
1615 [allFaviconLayers_ insertObject:faviconLayer.get() atIndex:to];
1616 base::scoped_nsobject<CALayer> titleLayer(
1617 [[allTitleLayers_ objectAtIndex:from] retain]);
1618 [allTitleLayers_ removeObjectAtIndex:from];
1619 [allTitleLayers_ insertObject:titleLayer.get() atIndex:to];
1620
1621 // Update selection.
1622 int selectedIndex = tileSet_->selected_index();
1623 if (from == selectedIndex)
1624 selectedIndex = to;
1625 else if (from < selectedIndex && selectedIndex <= to)
1626 selectedIndex--;
1627 else if (to <= selectedIndex && selectedIndex < from)
1628 selectedIndex++;
1629 [self selectTileAtIndexWithoutAnimation:selectedIndex];
1630
1631 // Update frames of the layers.
1632 for (int i = std::min(from, to); i <= std::max(from, to); ++i)
1633 [self refreshLayerFramesAtIndex:i];
1634 }
1635
1636 - (void)tabChangedWithContents:(content::WebContents*)contents
1637 atIndex:(NSInteger)index
1638 changeType:(TabStripModelObserver::TabChangeType)change {
1639 // Tell the window to update text, title, and thumb layers at |index| to get
1640 // their data from |contents|. |contents| can be different from the old
1641 // contents at that index!
1642 // While a tab is loading, this is unfortunately called quite often for
1643 // both the "loading" and the "all" change types, so we don't really want to
1644 // send thumb requests to the corresponding renderer when this is called.
1645 // For now, just make sure that we don't hold on to an invalid WebContents
1646 // object.
1647 tabpose::Tile& tile = tileSet_->tile_at(index);
1648 if (contents == tile.web_contents()) {
1649 // TODO(thakis): Install a timer to send a thumb request/update title/update
1650 // favicon after 20ms or so, and reset the timer every time this is called
1651 // to make sure we get an updated thumb, without requesting them all over.
1652 return;
1653 }
1654
1655 tile.set_tab_contents(contents);
1656 ThumbnailLayer* thumbLayer = [allThumbnailLayers_ objectAtIndex:index];
1657 [thumbLayer setWebContents:contents];
1658 }
1659
1660 - (void)tabStripModelDeleted {
1661 [self close];
1662 }
1663
1664 @end
OLDNEW
« no previous file with comments | « chrome/browser/ui/cocoa/tabpose_window.h ('k') | chrome/browser/ui/cocoa/tabpose_window_unittest.mm » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698