diff options
author | Kristian Monsen <kristianm@google.com> | 2011-05-11 20:53:37 +0100 |
---|---|---|
committer | Kristian Monsen <kristianm@google.com> | 2011-05-16 13:54:48 +0100 |
commit | 21d179b334e59e9a3bfcaed4c4430bef1bc5759d (patch) | |
tree | 64e2bb6da27af6a5c93ca34f6051584aafbfcb9e /chrome/browser/ui/cocoa/tabpose_window.mm | |
parent | 0c63f00edd6ed0482fd5cbcea937ca088baf7858 (diff) | |
download | external_chromium-21d179b334e59e9a3bfcaed4c4430bef1bc5759d.zip external_chromium-21d179b334e59e9a3bfcaed4c4430bef1bc5759d.tar.gz external_chromium-21d179b334e59e9a3bfcaed4c4430bef1bc5759d.tar.bz2 |
Merge Chromium at 10.0.621.0: Initial merge by git.
Change-Id: I070cc91c608dfa4a968a5a54c173260765ac8097
Diffstat (limited to 'chrome/browser/ui/cocoa/tabpose_window.mm')
-rw-r--r-- | chrome/browser/ui/cocoa/tabpose_window.mm | 1554 |
1 files changed, 1554 insertions, 0 deletions
diff --git a/chrome/browser/ui/cocoa/tabpose_window.mm b/chrome/browser/ui/cocoa/tabpose_window.mm new file mode 100644 index 0000000..7f96f59 --- /dev/null +++ b/chrome/browser/ui/cocoa/tabpose_window.mm @@ -0,0 +1,1554 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/tabpose_window.h" + +#import <QuartzCore/QuartzCore.h> + +#include "app/resource_bundle.h" +#include "base/mac_util.h" +#include "base/mac/scoped_cftyperef.h" +#include "base/scoped_callback_factory.h" +#include "base/sys_string_conversions.h" +#include "chrome/app/chrome_command_ids.h" +#include "chrome/browser/browser_process.h" +#import "chrome/browser/debugger/devtools_window.h" +#include "chrome/browser/prefs/pref_service.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/renderer_host/backing_store_mac.h" +#include "chrome/browser/renderer_host/render_view_host.h" +#include "chrome/browser/renderer_host/render_widget_host_view_mac.h" +#include "chrome/browser/tab_contents/tab_contents.h" +#include "chrome/browser/tab_contents/thumbnail_generator.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h" +#import "chrome/browser/ui/cocoa/browser_window_controller.h" +#import "chrome/browser/ui/cocoa/tab_strip_controller.h" +#import "chrome/browser/ui/cocoa/tab_strip_model_observer_bridge.h" +#include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h" +#include "chrome/common/pref_names.h" +#include "gfx/scoped_cg_context_state_mac.h" +#include "grit/app_resources.h" +#include "skia/ext/skia_utils_mac.h" +#include "third_party/skia/include/utils/mac/SkCGUtils.h" + +const int kTopGradientHeight = 15; + +NSString* const kAnimationIdKey = @"AnimationId"; +NSString* const kAnimationIdFadeIn = @"FadeIn"; +NSString* const kAnimationIdFadeOut = @"FadeOut"; + +const CGFloat kDefaultAnimationDuration = 0.25; // In seconds. +const CGFloat kSlomoFactor = 4; +const CGFloat kObserverChangeAnimationDuration = 0.75; // In seconds. + +// CAGradientLayer is 10.6-only -- roll our own. +@interface DarkGradientLayer : CALayer +- (void)drawInContext:(CGContextRef)context; +@end + +@implementation DarkGradientLayer +- (void)drawInContext:(CGContextRef)context { + base::mac::ScopedCFTypeRef<CGColorSpaceRef> grayColorSpace( + CGColorSpaceCreateWithName(kCGColorSpaceGenericGray)); + CGFloat grays[] = { 0.277, 1.0, 0.39, 1.0 }; + CGFloat locations[] = { 0, 1 }; + base::mac::ScopedCFTypeRef<CGGradientRef> gradient( + CGGradientCreateWithColorComponents( + grayColorSpace.get(), grays, locations, arraysize(locations))); + CGPoint topLeft = CGPointMake(0.0, kTopGradientHeight); + CGContextDrawLinearGradient(context, gradient.get(), topLeft, CGPointZero, 0); +} +@end + +namespace tabpose { +class ThumbnailLoader; +} + +// A CALayer that draws a thumbnail for a TabContents object. The layer tries +// to draw the TabContents's backing store directly if possible, and requests +// a thumbnail bitmap from the TabContents's renderer process if not. +@interface ThumbnailLayer : CALayer { + // The TabContents the thumbnail is for. + TabContents* contents_; // weak + + // The size the thumbnail is drawn at when zoomed in. + NSSize fullSize_; + + // Used to load a thumbnail, if required. + scoped_refptr<tabpose::ThumbnailLoader> loader_; + + // If the backing store couldn't be used and a thumbnail was returned from a + // renderer process, it's stored in |thumbnail_|. + base::mac::ScopedCFTypeRef<CGImageRef> thumbnail_; + + // True if the layer already sent a thumbnail request to a renderer. + BOOL didSendLoad_; +} +- (id)initWithTabContents:(TabContents*)contents fullSize:(NSSize)fullSize; +- (void)drawInContext:(CGContextRef)context; +- (void)setThumbnail:(const SkBitmap&)bitmap; +@end + +namespace tabpose { + +// ThumbnailLoader talks to the renderer process to load a thumbnail of a given +// RenderWidgetHost, and sends the thumbnail back to a ThumbnailLayer once it +// comes back from the renderer. +class ThumbnailLoader : public base::RefCountedThreadSafe<ThumbnailLoader> { + public: + ThumbnailLoader(gfx::Size size, RenderWidgetHost* rwh, ThumbnailLayer* layer) + : size_(size), rwh_(rwh), layer_(layer), factory_(this) {} + + // Starts the fetch. + void LoadThumbnail(); + + private: + friend class base::RefCountedThreadSafe<ThumbnailLoader>; + ~ThumbnailLoader() { + ResetPaintingObserver(); + } + + void DidReceiveBitmap(const SkBitmap& bitmap) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + ResetPaintingObserver(); + [layer_ setThumbnail:bitmap]; + } + + void ResetPaintingObserver() { + if (rwh_->painting_observer() != NULL) { + DCHECK(rwh_->painting_observer() == + g_browser_process->GetThumbnailGenerator()); + rwh_->set_painting_observer(NULL); + } + } + + gfx::Size size_; + RenderWidgetHost* rwh_; // weak + ThumbnailLayer* layer_; // weak, owns us + base::ScopedCallbackFactory<ThumbnailLoader> factory_; + + DISALLOW_COPY_AND_ASSIGN(ThumbnailLoader); +}; + +void ThumbnailLoader::LoadThumbnail() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + ThumbnailGenerator* generator = g_browser_process->GetThumbnailGenerator(); + if (!generator) // In unit tests. + return; + + // As mentioned in ThumbnailLayer's -drawInContext:, it's sufficient to have + // thumbnails at the zoomed-out pixel size for all but the thumbnail the user + // clicks on in the end. But we don't don't which thumbnail that will be, so + // keep it simple and request full thumbnails for everything. + // TODO(thakis): Request smaller thumbnails for users with many tabs. + gfx::Size page_size(size_); // Logical size the renderer renders at. + gfx::Size pixel_size(size_); // Physical pixel size the image is rendered at. + + DCHECK(rwh_->painting_observer() == NULL || + rwh_->painting_observer() == generator); + rwh_->set_painting_observer(generator); + + // Will send an IPC to the renderer on the IO thread. + generator->AskForSnapshot( + rwh_, + /*prefer_backing_store=*/false, + factory_.NewCallback(&ThumbnailLoader::DidReceiveBitmap), + page_size, + pixel_size); +} + +} // namespace tabpose + +@implementation ThumbnailLayer + +- (id)initWithTabContents:(TabContents*)contents fullSize:(NSSize)fullSize { + CHECK(contents); + if ((self = [super init])) { + contents_ = contents; + fullSize_ = fullSize; + } + return self; +} + +- (void)setTabContents:(TabContents*)contents { + contents_ = contents; +} + +- (void)setThumbnail:(const SkBitmap&)bitmap { + // SkCreateCGImageRef() holds on to |bitmaps|'s memory, so this doesn't + // create a copy. + thumbnail_.reset(SkCreateCGImageRef(bitmap)); + loader_ = NULL; + [self setNeedsDisplay]; +} + +- (int)topOffset { + int topOffset = 0; + + // Medium term, we want to show thumbs of the actual info bar views, which + // means I need to create InfoBarControllers here. At that point, we can get + // the height from that controller. Until then, hardcode. :-/ + const int kInfoBarHeight = 31; + topOffset += contents_->infobar_delegate_count() * kInfoBarHeight; + + bool always_show_bookmark_bar = + contents_->profile()->GetPrefs()->GetBoolean(prefs::kShowBookmarkBar); + bool has_detached_bookmark_bar = + contents_->ShouldShowBookmarkBar() && !always_show_bookmark_bar; + if (has_detached_bookmark_bar) + topOffset += bookmarks::kNTPBookmarkBarHeight; + + return topOffset; +} + +- (int)bottomOffset { + int bottomOffset = 0; + TabContents* devToolsContents = + DevToolsWindow::GetDevToolsContents(contents_); + if (devToolsContents && devToolsContents->render_view_host() && + devToolsContents->render_view_host()->view()) { + // The devtool's size might not be up-to-date, but since its height doesn't + // change on window resize, and since most users don't use devtools, this is + // good enough. + bottomOffset += + devToolsContents->render_view_host()->view()->GetViewBounds().height(); + bottomOffset += 1; // :-( Divider line between web contents and devtools. + } + return bottomOffset; +} + +- (void)drawBackingStore:(BackingStoreMac*)backing_store + inRect:(CGRect)destRect + context:(CGContextRef)context { + // TODO(thakis): Add a sublayer for each accelerated surface in the rwhv. + // Until then, accelerated layers (CoreAnimation NPAPI plugins, compositor) + // won't show up in tabpose. + gfx::ScopedCGContextSaveGState CGContextSaveGState(context); + CGContextSetInterpolationQuality(context, kCGInterpolationHigh); + if (backing_store->cg_layer()) { + CGContextDrawLayerInRect(context, destRect, backing_store->cg_layer()); + } else { + base::mac::ScopedCFTypeRef<CGImageRef> image( + CGBitmapContextCreateImage(backing_store->cg_bitmap())); + CGContextDrawImage(context, destRect, image); + } +} + +- (void)drawInContext:(CGContextRef)context { + RenderWidgetHost* rwh = contents_->render_view_host(); + // NULL if renderer crashed. + RenderWidgetHostView* rwhv = rwh ? rwh->view() : NULL; + if (!rwhv) { + // TODO(thakis): Maybe draw a sad tab layer? + [super drawInContext:context]; + return; + } + + // The size of the TabContent's RenderWidgetHost might not fit to the + // current browser window at all, for example if the window was resized while + // this TabContents object was not an active tab. + // Compute the required size ourselves. Leave room for eventual infobars and + // a detached bookmarks bar on the top, and for the devtools on the bottom. + // Download shelf is not included in the |fullSize| rect, so no need to + // correct for it here. + // TODO(thakis): This is not resolution-independent. + int topOffset = [self topOffset]; + int bottomOffset = [self bottomOffset]; + gfx::Size desiredThumbSize(fullSize_.width, + fullSize_.height - topOffset - bottomOffset); + + // We need to ask the renderer for a thumbnail if + // a) there's no backing store or + // b) the backing store's size doesn't match our required size and + // c) we didn't already send a thumbnail request to the renderer. + BackingStoreMac* backing_store = + (BackingStoreMac*)rwh->GetBackingStore(/*force_create=*/false); + bool draw_backing_store = + backing_store && backing_store->size() == desiredThumbSize; + + // Next weirdness: The destination rect. If the layer is |fullSize_| big, the + // destination rect is (0, bottomOffset), (fullSize_.width, topOffset). But we + // might be amidst an animation, so interpolate that rect. + CGRect destRect = [self bounds]; + CGFloat scale = destRect.size.width / fullSize_.width; + destRect.origin.y += bottomOffset * scale; + destRect.size.height -= (bottomOffset + topOffset) * scale; + + // TODO(thakis): Draw infobars, detached bookmark bar as well. + + // If we haven't already, sent a thumbnail request to the renderer. + if (!draw_backing_store && !didSendLoad_) { + // Either the tab was never visible, or its backing store got evicted, or + // the size of the backing store is wrong. + + // We only need a thumbnail the size of the zoomed-out layer for all + // layers except the one the user clicks on. But since we can't know which + // layer that is, request full-resolution layers for all tabs. This is + // simple and seems to work in practice. + loader_ = new tabpose::ThumbnailLoader(desiredThumbSize, rwh, self); + loader_->LoadThumbnail(); + didSendLoad_ = YES; + + // Fill with bg color. + [super drawInContext:context]; + } + + if (draw_backing_store) { + // Backing store 'cache' hit! + [self drawBackingStore:backing_store inRect:destRect context:context]; + } else if (thumbnail_) { + // No cache hit, but the renderer returned a thumbnail to us. + gfx::ScopedCGContextSaveGState CGContextSaveGState(context); + CGContextSetInterpolationQuality(context, kCGInterpolationHigh); + CGContextDrawImage(context, destRect, thumbnail_.get()); + } +} + +@end + +namespace { + +class ScopedCAActionDisabler { + public: + ScopedCAActionDisabler() { + [CATransaction begin]; + [CATransaction setValue:[NSNumber numberWithBool:YES] + forKey:kCATransactionDisableActions]; + } + + ~ScopedCAActionDisabler() { + [CATransaction commit]; + } +}; + +class ScopedCAActionSetDuration { + public: + explicit ScopedCAActionSetDuration(CGFloat duration) { + [CATransaction begin]; + [CATransaction setValue:[NSNumber numberWithFloat:duration] + forKey:kCATransactionAnimationDuration]; + } + + ~ScopedCAActionSetDuration() { + [CATransaction commit]; + } +}; + +} // namespace + +// Given the number |n| of tiles with a desired aspect ratio of |a| and a +// desired distance |dx|, |dy| between tiles, returns how many tiles fit +// vertically into a rectangle with the dimensions |w_c|, |h_c|. This returns +// an exact solution, which is usually a fractional number. +static float FitNRectsWithAspectIntoBoundingSizeWithConstantPadding( + int n, double a, int w_c, int h_c, int dx, int dy) { + // We want to have the small rects have the same aspect ratio a as a full + // tab. Let w, h be the size of a small rect, and w_c, h_c the size of the + // container. dx, dy are the distances between small rects in x, y direction. + + // Geometry yields: + // w_c = nx * (w + dx) - dx <=> w = (w_c + d_x) / nx - d_x + // h_c = ny * (h + dy) - dy <=> h = (h_c + d_y) / ny - d_t + // Plugging this into + // a := tab_width / tab_height = w / h + // yields + // a = ((w_c - (nx - 1)*d_x)*ny) / (nx*(h_c - (ny - 1)*d_y)) + // Plugging in nx = n/ny and pen and paper (or wolfram alpha: + // 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) + // 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) + // ) gives us nx and ny (but the wrong root -- s/-sqrt(FOO)/sqrt(FOO)/. + + // This function returns ny. + return (sqrt(pow(n * (a * dy - dx), 2) + + 4 * n * a * (dx + w_c) * (dy + h_c)) - + n * (a * dy - dx)) + / + (2 * (dx + w_c)); +} + +namespace tabpose { + +CGFloat ScaleWithOrigin(CGFloat x, CGFloat origin, CGFloat scale) { + return (x - origin) * scale + origin; +} + +NSRect ScaleRectWithOrigin(NSRect r, NSPoint p, CGFloat scale) { + return NSMakeRect(ScaleWithOrigin(NSMinX(r), p.x, scale), + ScaleWithOrigin(NSMinY(r), p.y, scale), + NSWidth(r) * scale, + NSHeight(r) * scale); +} + +// A tile is what is shown for a single tab in tabpose mode. It consists of a +// title, favicon, thumbnail image, and pre- and postanimation rects. +class Tile { + public: + Tile() {} + + // Returns the rectangle this thumbnail is at at the beginning of the zoom-in + // animation. |tile| is the rectangle that's covering the whole tab area when + // the animation starts. + NSRect GetStartRectRelativeTo(const Tile& tile) const; + NSRect thumb_rect() const { return thumb_rect_; } + + NSRect GetFaviconStartRectRelativeTo(const Tile& tile) const; + NSRect favicon_rect() const { return NSIntegralRect(favicon_rect_); } + SkBitmap favicon() const; + + // This changes |title_rect| and |favicon_rect| such that the favicon is on + // the font's baseline and that the minimum distance between thumb rect and + // favicon and title rects doesn't change. + // The view code + // 1. queries desired font size by calling |title_font_size()| + // 2. loads that font + // 3. calls |set_font_metrics()| which updates the title rect + // 4. receives the title rect and puts the title on it with the font from 2. + void set_font_metrics(CGFloat ascender, CGFloat descender); + CGFloat title_font_size() const { return title_font_size_; } + + NSRect GetTitleStartRectRelativeTo(const Tile& tile) const; + NSRect title_rect() const { return NSIntegralRect(title_rect_); } + + // Returns an unelided title. The view logic is responsible for eliding. + const string16& title() const { return contents_->GetTitle(); } + + TabContents* tab_contents() const { return contents_; } + void set_tab_contents(TabContents* new_contents) { contents_ = new_contents; } + + private: + friend class TileSet; + + // The thumb rect includes infobars, detached thumbnail bar, web contents, + // and devtools. + NSRect thumb_rect_; + NSRect start_thumb_rect_; + + NSRect favicon_rect_; + + CGFloat title_font_size_; + NSRect title_rect_; + + TabContents* contents_; // weak + + DISALLOW_COPY_AND_ASSIGN(Tile); +}; + +NSRect Tile::GetStartRectRelativeTo(const Tile& tile) const { + NSRect rect = start_thumb_rect_; + rect.origin.x -= tile.start_thumb_rect_.origin.x; + rect.origin.y -= tile.start_thumb_rect_.origin.y; + return rect; +} + +NSRect Tile::GetFaviconStartRectRelativeTo(const Tile& tile) const { + NSRect thumb_start = GetStartRectRelativeTo(tile); + CGFloat scale_to_start = NSWidth(thumb_start) / NSWidth(thumb_rect_); + NSRect rect = + ScaleRectWithOrigin(favicon_rect_, thumb_rect_.origin, scale_to_start); + rect.origin.x += NSMinX(thumb_start) - NSMinX(thumb_rect_); + rect.origin.y += NSMinY(thumb_start) - NSMinY(thumb_rect_); + return rect; +} + +SkBitmap Tile::favicon() const { + if (contents_->is_app()) { + SkBitmap* icon = contents_->GetExtensionAppIcon(); + if (icon) + return *icon; + } + return contents_->GetFavIcon(); +} + +NSRect Tile::GetTitleStartRectRelativeTo(const Tile& tile) const { + NSRect thumb_start = GetStartRectRelativeTo(tile); + CGFloat scale_to_start = NSWidth(thumb_start) / NSWidth(thumb_rect_); + NSRect rect = + ScaleRectWithOrigin(title_rect_, thumb_rect_.origin, scale_to_start); + rect.origin.x += NSMinX(thumb_start) - NSMinX(thumb_rect_); + rect.origin.y += NSMinY(thumb_start) - NSMinY(thumb_rect_); + return rect; +} + +// Changes |title_rect| and |favicon_rect| such that the favicon's and the +// title's vertical center is aligned and that the minimum distance between +// the thumb rect and favicon and title rects doesn't change. +void Tile::set_font_metrics(CGFloat ascender, CGFloat descender) { + // Make the title height big enough to fit the font, and adopt the title + // position to keep its distance from the thumb rect. + title_rect_.origin.y -= ascender + descender - NSHeight(title_rect_); + title_rect_.size.height = ascender + descender; + + // Align vertical center. Both rects are currently aligned on their top edge. + CGFloat delta_y = NSMidY(title_rect_) - NSMidY(favicon_rect_); + if (delta_y > 0) { + // Title is higher: Move favicon down to align the centers. + favicon_rect_.origin.y += delta_y; + } else { + // Favicon is higher: Move title down to align the centers. + title_rect_.origin.y -= delta_y; + } +} + +// A tileset is responsible for owning and laying out all |Tile|s shown in a +// tabpose window. +class TileSet { + public: + TileSet() {} + + // Fills in |tiles_|. + void Build(TabStripModel* source_model); + + // Computes coordinates for |tiles_|. + void Layout(NSRect containing_rect); + + int selected_index() const { return selected_index_; } + void set_selected_index(int index); + + const Tile& selected_tile() const { return *tiles_[selected_index()]; } + Tile& tile_at(int index) { return *tiles_[index]; } + const Tile& tile_at(int index) const { return *tiles_[index]; } + + // These return which index needs to be selected when the user presses + // up, down, left, or right respectively. + int up_index() const; + int down_index() const; + int left_index() const; + int right_index() const; + + // These return which index needs to be selected on tab / shift-tab. + int next_index() const; + int previous_index() const; + + // Inserts a new Tile object containing |contents| at |index|. Does no + // relayout. + void InsertTileAt(int index, TabContents* contents); + + // Removes the Tile object at |index|. Does no relayout. + void RemoveTileAt(int index); + + // Moves the Tile object at |from_index| to |to_index|. Since this doesn't + // change the number of tiles, relayout can be done just by swapping the + // tile rectangles in the index interval [from_index, to_index], so this does + // layout. + void MoveTileFromTo(int from_index, int to_index); + + private: + int count_x() const { + return ceilf(tiles_.size() / static_cast<float>(count_y_)); + } + int count_y() const { + return count_y_; + } + int last_row_count_x() const { + return tiles_.size() - count_x() * (count_y() - 1); + } + int tiles_in_row(int row) const { + return row != count_y() - 1 ? count_x() : last_row_count_x(); + } + void index_to_tile_xy(int index, int* tile_x, int* tile_y) const { + *tile_x = index % count_x(); + *tile_y = index / count_x(); + } + int tile_xy_to_index(int tile_x, int tile_y) const { + return tile_y * count_x() + tile_x; + } + + ScopedVector<Tile> tiles_; + int selected_index_; + int count_y_; + + DISALLOW_COPY_AND_ASSIGN(TileSet); +}; + +void TileSet::Build(TabStripModel* source_model) { + selected_index_ = source_model->selected_index(); + tiles_.resize(source_model->count()); + for (size_t i = 0; i < tiles_.size(); ++i) { + tiles_[i] = new Tile; + tiles_[i]->contents_ = source_model->GetTabContentsAt(i)->tab_contents(); + } +} + +void TileSet::Layout(NSRect containing_rect) { + int tile_count = tiles_.size(); + if (tile_count == 0) // Happens e.g. during test shutdown. + return; + + // Room around the tiles insde of |containing_rect|. + const int kSmallPaddingTop = 30; + const int kSmallPaddingLeft = 30; + const int kSmallPaddingRight = 30; + const int kSmallPaddingBottom = 30; + + // Favicon / title area. + const int kThumbTitlePaddingY = 6; + const int kFaviconSize = 16; + const int kTitleHeight = 14; // Font size. + const int kTitleExtraHeight = kThumbTitlePaddingY + kTitleHeight; + const int kFaviconExtraHeight = kThumbTitlePaddingY + kFaviconSize; + const int kFaviconTitleDistanceX = 6; + const int kFooterExtraHeight = + std::max(kFaviconExtraHeight, kTitleExtraHeight); + + // Room between the tiles. + const int kSmallPaddingX = 15; + const int kSmallPaddingY = kFooterExtraHeight; + + // Aspect ratio of the containing rect. + CGFloat aspect = NSWidth(containing_rect) / NSHeight(containing_rect); + + // Room left in container after the outer padding is removed. + double container_width = + NSWidth(containing_rect) - kSmallPaddingLeft - kSmallPaddingRight; + double container_height = + NSHeight(containing_rect) - kSmallPaddingTop - kSmallPaddingBottom; + + // The tricky part is figuring out the size of a tab thumbnail, or since the + // size of the containing rect is known, the number of tiles in x and y + // direction. + // Given are the size of the containing rect, and the number of thumbnails + // that need to fit into that rect. The aspect ratio of the thumbnails needs + // to be the same as that of |containing_rect|, else they will look distorted. + // The thumbnails need to be distributed such that + // |count_x * count_y >= tile_count|, and such that wasted space is minimized. + // See the comments in + // |FitNRectsWithAspectIntoBoundingSizeWithConstantPadding()| for a more + // detailed discussion. + // TODO(thakis): It might be good enough to choose |count_x| and |count_y| + // such that count_x / count_y is roughly equal to |aspect|? + double fny = FitNRectsWithAspectIntoBoundingSizeWithConstantPadding( + tile_count, aspect, + container_width, container_height - kFooterExtraHeight, + kSmallPaddingX, kSmallPaddingY + kFooterExtraHeight); + count_y_ = roundf(fny); + + // Now that |count_x()| and |count_y_| are known, it's straightforward to + // compute thumbnail width/height. See comment in + // |FitNRectsWithAspectIntoBoundingSizeWithConstantPadding| for the derivation + // of these two formulas. + int small_width = + floor((container_width + kSmallPaddingX) / static_cast<float>(count_x()) - + kSmallPaddingX); + int small_height = + floor((container_height + kSmallPaddingY) / static_cast<float>(count_y_) - + (kSmallPaddingY + kFooterExtraHeight)); + + // |small_width / small_height| has only roughly an aspect ratio of |aspect|. + // Shrink the thumbnail rect to make the aspect ratio fit exactly, and add + // the extra space won by shrinking to the outer padding. + int smallExtraPaddingLeft = 0; + int smallExtraPaddingTop = 0; + if (aspect > small_width/static_cast<float>(small_height)) { + small_height = small_width / aspect; + CGFloat all_tiles_height = + (small_height + kSmallPaddingY + kFooterExtraHeight) * count_y() - + (kSmallPaddingY + kFooterExtraHeight); + smallExtraPaddingTop = (container_height - all_tiles_height)/2; + } else { + small_width = small_height * aspect; + CGFloat all_tiles_width = + (small_width + kSmallPaddingX) * count_x() - kSmallPaddingX; + smallExtraPaddingLeft = (container_width - all_tiles_width)/2; + } + + // Compute inter-tile padding in the zoomed-out view. + CGFloat scale_small_to_big = + NSWidth(containing_rect) / static_cast<float>(small_width); + CGFloat big_padding_x = kSmallPaddingX * scale_small_to_big; + CGFloat big_padding_y = + (kSmallPaddingY + kFooterExtraHeight) * scale_small_to_big; + + // Now all dimensions are known. Lay out all tiles on a regular grid: + // X X X X + // X X X X + // X X + for (int row = 0, i = 0; i < tile_count; ++row) { + for (int col = 0; col < count_x() && i < tile_count; ++col, ++i) { + // Compute the smalled, zoomed-out thumbnail rect. + tiles_[i]->thumb_rect_.size = NSMakeSize(small_width, small_height); + + int small_x = col * (small_width + kSmallPaddingX) + + kSmallPaddingLeft + smallExtraPaddingLeft; + int small_y = row * (small_height + kSmallPaddingY + kFooterExtraHeight) + + kSmallPaddingTop + smallExtraPaddingTop; + + tiles_[i]->thumb_rect_.origin = NSMakePoint( + small_x, NSHeight(containing_rect) - small_y - small_height); + + tiles_[i]->favicon_rect_.size = NSMakeSize(kFaviconSize, kFaviconSize); + tiles_[i]->favicon_rect_.origin = NSMakePoint( + small_x, + NSHeight(containing_rect) - + (small_y + small_height + kFaviconExtraHeight)); + + // Align lower left corner of title rect with lower left corner of favicon + // for now. The final position is computed later by + // |Tile::set_font_metrics()|. + tiles_[i]->title_font_size_ = kTitleHeight; + tiles_[i]->title_rect_.origin = NSMakePoint( + NSMaxX(tiles_[i]->favicon_rect()) + kFaviconTitleDistanceX, + NSMinY(tiles_[i]->favicon_rect())); + tiles_[i]->title_rect_.size = NSMakeSize( + small_width - + NSWidth(tiles_[i]->favicon_rect()) - kFaviconTitleDistanceX, + kTitleHeight); + + // Compute the big, pre-zoom thumbnail rect. + tiles_[i]->start_thumb_rect_.size = containing_rect.size; + + int big_x = col * (NSWidth(containing_rect) + big_padding_x); + int big_y = row * (NSHeight(containing_rect) + big_padding_y); + tiles_[i]->start_thumb_rect_.origin = NSMakePoint(big_x, -big_y); + } + } + + // Go through last row and center it: + // X X X X + // X X X X + // X X + int last_row_empty_tiles_x = count_x() - last_row_count_x(); + CGFloat small_last_row_shift_x = + last_row_empty_tiles_x * (small_width + kSmallPaddingX) / 2; + CGFloat big_last_row_shift_x = + last_row_empty_tiles_x * (NSWidth(containing_rect) + big_padding_x) / 2; + for (int i = tile_count - last_row_count_x(); i < tile_count; ++i) { + tiles_[i]->thumb_rect_.origin.x += small_last_row_shift_x; + tiles_[i]->start_thumb_rect_.origin.x += big_last_row_shift_x; + tiles_[i]->favicon_rect_.origin.x += small_last_row_shift_x; + tiles_[i]->title_rect_.origin.x += small_last_row_shift_x; + } +} + +void TileSet::set_selected_index(int index) { + CHECK_GE(index, 0); + CHECK_LT(index, static_cast<int>(tiles_.size())); + selected_index_ = index; +} + +// Given a |value| in [0, from_scale), map it into [0, to_scale) such that: +// * [0, from_scale) ends up in the middle of [0, to_scale) if the latter is +// a bigger range +// * The middle of [0, from_scale) is mapped to [0, to_scale), and the parts +// of the former that don't fit are mapped to 0 and to_scale - respectively +// if the former is a bigger range. +static int rescale(int value, int from_scale, int to_scale) { + int left = (to_scale - from_scale) / 2; + int result = value + left; + if (result < 0) + return 0; + if (result >= to_scale) + return to_scale - 1; + return result; +} + +int TileSet::up_index() const { + int tile_x, tile_y; + index_to_tile_xy(selected_index(), &tile_x, &tile_y); + tile_y -= 1; + if (tile_y == count_y() - 2) { + // Transition from last row to second-to-last row. + tile_x = rescale(tile_x, last_row_count_x(), count_x()); + } else if (tile_y < 0) { + // Transition from first row to last row. + tile_x = rescale(tile_x, count_x(), last_row_count_x()); + tile_y = count_y() - 1; + } + return tile_xy_to_index(tile_x, tile_y); +} + +int TileSet::down_index() const { + int tile_x, tile_y; + index_to_tile_xy(selected_index(), &tile_x, &tile_y); + tile_y += 1; + if (tile_y == count_y() - 1) { + // Transition from second-to-last row to last row. + tile_x = rescale(tile_x, count_x(), last_row_count_x()); + } else if (tile_y >= count_y()) { + // Transition from last row to first row. + tile_x = rescale(tile_x, last_row_count_x(), count_x()); + tile_y = 0; + } + return tile_xy_to_index(tile_x, tile_y); +} + +int TileSet::left_index() const { + int tile_x, tile_y; + index_to_tile_xy(selected_index(), &tile_x, &tile_y); + tile_x -= 1; + if (tile_x < 0) + tile_x = tiles_in_row(tile_y) - 1; + return tile_xy_to_index(tile_x, tile_y); +} + +int TileSet::right_index() const { + int tile_x, tile_y; + index_to_tile_xy(selected_index(), &tile_x, &tile_y); + tile_x += 1; + if (tile_x >= tiles_in_row(tile_y)) + tile_x = 0; + return tile_xy_to_index(tile_x, tile_y); +} + +int TileSet::next_index() const { + int new_index = selected_index() + 1; + if (new_index >= static_cast<int>(tiles_.size())) + new_index = 0; + return new_index; +} + +int TileSet::previous_index() const { + int new_index = selected_index() - 1; + if (new_index < 0) + new_index = tiles_.size() - 1; + return new_index; +} + +void TileSet::InsertTileAt(int index, TabContents* contents) { + tiles_.insert(tiles_.begin() + index, new Tile); + tiles_[index]->contents_ = contents; +} + +void TileSet::RemoveTileAt(int index) { + tiles_.erase(tiles_.begin() + index); +} + +// Moves the Tile object at |from_index| to |to_index|. Also updates rectangles +// so that the tiles stay in a left-to-right, top-to-bottom layout when walked +// in sequential order. +void TileSet::MoveTileFromTo(int from_index, int to_index) { + NSRect thumb = tiles_[from_index]->thumb_rect_; + NSRect start_thumb = tiles_[from_index]->start_thumb_rect_; + NSRect favicon = tiles_[from_index]->favicon_rect_; + NSRect title = tiles_[from_index]->title_rect_; + + scoped_ptr<Tile> tile(tiles_[from_index]); + tiles_.weak_erase(tiles_.begin() + from_index); + tiles_.insert(tiles_.begin() + to_index, tile.release()); + + int step = from_index < to_index ? -1 : 1; + for (int i = to_index; (i - from_index) * step < 0; i += step) { + tiles_[i]->thumb_rect_ = tiles_[i + step]->thumb_rect_; + tiles_[i]->start_thumb_rect_ = tiles_[i + step]->start_thumb_rect_; + tiles_[i]->favicon_rect_ = tiles_[i + step]->favicon_rect_; + tiles_[i]->title_rect_ = tiles_[i + step]->title_rect_; + } + tiles_[from_index]->thumb_rect_ = thumb; + tiles_[from_index]->start_thumb_rect_ = start_thumb; + tiles_[from_index]->favicon_rect_ = favicon; + tiles_[from_index]->title_rect_ = title; +} + +} // namespace tabpose + +void AnimateScaledCALayerFrameFromTo( + CALayer* layer, + const NSRect& from, CGFloat from_scale, + const NSRect& to, CGFloat to_scale, + NSTimeInterval duration, id boundsAnimationDelegate) { + // http://developer.apple.com/mac/library/qa/qa2008/qa1620.html + CABasicAnimation* animation; + + animation = [CABasicAnimation animationWithKeyPath:@"bounds"]; + animation.fromValue = [NSValue valueWithRect:from]; + animation.toValue = [NSValue valueWithRect:to]; + animation.duration = duration; + animation.timingFunction = + [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; + animation.delegate = boundsAnimationDelegate; + + // Update the layer's bounds so the layer doesn't snap back when the animation + // completes. + layer.bounds = NSRectToCGRect(to); + + // Add the animation, overriding the implicit animation. + [layer addAnimation:animation forKey:@"bounds"]; + + // Prepare the animation from the current position to the new position. + NSPoint opoint = from.origin; + NSPoint point = to.origin; + + // Adapt to anchorPoint. + opoint.x += NSWidth(from) * from_scale * layer.anchorPoint.x; + opoint.y += NSHeight(from) * from_scale * layer.anchorPoint.y; + point.x += NSWidth(to) * to_scale * layer.anchorPoint.x; + point.y += NSHeight(to) * to_scale * layer.anchorPoint.y; + + animation = [CABasicAnimation animationWithKeyPath:@"position"]; + animation.fromValue = [NSValue valueWithPoint:opoint]; + animation.toValue = [NSValue valueWithPoint:point]; + animation.duration = duration; + animation.timingFunction = + [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; + + // Update the layer's position so that the layer doesn't snap back when the + // animation completes. + layer.position = NSPointToCGPoint(point); + + // Add the animation, overriding the implicit animation. + [layer addAnimation:animation forKey:@"position"]; +} + +void AnimateCALayerFrameFromTo( + CALayer* layer, const NSRect& from, const NSRect& to, + NSTimeInterval duration, id boundsAnimationDelegate) { + AnimateScaledCALayerFrameFromTo( + layer, from, 1.0, to, 1.0, duration, boundsAnimationDelegate); +} + +void AnimateCALayerOpacityFromTo( + CALayer* layer, double from, double to, NSTimeInterval duration) { + CABasicAnimation* animation; + animation = [CABasicAnimation animationWithKeyPath:@"opacity"]; + animation.fromValue = [NSNumber numberWithFloat:from]; + animation.toValue = [NSNumber numberWithFloat:to]; + animation.duration = duration; + + layer.opacity = to; + // Add the animation, overriding the implicit animation. + [layer addAnimation:animation forKey:@"opacity"]; +} + +@interface TabposeWindow (Private) +- (id)initForWindow:(NSWindow*)parent + rect:(NSRect)rect + slomo:(BOOL)slomo + tabStripModel:(TabStripModel*)tabStripModel; +- (void)setUpLayersInSlomo:(BOOL)slomo; +- (void)fadeAway:(BOOL)slomo; +- (void)selectTileAtIndex:(int)newIndex; +@end + +@implementation TabposeWindow + ++ (id)openTabposeFor:(NSWindow*)parent + rect:(NSRect)rect + slomo:(BOOL)slomo + tabStripModel:(TabStripModel*)tabStripModel { + // Releases itself when closed. + return [[TabposeWindow alloc] + initForWindow:parent rect:rect slomo:slomo tabStripModel:tabStripModel]; +} + +- (id)initForWindow:(NSWindow*)parent + rect:(NSRect)rect + slomo:(BOOL)slomo + tabStripModel:(TabStripModel*)tabStripModel { + NSRect frame = [parent frame]; + if ((self = [super initWithContentRect:frame + styleMask:NSBorderlessWindowMask + backing:NSBackingStoreBuffered + defer:NO])) { + containingRect_ = rect; + tabStripModel_ = tabStripModel; + state_ = tabpose::kFadingIn; + tileSet_.reset(new tabpose::TileSet); + tabStripModelObserverBridge_.reset( + new TabStripModelObserverBridge(tabStripModel_, self)); + [self setReleasedWhenClosed:YES]; + [self setOpaque:NO]; + [self setBackgroundColor:[NSColor clearColor]]; + [self setUpLayersInSlomo:slomo]; + [self setAcceptsMouseMovedEvents:YES]; + [parent addChildWindow:self ordered:NSWindowAbove]; + [self makeKeyAndOrderFront:self]; + } + return self; +} + +- (CALayer*)selectedLayer { + return [allThumbnailLayers_ objectAtIndex:tileSet_->selected_index()]; +} + +- (void)selectTileAtIndex:(int)newIndex { + const tabpose::Tile& tile = tileSet_->tile_at(newIndex); + selectionHighlight_.frame = + NSRectToCGRect(NSInsetRect(tile.thumb_rect(), -5, -5)); + tileSet_->set_selected_index(newIndex); +} + +- (void)selectTileAtIndexWithoutAnimation:(int)newIndex { + ScopedCAActionDisabler disabler; + [self selectTileAtIndex:newIndex]; +} + +- (void)addLayersForTile:(tabpose::Tile&)tile + showZoom:(BOOL)showZoom + slomo:(BOOL)slomo + animationDelegate:(id)animationDelegate { + scoped_nsobject<CALayer> layer([[ThumbnailLayer alloc] + initWithTabContents:tile.tab_contents() + fullSize:tile.GetStartRectRelativeTo( + tileSet_->selected_tile()).size]); + [layer setNeedsDisplay]; + + NSTimeInterval interval = + kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1); + + // Background color as placeholder for now. + layer.get().backgroundColor = CGColorGetConstantColor(kCGColorWhite); + if (showZoom) { + AnimateCALayerFrameFromTo( + layer, + tile.GetStartRectRelativeTo(tileSet_->selected_tile()), + tile.thumb_rect(), + interval, + animationDelegate); + } else { + layer.get().frame = NSRectToCGRect(tile.thumb_rect()); + } + + layer.get().shadowRadius = 10; + layer.get().shadowOffset = CGSizeMake(0, -10); + if (state_ == tabpose::kFadedIn) + layer.get().shadowOpacity = 0.5; + + [bgLayer_ addSublayer:layer]; + [allThumbnailLayers_ addObject:layer]; + + // Favicon and title. + NSFont* font = [NSFont systemFontOfSize:tile.title_font_size()]; + tile.set_font_metrics([font ascender], -[font descender]); + + NSImage* nsFavicon = gfx::SkBitmapToNSImage(tile.favicon()); + // Either we don't have a valid favicon or there was some issue converting + // it from an SkBitmap. Either way, just show the default. + if (!nsFavicon) { + NSImage* defaultFavIcon = + ResourceBundle::GetSharedInstance().GetNativeImageNamed( + IDR_DEFAULT_FAVICON); + nsFavicon = defaultFavIcon; + } + base::mac::ScopedCFTypeRef<CGImageRef> favicon( + mac_util::CopyNSImageToCGImage(nsFavicon)); + + CALayer* faviconLayer = [CALayer layer]; + if (showZoom) { + AnimateCALayerFrameFromTo( + faviconLayer, + tile.GetFaviconStartRectRelativeTo(tileSet_->selected_tile()), + tile.favicon_rect(), + interval, + nil); + AnimateCALayerOpacityFromTo(faviconLayer, 0.0, 1.0, interval); + } else { + faviconLayer.frame = NSRectToCGRect(tile.favicon_rect()); + } + faviconLayer.contents = (id)favicon.get(); + faviconLayer.zPosition = 1; // On top of the thumb shadow. + [bgLayer_ addSublayer:faviconLayer]; + [allFaviconLayers_ addObject:faviconLayer]; + + // CATextLayers can't animate their fontSize property, at least on 10.5. + // Animate transform.scale instead. + + // The scaling should have its origin in the layer's upper left corner. + // This needs to be set before |AnimateCALayerFrameFromTo()| is called. + CATextLayer* titleLayer = [CATextLayer layer]; + titleLayer.anchorPoint = CGPointMake(0, 1); + if (showZoom) { + NSRect fromRect = + tile.GetTitleStartRectRelativeTo(tileSet_->selected_tile()); + NSRect toRect = tile.title_rect(); + CGFloat scale = NSWidth(fromRect) / NSWidth(toRect); + fromRect.size = toRect.size; + + // Add scale animation. + CABasicAnimation* scaleAnimation = + [CABasicAnimation animationWithKeyPath:@"transform.scale"]; + scaleAnimation.fromValue = [NSNumber numberWithDouble:scale]; + scaleAnimation.toValue = [NSNumber numberWithDouble:1.0]; + scaleAnimation.duration = interval; + scaleAnimation.timingFunction = + [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; + [titleLayer addAnimation:scaleAnimation forKey:@"transform.scale"]; + + // Add the position and opacity animations. + AnimateScaledCALayerFrameFromTo( + titleLayer, fromRect, scale, toRect, 1.0, interval, nil); + AnimateCALayerOpacityFromTo(faviconLayer, 0.0, 1.0, interval); + } else { + titleLayer.frame = NSRectToCGRect(tile.title_rect()); + } + titleLayer.string = base::SysUTF16ToNSString(tile.title()); + titleLayer.fontSize = [font pointSize]; + titleLayer.truncationMode = kCATruncationEnd; + titleLayer.font = font; + titleLayer.zPosition = 1; // On top of the thumb shadow. + [bgLayer_ addSublayer:titleLayer]; + [allTitleLayers_ addObject:titleLayer]; +} + +- (void)setUpLayersInSlomo:(BOOL)slomo { + // Root layer -- covers whole window. + rootLayer_ = [CALayer layer]; + + // In a block so that the layers don't fade in. + { + ScopedCAActionDisabler disabler; + // Background layer -- the visible part of the window. + gray_.reset(CGColorCreateGenericGray(0.39, 1.0)); + bgLayer_ = [CALayer layer]; + bgLayer_.backgroundColor = gray_; + bgLayer_.frame = NSRectToCGRect(containingRect_); + bgLayer_.masksToBounds = YES; + [rootLayer_ addSublayer:bgLayer_]; + + // Selection highlight layer. + darkBlue_.reset(CGColorCreateGenericRGB(0.25, 0.34, 0.86, 1.0)); + selectionHighlight_ = [CALayer layer]; + selectionHighlight_.backgroundColor = darkBlue_; + selectionHighlight_.cornerRadius = 5.0; + selectionHighlight_.zPosition = -1; // Behind other layers. + selectionHighlight_.hidden = YES; + [bgLayer_ addSublayer:selectionHighlight_]; + + // Top gradient. + CALayer* gradientLayer = [DarkGradientLayer layer]; + gradientLayer.frame = CGRectMake( + 0, + NSHeight(containingRect_) - kTopGradientHeight, + NSWidth(containingRect_), + kTopGradientHeight); + [gradientLayer setNeedsDisplay]; // Draw once. + [bgLayer_ addSublayer:gradientLayer]; + } + + // Layers for the tab thumbnails. + tileSet_->Build(tabStripModel_); + tileSet_->Layout(containingRect_); + allThumbnailLayers_.reset( + [[NSMutableArray alloc] initWithCapacity:tabStripModel_->count()]); + allFaviconLayers_.reset( + [[NSMutableArray alloc] initWithCapacity:tabStripModel_->count()]); + allTitleLayers_.reset( + [[NSMutableArray alloc] initWithCapacity:tabStripModel_->count()]); + + for (int i = 0; i < tabStripModel_->count(); ++i) { + // Add a delegate to one of the animations to get a notification once the + // animations are done. + [self addLayersForTile:tileSet_->tile_at(i) + showZoom:YES + slomo:slomo + animationDelegate:i == tileSet_->selected_index() ? self : nil]; + if (i == tileSet_->selected_index()) { + CALayer* layer = [allThumbnailLayers_ objectAtIndex:i]; + CAAnimation* animation = [layer animationForKey:@"bounds"]; + DCHECK(animation); + [animation setValue:kAnimationIdFadeIn forKey:kAnimationIdKey]; + } + } + [self selectTileAtIndexWithoutAnimation:tileSet_->selected_index()]; + + // Needs to happen after all layers have been added to |rootLayer_|, else + // there's a one frame flash of grey at the beginning of the animation + // (|bgLayer_| showing through with none of its children visible yet). + [[self contentView] setLayer:rootLayer_]; + [[self contentView] setWantsLayer:YES]; +} + +- (BOOL)canBecomeKeyWindow { + return YES; +} + +// Handle key events that should be executed repeatedly while the key is down. +- (void)keyDown:(NSEvent*)event { + if (state_ == tabpose::kFadingOut) + return; + NSString* characters = [event characters]; + if ([characters length] < 1) + return; + + unichar character = [characters characterAtIndex:0]; + int newIndex = -1; + switch (character) { + case NSUpArrowFunctionKey: + newIndex = tileSet_->up_index(); + break; + case NSDownArrowFunctionKey: + newIndex = tileSet_->down_index(); + break; + case NSLeftArrowFunctionKey: + newIndex = tileSet_->left_index(); + break; + case NSRightArrowFunctionKey: + newIndex = tileSet_->right_index(); + break; + case NSTabCharacter: + newIndex = tileSet_->next_index(); + break; + case NSBackTabCharacter: + newIndex = tileSet_->previous_index(); + break; + } + if (newIndex != -1) + [self selectTileAtIndexWithoutAnimation:newIndex]; +} + +// Handle keyboard events that should be executed once when the key is released. +- (void)keyUp:(NSEvent*)event { + if (state_ == tabpose::kFadingOut) + return; + NSString* characters = [event characters]; + if ([characters length] < 1) + return; + + unichar character = [characters characterAtIndex:0]; + switch (character) { + case NSEnterCharacter: + case NSNewlineCharacter: + case NSCarriageReturnCharacter: + case ' ': + [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0]; + break; + case '\e': // Escape + tileSet_->set_selected_index(tabStripModel_->selected_index()); + [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0]; + break; + } +} + +// Handle keyboard events that contain cmd or ctrl. +- (BOOL)performKeyEquivalent:(NSEvent*)event { + if (state_ == tabpose::kFadingOut) + return NO; + NSString* characters = [event characters]; + if ([characters length] < 1) + return NO; + unichar character = [characters characterAtIndex:0]; + if ([event modifierFlags] & NSCommandKeyMask) { + if (character >= '1' && character <= '9') { + int index = + character == '9' ? tabStripModel_->count() - 1 : character - '1'; + if (index < tabStripModel_->count()) { + tileSet_->set_selected_index(index); + [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0]; + return YES; + } + } + } + return NO; +} + +-(void)selectTileFromMouseEvent:(NSEvent*)event { + int newIndex = -1; + CGPoint p = NSPointToCGPoint([event locationInWindow]); + for (NSUInteger i = 0; i < [allThumbnailLayers_ count]; ++i) { + CALayer* layer = [allThumbnailLayers_ objectAtIndex:i]; + CGPoint lp = [layer convertPoint:p fromLayer:rootLayer_]; + if ([static_cast<CALayer*>([layer presentationLayer]) containsPoint:lp]) + newIndex = i; + } + if (newIndex >= 0) + [self selectTileAtIndexWithoutAnimation:newIndex]; +} + +- (void)mouseMoved:(NSEvent*)event { + [self selectTileFromMouseEvent:event]; +} + +- (void)mouseDown:(NSEvent*)event { + // Just in case the user clicked without ever moving the mouse. + [self selectTileFromMouseEvent:event]; + + [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0]; +} + +- (void)swipeWithEvent:(NSEvent*)event { + if (abs([event deltaY]) > 0.5) // Swipe up or down. + [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0]; +} + +- (void)close { + // Prevent parent window from disappearing. + [[self parentWindow] removeChildWindow:self]; + + // We're dealloc'd in an autorelease pool – by then the observer registry + // might be dead, so explicitly reset the observer now. + tabStripModelObserverBridge_.reset(); + + [super close]; +} + +- (void)commandDispatch:(id)sender { + if ([sender tag] == IDC_TABPOSE) + [self fadeAway:NO]; +} + +- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item { + // Disable all browser-related menu items except the tab overview toggle. + SEL action = [item action]; + NSInteger tag = [item tag]; + return action == @selector(commandDispatch:) && tag == IDC_TABPOSE; +} + +- (void)fadeAwayTileAtIndex:(int)index { + const tabpose::Tile& tile = tileSet_->tile_at(index); + CALayer* layer = [allThumbnailLayers_ objectAtIndex:index]; + // Add a delegate to one of the implicit animations to get a notification + // once the animations are done. + if (static_cast<int>(index) == tileSet_->selected_index()) { + CAAnimation* animation = [CAAnimation animation]; + animation.delegate = self; + [animation setValue:kAnimationIdFadeOut forKey:kAnimationIdKey]; + [layer addAnimation:animation forKey:@"frame"]; + } + + // Thumbnail. + layer.frame = NSRectToCGRect( + tile.GetStartRectRelativeTo(tileSet_->selected_tile())); + + if (static_cast<int>(index) == tileSet_->selected_index()) { + // Redraw layer at big resolution, so that zoom-in isn't blocky. + [layer setNeedsDisplay]; + } + + // Title. + CALayer* faviconLayer = [allFaviconLayers_ objectAtIndex:index]; + faviconLayer.frame = NSRectToCGRect( + tile.GetFaviconStartRectRelativeTo(tileSet_->selected_tile())); + faviconLayer.opacity = 0; + + // Favicon. + // The |fontSize| cannot be animated directly, animate the layer's scale + // instead. |transform.scale| affects the rendered width, so keep the small + // bounds. + CALayer* titleLayer = [allTitleLayers_ objectAtIndex:index]; + NSRect titleRect = tile.title_rect(); + NSRect titleToRect = + tile.GetTitleStartRectRelativeTo(tileSet_->selected_tile()); + CGFloat scale = NSWidth(titleToRect) / NSWidth(titleRect); + titleToRect.origin.x += + NSWidth(titleRect) * scale * titleLayer.anchorPoint.x; + titleToRect.origin.y += + NSHeight(titleRect) * scale * titleLayer.anchorPoint.y; + titleLayer.position = NSPointToCGPoint(titleToRect.origin); + [titleLayer setValue:[NSNumber numberWithDouble:scale] + forKeyPath:@"transform.scale"]; + titleLayer.opacity = 0; +} + +- (void)fadeAway:(BOOL)slomo { + if (state_ == tabpose::kFadingOut) + return; + + state_ = tabpose::kFadingOut; + [self setAcceptsMouseMovedEvents:NO]; + + // Select chosen tab. + if (tileSet_->selected_index() < tabStripModel_->count()) { + tabStripModel_->SelectTabContentsAt(tileSet_->selected_index(), + /*user_gesture=*/true); + } else { + DCHECK_EQ(tileSet_->selected_index(), 0); + } + + { + ScopedCAActionDisabler disableCAActions; + + // Move the selected layer on top of all other layers. + [self selectedLayer].zPosition = 1; + + selectionHighlight_.hidden = YES; + // Running animations with shadows is slow, so turn shadows off before + // running the exit animation. + for (CALayer* layer in allThumbnailLayers_.get()) + layer.shadowOpacity = 0.0; + } + + // Animate layers out, all in one transaction. + CGFloat duration = + 1.3 * kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1); + ScopedCAActionSetDuration durationSetter(duration); + for (int i = 0; i < tabStripModel_->count(); ++i) + [self fadeAwayTileAtIndex:i]; +} + +- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished { + NSString* animationId = [animation valueForKey:kAnimationIdKey]; + if ([animationId isEqualToString:kAnimationIdFadeIn]) { + if (finished && state_ == tabpose::kFadingIn) { + // If the user clicks while the fade in animation is still running, + // |state_| is already kFadingOut. In that case, don't do anything. + state_ = tabpose::kFadedIn; + + selectionHighlight_.hidden = NO; + + // Running animations with shadows is slow, so turn shadows on only after + // the animation is done. + ScopedCAActionDisabler disableCAActions; + for (CALayer* layer in allThumbnailLayers_.get()) + layer.shadowOpacity = 0.5; + } + } else if ([animationId isEqualToString:kAnimationIdFadeOut]) { + DCHECK_EQ(tabpose::kFadingOut, state_); + [self close]; + } +} + +- (NSUInteger)thumbnailLayerCount { + return [allThumbnailLayers_ count]; +} + +- (int)selectedIndex { + return tileSet_->selected_index(); +} + +#pragma mark TabStripModelBridge + +- (void)refreshLayerFramesAtIndex:(int)i { + const tabpose::Tile& tile = tileSet_->tile_at(i); + + CALayer* faviconLayer = [allFaviconLayers_ objectAtIndex:i]; + faviconLayer.frame = NSRectToCGRect(tile.favicon_rect()); + CALayer* titleLayer = [allTitleLayers_ objectAtIndex:i]; + titleLayer.frame = NSRectToCGRect(tile.title_rect()); + CALayer* thumbLayer = [allThumbnailLayers_ objectAtIndex:i]; + thumbLayer.frame = NSRectToCGRect(tile.thumb_rect()); +} + +- (void)insertTabWithContents:(TabContentsWrapper*)contents + atIndex:(NSInteger)index + inForeground:(bool)inForeground { + // This happens if you cmd-click a link and then immediately open tabpose + // on a slowish machine. + ScopedCAActionSetDuration durationSetter(kObserverChangeAnimationDuration); + + // Insert new layer and relayout. + tileSet_->InsertTileAt(index, contents->tab_contents()); + tileSet_->Layout(containingRect_); + [self addLayersForTile:tileSet_->tile_at(index) + showZoom:NO + slomo:NO + animationDelegate:nil]; + + // Update old layers. + DCHECK_EQ(tabStripModel_->count(), + static_cast<int>([allThumbnailLayers_ count])); + DCHECK_EQ(tabStripModel_->count(), + static_cast<int>([allTitleLayers_ count])); + DCHECK_EQ(tabStripModel_->count(), + static_cast<int>([allFaviconLayers_ count])); + + for (int i = 0; i < tabStripModel_->count(); ++i) { + if (i == index) // The new layer. + continue; + [self refreshLayerFramesAtIndex:i]; + } + + // Update selection. + int selectedIndex = tileSet_->selected_index(); + if (selectedIndex >= index) + selectedIndex++; + [self selectTileAtIndex:selectedIndex]; +} + +- (void)tabClosingWithContents:(TabContentsWrapper*)contents + atIndex:(NSInteger)index { + // We will also get a -tabDetachedWithContents:atIndex: notification for + // closing tabs, so do nothing here. +} + +- (void)tabDetachedWithContents:(TabContentsWrapper*)contents + atIndex:(NSInteger)index { + ScopedCAActionSetDuration durationSetter(kObserverChangeAnimationDuration); + + // Remove layer and relayout. + tileSet_->RemoveTileAt(index); + tileSet_->Layout(containingRect_); + + [[allThumbnailLayers_ objectAtIndex:index] removeFromSuperlayer]; + [allThumbnailLayers_ removeObjectAtIndex:index]; + [[allTitleLayers_ objectAtIndex:index] removeFromSuperlayer]; + [allTitleLayers_ removeObjectAtIndex:index]; + [[allFaviconLayers_ objectAtIndex:index] removeFromSuperlayer]; + [allFaviconLayers_ removeObjectAtIndex:index]; + + // Update old layers. + DCHECK_EQ(tabStripModel_->count(), + static_cast<int>([allThumbnailLayers_ count])); + DCHECK_EQ(tabStripModel_->count(), + static_cast<int>([allTitleLayers_ count])); + DCHECK_EQ(tabStripModel_->count(), + static_cast<int>([allFaviconLayers_ count])); + + if (tabStripModel_->count() == 0) + [self close]; + + for (int i = 0; i < tabStripModel_->count(); ++i) + [self refreshLayerFramesAtIndex:i]; + + // Update selection. + int selectedIndex = tileSet_->selected_index(); + if (selectedIndex >= index) + selectedIndex--; + if (selectedIndex >= 0) + [self selectTileAtIndex:selectedIndex]; +} + +- (void)tabMovedWithContents:(TabContentsWrapper*)contents + fromIndex:(NSInteger)from + toIndex:(NSInteger)to { + ScopedCAActionSetDuration durationSetter(kObserverChangeAnimationDuration); + + // Move tile from |from| to |to|. + tileSet_->MoveTileFromTo(from, to); + + // Move corresponding layers from |from| to |to|. + scoped_nsobject<CALayer> thumbLayer( + [[allThumbnailLayers_ objectAtIndex:from] retain]); + [allThumbnailLayers_ removeObjectAtIndex:from]; + [allThumbnailLayers_ insertObject:thumbLayer.get() atIndex:to]; + scoped_nsobject<CALayer> faviconLayer( + [[allFaviconLayers_ objectAtIndex:from] retain]); + [allFaviconLayers_ removeObjectAtIndex:from]; + [allFaviconLayers_ insertObject:faviconLayer.get() atIndex:to]; + scoped_nsobject<CALayer> titleLayer( + [[allTitleLayers_ objectAtIndex:from] retain]); + [allTitleLayers_ removeObjectAtIndex:from]; + [allTitleLayers_ insertObject:titleLayer.get() atIndex:to]; + + // Update frames of the layers. + for (int i = std::min(from, to); i <= std::max(from, to); ++i) + [self refreshLayerFramesAtIndex:i]; + + // Update selection. + int selectedIndex = tileSet_->selected_index(); + if (from == selectedIndex) + selectedIndex = to; + else if (from < selectedIndex && selectedIndex <= to) + selectedIndex--; + else if (to <= selectedIndex && selectedIndex < from) + selectedIndex++; + [self selectTileAtIndex:selectedIndex]; +} + +- (void)tabChangedWithContents:(TabContentsWrapper*)contents + atIndex:(NSInteger)index + changeType:(TabStripModelObserver::TabChangeType)change { + // Tell the window to update text, title, and thumb layers at |index| to get + // their data from |contents|. |contents| can be different from the old + // contents at that index! + // While a tab is loading, this is unfortunately called quite often for + // both the "loading" and the "all" change types, so we don't really want to + // send thumb requests to the corresponding renderer when this is called. + // For now, just make sure that we don't hold on to an invalid TabContents + // object. + tabpose::Tile& tile = tileSet_->tile_at(index); + if (contents->tab_contents() == tile.tab_contents()) { + // TODO(thakis): Install a timer to send a thumb request/update title/update + // favicon after 20ms or so, and reset the timer every time this is called + // to make sure we get an updated thumb, without requesting them all over. + return; + } + + tile.set_tab_contents(contents->tab_contents()); + ThumbnailLayer* thumbLayer = [allThumbnailLayers_ objectAtIndex:index]; + [thumbLayer setTabContents:contents->tab_contents()]; +} + +- (void)tabStripModelDeleted { + [self close]; +} + +@end |