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 |