blob: c99c63fae6c0f9031e5aba28356c75398d822312 [file] [log] [blame]
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h"
6
7#import <QuartzCore/QuartzCore.h>
8
9#include <cmath>
10#include <limits>
11#include <string>
12
13#include "base/command_line.h"
14#include "base/mac/mac_util.h"
15#include "base/mac/scoped_nsautorelease_pool.h"
16#include "base/metrics/histogram.h"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000017#include "base/prefs/pref_service.h"
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +010018#include "base/strings/sys_string_conversions.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000019#include "chrome/app/chrome_command_ids.h"
20#include "chrome/browser/autocomplete/autocomplete_classifier.h"
21#include "chrome/browser/autocomplete/autocomplete_classifier_factory.h"
22#include "chrome/browser/autocomplete/autocomplete_match.h"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000023#include "chrome/browser/devtools/devtools_window.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000024#include "chrome/browser/extensions/tab_helper.h"
25#include "chrome/browser/favicon/favicon_tab_helper.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000026#include "chrome/browser/profiles/profile.h"
27#include "chrome/browser/profiles/profile_manager.h"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000028#include "chrome/browser/themes/theme_service.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000029#include "chrome/browser/ui/browser.h"
30#include "chrome/browser/ui/browser_navigator.h"
31#include "chrome/browser/ui/browser_tabstrip.h"
32#import "chrome/browser/ui/cocoa/browser_window_controller.h"
33#import "chrome/browser/ui/cocoa/constrained_window/constrained_window_sheet_controller.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000034#include "chrome/browser/ui/cocoa/drag_util.h"
35#import "chrome/browser/ui/cocoa/image_button_cell.h"
36#import "chrome/browser/ui/cocoa/new_tab_button.h"
37#import "chrome/browser/ui/cocoa/tab_contents/favicon_util_mac.h"
38#import "chrome/browser/ui/cocoa/tab_contents/tab_contents_controller.h"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000039#import "chrome/browser/ui/cocoa/tabs/tab_audio_indicator_view_mac.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000040#import "chrome/browser/ui/cocoa/tabs/tab_controller.h"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000041#import "chrome/browser/ui/cocoa/tabs/tab_projecting_image_view.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000042#import "chrome/browser/ui/cocoa/tabs/tab_strip_drag_controller.h"
43#import "chrome/browser/ui/cocoa/tabs/tab_strip_model_observer_bridge.h"
44#import "chrome/browser/ui/cocoa/tabs/tab_strip_view.h"
45#import "chrome/browser/ui/cocoa/tabs/tab_view.h"
46#import "chrome/browser/ui/cocoa/tabs/throbber_view.h"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000047#import "chrome/browser/ui/cocoa/tabs/throbbing_image_view.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000048#include "chrome/browser/ui/find_bar/find_bar.h"
49#include "chrome/browser/ui/find_bar/find_bar_controller.h"
50#include "chrome/browser/ui/find_bar/find_tab_helper.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000051#include "chrome/browser/ui/tabs/tab_menu_model.h"
52#include "chrome/browser/ui/tabs/tab_strip_model.h"
53#include "chrome/browser/ui/tabs/tab_strip_model_delegate.h"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000054#include "chrome/browser/ui/tabs/tab_utils.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000055#include "chrome/common/chrome_switches.h"
Ben Murdoch558790d2013-07-30 15:19:42 +010056#include "chrome/common/net/url_fixer_upper.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000057#include "chrome/common/pref_names.h"
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +010058#include "components/web_modal/web_contents_modal_dialog_manager.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000059#include "content/public/browser/navigation_controller.h"
60#include "content/public/browser/user_metrics.h"
61#include "content/public/browser/web_contents.h"
62#include "content/public/browser/web_contents_view.h"
63#include "grit/generated_resources.h"
64#include "grit/theme_resources.h"
65#include "grit/ui_resources.h"
66#include "skia/ext/skia_utils_mac.h"
67#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +010068#import "ui/base/animation/animation_container.h"
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +010069#include "ui/base/cocoa/animation_utils.h"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000070#import "ui/base/cocoa/tracking_area.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000071#include "ui/base/l10n/l10n_util.h"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000072#include "ui/base/models/list_selection_model.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000073#include "ui/base/resource/resource_bundle.h"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000074#include "ui/base/theme_provider.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000075#include "ui/gfx/image/image.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000076
77using content::OpenURLParams;
78using content::Referrer;
79using content::UserMetricsAction;
80using content::WebContents;
81
82namespace {
83
84// A value to indicate tab layout should use the full available width of the
85// view.
86const CGFloat kUseFullAvailableWidth = -1.0;
87
88// The amount by which tabs overlap.
89// Needs to be <= the x position of the favicon within a tab. Else, every time
90// the throbber is painted, the throbber's invalidation will also invalidate
91// parts of the tab to the left, and two tabs's backgrounds need to be painted
92// on each throbber frame instead of one.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000093const CGFloat kTabOverlap = 19.0;
Torne (Richard Coles)58218062012-11-14 11:43:16 +000094
95// The amount by which mini tabs are separated from normal tabs.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000096const CGFloat kLastMiniTabSpacing = 2.0;
Torne (Richard Coles)58218062012-11-14 11:43:16 +000097
98// The width and height for a tab's icon.
99const CGFloat kIconWidthAndHeight = 16.0;
100
101// The amount by which the new tab button is offset (from the tabs).
102const CGFloat kNewTabButtonOffset = 8.0;
103
104// Time (in seconds) in which tabs animate to their final position.
105const NSTimeInterval kAnimationDuration = 0.125;
106
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000107// The amount by which the profile menu button is offset (from tab tabs or new
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000108// tab button).
109const CGFloat kProfileMenuButtonOffset = 6.0;
110
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000111// The width and height of the icon + glow for projecting mode.
112const CGFloat kProjectingIconWidthAndHeight = 32.0;
113
114// Throbbing duration on webrtc "this web page is watching you" favicon overlay.
115const int kRecordingDurationMs = 1000;
116
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000117// Helper class for doing NSAnimationContext calls that takes a bool to disable
118// all the work. Useful for code that wants to conditionally animate.
119class ScopedNSAnimationContextGroup {
120 public:
121 explicit ScopedNSAnimationContextGroup(bool animate)
122 : animate_(animate) {
123 if (animate_) {
124 [NSAnimationContext beginGrouping];
125 }
126 }
127
128 ~ScopedNSAnimationContextGroup() {
129 if (animate_) {
130 [NSAnimationContext endGrouping];
131 }
132 }
133
134 void SetCurrentContextDuration(NSTimeInterval duration) {
135 if (animate_) {
136 [[NSAnimationContext currentContext] gtm_setDuration:duration
137 eventMask:NSLeftMouseUpMask];
138 }
139 }
140
141 void SetCurrentContextShortestDuration() {
142 if (animate_) {
143 // The minimum representable time interval. This used to stop an
144 // in-progress animation as quickly as possible.
145 const NSTimeInterval kMinimumTimeInterval =
146 std::numeric_limits<NSTimeInterval>::min();
147 // Directly set the duration to be short, avoiding the Steve slowmotion
148 // ettect the gtm_setDuration: provides.
149 [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval];
150 }
151 }
152
153private:
154 bool animate_;
155 DISALLOW_COPY_AND_ASSIGN(ScopedNSAnimationContextGroup);
156};
157
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000158// Creates an NSImage with size |size| and bitmap image representations for both
159// 1x and 2x scale factors. |drawingHandler| is called once for every scale
160// factor. This is similar to -[NSImage imageWithSize:flipped:drawingHandler:],
161// but this function always evaluates drawingHandler eagerly, and it works on
162// 10.6 and 10.7.
163NSImage* CreateImageWithSize(NSSize size,
164 void (^drawingHandler)(NSSize)) {
Ben Murdocheb525c52013-07-10 11:40:50 +0100165 base::scoped_nsobject<NSImage> result([[NSImage alloc] initWithSize:size]);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000166 [NSGraphicsContext saveGraphicsState];
167 for (ui::ScaleFactor scale_factor : ui::GetSupportedScaleFactors()) {
168 float scale = GetScaleFactorScale(scale_factor);
169 NSBitmapImageRep *bmpImageRep = [[NSBitmapImageRep alloc]
170 initWithBitmapDataPlanes:NULL
171 pixelsWide:size.width * scale
172 pixelsHigh:size.height * scale
173 bitsPerSample:8
174 samplesPerPixel:4
175 hasAlpha:YES
176 isPlanar:NO
177 colorSpaceName:NSDeviceRGBColorSpace
178 bytesPerRow:0
179 bitsPerPixel:0];
180 [bmpImageRep setSize:size];
181 [NSGraphicsContext setCurrentContext:
182 [NSGraphicsContext graphicsContextWithBitmapImageRep:bmpImageRep]];
183 drawingHandler(size);
184 [result addRepresentation:bmpImageRep];
185 }
186 [NSGraphicsContext restoreGraphicsState];
187
188 return result.release();
189}
190
191// Takes a normal bitmap and a mask image and returns an image the size of the
192// mask that has pixels from |image| but alpha information from |mask|.
193NSImage* ApplyMask(NSImage* image, NSImage* mask) {
194 return [CreateImageWithSize([mask size], ^(NSSize size) {
195 // Skip a few pixels from the top of the tab background gradient, because
196 // the new tab button is not drawn at the very top of the browser window.
197 const int kYOffset = 10;
198 CGFloat width = size.width;
199 CGFloat height = size.height;
200
201 // In some themes, the tab background image is narrower than the
202 // new tab button, so tile the background image.
203 CGFloat x = 0;
204 // The floor() is to make sure images with odd widths don't draw to the
205 // same pixel twice on retina displays. (Using NSDrawThreePartImage()
206 // caused a startup perf regression, so that cannot be used.)
207 CGFloat tileWidth = floor(std::min(width, [image size].width));
208 while (x < width) {
209 [image drawAtPoint:NSMakePoint(x, 0)
210 fromRect:NSMakeRect(0,
211 [image size].height - height - kYOffset,
212 tileWidth,
213 height)
214 operation:NSCompositeCopy
215 fraction:1.0];
216 x += tileWidth;
217 }
218
219 [mask drawAtPoint:NSZeroPoint
220 fromRect:NSMakeRect(0, 0, width, height)
221 operation:NSCompositeDestinationIn
222 fraction:1.0];
223 }) autorelease];
224}
225
226// Creates a modified favicon used for the recording case. The mask is used for
227// making part of the favicon transparent. (The part where the recording dot
228// later is drawn.)
229NSImage* CreateMaskedFaviconForRecording(NSImage* image,
230 NSImage* mask,
231 NSImage* recImage) {
232 return [CreateImageWithSize([image size], ^(NSSize size) {
233 CGFloat width = size.width;
234 CGFloat height = size.height;
235
236 [image drawAtPoint:NSZeroPoint
237 fromRect:NSMakeRect(0, 0, width, height)
238 operation:NSCompositeCopy
239 fraction:1.0];
240
241 NSSize maskSize = [mask size];
242 NSSize recImageSize = [recImage size];
243 CGFloat offsetFromRight = recImageSize.width +
244 (maskSize.width - recImageSize.width) / 2;
245 CGFloat offsetFromBottom = (maskSize.height - recImageSize.height) / 2;
246
247 NSRect maskBounds;
248 maskBounds.origin.x = width - offsetFromRight;
249 maskBounds.origin.y = -offsetFromBottom;
250 maskBounds.size = maskSize;
251
252 [mask drawInRect:maskBounds
253 fromRect:NSZeroRect
254 operation:NSCompositeDestinationOut
255 fraction:1.0];
256 }) autorelease];
257}
258
259// Paints |overlay| on top of |ground|.
260NSImage* Overlay(NSImage* ground, NSImage* overlay, CGFloat alpha) {
261 DCHECK_EQ([ground size].width, [overlay size].width);
262 DCHECK_EQ([ground size].height, [overlay size].height);
263
264 return [CreateImageWithSize([ground size], ^(NSSize size) {
265 CGFloat width = size.width;
266 CGFloat height = size.height;
267 [ground drawAtPoint:NSZeroPoint
268 fromRect:NSMakeRect(0, 0, width, height)
269 operation:NSCompositeCopy
270 fraction:1.0];
271 [overlay drawAtPoint:NSZeroPoint
272 fromRect:NSMakeRect(0, 0, width, height)
273 operation:NSCompositeSourceOver
274 fraction:alpha];
275 }) autorelease];
276}
277
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000278} // namespace
279
280@interface TabStripController (Private)
281- (void)addSubviewToPermanentList:(NSView*)aView;
282- (void)regenerateSubviewList;
283- (NSInteger)indexForContentsView:(NSView*)view;
284- (NSImageView*)iconImageViewForContents:(content::WebContents*)contents;
285- (void)updateFaviconForContents:(content::WebContents*)contents
286 atIndex:(NSInteger)modelIndex;
287- (void)layoutTabsWithAnimation:(BOOL)animate
288 regenerateSubviews:(BOOL)doUpdate;
289- (void)animationDidStopForController:(TabController*)controller
290 finished:(BOOL)finished;
291- (NSInteger)indexFromModelIndex:(NSInteger)index;
292- (void)clickNewTabButton:(id)sender;
293- (NSInteger)numberOfOpenTabs;
294- (NSInteger)numberOfOpenMiniTabs;
295- (NSInteger)numberOfOpenNonMiniTabs;
296- (void)mouseMoved:(NSEvent*)event;
297- (void)setTabTrackingAreasEnabled:(BOOL)enabled;
298- (void)droppingURLsAt:(NSPoint)point
299 givesIndex:(NSInteger*)index
300 disposition:(WindowOpenDisposition*)disposition;
301- (void)setNewTabButtonHoverState:(BOOL)showHover;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000302- (void)themeDidChangeNotification:(NSNotification*)notification;
303- (void)setNewTabImages;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000304@end
305
306// A simple view class that prevents the Window Server from dragging the area
307// behind tabs. Sometimes core animation confuses it. Unfortunately, it can also
308// falsely pick up clicks during rapid tab closure, so we have to account for
309// that.
310@interface TabStripControllerDragBlockingView : NSView {
311 TabStripController* controller_; // weak; owns us
312}
313
314- (id)initWithFrame:(NSRect)frameRect
315 controller:(TabStripController*)controller;
316
317// Runs a nested runloop to do window move tracking. Overriding
318// -mouseDownCanMoveWindow with a dynamic result instead doesn't work:
319// http://www.cocoabuilder.com/archive/cocoa/219261-conditional-mousedowncanmovewindow-for-nsview.html
320// http://www.cocoabuilder.com/archive/cocoa/92973-brushed-metal-window-dragging.html
321- (void)trackClickForWindowMove:(NSEvent*)event;
322@end
323
324@implementation TabStripControllerDragBlockingView
325- (BOOL)mouseDownCanMoveWindow {
326 return NO;
327}
328
329- (void)drawRect:(NSRect)rect {
330}
331
332- (id)initWithFrame:(NSRect)frameRect
333 controller:(TabStripController*)controller {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000334 if ((self = [super initWithFrame:frameRect])) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000335 controller_ = controller;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000336 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000337 return self;
338}
339
340// In "rapid tab closure" mode (i.e., the user is clicking close tab buttons in
341// rapid succession), the animations confuse Cocoa's hit testing (which appears
342// to use cached results, among other tricks), so this view can somehow end up
343// getting a mouse down event. Thus we do an explicit hit test during rapid tab
344// closure, and if we find that we got a mouse down we shouldn't have, we send
345// it off to the appropriate view.
346- (void)mouseDown:(NSEvent*)event {
347 NSView* superview = [self superview];
348 NSPoint hitLocation =
349 [[superview superview] convertPoint:[event locationInWindow]
350 fromView:nil];
351 NSView* hitView = [superview hitTest:hitLocation];
352
353 if ([controller_ inRapidClosureMode]) {
354 if (hitView != self) {
355 [hitView mouseDown:event];
356 return;
357 }
358 }
359
360 if (hitView == self) {
361 BrowserWindowController* windowController =
362 [BrowserWindowController browserWindowControllerForView:self];
363 if (![windowController isFullscreen]) {
364 [self trackClickForWindowMove:event];
365 return;
366 }
367 }
368 [super mouseDown:event];
369}
370
371- (void)trackClickForWindowMove:(NSEvent*)event {
372 NSWindow* window = [self window];
373 NSPoint frameOrigin = [window frame].origin;
374 NSPoint lastEventLoc = [window convertBaseToScreen:[event locationInWindow]];
375 while ((event = [NSApp nextEventMatchingMask:
376 NSLeftMouseDownMask|NSLeftMouseDraggedMask|NSLeftMouseUpMask
377 untilDate:[NSDate distantFuture]
378 inMode:NSEventTrackingRunLoopMode
379 dequeue:YES]) &&
380 [event type] != NSLeftMouseUp) {
381 base::mac::ScopedNSAutoreleasePool pool;
382
383 NSPoint now = [window convertBaseToScreen:[event locationInWindow]];
384 frameOrigin.x += now.x - lastEventLoc.x;
385 frameOrigin.y += now.y - lastEventLoc.y;
386 [window setFrameOrigin:frameOrigin];
387 lastEventLoc = now;
388 }
389}
390
391@end
392
393#pragma mark -
394
395// A delegate, owned by the CAAnimation system, that is alerted when the
396// animation to close a tab is completed. Calls back to the given tab strip
397// to let it know that |controller_| is ready to be removed from the model.
398// Since we only maintain weak references, the tab strip must call -invalidate:
399// to prevent the use of dangling pointers.
400@interface TabCloseAnimationDelegate : NSObject {
401 @private
402 TabStripController* strip_; // weak; owns us indirectly
403 TabController* controller_; // weak
404}
405
406// Will tell |strip| when the animation for |controller|'s view has completed.
407// These should not be nil, and will not be retained.
408- (id)initWithTabStrip:(TabStripController*)strip
409 tabController:(TabController*)controller;
410
411// Invalidates this object so that no further calls will be made to
412// |strip_|. This should be called when |strip_| is released, to
413// prevent attempts to call into the released object.
414- (void)invalidate;
415
416// CAAnimation delegate method
417- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished;
418
419@end
420
421@implementation TabCloseAnimationDelegate
422
423- (id)initWithTabStrip:(TabStripController*)strip
424 tabController:(TabController*)controller {
425 if ((self = [super init])) {
426 DCHECK(strip && controller);
427 strip_ = strip;
428 controller_ = controller;
429 }
430 return self;
431}
432
433- (void)invalidate {
434 strip_ = nil;
435 controller_ = nil;
436}
437
438- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
439 [strip_ animationDidStopForController:controller_ finished:finished];
440}
441
442@end
443
444#pragma mark -
445
446// In general, there is a one-to-one correspondence between TabControllers,
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000447// TabViews, TabContentsControllers, and the WebContents in the
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000448// TabStripModel. In the steady-state, the indices line up so an index coming
449// from the model is directly mapped to the same index in the parallel arrays
450// holding our views and controllers. This is also true when new tabs are
451// created (even though there is a small period of animation) because the tab is
452// present in the model while the TabView is animating into place. As a result,
453// nothing special need be done to handle "new tab" animation.
454//
455// This all goes out the window with the "close tab" animation. The animation
456// kicks off in |-tabDetachedWithContents:atIndex:| with the notification that
457// the tab has been removed from the model. The simplest solution at this
458// point would be to remove the views and controllers as well, however once
459// the TabView is removed from the view list, the tab z-order code takes care of
460// removing it from the tab strip and we'll get no animation. That means if
461// there is to be any visible animation, the TabView needs to stay around until
462// its animation is complete. In order to maintain consistency among the
463// internal parallel arrays, this means all structures are kept around until
464// the animation completes. At this point, though, the model and our internal
465// structures are out of sync: the indices no longer line up. As a result,
466// there is a concept of a "model index" which represents an index valid in
467// the TabStripModel. During steady-state, the "model index" is just the same
468// index as our parallel arrays (as above), but during tab close animations,
469// it is different, offset by the number of tabs preceding the index which
470// are undergoing tab closing animation. As a result, the caller needs to be
471// careful to use the available conversion routines when accessing the internal
472// parallel arrays (e.g., -indexFromModelIndex:). Care also needs to be taken
473// during tab layout to ignore closing tabs in the total width calculations and
474// in individual tab positioning (to avoid moving them right back to where they
475// were).
476//
477// In order to prevent actions being taken on tabs which are closing, the tab
478// itself gets marked as such so it no longer will send back its select action
479// or allow itself to be dragged. In addition, drags on the tab strip as a
480// whole are disabled while there are tabs closing.
481
482@implementation TabStripController
483
484@synthesize leftIndentForControls = leftIndentForControls_;
485@synthesize rightIndentForControls = rightIndentForControls_;
486
487- (id)initWithView:(TabStripView*)view
488 switchView:(NSView*)switchView
489 browser:(Browser*)browser
490 delegate:(id<TabStripControllerDelegate>)delegate {
491 DCHECK(view && switchView && browser && delegate);
492 if ((self = [super init])) {
493 tabStripView_.reset([view retain]);
494 [tabStripView_ setController:self];
495 switchView_ = switchView;
496 browser_ = browser;
497 tabStripModel_ = browser_->tab_strip_model();
498 hoverTabSelector_.reset(new HoverTabSelector(tabStripModel_));
499 delegate_ = delegate;
500 bridge_.reset(new TabStripModelObserverBridge(tabStripModel_, self));
501 dragController_.reset(
502 [[TabStripDragController alloc] initWithTabStripController:self]);
503 tabContentsArray_.reset([[NSMutableArray alloc] init]);
504 tabArray_.reset([[NSMutableArray alloc] init]);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100505 animationContainer_ = new ui::AnimationContainer;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000506 NSWindow* browserWindow = [view window];
507
508 // Important note: any non-tab subviews not added to |permanentSubviews_|
509 // (see |-addSubviewToPermanentList:|) will be wiped out.
510 permanentSubviews_.reset([[NSMutableArray alloc] init]);
511
512 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
513 defaultFavicon_.reset(
514 rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).CopyNSImage());
515
516 [self setLeftIndentForControls:[[self class] defaultLeftIndentForControls]];
517 [self setRightIndentForControls:0];
518
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000519 newTabButton_ = [view getNewTabButton];
520 [self addSubviewToPermanentList:newTabButton_];
521 [newTabButton_ setTarget:self];
522 [newTabButton_ setAction:@selector(clickNewTabButton:)];
523
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000524 [self setNewTabImages];
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000525 newTabButtonShowingHoverImage_ = NO;
526 newTabTrackingArea_.reset(
527 [[CrTrackingArea alloc] initWithRect:[newTabButton_ bounds]
528 options:(NSTrackingMouseEnteredAndExited |
529 NSTrackingActiveAlways)
530 owner:self
531 userInfo:nil]);
532 if (browserWindow) // Nil for Browsers without a tab strip (e.g. popups).
533 [newTabTrackingArea_ clearOwnerWhenWindowWillClose:browserWindow];
534 [newTabButton_ addTrackingArea:newTabTrackingArea_.get()];
535 targetFrames_.reset([[NSMutableDictionary alloc] init]);
536
537 dragBlockingView_.reset(
538 [[TabStripControllerDragBlockingView alloc] initWithFrame:NSZeroRect
539 controller:self]);
540 [self addSubviewToPermanentList:dragBlockingView_];
541
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100542 newTabTargetFrame_ = NSZeroRect;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000543 availableResizeWidth_ = kUseFullAvailableWidth;
544
545 closingControllers_.reset([[NSMutableSet alloc] init]);
546
547 // Install the permanent subviews.
548 [self regenerateSubviewList];
549
550 // Watch for notifications that the tab strip view has changed size so
551 // we can tell it to layout for the new size.
552 [[NSNotificationCenter defaultCenter]
553 addObserver:self
554 selector:@selector(tabViewFrameChanged:)
555 name:NSViewFrameDidChangeNotification
556 object:tabStripView_];
557
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000558 [[NSNotificationCenter defaultCenter]
559 addObserver:self
560 selector:@selector(themeDidChangeNotification:)
561 name:kBrowserThemeDidChangeNotification
562 object:nil];
563
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000564 trackingArea_.reset([[CrTrackingArea alloc]
565 initWithRect:NSZeroRect // Ignored by NSTrackingInVisibleRect
566 options:NSTrackingMouseEnteredAndExited |
567 NSTrackingMouseMoved |
568 NSTrackingActiveAlways |
569 NSTrackingInVisibleRect
570 owner:self
571 userInfo:nil]);
572 if (browserWindow) // Nil for Browsers without a tab strip (e.g. popups).
573 [trackingArea_ clearOwnerWhenWindowWillClose:browserWindow];
574 [tabStripView_ addTrackingArea:trackingArea_.get()];
575
576 // Check to see if the mouse is currently in our bounds so we can
577 // enable the tracking areas. Otherwise we won't get hover states
578 // or tab gradients if we load the window up under the mouse.
579 NSPoint mouseLoc = [[view window] mouseLocationOutsideOfEventStream];
580 mouseLoc = [view convertPoint:mouseLoc fromView:nil];
581 if (NSPointInRect(mouseLoc, [view bounds])) {
582 [self setTabTrackingAreasEnabled:YES];
583 mouseInside_ = YES;
584 }
585
586 // Set accessibility descriptions. http://openradar.appspot.com/7496255
587 NSString* description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_NEWTAB);
588 [[newTabButton_ cell]
589 accessibilitySetOverrideValue:description
590 forAttribute:NSAccessibilityDescriptionAttribute];
591
592 // Controller may have been (re-)created by switching layout modes, which
593 // means the tab model is already fully formed with tabs. Need to walk the
594 // list and create the UI for each.
595 const int existingTabCount = tabStripModel_->count();
596 const content::WebContents* selection =
597 tabStripModel_->GetActiveWebContents();
598 for (int i = 0; i < existingTabCount; ++i) {
599 content::WebContents* currentContents =
600 tabStripModel_->GetWebContentsAt(i);
601 [self insertTabWithContents:currentContents
602 atIndex:i
603 inForeground:NO];
604 if (selection == currentContents) {
605 // Must manually force a selection since the model won't send
606 // selection messages in this scenario.
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100607 [self
608 activateTabWithContents:currentContents
609 previousContents:NULL
610 atIndex:i
611 reason:TabStripModelObserver::CHANGE_REASON_NONE];
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000612 }
613 }
614 // Don't lay out the tabs until after the controller has been fully
615 // constructed.
616 if (existingTabCount) {
617 [self performSelectorOnMainThread:@selector(layoutTabs)
618 withObject:nil
619 waitUntilDone:NO];
620 }
621 }
622 return self;
623}
624
625- (void)dealloc {
626 [tabStripView_ setController:nil];
627
628 if (trackingArea_.get())
629 [tabStripView_ removeTrackingArea:trackingArea_.get()];
630
631 [newTabButton_ removeTrackingArea:newTabTrackingArea_.get()];
632 // Invalidate all closing animations so they don't call back to us after
633 // we're gone.
634 for (TabController* controller in closingControllers_.get()) {
635 NSView* view = [controller view];
636 [[[view animationForKey:@"frameOrigin"] delegate] invalidate];
637 }
638 [[NSNotificationCenter defaultCenter] removeObserver:self];
639 [tabStripView_ removeAllToolTips];
640 [super dealloc];
641}
642
643+ (CGFloat)defaultTabHeight {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000644 return 26.0;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000645}
646
647+ (CGFloat)defaultLeftIndentForControls {
648 // Default indentation leaves enough room so tabs don't overlap with the
649 // window controls.
650 return 70.0;
651}
652
653// Finds the TabContentsController associated with the given index into the tab
654// model and swaps out the sole child of the contentArea to display its
655// contents.
656- (void)swapInTabAtIndex:(NSInteger)modelIndex {
657 DCHECK(modelIndex >= 0 && modelIndex < tabStripModel_->count());
658 NSInteger index = [self indexFromModelIndex:modelIndex];
659 TabContentsController* controller = [tabContentsArray_ objectAtIndex:index];
660
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100661 // Make sure that any layers that move are not animated to their new
662 // positions.
663 ScopedCAActionDisabler disabler;
664
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000665 // Resize the new view to fit the window. Calling |view| may lazily
666 // instantiate the TabContentsController from the nib. Until we call
667 // |-ensureContentsVisible|, the controller doesn't install the RWHVMac into
668 // the view hierarchy. This is in order to avoid sending the renderer a
669 // spurious default size loaded from the nib during the call to |-view|.
670 NSView* newView = [controller view];
671
672 // Turns content autoresizing off, so removing and inserting views won't
673 // trigger unnecessary content relayout.
674 [controller ensureContentsSizeDoesNotChange];
675
676 // Remove the old view from the view hierarchy. We know there's only one
677 // child of |switchView_| because we're the one who put it there. There
678 // may not be any children in the case of a tab that's been closed, in
679 // which case there's no swapping going on.
680 NSArray* subviews = [switchView_ subviews];
681 if ([subviews count]) {
682 NSView* oldView = [subviews objectAtIndex:0];
683 // Set newView frame to the oldVew frame to prevent NSSplitView hosting
684 // sidebar and tab content from resizing sidebar's content view.
685 // ensureContentsVisible (see below) sets content size and autoresizing
686 // properties.
687 [newView setFrame:[oldView frame]];
688 [switchView_ replaceSubview:oldView with:newView];
689 } else {
690 [newView setFrame:[switchView_ bounds]];
691 [switchView_ addSubview:newView];
692 }
693
694 // New content is in place, delegate should adjust itself accordingly.
695 [delegate_ onActivateTabWithContents:[controller webContents]];
696
697 // It also restores content autoresizing properties.
698 [controller ensureContentsVisible];
699
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000700 NSWindow* parentWindow = [switchView_ window];
701 ConstrainedWindowSheetController* sheetController =
702 [ConstrainedWindowSheetController
703 controllerForParentWindow:parentWindow];
704 [sheetController parentViewDidBecomeActive:newView];
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000705}
706
707// Create a new tab view and set its cell correctly so it draws the way we want
708// it to. It will be sized and positioned by |-layoutTabs| so there's no need to
709// set the frame here. This also creates the view as hidden, it will be
710// shown during layout.
711- (TabController*)newTab {
712 TabController* controller = [[[TabController alloc] init] autorelease];
713 [controller setTarget:self];
714 [controller setAction:@selector(selectTab:)];
715 [[controller view] setHidden:YES];
716
717 return controller;
718}
719
720// (Private) Handles a click on the new tab button.
721- (void)clickNewTabButton:(id)sender {
722 content::RecordAction(UserMetricsAction("NewTab_Button"));
723 UMA_HISTOGRAM_ENUMERATION("Tab.NewTab", TabStripModel::NEW_TAB_BUTTON,
724 TabStripModel::NEW_TAB_ENUM_COUNT);
725 tabStripModel_->delegate()->AddBlankTabAt(-1, true);
726}
727
728// (Private) Returns the number of open tabs in the tab strip. This is the
729// number of TabControllers we know about (as there's a 1-to-1 mapping from
730// these controllers to a tab) less the number of closing tabs.
731- (NSInteger)numberOfOpenTabs {
732 return static_cast<NSInteger>(tabStripModel_->count());
733}
734
735// (Private) Returns the number of open, mini-tabs.
736- (NSInteger)numberOfOpenMiniTabs {
737 // Ask the model for the number of mini tabs. Note that tabs which are in
738 // the process of closing (i.e., whose controllers are in
739 // |closingControllers_|) have already been removed from the model.
740 return tabStripModel_->IndexOfFirstNonMiniTab();
741}
742
743// (Private) Returns the number of open, non-mini tabs.
744- (NSInteger)numberOfOpenNonMiniTabs {
745 NSInteger number = [self numberOfOpenTabs] - [self numberOfOpenMiniTabs];
746 DCHECK_GE(number, 0);
747 return number;
748}
749
750// Given an index into the tab model, returns the index into the tab controller
751// or tab contents controller array accounting for tabs that are currently
752// closing. For example, if there are two tabs in the process of closing before
753// |index|, this returns |index| + 2. If there are no closing tabs, this will
754// return |index|.
755- (NSInteger)indexFromModelIndex:(NSInteger)index {
756 DCHECK_GE(index, 0);
757 if (index < 0)
758 return index;
759
760 NSInteger i = 0;
761 for (TabController* controller in tabArray_.get()) {
762 if ([closingControllers_ containsObject:controller]) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000763 DCHECK([[controller tabView] isClosing]);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000764 ++index;
765 }
766 if (i == index) // No need to check anything after, it has no effect.
767 break;
768 ++i;
769 }
770 return index;
771}
772
773// Given an index into |tabArray_|, return the corresponding index into
774// |tabStripModel_| or NSNotFound if the specified tab does not exist in
775// the model (if it's closing, for example).
776- (NSInteger)modelIndexFromIndex:(NSInteger)index {
777 NSInteger modelIndex = 0;
778 NSInteger arrayIndex = 0;
779 for (TabController* controller in tabArray_.get()) {
780 if (![closingControllers_ containsObject:controller]) {
781 if (arrayIndex == index)
782 return modelIndex;
783 ++modelIndex;
784 } else if (arrayIndex == index) {
785 // Tab is closing - no model index.
786 return NSNotFound;
787 }
788 ++arrayIndex;
789 }
790 return NSNotFound;
791}
792
793// Returns the index of the subview |view|. Returns -1 if not present. Takes
794// closing tabs into account such that this index will correctly match the tab
795// model. If |view| is in the process of closing, returns -1, as closing tabs
796// are no longer in the model.
797- (NSInteger)modelIndexForTabView:(NSView*)view {
798 NSInteger index = 0;
799 for (TabController* current in tabArray_.get()) {
800 // If |current| is closing, skip it.
801 if ([closingControllers_ containsObject:current])
802 continue;
803 else if ([current view] == view)
804 return index;
805 ++index;
806 }
807 return -1;
808}
809
810// Returns the index of the contents subview |view|. Returns -1 if not present.
811// Takes closing tabs into account such that this index will correctly match the
812// tab model. If |view| is in the process of closing, returns -1, as closing
813// tabs are no longer in the model.
814- (NSInteger)modelIndexForContentsView:(NSView*)view {
815 NSInteger index = 0;
816 NSInteger i = 0;
817 for (TabContentsController* current in tabContentsArray_.get()) {
818 // If the TabController corresponding to |current| is closing, skip it.
819 TabController* controller = [tabArray_ objectAtIndex:i];
820 if ([closingControllers_ containsObject:controller]) {
821 ++i;
822 continue;
823 } else if ([current view] == view) {
824 return index;
825 }
826 ++index;
827 ++i;
828 }
829 return -1;
830}
831
832
833// Returns the view at the given index, using the array of TabControllers to
834// get the associated view. Returns nil if out of range.
835- (NSView*)viewAtIndex:(NSUInteger)index {
836 if (index >= [tabArray_ count])
837 return NULL;
838 return [[tabArray_ objectAtIndex:index] view];
839}
840
841- (NSUInteger)viewsCount {
842 return [tabArray_ count];
843}
844
845// Called when the user clicks a tab. Tell the model the selection has changed,
846// which feeds back into us via a notification.
847- (void)selectTab:(id)sender {
848 DCHECK([sender isKindOfClass:[NSView class]]);
849 int index = [self modelIndexForTabView:sender];
850 NSUInteger modifiers = [[NSApp currentEvent] modifierFlags];
851 if (tabStripModel_->ContainsIndex(index)) {
852 if (modifiers & NSCommandKeyMask && modifiers & NSShiftKeyMask) {
853 tabStripModel_->AddSelectionFromAnchorTo(index);
854 } else if (modifiers & NSShiftKeyMask) {
855 tabStripModel_->ExtendSelectionTo(index);
856 } else if (modifiers & NSCommandKeyMask) {
857 tabStripModel_->ToggleSelectionAt(index);
858 } else if (!tabStripModel_->IsTabSelected(index)) {
859 tabStripModel_->ActivateTabAt(index, true);
860 }
861 }
862}
863
864// Called when the user closes a tab. Asks the model to close the tab. |sender|
865// is the TabView that is potentially going away.
866- (void)closeTab:(id)sender {
867 DCHECK([sender isKindOfClass:[TabView class]]);
868
869 // Cancel any pending tab transition.
870 hoverTabSelector_->CancelTabTransition();
871
872 if ([hoveredTab_ isEqual:sender]) {
873 hoveredTab_ = nil;
874 }
875
876 NSInteger index = [self modelIndexForTabView:sender];
877 if (!tabStripModel_->ContainsIndex(index))
878 return;
879
880 content::RecordAction(UserMetricsAction("CloseTab_Mouse"));
881 const NSInteger numberOfOpenTabs = [self numberOfOpenTabs];
882 if (numberOfOpenTabs > 1) {
883 bool isClosingLastTab = index == numberOfOpenTabs - 1;
884 if (!isClosingLastTab) {
885 // Limit the width available for laying out tabs so that tabs are not
886 // resized until a later time (when the mouse leaves the tab strip).
887 // However, if the tab being closed is a pinned tab, break out of
888 // rapid-closure mode since the mouse is almost guaranteed not to be over
889 // the closebox of the adjacent tab (due to the difference in widths).
890 // TODO(pinkerton): re-visit when handling tab overflow.
891 // http://crbug.com/188
892 if (tabStripModel_->IsTabPinned(index)) {
893 availableResizeWidth_ = kUseFullAvailableWidth;
894 } else {
895 NSView* penultimateTab = [self viewAtIndex:numberOfOpenTabs - 2];
896 availableResizeWidth_ = NSMaxX([penultimateTab frame]);
897 }
898 } else {
899 // If the rightmost tab is closed, change the available width so that
900 // another tab's close button lands below the cursor (assuming the tabs
901 // are currently below their maximum width and can grow).
902 NSView* lastTab = [self viewAtIndex:numberOfOpenTabs - 1];
903 availableResizeWidth_ = NSMaxX([lastTab frame]);
904 }
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000905 tabStripModel_->CloseWebContentsAt(
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000906 index,
907 TabStripModel::CLOSE_USER_GESTURE |
908 TabStripModel::CLOSE_CREATE_HISTORICAL_TAB);
909 } else {
910 // Use the standard window close if this is the last tab
911 // this prevents the tab from being removed from the model until after
912 // the window dissapears
913 [[tabStripView_ window] performClose:nil];
914 }
915}
916
917// Dispatch context menu commands for the given tab controller.
918- (void)commandDispatch:(TabStripModel::ContextMenuCommand)command
919 forController:(TabController*)controller {
920 int index = [self modelIndexForTabView:[controller view]];
921 if (tabStripModel_->ContainsIndex(index))
922 tabStripModel_->ExecuteContextMenuCommand(index, command);
923}
924
925// Returns YES if the specificed command should be enabled for the given
926// controller.
927- (BOOL)isCommandEnabled:(TabStripModel::ContextMenuCommand)command
928 forController:(TabController*)controller {
929 int index = [self modelIndexForTabView:[controller view]];
930 if (!tabStripModel_->ContainsIndex(index))
931 return NO;
932 return tabStripModel_->IsContextMenuCommandEnabled(index, command) ? YES : NO;
933}
934
935// Returns a context menu model for a given controller. Caller owns the result.
936- (ui::SimpleMenuModel*)contextMenuModelForController:(TabController*)controller
937 menuDelegate:(ui::SimpleMenuModel::Delegate*)delegate {
938 int index = [self modelIndexForTabView:[controller view]];
939 return new TabMenuModel(delegate, tabStripModel_, index);
940}
941
942// Returns a weak reference to the controller that manages dragging of tabs.
943- (id<TabDraggingEventTarget>)dragController {
944 return dragController_.get();
945}
946
947- (void)insertPlaceholderForTab:(TabView*)tab frame:(NSRect)frame {
948 placeholderTab_ = tab;
949 placeholderFrame_ = frame;
950 [self layoutTabsWithAnimation:initialLayoutComplete_ regenerateSubviews:NO];
951}
952
953- (BOOL)isDragSessionActive {
954 return placeholderTab_ != nil;
955}
956
957- (BOOL)isTabFullyVisible:(TabView*)tab {
958 NSRect frame = [tab frame];
959 return NSMinX(frame) >= [self leftIndentForControls] &&
960 NSMaxX(frame) <= (NSMaxX([tabStripView_ frame]) -
961 [self rightIndentForControls]);
962}
963
964- (void)showNewTabButton:(BOOL)show {
965 forceNewTabButtonHidden_ = show ? NO : YES;
966 if (forceNewTabButtonHidden_)
967 [newTabButton_ setHidden:YES];
968}
969
970// Lay out all tabs in the order of their TabContentsControllers, which matches
971// the ordering in the TabStripModel. This call isn't that expensive, though
972// it is O(n) in the number of tabs. Tabs will animate to their new position
973// if the window is visible and |animate| is YES.
974// TODO(pinkerton): Note this doesn't do too well when the number of min-sized
975// tabs would cause an overflow. http://crbug.com/188
976- (void)layoutTabsWithAnimation:(BOOL)animate
977 regenerateSubviews:(BOOL)doUpdate {
978 DCHECK([NSThread isMainThread]);
979 if (![tabArray_ count])
980 return;
981
982 const CGFloat kMaxTabWidth = [TabController maxTabWidth];
983 const CGFloat kMinTabWidth = [TabController minTabWidth];
984 const CGFloat kMinSelectedTabWidth = [TabController minSelectedTabWidth];
985 const CGFloat kMiniTabWidth = [TabController miniTabWidth];
986 const CGFloat kAppTabWidth = [TabController appTabWidth];
987
988 NSRect enclosingRect = NSZeroRect;
989 ScopedNSAnimationContextGroup mainAnimationGroup(animate);
990 mainAnimationGroup.SetCurrentContextDuration(kAnimationDuration);
991
992 // Update the current subviews and their z-order if requested.
993 if (doUpdate)
994 [self regenerateSubviewList];
995
996 // Compute the base width of tabs given how much room we're allowed. Note that
997 // mini-tabs have a fixed width. We may not be able to use the entire width
998 // if the user is quickly closing tabs. This may be negative, but that's okay
999 // (taken care of by |MAX()| when calculating tab sizes).
1000 CGFloat availableSpace = 0;
1001 if ([self inRapidClosureMode]) {
1002 availableSpace = availableResizeWidth_;
1003 } else {
1004 availableSpace = NSWidth([tabStripView_ frame]);
1005
1006 // Account for the width of the new tab button.
1007 availableSpace -= NSWidth([newTabButton_ frame]) + kNewTabButtonOffset;
1008
1009 // Account for the right-side controls if not in rapid closure mode.
1010 // (In rapid closure mode, the available width is set based on the
1011 // position of the rightmost tab, not based on the width of the tab strip,
1012 // so the right controls have already been accounted for.)
1013 availableSpace -= [self rightIndentForControls];
1014 }
1015
1016 // Need to leave room for the left-side controls even in rapid closure mode.
1017 availableSpace -= [self leftIndentForControls];
1018
1019 // If there are any mini tabs, account for the extra spacing between the last
1020 // mini tab and the first regular tab.
1021 if ([self numberOfOpenMiniTabs])
1022 availableSpace -= kLastMiniTabSpacing;
1023
1024 // This may be negative, but that's okay (taken care of by |MAX()| when
1025 // calculating tab sizes). "mini" tabs in horizontal mode just get a special
1026 // section, they don't change size.
1027 CGFloat availableSpaceForNonMini = availableSpace;
1028 availableSpaceForNonMini -=
1029 [self numberOfOpenMiniTabs] * (kMiniTabWidth - kTabOverlap);
1030
1031 // Initialize |nonMiniTabWidth| in case there aren't any non-mini-tabs; this
1032 // value shouldn't actually be used.
1033 CGFloat nonMiniTabWidth = kMaxTabWidth;
1034 CGFloat nonMiniTabWidthFraction = 0;
1035 const NSInteger numberOfOpenNonMiniTabs = [self numberOfOpenNonMiniTabs];
1036 if (numberOfOpenNonMiniTabs) {
1037 // Find the width of a non-mini-tab. This only applies to horizontal
1038 // mode. Add in the amount we "get back" from the tabs overlapping.
1039 availableSpaceForNonMini += (numberOfOpenNonMiniTabs - 1) * kTabOverlap;
1040
1041 // Divide up the space between the non-mini-tabs.
1042 nonMiniTabWidth = availableSpaceForNonMini / numberOfOpenNonMiniTabs;
1043
1044 // Clamp the width between the max and min.
1045 nonMiniTabWidth = MAX(MIN(nonMiniTabWidth, kMaxTabWidth), kMinTabWidth);
1046
1047 // Separate integral and fractional parts.
1048 CGFloat integralPart = std::floor(nonMiniTabWidth);
1049 nonMiniTabWidthFraction = nonMiniTabWidth - integralPart;
1050 nonMiniTabWidth = integralPart;
1051 }
1052
1053 BOOL visible = [[tabStripView_ window] isVisible];
1054
1055 CGFloat offset = [self leftIndentForControls];
1056 bool hasPlaceholderGap = false;
1057 // Whether or not the last tab processed by the loop was a mini tab.
1058 BOOL isLastTabMini = NO;
1059 CGFloat tabWidthAccumulatedFraction = 0;
1060 NSInteger laidOutNonMiniTabs = 0;
1061
1062 // Remove all the tooltip rects on the tab strip so that we can re-apply
1063 // them to correspond with the new tab positions.
1064 [tabStripView_ removeAllToolTips];
1065
1066 for (TabController* tab in tabArray_.get()) {
1067 // Ignore a tab that is going through a close animation.
1068 if ([closingControllers_ containsObject:tab])
1069 continue;
1070
1071 BOOL isPlaceholder = [[tab view] isEqual:placeholderTab_];
1072 NSRect tabFrame = [[tab view] frame];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001073 tabFrame.size.height = [[self class] defaultTabHeight];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001074 tabFrame.origin.y = 0;
1075 tabFrame.origin.x = offset;
1076
1077 // If the tab is hidden, we consider it a new tab. We make it visible
1078 // and animate it in.
1079 BOOL newTab = [[tab view] isHidden];
1080 if (newTab)
1081 [[tab view] setHidden:NO];
1082
1083 if (isPlaceholder) {
1084 // Move the current tab to the correct location instantly.
1085 // We need a duration or else it doesn't cancel an inflight animation.
1086 ScopedNSAnimationContextGroup localAnimationGroup(animate);
1087 localAnimationGroup.SetCurrentContextShortestDuration();
1088 tabFrame.origin.x = placeholderFrame_.origin.x;
1089 id target = animate ? [[tab view] animator] : [tab view];
1090 [target setFrame:tabFrame];
1091
1092 // Store the frame by identifier to avoid redundant calls to animator.
1093 NSValue* identifier = [NSValue valueWithPointer:[tab view]];
1094 [targetFrames_ setObject:[NSValue valueWithRect:tabFrame]
1095 forKey:identifier];
1096 continue;
1097 }
1098
1099 if (placeholderTab_ && !hasPlaceholderGap) {
1100 const CGFloat placeholderMin = NSMinX(placeholderFrame_);
1101 // If the left edge is to the left of the placeholder's left, but the
1102 // mid is to the right of it slide over to make space for it.
1103 if (NSMidX(tabFrame) > placeholderMin) {
1104 hasPlaceholderGap = true;
1105 offset += NSWidth(placeholderFrame_);
1106 offset -= kTabOverlap;
1107 tabFrame.origin.x = offset;
1108 }
1109 }
1110
1111 // Set the width. Selected tabs are slightly wider when things get really
1112 // small and thus we enforce a different minimum width.
1113 BOOL isMini = [tab mini];
1114 if (isMini) {
1115 tabFrame.size.width = [tab app] ? kAppTabWidth : kMiniTabWidth;
1116 } else {
1117 // Tabs have non-integer widths. Assign the integer part to the tab, and
1118 // keep an accumulation of the fractional parts. When the fractional
1119 // accumulation gets to be more than one pixel, assign that to the current
1120 // tab being laid out. This is vaguely inspired by Bresenham's line
1121 // algorithm.
1122 tabFrame.size.width = nonMiniTabWidth;
1123 tabWidthAccumulatedFraction += nonMiniTabWidthFraction;
1124
1125 if (tabWidthAccumulatedFraction >= 1.0) {
1126 ++tabFrame.size.width;
1127 --tabWidthAccumulatedFraction;
1128 }
1129
1130 // In case of rounding error, give any left over pixels to the last tab.
1131 if (laidOutNonMiniTabs == numberOfOpenNonMiniTabs - 1 &&
1132 tabWidthAccumulatedFraction > 0.5) {
1133 ++tabFrame.size.width;
1134 }
1135
1136 ++laidOutNonMiniTabs;
1137 }
1138
1139 if ([tab selected])
1140 tabFrame.size.width = MAX(tabFrame.size.width, kMinSelectedTabWidth);
1141
1142 // If this is the first non-mini tab, then add a bit of spacing between this
1143 // and the last mini tab.
1144 if (!isMini && isLastTabMini) {
1145 offset += kLastMiniTabSpacing;
1146 tabFrame.origin.x = offset;
1147 }
1148 isLastTabMini = isMini;
1149
1150 // Animate a new tab in by putting it below the horizon unless told to put
1151 // it in a specific location (i.e., from a drop).
1152 if (newTab && visible && animate) {
1153 if (NSEqualRects(droppedTabFrame_, NSZeroRect)) {
1154 [[tab view] setFrame:NSOffsetRect(tabFrame, 0, -NSHeight(tabFrame))];
1155 } else {
1156 [[tab view] setFrame:droppedTabFrame_];
1157 droppedTabFrame_ = NSZeroRect;
1158 }
1159 }
1160
1161 // Check the frame by identifier to avoid redundant calls to animator.
1162 id frameTarget = visible && animate ? [[tab view] animator] : [tab view];
1163 NSValue* identifier = [NSValue valueWithPointer:[tab view]];
1164 NSValue* oldTargetValue = [targetFrames_ objectForKey:identifier];
1165 if (!oldTargetValue ||
1166 !NSEqualRects([oldTargetValue rectValue], tabFrame)) {
1167 [frameTarget setFrame:tabFrame];
1168 [targetFrames_ setObject:[NSValue valueWithRect:tabFrame]
1169 forKey:identifier];
1170 }
1171
1172 enclosingRect = NSUnionRect(tabFrame, enclosingRect);
1173
1174 offset += NSWidth(tabFrame);
1175 offset -= kTabOverlap;
1176
1177 // Create a rect which starts at the point where the tab overlap will end so
1178 // that as the mouse cursor crosses over the boundary it will get updated.
1179 // The inset is based on a multiplier of the height.
1180 float insetWidth = NSHeight(tabFrame) * [TabView insetMultiplier];
1181 // NSInsetRect will also expose the "insetWidth" at the right of the tab.
1182 NSRect tabToolTipRect = NSInsetRect(tabFrame, insetWidth, 0);
1183 [tabStripView_ addToolTipRect:tabToolTipRect owner:self userData:nil];
1184
1185 // Also create two more rects in the remaining space so that the tooltip
1186 // is more likely to get updated crossing tabs.
1187 // These rects "cover" the right edge of the previous tab that was exposed
1188 // since the tabs overlap.
1189 tabToolTipRect = tabFrame;
1190 tabToolTipRect.size.width = insetWidth / 2.0;
1191 [tabStripView_ addToolTipRect:tabToolTipRect owner:self userData:nil];
1192
1193 tabToolTipRect = NSOffsetRect(tabToolTipRect, insetWidth / 2.0, 0);
1194 [tabStripView_ addToolTipRect:tabToolTipRect owner:self userData:nil];
1195 }
1196
1197 // Hide the new tab button if we're explicitly told to. It may already
1198 // be hidden, doing it again doesn't hurt. Otherwise position it
1199 // appropriately, showing it if necessary.
1200 if (forceNewTabButtonHidden_) {
1201 [newTabButton_ setHidden:YES];
1202 } else {
1203 NSRect newTabNewFrame = [newTabButton_ frame];
1204 // We've already ensured there's enough space for the new tab button
1205 // so we don't have to check it against the available space. We do need
1206 // to make sure we put it after any placeholder.
1207 CGFloat maxTabX = MAX(offset, NSMaxX(placeholderFrame_) - kTabOverlap);
1208 newTabNewFrame.origin = NSMakePoint(maxTabX + kNewTabButtonOffset, 0);
1209 if ([tabContentsArray_ count])
1210 [newTabButton_ setHidden:NO];
1211
1212 if (!NSEqualRects(newTabTargetFrame_, newTabNewFrame)) {
1213 // Set the new tab button image correctly based on where the cursor is.
1214 NSWindow* window = [tabStripView_ window];
1215 NSPoint currentMouse = [window mouseLocationOutsideOfEventStream];
1216 currentMouse = [tabStripView_ convertPoint:currentMouse fromView:nil];
1217
1218 BOOL shouldShowHover = [newTabButton_ pointIsOverButton:currentMouse];
1219 [self setNewTabButtonHoverState:shouldShowHover];
1220
1221 // Move the new tab button into place. We want to animate the new tab
1222 // button if it's moving to the left (closing a tab), but not when it's
1223 // moving to the right (inserting a new tab). If moving right, we need
1224 // to use a very small duration to make sure we cancel any in-flight
1225 // animation to the left.
1226 if (visible && animate) {
1227 ScopedNSAnimationContextGroup localAnimationGroup(true);
1228 BOOL movingLeft = NSMinX(newTabNewFrame) < NSMinX(newTabTargetFrame_);
1229 if (!movingLeft) {
1230 localAnimationGroup.SetCurrentContextShortestDuration();
1231 }
1232 [[newTabButton_ animator] setFrame:newTabNewFrame];
1233 newTabTargetFrame_ = newTabNewFrame;
1234 } else {
1235 [newTabButton_ setFrame:newTabNewFrame];
1236 newTabTargetFrame_ = newTabNewFrame;
1237 }
1238 }
1239 }
1240
1241 [dragBlockingView_ setFrame:enclosingRect];
1242
1243 // Add a catch-all tooltip rect which will handle any remaining tab strip
1244 // region not covered by tab-specific rects.
1245 [tabStripView_ addToolTipRect:enclosingRect owner:self userData:nil];
1246
1247 // Mark that we've successfully completed layout of at least one tab.
1248 initialLayoutComplete_ = YES;
1249}
1250
1251// Return the current hovered tab's tooltip when requested by the tooltip
1252// manager.
1253- (NSString*) view:(NSView*)view
1254 stringForToolTip:(NSToolTipTag)tag
1255 point:(NSPoint)point
1256 userData:(void*)data {
1257 return [hoveredTab_ toolTipText];
1258}
1259
1260// When we're told to layout from the public API we usually want to animate,
1261// except when it's the first time.
1262- (void)layoutTabs {
1263 [self layoutTabsWithAnimation:initialLayoutComplete_ regenerateSubviews:YES];
1264}
1265
1266- (void)layoutTabsWithoutAnimation {
1267 [self layoutTabsWithAnimation:NO regenerateSubviews:YES];
1268}
1269
1270// Handles setting the title of the tab based on the given |contents|. Uses
1271// a canned string if |contents| is NULL.
1272- (void)setTabTitle:(NSViewController*)tab withContents:(WebContents*)contents {
1273 NSString* titleString = nil;
1274 if (contents)
1275 titleString = base::SysUTF16ToNSString(contents->GetTitle());
1276 if (![titleString length]) {
1277 titleString = l10n_util::GetNSString(IDS_BROWSER_WINDOW_MAC_TAB_UNTITLED);
1278 }
1279 [tab setTitle:titleString];
1280}
1281
1282// Called when a notification is received from the model to insert a new tab
1283// at |modelIndex|.
1284- (void)insertTabWithContents:(content::WebContents*)contents
1285 atIndex:(NSInteger)modelIndex
1286 inForeground:(bool)inForeground {
1287 DCHECK(contents);
1288 DCHECK(modelIndex == TabStripModel::kNoTab ||
1289 tabStripModel_->ContainsIndex(modelIndex));
1290
1291 // Cancel any pending tab transition.
1292 hoverTabSelector_->CancelTabTransition();
1293
1294 // Take closing tabs into account.
1295 NSInteger index = [self indexFromModelIndex:modelIndex];
1296
1297 // Make a new tab. Load the contents of this tab from the nib and associate
1298 // the new controller with |contents| so it can be looked up later.
Ben Murdocheb525c52013-07-10 11:40:50 +01001299 base::scoped_nsobject<TabContentsController> contentsController(
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001300 [[TabContentsController alloc] initWithContents:contents]);
1301 [tabContentsArray_ insertObject:contentsController atIndex:index];
1302
1303 // Make a new tab and add it to the strip. Keep track of its controller.
1304 TabController* newController = [self newTab];
1305 [newController setMini:tabStripModel_->IsMiniTab(modelIndex)];
1306 [newController setPinned:tabStripModel_->IsTabPinned(modelIndex)];
1307 [newController setApp:tabStripModel_->IsAppTab(modelIndex)];
1308 [newController setUrl:contents->GetURL()];
1309 [tabArray_ insertObject:newController atIndex:index];
1310 NSView* newView = [newController view];
1311
1312 // Set the originating frame to just below the strip so that it animates
1313 // upwards as it's being initially layed out. Oddly, this works while doing
1314 // something similar in |-layoutTabs| confuses the window server.
1315 [newView setFrame:NSOffsetRect([newView frame],
1316 0, -[[self class] defaultTabHeight])];
1317
1318 [self setTabTitle:newController withContents:contents];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001319 [newController setProjecting:chrome::ShouldShowProjectingIndicator(contents)];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001320
1321 // If a tab is being inserted, we can again use the entire tab strip width
1322 // for layout.
1323 availableResizeWidth_ = kUseFullAvailableWidth;
1324
1325 // We don't need to call |-layoutTabs| if the tab will be in the foreground
1326 // because it will get called when the new tab is selected by the tab model.
1327 // Whenever |-layoutTabs| is called, it'll also add the new subview.
1328 if (!inForeground) {
1329 [self layoutTabs];
1330 }
1331
1332 // During normal loading, we won't yet have a favicon and we'll get
1333 // subsequent state change notifications to show the throbber, but when we're
1334 // dragging a tab out into a new window, we have to put the tab's favicon
1335 // into the right state up front as we won't be told to do it from anywhere
1336 // else.
1337 [self updateFaviconForContents:contents atIndex:modelIndex];
1338}
1339
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001340// Called before |contents| is deactivated.
1341- (void)tabDeactivatedWithContents:(content::WebContents*)contents {
1342 contents->GetView()->StoreFocus();
1343}
1344
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001345// Called when a notification is received from the model to select a particular
1346// tab. Swaps in the toolbar and content area associated with |newContents|.
1347- (void)activateTabWithContents:(content::WebContents*)newContents
1348 previousContents:(content::WebContents*)oldContents
1349 atIndex:(NSInteger)modelIndex
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001350 reason:(int)reason {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001351 // Take closing tabs into account.
1352 NSInteger activeIndex = [self indexFromModelIndex:modelIndex];
1353
1354 if (oldContents) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001355 int oldModelIndex =
1356 browser_->tab_strip_model()->GetIndexOfWebContents(oldContents);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001357 if (oldModelIndex != -1) { // When closing a tab, the old tab may be gone.
1358 NSInteger oldIndex = [self indexFromModelIndex:oldModelIndex];
1359 TabContentsController* oldController =
1360 [tabContentsArray_ objectAtIndex:oldIndex];
1361 [oldController willBecomeUnselectedTab];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001362 oldContents->WasHidden();
1363 }
1364 }
1365
1366 // First get the vector of indices, which is allays sorted in ascending order.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001367 ui::ListSelectionModel::SelectedIndices selection(
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001368 tabStripModel_->selection_model().selected_indices());
1369 // Iterate through all of the tabs, selecting each as necessary.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001370 ui::ListSelectionModel::SelectedIndices::iterator iter = selection.begin();
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001371 int i = 0;
1372 for (TabController* current in tabArray_.get()) {
1373 BOOL selected = iter != selection.end() &&
1374 [self indexFromModelIndex:*iter] == i;
1375 [current setSelected:selected];
1376 [current setActive:i == activeIndex];
1377 if (selected)
1378 ++iter;
1379 ++i;
1380 }
1381
1382 // Tell the new tab contents it is about to become the selected tab. Here it
1383 // can do things like make sure the toolbar is up to date.
1384 TabContentsController* newController =
1385 [tabContentsArray_ objectAtIndex:activeIndex];
1386 [newController willBecomeSelectedTab];
1387
1388 // Relayout for new tabs and to let the selected tab grow to be larger in
1389 // size than surrounding tabs if the user has many. This also raises the
1390 // selected tab to the top.
1391 [self layoutTabs];
1392
1393 // Swap in the contents for the new tab.
1394 [self swapInTabAtIndex:modelIndex];
1395
1396 if (newContents) {
1397 newContents->WasShown();
1398 newContents->GetView()->RestoreFocus();
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001399 }
1400}
1401
1402- (void)tabReplacedWithContents:(content::WebContents*)newContents
1403 previousContents:(content::WebContents*)oldContents
1404 atIndex:(NSInteger)modelIndex {
1405 NSInteger index = [self indexFromModelIndex:modelIndex];
1406 TabContentsController* oldController =
1407 [tabContentsArray_ objectAtIndex:index];
1408 DCHECK_EQ(oldContents, [oldController webContents]);
1409
1410 // Simply create a new TabContentsController for |newContents| and place it
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001411 // into the array, replacing |oldContents|. An ActiveTabChanged notification
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001412 // will follow, at which point we will install the new view.
Ben Murdocheb525c52013-07-10 11:40:50 +01001413 base::scoped_nsobject<TabContentsController> newController(
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001414 [[TabContentsController alloc] initWithContents:newContents]);
1415
1416 // Bye bye, |oldController|.
1417 [tabContentsArray_ replaceObjectAtIndex:index withObject:newController];
1418
1419 // Fake a tab changed notification to force tab titles and favicons to update.
1420 [self tabChangedWithContents:newContents
1421 atIndex:modelIndex
1422 changeType:TabStripModelObserver::ALL];
1423}
1424
1425// Remove all knowledge about this tab and its associated controller, and remove
1426// the view from the strip.
1427- (void)removeTab:(TabController*)controller {
1428 // Cancel any pending tab transition.
1429 hoverTabSelector_->CancelTabTransition();
1430
1431 NSUInteger index = [tabArray_ indexOfObject:controller];
1432
1433 // Release the tab contents controller so those views get destroyed. This
1434 // will remove all the tab content Cocoa views from the hierarchy. A
1435 // subsequent "select tab" notification will follow from the model. To
1436 // tell us what to swap in in its absence.
1437 [tabContentsArray_ removeObjectAtIndex:index];
1438
1439 // Remove the view from the tab strip.
1440 NSView* tab = [controller view];
1441 [tab removeFromSuperview];
1442
1443 // Remove ourself as an observer.
1444 [[NSNotificationCenter defaultCenter]
1445 removeObserver:self
1446 name:NSViewDidUpdateTrackingAreasNotification
1447 object:tab];
1448
1449 // Clear the tab controller's target.
1450 // TODO(viettrungluu): [crbug.com/23829] Find a better way to handle the tab
1451 // controller's target.
1452 [controller setTarget:nil];
1453
1454 if ([hoveredTab_ isEqual:tab])
1455 hoveredTab_ = nil;
1456
1457 NSValue* identifier = [NSValue valueWithPointer:tab];
1458 [targetFrames_ removeObjectForKey:identifier];
1459
1460 // Once we're totally done with the tab, delete its controller
1461 [tabArray_ removeObjectAtIndex:index];
1462}
1463
1464// Called by the CAAnimation delegate when the tab completes the closing
1465// animation.
1466- (void)animationDidStopForController:(TabController*)controller
1467 finished:(BOOL)finished {
1468 [closingControllers_ removeObject:controller];
1469 [self removeTab:controller];
1470}
1471
1472// Save off which TabController is closing and tell its view's animator
1473// where to move the tab to. Registers a delegate to call back when the
1474// animation is complete in order to remove the tab from the model.
1475- (void)startClosingTabWithAnimation:(TabController*)closingTab {
1476 DCHECK([NSThread isMainThread]);
1477
1478 // Cancel any pending tab transition.
1479 hoverTabSelector_->CancelTabTransition();
1480
1481 // Save off the controller into the set of animating tabs. This alerts
1482 // the layout method to not do anything with it and allows us to correctly
1483 // calculate offsets when working with indices into the model.
1484 [closingControllers_ addObject:closingTab];
1485
1486 // Mark the tab as closing. This prevents it from generating any drags or
1487 // selections while it's animating closed.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001488 [[closingTab tabView] setClosing:YES];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001489
1490 // Register delegate (owned by the animation system).
1491 NSView* tabView = [closingTab view];
1492 CAAnimation* animation = [[tabView animationForKey:@"frameOrigin"] copy];
1493 [animation autorelease];
Ben Murdocheb525c52013-07-10 11:40:50 +01001494 base::scoped_nsobject<TabCloseAnimationDelegate> delegate(
1495 [[TabCloseAnimationDelegate alloc] initWithTabStrip:self
1496 tabController:closingTab]);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001497 [animation setDelegate:delegate.get()]; // Retains delegate.
1498 NSMutableDictionary* animationDictionary =
1499 [NSMutableDictionary dictionaryWithDictionary:[tabView animations]];
1500 [animationDictionary setObject:animation forKey:@"frameOrigin"];
1501 [tabView setAnimations:animationDictionary];
1502
1503 // Periscope down! Animate the tab.
1504 NSRect newFrame = [tabView frame];
1505 newFrame = NSOffsetRect(newFrame, 0, -newFrame.size.height);
1506 ScopedNSAnimationContextGroup animationGroup(true);
1507 animationGroup.SetCurrentContextDuration(kAnimationDuration);
1508 [[tabView animator] setFrame:newFrame];
1509}
1510
1511// Called when a notification is received from the model that the given tab
1512// has gone away. Start an animation then force a layout to put everything
1513// in motion.
1514- (void)tabDetachedWithContents:(content::WebContents*)contents
1515 atIndex:(NSInteger)modelIndex {
1516 // Take closing tabs into account.
1517 NSInteger index = [self indexFromModelIndex:modelIndex];
1518
1519 // Cancel any pending tab transition.
1520 hoverTabSelector_->CancelTabTransition();
1521
1522 TabController* tab = [tabArray_ objectAtIndex:index];
1523 if (tabStripModel_->count() > 0) {
1524 [self startClosingTabWithAnimation:tab];
1525 [self layoutTabs];
1526 } else {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001527 // Don't remove the tab, as that makes the window look jarring without any
1528 // tabs. Instead, simply mark it as closing to prevent the tab from
1529 // generating any drags or selections.
1530 [[tab tabView] setClosing:YES];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001531 }
1532
1533 [delegate_ onTabDetachedWithContents:contents];
1534}
1535
1536// A helper routine for creating an NSImageView to hold the favicon or app icon
1537// for |contents|.
1538- (NSImageView*)iconImageViewForContents:(content::WebContents*)contents {
1539 extensions::TabHelper* extensions_tab_helper =
1540 extensions::TabHelper::FromWebContents(contents);
1541 BOOL isApp = extensions_tab_helper->is_app();
1542 NSImage* image = nil;
1543 // Favicons come from the renderer, and the renderer draws everything in the
1544 // system color space.
1545 CGColorSpaceRef colorSpace = base::mac::GetSystemColorSpace();
1546 if (isApp) {
1547 SkBitmap* icon = extensions_tab_helper->GetExtensionAppIcon();
1548 if (icon)
1549 image = gfx::SkBitmapToNSImageWithColorSpace(*icon, colorSpace);
1550 } else {
1551 image = mac::FaviconForWebContents(contents);
1552 }
1553
1554 // Either we don't have a valid favicon or there was some issue converting it
1555 // from an SkBitmap. Either way, just show the default.
1556 if (!image)
1557 image = defaultFavicon_.get();
1558 NSRect frame = NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight);
1559 NSImageView* view = [[[NSImageView alloc] initWithFrame:frame] autorelease];
1560 [view setImage:image];
1561 return view;
1562}
1563
1564// Updates the current loading state, replacing the icon view with a favicon,
1565// a throbber, the default icon, or nothing at all.
1566- (void)updateFaviconForContents:(content::WebContents*)contents
1567 atIndex:(NSInteger)modelIndex {
1568 if (!contents)
1569 return;
1570
1571 static NSImage* throbberWaitingImage =
1572 ResourceBundle::GetSharedInstance().GetNativeImageNamed(
1573 IDR_THROBBER_WAITING).CopyNSImage();
1574 static NSImage* throbberLoadingImage =
1575 ResourceBundle::GetSharedInstance().GetNativeImageNamed(
1576 IDR_THROBBER).CopyNSImage();
1577 static NSImage* sadFaviconImage =
1578 ResourceBundle::GetSharedInstance().GetNativeImageNamed(
1579 IDR_SAD_FAVICON).CopyNSImage();
1580
1581 // Take closing tabs into account.
1582 NSInteger index = [self indexFromModelIndex:modelIndex];
1583 TabController* tabController = [tabArray_ objectAtIndex:index];
1584
1585 FaviconTabHelper* favicon_tab_helper =
1586 FaviconTabHelper::FromWebContents(contents);
1587 bool oldHasIcon = [tabController iconView] != nil;
1588 bool newHasIcon = favicon_tab_helper->ShouldDisplayFavicon() ||
1589 tabStripModel_->IsMiniTab(modelIndex); // Always show icon if mini.
1590
1591 TabLoadingState oldState = [tabController loadingState];
1592 TabLoadingState newState = kTabDone;
1593 NSImage* throbberImage = nil;
1594 if (contents->IsCrashed()) {
1595 newState = kTabCrashed;
1596 newHasIcon = true;
1597 } else if (contents->IsWaitingForResponse()) {
1598 newState = kTabWaiting;
1599 throbberImage = throbberWaitingImage;
1600 } else if (contents->IsLoading()) {
1601 newState = kTabLoading;
1602 throbberImage = throbberLoadingImage;
1603 }
1604
1605 if (oldState != newState)
1606 [tabController setLoadingState:newState];
1607
1608 // While loading, this function is called repeatedly with the same state.
1609 // To avoid expensive unnecessary view manipulation, only make changes when
1610 // the state is actually changing. When loading is complete (kTabDone),
1611 // every call to this function is significant.
1612 if (newState == kTabDone || oldState != newState ||
1613 oldHasIcon != newHasIcon) {
1614 NSView* iconView = nil;
1615 if (newHasIcon) {
1616 if (newState == kTabDone) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001617 NSImageView* imageView = [self iconImageViewForContents:contents];
1618 TabAudioIndicatorViewMac* tabAudioIndicatorViewMac =
1619 base::mac::ObjCCast<TabAudioIndicatorViewMac>(
1620 [tabController iconView]);
1621
1622 ui::ThemeProvider* theme = [[tabStripView_ window] themeProvider];
1623 if (theme && [tabController projecting]) {
1624 NSImage* projectorGlow =
Ben Murdochbb1529c2013-08-08 10:24:53 +01001625 theme->GetNSImageNamed(IDR_TAB_CAPTURE_GLOW);
1626 NSImage* projector = theme->GetNSImageNamed(IDR_TAB_CAPTURE);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001627
1628 NSRect frame = NSMakeRect(0,
1629 0,
1630 kProjectingIconWidthAndHeight,
1631 kProjectingIconWidthAndHeight);
1632 TabProjectingImageView* projectingView =
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001633 [[[TabProjectingImageView alloc]
1634 initWithFrame:frame
1635 backgroundImage:[imageView image]
1636 projectorImage:projector
1637 throbImage:projectorGlow
1638 durationMS:kRecordingDurationMs
Ben Murdocheb525c52013-07-10 11:40:50 +01001639 animationContainer:animationContainer_.get()] autorelease];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001640
1641 iconView = projectingView;
1642 } else if (theme && chrome::ShouldShowRecordingIndicator(contents)) {
1643 // Create a masked favicon.
Ben Murdochbb1529c2013-08-08 10:24:53 +01001644 NSImage* mask = theme->GetNSImageNamed(IDR_TAB_RECORDING_MASK);
1645 NSImage* recording = theme->GetNSImageNamed(IDR_TAB_RECORDING);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001646 NSImage* favIconMasked = CreateMaskedFaviconForRecording(
1647 [imageView image], mask, recording);
1648
1649 NSRect frame =
1650 NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight);
1651 ThrobbingImageView* recordingView =
1652 [[[ThrobbingImageView alloc]
1653 initWithFrame:frame
1654 backgroundImage:favIconMasked
1655 throbImage:recording
1656 durationMS:kRecordingDurationMs
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001657 throbPosition:kThrobPositionBottomRight
Ben Murdocheb525c52013-07-10 11:40:50 +01001658 animationContainer:animationContainer_.get()] autorelease];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001659
1660 iconView = recordingView;
1661 } else if (chrome::IsPlayingAudio(contents) ||
1662 [tabAudioIndicatorViewMac isAnimating]) {
1663 if (!tabAudioIndicatorViewMac) {
1664 NSRect frame =
1665 NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight);
1666 tabAudioIndicatorViewMac = [[[TabAudioIndicatorViewMac alloc]
1667 initWithFrame:frame] autorelease];
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001668 [tabAudioIndicatorViewMac
Ben Murdocheb525c52013-07-10 11:40:50 +01001669 setAnimationContainer:animationContainer_.get()];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001670 }
1671 [tabAudioIndicatorViewMac
1672 setIsPlayingAudio:chrome::IsPlayingAudio(contents)];
1673 [tabAudioIndicatorViewMac setBackgroundImage:[imageView image]];
1674 iconView = tabAudioIndicatorViewMac;
1675 } else {
1676 iconView = imageView;
1677 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001678 } else if (newState == kTabCrashed) {
1679 NSImage* oldImage = [[self iconImageViewForContents:contents] image];
1680 NSRect frame =
1681 NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight);
1682 iconView = [ThrobberView toastThrobberViewWithFrame:frame
1683 beforeImage:oldImage
1684 afterImage:sadFaviconImage];
1685 } else {
1686 NSRect frame =
1687 NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight);
1688 iconView = [ThrobberView filmstripThrobberViewWithFrame:frame
1689 image:throbberImage];
1690 }
1691 }
1692
1693 [tabController setIconView:iconView];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001694 if (iconView && ![tabController projecting]) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001695 // See the comment above kTabOverlap for why these DCHECKs exist.
1696 DCHECK_GE(NSMinX([iconView frame]), kTabOverlap);
1697 // TODO(thakis): Ideally, this would be true too, but it's not true in
1698 // some tests.
1699 //DCHECK_LE(NSMaxX([iconView frame]),
1700 // NSWidth([[tabController view] frame]) - kTabOverlap);
1701 }
1702 }
1703}
1704
1705// Called when a notification is received from the model that the given tab
1706// has been updated. |loading| will be YES when we only want to update the
1707// throbber state, not anything else about the (partially) loading tab.
1708- (void)tabChangedWithContents:(content::WebContents*)contents
1709 atIndex:(NSInteger)modelIndex
1710 changeType:(TabStripModelObserver::TabChangeType)change {
1711 // Take closing tabs into account.
1712 NSInteger index = [self indexFromModelIndex:modelIndex];
1713
1714 if (modelIndex == tabStripModel_->active_index())
1715 [delegate_ onTabChanged:change withContents:contents];
1716
1717 if (change == TabStripModelObserver::TITLE_NOT_LOADING) {
1718 // TODO(sky): make this work.
1719 // We'll receive another notification of the change asynchronously.
1720 return;
1721 }
1722
1723 TabController* tabController = [tabArray_ objectAtIndex:index];
1724
1725 if (change != TabStripModelObserver::LOADING_ONLY)
1726 [self setTabTitle:tabController withContents:contents];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001727 [tabController setProjecting:chrome::ShouldShowProjectingIndicator(contents)];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001728
1729 [self updateFaviconForContents:contents atIndex:modelIndex];
1730
1731 TabContentsController* updatedController =
1732 [tabContentsArray_ objectAtIndex:index];
1733 [updatedController tabDidChange:contents];
1734}
1735
1736// Called when a tab is moved (usually by drag&drop). Keep our parallel arrays
1737// in sync with the tab strip model. It can also be pinned/unpinned
1738// simultaneously, so we need to take care of that.
1739- (void)tabMovedWithContents:(content::WebContents*)contents
1740 fromIndex:(NSInteger)modelFrom
1741 toIndex:(NSInteger)modelTo {
1742 // Take closing tabs into account.
1743 NSInteger from = [self indexFromModelIndex:modelFrom];
1744 NSInteger to = [self indexFromModelIndex:modelTo];
1745
1746 // Cancel any pending tab transition.
1747 hoverTabSelector_->CancelTabTransition();
1748
Ben Murdocheb525c52013-07-10 11:40:50 +01001749 base::scoped_nsobject<TabContentsController> movedTabContentsController(
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001750 [[tabContentsArray_ objectAtIndex:from] retain]);
1751 [tabContentsArray_ removeObjectAtIndex:from];
1752 [tabContentsArray_ insertObject:movedTabContentsController.get()
1753 atIndex:to];
Ben Murdocheb525c52013-07-10 11:40:50 +01001754 base::scoped_nsobject<TabController> movedTabController(
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001755 [[tabArray_ objectAtIndex:from] retain]);
1756 DCHECK([movedTabController isKindOfClass:[TabController class]]);
1757 [tabArray_ removeObjectAtIndex:from];
1758 [tabArray_ insertObject:movedTabController.get() atIndex:to];
1759
1760 // The tab moved, which means that the mini-tab state may have changed.
1761 if (tabStripModel_->IsMiniTab(modelTo) != [movedTabController mini])
1762 [self tabMiniStateChangedWithContents:contents atIndex:modelTo];
1763
1764 [self layoutTabs];
1765}
1766
1767// Called when a tab is pinned or unpinned without moving.
1768- (void)tabMiniStateChangedWithContents:(content::WebContents*)contents
1769 atIndex:(NSInteger)modelIndex {
1770 // Take closing tabs into account.
1771 NSInteger index = [self indexFromModelIndex:modelIndex];
1772
1773 TabController* tabController = [tabArray_ objectAtIndex:index];
1774 DCHECK([tabController isKindOfClass:[TabController class]]);
1775
1776 // Don't do anything if the change was already picked up by the move event.
1777 if (tabStripModel_->IsMiniTab(modelIndex) == [tabController mini])
1778 return;
1779
1780 [tabController setMini:tabStripModel_->IsMiniTab(modelIndex)];
1781 [tabController setPinned:tabStripModel_->IsTabPinned(modelIndex)];
1782 [tabController setApp:tabStripModel_->IsAppTab(modelIndex)];
1783 [tabController setUrl:contents->GetURL()];
1784 [self updateFaviconForContents:contents atIndex:modelIndex];
1785 // If the tab is being restored and it's pinned, the mini state is set after
1786 // the tab has already been rendered, so re-layout the tabstrip. In all other
1787 // cases, the state is set before the tab is rendered so this isn't needed.
1788 [self layoutTabs];
1789}
1790
1791- (void)setFrameOfActiveTab:(NSRect)frame {
1792 NSView* view = [self activeTabView];
1793 NSValue* identifier = [NSValue valueWithPointer:view];
1794 [targetFrames_ setObject:[NSValue valueWithRect:frame]
1795 forKey:identifier];
1796 [view setFrame:frame];
1797}
1798
1799- (TabStripModel*)tabStripModel {
1800 return tabStripModel_;
1801}
1802
1803- (NSView*)activeTabView {
1804 int activeIndex = tabStripModel_->active_index();
1805 // Take closing tabs into account. They can't ever be selected.
1806 activeIndex = [self indexFromModelIndex:activeIndex];
1807 return [self viewAtIndex:activeIndex];
1808}
1809
1810// Find the model index based on the x coordinate of the placeholder. If there
1811// is no placeholder, this returns the end of the tab strip. Closing tabs are
1812// not considered in computing the index.
1813- (int)indexOfPlaceholder {
1814 double placeholderX = placeholderFrame_.origin.x;
1815 int index = 0;
1816 int location = 0;
1817 // Use |tabArray_| here instead of the tab strip count in order to get the
1818 // correct index when there are closing tabs to the left of the placeholder.
1819 const int count = [tabArray_ count];
1820 while (index < count) {
1821 // Ignore closing tabs for simplicity. The only drawback of this is that
1822 // if the placeholder is placed right before one or several contiguous
1823 // currently closing tabs, the associated TabController will start at the
1824 // end of the closing tabs.
1825 if ([closingControllers_ containsObject:[tabArray_ objectAtIndex:index]]) {
1826 index++;
1827 continue;
1828 }
1829 NSView* curr = [self viewAtIndex:index];
1830 // The placeholder tab works by changing the frame of the tab being dragged
1831 // to be the bounds of the placeholder, so we need to skip it while we're
1832 // iterating, otherwise we'll end up off by one. Note This only effects
1833 // dragging to the right, not to the left.
1834 if (curr == placeholderTab_) {
1835 index++;
1836 continue;
1837 }
1838 if (placeholderX <= NSMinX([curr frame]))
1839 break;
1840 index++;
1841 location++;
1842 }
1843 return location;
1844}
1845
1846// Move the given tab at index |from| in this window to the location of the
1847// current placeholder.
1848- (void)moveTabFromIndex:(NSInteger)from {
1849 int toIndex = [self indexOfPlaceholder];
1850 // Cancel any pending tab transition.
1851 hoverTabSelector_->CancelTabTransition();
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001852 tabStripModel_->MoveWebContentsAt(from, toIndex, true);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001853}
1854
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001855// Drop a given WebContents at the location of the current placeholder.
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001856// If there is no placeholder, it will go at the end. Used when dragging from
1857// another window when we don't have access to the WebContents as part of our
1858// strip. |frame| is in the coordinate system of the tab strip view and
1859// represents where the user dropped the new tab so it can be animated into its
1860// correct location when the tab is added to the model. If the tab was pinned in
1861// its previous window, setting |pinned| to YES will propagate that state to the
1862// new window. Mini-tabs are either app or pinned tabs; the app state is stored
1863// by the |contents|, but the |pinned| state is the caller's responsibility.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001864- (void)dropWebContents:(WebContents*)contents
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001865 withFrame:(NSRect)frame
1866 asPinnedTab:(BOOL)pinned {
1867 int modelIndex = [self indexOfPlaceholder];
1868
1869 // Mark that the new tab being created should start at |frame|. It will be
1870 // reset as soon as the tab has been positioned.
1871 droppedTabFrame_ = frame;
1872
1873 // Insert it into this tab strip. We want it in the foreground and to not
1874 // inherit the current tab's group.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001875 tabStripModel_->InsertWebContentsAt(
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001876 modelIndex, contents,
1877 TabStripModel::ADD_ACTIVE | (pinned ? TabStripModel::ADD_PINNED : 0));
1878}
1879
1880// Called when the tab strip view changes size. As we only registered for
1881// changes on our view, we know it's only for our view. Layout w/out
1882// animations since they are blocked by the resize nested runloop. We need
1883// the views to adjust immediately. Neither the tabs nor their z-order are
1884// changed, so we don't need to update the subviews.
1885- (void)tabViewFrameChanged:(NSNotification*)info {
1886 [self layoutTabsWithAnimation:NO regenerateSubviews:NO];
1887}
1888
1889// Called when the tracking areas for any given tab are updated. This allows
1890// the individual tabs to update their hover states correctly.
1891// Only generates the event if the cursor is in the tab strip.
1892- (void)tabUpdateTracking:(NSNotification*)notification {
1893 DCHECK([[notification object] isKindOfClass:[TabView class]]);
1894 DCHECK(mouseInside_);
1895 NSWindow* window = [tabStripView_ window];
1896 NSPoint location = [window mouseLocationOutsideOfEventStream];
1897 if (NSPointInRect(location, [tabStripView_ frame])) {
1898 NSEvent* mouseEvent = [NSEvent mouseEventWithType:NSMouseMoved
1899 location:location
1900 modifierFlags:0
1901 timestamp:0
1902 windowNumber:[window windowNumber]
1903 context:nil
1904 eventNumber:0
1905 clickCount:0
1906 pressure:0];
1907 [self mouseMoved:mouseEvent];
1908 }
1909}
1910
1911- (BOOL)inRapidClosureMode {
1912 return availableResizeWidth_ != kUseFullAvailableWidth;
1913}
1914
1915// Disable tab dragging when there are any pending animations.
1916- (BOOL)tabDraggingAllowed {
1917 return [closingControllers_ count] == 0;
1918}
1919
1920- (void)mouseMoved:(NSEvent*)event {
1921 // Use hit test to figure out what view we are hovering over.
1922 NSView* targetView = [tabStripView_ hitTest:[event locationInWindow]];
1923
1924 // Set the new tab button hover state iff the mouse is over the button.
1925 BOOL shouldShowHoverImage = [targetView isKindOfClass:[NewTabButton class]];
1926 [self setNewTabButtonHoverState:shouldShowHoverImage];
1927
1928 TabView* tabView = (TabView*)targetView;
1929 if (![tabView isKindOfClass:[TabView class]]) {
1930 if ([[tabView superview] isKindOfClass:[TabView class]]) {
1931 tabView = (TabView*)[targetView superview];
1932 } else {
1933 tabView = nil;
1934 }
1935 }
1936
1937 if (hoveredTab_ != tabView) {
1938 [hoveredTab_ mouseExited:nil]; // We don't pass event because moved events
1939 [tabView mouseEntered:nil]; // don't have valid tracking areas
1940 hoveredTab_ = tabView;
1941 } else {
1942 [hoveredTab_ mouseMoved:event];
1943 }
1944}
1945
1946- (void)mouseEntered:(NSEvent*)event {
1947 NSTrackingArea* area = [event trackingArea];
1948 if ([area isEqual:trackingArea_]) {
1949 mouseInside_ = YES;
1950 [self setTabTrackingAreasEnabled:YES];
1951 [self mouseMoved:event];
1952 }
1953}
1954
1955// Called when the tracking area is in effect which means we're tracking to
1956// see if the user leaves the tab strip with their mouse. When they do,
1957// reset layout to use all available width.
1958- (void)mouseExited:(NSEvent*)event {
1959 NSTrackingArea* area = [event trackingArea];
1960 if ([area isEqual:trackingArea_]) {
1961 mouseInside_ = NO;
1962 [self setTabTrackingAreasEnabled:NO];
1963 availableResizeWidth_ = kUseFullAvailableWidth;
1964 [hoveredTab_ mouseExited:event];
1965 hoveredTab_ = nil;
1966 [self layoutTabs];
1967 } else if ([area isEqual:newTabTrackingArea_]) {
1968 // If the mouse is moved quickly enough, it is possible for the mouse to
1969 // leave the tabstrip without sending any mouseMoved: messages at all.
1970 // Since this would result in the new tab button incorrectly staying in the
1971 // hover state, disable the hover image on every mouse exit.
1972 [self setNewTabButtonHoverState:NO];
1973 }
1974}
1975
1976// Enable/Disable the tracking areas for the tabs. They are only enabled
1977// when the mouse is in the tabstrip.
1978- (void)setTabTrackingAreasEnabled:(BOOL)enabled {
1979 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
1980 for (TabController* controller in tabArray_.get()) {
1981 TabView* tabView = [controller tabView];
1982 if (enabled) {
1983 // Set self up to observe tabs so hover states will be correct.
1984 [defaultCenter addObserver:self
1985 selector:@selector(tabUpdateTracking:)
1986 name:NSViewDidUpdateTrackingAreasNotification
1987 object:tabView];
1988 } else {
1989 [defaultCenter removeObserver:self
1990 name:NSViewDidUpdateTrackingAreasNotification
1991 object:tabView];
1992 }
1993 [tabView setTrackingEnabled:enabled];
1994 }
1995}
1996
1997// Sets the new tab button's image based on the current hover state. Does
1998// nothing if the hover state is already correct.
1999- (void)setNewTabButtonHoverState:(BOOL)shouldShowHover {
2000 if (shouldShowHover && !newTabButtonShowingHoverImage_) {
2001 newTabButtonShowingHoverImage_ = YES;
2002 [[newTabButton_ cell] setIsMouseInside:YES];
2003 } else if (!shouldShowHover && newTabButtonShowingHoverImage_) {
2004 newTabButtonShowingHoverImage_ = NO;
2005 [[newTabButton_ cell] setIsMouseInside:NO];
2006 }
2007}
2008
2009// Adds the given subview to (the end of) the list of permanent subviews
2010// (specified from bottom up). These subviews will always be below the
2011// transitory subviews (tabs). |-regenerateSubviewList| must be called to
2012// effectuate the addition.
2013- (void)addSubviewToPermanentList:(NSView*)aView {
2014 if (aView)
2015 [permanentSubviews_ addObject:aView];
2016}
2017
2018// Update the subviews, keeping the permanent ones (or, more correctly, putting
2019// in the ones listed in permanentSubviews_), and putting in the current tabs in
2020// the correct z-order. Any current subviews which is neither in the permanent
2021// list nor a (current) tab will be removed. So if you add such a subview, you
2022// should call |-addSubviewToPermanentList:| (or better yet, call that and then
2023// |-regenerateSubviewList| to actually add it).
2024- (void)regenerateSubviewList {
2025 // Remove self as an observer from all the old tabs before a new set of
2026 // potentially different tabs is put in place.
2027 [self setTabTrackingAreasEnabled:NO];
2028
2029 // Subviews to put in (in bottom-to-top order), beginning with the permanent
2030 // ones.
2031 NSMutableArray* subviews = [NSMutableArray arrayWithArray:permanentSubviews_];
2032
2033 NSView* activeTabView = nil;
2034 // Go through tabs in reverse order, since |subviews| is bottom-to-top.
2035 for (TabController* tab in [tabArray_ reverseObjectEnumerator]) {
2036 NSView* tabView = [tab view];
2037 if ([tab active]) {
2038 DCHECK(!activeTabView);
2039 activeTabView = tabView;
2040 } else {
2041 [subviews addObject:tabView];
2042 }
2043 }
2044 if (activeTabView) {
2045 [subviews addObject:activeTabView];
2046 }
Ben Murdocheb525c52013-07-10 11:40:50 +01002047 WithNoAnimation noAnimation;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002048 [tabStripView_ setSubviews:subviews];
2049 [self setTabTrackingAreasEnabled:mouseInside_];
2050}
2051
2052// Get the index and disposition for a potential URL(s) drop given a point (in
2053// the |TabStripView|'s coordinates). It considers only the x-coordinate of the
2054// given point. If it's in the "middle" of a tab, it drops on that tab. If it's
2055// to the left, it inserts to the left, and similarly for the right.
2056- (void)droppingURLsAt:(NSPoint)point
2057 givesIndex:(NSInteger*)index
2058 disposition:(WindowOpenDisposition*)disposition {
2059 // Proportion of the tab which is considered the "middle" (and causes things
2060 // to drop on that tab).
2061 const double kMiddleProportion = 0.5;
2062 const double kLRProportion = (1.0 - kMiddleProportion) / 2.0;
2063
2064 DCHECK(index && disposition);
2065 NSInteger i = 0;
2066 for (TabController* tab in tabArray_.get()) {
2067 NSView* view = [tab view];
2068 DCHECK([view isKindOfClass:[TabView class]]);
2069
2070 // Recall that |-[NSView frame]| is in its superview's coordinates, so a
2071 // |TabView|'s frame is in the coordinates of the |TabStripView| (which
2072 // matches the coordinate system of |point|).
2073 NSRect frame = [view frame];
2074
2075 // Modify the frame to make it "unoverlapped".
2076 frame.origin.x += kTabOverlap / 2.0;
2077 frame.size.width -= kTabOverlap;
2078 if (frame.size.width < 1.0)
2079 frame.size.width = 1.0; // try to avoid complete failure
2080
2081 // Drop in a new tab to the left of tab |i|?
2082 if (point.x < (frame.origin.x + kLRProportion * frame.size.width)) {
2083 *index = i;
2084 *disposition = NEW_FOREGROUND_TAB;
2085 return;
2086 }
2087
2088 // Drop on tab |i|?
2089 if (point.x <= (frame.origin.x +
2090 (1.0 - kLRProportion) * frame.size.width)) {
2091 *index = i;
2092 *disposition = CURRENT_TAB;
2093 return;
2094 }
2095
2096 // (Dropping in a new tab to the right of tab |i| will be taken care of in
2097 // the next iteration.)
2098 i++;
2099 }
2100
2101 // If we've made it here, we want to append a new tab to the end.
2102 *index = -1;
2103 *disposition = NEW_FOREGROUND_TAB;
2104}
2105
2106- (void)openURL:(GURL*)url inView:(NSView*)view at:(NSPoint)point {
2107 // Get the index and disposition.
2108 NSInteger index;
2109 WindowOpenDisposition disposition;
2110 [self droppingURLsAt:point
2111 givesIndex:&index
2112 disposition:&disposition];
2113
2114 // Either insert a new tab or open in a current tab.
2115 switch (disposition) {
2116 case NEW_FOREGROUND_TAB: {
2117 content::RecordAction(UserMetricsAction("Tab_DropURLBetweenTabs"));
2118 chrome::NavigateParams params(browser_, *url,
2119 content::PAGE_TRANSITION_TYPED);
2120 params.disposition = disposition;
2121 params.tabstrip_index = index;
2122 params.tabstrip_add_types =
2123 TabStripModel::ADD_ACTIVE | TabStripModel::ADD_FORCE_INDEX;
2124 chrome::Navigate(&params);
2125 break;
2126 }
2127 case CURRENT_TAB: {
2128 content::RecordAction(UserMetricsAction("Tab_DropURLOnTab"));
2129 OpenURLParams params(
2130 *url, Referrer(), CURRENT_TAB, content::PAGE_TRANSITION_TYPED, false);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002131 tabStripModel_->GetWebContentsAt(index)->OpenURL(params);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002132 tabStripModel_->ActivateTabAt(index, true);
2133 break;
2134 }
2135 default:
2136 NOTIMPLEMENTED();
2137 }
2138}
2139
2140// (URLDropTargetController protocol)
2141- (void)dropURLs:(NSArray*)urls inView:(NSView*)view at:(NSPoint)point {
2142 DCHECK_EQ(view, tabStripView_.get());
2143
2144 if ([urls count] < 1) {
2145 NOTREACHED();
2146 return;
2147 }
2148
2149 //TODO(viettrungluu): dropping multiple URLs.
2150 if ([urls count] > 1)
2151 NOTIMPLEMENTED();
2152
2153 // Get the first URL and fix it up.
2154 GURL url(GURL(URLFixerUpper::FixupURL(
2155 base::SysNSStringToUTF8([urls objectAtIndex:0]), std::string())));
2156
2157 [self openURL:&url inView:view at:point];
2158}
2159
2160// (URLDropTargetController protocol)
2161- (void)dropText:(NSString*)text inView:(NSView*)view at:(NSPoint)point {
2162 DCHECK_EQ(view, tabStripView_.get());
2163
2164 // If the input is plain text, classify the input and make the URL.
2165 AutocompleteMatch match;
2166 AutocompleteClassifierFactory::GetForProfile(browser_->profile())->Classify(
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002167 base::SysNSStringToUTF16(text), false, false, &match, NULL);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002168 GURL url(match.destination_url);
2169
2170 [self openURL:&url inView:view at:point];
2171}
2172
2173// (URLDropTargetController protocol)
2174- (void)indicateDropURLsInView:(NSView*)view at:(NSPoint)point {
2175 DCHECK_EQ(view, tabStripView_.get());
2176
2177 // The minimum y-coordinate at which one should consider place the arrow.
2178 const CGFloat arrowBaseY = 25;
2179
2180 NSInteger index;
2181 WindowOpenDisposition disposition;
2182 [self droppingURLsAt:point
2183 givesIndex:&index
2184 disposition:&disposition];
2185
2186 NSPoint arrowPos = NSMakePoint(0, arrowBaseY);
2187 if (index == -1) {
2188 // Append a tab at the end.
2189 DCHECK(disposition == NEW_FOREGROUND_TAB);
2190 NSInteger lastIndex = [tabArray_ count] - 1;
2191 NSRect overRect = [[[tabArray_ objectAtIndex:lastIndex] view] frame];
2192 arrowPos.x = overRect.origin.x + overRect.size.width - kTabOverlap / 2.0;
2193 } else {
2194 NSRect overRect = [[[tabArray_ objectAtIndex:index] view] frame];
2195 switch (disposition) {
2196 case NEW_FOREGROUND_TAB:
2197 // Insert tab (to the left of the given tab).
2198 arrowPos.x = overRect.origin.x + kTabOverlap / 2.0;
2199 break;
2200 case CURRENT_TAB:
2201 // Overwrite the given tab.
2202 arrowPos.x = overRect.origin.x + overRect.size.width / 2.0;
2203 break;
2204 default:
2205 NOTREACHED();
2206 }
2207 }
2208
2209 [tabStripView_ setDropArrowPosition:arrowPos];
2210 [tabStripView_ setDropArrowShown:YES];
2211 [tabStripView_ setNeedsDisplay:YES];
2212
2213 // Perform a delayed tab transition if hovering directly over a tab.
2214 if (index != -1 && disposition == CURRENT_TAB) {
2215 NSInteger modelIndex = [self modelIndexFromIndex:index];
2216 // Only start the transition if it has a valid model index (i.e. it's not
2217 // in the middle of closing).
2218 if (modelIndex != NSNotFound) {
2219 hoverTabSelector_->StartTabTransition(modelIndex);
2220 return;
2221 }
2222 }
2223 // If a tab transition was not started, cancel the pending one.
2224 hoverTabSelector_->CancelTabTransition();
2225}
2226
2227// (URLDropTargetController protocol)
2228- (void)hideDropURLsIndicatorInView:(NSView*)view {
2229 DCHECK_EQ(view, tabStripView_.get());
2230
2231 // Cancel any pending tab transition.
2232 hoverTabSelector_->CancelTabTransition();
2233
2234 if ([tabStripView_ dropArrowShown]) {
2235 [tabStripView_ setDropArrowShown:NO];
2236 [tabStripView_ setNeedsDisplay:YES];
2237 }
2238}
2239
2240// (URLDropTargetController protocol)
2241- (BOOL)isUnsupportedDropData:(id<NSDraggingInfo>)info {
2242 return drag_util::IsUnsupportedDropData(browser_->profile(), info);
2243}
2244
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002245- (TabContentsController*)activeTabContentsController {
2246 int modelIndex = tabStripModel_->active_index();
2247 if (modelIndex < 0)
2248 return nil;
2249 NSInteger index = [self indexFromModelIndex:modelIndex];
2250 if (index < 0 ||
2251 index >= (NSInteger)[tabContentsArray_ count])
2252 return nil;
2253 return [tabContentsArray_ objectAtIndex:index];
2254}
2255
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002256- (void)themeDidChangeNotification:(NSNotification*)notification {
2257 [self setNewTabImages];
2258}
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002259
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002260- (void)setNewTabImages {
2261 ThemeService *theme =
2262 static_cast<ThemeService*>([[tabStripView_ window] themeProvider]);
2263 if (!theme)
2264 return;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002265
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002266 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
2267 NSImage* mask = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON_MASK).ToNSImage();
2268 NSImage* normal = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON).ToNSImage();
2269 NSImage* hover = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON_H).ToNSImage();
2270 NSImage* pressed = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON_P).ToNSImage();
2271
2272 NSImage* foreground = ApplyMask(
Ben Murdochbb1529c2013-08-08 10:24:53 +01002273 theme->GetNSImageNamed(IDR_THEME_TAB_BACKGROUND), mask);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002274
2275 [[newTabButton_ cell] setImage:Overlay(foreground, normal, 1.0)
2276 forButtonState:image_button_cell::kDefaultState];
2277 [[newTabButton_ cell] setImage:Overlay(foreground, hover, 1.0)
2278 forButtonState:image_button_cell::kHoverState];
2279 [[newTabButton_ cell] setImage:Overlay(foreground, pressed, 1.0)
2280 forButtonState:image_button_cell::kPressedState];
2281
2282 // IDR_THEME_TAB_BACKGROUND_INACTIVE is only used with the default theme.
2283 if (theme->UsingDefaultTheme()) {
2284 const CGFloat alpha = tabs::kImageNoFocusAlpha;
2285 NSImage* background = ApplyMask(
Ben Murdochbb1529c2013-08-08 10:24:53 +01002286 theme->GetNSImageNamed(IDR_THEME_TAB_BACKGROUND_INACTIVE), mask);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002287 [[newTabButton_ cell] setImage:Overlay(background, normal, alpha)
2288 forButtonState:image_button_cell::kDefaultStateBackground];
2289 [[newTabButton_ cell] setImage:Overlay(background, hover, alpha)
2290 forButtonState:image_button_cell::kHoverStateBackground];
2291 } else {
2292 [[newTabButton_ cell] setImage:nil
2293 forButtonState:image_button_cell::kDefaultStateBackground];
2294 [[newTabButton_ cell] setImage:nil
2295 forButtonState:image_button_cell::kHoverStateBackground];
2296 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002297}
2298
2299@end
2300
2301NSView* GetSheetParentViewForWebContents(WebContents* web_contents) {
2302 // View hierarchy of the contents view:
2303 // NSView -- switchView, same for all tabs
2304 // +- NSView -- TabContentsController's view
2305 // +- TabContentsViewCocoa
2306 //
2307 // Changing it? Do not forget to modify
2308 // -[TabStripController swapInTabAtIndex:] too.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002309 return [web_contents->GetView()->GetNativeView() superview];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002310}