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

Unified 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 side-by-side diff with in-line comments
Download patch
Index: ios/chrome/browser/ui/tabs/tab_strip_controller.mm
diff --git a/ios/chrome/browser/ui/tabs/tab_strip_controller.mm b/ios/chrome/browser/ui/tabs/tab_strip_controller.mm
new file mode 100644
index 0000000000000000000000000000000000000000..ced59b8ddf92bb3c4f5b09453dda550999c6260b
--- /dev/null
+++ b/ios/chrome/browser/ui/tabs/tab_strip_controller.mm
@@ -0,0 +1,1666 @@
+// Copyright 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/chrome/browser/ui/tabs/tab_strip_controller.h"
+#import "ios/chrome/browser/ui/tabs/tab_strip_controller_private.h"
+
+#include <cmath>
+#include <vector>
+
+#include "base/i18n/rtl.h"
+#import "base/ios/weak_nsobject.h"
+#include "base/mac/bundle_locations.h"
+#include "base/mac/foundation_util.h"
+#include "base/mac/objc_property_releaser.h"
+#include "base/mac/scoped_nsobject.h"
+#include "base/metrics/user_metrics.h"
+#include "base/metrics/user_metrics_action.h"
+#include "base/strings/sys_string_conversions.h"
+#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
+#include "ios/chrome/browser/experimental_flags.h"
+#import "ios/chrome/browser/tabs/tab.h"
+#import "ios/chrome/browser/tabs/tab_model.h"
+#import "ios/chrome/browser/tabs/tab_model_observer.h"
+#import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h"
+#include "ios/chrome/browser/ui/commands/ios_command_ids.h"
+#import "ios/chrome/browser/ui/fullscreen_controller.h"
+#include "ios/chrome/browser/ui/rtl_geometry.h"
+#include "ios/chrome/browser/ui/tab_switcher/tab_switcher_tab_strip_placeholder_view.h"
+#import "ios/chrome/browser/ui/tabs/tab_strip_controller+tab_switcher_animation.h"
+#import "ios/chrome/browser/ui/tabs/tab_strip_view.h"
+#import "ios/chrome/browser/ui/tabs/tab_view.h"
+#include "ios/chrome/browser/ui/tabs/target_frame_cache.h"
+#include "ios/chrome/browser/ui/ui_util.h"
+#import "ios/chrome/browser/ui/uikit_ui_util.h"
+#import "ios/chrome/browser/ui/util/snapshot_util.h"
+#include "ios/chrome/grit/ios_strings.h"
+#import "ios/web/public/web_state/web_state.h"
+#include "third_party/google_toolbox_for_mac/src/iPhone/GTMFadeTruncatingLabel.h"
+#include "ui/gfx/image/image.h"
+
+using base::UserMetricsAction;
+
+NSString* const kWillStartTabStripTabAnimation =
+ @"kWillStartTabStripTabAnimation";
+NSString* const kTabStripDragStarted = @"kTabStripDragStarted";
+NSString* const kTabStripDragEnded = @"kTabStripDragEnded";
+
+namespace TabStrip {
+UIColor* BackgroundColor() {
+ DCHECK(IsIPadIdiom());
+ return [UIColor colorWithRed:0.149 green:0.149 blue:0.164 alpha:1];
+}
+}
+
+namespace {
+
+// Animation duration for tab animations.
+const NSTimeInterval kTabAnimationDuration = 0.25;
+
+// Animation duration for tab strip fade.
+const NSTimeInterval kTabStripFadeAnimationDuration = 0.15;
+
+// Amount of time needed to trigger drag and drop mode when long pressing.
+const NSTimeInterval kDragAndDropLongPressDuration = 0.4;
+
+// Tab dimensions.
+const CGFloat kTabOverlap = 26.0;
+const CGFloat kTabOverlapForCompactLayout = 30.0;
+
+const CGFloat kNewTabOverlap = 8.0;
+const CGFloat kMaxTabWidth = 265.0;
+
+// Toggle button dimensions.
+const CGFloat kModeToggleButtonWidth = 36.0;
+const CGFloat kTabSwitcherToggleButtonWidth = 46.0;
+const CGFloat kModeToggleButtonBackgroundWidth = 62.0;
+
+const CGFloat kNewTabRightPadding = 4.0;
+const CGFloat kMinTabWidth = 200.0;
+const CGFloat kMinTabWidthForCompactLayout = 160.0;
+
+const CGFloat kCollapsedTabOverlap = 5.0;
+const NSUInteger kMaxNumCollapsedTabs = 3;
+const NSUInteger kMaxNumCollapsedTabsForCompactLayout = 0;
+
+// Tabs with a visible width smaller than this draw as collapsed tabs..
+const CGFloat kCollapsedTabWidthThreshold = 40.0;
+
+// Autoscroll constants. The autoscroll distance is set to
+// |kMaxAutoscrollDistance| at the edges of the scroll view. As the tab moves
+// away from the edges of the scroll view, the autoscroll distance decreases by
+// one for each |kAutoscrollDecrementWidth| points.
+const CGFloat kMaxAutoscrollDistance = 10.0;
+const CGFloat kAutoscrollDecrementWidth = 10.0;
+
+// Dimming view constants, in points.
+const CGFloat kDimmingViewBottomInsetHighRes = 0.0;
+const CGFloat kDimmingViewBottomInset = 0.0;
+
+// The size of the tab strip view.
+const CGFloat kTabStripHeight = 39.0;
+
+// The size of the new tab button.
+const CGFloat kNewTabButtonWidth = 59.9;
+
+// Default image insets for the new tab button.
+const CGFloat kNewTabButtonHorizontalImageInset = 2.0;
+const CGFloat kNewTabButtonTopImageInset = 6.0;
+const CGFloat kNewTabButtonBottomImageInset = 7.0;
+
+// Offsets needed to keep the UI properly centered on high-res screens, in
+// points.
+const CGFloat kNewTabButtonBottomOffsetHighRes = 2.0;
+}
+
+@interface TabStripController ()<TabModelObserver,
+ TabStripViewLayoutDelegate,
+ UIGestureRecognizerDelegate,
+ UIScrollViewDelegate> {
+ base::scoped_nsobject<TabModel> _tabModel;
+ UIView* _view;
+ TabStripView* _tabStripView;
+ UIButton* _buttonNewTab;
+ UIButton* _modeToggleButton; // weak, nil if not visible.
+ UIButton* _tabSwitcherToggleButton; // weak, nil if not visible.
+
+ // Background view of the toggle button. Only visible while in compact layout.
+ base::scoped_nsobject<UIImageView> _toggleButtonBackgroundView;
+
+ TabStrip::Style _style;
+ base::WeakNSProtocol<id<FullScreenControllerDelegate>> _fullscreenDelegate;
+
+ // Array of TabViews. There is a one-to-one correspondence between this array
+ // and the set of Tabs in the TabModel.
+ base::scoped_nsobject<NSMutableArray> _tabArray;
+
+ // Set of TabViews that are currently closing. These TabViews are also in
+ // |_tabArray|. Used to translate between |_tabArray| indexes and TabModel
+ // indexes.
+ base::scoped_nsobject<NSMutableSet> _closingTabs;
+
+ // Tracks target frames for TabViews.
+ // TODO(rohitrao): This is unnecessary, as UIKit updates view frames
+ // immediately, so [view frame] will always return the end state of the
+ // current animation. We can remove this cache entirely. b/5516053
+ TargetFrameCache _targetFrames;
+
+ // Animate when doing layout. This flag is set by setNeedsLayoutWithAnimation
+ // and cleared in layoutSubviews.
+ BOOL _animateLayout;
+
+ // The current tab width. Recomputed whenever a tab is added or removed.
+ CGFloat _currentTabWidth;
+
+ // View used to dim unselected tabs when in reordering mode. Nil when not
+ // reordering tabs.
+ base::scoped_nsobject<UIView> _dimmingView;
+
+ // Is the selected tab highlighted, used when dragging or swiping tabs.
+ BOOL _highlightsSelectedTab;
+
+ // YES when in reordering mode.
+ // TODO(rohitrao): This is redundant with |_draggedTab|. Remove it.
+ BOOL _isReordering;
+
+ // The tab that is currently being dragged. nil when not in reordering mode.
+ base::scoped_nsobject<TabView> _draggedTab;
+
+ // The last known location of the touch that is dragging the tab. This
+ // location is in the coordinate system of |[_tabStripView superview]| because
+ // that coordinate system does not change as the scroll view scrolls.
+ CGPoint _lastDragLocation;
+
+ // Timer used to autoscroll when in reordering mode. Is nil when not active.
+ // Owned by its runloop.
+ NSTimer* _autoscrollTimer; // weak
+
+ // The distance to scroll for each autoscroll timer tick. If negative, the
+ // tabstrip will scroll to the left; if positive, to the right.
+ CGFloat _autoscrollDistance;
+
+ // The model index of the placeholder gap, if one exists. This value is used
+ // as the new model index of the dragged tab when it is dropped.
+ NSUInteger _placeholderGapModelIndex;
+
+ // If YES, display the mode toggle switch at the left side of the strip. Can
+ // be set after creation.
+ BOOL _hasModeToggleSwitch;
+
+ // If YES, display the tab switcher toggle switch at the left side of the
+ // strip. Can be set after creation.
+ BOOL _hasTabSwitcherToggleSwitch;
+
+ base::mac::ObjCPropertyReleaser _propertyReleaser_TabStripController;
+}
+
+@property(nonatomic, readonly, retain) TabStripView* tabStripView;
+@property(nonatomic, readonly, retain) UIButton* buttonNewTab;
+@property(nonatomic, readonly, assign) UIButton* tabSwitcherToggleButton;
+
+// Initializes the tab array based on the the entries in the TabModel. Creates
+// one TabView per Tab and adds it to the tabstrip. A later call to
+// |-layoutTabs| is needed to properly place the tabs in the correct positions.
+- (void)initializeTabArrayFromTabModel;
+
+// Initializes the tab array to have only one empty tab, for the case (used
+// during startup) when there is not a tab model available.
+- (void)initializeTabArrayWithNoModel;
+
+// Add and remove the mode toggle icon and adjusts the size of the scroll view
+// accordingly. Assumes incognito style has already been checked.
+- (void)installModeToggleButton;
+- (void)removeModeToggleButton;
+
+// Add and remove the tab switcher toggle icon and adjusts the size of the
+// scroll view accordingly. The tab switcher toggle button is replacing the
+// incognito mode toggle button.
+// TODO:(jbbegue) crbug/477676 Remove reference to the incognito toggle button
+// once we know for sure that it will be replaced by the tab switcher toggle
+// button.
+- (void)installTabSwitcherToggleButton;
+- (void)removeTabSwitcherToggleButton;
+
+// Returns an autoreleased TabView object with no content.
+- (TabView*)emptyTabView;
+
+// Returns an autoreleased TabView object based on the given Tab.
+// |isSelected| is passed in here as an optimization, so that the TabView is
+// drawn correctly the first time, without requiring the model to send a
+// -setSelected message to the TabView.
+- (TabView*)tabViewForTab:(Tab*)tab isSelected:(BOOL)isSelected;
+
+// Creates and installs the view used to dim unselected tabs. Does nothing if
+// the view already exists.
+- (void)installDimmingViewWithAnimation:(BOOL)animate;
+
+// Remove the dimming view,
+- (void)removeDimmingViewWithAnimation:(BOOL)animate;
+
+// Converts between model indexes and |_tabArray| indexes. The conversion is
+// necessary because |_tabArray| contains closing tabs whereas the TabModel does
+// not.
+- (NSUInteger)indexForModelIndex:(NSUInteger)modelIndex;
+- (NSUInteger)modelIndexForIndex:(NSUInteger)index;
+- (NSUInteger)modelIndexForTabView:(TabView*)view;
+
+// Helper methods to handle each stage of a drag.
+- (void)beginDrag:(UILongPressGestureRecognizer*)gesture;
+- (void)continueDrag:(UILongPressGestureRecognizer*)gesture;
+- (void)endDrag:(UILongPressGestureRecognizer*)gesture;
+- (void)cancelDrag:(UILongPressGestureRecognizer*)gesture;
+
+// Resets any internal variables used to track drag state.
+- (void)resetDragState;
+
+// Returns whether or not the tabstrip is currently in reordering mode.
+- (BOOL)isReorderingTabs;
+
+// Installs or removes the autoscroll timer.
+- (void)installAutoscrollTimerIfNeeded;
+- (void)removeAutoscrollTimer;
+
+// Called once per autoscroll timer tick. Adjusts the scroll view's content
+// offset as needed.
+- (void)autoscrollTimerFired:(NSTimer*)timer;
+
+// Calculates and stores the autoscroll distance for the given tab view. The
+// autoscroll distance is a function of the distance between the edge of the
+// scroll view and the tab's frame.
+- (void)computeAutoscrollDistanceForTabView:(TabView*)view;
+
+// Constrains the stored autoscroll distance to prevent the scroll view from
+// overscrolling.
+- (void)constrainAutoscrollDistance;
+
+#if 0
+// Returns the appropriate model index for the currently dragged tab, given its
+// current position. (If dropped, the tab would be at this index in the model.)
+// TODO(rohitrao): Implement this method.
+- (NSUInteger)modelIndexForDraggedTab;
+#endif
+
+// Returns the horizontal visible tab strip width used to compute the tab width
+// and the tabs and new tab button in regular layout mode.
+// Takes into account whether or not the mode toggle button is showing.
+- (CGFloat)tabStripVisibleSpace;
+
+// Updates the scroll view's content size based on the current set of tabs and
+// closing tabs. After updating the content size, repositions views so they
+// they will appear stationary on screen.
+- (void)updateContentSizeAndRepositionViews;
+
+// Returns the frame, in the scroll view content's coordinate system, of the
+// given tab view.
+- (CGRect)scrollViewFrameForTab:(TabView*)view;
+
+// Returns the portion of |frame| which is not covered by |frameOnTop|.
+- (CGRect)calculateVisibleFrameForFrame:(CGRect)frame
+ whenUnderFrame:(CGRect)frameOnTop;
+
+// Schedules a layout of the scroll view and sets the internal |_animateLayout|
+// flag so that the layout will be animated.
+- (void)setNeedsLayoutWithAnimation;
+
+// Returns the maximum number of collapsed tabs depending on the current layout
+// mode.
+- (NSUInteger)maxNumCollapsedTabs;
+
+// Returns the tab overlap width depending on the current layout mode.
+- (CGFloat)tabOverlap;
+
+// Returns the minimum tab view width depending on the current layout mode.
+- (CGFloat)minTabWidth;
+
+// Updates the content offset of the tab strip view in order to keep the
+// selected tab view visible.
+// Content offset adjustement is only needed/performed in compact mode.
+// This method must be called with a valid |tabIndex|.
+- (void)updateContentOffsetForTabIndex:(NSUInteger)tabIndex;
+
+// Update the frame of the tab strip view (scrollview) frame, content inset and
+// toggle buttons states depending on the current layout mode.
+- (void)updateScrollViewFrameForToggleButton;
+
+@end
+
+@implementation TabStripController
+
+@synthesize buttonNewTab = _buttonNewTab;
+@synthesize hasModeToggleSwitch = _hasModeToggleSwitch;
+@synthesize hasTabSwitcherToggleSwitch = _hasTabSwitcherToggleSwitch;
+@synthesize highlightsSelectedTab = _highlightsSelectedTab;
+@synthesize modeToggleButton = _modeToggleButton;
+@synthesize tabStripView = _tabStripView;
+@synthesize tabSwitcherToggleButton = _tabSwitcherToggleButton;
+@synthesize view = _view;
+
+- (instancetype)initWithTabModel:(TabModel*)tabModel
+ style:(TabStrip::Style)style {
+ if ((self = [super init])) {
+ _propertyReleaser_TabStripController.Init(self, [TabStripController class]);
+ _tabArray.reset([[NSMutableArray alloc] initWithCapacity:10]);
+ _closingTabs.reset([[NSMutableSet alloc] initWithCapacity:5]);
+
+ _tabModel.reset([tabModel retain]);
+ [_tabModel addObserver:self];
+ _style = style;
+ _hasModeToggleSwitch = NO;
+ _hasTabSwitcherToggleSwitch = NO;
+
+ // |self.view| setup.
+ CGRect tabStripFrame = [UIApplication sharedApplication].keyWindow.bounds;
+ tabStripFrame.size.height = kTabStripHeight;
+ _view = [[UIView alloc] initWithFrame:tabStripFrame];
+ _view.autoresizingMask = (UIViewAutoresizingFlexibleWidth |
+ UIViewAutoresizingFlexibleBottomMargin);
+ _view.backgroundColor = TabStrip::BackgroundColor();
+ if (UseRTLLayout())
+ _view.transform = CGAffineTransformMakeScale(-1, 1);
+
+ // |self.tabStripView| setup.
+ _tabStripView = [[TabStripView alloc] initWithFrame:_view.bounds];
+ _tabStripView.autoresizingMask =
+ (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
+ _tabStripView.backgroundColor = _view.backgroundColor;
+ _tabStripView.layoutDelegate = self;
+ _tabStripView.accessibilityIdentifier = style == TabStrip::kStyleIncognito
+ ? @"Incognito Tab Strip"
+ : @"Tab Strip";
+ [_view addSubview:_tabStripView];
+
+ // |self.buttonNewTab| setup.
+ CGRect buttonNewTabFrame = tabStripFrame;
+ buttonNewTabFrame.size.width = kNewTabButtonWidth;
+ _buttonNewTab = [[UIButton alloc] initWithFrame:buttonNewTabFrame];
+ BOOL isBrowserStateIncognito =
+ tabModel && tabModel.browserState->IsOffTheRecord();
+ _buttonNewTab.tag =
+ isBrowserStateIncognito ? IDC_NEW_INCOGNITO_TAB : IDC_NEW_TAB;
+ // TODO(crbug.com/600829): Rewrite layout code and convert these masks to
+ // to trailing and leading margins rather than right and bottom.
+ _buttonNewTab.autoresizingMask = (UIViewAutoresizingFlexibleRightMargin |
+ UIViewAutoresizingFlexibleBottomMargin);
+ _buttonNewTab.imageView.contentMode = UIViewContentModeCenter;
+ UIImage* buttonNewTabImage = nil;
+ UIImage* buttonNewTabPressedImage = nil;
+ if (_style == TabStrip::kStyleIncognito) {
+ buttonNewTabImage = [UIImage imageNamed:@"tabstrip_new_tab_incognito"];
+ buttonNewTabPressedImage =
+ [UIImage imageNamed:@"tabstrip_new_tab_incognito_pressed"];
+ } else {
+ buttonNewTabImage = [UIImage imageNamed:@"tabstrip_new_tab"];
+ buttonNewTabPressedImage =
+ [UIImage imageNamed:@"tabstrip_new_tab_pressed"];
+ }
+ [_buttonNewTab setImage:buttonNewTabImage forState:UIControlStateNormal];
+ [_buttonNewTab setImage:buttonNewTabPressedImage
+ forState:UIControlStateHighlighted];
+ UIEdgeInsets imageInsets = UIEdgeInsetsMake(
+ kNewTabButtonTopImageInset, kNewTabButtonHorizontalImageInset,
+ kNewTabButtonBottomImageInset, kNewTabButtonHorizontalImageInset);
+ if (IsHighResScreen()) {
+ imageInsets.top += kNewTabButtonBottomOffsetHighRes;
+ imageInsets.bottom -= kNewTabButtonBottomOffsetHighRes;
+ }
+ _buttonNewTab.imageEdgeInsets = imageInsets;
+ SetA11yLabelAndUiAutomationName(
+ _buttonNewTab,
+ isBrowserStateIncognito ? IDS_IOS_TOOLS_MENU_NEW_INCOGNITO_TAB
+ : IDS_IOS_TOOLS_MENU_NEW_TAB,
+ isBrowserStateIncognito ? @"New Incognito Tab" : @"New Tab");
+ // Use a nil target to send |-chromeExecuteCommand:| down the responder
+ // chain.
+ [_buttonNewTab addTarget:nil
+ action:@selector(chromeExecuteCommand:)
+ forControlEvents:UIControlEventTouchUpInside];
+ [_buttonNewTab addTarget:self
+ action:@selector(recordUserMetrics:)
+ forControlEvents:UIControlEventTouchUpInside];
+ [_tabStripView addSubview:_buttonNewTab];
+
+ // Add tab buttons to tab strip.
+ if (_tabModel)
+ [self initializeTabArrayFromTabModel];
+ else
+ [self initializeTabArrayWithNoModel];
+
+ // Update the layout of the tab buttons.
+ [self updateContentSizeAndRepositionViews];
+ [self layoutTabStripSubviews];
+
+ // Don't highlight the selected tab by default.
+ self.highlightsSelectedTab = NO;
+ }
+ return self;
+}
+
+- (instancetype)init {
+ NOTREACHED();
+ return nil;
+}
+
+- (void)dealloc {
+ [_tabStripView setDelegate:nil];
+ [_tabStripView setLayoutDelegate:nil];
+ [_tabModel removeObserver:self];
+ [super dealloc];
+}
+
+- (id<FullScreenControllerDelegate>)fullscreenDelegate {
+ return _fullscreenDelegate;
+}
+
+- (void)setFullscreenDelegate:
+ (id<FullScreenControllerDelegate>)fullscreenDelegate {
+ _fullscreenDelegate.reset(fullscreenDelegate);
+}
+
+- (void)initializeTabArrayFromTabModel {
+ DCHECK(_tabModel);
+ for (Tab* tab in _tabModel.get()) {
+ BOOL isSelectedTab = [_tabModel currentTab] == tab;
+ TabView* view = [self tabViewForTab:tab isSelected:isSelectedTab];
+ [_tabArray addObject:view];
+ [_tabStripView addSubview:view];
+ }
+}
+
+- (void)initializeTabArrayWithNoModel {
+ DCHECK(!_tabModel);
+ TabView* view = [self emptyTabView];
+ [_tabArray addObject:view];
+ [_tabStripView addSubview:view];
+ [view setSelected:YES];
+ return;
+}
+
+- (void)setHasModeToggleSwitch:(BOOL)hasModeToggleSwitch {
+ if (_hasModeToggleSwitch && !hasModeToggleSwitch)
+ [self removeModeToggleButton];
+ if (!_hasModeToggleSwitch && hasModeToggleSwitch)
+ [self installModeToggleButton];
+ if (_hasModeToggleSwitch != hasModeToggleSwitch) {
+ _hasModeToggleSwitch = hasModeToggleSwitch;
+ [self updateContentSizeAndRepositionViews];
+ [self setNeedsLayoutWithAnimation];
+ }
+}
+
+- (void)setHasTabSwitcherToggleSwitch:(BOOL)hasTabSwitcherToggleSwitch {
+ if (_hasTabSwitcherToggleSwitch && !hasTabSwitcherToggleSwitch)
+ [self removeTabSwitcherToggleButton];
+ if (!_hasTabSwitcherToggleSwitch && hasTabSwitcherToggleSwitch)
+ [self installTabSwitcherToggleButton];
+ if (_hasTabSwitcherToggleSwitch != hasTabSwitcherToggleSwitch) {
+ _hasTabSwitcherToggleSwitch = hasTabSwitcherToggleSwitch;
+ [self updateContentSizeAndRepositionViews];
+ [self setNeedsLayoutWithAnimation];
+ }
+}
+
+- (TabView*)emptyTabView {
+ TabView* view =
+ [[[TabView alloc] initWithEmptyView:YES selected:YES] autorelease];
+ [view setIncognitoStyle:(_style == TabStrip::kStyleIncognito)];
+ [view setContentMode:UIViewContentModeRedraw];
+
+ // Setting the tab to be hidden marks it as a new tab. The layout code will
+ // make the tab visible and set up the appropriate animations.
+ [view setHidden:YES];
+
+ return view;
+}
+
+- (TabView*)tabViewForTab:(Tab*)tab isSelected:(BOOL)isSelected {
+ TabView* view =
+ [[[TabView alloc] initWithEmptyView:NO selected:isSelected] autorelease];
+ if (UseRTLLayout())
+ [view setTransform:CGAffineTransformMakeScale(-1, 1)];
+ [view setIncognitoStyle:(_style == TabStrip::kStyleIncognito)];
+ [view setContentMode:UIViewContentModeRedraw];
+ [[view titleLabel] setText:[tab title]];
+ [view setFavicon:[tab favicon]];
+
+ // Set the tab buttons' action messages.
+ [view addTarget:self
+ action:@selector(tabTapped:)
+ forControlEvents:UIControlEventTouchUpInside];
+ [[view closeButton] addTarget:self
+ action:@selector(closeTab:)
+ forControlEvents:UIControlEventTouchUpInside];
+
+ // Install a long press gesture recognizer to handle drag and drop.
+ base::scoped_nsobject<UILongPressGestureRecognizer> longPress(
+ [[UILongPressGestureRecognizer alloc]
+ initWithTarget:self
+ action:@selector(handleLongPress:)]);
+ [longPress setMinimumPressDuration:kDragAndDropLongPressDuration];
+ [longPress setDelegate:self];
+ [view addGestureRecognizer:longPress];
+
+ // Giving the tab view exclusive touch prevents other views from receiving
+ // touches while a TabView is handling a touch.
+ [view setExclusiveTouch:YES];
+
+ // Setting the tab to be hidden marks it as a new tab. The layout code will
+ // make the tab visible and set up the appropriate animations.
+ [view setHidden:YES];
+
+ return view;
+}
+
+- (void)setHighlightsSelectedTab:(BOOL)highlightsSelectedTab {
+ if (highlightsSelectedTab)
+ [self installDimmingViewWithAnimation:YES];
+ else
+ [self removeDimmingViewWithAnimation:YES];
+
+ _highlightsSelectedTab = highlightsSelectedTab;
+}
+
+- (void)installDimmingViewWithAnimation:(BOOL)animate {
+ // The dimming view should not cover the bottom 2px of the tab strip, as those
+ // pixels are visually part of the top border of the toolbar. The bottom
+ // inset constants take into account the conversion from pixels to points.
+ CGRect frame = [_tabStripView bounds];
+ frame.size.height -= (IsHighResScreen() ? kDimmingViewBottomInsetHighRes
+ : kDimmingViewBottomInset);
+
+ // Create the dimming view if it doesn't exist. In all cases, make sure it's
+ // set up correctly.
+ if (_dimmingView.get())
+ [_dimmingView setFrame:frame];
+ else
+ _dimmingView.reset([[UIView alloc] initWithFrame:frame]);
+
+ // Enable user interaction in order to eat touches from views behind it.
+ [_dimmingView setUserInteractionEnabled:YES];
+ [_dimmingView setBackgroundColor:[TabStrip::BackgroundColor()
+ colorWithAlphaComponent:0]];
+ [_dimmingView setAutoresizingMask:(UIViewAutoresizingFlexibleWidth |
+ UIViewAutoresizingFlexibleHeight)];
+ [_tabStripView addSubview:_dimmingView];
+
+ CGFloat duration = animate ? kTabStripFadeAnimationDuration : 0;
+ [UIView animateWithDuration:duration
+ animations:^{
+ [_dimmingView
+ setBackgroundColor:[TabStrip::BackgroundColor()
+ colorWithAlphaComponent:0.6]];
+ }];
+}
+
+- (void)removeDimmingViewWithAnimation:(BOOL)animate {
+ if (_dimmingView) {
+ CGFloat duration = animate ? kTabStripFadeAnimationDuration : 0;
+ [UIView animateWithDuration:duration
+ animations:^{
+ [_dimmingView setBackgroundColor:[TabStrip::BackgroundColor()
+ colorWithAlphaComponent:0]];
+ }
+ completion:^(BOOL finished) {
+ // Do not remove the dimming view if the animation was aborted.
+ if (finished) {
+ [_dimmingView removeFromSuperview];
+ _dimmingView.reset();
+ }
+ }];
+ }
+}
+
+- (void)recordUserMetrics:(id)sender {
+ if (sender == _buttonNewTab)
+ base::RecordAction(UserMetricsAction("MobileTabStripNewTab"));
+ else if (sender == _modeToggleButton)
+ base::RecordAction(UserMetricsAction("MobileTabStripSwitchMode"));
+ else if (sender == _tabSwitcherToggleButton)
+ base::RecordAction(UserMetricsAction("MobileTabSwitcherOpen"));
+ else
+ LOG(WARNING) << "Trying to record metrics for unknown sender "
+ << base::SysNSStringToUTF8([sender description]);
+}
+
+- (void)tabTapped:(id)sender {
+ DCHECK([sender isKindOfClass:[TabView class]]);
+
+ // Ignore taps while in reordering mode.
+ if ([self isReorderingTabs])
+ return;
+
+ NSUInteger index = [self modelIndexForTabView:(TabView*)sender];
+ DCHECK_NE(NSNotFound, static_cast<NSInteger>(index));
+ if (index == NSNotFound)
+ return;
+ Tab* tappedTab = [_tabModel tabAtIndex:index];
+ Tab* currentTab = [_tabModel currentTab];
+ if (IsIPadIdiom() && (currentTab != tappedTab)) {
+ [currentTab updateSnapshotWithOverlay:YES visibleFrameOnly:YES];
+ }
+ [_tabModel setCurrentTab:tappedTab];
+ [self updateContentOffsetForTabIndex:index];
+}
+
+- (void)closeTab:(id)sender {
+ // Ignore taps while in reordering mode.
+ // TODO(rohitrao): We should just hide the close buttons instead.
+ if ([self isReorderingTabs])
+ return;
+
+ base::RecordAction(UserMetricsAction("MobileTabStripCloseTab"));
+ DCHECK([sender isKindOfClass:[UIButton class]]);
+ UIView* superview = [sender superview];
+ DCHECK([superview isKindOfClass:[TabView class]]);
+ TabView* tab = (TabView*)superview;
+ NSUInteger modelIndex = [self modelIndexForTabView:tab];
+ if (modelIndex != NSNotFound)
+ [_tabModel closeTabAtIndex:modelIndex];
+}
+
+- (void)handleLongPress:(UILongPressGestureRecognizer*)gesture {
+ switch ([gesture state]) {
+ case UIGestureRecognizerStateBegan:
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kTabStripDragStarted
+ object:nil];
+ [self beginDrag:gesture];
+ break;
+ case UIGestureRecognizerStateChanged:
+ [self continueDrag:gesture];
+ break;
+ case UIGestureRecognizerStateEnded:
+ [self endDrag:gesture];
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kTabStripDragEnded
+ object:nil];
+ break;
+ case UIGestureRecognizerStateCancelled:
+ [self cancelDrag:gesture];
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kTabStripDragEnded
+ object:nil];
+ break;
+ default:
+ NOTREACHED();
+ }
+}
+
+- (NSUInteger)indexForModelIndex:(NSUInteger)modelIndex {
+ NSUInteger index = modelIndex;
+ NSUInteger i = 0;
+ for (TabView* tab in _tabArray.get()) {
+ if ([_closingTabs containsObject:tab])
+ ++index;
+
+ if (i == index)
+ break;
+
+ ++i;
+ }
+
+ DCHECK_GE(index, modelIndex);
+ return index;
+}
+
+- (NSUInteger)modelIndexForIndex:(NSUInteger)index {
+ NSUInteger modelIndex = 0;
+ NSUInteger arrayIndex = 0;
+ for (TabView* tab in _tabArray.get()) {
+ if (arrayIndex == index) {
+ if ([_closingTabs containsObject:tab])
+ return NSNotFound;
+ return modelIndex;
+ }
+
+ if (![_closingTabs containsObject:tab])
+ ++modelIndex;
+
+ ++arrayIndex;
+ }
+
+ return NSNotFound;
+}
+
+- (NSUInteger)modelIndexForTabView:(TabView*)view {
+ return [self modelIndexForIndex:[_tabArray indexOfObject:view]];
+}
+
+#pragma mark -
+#pragma mark Tab Drag and Drop methods
+
+- (void)beginDrag:(UILongPressGestureRecognizer*)gesture {
+ DCHECK([[gesture view] isKindOfClass:[TabView class]]);
+ TabView* view = (TabView*)[gesture view];
+
+ // Sanity checks.
+ NSUInteger index = [self modelIndexForTabView:view];
+ DCHECK_NE(NSNotFound, static_cast<NSInteger>(index));
+ if (index == NSNotFound)
+ return;
+
+ // Install the dimming view, hide the new tab button, and select the tab so it
+ // appears highlighted.
+ Tab* tab = [_tabModel tabAtIndex:index];
+ self.highlightsSelectedTab = YES;
+ _buttonNewTab.hidden = YES;
+ [_tabModel setCurrentTab:tab];
+
+ // Set up initial drag state.
+ _lastDragLocation = [gesture locationInView:[_tabStripView superview]];
+ _isReordering = YES;
+ _draggedTab.reset([view retain]);
+ _placeholderGapModelIndex = [self modelIndexForTabView:_draggedTab];
+
+ // Update the autoscroll distance and timer.
+ [self computeAutoscrollDistanceForTabView:_draggedTab];
+ if (_autoscrollDistance != 0)
+ [self installAutoscrollTimerIfNeeded];
+ else
+ [self removeAutoscrollTimer];
+}
+
+- (void)continueDrag:(UILongPressGestureRecognizer*)gesture {
+ DCHECK([[gesture view] isKindOfClass:[TabView class]]);
+ TabView* view = (TabView*)[gesture view];
+
+ // Update the position of the dragged tab.
+ CGPoint location = [gesture locationInView:[_tabStripView superview]];
+ CGFloat dx = location.x - _lastDragLocation.x;
+ CGRect frame = [view frame];
+ frame.origin.x += dx;
+ [view setFrame:frame];
+ _lastDragLocation = location;
+
+ // Update the autoscroll distance and timer.
+ [self computeAutoscrollDistanceForTabView:_draggedTab];
+ if (_autoscrollDistance != 0)
+ [self installAutoscrollTimerIfNeeded];
+ else
+ [self removeAutoscrollTimer];
+
+ [self setNeedsLayoutWithAnimation];
+}
+
+- (void)endDrag:(UILongPressGestureRecognizer*)gesture {
+ DCHECK([[gesture view] isKindOfClass:[TabView class]]);
+
+ NSUInteger fromIndex = [self modelIndexForTabView:_draggedTab];
+ // TODO(rohitrao): We're seeing crashes where fromIndex is NSNotFound,
+ // indicating that the dragged tab is no longer in the TabModel. This could
+ // happen if a tab closed itself during a drag. Investigate this further, but
+ // for now, simply test |fromIndex| before proceeding.
+ if (fromIndex == NSNotFound) {
+ [self resetDragState];
+ [self setNeedsLayoutWithAnimation];
+ return;
+ }
+
+ Tab* tab = [_tabModel tabAtIndex:fromIndex];
+ NSUInteger toIndex = _placeholderGapModelIndex;
+ DCHECK_NE(NSNotFound, static_cast<NSInteger>(toIndex));
+ DCHECK_LT(toIndex, [_tabModel count]);
+
+ // Reset drag state variables before notifying the model that the tab moved.
+ [self resetDragState];
+
+ [_tabModel moveTab:tab toIndex:toIndex];
+ [self setNeedsLayoutWithAnimation];
+}
+
+- (void)cancelDrag:(UILongPressGestureRecognizer*)gesture {
+ DCHECK([[gesture view] isKindOfClass:[TabView class]]);
+
+ // Reset drag state and trigger a relayout to moved tabs back into their
+ // correct positions.
+ [self resetDragState];
+ [self setNeedsLayoutWithAnimation];
+}
+
+- (void)resetDragState {
+ self.highlightsSelectedTab = NO;
+ _buttonNewTab.hidden = NO;
+ [self removeAutoscrollTimer];
+
+ _isReordering = NO;
+ _placeholderGapModelIndex = NSNotFound;
+ _draggedTab.reset();
+}
+
+- (BOOL)isReorderingTabs {
+ return _isReordering;
+}
+
+- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer*)recognizer {
+ DCHECK([recognizer isKindOfClass:[UILongPressGestureRecognizer class]]);
+
+ // If a drag is already in progress, do not allow another to start.
+ return ![self isReorderingTabs];
+}
+
+#pragma mark -
+#pragma mark Autoscroll methods
+
+- (void)installAutoscrollTimerIfNeeded {
+ if (_autoscrollTimer)
+ return;
+
+ _autoscrollTimer =
+ [NSTimer scheduledTimerWithTimeInterval:(1.0 / 60.0)
+ target:self
+ selector:@selector(autoscrollTimerFired:)
+ userInfo:nil
+ repeats:YES];
+}
+
+- (void)removeAutoscrollTimer {
+ [_autoscrollTimer invalidate];
+ _autoscrollTimer = nil;
+}
+
+- (void)autoscrollTimerFired:(NSTimer*)timer {
+ [self constrainAutoscrollDistance];
+
+ CGPoint offset = [_tabStripView contentOffset];
+ offset.x += _autoscrollDistance;
+ [_tabStripView setContentOffset:offset];
+
+ // Fixed-position views need to have their frames adusted to compensate for
+ // the content offset shift. These include the dragged tab, the dimming
+ // view, and the new tab button.
+ CGRect tabFrame = [_draggedTab frame];
+ tabFrame.origin.x += _autoscrollDistance;
+ [_draggedTab setFrame:tabFrame];
+
+ CGRect dimFrame = [_dimmingView frame];
+ dimFrame.origin.x += _autoscrollDistance;
+ [_dimmingView setFrame:dimFrame];
+
+ // Even though the new tab button is hidden during drag and drop, keep its
+ // frame updated to prevent it from animating back into place when the drag
+ // finishes.
+ CGRect newTabFrame = [_buttonNewTab frame];
+ newTabFrame.origin.x += _autoscrollDistance;
+ [_buttonNewTab setFrame:newTabFrame];
+
+ // TODO(rohitrao): Find a good way to re-enable the sliding over animation
+ // when autoscrolling. Right now any running animations are immediately
+ // stopped by the next call to autoscrollTimerFired.
+ [_tabStripView setNeedsLayout];
+}
+
+- (void)computeAutoscrollDistanceForTabView:(TabView*)view {
+ CGRect scrollBounds = [_tabStripView bounds];
+ CGRect viewFrame = [view frame];
+
+ // The distance between this tab and the edge of the scroll view.
+ CGFloat distanceFromEdge =
+ MIN(CGRectGetMinX(viewFrame) - CGRectGetMinX(scrollBounds),
+ CGRectGetMaxX(scrollBounds) - CGRectGetMaxX(viewFrame));
+ if (distanceFromEdge < 0)
+ distanceFromEdge = 0;
+
+ // Negative if the tab is closer to the left edge of the scroll view, positive
+ // if it is closer to the right edge.
+ CGFloat leftRightMultiplier =
+ (CGRectGetMidX(viewFrame) < CGRectGetMidX(scrollBounds)) ? -1.0 : 1.0;
+
+ // The autoscroll distance decreases linearly as the tab view gets further
+ // from the edge of the scroll view.
+ _autoscrollDistance =
+ leftRightMultiplier *
+ MAX(0.0, ceilf(kMaxAutoscrollDistance -
+ distanceFromEdge / kAutoscrollDecrementWidth));
+}
+
+- (void)constrainAutoscrollDistance {
+ // Make sure autoscroll distance is not so large as to cause overscroll.
+ CGPoint offset = [_tabStripView contentOffset];
+
+ // Check to make sure there is no overscroll off the right edge.
+ CGFloat maxOffset = [_tabStripView contentSize].width -
+ CGRectGetWidth([_tabStripView bounds]);
+ if (offset.x + _autoscrollDistance > maxOffset)
+ _autoscrollDistance = (maxOffset - offset.x);
+
+ // Perform the left edge check after the right edge check, to prevent
+ // right-justifying the tabs when there is no overflow.
+ if (offset.x + _autoscrollDistance < 0)
+ _autoscrollDistance = -offset.x;
+}
+
+#pragma mark -
+#pragma mark TabStripModelObserver methods
+
+// Observer method.
+- (void)tabModel:(TabModel*)model
+ didInsertTab:(Tab*)tab
+ atIndex:(NSUInteger)modelIndex
+ inForeground:(BOOL)fg {
+ TabView* view = [self tabViewForTab:tab isSelected:fg];
+ [_tabArray insertObject:view atIndex:[self indexForModelIndex:modelIndex]];
+ [[self tabStripView] addSubview:view];
+
+ [self updateContentSizeAndRepositionViews];
+ [self setNeedsLayoutWithAnimation];
+ [self updateContentOffsetForTabIndex:modelIndex];
+}
+
+// Observer method.
+- (void)tabModel:(TabModel*)model
+ didRemoveTab:(Tab*)tab
+ atIndex:(NSUInteger)modelIndex {
+ // Keep the actual view around while it is animating out. Once the animation
+ // is done, remove the view.
+ NSUInteger index = [self indexForModelIndex:modelIndex];
+ TabView* view = [_tabArray objectAtIndex:index];
+ [_closingTabs addObject:view];
+ _targetFrames.RemoveFrame(view);
+
+ // Adjust the content size now that the tab has been removed from the model.
+ [self updateContentSizeAndRepositionViews];
+
+ // Signal the FullscreenController that the toolbar needs to stay on
+ // screen for a bit, so the animation is visible.
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kWillStartTabStripTabAnimation
+ object:nil];
+
+ // Leave the view where it is horizontally and animate it downwards out of
+ // sight.
+ CGRect frame = [view frame];
+ frame = CGRectOffset(frame, 0, CGRectGetHeight(frame));
+ [UIView animateWithDuration:kTabAnimationDuration
+ animations:^{
+ [view setFrame:frame];
+ }
+ completion:^(BOOL finished) {
+ [view removeFromSuperview];
+ [_tabArray removeObject:view];
+ [_closingTabs removeObject:view];
+ }];
+
+ [self setNeedsLayoutWithAnimation];
+}
+
+// Observer method.
+- (void)tabModel:(TabModel*)model
+ didMoveTab:(Tab*)tab
+ fromIndex:(NSUInteger)fromIndex
+ toIndex:(NSUInteger)toIndex {
+ DCHECK(!_isReordering);
+
+ // Reorder the objects in _tabArray to keep in sync with the model ordering.
+ NSUInteger arrayIndex = [self indexForModelIndex:fromIndex];
+ base::scoped_nsobject<TabView> view(
+ [[_tabArray objectAtIndex:arrayIndex] retain]);
+ [_tabArray removeObject:view];
+ [_tabArray insertObject:view atIndex:toIndex];
+ [self setNeedsLayoutWithAnimation];
+}
+
+// Observer method.
+- (void)tabModel:(TabModel*)model
+ didChangeActiveTab:(Tab*)newTab
+ previousTab:(Tab*)previousTab
+ atIndex:(NSUInteger)modelIndex {
+ for (TabView* view in _tabArray.get()) {
+ [view setSelected:NO];
+ }
+
+ NSUInteger index = [self indexForModelIndex:modelIndex];
+ TabView* activeView = [_tabArray objectAtIndex:index];
+ [activeView setSelected:YES];
+
+ // No need to animate this change, as selecting a new tab simply changes the
+ // z-ordering of the TabViews. If a new tab was selected as a result of a tab
+ // closure, then the animated layout has already been scheduled.
+ [_tabStripView setNeedsLayout];
+}
+
+// Observer method.
+- (void)tabModel:(TabModel*)model didChangeTab:(Tab*)tab {
+ NSUInteger modelIndex = [_tabModel indexOfTab:tab];
+ if (modelIndex == NSNotFound) {
+ DCHECK(false) << "Received notification for a Tab that is not contained in "
+ << "the TabModel";
+ return;
+ }
+ NSUInteger index = [self indexForModelIndex:modelIndex];
+ TabView* view = [_tabArray objectAtIndex:index];
+ [view setTitle:tab.title];
+ [view setFavicon:[tab favicon]];
+ if (tab.webState->IsLoading())
+ [view startProgressSpinner];
+ else
+ [view stopProgressSpinner];
+ [view setNeedsDisplay];
+}
+
+// Observer method.
+- (void)tabModel:(TabModel*)model
+ didReplaceTab:(Tab*)oldTab
+ withTab:(Tab*)newTab
+ atIndex:(NSUInteger)index {
+ // TabViews do not hold references to their parent Tabs, so it's safe to treat
+ // this as a tab change rather than a tab replace.
+ [self tabModel:model didChangeTab:newTab];
+}
+
+#pragma mark -
+#pragma mark Views and Layout
+
+- (void)installModeToggleButton {
+ // Add the mode toggle button view.
+ DCHECK(!_modeToggleButton);
+ UIImage* toggleIcon = nil;
+ int toggleIdsAccessibilityLabel;
+ NSString* toggleEnglishUiAutomationName;
+ if (_style == TabStrip::kStyleDark) {
+ toggleIcon = [UIImage imageNamed:@"tabstrip_switch"];
+ toggleIdsAccessibilityLabel = IDS_IOS_SWITCH_BROWSER_MODE_ENTER_INCOGNITO;
+ toggleEnglishUiAutomationName = @"Enter Incognito* Mode";
+ } else {
+ toggleIcon = [UIImage imageNamed:@"tabstrip_incognito_switch"];
+ toggleIdsAccessibilityLabel = IDS_IOS_SWITCH_BROWSER_MODE_LEAVE_INCOGNITO;
+ toggleEnglishUiAutomationName = @"Leave Incognito* Mode";
+ }
+ const CGFloat tabStripHeight = _view.frame.size.height;
+ CGRect buttonFrame =
+ CGRectMake(CGRectGetMaxX(_view.frame) - kModeToggleButtonWidth, 0.0,
+ kModeToggleButtonWidth, tabStripHeight);
+ _modeToggleButton = [UIButton buttonWithType:UIButtonTypeCustom];
+ _modeToggleButton.frame = buttonFrame;
+ [_modeToggleButton setImageEdgeInsets:UIEdgeInsetsMake(7, 5, 7, 5)];
+ _modeToggleButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
+ _modeToggleButton.backgroundColor = [UIColor clearColor];
+ [_modeToggleButton setImage:toggleIcon forState:UIControlStateNormal];
+ // Set target/action to bubble up with command id as tag.
+ [_modeToggleButton addTarget:nil
+ action:@selector(chromeExecuteCommand:)
+ forControlEvents:UIControlEventTouchUpInside];
+ [_modeToggleButton setTag:IDC_SWITCH_BROWSER_MODES];
+ [_modeToggleButton addTarget:self
+ action:@selector(recordUserMetrics:)
+ forControlEvents:UIControlEventTouchUpInside];
+
+ SetA11yLabelAndUiAutomationName(_modeToggleButton,
+ toggleIdsAccessibilityLabel,
+ toggleEnglishUiAutomationName);
+ [_view addSubview:_modeToggleButton];
+ // Shrink the scroll view.
+ [self updateScrollViewFrameForToggleButton];
+}
+
+- (void)removeModeToggleButton {
+ // Remove the button view.
+ DCHECK(_modeToggleButton);
+ [_modeToggleButton removeFromSuperview];
+ _modeToggleButton = nil;
+ // Extend the scroll view.
+ [self updateScrollViewFrameForToggleButton];
+}
+
+- (CGFloat)tabStripVisibleSpace {
+ CGFloat availableSpace = CGRectGetWidth([_tabStripView bounds]) -
+ CGRectGetWidth([_buttonNewTab frame]) +
+ kNewTabOverlap;
+ if (IsCompactTablet()) {
+ if ([self hasModeToggleSwitch])
+ availableSpace -= kNewTabRightPadding + kModeToggleButtonWidth;
+ else
+ availableSpace -= kNewTabRightPadding;
+ } else {
+ if (![self hasModeToggleSwitch])
+ availableSpace -= kNewTabRightPadding;
+ }
+ return availableSpace;
+}
+
+- (void)installTabSwitcherToggleButton {
+ // Add the mode toggle button view.
+ DCHECK(!_tabSwitcherToggleButton);
+ UIImage* tabSwitcherToggleIcon =
+ [UIImage imageNamed:@"tabswitcher_tab_switcher_button"];
+ tabSwitcherToggleIcon = [tabSwitcherToggleIcon
+ imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+ int tabSwitcherToggleIdsAccessibilityLabel =
+ IDS_IOS_TAB_STRIP_ENTER_TAB_SWITCHER;
+ NSString* tabSwitcherToggleEnglishUiAutomationName = @"Enter Tab Switcher";
+ const CGFloat tabStripHeight = _view.frame.size.height;
+ CGRect buttonFrame =
+ CGRectMake(CGRectGetMaxX(_view.frame) - kTabSwitcherToggleButtonWidth,
+ 0.0, kTabSwitcherToggleButtonWidth, tabStripHeight);
+ _tabSwitcherToggleButton = [UIButton buttonWithType:UIButtonTypeCustom];
+ [_tabSwitcherToggleButton setTintColor:[UIColor whiteColor]];
+ _tabSwitcherToggleButton.frame = buttonFrame;
+ [_tabSwitcherToggleButton setContentMode:UIViewContentModeCenter];
+ _tabSwitcherToggleButton.autoresizingMask =
+ UIViewAutoresizingFlexibleLeftMargin;
+ _tabSwitcherToggleButton.backgroundColor = [UIColor clearColor];
+ _tabSwitcherToggleButton.exclusiveTouch = YES;
+ [_tabSwitcherToggleButton setImage:tabSwitcherToggleIcon
+ forState:UIControlStateNormal];
+ // Set target/action to bubble up with command id as tag.
+ [_tabSwitcherToggleButton addTarget:nil
+ action:@selector(chromeExecuteCommand:)
+ forControlEvents:UIControlEventTouchUpInside];
+ [_tabSwitcherToggleButton setTag:IDC_TOGGLE_TAB_SWITCHER];
+ [_tabSwitcherToggleButton addTarget:self
+ action:@selector(recordUserMetrics:)
+ forControlEvents:UIControlEventTouchUpInside];
+
+ SetA11yLabelAndUiAutomationName(_tabSwitcherToggleButton,
+ tabSwitcherToggleIdsAccessibilityLabel,
+ tabSwitcherToggleEnglishUiAutomationName);
+ [_view addSubview:_tabSwitcherToggleButton];
+ // Shrink the scroll view.
+ [self updateScrollViewFrameForToggleButton];
+}
+
+- (void)removeTabSwitcherToggleButton {
+ // Remove the button view.
+ DCHECK(_tabSwitcherToggleButton);
+ [_tabSwitcherToggleButton removeFromSuperview];
+ _tabSwitcherToggleButton = nil;
+ // Extend the scroll view.
+ [self updateScrollViewFrameForToggleButton];
+}
+
+- (void)updateContentSizeAndRepositionViews {
+ // TODO(rohitrao): The following lines are duplicated in
+ // layoutTabStripSubviews. Find a way to consolidate this logic.
+ const NSUInteger tabCount = [_tabArray count] - [_closingTabs count];
+ if (!tabCount)
+ return;
+ const CGFloat tabHeight = CGRectGetHeight([_tabStripView bounds]);
+ CGFloat visibleSpace = [self tabStripVisibleSpace];
+ _currentTabWidth =
+ (visibleSpace + ([self tabOverlap] * (tabCount - 1))) / tabCount;
+ _currentTabWidth = MIN(_currentTabWidth, kMaxTabWidth);
+ _currentTabWidth = MAX(_currentTabWidth, [self minTabWidth]);
+
+ // Set the content size to be large enough to contain all the tabs at the
+ // desired width, with the standard overlap, plus the new tab button.
+ CGSize contentSize = CGSizeMake(
+ _currentTabWidth * tabCount - ([self tabOverlap] * (tabCount - 1)) +
+ CGRectGetWidth([_buttonNewTab frame]) - kNewTabOverlap,
+ tabHeight);
+ if (CGSizeEqualToSize([_tabStripView contentSize], contentSize))
+ return;
+
+ // Background: The scroll view might change the content offset when updating
+ // the content size. This can happen when the old content offset would result
+ // in an overscroll at the new content size. (Note that the content offset
+ // will never change if the content size is growing.)
+ //
+ // To handle this without making views appear to jump, shift all of the
+ // subviews by an amount equal to the size change. This effectively places
+ // the subviews back where they were before the change, in terms of screen
+ // coordinates.
+ CGPoint oldOffset = [_tabStripView contentOffset];
+ [_tabStripView setContentSize:contentSize];
+
+ CGFloat dx = [_tabStripView contentOffset].x - oldOffset.x;
+ for (UIView* view in [_tabStripView subviews]) {
+ CGRect frame = [view frame];
+ frame.origin.x += dx;
+ [view setFrame:frame];
+ _targetFrames.AddFrame(view, frame);
+ }
+}
+
+- (CGRect)scrollViewFrameForTab:(TabView*)view {
+ NSUInteger index = [self modelIndexForTabView:view];
+
+ CGRect frame = [view frame];
+ frame.origin.x =
+ (_currentTabWidth * index) - ([self tabOverlap] * (index - 1));
+ return frame;
+}
+
+- (CGRect)calculateVisibleFrameForFrame:(CGRect)frame
+ whenUnderFrame:(CGRect)frameOnTop {
+ CGFloat minX = CGRectGetMinX(frame);
+ CGFloat maxX = CGRectGetMaxX(frame);
+
+ if (CGRectGetMinX(frame) < CGRectGetMinX(frameOnTop))
+ maxX = CGRectGetMinX(frameOnTop);
+ else
+ minX = CGRectGetMaxX(frameOnTop);
+
+ frame.origin.x = minX;
+ frame.size.width = maxX - minX;
+ return frame;
+}
+
+#pragma mark -
+#pragma mark - compact layout
+
+- (NSUInteger)maxNumCollapsedTabs {
+ return IsCompactTablet() ? kMaxNumCollapsedTabsForCompactLayout
+ : kMaxNumCollapsedTabs;
+}
+
+- (CGFloat)tabOverlap {
+ return IsCompactTablet() ? kTabOverlapForCompactLayout : kTabOverlap;
+}
+
+- (CGFloat)minTabWidth {
+ return IsCompactTablet() ? kMinTabWidthForCompactLayout : kMinTabWidth;
+}
+
+- (void)updateContentOffsetForTabIndex:(NSUInteger)tabIndex {
+ DCHECK_NE(NSNotFound, static_cast<NSInteger>(tabIndex));
+
+ if (IsCompactTablet()) {
+ if (tabIndex == [_tabArray count] - 1) {
+ const CGFloat tabStripAvailableSpace =
+ _tabStripView.frame.size.width - _tabStripView.contentInset.right;
+ if (_tabStripView.contentSize.width > tabStripAvailableSpace) {
+ CGFloat scrollToPoint =
+ _tabStripView.contentSize.width - tabStripAvailableSpace;
+ [_tabStripView setContentOffset:CGPointMake(scrollToPoint, 0)
+ animated:YES];
+ }
+ } else {
+ TabView* tabView = [_tabArray objectAtIndex:tabIndex];
+ CGRect scrollRect =
+ CGRectInset(tabView.frame, -_tabStripView.contentInset.right, 0);
+ if (tabView)
+ [_tabStripView scrollRectToVisible:scrollRect animated:YES];
+ }
+ }
+}
+
+- (void)updateScrollViewFrameForToggleButton {
+ CGRect tabFrame = _tabStripView.frame;
+ tabFrame.size.width = _view.bounds.size.width;
+ if (!IsCompactTablet()) {
+ if (_modeToggleButton)
+ tabFrame.size.width -= kModeToggleButtonWidth;
+ if (_tabSwitcherToggleButton)
+ tabFrame.size.width -= kTabSwitcherToggleButtonWidth;
+ _tabStripView.contentInset = UIEdgeInsetsZero;
+ [_toggleButtonBackgroundView setHidden:YES];
+ } else {
+ if (!_toggleButtonBackgroundView) {
+ _toggleButtonBackgroundView.reset([[UIImageView alloc] init]);
+ const CGFloat tabStripHeight = _view.frame.size.height;
+ const CGRect backgroundViewFrame = CGRectMake(
+ CGRectGetMaxX(_view.frame) - kModeToggleButtonBackgroundWidth, 0.0,
+ kModeToggleButtonBackgroundWidth, tabStripHeight);
+ [_toggleButtonBackgroundView setFrame:backgroundViewFrame];
+ [_toggleButtonBackgroundView
+ setAutoresizingMask:UIViewAutoresizingFlexibleLeftMargin];
+ UIImage* backgroundToggleImage =
+ [UIImage imageNamed:@"tabstrip_toggle_button_gradient"];
+ [_toggleButtonBackgroundView setImage:backgroundToggleImage];
+ [_view addSubview:_toggleButtonBackgroundView];
+ }
+ const BOOL hasModeToggleButton =
+ _modeToggleButton || _tabSwitcherToggleButton;
+ [_toggleButtonBackgroundView setHidden:!hasModeToggleButton];
+ if (!hasModeToggleButton)
+ _tabStripView.contentInset = UIEdgeInsetsZero;
+ if (_modeToggleButton) {
+ _tabStripView.contentInset =
+ UIEdgeInsetsMake(0, 0, 0, kModeToggleButtonWidth);
+ [_view bringSubviewToFront:_modeToggleButton];
+ }
+ if (_tabSwitcherToggleButton) {
+ _tabStripView.contentInset =
+ UIEdgeInsetsMake(0, 0, 0, kTabSwitcherToggleButtonWidth);
+ [_view bringSubviewToFront:_tabSwitcherToggleButton];
+ }
+ }
+ [_tabStripView setFrame:tabFrame];
+}
+
+#pragma mark - TabStripViewLayoutDelegate
+
+// Creates TabViews for each Tab in the TabModel and positions them in the
+// correct location onscreen.
+- (void)layoutTabStripSubviews {
+ const NSUInteger tabCount = [_tabArray count] - [_closingTabs count];
+ if (!tabCount)
+ return;
+ BOOL animate = _animateLayout;
+ _animateLayout = NO;
+ // Disable the animation if the tab count is changing from 0 to 1.
+ if (tabCount == 1 && [_closingTabs count] == 0)
+ animate = NO;
+
+ const CGFloat tabHeight = CGRectGetHeight([_tabStripView bounds]);
+
+ // In compact layout mode the space used to layout the tabs is not
+ // constrained and uses the whole scroll view content size width. In regular
+ // layout mode the available space is constrained to the visible space.
+ CGFloat availableSpace = IsCompactTablet() ? _tabStripView.contentSize.width
+ : [self tabStripVisibleSpace];
+
+ // The array and model indexes of the selected tab.
+ NSUInteger selectedModelIndex = [_tabModel indexOfTab:[_tabModel currentTab]];
+ NSUInteger selectedArrayIndex = [self indexForModelIndex:selectedModelIndex];
+
+ // This method lays out tabs in two coordinate systems. The first, the
+ // "virtual" coordinate system, is a system rooted at x=0 that contains all
+ // the tabs laid out as if the tabstrip was infinitely long. In this system,
+ // |virtualMinX| contains the starting X coordinate of the next tab to be
+ // placed and |virtualMaxX| contains the maximum X coordinate of the last tab
+ // to be placed.
+ //
+ // The scroll view's content area is sized to be large enough to hold all the
+ // tabs with proper overlap, but the viewport is set to only show a part of
+ // the content area. The specific part that is shown is given by the scroll
+ // view's contentOffset.
+ //
+ // To layout tabs, first calculate where the tab should be in the "virtual"
+ // coordinate system. This gives the frame of the tab assuming the tabstrip
+ // was large enough to hold all tabs without needing to overflow. Then,
+ // adjust the tab's virtual frame to move it onscreen. This gives the tab's
+ // real frame.
+ CGFloat virtualMinX = 0;
+ CGFloat virtualMaxX = 0;
+ CGFloat offset = IsCompactTablet() ? 0 : [_tabStripView contentOffset].x;
+
+ // Keeps track of which tabs need to be animated. Using an autoreleased array
+ // instead of scoped_nsobject because scoped_nsobject doesn't seem to work
+ // well with blocks.
+ NSMutableArray* tabsNeedingAnimation =
+ [NSMutableArray arrayWithCapacity:tabCount];
+
+ CGRect dragFrame = [_draggedTab frame];
+
+ TabView* previousTabView = nil;
+ CGRect previousTabFrame = CGRectZero;
+ BOOL hasPlaceholderGap = NO;
+ for (NSUInteger arrayIndex = 0; arrayIndex < [_tabArray count];
+ ++arrayIndex) {
+ TabView* view = (TabView*)[_tabArray objectAtIndex:arrayIndex];
+
+ // Arrange the tabs in a V going backwards from the selected tab. This
+ // differs from desktop in order to make the tab overflow behavior work (on
+ // desktop, the tabs are arranged going backwards from left to right, with
+ // the selected tab above all others).
+ //
+ // When reordering, use slightly different logic. Instead of a V based on
+ // the model indexes of the tabs, the V fans out from the placeholder gap,
+ // which is visually where the dragged tab is. In reordering mode, the tabs
+ // are not necessarily z-ordered according to their model indexes, because
+ // they are not necessarily drawn in the spot dictated by their current
+ // model index.
+ BOOL isSelectedTab = (arrayIndex == selectedArrayIndex);
+ BOOL zOrderedAbove =
+ _isReordering ? !hasPlaceholderGap : (arrayIndex <= selectedArrayIndex);
+
+ if (isSelectedTab) {
+ // Order matters. The dimming view needs to end up behind the selected
+ // tab, so it's brought to the front first, followed by the tab.
+ [_tabStripView bringSubviewToFront:_dimmingView];
+ [_tabStripView bringSubviewToFront:view];
+ } else if (zOrderedAbove) {
+ // If the current tab comes after the selected tab in the model but still
+ // needs to be z-ordered above, place it relative to the dimming view,
+ // rather than blindly bringing it to the front. This can only happen in
+ // reordering mode.
+ if (arrayIndex > selectedArrayIndex) {
+ DCHECK(_isReordering);
+ [_tabStripView insertSubview:view belowSubview:_dimmingView];
+ } else {
+ [_tabStripView bringSubviewToFront:view];
+ }
+ } else {
+ [_tabStripView sendSubviewToBack:view];
+ }
+
+ // Ignore closing tabs when repositioning.
+ NSUInteger currentModelIndex = [self modelIndexForIndex:arrayIndex];
+ if (currentModelIndex == NSNotFound)
+ continue;
+
+ // Ignore the tab that is currently being dragged.
+ if (_isReordering && view == _draggedTab)
+ continue;
+
+ // |realMinX| is the furthest left the tab can be, in real coordinates.
+ // This is computed by counting the number of possible collapsed tabs that
+ // can be to the left of this tab, then multiplying that count by the size
+ // of a collapsed tab.
+ //
+ // There can be up to |[self maxNumCollapsedTabs]| to the left of the
+ // selected
+ // tab, and the same number to the right of the selected tab.
+ NSUInteger numPossibleCollapsedTabsToLeft =
+ std::min(currentModelIndex, [self maxNumCollapsedTabs]);
+ if (currentModelIndex > selectedModelIndex) {
+ // If this tab is to the right of the selected tab, also include the
+ // number of collapsed tabs on the right of the selected tab.
+ numPossibleCollapsedTabsToLeft =
+ std::min(selectedModelIndex, [self maxNumCollapsedTabs]) +
+ std::min(currentModelIndex - selectedModelIndex,
+ [self maxNumCollapsedTabs]);
+ }
+ CGFloat realMinX =
+ offset + (numPossibleCollapsedTabsToLeft * kCollapsedTabOverlap);
+
+ // |realMaxX| is the furthest right the tab can be, in real coordinates.
+ NSUInteger numPossibleCollapsedTabsToRight =
+ std::min(tabCount - currentModelIndex - 1, [self maxNumCollapsedTabs]);
+ if (currentModelIndex < selectedModelIndex) {
+ // If this tab is to the left of the selected tab, also include the
+ // number of collapsed tabs on the left of the selected tab.
+ numPossibleCollapsedTabsToRight =
+ std::min(tabCount - selectedModelIndex - 1,
+ [self maxNumCollapsedTabs]) +
+ std::min(selectedModelIndex - currentModelIndex,
+ [self maxNumCollapsedTabs]);
+ }
+ CGFloat realMaxX = offset + availableSpace -
+ (numPossibleCollapsedTabsToRight * kCollapsedTabOverlap);
+
+ // If this tab is to the right of the currently dragged tab, add a
+ // placeholder gap.
+ if (_isReordering && !hasPlaceholderGap &&
+ CGRectGetMinX(dragFrame) < virtualMinX + (_currentTabWidth / 2.0)) {
+ virtualMinX += _currentTabWidth - [self tabOverlap];
+ hasPlaceholderGap = YES;
+
+ // Fix up the z-ordering of the current view. It was placed assuming that
+ // the placeholder gap hasn't been hit yet.
+ [_tabStripView sendSubviewToBack:view];
+
+ // The model index of the placeholder gap is equal to the model index of
+ // the shifted tab, adjusted for the presence of the dragged tab. This
+ // value will be used as the new model index for the dragged tab when it
+ // is dropped.
+ _placeholderGapModelIndex = currentModelIndex;
+ if ([self modelIndexForTabView:_draggedTab] < currentModelIndex)
+ _placeholderGapModelIndex--;
+ }
+
+ // |tabX| stores where we are placing the tab, in real coordinates. Start
+ // by trying to place the tab at the computed |virtualMinX|, then constrain
+ // that by |realMinX| and |realMaxX|.
+ CGFloat tabX = MAX(virtualMinX, realMinX);
+ if (tabX + _currentTabWidth > realMaxX)
+ tabX = realMaxX - _currentTabWidth;
+
+ CGRect frame = CGRectMake(AlignValueToPixel(tabX), 0,
+ AlignValueToPixel(_currentTabWidth), tabHeight);
+ virtualMinX += (_currentTabWidth - [self tabOverlap]);
+ virtualMaxX = CGRectGetMaxX(frame);
+
+// TODO(rohitrao): Temporarily disabled this logic as it does not play well with
+// tab scrolling.
+#if 0
+ // If this tab is completely hidden by the previous tab, remove it from the
+ // scroll view. Otherwise, add it back in if needed.
+ if (selectedArrayIndex != arrayIndex &&
+ CGRectEqualToRect(frame, previousTabFrame)) {
+ [view removeFromSuperview];
+ } else if (![view superview]) {
+ // TODO(rohitrao): Find a way to move the z-ordering code from the top of
+ // the function to down here, so we can consolidate the logic.
+ [_tabStripView insertSubview:view atIndex:
+ (zOrderedAbove ? [[_tabStripView subviews] count] : 0)];
+ }
+#endif
+
+ // Update the tab's collapsed state based on overlap with the previous tab.
+ if (zOrderedAbove) {
+ CGRect visibleRect = [self calculateVisibleFrameForFrame:previousTabFrame
+ whenUnderFrame:frame];
+ BOOL collapsed =
+ CGRectGetWidth(visibleRect) < kCollapsedTabWidthThreshold;
+ [previousTabView setCollapsed:collapsed];
+
+ // The selected tab can never be collapsed, since no tab will ever be
+ // z-ordered above it to obscure it.
+ if (isSelectedTab)
+ [view setCollapsed:NO];
+ } else {
+ CGRect visibleRect =
+ [self calculateVisibleFrameForFrame:frame
+ whenUnderFrame:previousTabFrame];
+ BOOL collapsed =
+ CGRectGetWidth(visibleRect) < kCollapsedTabWidthThreshold;
+ [view setCollapsed:collapsed];
+ }
+
+ if (animate) {
+ if (!CGRectEqualToRect(frame, [view frame]))
+ [tabsNeedingAnimation addObject:view];
+ } else {
+ if (!CGRectEqualToRect(frame, [view frame]))
+ [view setFrame:frame];
+ }
+
+ // Throw the target frame into the dictionary so we can animate it later.
+ _targetFrames.AddFrame(view, frame);
+
+ // Ensure the tab is visible.
+ if ([view isHidden]) {
+ if (animate) {
+ // If it is a new tab, and animation is enabled, make it a submarine tab
+ // by immediately positioning it under the tabstrip.
+ CGRect submarineFrame = CGRectOffset(frame, 0, CGRectGetHeight(frame));
+ [view setFrame:submarineFrame];
+ }
+ [view setHidden:NO];
+ }
+
+ previousTabView = view;
+ previousTabFrame = frame;
+ }
+
+ // If in reordering mode and there was no placeholder gap, then the dragged
+ // tab must be all the way to the right of the other tabs. Set the
+ // _placeholderGapModelIndex accordingly.
+ if (!hasPlaceholderGap && _isReordering)
+ _placeholderGapModelIndex = [_tabModel count] - 1;
+
+ // Do not move the new tab button if it is hidden. This will lead to better
+ // animations when exiting drag and drop mode, as the new tab button will not
+ // have moved during the drag.
+ CGRect newTabFrame = [_buttonNewTab frame];
+ BOOL moveNewTab =
+ (newTabFrame.origin.x != virtualMaxX) && !_buttonNewTab.hidden;
+ newTabFrame.origin = CGPointMake(virtualMaxX - kNewTabOverlap, 0);
+ if (!animate && moveNewTab)
+ [_buttonNewTab setFrame:newTabFrame];
+
+ if (animate) {
+ float delay = 0.0;
+ if ([self.fullscreenDelegate currentHeaderOffset] != 0) {
+ // Move the toolbar to visible and wait for the end of that animation to
+ // animate the appearance of the new tab.
+ delay = ios_internal::kToolbarAnimationDuration;
+ // Signal the FullscreenController that the toolbar needs to stay on
+ // screen for a bit, so the animation is visible.
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kWillStartTabStripTabAnimation
+ object:nil];
+ }
+
+ [UIView animateWithDuration:kTabAnimationDuration
+ delay:delay
+ options:UIViewAnimationOptionAllowUserInteraction
+ animations:^{
+ for (TabView* view in tabsNeedingAnimation) {
+ DCHECK(_targetFrames.HasFrame(view));
+ [view setFrame:_targetFrames.GetFrame(view)];
+ }
+ if (moveNewTab)
+ [_buttonNewTab setFrame:newTabFrame];
+ }
+ completion:nil];
+ }
+}
+
+- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
+ [self updateScrollViewFrameForToggleButton];
+ [self updateContentSizeAndRepositionViews];
+ NSUInteger selectedModelIndex = [_tabModel indexOfTab:[_tabModel currentTab]];
+ if (selectedModelIndex != NSNotFound) {
+ [self updateContentOffsetForTabIndex:selectedModelIndex];
+ }
+}
+
+- (void)setNeedsLayoutWithAnimation {
+ _animateLayout = YES;
+ [_tabStripView setNeedsLayout];
+}
+
+@end
+
+#pragma mark - TabSwitcherAnimation
+
+@implementation TabStripController (TabSwitcherAnimation)
+
+- (TabSwitcherTabStripPlaceholderView*)placeholderView {
+ TabSwitcherTabStripPlaceholderView* placeholderView =
+ [[[TabSwitcherTabStripPlaceholderView alloc]
+ initWithFrame:self.view.bounds] autorelease];
+ CGFloat xOffset = [_tabStripView contentOffset].x;
+ UIView* previousView = nil;
+ const NSUInteger selectedModelIndex =
+ [_tabModel indexOfTab:[_tabModel currentTab]];
+ const NSUInteger selectedArrayIndex =
+ [self indexForModelIndex:selectedModelIndex];
+ [self updateContentSizeAndRepositionViews];
+ [self layoutTabStripSubviews];
+ for (NSUInteger tabArrayIndex = 0; tabArrayIndex < [_tabArray count];
+ ++tabArrayIndex) {
+ UIView* tabView = _tabArray[tabArrayIndex];
+ UIView* tabSnapshotView = snapshot_util::GenerateSnapshot(tabView);
+ tabSnapshotView.frame = CGRectOffset(tabView.frame, -xOffset, 0);
+ tabSnapshotView.transform = tabView.transform;
+ // Order views of the tabs in a pyramid fashion, culminating with
+ // the selected tab.
+ // For example, if _tabArray has views [0..6], and 3 is the selected index,
+ // they will be arranged in the order [0, 1, 2, 6, 5, 4, 3].
+ if (previousView && tabArrayIndex > selectedArrayIndex) {
+ [placeholderView insertSubview:tabSnapshotView belowSubview:previousView];
+ } else {
+ [placeholderView addSubview:tabSnapshotView];
+ }
+ previousView = tabSnapshotView;
+ }
+ UIView* buttonSnapshot = snapshot_util::GenerateSnapshot(_buttonNewTab);
+ buttonSnapshot.frame = CGRectOffset(_buttonNewTab.frame, -xOffset, 0);
+ [placeholderView addSubview:buttonSnapshot];
+ return placeholderView;
+}
+
+@end
+
+@implementation TabStripController (Testing)
+
+- (TabView*)existingTabViewForTab:(Tab*)tab {
+ NSUInteger tabIndex = [_tabModel indexOfTab:tab];
+ NSUInteger tabViewIndex = [self indexForModelIndex:tabIndex];
+ return [_tabArray objectAtIndex:tabViewIndex];
+}
+
+@end

Powered by Google App Engine
This is Rietveld 408576698