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

Side by Side Diff: ios/chrome/browser/ui/tabs/tab_strip_controller.mm

Issue 2588733002: Upstream Chrome on iOS source code [9/11]. (Closed)
Patch Set: Created 4 years 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
OLDNEW
(Empty)
1 // Copyright 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #import "ios/chrome/browser/ui/tabs/tab_strip_controller.h"
6 #import "ios/chrome/browser/ui/tabs/tab_strip_controller_private.h"
7
8 #include <cmath>
9 #include <vector>
10
11 #include "base/i18n/rtl.h"
12 #import "base/ios/weak_nsobject.h"
13 #include "base/mac/bundle_locations.h"
14 #include "base/mac/foundation_util.h"
15 #include "base/mac/objc_property_releaser.h"
16 #include "base/mac/scoped_nsobject.h"
17 #include "base/metrics/user_metrics.h"
18 #include "base/metrics/user_metrics_action.h"
19 #include "base/strings/sys_string_conversions.h"
20 #include "ios/chrome/browser/browser_state/chrome_browser_state.h"
21 #include "ios/chrome/browser/experimental_flags.h"
22 #import "ios/chrome/browser/tabs/tab.h"
23 #import "ios/chrome/browser/tabs/tab_model.h"
24 #import "ios/chrome/browser/tabs/tab_model_observer.h"
25 #import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h"
26 #include "ios/chrome/browser/ui/commands/ios_command_ids.h"
27 #import "ios/chrome/browser/ui/fullscreen_controller.h"
28 #include "ios/chrome/browser/ui/rtl_geometry.h"
29 #include "ios/chrome/browser/ui/tab_switcher/tab_switcher_tab_strip_placeholder_ view.h"
30 #import "ios/chrome/browser/ui/tabs/tab_strip_controller+tab_switcher_animation. h"
31 #import "ios/chrome/browser/ui/tabs/tab_strip_view.h"
32 #import "ios/chrome/browser/ui/tabs/tab_view.h"
33 #include "ios/chrome/browser/ui/tabs/target_frame_cache.h"
34 #include "ios/chrome/browser/ui/ui_util.h"
35 #import "ios/chrome/browser/ui/uikit_ui_util.h"
36 #import "ios/chrome/browser/ui/util/snapshot_util.h"
37 #include "ios/chrome/grit/ios_strings.h"
38 #import "ios/web/public/web_state/web_state.h"
39 #include "third_party/google_toolbox_for_mac/src/iPhone/GTMFadeTruncatingLabel.h "
40 #include "ui/gfx/image/image.h"
41
42 using base::UserMetricsAction;
43
44 NSString* const kWillStartTabStripTabAnimation =
45 @"kWillStartTabStripTabAnimation";
46 NSString* const kTabStripDragStarted = @"kTabStripDragStarted";
47 NSString* const kTabStripDragEnded = @"kTabStripDragEnded";
48
49 namespace TabStrip {
50 UIColor* BackgroundColor() {
51 DCHECK(IsIPadIdiom());
52 return [UIColor colorWithRed:0.149 green:0.149 blue:0.164 alpha:1];
53 }
54 }
55
56 namespace {
57
58 // Animation duration for tab animations.
59 const NSTimeInterval kTabAnimationDuration = 0.25;
60
61 // Animation duration for tab strip fade.
62 const NSTimeInterval kTabStripFadeAnimationDuration = 0.15;
63
64 // Amount of time needed to trigger drag and drop mode when long pressing.
65 const NSTimeInterval kDragAndDropLongPressDuration = 0.4;
66
67 // Tab dimensions.
68 const CGFloat kTabOverlap = 26.0;
69 const CGFloat kTabOverlapForCompactLayout = 30.0;
70
71 const CGFloat kNewTabOverlap = 8.0;
72 const CGFloat kMaxTabWidth = 265.0;
73
74 // Toggle button dimensions.
75 const CGFloat kModeToggleButtonWidth = 36.0;
76 const CGFloat kTabSwitcherToggleButtonWidth = 46.0;
77 const CGFloat kModeToggleButtonBackgroundWidth = 62.0;
78
79 const CGFloat kNewTabRightPadding = 4.0;
80 const CGFloat kMinTabWidth = 200.0;
81 const CGFloat kMinTabWidthForCompactLayout = 160.0;
82
83 const CGFloat kCollapsedTabOverlap = 5.0;
84 const NSUInteger kMaxNumCollapsedTabs = 3;
85 const NSUInteger kMaxNumCollapsedTabsForCompactLayout = 0;
86
87 // Tabs with a visible width smaller than this draw as collapsed tabs..
88 const CGFloat kCollapsedTabWidthThreshold = 40.0;
89
90 // Autoscroll constants. The autoscroll distance is set to
91 // |kMaxAutoscrollDistance| at the edges of the scroll view. As the tab moves
92 // away from the edges of the scroll view, the autoscroll distance decreases by
93 // one for each |kAutoscrollDecrementWidth| points.
94 const CGFloat kMaxAutoscrollDistance = 10.0;
95 const CGFloat kAutoscrollDecrementWidth = 10.0;
96
97 // Dimming view constants, in points.
98 const CGFloat kDimmingViewBottomInsetHighRes = 0.0;
99 const CGFloat kDimmingViewBottomInset = 0.0;
100
101 // The size of the tab strip view.
102 const CGFloat kTabStripHeight = 39.0;
103
104 // The size of the new tab button.
105 const CGFloat kNewTabButtonWidth = 59.9;
106
107 // Default image insets for the new tab button.
108 const CGFloat kNewTabButtonHorizontalImageInset = 2.0;
109 const CGFloat kNewTabButtonTopImageInset = 6.0;
110 const CGFloat kNewTabButtonBottomImageInset = 7.0;
111
112 // Offsets needed to keep the UI properly centered on high-res screens, in
113 // points.
114 const CGFloat kNewTabButtonBottomOffsetHighRes = 2.0;
115 }
116
117 @interface TabStripController ()<TabModelObserver,
118 TabStripViewLayoutDelegate,
119 UIGestureRecognizerDelegate,
120 UIScrollViewDelegate> {
121 base::scoped_nsobject<TabModel> _tabModel;
122 UIView* _view;
123 TabStripView* _tabStripView;
124 UIButton* _buttonNewTab;
125 UIButton* _modeToggleButton; // weak, nil if not visible.
126 UIButton* _tabSwitcherToggleButton; // weak, nil if not visible.
127
128 // Background view of the toggle button. Only visible while in compact layout.
129 base::scoped_nsobject<UIImageView> _toggleButtonBackgroundView;
130
131 TabStrip::Style _style;
132 base::WeakNSProtocol<id<FullScreenControllerDelegate>> _fullscreenDelegate;
133
134 // Array of TabViews. There is a one-to-one correspondence between this array
135 // and the set of Tabs in the TabModel.
136 base::scoped_nsobject<NSMutableArray> _tabArray;
137
138 // Set of TabViews that are currently closing. These TabViews are also in
139 // |_tabArray|. Used to translate between |_tabArray| indexes and TabModel
140 // indexes.
141 base::scoped_nsobject<NSMutableSet> _closingTabs;
142
143 // Tracks target frames for TabViews.
144 // TODO(rohitrao): This is unnecessary, as UIKit updates view frames
145 // immediately, so [view frame] will always return the end state of the
146 // current animation. We can remove this cache entirely. b/5516053
147 TargetFrameCache _targetFrames;
148
149 // Animate when doing layout. This flag is set by setNeedsLayoutWithAnimation
150 // and cleared in layoutSubviews.
151 BOOL _animateLayout;
152
153 // The current tab width. Recomputed whenever a tab is added or removed.
154 CGFloat _currentTabWidth;
155
156 // View used to dim unselected tabs when in reordering mode. Nil when not
157 // reordering tabs.
158 base::scoped_nsobject<UIView> _dimmingView;
159
160 // Is the selected tab highlighted, used when dragging or swiping tabs.
161 BOOL _highlightsSelectedTab;
162
163 // YES when in reordering mode.
164 // TODO(rohitrao): This is redundant with |_draggedTab|. Remove it.
165 BOOL _isReordering;
166
167 // The tab that is currently being dragged. nil when not in reordering mode.
168 base::scoped_nsobject<TabView> _draggedTab;
169
170 // The last known location of the touch that is dragging the tab. This
171 // location is in the coordinate system of |[_tabStripView superview]| because
172 // that coordinate system does not change as the scroll view scrolls.
173 CGPoint _lastDragLocation;
174
175 // Timer used to autoscroll when in reordering mode. Is nil when not active.
176 // Owned by its runloop.
177 NSTimer* _autoscrollTimer; // weak
178
179 // The distance to scroll for each autoscroll timer tick. If negative, the
180 // tabstrip will scroll to the left; if positive, to the right.
181 CGFloat _autoscrollDistance;
182
183 // The model index of the placeholder gap, if one exists. This value is used
184 // as the new model index of the dragged tab when it is dropped.
185 NSUInteger _placeholderGapModelIndex;
186
187 // If YES, display the mode toggle switch at the left side of the strip. Can
188 // be set after creation.
189 BOOL _hasModeToggleSwitch;
190
191 // If YES, display the tab switcher toggle switch at the left side of the
192 // strip. Can be set after creation.
193 BOOL _hasTabSwitcherToggleSwitch;
194
195 base::mac::ObjCPropertyReleaser _propertyReleaser_TabStripController;
196 }
197
198 @property(nonatomic, readonly, retain) TabStripView* tabStripView;
199 @property(nonatomic, readonly, retain) UIButton* buttonNewTab;
200 @property(nonatomic, readonly, assign) UIButton* tabSwitcherToggleButton;
201
202 // Initializes the tab array based on the the entries in the TabModel. Creates
203 // one TabView per Tab and adds it to the tabstrip. A later call to
204 // |-layoutTabs| is needed to properly place the tabs in the correct positions.
205 - (void)initializeTabArrayFromTabModel;
206
207 // Initializes the tab array to have only one empty tab, for the case (used
208 // during startup) when there is not a tab model available.
209 - (void)initializeTabArrayWithNoModel;
210
211 // Add and remove the mode toggle icon and adjusts the size of the scroll view
212 // accordingly. Assumes incognito style has already been checked.
213 - (void)installModeToggleButton;
214 - (void)removeModeToggleButton;
215
216 // Add and remove the tab switcher toggle icon and adjusts the size of the
217 // scroll view accordingly. The tab switcher toggle button is replacing the
218 // incognito mode toggle button.
219 // TODO:(jbbegue) crbug/477676 Remove reference to the incognito toggle button
220 // once we know for sure that it will be replaced by the tab switcher toggle
221 // button.
222 - (void)installTabSwitcherToggleButton;
223 - (void)removeTabSwitcherToggleButton;
224
225 // Returns an autoreleased TabView object with no content.
226 - (TabView*)emptyTabView;
227
228 // Returns an autoreleased TabView object based on the given Tab.
229 // |isSelected| is passed in here as an optimization, so that the TabView is
230 // drawn correctly the first time, without requiring the model to send a
231 // -setSelected message to the TabView.
232 - (TabView*)tabViewForTab:(Tab*)tab isSelected:(BOOL)isSelected;
233
234 // Creates and installs the view used to dim unselected tabs. Does nothing if
235 // the view already exists.
236 - (void)installDimmingViewWithAnimation:(BOOL)animate;
237
238 // Remove the dimming view,
239 - (void)removeDimmingViewWithAnimation:(BOOL)animate;
240
241 // Converts between model indexes and |_tabArray| indexes. The conversion is
242 // necessary because |_tabArray| contains closing tabs whereas the TabModel does
243 // not.
244 - (NSUInteger)indexForModelIndex:(NSUInteger)modelIndex;
245 - (NSUInteger)modelIndexForIndex:(NSUInteger)index;
246 - (NSUInteger)modelIndexForTabView:(TabView*)view;
247
248 // Helper methods to handle each stage of a drag.
249 - (void)beginDrag:(UILongPressGestureRecognizer*)gesture;
250 - (void)continueDrag:(UILongPressGestureRecognizer*)gesture;
251 - (void)endDrag:(UILongPressGestureRecognizer*)gesture;
252 - (void)cancelDrag:(UILongPressGestureRecognizer*)gesture;
253
254 // Resets any internal variables used to track drag state.
255 - (void)resetDragState;
256
257 // Returns whether or not the tabstrip is currently in reordering mode.
258 - (BOOL)isReorderingTabs;
259
260 // Installs or removes the autoscroll timer.
261 - (void)installAutoscrollTimerIfNeeded;
262 - (void)removeAutoscrollTimer;
263
264 // Called once per autoscroll timer tick. Adjusts the scroll view's content
265 // offset as needed.
266 - (void)autoscrollTimerFired:(NSTimer*)timer;
267
268 // Calculates and stores the autoscroll distance for the given tab view. The
269 // autoscroll distance is a function of the distance between the edge of the
270 // scroll view and the tab's frame.
271 - (void)computeAutoscrollDistanceForTabView:(TabView*)view;
272
273 // Constrains the stored autoscroll distance to prevent the scroll view from
274 // overscrolling.
275 - (void)constrainAutoscrollDistance;
276
277 #if 0
278 // Returns the appropriate model index for the currently dragged tab, given its
279 // current position. (If dropped, the tab would be at this index in the model.)
280 // TODO(rohitrao): Implement this method.
281 - (NSUInteger)modelIndexForDraggedTab;
282 #endif
283
284 // Returns the horizontal visible tab strip width used to compute the tab width
285 // and the tabs and new tab button in regular layout mode.
286 // Takes into account whether or not the mode toggle button is showing.
287 - (CGFloat)tabStripVisibleSpace;
288
289 // Updates the scroll view's content size based on the current set of tabs and
290 // closing tabs. After updating the content size, repositions views so they
291 // they will appear stationary on screen.
292 - (void)updateContentSizeAndRepositionViews;
293
294 // Returns the frame, in the scroll view content's coordinate system, of the
295 // given tab view.
296 - (CGRect)scrollViewFrameForTab:(TabView*)view;
297
298 // Returns the portion of |frame| which is not covered by |frameOnTop|.
299 - (CGRect)calculateVisibleFrameForFrame:(CGRect)frame
300 whenUnderFrame:(CGRect)frameOnTop;
301
302 // Schedules a layout of the scroll view and sets the internal |_animateLayout|
303 // flag so that the layout will be animated.
304 - (void)setNeedsLayoutWithAnimation;
305
306 // Returns the maximum number of collapsed tabs depending on the current layout
307 // mode.
308 - (NSUInteger)maxNumCollapsedTabs;
309
310 // Returns the tab overlap width depending on the current layout mode.
311 - (CGFloat)tabOverlap;
312
313 // Returns the minimum tab view width depending on the current layout mode.
314 - (CGFloat)minTabWidth;
315
316 // Updates the content offset of the tab strip view in order to keep the
317 // selected tab view visible.
318 // Content offset adjustement is only needed/performed in compact mode.
319 // This method must be called with a valid |tabIndex|.
320 - (void)updateContentOffsetForTabIndex:(NSUInteger)tabIndex;
321
322 // Update the frame of the tab strip view (scrollview) frame, content inset and
323 // toggle buttons states depending on the current layout mode.
324 - (void)updateScrollViewFrameForToggleButton;
325
326 @end
327
328 @implementation TabStripController
329
330 @synthesize buttonNewTab = _buttonNewTab;
331 @synthesize hasModeToggleSwitch = _hasModeToggleSwitch;
332 @synthesize hasTabSwitcherToggleSwitch = _hasTabSwitcherToggleSwitch;
333 @synthesize highlightsSelectedTab = _highlightsSelectedTab;
334 @synthesize modeToggleButton = _modeToggleButton;
335 @synthesize tabStripView = _tabStripView;
336 @synthesize tabSwitcherToggleButton = _tabSwitcherToggleButton;
337 @synthesize view = _view;
338
339 - (instancetype)initWithTabModel:(TabModel*)tabModel
340 style:(TabStrip::Style)style {
341 if ((self = [super init])) {
342 _propertyReleaser_TabStripController.Init(self, [TabStripController class]);
343 _tabArray.reset([[NSMutableArray alloc] initWithCapacity:10]);
344 _closingTabs.reset([[NSMutableSet alloc] initWithCapacity:5]);
345
346 _tabModel.reset([tabModel retain]);
347 [_tabModel addObserver:self];
348 _style = style;
349 _hasModeToggleSwitch = NO;
350 _hasTabSwitcherToggleSwitch = NO;
351
352 // |self.view| setup.
353 CGRect tabStripFrame = [UIApplication sharedApplication].keyWindow.bounds;
354 tabStripFrame.size.height = kTabStripHeight;
355 _view = [[UIView alloc] initWithFrame:tabStripFrame];
356 _view.autoresizingMask = (UIViewAutoresizingFlexibleWidth |
357 UIViewAutoresizingFlexibleBottomMargin);
358 _view.backgroundColor = TabStrip::BackgroundColor();
359 if (UseRTLLayout())
360 _view.transform = CGAffineTransformMakeScale(-1, 1);
361
362 // |self.tabStripView| setup.
363 _tabStripView = [[TabStripView alloc] initWithFrame:_view.bounds];
364 _tabStripView.autoresizingMask =
365 (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
366 _tabStripView.backgroundColor = _view.backgroundColor;
367 _tabStripView.layoutDelegate = self;
368 _tabStripView.accessibilityIdentifier = style == TabStrip::kStyleIncognito
369 ? @"Incognito Tab Strip"
370 : @"Tab Strip";
371 [_view addSubview:_tabStripView];
372
373 // |self.buttonNewTab| setup.
374 CGRect buttonNewTabFrame = tabStripFrame;
375 buttonNewTabFrame.size.width = kNewTabButtonWidth;
376 _buttonNewTab = [[UIButton alloc] initWithFrame:buttonNewTabFrame];
377 BOOL isBrowserStateIncognito =
378 tabModel && tabModel.browserState->IsOffTheRecord();
379 _buttonNewTab.tag =
380 isBrowserStateIncognito ? IDC_NEW_INCOGNITO_TAB : IDC_NEW_TAB;
381 // TODO(crbug.com/600829): Rewrite layout code and convert these masks to
382 // to trailing and leading margins rather than right and bottom.
383 _buttonNewTab.autoresizingMask = (UIViewAutoresizingFlexibleRightMargin |
384 UIViewAutoresizingFlexibleBottomMargin);
385 _buttonNewTab.imageView.contentMode = UIViewContentModeCenter;
386 UIImage* buttonNewTabImage = nil;
387 UIImage* buttonNewTabPressedImage = nil;
388 if (_style == TabStrip::kStyleIncognito) {
389 buttonNewTabImage = [UIImage imageNamed:@"tabstrip_new_tab_incognito"];
390 buttonNewTabPressedImage =
391 [UIImage imageNamed:@"tabstrip_new_tab_incognito_pressed"];
392 } else {
393 buttonNewTabImage = [UIImage imageNamed:@"tabstrip_new_tab"];
394 buttonNewTabPressedImage =
395 [UIImage imageNamed:@"tabstrip_new_tab_pressed"];
396 }
397 [_buttonNewTab setImage:buttonNewTabImage forState:UIControlStateNormal];
398 [_buttonNewTab setImage:buttonNewTabPressedImage
399 forState:UIControlStateHighlighted];
400 UIEdgeInsets imageInsets = UIEdgeInsetsMake(
401 kNewTabButtonTopImageInset, kNewTabButtonHorizontalImageInset,
402 kNewTabButtonBottomImageInset, kNewTabButtonHorizontalImageInset);
403 if (IsHighResScreen()) {
404 imageInsets.top += kNewTabButtonBottomOffsetHighRes;
405 imageInsets.bottom -= kNewTabButtonBottomOffsetHighRes;
406 }
407 _buttonNewTab.imageEdgeInsets = imageInsets;
408 SetA11yLabelAndUiAutomationName(
409 _buttonNewTab,
410 isBrowserStateIncognito ? IDS_IOS_TOOLS_MENU_NEW_INCOGNITO_TAB
411 : IDS_IOS_TOOLS_MENU_NEW_TAB,
412 isBrowserStateIncognito ? @"New Incognito Tab" : @"New Tab");
413 // Use a nil target to send |-chromeExecuteCommand:| down the responder
414 // chain.
415 [_buttonNewTab addTarget:nil
416 action:@selector(chromeExecuteCommand:)
417 forControlEvents:UIControlEventTouchUpInside];
418 [_buttonNewTab addTarget:self
419 action:@selector(recordUserMetrics:)
420 forControlEvents:UIControlEventTouchUpInside];
421 [_tabStripView addSubview:_buttonNewTab];
422
423 // Add tab buttons to tab strip.
424 if (_tabModel)
425 [self initializeTabArrayFromTabModel];
426 else
427 [self initializeTabArrayWithNoModel];
428
429 // Update the layout of the tab buttons.
430 [self updateContentSizeAndRepositionViews];
431 [self layoutTabStripSubviews];
432
433 // Don't highlight the selected tab by default.
434 self.highlightsSelectedTab = NO;
435 }
436 return self;
437 }
438
439 - (instancetype)init {
440 NOTREACHED();
441 return nil;
442 }
443
444 - (void)dealloc {
445 [_tabStripView setDelegate:nil];
446 [_tabStripView setLayoutDelegate:nil];
447 [_tabModel removeObserver:self];
448 [super dealloc];
449 }
450
451 - (id<FullScreenControllerDelegate>)fullscreenDelegate {
452 return _fullscreenDelegate;
453 }
454
455 - (void)setFullscreenDelegate:
456 (id<FullScreenControllerDelegate>)fullscreenDelegate {
457 _fullscreenDelegate.reset(fullscreenDelegate);
458 }
459
460 - (void)initializeTabArrayFromTabModel {
461 DCHECK(_tabModel);
462 for (Tab* tab in _tabModel.get()) {
463 BOOL isSelectedTab = [_tabModel currentTab] == tab;
464 TabView* view = [self tabViewForTab:tab isSelected:isSelectedTab];
465 [_tabArray addObject:view];
466 [_tabStripView addSubview:view];
467 }
468 }
469
470 - (void)initializeTabArrayWithNoModel {
471 DCHECK(!_tabModel);
472 TabView* view = [self emptyTabView];
473 [_tabArray addObject:view];
474 [_tabStripView addSubview:view];
475 [view setSelected:YES];
476 return;
477 }
478
479 - (void)setHasModeToggleSwitch:(BOOL)hasModeToggleSwitch {
480 if (_hasModeToggleSwitch && !hasModeToggleSwitch)
481 [self removeModeToggleButton];
482 if (!_hasModeToggleSwitch && hasModeToggleSwitch)
483 [self installModeToggleButton];
484 if (_hasModeToggleSwitch != hasModeToggleSwitch) {
485 _hasModeToggleSwitch = hasModeToggleSwitch;
486 [self updateContentSizeAndRepositionViews];
487 [self setNeedsLayoutWithAnimation];
488 }
489 }
490
491 - (void)setHasTabSwitcherToggleSwitch:(BOOL)hasTabSwitcherToggleSwitch {
492 if (_hasTabSwitcherToggleSwitch && !hasTabSwitcherToggleSwitch)
493 [self removeTabSwitcherToggleButton];
494 if (!_hasTabSwitcherToggleSwitch && hasTabSwitcherToggleSwitch)
495 [self installTabSwitcherToggleButton];
496 if (_hasTabSwitcherToggleSwitch != hasTabSwitcherToggleSwitch) {
497 _hasTabSwitcherToggleSwitch = hasTabSwitcherToggleSwitch;
498 [self updateContentSizeAndRepositionViews];
499 [self setNeedsLayoutWithAnimation];
500 }
501 }
502
503 - (TabView*)emptyTabView {
504 TabView* view =
505 [[[TabView alloc] initWithEmptyView:YES selected:YES] autorelease];
506 [view setIncognitoStyle:(_style == TabStrip::kStyleIncognito)];
507 [view setContentMode:UIViewContentModeRedraw];
508
509 // Setting the tab to be hidden marks it as a new tab. The layout code will
510 // make the tab visible and set up the appropriate animations.
511 [view setHidden:YES];
512
513 return view;
514 }
515
516 - (TabView*)tabViewForTab:(Tab*)tab isSelected:(BOOL)isSelected {
517 TabView* view =
518 [[[TabView alloc] initWithEmptyView:NO selected:isSelected] autorelease];
519 if (UseRTLLayout())
520 [view setTransform:CGAffineTransformMakeScale(-1, 1)];
521 [view setIncognitoStyle:(_style == TabStrip::kStyleIncognito)];
522 [view setContentMode:UIViewContentModeRedraw];
523 [[view titleLabel] setText:[tab title]];
524 [view setFavicon:[tab favicon]];
525
526 // Set the tab buttons' action messages.
527 [view addTarget:self
528 action:@selector(tabTapped:)
529 forControlEvents:UIControlEventTouchUpInside];
530 [[view closeButton] addTarget:self
531 action:@selector(closeTab:)
532 forControlEvents:UIControlEventTouchUpInside];
533
534 // Install a long press gesture recognizer to handle drag and drop.
535 base::scoped_nsobject<UILongPressGestureRecognizer> longPress(
536 [[UILongPressGestureRecognizer alloc]
537 initWithTarget:self
538 action:@selector(handleLongPress:)]);
539 [longPress setMinimumPressDuration:kDragAndDropLongPressDuration];
540 [longPress setDelegate:self];
541 [view addGestureRecognizer:longPress];
542
543 // Giving the tab view exclusive touch prevents other views from receiving
544 // touches while a TabView is handling a touch.
545 [view setExclusiveTouch:YES];
546
547 // Setting the tab to be hidden marks it as a new tab. The layout code will
548 // make the tab visible and set up the appropriate animations.
549 [view setHidden:YES];
550
551 return view;
552 }
553
554 - (void)setHighlightsSelectedTab:(BOOL)highlightsSelectedTab {
555 if (highlightsSelectedTab)
556 [self installDimmingViewWithAnimation:YES];
557 else
558 [self removeDimmingViewWithAnimation:YES];
559
560 _highlightsSelectedTab = highlightsSelectedTab;
561 }
562
563 - (void)installDimmingViewWithAnimation:(BOOL)animate {
564 // The dimming view should not cover the bottom 2px of the tab strip, as those
565 // pixels are visually part of the top border of the toolbar. The bottom
566 // inset constants take into account the conversion from pixels to points.
567 CGRect frame = [_tabStripView bounds];
568 frame.size.height -= (IsHighResScreen() ? kDimmingViewBottomInsetHighRes
569 : kDimmingViewBottomInset);
570
571 // Create the dimming view if it doesn't exist. In all cases, make sure it's
572 // set up correctly.
573 if (_dimmingView.get())
574 [_dimmingView setFrame:frame];
575 else
576 _dimmingView.reset([[UIView alloc] initWithFrame:frame]);
577
578 // Enable user interaction in order to eat touches from views behind it.
579 [_dimmingView setUserInteractionEnabled:YES];
580 [_dimmingView setBackgroundColor:[TabStrip::BackgroundColor()
581 colorWithAlphaComponent:0]];
582 [_dimmingView setAutoresizingMask:(UIViewAutoresizingFlexibleWidth |
583 UIViewAutoresizingFlexibleHeight)];
584 [_tabStripView addSubview:_dimmingView];
585
586 CGFloat duration = animate ? kTabStripFadeAnimationDuration : 0;
587 [UIView animateWithDuration:duration
588 animations:^{
589 [_dimmingView
590 setBackgroundColor:[TabStrip::BackgroundColor()
591 colorWithAlphaComponent:0.6]];
592 }];
593 }
594
595 - (void)removeDimmingViewWithAnimation:(BOOL)animate {
596 if (_dimmingView) {
597 CGFloat duration = animate ? kTabStripFadeAnimationDuration : 0;
598 [UIView animateWithDuration:duration
599 animations:^{
600 [_dimmingView setBackgroundColor:[TabStrip::BackgroundColor()
601 colorWithAlphaComponent:0]];
602 }
603 completion:^(BOOL finished) {
604 // Do not remove the dimming view if the animation was aborted.
605 if (finished) {
606 [_dimmingView removeFromSuperview];
607 _dimmingView.reset();
608 }
609 }];
610 }
611 }
612
613 - (void)recordUserMetrics:(id)sender {
614 if (sender == _buttonNewTab)
615 base::RecordAction(UserMetricsAction("MobileTabStripNewTab"));
616 else if (sender == _modeToggleButton)
617 base::RecordAction(UserMetricsAction("MobileTabStripSwitchMode"));
618 else if (sender == _tabSwitcherToggleButton)
619 base::RecordAction(UserMetricsAction("MobileTabSwitcherOpen"));
620 else
621 LOG(WARNING) << "Trying to record metrics for unknown sender "
622 << base::SysNSStringToUTF8([sender description]);
623 }
624
625 - (void)tabTapped:(id)sender {
626 DCHECK([sender isKindOfClass:[TabView class]]);
627
628 // Ignore taps while in reordering mode.
629 if ([self isReorderingTabs])
630 return;
631
632 NSUInteger index = [self modelIndexForTabView:(TabView*)sender];
633 DCHECK_NE(NSNotFound, static_cast<NSInteger>(index));
634 if (index == NSNotFound)
635 return;
636 Tab* tappedTab = [_tabModel tabAtIndex:index];
637 Tab* currentTab = [_tabModel currentTab];
638 if (IsIPadIdiom() && (currentTab != tappedTab)) {
639 [currentTab updateSnapshotWithOverlay:YES visibleFrameOnly:YES];
640 }
641 [_tabModel setCurrentTab:tappedTab];
642 [self updateContentOffsetForTabIndex:index];
643 }
644
645 - (void)closeTab:(id)sender {
646 // Ignore taps while in reordering mode.
647 // TODO(rohitrao): We should just hide the close buttons instead.
648 if ([self isReorderingTabs])
649 return;
650
651 base::RecordAction(UserMetricsAction("MobileTabStripCloseTab"));
652 DCHECK([sender isKindOfClass:[UIButton class]]);
653 UIView* superview = [sender superview];
654 DCHECK([superview isKindOfClass:[TabView class]]);
655 TabView* tab = (TabView*)superview;
656 NSUInteger modelIndex = [self modelIndexForTabView:tab];
657 if (modelIndex != NSNotFound)
658 [_tabModel closeTabAtIndex:modelIndex];
659 }
660
661 - (void)handleLongPress:(UILongPressGestureRecognizer*)gesture {
662 switch ([gesture state]) {
663 case UIGestureRecognizerStateBegan:
664 [[NSNotificationCenter defaultCenter]
665 postNotificationName:kTabStripDragStarted
666 object:nil];
667 [self beginDrag:gesture];
668 break;
669 case UIGestureRecognizerStateChanged:
670 [self continueDrag:gesture];
671 break;
672 case UIGestureRecognizerStateEnded:
673 [self endDrag:gesture];
674 [[NSNotificationCenter defaultCenter]
675 postNotificationName:kTabStripDragEnded
676 object:nil];
677 break;
678 case UIGestureRecognizerStateCancelled:
679 [self cancelDrag:gesture];
680 [[NSNotificationCenter defaultCenter]
681 postNotificationName:kTabStripDragEnded
682 object:nil];
683 break;
684 default:
685 NOTREACHED();
686 }
687 }
688
689 - (NSUInteger)indexForModelIndex:(NSUInteger)modelIndex {
690 NSUInteger index = modelIndex;
691 NSUInteger i = 0;
692 for (TabView* tab in _tabArray.get()) {
693 if ([_closingTabs containsObject:tab])
694 ++index;
695
696 if (i == index)
697 break;
698
699 ++i;
700 }
701
702 DCHECK_GE(index, modelIndex);
703 return index;
704 }
705
706 - (NSUInteger)modelIndexForIndex:(NSUInteger)index {
707 NSUInteger modelIndex = 0;
708 NSUInteger arrayIndex = 0;
709 for (TabView* tab in _tabArray.get()) {
710 if (arrayIndex == index) {
711 if ([_closingTabs containsObject:tab])
712 return NSNotFound;
713 return modelIndex;
714 }
715
716 if (![_closingTabs containsObject:tab])
717 ++modelIndex;
718
719 ++arrayIndex;
720 }
721
722 return NSNotFound;
723 }
724
725 - (NSUInteger)modelIndexForTabView:(TabView*)view {
726 return [self modelIndexForIndex:[_tabArray indexOfObject:view]];
727 }
728
729 #pragma mark -
730 #pragma mark Tab Drag and Drop methods
731
732 - (void)beginDrag:(UILongPressGestureRecognizer*)gesture {
733 DCHECK([[gesture view] isKindOfClass:[TabView class]]);
734 TabView* view = (TabView*)[gesture view];
735
736 // Sanity checks.
737 NSUInteger index = [self modelIndexForTabView:view];
738 DCHECK_NE(NSNotFound, static_cast<NSInteger>(index));
739 if (index == NSNotFound)
740 return;
741
742 // Install the dimming view, hide the new tab button, and select the tab so it
743 // appears highlighted.
744 Tab* tab = [_tabModel tabAtIndex:index];
745 self.highlightsSelectedTab = YES;
746 _buttonNewTab.hidden = YES;
747 [_tabModel setCurrentTab:tab];
748
749 // Set up initial drag state.
750 _lastDragLocation = [gesture locationInView:[_tabStripView superview]];
751 _isReordering = YES;
752 _draggedTab.reset([view retain]);
753 _placeholderGapModelIndex = [self modelIndexForTabView:_draggedTab];
754
755 // Update the autoscroll distance and timer.
756 [self computeAutoscrollDistanceForTabView:_draggedTab];
757 if (_autoscrollDistance != 0)
758 [self installAutoscrollTimerIfNeeded];
759 else
760 [self removeAutoscrollTimer];
761 }
762
763 - (void)continueDrag:(UILongPressGestureRecognizer*)gesture {
764 DCHECK([[gesture view] isKindOfClass:[TabView class]]);
765 TabView* view = (TabView*)[gesture view];
766
767 // Update the position of the dragged tab.
768 CGPoint location = [gesture locationInView:[_tabStripView superview]];
769 CGFloat dx = location.x - _lastDragLocation.x;
770 CGRect frame = [view frame];
771 frame.origin.x += dx;
772 [view setFrame:frame];
773 _lastDragLocation = location;
774
775 // Update the autoscroll distance and timer.
776 [self computeAutoscrollDistanceForTabView:_draggedTab];
777 if (_autoscrollDistance != 0)
778 [self installAutoscrollTimerIfNeeded];
779 else
780 [self removeAutoscrollTimer];
781
782 [self setNeedsLayoutWithAnimation];
783 }
784
785 - (void)endDrag:(UILongPressGestureRecognizer*)gesture {
786 DCHECK([[gesture view] isKindOfClass:[TabView class]]);
787
788 NSUInteger fromIndex = [self modelIndexForTabView:_draggedTab];
789 // TODO(rohitrao): We're seeing crashes where fromIndex is NSNotFound,
790 // indicating that the dragged tab is no longer in the TabModel. This could
791 // happen if a tab closed itself during a drag. Investigate this further, but
792 // for now, simply test |fromIndex| before proceeding.
793 if (fromIndex == NSNotFound) {
794 [self resetDragState];
795 [self setNeedsLayoutWithAnimation];
796 return;
797 }
798
799 Tab* tab = [_tabModel tabAtIndex:fromIndex];
800 NSUInteger toIndex = _placeholderGapModelIndex;
801 DCHECK_NE(NSNotFound, static_cast<NSInteger>(toIndex));
802 DCHECK_LT(toIndex, [_tabModel count]);
803
804 // Reset drag state variables before notifying the model that the tab moved.
805 [self resetDragState];
806
807 [_tabModel moveTab:tab toIndex:toIndex];
808 [self setNeedsLayoutWithAnimation];
809 }
810
811 - (void)cancelDrag:(UILongPressGestureRecognizer*)gesture {
812 DCHECK([[gesture view] isKindOfClass:[TabView class]]);
813
814 // Reset drag state and trigger a relayout to moved tabs back into their
815 // correct positions.
816 [self resetDragState];
817 [self setNeedsLayoutWithAnimation];
818 }
819
820 - (void)resetDragState {
821 self.highlightsSelectedTab = NO;
822 _buttonNewTab.hidden = NO;
823 [self removeAutoscrollTimer];
824
825 _isReordering = NO;
826 _placeholderGapModelIndex = NSNotFound;
827 _draggedTab.reset();
828 }
829
830 - (BOOL)isReorderingTabs {
831 return _isReordering;
832 }
833
834 - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer*)recognizer {
835 DCHECK([recognizer isKindOfClass:[UILongPressGestureRecognizer class]]);
836
837 // If a drag is already in progress, do not allow another to start.
838 return ![self isReorderingTabs];
839 }
840
841 #pragma mark -
842 #pragma mark Autoscroll methods
843
844 - (void)installAutoscrollTimerIfNeeded {
845 if (_autoscrollTimer)
846 return;
847
848 _autoscrollTimer =
849 [NSTimer scheduledTimerWithTimeInterval:(1.0 / 60.0)
850 target:self
851 selector:@selector(autoscrollTimerFired:)
852 userInfo:nil
853 repeats:YES];
854 }
855
856 - (void)removeAutoscrollTimer {
857 [_autoscrollTimer invalidate];
858 _autoscrollTimer = nil;
859 }
860
861 - (void)autoscrollTimerFired:(NSTimer*)timer {
862 [self constrainAutoscrollDistance];
863
864 CGPoint offset = [_tabStripView contentOffset];
865 offset.x += _autoscrollDistance;
866 [_tabStripView setContentOffset:offset];
867
868 // Fixed-position views need to have their frames adusted to compensate for
869 // the content offset shift. These include the dragged tab, the dimming
870 // view, and the new tab button.
871 CGRect tabFrame = [_draggedTab frame];
872 tabFrame.origin.x += _autoscrollDistance;
873 [_draggedTab setFrame:tabFrame];
874
875 CGRect dimFrame = [_dimmingView frame];
876 dimFrame.origin.x += _autoscrollDistance;
877 [_dimmingView setFrame:dimFrame];
878
879 // Even though the new tab button is hidden during drag and drop, keep its
880 // frame updated to prevent it from animating back into place when the drag
881 // finishes.
882 CGRect newTabFrame = [_buttonNewTab frame];
883 newTabFrame.origin.x += _autoscrollDistance;
884 [_buttonNewTab setFrame:newTabFrame];
885
886 // TODO(rohitrao): Find a good way to re-enable the sliding over animation
887 // when autoscrolling. Right now any running animations are immediately
888 // stopped by the next call to autoscrollTimerFired.
889 [_tabStripView setNeedsLayout];
890 }
891
892 - (void)computeAutoscrollDistanceForTabView:(TabView*)view {
893 CGRect scrollBounds = [_tabStripView bounds];
894 CGRect viewFrame = [view frame];
895
896 // The distance between this tab and the edge of the scroll view.
897 CGFloat distanceFromEdge =
898 MIN(CGRectGetMinX(viewFrame) - CGRectGetMinX(scrollBounds),
899 CGRectGetMaxX(scrollBounds) - CGRectGetMaxX(viewFrame));
900 if (distanceFromEdge < 0)
901 distanceFromEdge = 0;
902
903 // Negative if the tab is closer to the left edge of the scroll view, positive
904 // if it is closer to the right edge.
905 CGFloat leftRightMultiplier =
906 (CGRectGetMidX(viewFrame) < CGRectGetMidX(scrollBounds)) ? -1.0 : 1.0;
907
908 // The autoscroll distance decreases linearly as the tab view gets further
909 // from the edge of the scroll view.
910 _autoscrollDistance =
911 leftRightMultiplier *
912 MAX(0.0, ceilf(kMaxAutoscrollDistance -
913 distanceFromEdge / kAutoscrollDecrementWidth));
914 }
915
916 - (void)constrainAutoscrollDistance {
917 // Make sure autoscroll distance is not so large as to cause overscroll.
918 CGPoint offset = [_tabStripView contentOffset];
919
920 // Check to make sure there is no overscroll off the right edge.
921 CGFloat maxOffset = [_tabStripView contentSize].width -
922 CGRectGetWidth([_tabStripView bounds]);
923 if (offset.x + _autoscrollDistance > maxOffset)
924 _autoscrollDistance = (maxOffset - offset.x);
925
926 // Perform the left edge check after the right edge check, to prevent
927 // right-justifying the tabs when there is no overflow.
928 if (offset.x + _autoscrollDistance < 0)
929 _autoscrollDistance = -offset.x;
930 }
931
932 #pragma mark -
933 #pragma mark TabStripModelObserver methods
934
935 // Observer method.
936 - (void)tabModel:(TabModel*)model
937 didInsertTab:(Tab*)tab
938 atIndex:(NSUInteger)modelIndex
939 inForeground:(BOOL)fg {
940 TabView* view = [self tabViewForTab:tab isSelected:fg];
941 [_tabArray insertObject:view atIndex:[self indexForModelIndex:modelIndex]];
942 [[self tabStripView] addSubview:view];
943
944 [self updateContentSizeAndRepositionViews];
945 [self setNeedsLayoutWithAnimation];
946 [self updateContentOffsetForTabIndex:modelIndex];
947 }
948
949 // Observer method.
950 - (void)tabModel:(TabModel*)model
951 didRemoveTab:(Tab*)tab
952 atIndex:(NSUInteger)modelIndex {
953 // Keep the actual view around while it is animating out. Once the animation
954 // is done, remove the view.
955 NSUInteger index = [self indexForModelIndex:modelIndex];
956 TabView* view = [_tabArray objectAtIndex:index];
957 [_closingTabs addObject:view];
958 _targetFrames.RemoveFrame(view);
959
960 // Adjust the content size now that the tab has been removed from the model.
961 [self updateContentSizeAndRepositionViews];
962
963 // Signal the FullscreenController that the toolbar needs to stay on
964 // screen for a bit, so the animation is visible.
965 [[NSNotificationCenter defaultCenter]
966 postNotificationName:kWillStartTabStripTabAnimation
967 object:nil];
968
969 // Leave the view where it is horizontally and animate it downwards out of
970 // sight.
971 CGRect frame = [view frame];
972 frame = CGRectOffset(frame, 0, CGRectGetHeight(frame));
973 [UIView animateWithDuration:kTabAnimationDuration
974 animations:^{
975 [view setFrame:frame];
976 }
977 completion:^(BOOL finished) {
978 [view removeFromSuperview];
979 [_tabArray removeObject:view];
980 [_closingTabs removeObject:view];
981 }];
982
983 [self setNeedsLayoutWithAnimation];
984 }
985
986 // Observer method.
987 - (void)tabModel:(TabModel*)model
988 didMoveTab:(Tab*)tab
989 fromIndex:(NSUInteger)fromIndex
990 toIndex:(NSUInteger)toIndex {
991 DCHECK(!_isReordering);
992
993 // Reorder the objects in _tabArray to keep in sync with the model ordering.
994 NSUInteger arrayIndex = [self indexForModelIndex:fromIndex];
995 base::scoped_nsobject<TabView> view(
996 [[_tabArray objectAtIndex:arrayIndex] retain]);
997 [_tabArray removeObject:view];
998 [_tabArray insertObject:view atIndex:toIndex];
999 [self setNeedsLayoutWithAnimation];
1000 }
1001
1002 // Observer method.
1003 - (void)tabModel:(TabModel*)model
1004 didChangeActiveTab:(Tab*)newTab
1005 previousTab:(Tab*)previousTab
1006 atIndex:(NSUInteger)modelIndex {
1007 for (TabView* view in _tabArray.get()) {
1008 [view setSelected:NO];
1009 }
1010
1011 NSUInteger index = [self indexForModelIndex:modelIndex];
1012 TabView* activeView = [_tabArray objectAtIndex:index];
1013 [activeView setSelected:YES];
1014
1015 // No need to animate this change, as selecting a new tab simply changes the
1016 // z-ordering of the TabViews. If a new tab was selected as a result of a tab
1017 // closure, then the animated layout has already been scheduled.
1018 [_tabStripView setNeedsLayout];
1019 }
1020
1021 // Observer method.
1022 - (void)tabModel:(TabModel*)model didChangeTab:(Tab*)tab {
1023 NSUInteger modelIndex = [_tabModel indexOfTab:tab];
1024 if (modelIndex == NSNotFound) {
1025 DCHECK(false) << "Received notification for a Tab that is not contained in "
1026 << "the TabModel";
1027 return;
1028 }
1029 NSUInteger index = [self indexForModelIndex:modelIndex];
1030 TabView* view = [_tabArray objectAtIndex:index];
1031 [view setTitle:tab.title];
1032 [view setFavicon:[tab favicon]];
1033 if (tab.webState->IsLoading())
1034 [view startProgressSpinner];
1035 else
1036 [view stopProgressSpinner];
1037 [view setNeedsDisplay];
1038 }
1039
1040 // Observer method.
1041 - (void)tabModel:(TabModel*)model
1042 didReplaceTab:(Tab*)oldTab
1043 withTab:(Tab*)newTab
1044 atIndex:(NSUInteger)index {
1045 // TabViews do not hold references to their parent Tabs, so it's safe to treat
1046 // this as a tab change rather than a tab replace.
1047 [self tabModel:model didChangeTab:newTab];
1048 }
1049
1050 #pragma mark -
1051 #pragma mark Views and Layout
1052
1053 - (void)installModeToggleButton {
1054 // Add the mode toggle button view.
1055 DCHECK(!_modeToggleButton);
1056 UIImage* toggleIcon = nil;
1057 int toggleIdsAccessibilityLabel;
1058 NSString* toggleEnglishUiAutomationName;
1059 if (_style == TabStrip::kStyleDark) {
1060 toggleIcon = [UIImage imageNamed:@"tabstrip_switch"];
1061 toggleIdsAccessibilityLabel = IDS_IOS_SWITCH_BROWSER_MODE_ENTER_INCOGNITO;
1062 toggleEnglishUiAutomationName = @"Enter Incognito* Mode";
1063 } else {
1064 toggleIcon = [UIImage imageNamed:@"tabstrip_incognito_switch"];
1065 toggleIdsAccessibilityLabel = IDS_IOS_SWITCH_BROWSER_MODE_LEAVE_INCOGNITO;
1066 toggleEnglishUiAutomationName = @"Leave Incognito* Mode";
1067 }
1068 const CGFloat tabStripHeight = _view.frame.size.height;
1069 CGRect buttonFrame =
1070 CGRectMake(CGRectGetMaxX(_view.frame) - kModeToggleButtonWidth, 0.0,
1071 kModeToggleButtonWidth, tabStripHeight);
1072 _modeToggleButton = [UIButton buttonWithType:UIButtonTypeCustom];
1073 _modeToggleButton.frame = buttonFrame;
1074 [_modeToggleButton setImageEdgeInsets:UIEdgeInsetsMake(7, 5, 7, 5)];
1075 _modeToggleButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
1076 _modeToggleButton.backgroundColor = [UIColor clearColor];
1077 [_modeToggleButton setImage:toggleIcon forState:UIControlStateNormal];
1078 // Set target/action to bubble up with command id as tag.
1079 [_modeToggleButton addTarget:nil
1080 action:@selector(chromeExecuteCommand:)
1081 forControlEvents:UIControlEventTouchUpInside];
1082 [_modeToggleButton setTag:IDC_SWITCH_BROWSER_MODES];
1083 [_modeToggleButton addTarget:self
1084 action:@selector(recordUserMetrics:)
1085 forControlEvents:UIControlEventTouchUpInside];
1086
1087 SetA11yLabelAndUiAutomationName(_modeToggleButton,
1088 toggleIdsAccessibilityLabel,
1089 toggleEnglishUiAutomationName);
1090 [_view addSubview:_modeToggleButton];
1091 // Shrink the scroll view.
1092 [self updateScrollViewFrameForToggleButton];
1093 }
1094
1095 - (void)removeModeToggleButton {
1096 // Remove the button view.
1097 DCHECK(_modeToggleButton);
1098 [_modeToggleButton removeFromSuperview];
1099 _modeToggleButton = nil;
1100 // Extend the scroll view.
1101 [self updateScrollViewFrameForToggleButton];
1102 }
1103
1104 - (CGFloat)tabStripVisibleSpace {
1105 CGFloat availableSpace = CGRectGetWidth([_tabStripView bounds]) -
1106 CGRectGetWidth([_buttonNewTab frame]) +
1107 kNewTabOverlap;
1108 if (IsCompactTablet()) {
1109 if ([self hasModeToggleSwitch])
1110 availableSpace -= kNewTabRightPadding + kModeToggleButtonWidth;
1111 else
1112 availableSpace -= kNewTabRightPadding;
1113 } else {
1114 if (![self hasModeToggleSwitch])
1115 availableSpace -= kNewTabRightPadding;
1116 }
1117 return availableSpace;
1118 }
1119
1120 - (void)installTabSwitcherToggleButton {
1121 // Add the mode toggle button view.
1122 DCHECK(!_tabSwitcherToggleButton);
1123 UIImage* tabSwitcherToggleIcon =
1124 [UIImage imageNamed:@"tabswitcher_tab_switcher_button"];
1125 tabSwitcherToggleIcon = [tabSwitcherToggleIcon
1126 imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
1127 int tabSwitcherToggleIdsAccessibilityLabel =
1128 IDS_IOS_TAB_STRIP_ENTER_TAB_SWITCHER;
1129 NSString* tabSwitcherToggleEnglishUiAutomationName = @"Enter Tab Switcher";
1130 const CGFloat tabStripHeight = _view.frame.size.height;
1131 CGRect buttonFrame =
1132 CGRectMake(CGRectGetMaxX(_view.frame) - kTabSwitcherToggleButtonWidth,
1133 0.0, kTabSwitcherToggleButtonWidth, tabStripHeight);
1134 _tabSwitcherToggleButton = [UIButton buttonWithType:UIButtonTypeCustom];
1135 [_tabSwitcherToggleButton setTintColor:[UIColor whiteColor]];
1136 _tabSwitcherToggleButton.frame = buttonFrame;
1137 [_tabSwitcherToggleButton setContentMode:UIViewContentModeCenter];
1138 _tabSwitcherToggleButton.autoresizingMask =
1139 UIViewAutoresizingFlexibleLeftMargin;
1140 _tabSwitcherToggleButton.backgroundColor = [UIColor clearColor];
1141 _tabSwitcherToggleButton.exclusiveTouch = YES;
1142 [_tabSwitcherToggleButton setImage:tabSwitcherToggleIcon
1143 forState:UIControlStateNormal];
1144 // Set target/action to bubble up with command id as tag.
1145 [_tabSwitcherToggleButton addTarget:nil
1146 action:@selector(chromeExecuteCommand:)
1147 forControlEvents:UIControlEventTouchUpInside];
1148 [_tabSwitcherToggleButton setTag:IDC_TOGGLE_TAB_SWITCHER];
1149 [_tabSwitcherToggleButton addTarget:self
1150 action:@selector(recordUserMetrics:)
1151 forControlEvents:UIControlEventTouchUpInside];
1152
1153 SetA11yLabelAndUiAutomationName(_tabSwitcherToggleButton,
1154 tabSwitcherToggleIdsAccessibilityLabel,
1155 tabSwitcherToggleEnglishUiAutomationName);
1156 [_view addSubview:_tabSwitcherToggleButton];
1157 // Shrink the scroll view.
1158 [self updateScrollViewFrameForToggleButton];
1159 }
1160
1161 - (void)removeTabSwitcherToggleButton {
1162 // Remove the button view.
1163 DCHECK(_tabSwitcherToggleButton);
1164 [_tabSwitcherToggleButton removeFromSuperview];
1165 _tabSwitcherToggleButton = nil;
1166 // Extend the scroll view.
1167 [self updateScrollViewFrameForToggleButton];
1168 }
1169
1170 - (void)updateContentSizeAndRepositionViews {
1171 // TODO(rohitrao): The following lines are duplicated in
1172 // layoutTabStripSubviews. Find a way to consolidate this logic.
1173 const NSUInteger tabCount = [_tabArray count] - [_closingTabs count];
1174 if (!tabCount)
1175 return;
1176 const CGFloat tabHeight = CGRectGetHeight([_tabStripView bounds]);
1177 CGFloat visibleSpace = [self tabStripVisibleSpace];
1178 _currentTabWidth =
1179 (visibleSpace + ([self tabOverlap] * (tabCount - 1))) / tabCount;
1180 _currentTabWidth = MIN(_currentTabWidth, kMaxTabWidth);
1181 _currentTabWidth = MAX(_currentTabWidth, [self minTabWidth]);
1182
1183 // Set the content size to be large enough to contain all the tabs at the
1184 // desired width, with the standard overlap, plus the new tab button.
1185 CGSize contentSize = CGSizeMake(
1186 _currentTabWidth * tabCount - ([self tabOverlap] * (tabCount - 1)) +
1187 CGRectGetWidth([_buttonNewTab frame]) - kNewTabOverlap,
1188 tabHeight);
1189 if (CGSizeEqualToSize([_tabStripView contentSize], contentSize))
1190 return;
1191
1192 // Background: The scroll view might change the content offset when updating
1193 // the content size. This can happen when the old content offset would result
1194 // in an overscroll at the new content size. (Note that the content offset
1195 // will never change if the content size is growing.)
1196 //
1197 // To handle this without making views appear to jump, shift all of the
1198 // subviews by an amount equal to the size change. This effectively places
1199 // the subviews back where they were before the change, in terms of screen
1200 // coordinates.
1201 CGPoint oldOffset = [_tabStripView contentOffset];
1202 [_tabStripView setContentSize:contentSize];
1203
1204 CGFloat dx = [_tabStripView contentOffset].x - oldOffset.x;
1205 for (UIView* view in [_tabStripView subviews]) {
1206 CGRect frame = [view frame];
1207 frame.origin.x += dx;
1208 [view setFrame:frame];
1209 _targetFrames.AddFrame(view, frame);
1210 }
1211 }
1212
1213 - (CGRect)scrollViewFrameForTab:(TabView*)view {
1214 NSUInteger index = [self modelIndexForTabView:view];
1215
1216 CGRect frame = [view frame];
1217 frame.origin.x =
1218 (_currentTabWidth * index) - ([self tabOverlap] * (index - 1));
1219 return frame;
1220 }
1221
1222 - (CGRect)calculateVisibleFrameForFrame:(CGRect)frame
1223 whenUnderFrame:(CGRect)frameOnTop {
1224 CGFloat minX = CGRectGetMinX(frame);
1225 CGFloat maxX = CGRectGetMaxX(frame);
1226
1227 if (CGRectGetMinX(frame) < CGRectGetMinX(frameOnTop))
1228 maxX = CGRectGetMinX(frameOnTop);
1229 else
1230 minX = CGRectGetMaxX(frameOnTop);
1231
1232 frame.origin.x = minX;
1233 frame.size.width = maxX - minX;
1234 return frame;
1235 }
1236
1237 #pragma mark -
1238 #pragma mark - compact layout
1239
1240 - (NSUInteger)maxNumCollapsedTabs {
1241 return IsCompactTablet() ? kMaxNumCollapsedTabsForCompactLayout
1242 : kMaxNumCollapsedTabs;
1243 }
1244
1245 - (CGFloat)tabOverlap {
1246 return IsCompactTablet() ? kTabOverlapForCompactLayout : kTabOverlap;
1247 }
1248
1249 - (CGFloat)minTabWidth {
1250 return IsCompactTablet() ? kMinTabWidthForCompactLayout : kMinTabWidth;
1251 }
1252
1253 - (void)updateContentOffsetForTabIndex:(NSUInteger)tabIndex {
1254 DCHECK_NE(NSNotFound, static_cast<NSInteger>(tabIndex));
1255
1256 if (IsCompactTablet()) {
1257 if (tabIndex == [_tabArray count] - 1) {
1258 const CGFloat tabStripAvailableSpace =
1259 _tabStripView.frame.size.width - _tabStripView.contentInset.right;
1260 if (_tabStripView.contentSize.width > tabStripAvailableSpace) {
1261 CGFloat scrollToPoint =
1262 _tabStripView.contentSize.width - tabStripAvailableSpace;
1263 [_tabStripView setContentOffset:CGPointMake(scrollToPoint, 0)
1264 animated:YES];
1265 }
1266 } else {
1267 TabView* tabView = [_tabArray objectAtIndex:tabIndex];
1268 CGRect scrollRect =
1269 CGRectInset(tabView.frame, -_tabStripView.contentInset.right, 0);
1270 if (tabView)
1271 [_tabStripView scrollRectToVisible:scrollRect animated:YES];
1272 }
1273 }
1274 }
1275
1276 - (void)updateScrollViewFrameForToggleButton {
1277 CGRect tabFrame = _tabStripView.frame;
1278 tabFrame.size.width = _view.bounds.size.width;
1279 if (!IsCompactTablet()) {
1280 if (_modeToggleButton)
1281 tabFrame.size.width -= kModeToggleButtonWidth;
1282 if (_tabSwitcherToggleButton)
1283 tabFrame.size.width -= kTabSwitcherToggleButtonWidth;
1284 _tabStripView.contentInset = UIEdgeInsetsZero;
1285 [_toggleButtonBackgroundView setHidden:YES];
1286 } else {
1287 if (!_toggleButtonBackgroundView) {
1288 _toggleButtonBackgroundView.reset([[UIImageView alloc] init]);
1289 const CGFloat tabStripHeight = _view.frame.size.height;
1290 const CGRect backgroundViewFrame = CGRectMake(
1291 CGRectGetMaxX(_view.frame) - kModeToggleButtonBackgroundWidth, 0.0,
1292 kModeToggleButtonBackgroundWidth, tabStripHeight);
1293 [_toggleButtonBackgroundView setFrame:backgroundViewFrame];
1294 [_toggleButtonBackgroundView
1295 setAutoresizingMask:UIViewAutoresizingFlexibleLeftMargin];
1296 UIImage* backgroundToggleImage =
1297 [UIImage imageNamed:@"tabstrip_toggle_button_gradient"];
1298 [_toggleButtonBackgroundView setImage:backgroundToggleImage];
1299 [_view addSubview:_toggleButtonBackgroundView];
1300 }
1301 const BOOL hasModeToggleButton =
1302 _modeToggleButton || _tabSwitcherToggleButton;
1303 [_toggleButtonBackgroundView setHidden:!hasModeToggleButton];
1304 if (!hasModeToggleButton)
1305 _tabStripView.contentInset = UIEdgeInsetsZero;
1306 if (_modeToggleButton) {
1307 _tabStripView.contentInset =
1308 UIEdgeInsetsMake(0, 0, 0, kModeToggleButtonWidth);
1309 [_view bringSubviewToFront:_modeToggleButton];
1310 }
1311 if (_tabSwitcherToggleButton) {
1312 _tabStripView.contentInset =
1313 UIEdgeInsetsMake(0, 0, 0, kTabSwitcherToggleButtonWidth);
1314 [_view bringSubviewToFront:_tabSwitcherToggleButton];
1315 }
1316 }
1317 [_tabStripView setFrame:tabFrame];
1318 }
1319
1320 #pragma mark - TabStripViewLayoutDelegate
1321
1322 // Creates TabViews for each Tab in the TabModel and positions them in the
1323 // correct location onscreen.
1324 - (void)layoutTabStripSubviews {
1325 const NSUInteger tabCount = [_tabArray count] - [_closingTabs count];
1326 if (!tabCount)
1327 return;
1328 BOOL animate = _animateLayout;
1329 _animateLayout = NO;
1330 // Disable the animation if the tab count is changing from 0 to 1.
1331 if (tabCount == 1 && [_closingTabs count] == 0)
1332 animate = NO;
1333
1334 const CGFloat tabHeight = CGRectGetHeight([_tabStripView bounds]);
1335
1336 // In compact layout mode the space used to layout the tabs is not
1337 // constrained and uses the whole scroll view content size width. In regular
1338 // layout mode the available space is constrained to the visible space.
1339 CGFloat availableSpace = IsCompactTablet() ? _tabStripView.contentSize.width
1340 : [self tabStripVisibleSpace];
1341
1342 // The array and model indexes of the selected tab.
1343 NSUInteger selectedModelIndex = [_tabModel indexOfTab:[_tabModel currentTab]];
1344 NSUInteger selectedArrayIndex = [self indexForModelIndex:selectedModelIndex];
1345
1346 // This method lays out tabs in two coordinate systems. The first, the
1347 // "virtual" coordinate system, is a system rooted at x=0 that contains all
1348 // the tabs laid out as if the tabstrip was infinitely long. In this system,
1349 // |virtualMinX| contains the starting X coordinate of the next tab to be
1350 // placed and |virtualMaxX| contains the maximum X coordinate of the last tab
1351 // to be placed.
1352 //
1353 // The scroll view's content area is sized to be large enough to hold all the
1354 // tabs with proper overlap, but the viewport is set to only show a part of
1355 // the content area. The specific part that is shown is given by the scroll
1356 // view's contentOffset.
1357 //
1358 // To layout tabs, first calculate where the tab should be in the "virtual"
1359 // coordinate system. This gives the frame of the tab assuming the tabstrip
1360 // was large enough to hold all tabs without needing to overflow. Then,
1361 // adjust the tab's virtual frame to move it onscreen. This gives the tab's
1362 // real frame.
1363 CGFloat virtualMinX = 0;
1364 CGFloat virtualMaxX = 0;
1365 CGFloat offset = IsCompactTablet() ? 0 : [_tabStripView contentOffset].x;
1366
1367 // Keeps track of which tabs need to be animated. Using an autoreleased array
1368 // instead of scoped_nsobject because scoped_nsobject doesn't seem to work
1369 // well with blocks.
1370 NSMutableArray* tabsNeedingAnimation =
1371 [NSMutableArray arrayWithCapacity:tabCount];
1372
1373 CGRect dragFrame = [_draggedTab frame];
1374
1375 TabView* previousTabView = nil;
1376 CGRect previousTabFrame = CGRectZero;
1377 BOOL hasPlaceholderGap = NO;
1378 for (NSUInteger arrayIndex = 0; arrayIndex < [_tabArray count];
1379 ++arrayIndex) {
1380 TabView* view = (TabView*)[_tabArray objectAtIndex:arrayIndex];
1381
1382 // Arrange the tabs in a V going backwards from the selected tab. This
1383 // differs from desktop in order to make the tab overflow behavior work (on
1384 // desktop, the tabs are arranged going backwards from left to right, with
1385 // the selected tab above all others).
1386 //
1387 // When reordering, use slightly different logic. Instead of a V based on
1388 // the model indexes of the tabs, the V fans out from the placeholder gap,
1389 // which is visually where the dragged tab is. In reordering mode, the tabs
1390 // are not necessarily z-ordered according to their model indexes, because
1391 // they are not necessarily drawn in the spot dictated by their current
1392 // model index.
1393 BOOL isSelectedTab = (arrayIndex == selectedArrayIndex);
1394 BOOL zOrderedAbove =
1395 _isReordering ? !hasPlaceholderGap : (arrayIndex <= selectedArrayIndex);
1396
1397 if (isSelectedTab) {
1398 // Order matters. The dimming view needs to end up behind the selected
1399 // tab, so it's brought to the front first, followed by the tab.
1400 [_tabStripView bringSubviewToFront:_dimmingView];
1401 [_tabStripView bringSubviewToFront:view];
1402 } else if (zOrderedAbove) {
1403 // If the current tab comes after the selected tab in the model but still
1404 // needs to be z-ordered above, place it relative to the dimming view,
1405 // rather than blindly bringing it to the front. This can only happen in
1406 // reordering mode.
1407 if (arrayIndex > selectedArrayIndex) {
1408 DCHECK(_isReordering);
1409 [_tabStripView insertSubview:view belowSubview:_dimmingView];
1410 } else {
1411 [_tabStripView bringSubviewToFront:view];
1412 }
1413 } else {
1414 [_tabStripView sendSubviewToBack:view];
1415 }
1416
1417 // Ignore closing tabs when repositioning.
1418 NSUInteger currentModelIndex = [self modelIndexForIndex:arrayIndex];
1419 if (currentModelIndex == NSNotFound)
1420 continue;
1421
1422 // Ignore the tab that is currently being dragged.
1423 if (_isReordering && view == _draggedTab)
1424 continue;
1425
1426 // |realMinX| is the furthest left the tab can be, in real coordinates.
1427 // This is computed by counting the number of possible collapsed tabs that
1428 // can be to the left of this tab, then multiplying that count by the size
1429 // of a collapsed tab.
1430 //
1431 // There can be up to |[self maxNumCollapsedTabs]| to the left of the
1432 // selected
1433 // tab, and the same number to the right of the selected tab.
1434 NSUInteger numPossibleCollapsedTabsToLeft =
1435 std::min(currentModelIndex, [self maxNumCollapsedTabs]);
1436 if (currentModelIndex > selectedModelIndex) {
1437 // If this tab is to the right of the selected tab, also include the
1438 // number of collapsed tabs on the right of the selected tab.
1439 numPossibleCollapsedTabsToLeft =
1440 std::min(selectedModelIndex, [self maxNumCollapsedTabs]) +
1441 std::min(currentModelIndex - selectedModelIndex,
1442 [self maxNumCollapsedTabs]);
1443 }
1444 CGFloat realMinX =
1445 offset + (numPossibleCollapsedTabsToLeft * kCollapsedTabOverlap);
1446
1447 // |realMaxX| is the furthest right the tab can be, in real coordinates.
1448 NSUInteger numPossibleCollapsedTabsToRight =
1449 std::min(tabCount - currentModelIndex - 1, [self maxNumCollapsedTabs]);
1450 if (currentModelIndex < selectedModelIndex) {
1451 // If this tab is to the left of the selected tab, also include the
1452 // number of collapsed tabs on the left of the selected tab.
1453 numPossibleCollapsedTabsToRight =
1454 std::min(tabCount - selectedModelIndex - 1,
1455 [self maxNumCollapsedTabs]) +
1456 std::min(selectedModelIndex - currentModelIndex,
1457 [self maxNumCollapsedTabs]);
1458 }
1459 CGFloat realMaxX = offset + availableSpace -
1460 (numPossibleCollapsedTabsToRight * kCollapsedTabOverlap);
1461
1462 // If this tab is to the right of the currently dragged tab, add a
1463 // placeholder gap.
1464 if (_isReordering && !hasPlaceholderGap &&
1465 CGRectGetMinX(dragFrame) < virtualMinX + (_currentTabWidth / 2.0)) {
1466 virtualMinX += _currentTabWidth - [self tabOverlap];
1467 hasPlaceholderGap = YES;
1468
1469 // Fix up the z-ordering of the current view. It was placed assuming that
1470 // the placeholder gap hasn't been hit yet.
1471 [_tabStripView sendSubviewToBack:view];
1472
1473 // The model index of the placeholder gap is equal to the model index of
1474 // the shifted tab, adjusted for the presence of the dragged tab. This
1475 // value will be used as the new model index for the dragged tab when it
1476 // is dropped.
1477 _placeholderGapModelIndex = currentModelIndex;
1478 if ([self modelIndexForTabView:_draggedTab] < currentModelIndex)
1479 _placeholderGapModelIndex--;
1480 }
1481
1482 // |tabX| stores where we are placing the tab, in real coordinates. Start
1483 // by trying to place the tab at the computed |virtualMinX|, then constrain
1484 // that by |realMinX| and |realMaxX|.
1485 CGFloat tabX = MAX(virtualMinX, realMinX);
1486 if (tabX + _currentTabWidth > realMaxX)
1487 tabX = realMaxX - _currentTabWidth;
1488
1489 CGRect frame = CGRectMake(AlignValueToPixel(tabX), 0,
1490 AlignValueToPixel(_currentTabWidth), tabHeight);
1491 virtualMinX += (_currentTabWidth - [self tabOverlap]);
1492 virtualMaxX = CGRectGetMaxX(frame);
1493
1494 // TODO(rohitrao): Temporarily disabled this logic as it does not play well with
1495 // tab scrolling.
1496 #if 0
1497 // If this tab is completely hidden by the previous tab, remove it from the
1498 // scroll view. Otherwise, add it back in if needed.
1499 if (selectedArrayIndex != arrayIndex &&
1500 CGRectEqualToRect(frame, previousTabFrame)) {
1501 [view removeFromSuperview];
1502 } else if (![view superview]) {
1503 // TODO(rohitrao): Find a way to move the z-ordering code from the top of
1504 // the function to down here, so we can consolidate the logic.
1505 [_tabStripView insertSubview:view atIndex:
1506 (zOrderedAbove ? [[_tabStripView subviews] count] : 0)];
1507 }
1508 #endif
1509
1510 // Update the tab's collapsed state based on overlap with the previous tab.
1511 if (zOrderedAbove) {
1512 CGRect visibleRect = [self calculateVisibleFrameForFrame:previousTabFrame
1513 whenUnderFrame:frame];
1514 BOOL collapsed =
1515 CGRectGetWidth(visibleRect) < kCollapsedTabWidthThreshold;
1516 [previousTabView setCollapsed:collapsed];
1517
1518 // The selected tab can never be collapsed, since no tab will ever be
1519 // z-ordered above it to obscure it.
1520 if (isSelectedTab)
1521 [view setCollapsed:NO];
1522 } else {
1523 CGRect visibleRect =
1524 [self calculateVisibleFrameForFrame:frame
1525 whenUnderFrame:previousTabFrame];
1526 BOOL collapsed =
1527 CGRectGetWidth(visibleRect) < kCollapsedTabWidthThreshold;
1528 [view setCollapsed:collapsed];
1529 }
1530
1531 if (animate) {
1532 if (!CGRectEqualToRect(frame, [view frame]))
1533 [tabsNeedingAnimation addObject:view];
1534 } else {
1535 if (!CGRectEqualToRect(frame, [view frame]))
1536 [view setFrame:frame];
1537 }
1538
1539 // Throw the target frame into the dictionary so we can animate it later.
1540 _targetFrames.AddFrame(view, frame);
1541
1542 // Ensure the tab is visible.
1543 if ([view isHidden]) {
1544 if (animate) {
1545 // If it is a new tab, and animation is enabled, make it a submarine tab
1546 // by immediately positioning it under the tabstrip.
1547 CGRect submarineFrame = CGRectOffset(frame, 0, CGRectGetHeight(frame));
1548 [view setFrame:submarineFrame];
1549 }
1550 [view setHidden:NO];
1551 }
1552
1553 previousTabView = view;
1554 previousTabFrame = frame;
1555 }
1556
1557 // If in reordering mode and there was no placeholder gap, then the dragged
1558 // tab must be all the way to the right of the other tabs. Set the
1559 // _placeholderGapModelIndex accordingly.
1560 if (!hasPlaceholderGap && _isReordering)
1561 _placeholderGapModelIndex = [_tabModel count] - 1;
1562
1563 // Do not move the new tab button if it is hidden. This will lead to better
1564 // animations when exiting drag and drop mode, as the new tab button will not
1565 // have moved during the drag.
1566 CGRect newTabFrame = [_buttonNewTab frame];
1567 BOOL moveNewTab =
1568 (newTabFrame.origin.x != virtualMaxX) && !_buttonNewTab.hidden;
1569 newTabFrame.origin = CGPointMake(virtualMaxX - kNewTabOverlap, 0);
1570 if (!animate && moveNewTab)
1571 [_buttonNewTab setFrame:newTabFrame];
1572
1573 if (animate) {
1574 float delay = 0.0;
1575 if ([self.fullscreenDelegate currentHeaderOffset] != 0) {
1576 // Move the toolbar to visible and wait for the end of that animation to
1577 // animate the appearance of the new tab.
1578 delay = ios_internal::kToolbarAnimationDuration;
1579 // Signal the FullscreenController that the toolbar needs to stay on
1580 // screen for a bit, so the animation is visible.
1581 [[NSNotificationCenter defaultCenter]
1582 postNotificationName:kWillStartTabStripTabAnimation
1583 object:nil];
1584 }
1585
1586 [UIView animateWithDuration:kTabAnimationDuration
1587 delay:delay
1588 options:UIViewAnimationOptionAllowUserInteraction
1589 animations:^{
1590 for (TabView* view in tabsNeedingAnimation) {
1591 DCHECK(_targetFrames.HasFrame(view));
1592 [view setFrame:_targetFrames.GetFrame(view)];
1593 }
1594 if (moveNewTab)
1595 [_buttonNewTab setFrame:newTabFrame];
1596 }
1597 completion:nil];
1598 }
1599 }
1600
1601 - (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
1602 [self updateScrollViewFrameForToggleButton];
1603 [self updateContentSizeAndRepositionViews];
1604 NSUInteger selectedModelIndex = [_tabModel indexOfTab:[_tabModel currentTab]];
1605 if (selectedModelIndex != NSNotFound) {
1606 [self updateContentOffsetForTabIndex:selectedModelIndex];
1607 }
1608 }
1609
1610 - (void)setNeedsLayoutWithAnimation {
1611 _animateLayout = YES;
1612 [_tabStripView setNeedsLayout];
1613 }
1614
1615 @end
1616
1617 #pragma mark - TabSwitcherAnimation
1618
1619 @implementation TabStripController (TabSwitcherAnimation)
1620
1621 - (TabSwitcherTabStripPlaceholderView*)placeholderView {
1622 TabSwitcherTabStripPlaceholderView* placeholderView =
1623 [[[TabSwitcherTabStripPlaceholderView alloc]
1624 initWithFrame:self.view.bounds] autorelease];
1625 CGFloat xOffset = [_tabStripView contentOffset].x;
1626 UIView* previousView = nil;
1627 const NSUInteger selectedModelIndex =
1628 [_tabModel indexOfTab:[_tabModel currentTab]];
1629 const NSUInteger selectedArrayIndex =
1630 [self indexForModelIndex:selectedModelIndex];
1631 [self updateContentSizeAndRepositionViews];
1632 [self layoutTabStripSubviews];
1633 for (NSUInteger tabArrayIndex = 0; tabArrayIndex < [_tabArray count];
1634 ++tabArrayIndex) {
1635 UIView* tabView = _tabArray[tabArrayIndex];
1636 UIView* tabSnapshotView = snapshot_util::GenerateSnapshot(tabView);
1637 tabSnapshotView.frame = CGRectOffset(tabView.frame, -xOffset, 0);
1638 tabSnapshotView.transform = tabView.transform;
1639 // Order views of the tabs in a pyramid fashion, culminating with
1640 // the selected tab.
1641 // For example, if _tabArray has views [0..6], and 3 is the selected index,
1642 // they will be arranged in the order [0, 1, 2, 6, 5, 4, 3].
1643 if (previousView && tabArrayIndex > selectedArrayIndex) {
1644 [placeholderView insertSubview:tabSnapshotView belowSubview:previousView];
1645 } else {
1646 [placeholderView addSubview:tabSnapshotView];
1647 }
1648 previousView = tabSnapshotView;
1649 }
1650 UIView* buttonSnapshot = snapshot_util::GenerateSnapshot(_buttonNewTab);
1651 buttonSnapshot.frame = CGRectOffset(_buttonNewTab.frame, -xOffset, 0);
1652 [placeholderView addSubview:buttonSnapshot];
1653 return placeholderView;
1654 }
1655
1656 @end
1657
1658 @implementation TabStripController (Testing)
1659
1660 - (TabView*)existingTabViewForTab:(Tab*)tab {
1661 NSUInteger tabIndex = [_tabModel indexOfTab:tab];
1662 NSUInteger tabViewIndex = [self indexForModelIndex:tabIndex];
1663 return [_tabArray objectAtIndex:tabViewIndex];
1664 }
1665
1666 @end
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698