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