blob: 16d8ed22835648e66b14c0ab55ec49ea012efc55 [file] [log] [blame]
// Copyright (c) 2012 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 <algorithm>
#include "base/mac/mac_util.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/memory/weak_ptr.h"
#include "base/sys_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/debugger/devtools_window.h"
#include "chrome/browser/extensions/extension_tab_helper.h"
#include "chrome/browser/prefs/pref_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/tab_contents/thumbnail_generator.h"
#include "chrome/browser/ui/bookmarks/bookmark_tab_helper.h"
#include "chrome/browser/ui/cocoa/animation_utils.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/infobars/infobar_container_controller.h"
#import "chrome/browser/ui/cocoa/tab_contents/favicon_util.h"
#import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h"
#import "chrome/browser/ui/cocoa/tabs/tab_strip_model_observer_bridge.h"
#include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h"
#include "chrome/common/pref_names.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "grit/theme_resources.h"
#include "grit/ui_resources.h"
#include "skia/ext/skia_utils_mac.h"
#include "third_party/skia/include/utils/mac/SkCGUtils.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/scoped_cg_context_save_gstate_mac.h"
using content::BrowserThread;
using content::RenderWidgetHost;
// Height of the bottom gradient, in pixels.
const CGFloat kBottomGradientHeight = 50;
// The shade of gray at the top of the window. There's a gradient from
// this to |kCentralGray| at the top of the window.
const CGFloat kTopGray = 0.77;
// The shade of gray at the center of the window. Most of the window background
// has this color.
const CGFloat kCentralGray = 0.6;
// The shade of gray at the bottom of the window. There's a gradient from
// |kCentralGray| to this at the bottom of the window, |kBottomGradientHeight|
// high.
const CGFloat kBottomGray = 0.5;
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.25; // In seconds.
const CGFloat kSelectionInset = 5;
// CAGradientLayer is 10.6-only -- roll our own.
@interface GrayGradientLayer : CALayer {
@private
CGFloat startGray_;
CGFloat endGray_;
}
- (id)initWithStartGray:(CGFloat)startGray endGray:(CGFloat)endGray;
- (void)drawInContext:(CGContextRef)context;
@end
@implementation GrayGradientLayer
- (id)initWithStartGray:(CGFloat)startGray endGray:(CGFloat)endGray {
if ((self = [super init])) {
startGray_ = startGray;
endGray_ = endGray;
}
return self;
}
- (void)drawInContext:(CGContextRef)context {
base::mac::ScopedCFTypeRef<CGColorSpaceRef> grayColorSpace(
CGColorSpaceCreateWithName(kCGColorSpaceGenericGray));
CGFloat grays[] = { startGray_, 1.0, endGray_, 1.0 };
CGFloat locations[] = { 0, 1 };
base::mac::ScopedCFTypeRef<CGGradientRef> gradient(
CGGradientCreateWithColorComponents(
grayColorSpace.get(), grays, locations, arraysize(locations)));
CGPoint topLeft = CGPointMake(0.0, self.bounds.size.height);
CGContextDrawLinearGradient(context, gradient.get(), topLeft, CGPointZero, 0);
}
@end
namespace tabpose {
class ThumbnailLoader;
}
// A CALayer that draws a thumbnail for a TabContentsWrapper object. The layer
// tries to draw the WebContents's backing store directly if possible, and
// requests a thumbnail bitmap from the WebContents's renderer process if not.
@interface ThumbnailLayer : CALayer {
// The TabContentsWrapper the thumbnail is for.
TabContentsWrapper* 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:(TabContentsWrapper*)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), weak_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() {
g_browser_process->GetThumbnailGenerator()->MonitorRenderer(rwh_, false);
}
gfx::Size size_;
RenderWidgetHost* rwh_; // weak
ThumbnailLayer* layer_; // weak, owns us
base::WeakPtrFactory<ThumbnailLoader> weak_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.
generator->MonitorRenderer(rwh_, true);
// Will send an IPC to the renderer on the IO thread.
generator->AskForSnapshot(
rwh_,
base::Bind(&ThumbnailLoader::DidReceiveBitmap,
weak_factory_.GetWeakPtr()),
page_size,
pixel_size);
}
} // namespace tabpose
@implementation ThumbnailLayer
- (id)initWithTabContents:(TabContentsWrapper*)contents
fullSize:(NSSize)fullSize {
CHECK(contents);
if ((self = [super init])) {
contents_ = contents;
fullSize_ = fullSize;
}
return self;
}
- (void)setTabContents:(TabContentsWrapper*)contents {
contents_ = contents;
}
- (void)setThumbnail:(const SkBitmap&)bitmap {
// SkCreateCGImageRef() holds on to |bitmaps|'s memory, so this doesn't
// create a copy. The renderer always draws data in the system colorspace.
thumbnail_.reset(SkCreateCGImageRefWithColorspace(
bitmap, base::mac::GetSystemColorSpace()));
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.
NSWindow* window = [contents_->web_contents()->GetNativeView() window];
NSWindowController* windowController = [window windowController];
if ([windowController isKindOfClass:[BrowserWindowController class]]) {
BrowserWindowController* bwc =
static_cast<BrowserWindowController*>(windowController);
InfoBarContainerController* infoBarContainer =
[bwc infoBarContainerController];
// TODO(thakis|rsesek): This is not correct for background tabs with
// infobars as the aspect ratio will be wrong. Fix that.
topOffset += NSHeight([[infoBarContainer view] frame]) -
[infoBarContainer overlappingTipHeight];
}
bool always_show_bookmark_bar =
contents_->profile()->GetPrefs()->GetBoolean(prefs::kShowBookmarkBar);
bool has_detached_bookmark_bar =
contents_->bookmark_tab_helper()->ShouldShowBookmarkBar() &&
!always_show_bookmark_bar;
if (has_detached_bookmark_bar)
topOffset += bookmarks::kNTPBookmarkBarHeight;
return topOffset;
}
- (int)bottomOffset {
int bottomOffset = 0;
TabContentsWrapper* devToolsContents =
DevToolsWindow::GetDevToolsContents(contents_->web_contents());
if (devToolsContents && devToolsContents->web_contents() &&
devToolsContents->web_contents()->GetRenderViewHost() &&
devToolsContents->web_contents()->GetRenderViewHost()->GetView()) {
// 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->web_contents()->GetRenderViewHost()->GetView()->
GetViewBounds().height();
bottomOffset += 1; // :-( Divider line between web contents and devtools.
}
return bottomOffset;
}
- (void)drawInContext:(CGContextRef)context {
RenderWidgetHost* rwh = contents_->web_contents()->GetRenderViewHost();
// NULL if renderer crashed.
content::RenderWidgetHostView* rwhv = rwh ? rwh->GetView() : NULL;
if (!rwhv) {
// TODO(thakis): Maybe draw a sad tab layer?
[super drawInContext:context];
return;
}
// The size of the WebContents's RenderWidgetHost might not fit to the
// current browser window at all, for example if the window was resized while
// this WebContents 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.
bool draw_backing_store = rwh->GetBackingStoreSize() == 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!
// 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.
rwh->CopyFromBackingStoreToCGContext(destRect, 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
// 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_); }
NSImage* 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_->web_contents()->GetTitle();
}
TabContentsWrapper* tab_contents() const { return contents_; }
void set_tab_contents(TabContentsWrapper* 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_;
TabContentsWrapper* 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;
}
NSImage* Tile::favicon() const {
if (contents_->extension_tab_helper()->is_app()) {
SkBitmap* bitmap = contents_->extension_tab_helper()->GetExtensionAppIcon();
if (bitmap)
return gfx::SkBitmapToNSImage(*bitmap);
}
return mac::FaviconForTabContents(contents_);
}
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, TabContentsWrapper* 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->active_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);
}
}
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);
}
}
}
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, TabContentsWrapper* 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;
// Creates and initializes the CALayer in the background and all the CALayers
// for the thumbnails, favicons, and titles.
- (void)setUpLayersInSlomo:(BOOL)slomo;
// Tells the browser to make the tab corresponding to currently selected
// thumbnail the current tab and starts the tabpose exit animmation.
- (void)fadeAwayInSlomo:(BOOL)slomo;
// Returns the CALayer for the close button belonging to the thumbnail at
// index |index|.
- (CALayer*)closebuttonLayerAtIndex:(NSUInteger)index;
// Updates the visibility of all closebutton layers.
- (void)updateClosebuttonLayersVisibility;
@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));
NSImage* nsCloseIcon =
ResourceBundle::GetSharedInstance().GetNativeImageNamed(
IDR_TABPOSE_CLOSE);
closeIcon_.reset(base::mac::CopyNSImageToCGImage(nsCloseIcon));
[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)selectTileAtIndexWithoutAnimation:(int)newIndex {
ScopedCAActionDisabler disabler;
const tabpose::Tile& tile = tileSet_->tile_at(newIndex);
selectionHighlight_.frame =
NSRectToCGRect(NSInsetRect(tile.thumb_rect(),
-kSelectionInset, -kSelectionInset));
tileSet_->set_selected_index(newIndex);
[self updateClosebuttonLayersVisibility];
}
- (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;
// Add a close button to the thumb layer.
CALayer* closeLayer = [CALayer layer];
closeLayer.contents = reinterpret_cast<id>(closeIcon_.get());
CGRect closeBounds = {};
closeBounds.size.width = CGImageGetWidth(closeIcon_);
closeBounds.size.height = CGImageGetHeight(closeIcon_);
closeLayer.bounds = closeBounds;
closeLayer.hidden = YES;
[closeLayer addConstraint:
[CAConstraint constraintWithAttribute:kCAConstraintMidX
relativeTo:@"superlayer"
attribute:kCAConstraintMinX]];
[closeLayer addConstraint:
[CAConstraint constraintWithAttribute:kCAConstraintMidY
relativeTo:@"superlayer"
attribute:kCAConstraintMaxY]];
layer.get().layoutManager = [CAConstraintLayoutManager layoutManager];
[layer.get() addSublayer:closeLayer];
[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]);
base::mac::ScopedCFTypeRef<CGImageRef> favicon(
base::mac::CopyNSImageToCGImage(tile.favicon()));
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(kCentralGray, 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_];
// Bottom gradient.
CALayer* gradientLayer = [[[GrayGradientLayer alloc]
initWithStartGray:kCentralGray endGray:kBottomGray] autorelease];
gradientLayer.frame = CGRectMake(
0,
0,
NSWidth(containingRect_),
kBottomGradientHeight);
[gradientLayer setNeedsDisplay]; // Draw once.
[bgLayer_ addSublayer:gradientLayer];
}
// Top gradient (fades in).
CGFloat toolbarHeight = NSHeight([self frame]) - NSHeight(containingRect_);
topGradient_ = [[[GrayGradientLayer alloc]
initWithStartGray:kTopGray endGray:kCentralGray] autorelease];
topGradient_.frame = CGRectMake(
0,
NSHeight([self frame]) - toolbarHeight,
NSWidth(containingRect_),
toolbarHeight);
[topGradient_ setNeedsDisplay]; // Draw once.
[rootLayer_ addSublayer:topGradient_];
NSTimeInterval interval =
kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1);
AnimateCALayerOpacityFromTo(topGradient_, 0, 1, interval);
// 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;
}
// Lets the traffic light buttons on the browser window keep their "active"
// state while an info bubble is open. Only has an effect on 10.7.
- (BOOL)_sharesParentKeyState {
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 fadeAwayInSlomo:([event modifierFlags] & NSShiftKeyMask) != 0];
break;
case '\e': // Escape
tileSet_->set_selected_index(tabStripModel_->active_index());
[self fadeAwayInSlomo:([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 fadeAwayInSlomo:([event modifierFlags] & NSShiftKeyMask) != 0];
return YES;
}
}
}
return NO;
}
- (void)flagsChanged:(NSEvent*)event {
showAllCloseLayers_ = ([event modifierFlags] & NSAlternateKeyMask) != 0;
[self updateClosebuttonLayersVisibility];
}
- (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];
}
- (CALayer*)closebuttonLayerAtIndex:(NSUInteger)index {
CALayer* layer = [allThumbnailLayers_ objectAtIndex:index];
return [[layer sublayers] objectAtIndex:0];
}
- (void)updateClosebuttonLayersVisibility {
for (NSUInteger i = 0; i < [allThumbnailLayers_ count]; ++i) {
CALayer* layer = [self closebuttonLayerAtIndex:i];
BOOL isSelectedTile = static_cast<int>(i) == tileSet_->selected_index();
BOOL isVisible = state_ == tabpose::kFadedIn &&
(isSelectedTile || showAllCloseLayers_);
layer.hidden = !isVisible;
}
}
- (void)mouseDown:(NSEvent*)event {
// Just in case the user clicked without ever moving the mouse.
[self selectTileFromMouseEvent:event];
// If the click occurred in a close box, close that tab and don't do anything
// else.
CGPoint p = NSPointToCGPoint([event locationInWindow]);
for (NSUInteger i = 0; i < [allThumbnailLayers_ count]; ++i) {
CALayer* layer = [self closebuttonLayerAtIndex:i];
CGPoint lp = [layer convertPoint:p fromLayer:rootLayer_];
if ([static_cast<CALayer*>([layer presentationLayer]) containsPoint:lp] &&
!layer.hidden) {
tabStripModel_->CloseTabContentsAt(i,
TabStripModel::CLOSE_USER_GESTURE |
TabStripModel::CLOSE_CREATE_HISTORICAL_TAB);
return;
}
}
[self fadeAwayInSlomo:([event modifierFlags] & NSShiftKeyMask) != 0];
}
- (void)swipeWithEvent:(NSEvent*)event {
if (abs([event deltaY]) > 0.5) // Swipe up or down.
[self fadeAwayInSlomo:([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 fadeAwayInSlomo: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)fadeAwayInSlomo:(BOOL)slomo {
if (state_ == tabpose::kFadingOut)
return;
state_ = tabpose::kFadingOut;
[self setAcceptsMouseMovedEvents:NO];
// Select chosen tab.
if (tileSet_->selected_index() < tabStripModel_->count()) {
tabStripModel_->ActivateTabAt(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;
[self updateClosebuttonLayersVisibility];
}
// 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];
AnimateCALayerOpacityFromTo(topGradient_, 1, 0, duration);
}
- (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;
[self updateClosebuttonLayersVisibility];
}
} 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* thumbLayer = [allThumbnailLayers_ objectAtIndex:i];
if (i == tileSet_->selected_index()) {
AnimateCALayerFrameFromTo(
selectionHighlight_,
NSInsetRect(NSRectFromCGRect(thumbLayer.frame),
-kSelectionInset, -kSelectionInset),
NSInsetRect(tile.thumb_rect(),
-kSelectionInset, -kSelectionInset),
kObserverChangeAnimationDuration,
nil);
}
// Repaint layer if necessary.
if (!NSEqualSizes(NSRectFromCGRect(thumbLayer.frame).size,
tile.thumb_rect().size)) {
[thumbLayer setNeedsDisplay];
}
// Use AnimateCALayerFrameFromTo() instead of just setting |frame| to let
// the animation match the selection animation --
// |kCAMediaTimingFunctionDefault| is 10.6-only.
AnimateCALayerFrameFromTo(
thumbLayer,
NSRectFromCGRect(thumbLayer.frame),
tile.thumb_rect(),
kObserverChangeAnimationDuration,
nil);
CALayer* faviconLayer = [allFaviconLayers_ objectAtIndex:i];
AnimateCALayerFrameFromTo(
faviconLayer,
NSRectFromCGRect(faviconLayer.frame),
tile.favicon_rect(),
kObserverChangeAnimationDuration,
nil);
CALayer* titleLayer = [allTitleLayers_ objectAtIndex:i];
AnimateCALayerFrameFromTo(
titleLayer,
NSRectFromCGRect(titleLayer.frame),
tile.title_rect(),
kObserverChangeAnimationDuration,
nil);
}
- (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);
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]));
// Update selection.
int selectedIndex = tileSet_->selected_index();
if (selectedIndex >= index)
selectedIndex++;
[self selectTileAtIndexWithoutAnimation:selectedIndex];
// Animate everything into its new place.
for (int i = 0; i < tabStripModel_->count(); ++i) {
if (i == index) // The new layer.
continue;
[self refreshLayerFramesAtIndex:i];
}
}
- (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_);
{
ScopedCAActionDisabler disabler;
[[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];
// Update selection.
int selectedIndex = tileSet_->selected_index();
if (selectedIndex > index || selectedIndex >= tabStripModel_->count())
selectedIndex--;
if (selectedIndex >= 0)
[self selectTileAtIndexWithoutAnimation:selectedIndex];
// Animate everything into its new place.
for (int i = 0; i < tabStripModel_->count(); ++i)
[self refreshLayerFramesAtIndex:i];
}
- (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 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 selectTileAtIndexWithoutAnimation:selectedIndex];
// Update frames of the layers.
for (int i = std::min(from, to); i <= std::max(from, to); ++i)
[self refreshLayerFramesAtIndex:i];
}
- (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 WebContents
// object.
tabpose::Tile& tile = tileSet_->tile_at(index);
if (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);
ThumbnailLayer* thumbLayer = [allThumbnailLayers_ objectAtIndex:index];
[thumbLayer setTabContents:contents];
}
- (void)tabStripModelDeleted {
[self close];
}
@end