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