| 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
|
|
|