OLD | NEW |
| (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(¶ms); | |
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 | |
OLD | NEW |