| // 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/prefs/pref_service.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "chrome/app/chrome_command_ids.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/devtools/devtools_window.h" |
| #include "chrome/browser/extensions/tab_helper.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/thumbnails/render_widget_snapshot_taker.h" |
| #include "chrome/browser/ui/bookmarks/bookmark_tab_helper.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_mac.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/common/pref_names.h" |
| #include "content/public/browser/browser_thread.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 "content/public/browser/web_contents_view.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/cocoa/animation_utils.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::ScopedCFTypeRef<CGColorSpaceRef> grayColorSpace( |
| CGColorSpaceCreateWithName(kCGColorSpaceGenericGray)); |
| CGFloat grays[] = { startGray_, 1.0, endGray_, 1.0 }; |
| CGFloat locations[] = { 0, 1 }; |
| base::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 WebContents 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 WebContents the thumbnail is for. |
| content::WebContents* 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::ScopedCFTypeRef<CGImageRef> thumbnail_; |
| |
| // True if the layer already sent a thumbnail request to a renderer. |
| BOOL didSendLoad_; |
| } |
| - (id)initWithWebContents:(content::WebContents*)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() { |
| } |
| |
| void DidReceiveBitmap(const SkBitmap& bitmap) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| [layer_ setThumbnail:bitmap]; |
| } |
| |
| 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)); |
| |
| // 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. |
| |
| // Will send an IPC to the renderer on the IO thread. |
| g_browser_process->GetRenderWidgetSnapshotTaker()->AskForSnapshot( |
| rwh_, |
| base::Bind(&ThumbnailLoader::DidReceiveBitmap, |
| weak_factory_.GetWeakPtr()), |
| page_size, |
| pixel_size); |
| } |
| |
| } // namespace tabpose |
| |
| @implementation ThumbnailLayer |
| |
| - (id)initWithWebContents:(content::WebContents*)contents |
| fullSize:(NSSize)fullSize { |
| CHECK(contents); |
| if ((self = [super init])) { |
| contents_ = contents; |
| fullSize_ = fullSize; |
| } |
| return self; |
| } |
| |
| - (void)setWebContents:(content::WebContents*)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_->GetView()->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]; |
| } |
| |
| BookmarkTabHelper* bookmark_tab_helper = |
| BookmarkTabHelper::FromWebContents(contents_); |
| Profile* profile = |
| Profile::FromBrowserContext(contents_->GetBrowserContext()); |
| bool always_show_bookmark_bar = |
| profile->GetPrefs()->GetBoolean(prefs::kShowBookmarkBar); |
| bool has_detached_bookmark_bar = |
| bookmark_tab_helper->ShouldShowBookmarkBar() && |
| !always_show_bookmark_bar; |
| if (has_detached_bookmark_bar) |
| topOffset += chrome::kNTPBookmarkBarHeight; |
| |
| return topOffset; |
| } |
| |
| - (int)bottomOffset { |
| int bottomOffset = 0; |
| DevToolsWindow* devToolsWindow = |
| DevToolsWindow::GetDockedInstanceForInspectedTab(contents_); |
| content::WebContents* devToolsContents = |
| devToolsWindow ? devToolsWindow->web_contents() : NULL; |
| if (devToolsContents && devToolsContents->GetRenderViewHost() && |
| devToolsContents->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->GetRenderViewHost()->GetView()-> |
| GetViewBounds().height(); |
| bottomOffset += 1; // :-( Divider line between web contents and devtools. |
| } |
| return bottomOffset; |
| } |
| |
| - (void)drawInContext:(CGContextRef)context { |
| RenderWidgetHost* rwh = 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 save_gstate(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_->GetTitle(); |
| } |
| |
| content::WebContents* web_contents() const { return contents_; } |
| void set_tab_contents(content::WebContents* 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_; |
| |
| content::WebContents* 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 { |
| extensions::TabHelper* extensions_tab_helper = |
| extensions::TabHelper::FromWebContents(contents_); |
| if (extensions_tab_helper->is_app()) { |
| SkBitmap* bitmap = extensions_tab_helper->GetExtensionAppIcon(); |
| if (bitmap) |
| return gfx::SkBitmapToNSImage(*bitmap); |
| } |
| return mac::FaviconForWebContents(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, content::WebContents* 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->GetWebContentsAt(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, content::WebContents* 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)); |
| closeIcon_.reset([ResourceBundle::GetSharedInstance().GetNativeImageNamed( |
| IDR_TABPOSE_CLOSE).ToNSImage() retain]); |
| [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 { |
| base::scoped_nsobject<CALayer> layer( |
| [[ThumbnailLayer alloc] initWithWebContents:tile.web_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 = closeIcon_.get(); |
| CGRect closeBounds = {}; |
| closeBounds.size = NSSizeToCGSize([closeIcon_ size]); |
| 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]); |
| |
| 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 = tile.favicon(); |
| 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_->CloseWebContentsAt(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:(content::WebContents*)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:(content::WebContents*)contents |
| atIndex:(NSInteger)index { |
| // We will also get a -tabDetachedWithContents:atIndex: notification for |
| // closing tabs, so do nothing here. |
| } |
| |
| - (void)tabDetachedWithContents:(content::WebContents*)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:(content::WebContents*)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|. |
| base::scoped_nsobject<CALayer> thumbLayer( |
| [[allThumbnailLayers_ objectAtIndex:from] retain]); |
| [allThumbnailLayers_ removeObjectAtIndex:from]; |
| [allThumbnailLayers_ insertObject:thumbLayer.get() atIndex:to]; |
| base::scoped_nsobject<CALayer> faviconLayer( |
| [[allFaviconLayers_ objectAtIndex:from] retain]); |
| [allFaviconLayers_ removeObjectAtIndex:from]; |
| [allFaviconLayers_ insertObject:faviconLayer.get() atIndex:to]; |
| base::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:(content::WebContents*)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.web_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 setWebContents:contents]; |
| } |
| |
| - (void)tabStripModelDeleted { |
| [self close]; |
| } |
| |
| @end |