Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(352)

Side by Side Diff: chrome/browser/ui/cocoa/tab_strip_controller.mm

Issue 6362007: [Mac] Organize all tab/tab strip files into chrome/browser/ui/cocoa/tabs/.... (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src/
Patch Set: '' Created 9 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2010 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/tab_strip_controller.h"
6
7 #import <QuartzCore/QuartzCore.h>
8
9 #include <limits>
10 #include <string>
11
12 #include "app/l10n_util.h"
13 #include "app/mac/nsimage_cache.h"
14 #include "app/resource_bundle.h"
15 #include "base/mac/mac_util.h"
16 #include "base/sys_string_conversions.h"
17 #include "chrome/app/chrome_command_ids.h"
18 #include "chrome/browser/autocomplete/autocomplete.h"
19 #include "chrome/browser/autocomplete/autocomplete_classifier.h"
20 #include "chrome/browser/autocomplete/autocomplete_match.h"
21 #include "chrome/browser/metrics/user_metrics.h"
22 #include "chrome/browser/profiles/profile.h"
23 #include "chrome/browser/debugger/devtools_window.h"
24 #include "chrome/browser/net/url_fixer_upper.h"
25 #include "chrome/browser/sidebar/sidebar_container.h"
26 #include "chrome/browser/sidebar/sidebar_manager.h"
27 #include "chrome/browser/tab_contents/navigation_controller.h"
28 #include "chrome/browser/tab_contents/navigation_entry.h"
29 #include "chrome/browser/tab_contents/tab_contents.h"
30 #include "chrome/browser/tab_contents/tab_contents_view.h"
31 #include "chrome/browser/tabs/tab_strip_model.h"
32 #include "chrome/browser/ui/browser.h"
33 #include "chrome/browser/ui/browser_navigator.h"
34 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
35 #import "chrome/browser/ui/cocoa/constrained_window_mac.h"
36 #import "chrome/browser/ui/cocoa/new_tab_button.h"
37 #import "chrome/browser/ui/cocoa/tab_strip_view.h"
38 #import "chrome/browser/ui/cocoa/tab_contents_controller.h"
39 #import "chrome/browser/ui/cocoa/tab_controller.h"
40 #import "chrome/browser/ui/cocoa/tab_strip_model_observer_bridge.h"
41 #import "chrome/browser/ui/cocoa/tab_view.h"
42 #import "chrome/browser/ui/cocoa/throbber_view.h"
43 #include "chrome/browser/ui/find_bar/find_bar.h"
44 #include "chrome/browser/ui/find_bar/find_bar_controller.h"
45 #include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h"
46 #include "grit/app_resources.h"
47 #include "grit/generated_resources.h"
48 #include "grit/theme_resources.h"
49 #include "skia/ext/skia_utils_mac.h"
50 #import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
51
52 NSString* const kTabStripNumberOfTabsChanged = @"kTabStripNumberOfTabsChanged";
53
54 namespace {
55
56 // The images names used for different states of the new tab button.
57 NSString* const kNewTabHoverImage = @"newtab_h.pdf";
58 NSString* const kNewTabImage = @"newtab.pdf";
59 NSString* const kNewTabPressedImage = @"newtab_p.pdf";
60
61 // A value to indicate tab layout should use the full available width of the
62 // view.
63 const CGFloat kUseFullAvailableWidth = -1.0;
64
65 // The amount by which tabs overlap.
66 const CGFloat kTabOverlap = 20.0;
67
68 // The width and height for a tab's icon.
69 const CGFloat kIconWidthAndHeight = 16.0;
70
71 // The amount by which the new tab button is offset (from the tabs).
72 const CGFloat kNewTabButtonOffset = 8.0;
73
74 // The amount by which to shrink the tab strip (on the right) when the
75 // incognito badge is present.
76 const CGFloat kIncognitoBadgeTabStripShrink = 18;
77
78 // Time (in seconds) in which tabs animate to their final position.
79 const NSTimeInterval kAnimationDuration = 0.125;
80
81 // Helper class for doing NSAnimationContext calls that takes a bool to disable
82 // all the work. Useful for code that wants to conditionally animate.
83 class ScopedNSAnimationContextGroup {
84 public:
85 explicit ScopedNSAnimationContextGroup(bool animate)
86 : animate_(animate) {
87 if (animate_) {
88 [NSAnimationContext beginGrouping];
89 }
90 }
91
92 ~ScopedNSAnimationContextGroup() {
93 if (animate_) {
94 [NSAnimationContext endGrouping];
95 }
96 }
97
98 void SetCurrentContextDuration(NSTimeInterval duration) {
99 if (animate_) {
100 [[NSAnimationContext currentContext] gtm_setDuration:duration
101 eventMask:NSLeftMouseUpMask];
102 }
103 }
104
105 void SetCurrentContextShortestDuration() {
106 if (animate_) {
107 // The minimum representable time interval. This used to stop an
108 // in-progress animation as quickly as possible.
109 const NSTimeInterval kMinimumTimeInterval =
110 std::numeric_limits<NSTimeInterval>::min();
111 // Directly set the duration to be short, avoiding the Steve slowmotion
112 // ettect the gtm_setDuration: provides.
113 [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval];
114 }
115 }
116
117 private:
118 bool animate_;
119 DISALLOW_COPY_AND_ASSIGN(ScopedNSAnimationContextGroup);
120 };
121
122 } // namespace
123
124 @interface TabStripController (Private)
125 - (void)installTrackingArea;
126 - (void)addSubviewToPermanentList:(NSView*)aView;
127 - (void)regenerateSubviewList;
128 - (NSInteger)indexForContentsView:(NSView*)view;
129 - (void)updateFavIconForContents:(TabContents*)contents
130 atIndex:(NSInteger)modelIndex;
131 - (void)layoutTabsWithAnimation:(BOOL)animate
132 regenerateSubviews:(BOOL)doUpdate;
133 - (void)animationDidStopForController:(TabController*)controller
134 finished:(BOOL)finished;
135 - (NSInteger)indexFromModelIndex:(NSInteger)index;
136 - (NSInteger)numberOfOpenTabs;
137 - (NSInteger)numberOfOpenMiniTabs;
138 - (NSInteger)numberOfOpenNonMiniTabs;
139 - (void)mouseMoved:(NSEvent*)event;
140 - (void)setTabTrackingAreasEnabled:(BOOL)enabled;
141 - (void)droppingURLsAt:(NSPoint)point
142 givesIndex:(NSInteger*)index
143 disposition:(WindowOpenDisposition*)disposition;
144 - (void)setNewTabButtonHoverState:(BOOL)showHover;
145 @end
146
147 // A simple view class that prevents the Window Server from dragging the area
148 // behind tabs. Sometimes core animation confuses it. Unfortunately, it can also
149 // falsely pick up clicks during rapid tab closure, so we have to account for
150 // that.
151 @interface TabStripControllerDragBlockingView : NSView {
152 TabStripController* controller_; // weak; owns us
153 }
154
155 - (id)initWithFrame:(NSRect)frameRect
156 controller:(TabStripController*)controller;
157 @end
158
159 @implementation TabStripControllerDragBlockingView
160 - (BOOL)mouseDownCanMoveWindow {return NO;}
161 - (void)drawRect:(NSRect)rect {}
162
163 - (id)initWithFrame:(NSRect)frameRect
164 controller:(TabStripController*)controller {
165 if ((self = [super initWithFrame:frameRect]))
166 controller_ = controller;
167 return self;
168 }
169
170 // In "rapid tab closure" mode (i.e., the user is clicking close tab buttons in
171 // rapid succession), the animations confuse Cocoa's hit testing (which appears
172 // to use cached results, among other tricks), so this view can somehow end up
173 // getting a mouse down event. Thus we do an explicit hit test during rapid tab
174 // closure, and if we find that we got a mouse down we shouldn't have, we send
175 // it off to the appropriate view.
176 - (void)mouseDown:(NSEvent*)event {
177 if ([controller_ inRapidClosureMode]) {
178 NSView* superview = [self superview];
179 NSPoint hitLocation =
180 [[superview superview] convertPoint:[event locationInWindow]
181 fromView:nil];
182 NSView* hitView = [superview hitTest:hitLocation];
183 if (hitView != self) {
184 [hitView mouseDown:event];
185 return;
186 }
187 }
188 [super mouseDown:event];
189 }
190 @end
191
192 #pragma mark -
193
194 // A delegate, owned by the CAAnimation system, that is alerted when the
195 // animation to close a tab is completed. Calls back to the given tab strip
196 // to let it know that |controller_| is ready to be removed from the model.
197 // Since we only maintain weak references, the tab strip must call -invalidate:
198 // to prevent the use of dangling pointers.
199 @interface TabCloseAnimationDelegate : NSObject {
200 @private
201 TabStripController* strip_; // weak; owns us indirectly
202 TabController* controller_; // weak
203 }
204
205 // Will tell |strip| when the animation for |controller|'s view has completed.
206 // These should not be nil, and will not be retained.
207 - (id)initWithTabStrip:(TabStripController*)strip
208 tabController:(TabController*)controller;
209
210 // Invalidates this object so that no further calls will be made to
211 // |strip_|. This should be called when |strip_| is released, to
212 // prevent attempts to call into the released object.
213 - (void)invalidate;
214
215 // CAAnimation delegate method
216 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished;
217
218 @end
219
220 @implementation TabCloseAnimationDelegate
221
222 - (id)initWithTabStrip:(TabStripController*)strip
223 tabController:(TabController*)controller {
224 if ((self == [super init])) {
225 DCHECK(strip && controller);
226 strip_ = strip;
227 controller_ = controller;
228 }
229 return self;
230 }
231
232 - (void)invalidate {
233 strip_ = nil;
234 controller_ = nil;
235 }
236
237 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
238 [strip_ animationDidStopForController:controller_ finished:finished];
239 }
240
241 @end
242
243 #pragma mark -
244
245 // In general, there is a one-to-one correspondence between TabControllers,
246 // TabViews, TabContentsControllers, and the TabContents in the TabStripModel.
247 // In the steady-state, the indices line up so an index coming from the model
248 // is directly mapped to the same index in the parallel arrays holding our
249 // views and controllers. This is also true when new tabs are created (even
250 // though there is a small period of animation) because the tab is present
251 // in the model while the TabView is animating into place. As a result, nothing
252 // special need be done to handle "new tab" animation.
253 //
254 // This all goes out the window with the "close tab" animation. The animation
255 // kicks off in |-tabDetachedWithContents:atIndex:| with the notification that
256 // the tab has been removed from the model. The simplest solution at this
257 // point would be to remove the views and controllers as well, however once
258 // the TabView is removed from the view list, the tab z-order code takes care of
259 // removing it from the tab strip and we'll get no animation. That means if
260 // there is to be any visible animation, the TabView needs to stay around until
261 // its animation is complete. In order to maintain consistency among the
262 // internal parallel arrays, this means all structures are kept around until
263 // the animation completes. At this point, though, the model and our internal
264 // structures are out of sync: the indices no longer line up. As a result,
265 // there is a concept of a "model index" which represents an index valid in
266 // the TabStripModel. During steady-state, the "model index" is just the same
267 // index as our parallel arrays (as above), but during tab close animations,
268 // it is different, offset by the number of tabs preceding the index which
269 // are undergoing tab closing animation. As a result, the caller needs to be
270 // careful to use the available conversion routines when accessing the internal
271 // parallel arrays (e.g., -indexFromModelIndex:). Care also needs to be taken
272 // during tab layout to ignore closing tabs in the total width calculations and
273 // in individual tab positioning (to avoid moving them right back to where they
274 // were).
275 //
276 // In order to prevent actions being taken on tabs which are closing, the tab
277 // itself gets marked as such so it no longer will send back its select action
278 // or allow itself to be dragged. In addition, drags on the tab strip as a
279 // whole are disabled while there are tabs closing.
280
281 @implementation TabStripController
282
283 @synthesize indentForControls = indentForControls_;
284
285 - (id)initWithView:(TabStripView*)view
286 switchView:(NSView*)switchView
287 browser:(Browser*)browser
288 delegate:(id<TabStripControllerDelegate>)delegate {
289 DCHECK(view && switchView && browser && delegate);
290 if ((self = [super init])) {
291 tabStripView_.reset([view retain]);
292 switchView_ = switchView;
293 browser_ = browser;
294 tabStripModel_ = browser_->tabstrip_model();
295 delegate_ = delegate;
296 bridge_.reset(new TabStripModelObserverBridge(tabStripModel_, self));
297 tabContentsArray_.reset([[NSMutableArray alloc] init]);
298 tabArray_.reset([[NSMutableArray alloc] init]);
299
300 // Important note: any non-tab subviews not added to |permanentSubviews_|
301 // (see |-addSubviewToPermanentList:|) will be wiped out.
302 permanentSubviews_.reset([[NSMutableArray alloc] init]);
303
304 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
305 defaultFavIcon_.reset([rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON) retain]);
306
307 [self setIndentForControls:[[self class] defaultIndentForControls]];
308
309 // TODO(viettrungluu): WTF? "For some reason, if the view is present in the
310 // nib a priori, it draws correctly. If we create it in code and add it to
311 // the tab view, it draws with all sorts of crazy artifacts."
312 newTabButton_ = [view newTabButton];
313 [self addSubviewToPermanentList:newTabButton_];
314 [newTabButton_ setTarget:nil];
315 [newTabButton_ setAction:@selector(commandDispatch:)];
316 [newTabButton_ setTag:IDC_NEW_TAB];
317 // Set the images from code because Cocoa fails to find them in our sub
318 // bundle during tests.
319 [newTabButton_ setImage:app::mac::GetCachedImageWithName(kNewTabImage)];
320 [newTabButton_ setAlternateImage:
321 app::mac::GetCachedImageWithName(kNewTabPressedImage)];
322 newTabButtonShowingHoverImage_ = NO;
323 newTabTrackingArea_.reset(
324 [[NSTrackingArea alloc] initWithRect:[newTabButton_ bounds]
325 options:(NSTrackingMouseEnteredAndExited |
326 NSTrackingActiveAlways)
327 owner:self
328 userInfo:nil]);
329 [newTabButton_ addTrackingArea:newTabTrackingArea_.get()];
330 targetFrames_.reset([[NSMutableDictionary alloc] init]);
331
332 dragBlockingView_.reset(
333 [[TabStripControllerDragBlockingView alloc] initWithFrame:NSZeroRect
334 controller:self]);
335 [self addSubviewToPermanentList:dragBlockingView_];
336
337 newTabTargetFrame_ = NSMakeRect(0, 0, 0, 0);
338 availableResizeWidth_ = kUseFullAvailableWidth;
339
340 closingControllers_.reset([[NSMutableSet alloc] init]);
341
342 // Install the permanent subviews.
343 [self regenerateSubviewList];
344
345 // Watch for notifications that the tab strip view has changed size so
346 // we can tell it to layout for the new size.
347 [[NSNotificationCenter defaultCenter]
348 addObserver:self
349 selector:@selector(tabViewFrameChanged:)
350 name:NSViewFrameDidChangeNotification
351 object:tabStripView_];
352
353 trackingArea_.reset([[NSTrackingArea alloc]
354 initWithRect:NSZeroRect // Ignored by NSTrackingInVisibleRect
355 options:NSTrackingMouseEnteredAndExited |
356 NSTrackingMouseMoved |
357 NSTrackingActiveAlways |
358 NSTrackingInVisibleRect
359 owner:self
360 userInfo:nil]);
361 [tabStripView_ addTrackingArea:trackingArea_.get()];
362
363 // Check to see if the mouse is currently in our bounds so we can
364 // enable the tracking areas. Otherwise we won't get hover states
365 // or tab gradients if we load the window up under the mouse.
366 NSPoint mouseLoc = [[view window] mouseLocationOutsideOfEventStream];
367 mouseLoc = [view convertPoint:mouseLoc fromView:nil];
368 if (NSPointInRect(mouseLoc, [view bounds])) {
369 [self setTabTrackingAreasEnabled:YES];
370 mouseInside_ = YES;
371 }
372
373 // Set accessibility descriptions. http://openradar.appspot.com/7496255
374 NSString* description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_NEWTAB);
375 [[newTabButton_ cell]
376 accessibilitySetOverrideValue:description
377 forAttribute:NSAccessibilityDescriptionAttribute];
378
379 // Controller may have been (re-)created by switching layout modes, which
380 // means the tab model is already fully formed with tabs. Need to walk the
381 // list and create the UI for each.
382 const int existingTabCount = tabStripModel_->count();
383 const TabContentsWrapper* selection =
384 tabStripModel_->GetSelectedTabContents();
385 for (int i = 0; i < existingTabCount; ++i) {
386 TabContentsWrapper* currentContents = tabStripModel_->GetTabContentsAt(i);
387 [self insertTabWithContents:currentContents
388 atIndex:i
389 inForeground:NO];
390 if (selection == currentContents) {
391 // Must manually force a selection since the model won't send
392 // selection messages in this scenario.
393 [self selectTabWithContents:currentContents
394 previousContents:NULL
395 atIndex:i
396 userGesture:NO];
397 }
398 }
399 // Don't lay out the tabs until after the controller has been fully
400 // constructed. The |verticalLayout_| flag has not been initialized by
401 // subclasses at this point, which would cause layout to potentially use
402 // the wrong mode.
403 if (existingTabCount) {
404 [self performSelectorOnMainThread:@selector(layoutTabs)
405 withObject:nil
406 waitUntilDone:NO];
407 }
408 }
409 return self;
410 }
411
412 - (void)dealloc {
413 if (trackingArea_.get())
414 [tabStripView_ removeTrackingArea:trackingArea_.get()];
415
416 [newTabButton_ removeTrackingArea:newTabTrackingArea_.get()];
417 // Invalidate all closing animations so they don't call back to us after
418 // we're gone.
419 for (TabController* controller in closingControllers_.get()) {
420 NSView* view = [controller view];
421 [[[view animationForKey:@"frameOrigin"] delegate] invalidate];
422 }
423 [[NSNotificationCenter defaultCenter] removeObserver:self];
424 [super dealloc];
425 }
426
427 + (CGFloat)defaultTabHeight {
428 return 25.0;
429 }
430
431 + (CGFloat)defaultIndentForControls {
432 // Default indentation leaves enough room so tabs don't overlap with the
433 // window controls.
434 return 68.0;
435 }
436
437 // Finds the TabContentsController associated with the given index into the tab
438 // model and swaps out the sole child of the contentArea to display its
439 // contents.
440 - (void)swapInTabAtIndex:(NSInteger)modelIndex {
441 DCHECK(modelIndex >= 0 && modelIndex < tabStripModel_->count());
442 NSInteger index = [self indexFromModelIndex:modelIndex];
443 TabContentsController* controller = [tabContentsArray_ objectAtIndex:index];
444
445 // Resize the new view to fit the window. Calling |view| may lazily
446 // instantiate the TabContentsController from the nib. Until we call
447 // |-ensureContentsVisible|, the controller doesn't install the RWHVMac into
448 // the view hierarchy. This is in order to avoid sending the renderer a
449 // spurious default size loaded from the nib during the call to |-view|.
450 NSView* newView = [controller view];
451
452 // Turns content autoresizing off, so removing and inserting views won't
453 // trigger unnecessary content relayout.
454 [controller ensureContentsSizeDoesNotChange];
455
456 // Remove the old view from the view hierarchy. We know there's only one
457 // child of |switchView_| because we're the one who put it there. There
458 // may not be any children in the case of a tab that's been closed, in
459 // which case there's no swapping going on.
460 NSArray* subviews = [switchView_ subviews];
461 if ([subviews count]) {
462 NSView* oldView = [subviews objectAtIndex:0];
463 // Set newView frame to the oldVew frame to prevent NSSplitView hosting
464 // sidebar and tab content from resizing sidebar's content view.
465 // ensureContentsVisible (see below) sets content size and autoresizing
466 // properties.
467 [newView setFrame:[oldView frame]];
468 [switchView_ replaceSubview:oldView with:newView];
469 } else {
470 [newView setFrame:[switchView_ bounds]];
471 [switchView_ addSubview:newView];
472 }
473
474 // New content is in place, delegate should adjust itself accordingly.
475 [delegate_ onSelectTabWithContents:[controller tabContents]];
476
477 // It also restores content autoresizing properties.
478 [controller ensureContentsVisible];
479
480 // Make sure the new tabs's sheets are visible (necessary when a background
481 // tab opened a sheet while it was in the background and now becomes active).
482 TabContentsWrapper* newTab = tabStripModel_->GetTabContentsAt(modelIndex);
483 DCHECK(newTab);
484 if (newTab) {
485 TabContents::ConstrainedWindowList::iterator it, end;
486 end = newTab->tab_contents()->constrained_window_end();
487 NSWindowController* controller = [[newView window] windowController];
488 DCHECK([controller isKindOfClass:[BrowserWindowController class]]);
489
490 for (it = newTab->tab_contents()->constrained_window_begin();
491 it != end;
492 ++it) {
493 ConstrainedWindow* constrainedWindow = *it;
494 static_cast<ConstrainedWindowMac*>(constrainedWindow)->Realize(
495 static_cast<BrowserWindowController*>(controller));
496 }
497 }
498
499 // Tell per-tab sheet manager about currently selected tab.
500 if (sheetController_.get()) {
501 [sheetController_ setActiveView:newView];
502 }
503 }
504
505 // Create a new tab view and set its cell correctly so it draws the way we want
506 // it to. It will be sized and positioned by |-layoutTabs| so there's no need to
507 // set the frame here. This also creates the view as hidden, it will be
508 // shown during layout.
509 - (TabController*)newTab {
510 TabController* controller = [[[TabController alloc] init] autorelease];
511 [controller setTarget:self];
512 [controller setAction:@selector(selectTab:)];
513 [[controller view] setHidden:YES];
514
515 return controller;
516 }
517
518 // (Private) Returns the number of open tabs in the tab strip. This is the
519 // number of TabControllers we know about (as there's a 1-to-1 mapping from
520 // these controllers to a tab) less the number of closing tabs.
521 - (NSInteger)numberOfOpenTabs {
522 return static_cast<NSInteger>(tabStripModel_->count());
523 }
524
525 // (Private) Returns the number of open, mini-tabs.
526 - (NSInteger)numberOfOpenMiniTabs {
527 // Ask the model for the number of mini tabs. Note that tabs which are in
528 // the process of closing (i.e., whose controllers are in
529 // |closingControllers_|) have already been removed from the model.
530 return tabStripModel_->IndexOfFirstNonMiniTab();
531 }
532
533 // (Private) Returns the number of open, non-mini tabs.
534 - (NSInteger)numberOfOpenNonMiniTabs {
535 NSInteger number = [self numberOfOpenTabs] - [self numberOfOpenMiniTabs];
536 DCHECK_GE(number, 0);
537 return number;
538 }
539
540 // Given an index into the tab model, returns the index into the tab controller
541 // or tab contents controller array accounting for tabs that are currently
542 // closing. For example, if there are two tabs in the process of closing before
543 // |index|, this returns |index| + 2. If there are no closing tabs, this will
544 // return |index|.
545 - (NSInteger)indexFromModelIndex:(NSInteger)index {
546 DCHECK(index >= 0);
547 if (index < 0)
548 return index;
549
550 NSInteger i = 0;
551 for (TabController* controller in tabArray_.get()) {
552 if ([closingControllers_ containsObject:controller]) {
553 DCHECK([(TabView*)[controller view] isClosing]);
554 ++index;
555 }
556 if (i == index) // No need to check anything after, it has no effect.
557 break;
558 ++i;
559 }
560 return index;
561 }
562
563
564 // Returns the index of the subview |view|. Returns -1 if not present. Takes
565 // closing tabs into account such that this index will correctly match the tab
566 // model. If |view| is in the process of closing, returns -1, as closing tabs
567 // are no longer in the model.
568 - (NSInteger)modelIndexForTabView:(NSView*)view {
569 NSInteger index = 0;
570 for (TabController* current in tabArray_.get()) {
571 // If |current| is closing, skip it.
572 if ([closingControllers_ containsObject:current])
573 continue;
574 else if ([current view] == view)
575 return index;
576 ++index;
577 }
578 return -1;
579 }
580
581 // Returns the index of the contents subview |view|. Returns -1 if not present.
582 // Takes closing tabs into account such that this index will correctly match the
583 // tab model. If |view| is in the process of closing, returns -1, as closing
584 // tabs are no longer in the model.
585 - (NSInteger)modelIndexForContentsView:(NSView*)view {
586 NSInteger index = 0;
587 NSInteger i = 0;
588 for (TabContentsController* current in tabContentsArray_.get()) {
589 // If the TabController corresponding to |current| is closing, skip it.
590 TabController* controller = [tabArray_ objectAtIndex:i];
591 if ([closingControllers_ containsObject:controller]) {
592 ++i;
593 continue;
594 } else if ([current view] == view) {
595 return index;
596 }
597 ++index;
598 ++i;
599 }
600 return -1;
601 }
602
603
604 // Returns the view at the given index, using the array of TabControllers to
605 // get the associated view. Returns nil if out of range.
606 - (NSView*)viewAtIndex:(NSUInteger)index {
607 if (index >= [tabArray_ count])
608 return NULL;
609 return [[tabArray_ objectAtIndex:index] view];
610 }
611
612 - (NSUInteger)viewsCount {
613 return [tabArray_ count];
614 }
615
616 // Called when the user clicks a tab. Tell the model the selection has changed,
617 // which feeds back into us via a notification.
618 - (void)selectTab:(id)sender {
619 DCHECK([sender isKindOfClass:[NSView class]]);
620 int index = [self modelIndexForTabView:sender];
621 if (tabStripModel_->ContainsIndex(index))
622 tabStripModel_->SelectTabContentsAt(index, true);
623 }
624
625 // Called when the user closes a tab. Asks the model to close the tab. |sender|
626 // is the TabView that is potentially going away.
627 - (void)closeTab:(id)sender {
628 DCHECK([sender isKindOfClass:[TabView class]]);
629 if ([hoveredTab_ isEqual:sender]) {
630 hoveredTab_ = nil;
631 }
632
633 NSInteger index = [self modelIndexForTabView:sender];
634 if (!tabStripModel_->ContainsIndex(index))
635 return;
636
637 TabContentsWrapper* contents = tabStripModel_->GetTabContentsAt(index);
638 if (contents)
639 UserMetrics::RecordAction(UserMetricsAction("CloseTab_Mouse"),
640 contents->tab_contents()->profile());
641 const NSInteger numberOfOpenTabs = [self numberOfOpenTabs];
642 if (numberOfOpenTabs > 1) {
643 bool isClosingLastTab = index == numberOfOpenTabs - 1;
644 if (!isClosingLastTab) {
645 // Limit the width available for laying out tabs so that tabs are not
646 // resized until a later time (when the mouse leaves the tab strip).
647 // However, if the tab being closed is a pinned tab, break out of
648 // rapid-closure mode since the mouse is almost guaranteed not to be over
649 // the closebox of the adjacent tab (due to the difference in widths).
650 // TODO(pinkerton): re-visit when handling tab overflow.
651 // http://crbug.com/188
652 if (tabStripModel_->IsTabPinned(index)) {
653 availableResizeWidth_ = kUseFullAvailableWidth;
654 } else {
655 NSView* penultimateTab = [self viewAtIndex:numberOfOpenTabs - 2];
656 availableResizeWidth_ = NSMaxX([penultimateTab frame]);
657 }
658 } else {
659 // If the rightmost tab is closed, change the available width so that
660 // another tab's close button lands below the cursor (assuming the tabs
661 // are currently below their maximum width and can grow).
662 NSView* lastTab = [self viewAtIndex:numberOfOpenTabs - 1];
663 availableResizeWidth_ = NSMaxX([lastTab frame]);
664 }
665 tabStripModel_->CloseTabContentsAt(
666 index,
667 TabStripModel::CLOSE_USER_GESTURE |
668 TabStripModel::CLOSE_CREATE_HISTORICAL_TAB);
669 } else {
670 // Use the standard window close if this is the last tab
671 // this prevents the tab from being removed from the model until after
672 // the window dissapears
673 [[tabStripView_ window] performClose:nil];
674 }
675 }
676
677 // Dispatch context menu commands for the given tab controller.
678 - (void)commandDispatch:(TabStripModel::ContextMenuCommand)command
679 forController:(TabController*)controller {
680 int index = [self modelIndexForTabView:[controller view]];
681 if (tabStripModel_->ContainsIndex(index))
682 tabStripModel_->ExecuteContextMenuCommand(index, command);
683 }
684
685 // Returns YES if the specificed command should be enabled for the given
686 // controller.
687 - (BOOL)isCommandEnabled:(TabStripModel::ContextMenuCommand)command
688 forController:(TabController*)controller {
689 int index = [self modelIndexForTabView:[controller view]];
690 if (!tabStripModel_->ContainsIndex(index))
691 return NO;
692 return tabStripModel_->IsContextMenuCommandEnabled(index, command) ? YES : NO;
693 }
694
695 - (void)insertPlaceholderForTab:(TabView*)tab
696 frame:(NSRect)frame
697 yStretchiness:(CGFloat)yStretchiness {
698 placeholderTab_ = tab;
699 placeholderFrame_ = frame;
700 placeholderStretchiness_ = yStretchiness;
701 [self layoutTabsWithAnimation:initialLayoutComplete_ regenerateSubviews:NO];
702 }
703
704 - (BOOL)isDragSessionActive {
705 return placeholderTab_ != nil;
706 }
707
708 - (BOOL)isTabFullyVisible:(TabView*)tab {
709 NSRect frame = [tab frame];
710 return NSMinX(frame) >= [self indentForControls] &&
711 NSMaxX(frame) <= NSMaxX([tabStripView_ frame]);
712 }
713
714 - (void)showNewTabButton:(BOOL)show {
715 forceNewTabButtonHidden_ = show ? NO : YES;
716 if (forceNewTabButtonHidden_)
717 [newTabButton_ setHidden:YES];
718 }
719
720 // Lay out all tabs in the order of their TabContentsControllers, which matches
721 // the ordering in the TabStripModel. This call isn't that expensive, though
722 // it is O(n) in the number of tabs. Tabs will animate to their new position
723 // if the window is visible and |animate| is YES.
724 // TODO(pinkerton): Note this doesn't do too well when the number of min-sized
725 // tabs would cause an overflow. http://crbug.com/188
726 - (void)layoutTabsWithAnimation:(BOOL)animate
727 regenerateSubviews:(BOOL)doUpdate {
728 DCHECK([NSThread isMainThread]);
729 if (![tabArray_ count])
730 return;
731
732 const CGFloat kMaxTabWidth = [TabController maxTabWidth];
733 const CGFloat kMinTabWidth = [TabController minTabWidth];
734 const CGFloat kMinSelectedTabWidth = [TabController minSelectedTabWidth];
735 const CGFloat kMiniTabWidth = [TabController miniTabWidth];
736 const CGFloat kAppTabWidth = [TabController appTabWidth];
737
738 NSRect enclosingRect = NSZeroRect;
739 ScopedNSAnimationContextGroup mainAnimationGroup(animate);
740 mainAnimationGroup.SetCurrentContextDuration(kAnimationDuration);
741
742 // Update the current subviews and their z-order if requested.
743 if (doUpdate)
744 [self regenerateSubviewList];
745
746 // Compute the base width of tabs given how much room we're allowed. Note that
747 // mini-tabs have a fixed width. We may not be able to use the entire width
748 // if the user is quickly closing tabs. This may be negative, but that's okay
749 // (taken care of by |MAX()| when calculating tab sizes).
750 CGFloat availableSpace = 0;
751 if (verticalLayout_) {
752 availableSpace = NSHeight([tabStripView_ bounds]);
753 } else {
754 if ([self inRapidClosureMode]) {
755 availableSpace = availableResizeWidth_;
756 } else {
757 availableSpace = NSWidth([tabStripView_ frame]);
758 // Account for the new tab button and the incognito badge.
759 availableSpace -= NSWidth([newTabButton_ frame]) + kNewTabButtonOffset;
760 if (browser_->profile()->IsOffTheRecord())
761 availableSpace -= kIncognitoBadgeTabStripShrink;
762 }
763 availableSpace -= [self indentForControls];
764 }
765
766 // This may be negative, but that's okay (taken care of by |MAX()| when
767 // calculating tab sizes). "mini" tabs in horizontal mode just get a special
768 // section, they don't change size.
769 CGFloat availableSpaceForNonMini = availableSpace;
770 if (!verticalLayout_) {
771 availableSpaceForNonMini -=
772 [self numberOfOpenMiniTabs] * (kMiniTabWidth - kTabOverlap);
773 }
774
775 // Initialize |nonMiniTabWidth| in case there aren't any non-mini-tabs; this
776 // value shouldn't actually be used.
777 CGFloat nonMiniTabWidth = kMaxTabWidth;
778 const NSInteger numberOfOpenNonMiniTabs = [self numberOfOpenNonMiniTabs];
779 if (!verticalLayout_ && numberOfOpenNonMiniTabs) {
780 // Find the width of a non-mini-tab. This only applies to horizontal
781 // mode. Add in the amount we "get back" from the tabs overlapping.
782 availableSpaceForNonMini += (numberOfOpenNonMiniTabs - 1) * kTabOverlap;
783
784 // Divide up the space between the non-mini-tabs.
785 nonMiniTabWidth = availableSpaceForNonMini / numberOfOpenNonMiniTabs;
786
787 // Clamp the width between the max and min.
788 nonMiniTabWidth = MAX(MIN(nonMiniTabWidth, kMaxTabWidth), kMinTabWidth);
789 }
790
791 BOOL visible = [[tabStripView_ window] isVisible];
792
793 CGFloat offset = [self indentForControls];
794 bool hasPlaceholderGap = false;
795 for (TabController* tab in tabArray_.get()) {
796 // Ignore a tab that is going through a close animation.
797 if ([closingControllers_ containsObject:tab])
798 continue;
799
800 BOOL isPlaceholder = [[tab view] isEqual:placeholderTab_];
801 NSRect tabFrame = [[tab view] frame];
802 tabFrame.size.height = [[self class] defaultTabHeight] + 1;
803 if (verticalLayout_) {
804 tabFrame.origin.y = availableSpace - tabFrame.size.height - offset;
805 tabFrame.origin.x = 0;
806 } else {
807 tabFrame.origin.y = 0;
808 tabFrame.origin.x = offset;
809 }
810 // If the tab is hidden, we consider it a new tab. We make it visible
811 // and animate it in.
812 BOOL newTab = [[tab view] isHidden];
813 if (newTab)
814 [[tab view] setHidden:NO];
815
816 if (isPlaceholder) {
817 // Move the current tab to the correct location instantly.
818 // We need a duration or else it doesn't cancel an inflight animation.
819 ScopedNSAnimationContextGroup localAnimationGroup(animate);
820 localAnimationGroup.SetCurrentContextShortestDuration();
821 if (verticalLayout_)
822 tabFrame.origin.y = availableSpace - tabFrame.size.height - offset;
823 else
824 tabFrame.origin.x = placeholderFrame_.origin.x;
825 // TODO(alcor): reenable this
826 //tabFrame.size.height += 10.0 * placeholderStretchiness_;
827 id target = animate ? [[tab view] animator] : [tab view];
828 [target setFrame:tabFrame];
829
830 // Store the frame by identifier to aviod redundant calls to animator.
831 NSValue* identifier = [NSValue valueWithPointer:[tab view]];
832 [targetFrames_ setObject:[NSValue valueWithRect:tabFrame]
833 forKey:identifier];
834 continue;
835 }
836
837 if (placeholderTab_ && !hasPlaceholderGap) {
838 const CGFloat placeholderMin =
839 verticalLayout_ ? NSMinY(placeholderFrame_) :
840 NSMinX(placeholderFrame_);
841 if (verticalLayout_) {
842 if (NSMidY(tabFrame) > placeholderMin) {
843 hasPlaceholderGap = true;
844 offset += NSHeight(placeholderFrame_);
845 tabFrame.origin.y = availableSpace - tabFrame.size.height - offset;
846 }
847 } else {
848 // If the left edge is to the left of the placeholder's left, but the
849 // mid is to the right of it slide over to make space for it.
850 if (NSMidX(tabFrame) > placeholderMin) {
851 hasPlaceholderGap = true;
852 offset += NSWidth(placeholderFrame_);
853 offset -= kTabOverlap;
854 tabFrame.origin.x = offset;
855 }
856 }
857 }
858
859 // Set the width. Selected tabs are slightly wider when things get really
860 // small and thus we enforce a different minimum width.
861 tabFrame.size.width = [tab mini] ?
862 ([tab app] ? kAppTabWidth : kMiniTabWidth) : nonMiniTabWidth;
863 if ([tab selected])
864 tabFrame.size.width = MAX(tabFrame.size.width, kMinSelectedTabWidth);
865
866 // Animate a new tab in by putting it below the horizon unless told to put
867 // it in a specific location (i.e., from a drop).
868 // TODO(pinkerton): figure out vertical tab animations.
869 if (newTab && visible && animate) {
870 if (NSEqualRects(droppedTabFrame_, NSZeroRect)) {
871 [[tab view] setFrame:NSOffsetRect(tabFrame, 0, -NSHeight(tabFrame))];
872 } else {
873 [[tab view] setFrame:droppedTabFrame_];
874 droppedTabFrame_ = NSZeroRect;
875 }
876 }
877
878 // Check the frame by identifier to avoid redundant calls to animator.
879 id frameTarget = visible && animate ? [[tab view] animator] : [tab view];
880 NSValue* identifier = [NSValue valueWithPointer:[tab view]];
881 NSValue* oldTargetValue = [targetFrames_ objectForKey:identifier];
882 if (!oldTargetValue ||
883 !NSEqualRects([oldTargetValue rectValue], tabFrame)) {
884 [frameTarget setFrame:tabFrame];
885 [targetFrames_ setObject:[NSValue valueWithRect:tabFrame]
886 forKey:identifier];
887 }
888
889 enclosingRect = NSUnionRect(tabFrame, enclosingRect);
890
891 if (verticalLayout_) {
892 offset += NSHeight(tabFrame);
893 } else {
894 offset += NSWidth(tabFrame);
895 offset -= kTabOverlap;
896 }
897 }
898
899 // Hide the new tab button if we're explicitly told to. It may already
900 // be hidden, doing it again doesn't hurt. Otherwise position it
901 // appropriately, showing it if necessary.
902 if (forceNewTabButtonHidden_) {
903 [newTabButton_ setHidden:YES];
904 } else {
905 NSRect newTabNewFrame = [newTabButton_ frame];
906 // We've already ensured there's enough space for the new tab button
907 // so we don't have to check it against the available space. We do need
908 // to make sure we put it after any placeholder.
909 CGFloat maxTabX = MAX(offset, NSMaxX(placeholderFrame_) - kTabOverlap);
910 newTabNewFrame.origin = NSMakePoint(maxTabX + kNewTabButtonOffset, 0);
911 if ([tabContentsArray_ count])
912 [newTabButton_ setHidden:NO];
913
914 if (!NSEqualRects(newTabTargetFrame_, newTabNewFrame)) {
915 // Set the new tab button image correctly based on where the cursor is.
916 NSWindow* window = [tabStripView_ window];
917 NSPoint currentMouse = [window mouseLocationOutsideOfEventStream];
918 currentMouse = [tabStripView_ convertPoint:currentMouse fromView:nil];
919
920 BOOL shouldShowHover = [newTabButton_ pointIsOverButton:currentMouse];
921 [self setNewTabButtonHoverState:shouldShowHover];
922
923 // Move the new tab button into place. We want to animate the new tab
924 // button if it's moving to the left (closing a tab), but not when it's
925 // moving to the right (inserting a new tab). If moving right, we need
926 // to use a very small duration to make sure we cancel any in-flight
927 // animation to the left.
928 if (visible && animate) {
929 ScopedNSAnimationContextGroup localAnimationGroup(true);
930 BOOL movingLeft = NSMinX(newTabNewFrame) < NSMinX(newTabTargetFrame_);
931 if (!movingLeft) {
932 localAnimationGroup.SetCurrentContextShortestDuration();
933 }
934 [[newTabButton_ animator] setFrame:newTabNewFrame];
935 newTabTargetFrame_ = newTabNewFrame;
936 } else {
937 [newTabButton_ setFrame:newTabNewFrame];
938 newTabTargetFrame_ = newTabNewFrame;
939 }
940 }
941 }
942
943 [dragBlockingView_ setFrame:enclosingRect];
944
945 // Mark that we've successfully completed layout of at least one tab.
946 initialLayoutComplete_ = YES;
947 }
948
949 // When we're told to layout from the public API we usually want to animate,
950 // except when it's the first time.
951 - (void)layoutTabs {
952 [self layoutTabsWithAnimation:initialLayoutComplete_ regenerateSubviews:YES];
953 }
954
955 // Handles setting the title of the tab based on the given |contents|. Uses
956 // a canned string if |contents| is NULL.
957 - (void)setTabTitle:(NSViewController*)tab withContents:(TabContents*)contents {
958 NSString* titleString = nil;
959 if (contents)
960 titleString = base::SysUTF16ToNSString(contents->GetTitle());
961 if (![titleString length]) {
962 titleString = l10n_util::GetNSString(IDS_BROWSER_WINDOW_MAC_TAB_UNTITLED);
963 }
964 [tab setTitle:titleString];
965 }
966
967 // Called when a notification is received from the model to insert a new tab
968 // at |modelIndex|.
969 - (void)insertTabWithContents:(TabContentsWrapper*)contents
970 atIndex:(NSInteger)modelIndex
971 inForeground:(bool)inForeground {
972 DCHECK(contents);
973 DCHECK(modelIndex == TabStripModel::kNoTab ||
974 tabStripModel_->ContainsIndex(modelIndex));
975
976 // Take closing tabs into account.
977 NSInteger index = [self indexFromModelIndex:modelIndex];
978
979 // Make a new tab. Load the contents of this tab from the nib and associate
980 // the new controller with |contents| so it can be looked up later.
981 scoped_nsobject<TabContentsController> contentsController(
982 [[TabContentsController alloc] initWithContents:contents->tab_contents()
983 delegate:self]);
984 [tabContentsArray_ insertObject:contentsController atIndex:index];
985
986 // Make a new tab and add it to the strip. Keep track of its controller.
987 TabController* newController = [self newTab];
988 [newController setMini:tabStripModel_->IsMiniTab(modelIndex)];
989 [newController setPinned:tabStripModel_->IsTabPinned(modelIndex)];
990 [newController setApp:tabStripModel_->IsAppTab(modelIndex)];
991 [tabArray_ insertObject:newController atIndex:index];
992 NSView* newView = [newController view];
993
994 // Set the originating frame to just below the strip so that it animates
995 // upwards as it's being initially layed out. Oddly, this works while doing
996 // something similar in |-layoutTabs| confuses the window server.
997 [newView setFrame:NSOffsetRect([newView frame],
998 0, -[[self class] defaultTabHeight])];
999
1000 [self setTabTitle:newController withContents:contents->tab_contents()];
1001
1002 // If a tab is being inserted, we can again use the entire tab strip width
1003 // for layout.
1004 availableResizeWidth_ = kUseFullAvailableWidth;
1005
1006 // We don't need to call |-layoutTabs| if the tab will be in the foreground
1007 // because it will get called when the new tab is selected by the tab model.
1008 // Whenever |-layoutTabs| is called, it'll also add the new subview.
1009 if (!inForeground) {
1010 [self layoutTabs];
1011 }
1012
1013 // During normal loading, we won't yet have a favicon and we'll get
1014 // subsequent state change notifications to show the throbber, but when we're
1015 // dragging a tab out into a new window, we have to put the tab's favicon
1016 // into the right state up front as we won't be told to do it from anywhere
1017 // else.
1018 [self updateFavIconForContents:contents->tab_contents() atIndex:modelIndex];
1019
1020 // Send a broadcast that the number of tabs have changed.
1021 [[NSNotificationCenter defaultCenter]
1022 postNotificationName:kTabStripNumberOfTabsChanged
1023 object:self];
1024 }
1025
1026 // Called when a notification is received from the model to select a particular
1027 // tab. Swaps in the toolbar and content area associated with |newContents|.
1028 - (void)selectTabWithContents:(TabContentsWrapper*)newContents
1029 previousContents:(TabContentsWrapper*)oldContents
1030 atIndex:(NSInteger)modelIndex
1031 userGesture:(bool)wasUserGesture {
1032 // Take closing tabs into account.
1033 NSInteger index = [self indexFromModelIndex:modelIndex];
1034
1035 if (oldContents) {
1036 int oldModelIndex =
1037 browser_->GetIndexOfController(&(oldContents->controller()));
1038 if (oldModelIndex != -1) { // When closing a tab, the old tab may be gone.
1039 NSInteger oldIndex = [self indexFromModelIndex:oldModelIndex];
1040 TabContentsController* oldController =
1041 [tabContentsArray_ objectAtIndex:oldIndex];
1042 [oldController willBecomeUnselectedTab];
1043 oldContents->view()->StoreFocus();
1044 oldContents->tab_contents()->WasHidden();
1045 }
1046 }
1047
1048 // De-select all other tabs and select the new tab.
1049 int i = 0;
1050 for (TabController* current in tabArray_.get()) {
1051 [current setSelected:(i == index) ? YES : NO];
1052 ++i;
1053 }
1054
1055 // Tell the new tab contents it is about to become the selected tab. Here it
1056 // can do things like make sure the toolbar is up to date.
1057 TabContentsController* newController =
1058 [tabContentsArray_ objectAtIndex:index];
1059 [newController willBecomeSelectedTab];
1060
1061 // Relayout for new tabs and to let the selected tab grow to be larger in
1062 // size than surrounding tabs if the user has many. This also raises the
1063 // selected tab to the top.
1064 [self layoutTabs];
1065
1066 // Swap in the contents for the new tab.
1067 [self swapInTabAtIndex:modelIndex];
1068
1069 if (newContents) {
1070 newContents->tab_contents()->DidBecomeSelected();
1071 newContents->view()->RestoreFocus();
1072
1073 if (newContents->tab_contents()->find_ui_active())
1074 browser_->GetFindBarController()->find_bar()->SetFocusAndSelection();
1075 }
1076 }
1077
1078 - (void)tabReplacedWithContents:(TabContentsWrapper*)newContents
1079 previousContents:(TabContentsWrapper*)oldContents
1080 atIndex:(NSInteger)modelIndex {
1081 NSInteger index = [self indexFromModelIndex:modelIndex];
1082 TabContentsController* oldController =
1083 [tabContentsArray_ objectAtIndex:index];
1084 DCHECK_EQ(oldContents->tab_contents(), [oldController tabContents]);
1085
1086 // Simply create a new TabContentsController for |newContents| and place it
1087 // into the array, replacing |oldContents|. A TabSelectedAt notification will
1088 // follow, at which point we will install the new view.
1089 scoped_nsobject<TabContentsController> newController(
1090 [[TabContentsController alloc]
1091 initWithContents:newContents->tab_contents()
1092 delegate:self]);
1093
1094 // Bye bye, |oldController|.
1095 [tabContentsArray_ replaceObjectAtIndex:index withObject:newController];
1096
1097 [delegate_ onReplaceTabWithContents:newContents->tab_contents()];
1098
1099 // Fake a tab changed notification to force tab titles and favicons to update.
1100 [self tabChangedWithContents:newContents
1101 atIndex:modelIndex
1102 changeType:TabStripModelObserver::ALL];
1103 }
1104
1105 // Remove all knowledge about this tab and its associated controller, and remove
1106 // the view from the strip.
1107 - (void)removeTab:(TabController*)controller {
1108 NSUInteger index = [tabArray_ indexOfObject:controller];
1109
1110 // Release the tab contents controller so those views get destroyed. This
1111 // will remove all the tab content Cocoa views from the hierarchy. A
1112 // subsequent "select tab" notification will follow from the model. To
1113 // tell us what to swap in in its absence.
1114 [tabContentsArray_ removeObjectAtIndex:index];
1115
1116 // Remove the view from the tab strip.
1117 NSView* tab = [controller view];
1118 [tab removeFromSuperview];
1119
1120 // Remove ourself as an observer.
1121 [[NSNotificationCenter defaultCenter]
1122 removeObserver:self
1123 name:NSViewDidUpdateTrackingAreasNotification
1124 object:tab];
1125
1126 // Clear the tab controller's target.
1127 // TODO(viettrungluu): [crbug.com/23829] Find a better way to handle the tab
1128 // controller's target.
1129 [controller setTarget:nil];
1130
1131 if ([hoveredTab_ isEqual:tab])
1132 hoveredTab_ = nil;
1133
1134 NSValue* identifier = [NSValue valueWithPointer:tab];
1135 [targetFrames_ removeObjectForKey:identifier];
1136
1137 // Once we're totally done with the tab, delete its controller
1138 [tabArray_ removeObjectAtIndex:index];
1139 }
1140
1141 // Called by the CAAnimation delegate when the tab completes the closing
1142 // animation.
1143 - (void)animationDidStopForController:(TabController*)controller
1144 finished:(BOOL)finished {
1145 [closingControllers_ removeObject:controller];
1146 [self removeTab:controller];
1147 }
1148
1149 // Save off which TabController is closing and tell its view's animator
1150 // where to move the tab to. Registers a delegate to call back when the
1151 // animation is complete in order to remove the tab from the model.
1152 - (void)startClosingTabWithAnimation:(TabController*)closingTab {
1153 DCHECK([NSThread isMainThread]);
1154 // Save off the controller into the set of animating tabs. This alerts
1155 // the layout method to not do anything with it and allows us to correctly
1156 // calculate offsets when working with indices into the model.
1157 [closingControllers_ addObject:closingTab];
1158
1159 // Mark the tab as closing. This prevents it from generating any drags or
1160 // selections while it's animating closed.
1161 [(TabView*)[closingTab view] setClosing:YES];
1162
1163 // Register delegate (owned by the animation system).
1164 NSView* tabView = [closingTab view];
1165 CAAnimation* animation = [[tabView animationForKey:@"frameOrigin"] copy];
1166 [animation autorelease];
1167 scoped_nsobject<TabCloseAnimationDelegate> delegate(
1168 [[TabCloseAnimationDelegate alloc] initWithTabStrip:self
1169 tabController:closingTab]);
1170 [animation setDelegate:delegate.get()]; // Retains delegate.
1171 NSMutableDictionary* animationDictionary =
1172 [NSMutableDictionary dictionaryWithDictionary:[tabView animations]];
1173 [animationDictionary setObject:animation forKey:@"frameOrigin"];
1174 [tabView setAnimations:animationDictionary];
1175
1176 // Periscope down! Animate the tab.
1177 NSRect newFrame = [tabView frame];
1178 newFrame = NSOffsetRect(newFrame, 0, -newFrame.size.height);
1179 ScopedNSAnimationContextGroup animationGroup(true);
1180 animationGroup.SetCurrentContextDuration(kAnimationDuration);
1181 [[tabView animator] setFrame:newFrame];
1182 }
1183
1184 // Called when a notification is received from the model that the given tab
1185 // has gone away. Start an animation then force a layout to put everything
1186 // in motion.
1187 - (void)tabDetachedWithContents:(TabContentsWrapper*)contents
1188 atIndex:(NSInteger)modelIndex {
1189 // Take closing tabs into account.
1190 NSInteger index = [self indexFromModelIndex:modelIndex];
1191
1192 TabController* tab = [tabArray_ objectAtIndex:index];
1193 if (tabStripModel_->count() > 0) {
1194 [self startClosingTabWithAnimation:tab];
1195 [self layoutTabs];
1196 } else {
1197 [self removeTab:tab];
1198 }
1199
1200 // Send a broadcast that the number of tabs have changed.
1201 [[NSNotificationCenter defaultCenter]
1202 postNotificationName:kTabStripNumberOfTabsChanged
1203 object:self];
1204
1205 [delegate_ onTabDetachedWithContents:contents->tab_contents()];
1206 }
1207
1208 // A helper routine for creating an NSImageView to hold the fav icon or app icon
1209 // for |contents|.
1210 - (NSImageView*)iconImageViewForContents:(TabContents*)contents {
1211 BOOL isApp = contents->is_app();
1212 NSImage* image = nil;
1213 // Favicons come from the renderer, and the renderer draws everything in the
1214 // system color space.
1215 CGColorSpaceRef colorSpace = base::mac::GetSystemColorSpace();
1216 if (isApp) {
1217 SkBitmap* icon = contents->GetExtensionAppIcon();
1218 if (icon)
1219 image = gfx::SkBitmapToNSImageWithColorSpace(*icon, colorSpace);
1220 } else {
1221 image = gfx::SkBitmapToNSImageWithColorSpace(contents->GetFavIcon(),
1222 colorSpace);
1223 }
1224
1225 // Either we don't have a valid favicon or there was some issue converting it
1226 // from an SkBitmap. Either way, just show the default.
1227 if (!image)
1228 image = defaultFavIcon_.get();
1229 NSRect frame = NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight);
1230 NSImageView* view = [[[NSImageView alloc] initWithFrame:frame] autorelease];
1231 [view setImage:image];
1232 return view;
1233 }
1234
1235 // Updates the current loading state, replacing the icon view with a favicon,
1236 // a throbber, the default icon, or nothing at all.
1237 - (void)updateFavIconForContents:(TabContents*)contents
1238 atIndex:(NSInteger)modelIndex {
1239 if (!contents)
1240 return;
1241
1242 static NSImage* throbberWaitingImage =
1243 [ResourceBundle::GetSharedInstance().GetNativeImageNamed(
1244 IDR_THROBBER_WAITING) retain];
1245 static NSImage* throbberLoadingImage =
1246 [ResourceBundle::GetSharedInstance().GetNativeImageNamed(IDR_THROBBER)
1247 retain];
1248 static NSImage* sadFaviconImage =
1249 [ResourceBundle::GetSharedInstance().GetNativeImageNamed(IDR_SAD_FAVICON)
1250 retain];
1251
1252 // Take closing tabs into account.
1253 NSInteger index = [self indexFromModelIndex:modelIndex];
1254 TabController* tabController = [tabArray_ objectAtIndex:index];
1255
1256 bool oldHasIcon = [tabController iconView] != nil;
1257 bool newHasIcon = contents->ShouldDisplayFavIcon() ||
1258 tabStripModel_->IsMiniTab(modelIndex); // Always show icon if mini.
1259
1260 TabLoadingState oldState = [tabController loadingState];
1261 TabLoadingState newState = kTabDone;
1262 NSImage* throbberImage = nil;
1263 if (contents->is_crashed()) {
1264 newState = kTabCrashed;
1265 newHasIcon = true;
1266 } else if (contents->waiting_for_response()) {
1267 newState = kTabWaiting;
1268 throbberImage = throbberWaitingImage;
1269 } else if (contents->is_loading()) {
1270 newState = kTabLoading;
1271 throbberImage = throbberLoadingImage;
1272 }
1273
1274 if (oldState != newState)
1275 [tabController setLoadingState:newState];
1276
1277 // While loading, this function is called repeatedly with the same state.
1278 // To avoid expensive unnecessary view manipulation, only make changes when
1279 // the state is actually changing. When loading is complete (kTabDone),
1280 // every call to this function is significant.
1281 if (newState == kTabDone || oldState != newState ||
1282 oldHasIcon != newHasIcon) {
1283 NSView* iconView = nil;
1284 if (newHasIcon) {
1285 if (newState == kTabDone) {
1286 iconView = [self iconImageViewForContents:contents];
1287 } else if (newState == kTabCrashed) {
1288 NSImage* oldImage = [[self iconImageViewForContents:contents] image];
1289 NSRect frame =
1290 NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight);
1291 iconView = [ThrobberView toastThrobberViewWithFrame:frame
1292 beforeImage:oldImage
1293 afterImage:sadFaviconImage];
1294 } else {
1295 NSRect frame =
1296 NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight);
1297 iconView = [ThrobberView filmstripThrobberViewWithFrame:frame
1298 image:throbberImage];
1299 }
1300 }
1301
1302 [tabController setIconView:iconView];
1303 }
1304 }
1305
1306 // Called when a notification is received from the model that the given tab
1307 // has been updated. |loading| will be YES when we only want to update the
1308 // throbber state, not anything else about the (partially) loading tab.
1309 - (void)tabChangedWithContents:(TabContentsWrapper*)contents
1310 atIndex:(NSInteger)modelIndex
1311 changeType:(TabStripModelObserver::TabChangeType)change {
1312 // Take closing tabs into account.
1313 NSInteger index = [self indexFromModelIndex:modelIndex];
1314
1315 if (modelIndex == tabStripModel_->selected_index())
1316 [delegate_ onSelectedTabChange:change];
1317
1318 if (change == TabStripModelObserver::TITLE_NOT_LOADING) {
1319 // TODO(sky): make this work.
1320 // We'll receive another notification of the change asynchronously.
1321 return;
1322 }
1323
1324 TabController* tabController = [tabArray_ objectAtIndex:index];
1325
1326 if (change != TabStripModelObserver::LOADING_ONLY)
1327 [self setTabTitle:tabController withContents:contents->tab_contents()];
1328
1329 [self updateFavIconForContents:contents->tab_contents() atIndex:modelIndex];
1330
1331 TabContentsController* updatedController =
1332 [tabContentsArray_ objectAtIndex:index];
1333 [updatedController tabDidChange:contents->tab_contents()];
1334 }
1335
1336 // Called when a tab is moved (usually by drag&drop). Keep our parallel arrays
1337 // in sync with the tab strip model. It can also be pinned/unpinned
1338 // simultaneously, so we need to take care of that.
1339 - (void)tabMovedWithContents:(TabContentsWrapper*)contents
1340 fromIndex:(NSInteger)modelFrom
1341 toIndex:(NSInteger)modelTo {
1342 // Take closing tabs into account.
1343 NSInteger from = [self indexFromModelIndex:modelFrom];
1344 NSInteger to = [self indexFromModelIndex:modelTo];
1345
1346 scoped_nsobject<TabContentsController> movedTabContentsController(
1347 [[tabContentsArray_ objectAtIndex:from] retain]);
1348 [tabContentsArray_ removeObjectAtIndex:from];
1349 [tabContentsArray_ insertObject:movedTabContentsController.get()
1350 atIndex:to];
1351 scoped_nsobject<TabController> movedTabController(
1352 [[tabArray_ objectAtIndex:from] retain]);
1353 DCHECK([movedTabController isKindOfClass:[TabController class]]);
1354 [tabArray_ removeObjectAtIndex:from];
1355 [tabArray_ insertObject:movedTabController.get() atIndex:to];
1356
1357 // The tab moved, which means that the mini-tab state may have changed.
1358 if (tabStripModel_->IsMiniTab(modelTo) != [movedTabController mini])
1359 [self tabMiniStateChangedWithContents:contents atIndex:modelTo];
1360
1361 [self layoutTabs];
1362 }
1363
1364 // Called when a tab is pinned or unpinned without moving.
1365 - (void)tabMiniStateChangedWithContents:(TabContentsWrapper*)contents
1366 atIndex:(NSInteger)modelIndex {
1367 // Take closing tabs into account.
1368 NSInteger index = [self indexFromModelIndex:modelIndex];
1369
1370 TabController* tabController = [tabArray_ objectAtIndex:index];
1371 DCHECK([tabController isKindOfClass:[TabController class]]);
1372
1373 // Don't do anything if the change was already picked up by the move event.
1374 if (tabStripModel_->IsMiniTab(modelIndex) == [tabController mini])
1375 return;
1376
1377 [tabController setMini:tabStripModel_->IsMiniTab(modelIndex)];
1378 [tabController setPinned:tabStripModel_->IsTabPinned(modelIndex)];
1379 [tabController setApp:tabStripModel_->IsAppTab(modelIndex)];
1380 [self updateFavIconForContents:contents->tab_contents() atIndex:modelIndex];
1381 // If the tab is being restored and it's pinned, the mini state is set after
1382 // the tab has already been rendered, so re-layout the tabstrip. In all other
1383 // cases, the state is set before the tab is rendered so this isn't needed.
1384 [self layoutTabs];
1385 }
1386
1387 - (void)setFrameOfSelectedTab:(NSRect)frame {
1388 NSView* view = [self selectedTabView];
1389 NSValue* identifier = [NSValue valueWithPointer:view];
1390 [targetFrames_ setObject:[NSValue valueWithRect:frame]
1391 forKey:identifier];
1392 [view setFrame:frame];
1393 }
1394
1395 - (NSView*)selectedTabView {
1396 int selectedIndex = tabStripModel_->selected_index();
1397 // Take closing tabs into account. They can't ever be selected.
1398 selectedIndex = [self indexFromModelIndex:selectedIndex];
1399 return [self viewAtIndex:selectedIndex];
1400 }
1401
1402 // Find the model index based on the x coordinate of the placeholder. If there
1403 // is no placeholder, this returns the end of the tab strip. Closing tabs are
1404 // not considered in computing the index.
1405 - (int)indexOfPlaceholder {
1406 double placeholderX = placeholderFrame_.origin.x;
1407 int index = 0;
1408 int location = 0;
1409 // Use |tabArray_| here instead of the tab strip count in order to get the
1410 // correct index when there are closing tabs to the left of the placeholder.
1411 const int count = [tabArray_ count];
1412 while (index < count) {
1413 // Ignore closing tabs for simplicity. The only drawback of this is that
1414 // if the placeholder is placed right before one or several contiguous
1415 // currently closing tabs, the associated TabController will start at the
1416 // end of the closing tabs.
1417 if ([closingControllers_ containsObject:[tabArray_ objectAtIndex:index]]) {
1418 index++;
1419 continue;
1420 }
1421 NSView* curr = [self viewAtIndex:index];
1422 // The placeholder tab works by changing the frame of the tab being dragged
1423 // to be the bounds of the placeholder, so we need to skip it while we're
1424 // iterating, otherwise we'll end up off by one. Note This only effects
1425 // dragging to the right, not to the left.
1426 if (curr == placeholderTab_) {
1427 index++;
1428 continue;
1429 }
1430 if (placeholderX <= NSMinX([curr frame]))
1431 break;
1432 index++;
1433 location++;
1434 }
1435 return location;
1436 }
1437
1438 // Move the given tab at index |from| in this window to the location of the
1439 // current placeholder.
1440 - (void)moveTabFromIndex:(NSInteger)from {
1441 int toIndex = [self indexOfPlaceholder];
1442 tabStripModel_->MoveTabContentsAt(from, toIndex, true);
1443 }
1444
1445 // Drop a given TabContents at the location of the current placeholder. If there
1446 // is no placeholder, it will go at the end. Used when dragging from another
1447 // window when we don't have access to the TabContents as part of our strip.
1448 // |frame| is in the coordinate system of the tab strip view and represents
1449 // where the user dropped the new tab so it can be animated into its correct
1450 // location when the tab is added to the model. If the tab was pinned in its
1451 // previous window, setting |pinned| to YES will propagate that state to the
1452 // new window. Mini-tabs are either app or pinned tabs; the app state is stored
1453 // by the |contents|, but the |pinned| state is the caller's responsibility.
1454 - (void)dropTabContents:(TabContentsWrapper*)contents
1455 withFrame:(NSRect)frame
1456 asPinnedTab:(BOOL)pinned {
1457 int modelIndex = [self indexOfPlaceholder];
1458
1459 // Mark that the new tab being created should start at |frame|. It will be
1460 // reset as soon as the tab has been positioned.
1461 droppedTabFrame_ = frame;
1462
1463 // Insert it into this tab strip. We want it in the foreground and to not
1464 // inherit the current tab's group.
1465 tabStripModel_->InsertTabContentsAt(
1466 modelIndex, contents,
1467 TabStripModel::ADD_SELECTED | (pinned ? TabStripModel::ADD_PINNED : 0));
1468 }
1469
1470 // Called when the tab strip view changes size. As we only registered for
1471 // changes on our view, we know it's only for our view. Layout w/out
1472 // animations since they are blocked by the resize nested runloop. We need
1473 // the views to adjust immediately. Neither the tabs nor their z-order are
1474 // changed, so we don't need to update the subviews.
1475 - (void)tabViewFrameChanged:(NSNotification*)info {
1476 [self layoutTabsWithAnimation:NO regenerateSubviews:NO];
1477 }
1478
1479 // Called when the tracking areas for any given tab are updated. This allows
1480 // the individual tabs to update their hover states correctly.
1481 // Only generates the event if the cursor is in the tab strip.
1482 - (void)tabUpdateTracking:(NSNotification*)notification {
1483 DCHECK([[notification object] isKindOfClass:[TabView class]]);
1484 DCHECK(mouseInside_);
1485 NSWindow* window = [tabStripView_ window];
1486 NSPoint location = [window mouseLocationOutsideOfEventStream];
1487 if (NSPointInRect(location, [tabStripView_ frame])) {
1488 NSEvent* mouseEvent = [NSEvent mouseEventWithType:NSMouseMoved
1489 location:location
1490 modifierFlags:0
1491 timestamp:0
1492 windowNumber:[window windowNumber]
1493 context:nil
1494 eventNumber:0
1495 clickCount:0
1496 pressure:0];
1497 [self mouseMoved:mouseEvent];
1498 }
1499 }
1500
1501 - (BOOL)inRapidClosureMode {
1502 return availableResizeWidth_ != kUseFullAvailableWidth;
1503 }
1504
1505 // Disable tab dragging when there are any pending animations.
1506 - (BOOL)tabDraggingAllowed {
1507 return [closingControllers_ count] == 0;
1508 }
1509
1510 - (void)mouseMoved:(NSEvent*)event {
1511 // Use hit test to figure out what view we are hovering over.
1512 NSView* targetView = [tabStripView_ hitTest:[event locationInWindow]];
1513
1514 // Set the new tab button hover state iff the mouse is over the button.
1515 BOOL shouldShowHoverImage = [targetView isKindOfClass:[NewTabButton class]];
1516 [self setNewTabButtonHoverState:shouldShowHoverImage];
1517
1518 TabView* tabView = (TabView*)targetView;
1519 if (![tabView isKindOfClass:[TabView class]]) {
1520 if ([[tabView superview] isKindOfClass:[TabView class]]) {
1521 tabView = (TabView*)[targetView superview];
1522 } else {
1523 tabView = nil;
1524 }
1525 }
1526
1527 if (hoveredTab_ != tabView) {
1528 [hoveredTab_ mouseExited:nil]; // We don't pass event because moved events
1529 [tabView mouseEntered:nil]; // don't have valid tracking areas
1530 hoveredTab_ = tabView;
1531 } else {
1532 [hoveredTab_ mouseMoved:event];
1533 }
1534 }
1535
1536 - (void)mouseEntered:(NSEvent*)event {
1537 NSTrackingArea* area = [event trackingArea];
1538 if ([area isEqual:trackingArea_]) {
1539 mouseInside_ = YES;
1540 [self setTabTrackingAreasEnabled:YES];
1541 [self mouseMoved:event];
1542 }
1543 }
1544
1545 // Called when the tracking area is in effect which means we're tracking to
1546 // see if the user leaves the tab strip with their mouse. When they do,
1547 // reset layout to use all available width.
1548 - (void)mouseExited:(NSEvent*)event {
1549 NSTrackingArea* area = [event trackingArea];
1550 if ([area isEqual:trackingArea_]) {
1551 mouseInside_ = NO;
1552 [self setTabTrackingAreasEnabled:NO];
1553 availableResizeWidth_ = kUseFullAvailableWidth;
1554 [hoveredTab_ mouseExited:event];
1555 hoveredTab_ = nil;
1556 [self layoutTabs];
1557 } else if ([area isEqual:newTabTrackingArea_]) {
1558 // If the mouse is moved quickly enough, it is possible for the mouse to
1559 // leave the tabstrip without sending any mouseMoved: messages at all.
1560 // Since this would result in the new tab button incorrectly staying in the
1561 // hover state, disable the hover image on every mouse exit.
1562 [self setNewTabButtonHoverState:NO];
1563 }
1564 }
1565
1566 // Enable/Disable the tracking areas for the tabs. They are only enabled
1567 // when the mouse is in the tabstrip.
1568 - (void)setTabTrackingAreasEnabled:(BOOL)enabled {
1569 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
1570 for (TabController* controller in tabArray_.get()) {
1571 TabView* tabView = [controller tabView];
1572 if (enabled) {
1573 // Set self up to observe tabs so hover states will be correct.
1574 [defaultCenter addObserver:self
1575 selector:@selector(tabUpdateTracking:)
1576 name:NSViewDidUpdateTrackingAreasNotification
1577 object:tabView];
1578 } else {
1579 [defaultCenter removeObserver:self
1580 name:NSViewDidUpdateTrackingAreasNotification
1581 object:tabView];
1582 }
1583 [tabView setTrackingEnabled:enabled];
1584 }
1585 }
1586
1587 // Sets the new tab button's image based on the current hover state. Does
1588 // nothing if the hover state is already correct.
1589 - (void)setNewTabButtonHoverState:(BOOL)shouldShowHover {
1590 if (shouldShowHover && !newTabButtonShowingHoverImage_) {
1591 newTabButtonShowingHoverImage_ = YES;
1592 [newTabButton_ setImage:
1593 app::mac::GetCachedImageWithName(kNewTabHoverImage)];
1594 } else if (!shouldShowHover && newTabButtonShowingHoverImage_) {
1595 newTabButtonShowingHoverImage_ = NO;
1596 [newTabButton_ setImage:app::mac::GetCachedImageWithName(kNewTabImage)];
1597 }
1598 }
1599
1600 // Adds the given subview to (the end of) the list of permanent subviews
1601 // (specified from bottom up). These subviews will always be below the
1602 // transitory subviews (tabs). |-regenerateSubviewList| must be called to
1603 // effectuate the addition.
1604 - (void)addSubviewToPermanentList:(NSView*)aView {
1605 if (aView)
1606 [permanentSubviews_ addObject:aView];
1607 }
1608
1609 // Update the subviews, keeping the permanent ones (or, more correctly, putting
1610 // in the ones listed in permanentSubviews_), and putting in the current tabs in
1611 // the correct z-order. Any current subviews which is neither in the permanent
1612 // list nor a (current) tab will be removed. So if you add such a subview, you
1613 // should call |-addSubviewToPermanentList:| (or better yet, call that and then
1614 // |-regenerateSubviewList| to actually add it).
1615 - (void)regenerateSubviewList {
1616 // Remove self as an observer from all the old tabs before a new set of
1617 // potentially different tabs is put in place.
1618 [self setTabTrackingAreasEnabled:NO];
1619
1620 // Subviews to put in (in bottom-to-top order), beginning with the permanent
1621 // ones.
1622 NSMutableArray* subviews = [NSMutableArray arrayWithArray:permanentSubviews_];
1623
1624 NSView* selectedTabView = nil;
1625 // Go through tabs in reverse order, since |subviews| is bottom-to-top.
1626 for (TabController* tab in [tabArray_ reverseObjectEnumerator]) {
1627 NSView* tabView = [tab view];
1628 if ([tab selected]) {
1629 DCHECK(!selectedTabView);
1630 selectedTabView = tabView;
1631 } else {
1632 [subviews addObject:tabView];
1633 }
1634 }
1635 if (selectedTabView) {
1636 [subviews addObject:selectedTabView];
1637 }
1638 [tabStripView_ setSubviews:subviews];
1639 [self setTabTrackingAreasEnabled:mouseInside_];
1640 }
1641
1642 // Get the index and disposition for a potential URL(s) drop given a point (in
1643 // the |TabStripView|'s coordinates). It considers only the x-coordinate of the
1644 // given point. If it's in the "middle" of a tab, it drops on that tab. If it's
1645 // to the left, it inserts to the left, and similarly for the right.
1646 - (void)droppingURLsAt:(NSPoint)point
1647 givesIndex:(NSInteger*)index
1648 disposition:(WindowOpenDisposition*)disposition {
1649 // Proportion of the tab which is considered the "middle" (and causes things
1650 // to drop on that tab).
1651 const double kMiddleProportion = 0.5;
1652 const double kLRProportion = (1.0 - kMiddleProportion) / 2.0;
1653
1654 DCHECK(index && disposition);
1655 NSInteger i = 0;
1656 for (TabController* tab in tabArray_.get()) {
1657 NSView* view = [tab view];
1658 DCHECK([view isKindOfClass:[TabView class]]);
1659
1660 // Recall that |-[NSView frame]| is in its superview's coordinates, so a
1661 // |TabView|'s frame is in the coordinates of the |TabStripView| (which
1662 // matches the coordinate system of |point|).
1663 NSRect frame = [view frame];
1664
1665 // Modify the frame to make it "unoverlapped".
1666 frame.origin.x += kTabOverlap / 2.0;
1667 frame.size.width -= kTabOverlap;
1668 if (frame.size.width < 1.0)
1669 frame.size.width = 1.0; // try to avoid complete failure
1670
1671 // Drop in a new tab to the left of tab |i|?
1672 if (point.x < (frame.origin.x + kLRProportion * frame.size.width)) {
1673 *index = i;
1674 *disposition = NEW_FOREGROUND_TAB;
1675 return;
1676 }
1677
1678 // Drop on tab |i|?
1679 if (point.x <= (frame.origin.x +
1680 (1.0 - kLRProportion) * frame.size.width)) {
1681 *index = i;
1682 *disposition = CURRENT_TAB;
1683 return;
1684 }
1685
1686 // (Dropping in a new tab to the right of tab |i| will be taken care of in
1687 // the next iteration.)
1688 i++;
1689 }
1690
1691 // If we've made it here, we want to append a new tab to the end.
1692 *index = -1;
1693 *disposition = NEW_FOREGROUND_TAB;
1694 }
1695
1696 - (void)openURL:(GURL*)url inView:(NSView*)view at:(NSPoint)point {
1697 // Get the index and disposition.
1698 NSInteger index;
1699 WindowOpenDisposition disposition;
1700 [self droppingURLsAt:point
1701 givesIndex:&index
1702 disposition:&disposition];
1703
1704 // Either insert a new tab or open in a current tab.
1705 switch (disposition) {
1706 case NEW_FOREGROUND_TAB: {
1707 UserMetrics::RecordAction(UserMetricsAction("Tab_DropURLBetweenTabs"),
1708 browser_->profile());
1709 browser::NavigateParams params(browser_, *url, PageTransition::TYPED);
1710 params.disposition = disposition;
1711 params.tabstrip_index = index;
1712 params.tabstrip_add_types =
1713 TabStripModel::ADD_SELECTED | TabStripModel::ADD_FORCE_INDEX;
1714 browser::Navigate(&params);
1715 break;
1716 }
1717 case CURRENT_TAB:
1718 UserMetrics::RecordAction(UserMetricsAction("Tab_DropURLOnTab"),
1719 browser_->profile());
1720 tabStripModel_->GetTabContentsAt(index)
1721 ->tab_contents()->OpenURL(*url, GURL(), CURRENT_TAB,
1722 PageTransition::TYPED);
1723 tabStripModel_->SelectTabContentsAt(index, true);
1724 break;
1725 default:
1726 NOTIMPLEMENTED();
1727 }
1728 }
1729
1730 // (URLDropTargetController protocol)
1731 - (void)dropURLs:(NSArray*)urls inView:(NSView*)view at:(NSPoint)point {
1732 DCHECK_EQ(view, tabStripView_.get());
1733
1734 if ([urls count] < 1) {
1735 NOTREACHED();
1736 return;
1737 }
1738
1739 //TODO(viettrungluu): dropping multiple URLs.
1740 if ([urls count] > 1)
1741 NOTIMPLEMENTED();
1742
1743 // Get the first URL and fix it up.
1744 GURL url(GURL(URLFixerUpper::FixupURL(
1745 base::SysNSStringToUTF8([urls objectAtIndex:0]), std::string())));
1746
1747 [self openURL:&url inView:view at:point];
1748 }
1749
1750 // (URLDropTargetController protocol)
1751 - (void)dropText:(NSString*)text inView:(NSView*)view at:(NSPoint)point {
1752 DCHECK_EQ(view, tabStripView_.get());
1753
1754 // If the input is plain text, classify the input and make the URL.
1755 AutocompleteMatch match;
1756 browser_->profile()->GetAutocompleteClassifier()->Classify(
1757 base::SysNSStringToWide(text),
1758 std::wstring(), false, &match, NULL);
1759 GURL url(match.destination_url);
1760
1761 [self openURL:&url inView:view at:point];
1762 }
1763
1764 // (URLDropTargetController protocol)
1765 - (void)indicateDropURLsInView:(NSView*)view at:(NSPoint)point {
1766 DCHECK_EQ(view, tabStripView_.get());
1767
1768 // The minimum y-coordinate at which one should consider place the arrow.
1769 const CGFloat arrowBaseY = 25;
1770
1771 NSInteger index;
1772 WindowOpenDisposition disposition;
1773 [self droppingURLsAt:point
1774 givesIndex:&index
1775 disposition:&disposition];
1776
1777 NSPoint arrowPos = NSMakePoint(0, arrowBaseY);
1778 if (index == -1) {
1779 // Append a tab at the end.
1780 DCHECK(disposition == NEW_FOREGROUND_TAB);
1781 NSInteger lastIndex = [tabArray_ count] - 1;
1782 NSRect overRect = [[[tabArray_ objectAtIndex:lastIndex] view] frame];
1783 arrowPos.x = overRect.origin.x + overRect.size.width - kTabOverlap / 2.0;
1784 } else {
1785 NSRect overRect = [[[tabArray_ objectAtIndex:index] view] frame];
1786 switch (disposition) {
1787 case NEW_FOREGROUND_TAB:
1788 // Insert tab (to the left of the given tab).
1789 arrowPos.x = overRect.origin.x + kTabOverlap / 2.0;
1790 break;
1791 case CURRENT_TAB:
1792 // Overwrite the given tab.
1793 arrowPos.x = overRect.origin.x + overRect.size.width / 2.0;
1794 break;
1795 default:
1796 NOTREACHED();
1797 }
1798 }
1799
1800 [tabStripView_ setDropArrowPosition:arrowPos];
1801 [tabStripView_ setDropArrowShown:YES];
1802 [tabStripView_ setNeedsDisplay:YES];
1803 }
1804
1805 // (URLDropTargetController protocol)
1806 - (void)hideDropURLsIndicatorInView:(NSView*)view {
1807 DCHECK_EQ(view, tabStripView_.get());
1808
1809 if ([tabStripView_ dropArrowShown]) {
1810 [tabStripView_ setDropArrowShown:NO];
1811 [tabStripView_ setNeedsDisplay:YES];
1812 }
1813 }
1814
1815 - (GTMWindowSheetController*)sheetController {
1816 if (!sheetController_.get())
1817 sheetController_.reset([[GTMWindowSheetController alloc]
1818 initWithWindow:[switchView_ window] delegate:self]);
1819 return sheetController_.get();
1820 }
1821
1822 - (void)destroySheetController {
1823 // Make sure there are no open sheets.
1824 DCHECK_EQ(0U, [[sheetController_ viewsWithAttachedSheets] count]);
1825 sheetController_.reset();
1826 }
1827
1828 // TabContentsControllerDelegate protocol.
1829 - (void)tabContentsViewFrameWillChange:(TabContentsController*)source
1830 frameRect:(NSRect)frameRect {
1831 id<TabContentsControllerDelegate> controller =
1832 [[switchView_ window] windowController];
1833 [controller tabContentsViewFrameWillChange:source frameRect:frameRect];
1834 }
1835
1836 - (TabContentsController*)activeTabContentsController {
1837 int modelIndex = tabStripModel_->selected_index();
1838 if (modelIndex < 0)
1839 return nil;
1840 NSInteger index = [self indexFromModelIndex:modelIndex];
1841 if (index < 0 ||
1842 index >= (NSInteger)[tabContentsArray_ count])
1843 return nil;
1844 return [tabContentsArray_ objectAtIndex:index];
1845 }
1846
1847 - (void)gtm_systemRequestsVisibilityForView:(NSView*)view {
1848 // This implementation is required by GTMWindowSheetController.
1849
1850 // Raise window...
1851 [[switchView_ window] makeKeyAndOrderFront:self];
1852
1853 // ...and raise a tab with a sheet.
1854 NSInteger index = [self modelIndexForContentsView:view];
1855 DCHECK(index >= 0);
1856 if (index >= 0)
1857 tabStripModel_->SelectTabContentsAt(index, false /* not a user gesture */);
1858 }
1859
1860 - (void)attachConstrainedWindow:(ConstrainedWindowMac*)window {
1861 // TODO(thakis, avi): Figure out how to make this work when tabs are dragged
1862 // out or if fullscreen mode is toggled.
1863
1864 // View hierarchy of the contents view:
1865 // NSView -- switchView, same for all tabs
1866 // +- NSView -- TabContentsController's view
1867 // +- TabContentsViewCocoa
1868 // Changing it? Do not forget to modify removeConstrainedWindow too.
1869 // We use the TabContentsController's view in |swapInTabAtIndex|, so we have
1870 // to pass it to the sheet controller here.
1871 NSView* tabContentsView = [window->owner()->GetNativeView() superview];
1872 window->delegate()->RunSheet([self sheetController], tabContentsView);
1873
1874 // TODO(avi, thakis): GTMWindowSheetController has no api to move tabsheets
1875 // between windows. Until then, we have to prevent having to move a tabsheet
1876 // between windows, e.g. no tearing off of tabs.
1877 NSInteger modelIndex = [self modelIndexForContentsView:tabContentsView];
1878 NSInteger index = [self indexFromModelIndex:modelIndex];
1879 BrowserWindowController* controller =
1880 (BrowserWindowController*)[[switchView_ window] windowController];
1881 DCHECK(controller != nil);
1882 DCHECK(index >= 0);
1883 if (index >= 0) {
1884 [controller setTab:[self viewAtIndex:index] isDraggable:NO];
1885 }
1886 }
1887
1888 - (void)removeConstrainedWindow:(ConstrainedWindowMac*)window {
1889 NSView* tabContentsView = [window->owner()->GetNativeView() superview];
1890
1891 // TODO(avi, thakis): GTMWindowSheetController has no api to move tabsheets
1892 // between windows. Until then, we have to prevent having to move a tabsheet
1893 // between windows, e.g. no tearing off of tabs.
1894 NSInteger modelIndex = [self modelIndexForContentsView:tabContentsView];
1895 NSInteger index = [self indexFromModelIndex:modelIndex];
1896 BrowserWindowController* controller =
1897 (BrowserWindowController*)[[switchView_ window] windowController];
1898 DCHECK(index >= 0);
1899 if (index >= 0) {
1900 [controller setTab:[self viewAtIndex:index] isDraggable:YES];
1901 }
1902 }
1903
1904 @end
OLDNEW
« no previous file with comments | « chrome/browser/ui/cocoa/tab_strip_controller.h ('k') | chrome/browser/ui/cocoa/tab_strip_controller_unittest.mm » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698