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