blob: 33d7def6df0a984d7f120346332d19c7b894551e [file] [log] [blame]
// 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"
#include <cmath>
#include <memory>
#include <vector>
#include "base/i18n/rtl.h"
#include "base/mac/bundle_locations.h"
#include "base/mac/foundation_util.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/sys_string_conversions.h"
#include "components/favicon/ios/web_favicon_driver.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
#import "ios/chrome/browser/drag_and_drop/drag_item_util.h"
#import "ios/chrome/browser/drag_and_drop/url_drag_drop_handler.h"
#include "ios/chrome/browser/main/browser.h"
#import "ios/chrome/browser/snapshots/snapshot_tab_helper.h"
#include "ios/chrome/browser/system_flags.h"
#import "ios/chrome/browser/tabs/tab_title_util.h"
#import "ios/chrome/browser/ui/bubble/bubble_util.h"
#import "ios/chrome/browser/ui/bubble/bubble_view.h"
#import "ios/chrome/browser/ui/commands/application_commands.h"
#import "ios/chrome/browser/ui/commands/command_dispatcher.h"
#import "ios/chrome/browser/ui/commands/open_new_tab_command.h"
#import "ios/chrome/browser/ui/commands/popup_menu_commands.h"
#include "ios/chrome/browser/ui/fullscreen/fullscreen_controller.h"
#import "ios/chrome/browser/ui/fullscreen/fullscreen_features.h"
#include "ios/chrome/browser/ui/fullscreen/scoped_fullscreen_disabler.h"
#import "ios/chrome/browser/ui/gestures/view_revealing_vertical_pan_handler.h"
#import "ios/chrome/browser/ui/ntp/ntp_util.h"
#import "ios/chrome/browser/ui/popup_menu/public/popup_menu_long_press_delegate.h"
#import "ios/chrome/browser/ui/tabs/requirements/tab_strip_constants.h"
#import "ios/chrome/browser/ui/tabs/requirements/tab_strip_presentation.h"
#import "ios/chrome/browser/ui/tabs/tab_strip_container_view.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"
#import "ios/chrome/browser/ui/ui_feature_flags.h"
#import "ios/chrome/browser/ui/util/named_guide.h"
#include "ios/chrome/browser/ui/util/rtl_geometry.h"
#include "ios/chrome/browser/ui/util/ui_util.h"
#import "ios/chrome/browser/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/url_loading/url_loading_browser_agent.h"
#import "ios/chrome/browser/url_loading/url_loading_params.h"
#import "ios/chrome/browser/web_state_list/all_web_state_observation_forwarder.h"
#import "ios/chrome/browser/web_state_list/web_state_list.h"
#import "ios/chrome/browser/web_state_list/web_state_list_favicon_driver_observer.h"
#import "ios/chrome/browser/web_state_list/web_state_list_observer_bridge.h"
#include "ios/chrome/browser/web_state_list/web_state_opener.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#include "ios/chrome/grit/ios_strings.h"
#include "ios/public/provider/chrome/browser/chrome_browser_provider.h"
#import "ios/public/provider/chrome/browser/ui/fullscreen_provider.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_state_observer_bridge.h"
#include "ui/gfx/image/image.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using base::UserMetricsAction;
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 kTabOverlapStacked = 32.0;
const CGFloat kTabOverlapUnstacked = 30.0;
const CGFloat kNewTabOverlap = 13.0;
const CGFloat kMaxTabWidthStacked = 265.0;
const CGFloat kMaxTabWidthUnstacked = 225.0;
const CGFloat kMinTabWidthStacked = 200.0;
const CGFloat kMinTabWidthUnstacked = 160.0;
const CGFloat kCollapsedTabOverlap = 5.0;
const int kMaxNumCollapsedTabsStacked = 3;
const int kMaxNumCollapsedTabsUnstacked = 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;
// The size of the tab strip view.
const CGFloat kTabStripHeight = 39.0;
// The size of the new tab button.
const CGFloat kNewTabButtonWidth = 44;
// Default image insets for the new tab button.
const CGFloat kNewTabButtonLeadingImageInset = -10.0;
const CGFloat kNewTabButtonBottomImageInset = -2.0;
// Returns the background color.
UIColor* BackgroundColor() {
if (base::FeatureList::IsEnabled(kExpandedTabStrip)) {
// The background needs to be clear to allow the thumb strip to be seen
// from behind the tab strip during the enter/exit thumb strip animation.
// However, when using the fullscreen provider, the WKWebView extends behind
// the tab strip. In this case, a clear background would lead to seeing the
// WKWebView instead of the thumb strip.
return ios::GetChromeBrowserProvider()
->GetFullscreenProvider()
->IsInitialized()
? UIColor.blackColor
: UIColor.clearColor;
}
return UIColor.blackColor;
}
} // namespace
// Helper class to display a UIButton with the image and text centered
// vertically and horizontally.
@interface TabStripCenteredButton : UIButton {
}
@end
@implementation TabStripCenteredButton
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
self.titleLabel.textAlignment = NSTextAlignmentCenter;
self.titleLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightBold];
self.titleLabel.adjustsFontSizeToFitWidth = YES;
self.titleLabel.minimumScaleFactor = 0.1;
self.titleLabel.baselineAdjustment = UIBaselineAdjustmentAlignCenters;
#if defined(__IPHONE_13_4)
if (@available(iOS 13.4, *)) {
self.pointerInteractionEnabled = YES;
}
#endif // defined(__IPHONE_13_4)
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGSize size = self.bounds.size;
CGPoint center = CGPointMake(size.width / 2, size.height / 2);
self.imageView.center = center;
self.imageView.frame = AlignRectToPixel(self.imageView.frame);
self.titleLabel.frame = self.bounds;
}
@end
@interface TabStripController () <CRWWebStateObserver,
TabStripViewLayoutDelegate,
TabViewDelegate,
ViewRevealingAnimatee,
WebStateListObserving,
WebStateFaviconDriverObserver,
UIGestureRecognizerDelegate,
UIScrollViewDelegate,
URLDropDelegate> {
Browser* _browser;
WebStateList* _webStateList;
TabStripContainerView* _view;
TabStripView* _tabStripView;
UIButton* _buttonNewTab;
TabStripStyle _style;
// Array of TabViews. There is a one-to-one correspondence between this array
// and the set of Tabs in the WebStateList.
NSMutableArray* _tabArray;
// Set of TabViews that are currently closing. These TabViews are also in
// |_tabArray|. Used to translate between |_tabArray| indexes and
// WebStateList indexes.
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.
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.
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.
__weak 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 WebStateList index of the placeholder gap, if one exists. This value is
// used as the new WebStateList index of the dragged tab when it is dropped.
int _placeholderGapWebStateListIndex;
// YES if this tab strip is representing an incognito browser.
BOOL _isIncognito;
// The disabler that prevents the toolbar from being scrolled offscreen during
// drags.
std::unique_ptr<ScopedFullscreenDisabler> _fullscreenDisabler;
// Bridges C++ WebStateListObserver methods to this TabStripController.
std::unique_ptr<WebStateListObserverBridge> _webStateListObserver;
// Bridges FaviconDriverObservers methods to this TabStripController, and
// maintains a FaviconObserver for each all webstates.
std::unique_ptr<WebStateListFaviconDriverObserver>
_webStateListFaviconObserver;
// Bridges C++ WebStateObserver methods to this TabStripController.
std::unique_ptr<web::WebStateObserverBridge> _webStateObserver;
// Forwards observer methods for all WebStates in the WebStateList monitored
// by the TabStripController.
std::unique_ptr<AllWebStateObservationForwarder>
_allWebStateObservationForwarder;
}
@property(nonatomic, readonly, retain) TabStripView* tabStripView;
@property(nonatomic, readonly, retain) UIButton* buttonNewTab;
// YES if the controller has been disconnected.
@property(nonatomic) BOOL disconnected;
// If set to |YES|, tabs at either end of the tabstrip are "collapsed" into a
// stack, such that the visible width of the tabstrip is constant. If set to
// |NO|, tabs are never collapsed and the tabstrip scrolls horizontally as a
// normal scroll view would. Changing this property causes the tabstrip to
// redraw and relayout. Defaults to |YES|.
@property(nonatomic, assign) BOOL useTabStacking;
// Handler for URL drop interactions.
@property(nonatomic, strong) URLDragDropHandler* dragDropHandler;
// Pan gesture recognizer for the view revealing pan gesture handler.
@property(nonatomic, weak) UIPanGestureRecognizer* panGestureRecognizer;
// Initializes the tab array based on the the entries in the |_webStateList|'s.
// 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)initializeTabArray;
// Returns an autoreleased TabView object with no content.
- (TabView*)emptyTabView;
// Returns an autoreleased TabView object based on the given |webState|.
// |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*)createTabViewForWebState:(web::WebState*)webState
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 WebStateList
// does not.
- (NSUInteger)indexForWebStateListIndex:(int)modelIndex;
- (int)webStateListIndexForIndex:(NSUInteger)index;
- (int)webStateListIndexForTabView:(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.
- (CGFloat)tabStripVisibleSpace;
// Shift all of the tab strip subviews by an amount equal to the content offset
// change, which effectively places the subviews back where they were before the
// change, in terms of screen coordinates.
- (void)shiftTabStripSubviews:(CGPoint)oldContentOffset;
// 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.
- (int)maxNumCollapsedTabs;
// Returns the tab overlap width depending on the current layout mode.
- (CGFloat)tabOverlap;
// Returns the maximum tab view width depending on the current layout mode.
- (CGFloat)maxTabWidth;
// Returns the minimum tab view width depending on the current layout mode.
- (CGFloat)minTabWidth;
// Automatically scroll the tab strip view to keep the given tab view visible.
// This method must be called with a valid |tabIndex|.
- (void)scrollTabToVisible:(int)tabIndex;
// 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 unstacked mode or
// regular mode for newly opened webStates.
// This method must be called with a valid |WebStateIndex|.
- (void)updateContentOffsetForWebStateIndex:(int)WebStateIndex
isNewWebState:(BOOL)isNewWebState;
// Update the frame of the tab strip view (scrollview) frame, content inset and
// toggle buttons states depending on the current layout mode.
- (void)updateScrollViewFrameForTabSwitcherButton;
// Returns the existing tab view for |webState| or nil if there is no TabView
// for it.
- (TabView*)tabViewForWebState:(web::WebState*)webState;
// Computes whether the tabstrip should use tab stacking.
- (BOOL)shouldUseTabStacking;
@end
@implementation TabStripController
@synthesize buttonNewTab = _buttonNewTab;
@synthesize highlightsSelectedTab = _highlightsSelectedTab;
@synthesize tabStripView = _tabStripView;
@synthesize view = _view;
@synthesize longPressDelegate = _longPressDelegate;
@synthesize presentationProvider = _presentationProvider;
@synthesize animationWaitDuration = _animationWaitDuration;
@synthesize panGestureHandler = _panGestureHandler;
- (instancetype)initWithBrowser:(Browser*)browser style:(TabStripStyle)style {
if ((self = [super init])) {
_tabArray = [[NSMutableArray alloc] initWithCapacity:10];
_closingTabs = [[NSMutableSet alloc] initWithCapacity:5];
DCHECK(browser);
_browser = browser;
_webStateList = _browser->GetWebStateList();
_webStateListObserver = std::make_unique<WebStateListObserverBridge>(self);
_webStateList->AddObserver(_webStateListObserver.get());
_webStateListFaviconObserver =
std::make_unique<WebStateListFaviconDriverObserver>(_webStateList,
self);
_webStateObserver = std::make_unique<web::WebStateObserverBridge>(self);
// Observe all webStates of this |_webStateList|.
_allWebStateObservationForwarder =
std::make_unique<AllWebStateObservationForwarder>(
_webStateList, _webStateObserver.get());
_style = style;
// |self.view| setup.
_useTabStacking = [self shouldUseTabStacking];
CGRect tabStripFrame = [UIApplication sharedApplication].keyWindow.bounds;
tabStripFrame.size.height = kTabStripHeight;
_view = [[TabStripContainerView alloc] initWithFrame:tabStripFrame];
_view.autoresizingMask = (UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleBottomMargin);
_view.backgroundColor = 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 == INCOGNITO ? @"Incognito Tab Strip" : @"Tab Strip";
[_view addSubview:_tabStripView];
_view.tabStripView = _tabStripView;
// |self.buttonNewTab| setup.
CGRect buttonNewTabFrame = tabStripFrame;
buttonNewTabFrame.size.width = kNewTabButtonWidth;
_buttonNewTab = [[UIButton alloc] initWithFrame:buttonNewTabFrame];
_isIncognito = _browser->GetBrowserState()->IsOffTheRecord();
// 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 = [UIImage imageNamed:@"tabstrip_new_tab"];
buttonNewTabImage = [buttonNewTabImage
imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
[_buttonNewTab setImage:buttonNewTabImage forState:UIControlStateNormal];
[_buttonNewTab.imageView setTintColor:[UIColor colorNamed:kGrey500Color]];
UIEdgeInsets imageInsets = UIEdgeInsetsMake(
0, kNewTabButtonLeadingImageInset, kNewTabButtonBottomImageInset, 0);
_buttonNewTab.imageEdgeInsets = imageInsets;
SetA11yLabelAndUiAutomationName(
_buttonNewTab,
_isIncognito ? IDS_IOS_TOOLS_MENU_NEW_INCOGNITO_TAB
: IDS_IOS_TOOLS_MENU_NEW_TAB,
_isIncognito ? @"New Incognito Tab" : @"New Tab");
[_buttonNewTab addTarget:self
action:@selector(sendNewTabCommand)
forControlEvents:UIControlEventTouchUpInside];
[_buttonNewTab addTarget:self
action:@selector(recordUserMetrics:)
forControlEvents:UIControlEventTouchUpInside];
#if defined(__IPHONE_13_4)
if (@available(iOS 13.4, *)) {
_buttonNewTab.pointerInteractionEnabled = YES;
}
#endif // defined(__IPHONE_13_4)
[_tabStripView addSubview:_buttonNewTab];
// Add tab buttons to tab strip.
[self initializeTabArray];
// Update the layout of the tab buttons.
[self updateContentSizeAndRepositionViews];
[self layoutTabStripSubviews];
// Don't highlight the selected tab by default.
self.highlightsSelectedTab = NO;
// Register for VoiceOver notifications.
if (base::FeatureList::IsEnabled(kVoiceOverUnstackedTabstrip)) {
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(voiceOverStatusDidChange)
name:UIAccessibilityVoiceOverStatusDidChangeNotification
object:nil];
}
self.dragDropHandler = [[URLDragDropHandler alloc] init];
self.dragDropHandler.dropDelegate = self;
[_view addInteraction:[[UIDropInteraction alloc]
initWithDelegate:self.dragDropHandler]];
}
return self;
}
- (void)dealloc {
DCHECK(_disconnected);
}
- (void)disconnect {
[_tabStripView setDelegate:nil];
[_tabStripView setLayoutDelegate:nil];
_allWebStateObservationForwarder.reset();
_webStateListFaviconObserver.reset();
_webStateList->RemoveObserver(_webStateListObserver.get());
[[NSNotificationCenter defaultCenter] removeObserver:self];
self.disconnected = YES;
}
- (void)hideTabStrip:(BOOL)hidden {
self.view.hidden = hidden;
}
- (void)tabStripSizeDidChange {
[self updateContentSizeAndRepositionViews];
[self layoutTabStripSubviews];
}
- (void)setPanGestureHandler:
(ViewRevealingVerticalPanHandler*)panGestureHandler {
_panGestureHandler = panGestureHandler;
[self.panGestureHandler addAnimatee:self];
[self.view removeGestureRecognizer:self.panGestureRecognizer];
UIPanGestureRecognizer* panGestureRecognizer = [[UIPanGestureRecognizer alloc]
initWithTarget:panGestureHandler
action:@selector(handlePanGesture:)];
panGestureRecognizer.maximumNumberOfTouches = 1;
[self.view addGestureRecognizer:panGestureRecognizer];
self.panGestureRecognizer = panGestureRecognizer;
}
#pragma mark - Private
- (void)initializeTabArray {
for (int index = 0; index < _webStateList->count(); ++index) {
web::WebState* webState = _webStateList->GetWebStateAt(index);
BOOL isSelected = index == _webStateList->active_index();
TabView* view = [self createTabViewForWebState:webState
isSelected:isSelected];
[_tabArray addObject:view];
[_tabStripView addSubview:view];
}
}
- (TabView*)emptyTabView {
TabView* view = [[TabView alloc] initWithEmptyView:YES selected:YES];
[view setIncognitoStyle:(_style == INCOGNITO)];
[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*)createTabViewForWebState:(web::WebState*)webState
isSelected:(BOOL)isSelected {
TabView* view = [[TabView alloc] initWithEmptyView:NO selected:isSelected];
if (UseRTLLayout())
[view setTransform:CGAffineTransformMakeScale(-1, 1)];
[view setIncognitoStyle:(_style == INCOGNITO)];
[view setContentMode:UIViewContentModeRedraw];
[self updateTabView:view withWebState:webState];
// Install a long press gesture recognizer to handle drag and drop.
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];
view.delegate = self;
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];
// Create the dimming view if it doesn't exist. In all cases, make sure it's
// set up correctly.
if (_dimmingView)
[_dimmingView setFrame:frame];
else
_dimmingView = [[UIView alloc] initWithFrame:frame];
// Enable user interaction in order to eat touches from views behind it.
[_dimmingView setUserInteractionEnabled:YES];
[_dimmingView
setBackgroundColor:[BackgroundColor() colorWithAlphaComponent:0]];
[_dimmingView setAutoresizingMask:(UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight)];
[_tabStripView addSubview:_dimmingView];
CGFloat duration = animate ? kTabStripFadeAnimationDuration : 0;
[UIView animateWithDuration:duration
animations:^{
[_dimmingView
setBackgroundColor:[BackgroundColor()
colorWithAlphaComponent:0.6]];
}];
}
- (void)removeDimmingViewWithAnimation:(BOOL)animate {
if (_dimmingView) {
CGFloat duration = animate ? kTabStripFadeAnimationDuration : 0;
[UIView animateWithDuration:duration
animations:^{
[_dimmingView
setBackgroundColor:[BackgroundColor() colorWithAlphaComponent:0]];
}
completion:^(BOOL finished) {
// Do not remove the dimming view if the animation was aborted.
if (finished) {
[_dimmingView removeFromSuperview];
_dimmingView = nil;
}
}];
}
}
- (void)recordUserMetrics:(id)sender {
if (sender == _buttonNewTab)
base::RecordAction(UserMetricsAction("MobileTabStripNewTab"));
else
LOG(WARNING) << "Trying to record metrics for unknown sender "
<< base::SysNSStringToUTF8([sender description]);
}
- (void)sendNewTabCommand {
CGPoint center = [_buttonNewTab.superview convertPoint:_buttonNewTab.center
toView:_buttonNewTab.window];
OpenNewTabCommand* command =
[OpenNewTabCommand commandWithIncognito:_isIncognito originPoint:center];
[[self applicationCommandsHandler] openURLInNewTab:command];
}
- (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)indexForWebStateListIndex:(int)modelIndex {
NSUInteger index = modelIndex;
NSUInteger i = 0;
for (TabView* tab in _tabArray) {
if ([_closingTabs containsObject:tab])
++index;
if (i == index)
break;
++i;
}
DCHECK_GE(index, static_cast<NSUInteger>(modelIndex));
return index;
}
- (int)webStateListIndexForIndex:(NSUInteger)index {
int listIndex = 0;
NSUInteger arrayIndex = 0;
for (TabView* tab in _tabArray) {
if (arrayIndex == index) {
if ([_closingTabs containsObject:tab])
return WebStateList::kInvalidIndex;
return listIndex;
}
if (![_closingTabs containsObject:tab])
++listIndex;
++arrayIndex;
}
return WebStateList::kInvalidIndex;
}
- (int)webStateListIndexForTabView:(TabView*)view {
return [self webStateListIndexForIndex:[_tabArray indexOfObject:view]];
}
// Updates the title and the favicon of the |view| with data from |webState|.
- (void)updateTabView:(TabView*)view withWebState:(web::WebState*)webState {
[[view titleLabel] setText:tab_util::GetTabTitle(webState)];
[view setFavicon:nil];
favicon::FaviconDriver* faviconDriver =
favicon::WebFaviconDriver::FromWebState(webState);
if (faviconDriver && faviconDriver->FaviconIsValid()) {
gfx::Image favicon = faviconDriver->GetFavicon();
if (!favicon.IsEmpty())
[view setFavicon:favicon.ToUIImage()];
}
[_tabStripView setNeedsLayout];
}
// Gets PopupMenuCommands handler from |_browser|'s command dispatcher.
- (id<PopupMenuCommands>)popupMenuCommandsHandler {
return HandlerForProtocol(_browser->GetCommandDispatcher(),
PopupMenuCommands);
}
// Gets ApplicationCommands handler from |_browser|'s command dispatcher.
- (id<ApplicationCommands>)applicationCommandsHandler {
return HandlerForProtocol(_browser->GetCommandDispatcher(),
ApplicationCommands);
}
- (void)insertNewItemAtIndex:(NSUInteger)index withURL:(const GURL&)newTabURL {
UrlLoadParams params =
UrlLoadParams::InNewTab(newTabURL, base::checked_cast<int>(index));
UrlLoadingBrowserAgent::FromBrowser(_browser)->Load(params);
}
#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.
int index = [self webStateListIndexForTabView:view];
DCHECK_NE(WebStateList::kInvalidIndex, index);
if (index == WebStateList::kInvalidIndex)
return;
// Install the dimming view, hide the new tab button, and select the tab so it
// appears highlighted.
self.highlightsSelectedTab = YES;
_buttonNewTab.hidden = YES;
_webStateList->ActivateWebStateAt(index);
// Set up initial drag state.
_lastDragLocation = [gesture locationInView:[_tabStripView superview]];
_isReordering = YES;
_draggedTab = view;
_placeholderGapWebStateListIndex =
[self webStateListIndexForTabView:_draggedTab];
// Update the autoscroll distance and timer.
[self computeAutoscrollDistanceForTabView:_draggedTab];
if (_autoscrollDistance != 0)
[self installAutoscrollTimerIfNeeded];
else
[self removeAutoscrollTimer];
// Disable fullscreen during drags.
if (fullscreen::features::ShouldScopeFullscreenControllerToBrowser()) {
_fullscreenDisabler = std::make_unique<ScopedFullscreenDisabler>(
FullscreenController::FromBrowser(_browser));
} else {
_fullscreenDisabler = std::make_unique<ScopedFullscreenDisabler>(
FullscreenController::FromBrowserState(_browser->GetBrowserState()));
}
}
- (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]]);
// Stop disabling fullscreen.
_fullscreenDisabler = nullptr;
int fromIndex = [self webStateListIndexForTabView:_draggedTab];
// TODO(crbug.com/1049882): We're seeing crashes where fromIndex is
// kInvalidIndex, indicating that the dragged tab is no longer in the
// WebStateList. This could happen if a tab closed itself during a drag.
// Investigate this further, but for now, simply test |fromIndex| before
// proceeding.
if (fromIndex == WebStateList::kInvalidIndex) {
[self resetDragState];
[self setNeedsLayoutWithAnimation];
return;
}
int toIndex = _placeholderGapWebStateListIndex;
DCHECK_NE(WebStateList::kInvalidIndex, toIndex);
DCHECK_LT(toIndex, _webStateList->count());
// Reset drag state variables before notifying the model that the tab moved.
[self resetDragState];
_webStateList->MoveWebStateAt(fromIndex, toIndex);
[self setNeedsLayoutWithAnimation];
}
- (void)cancelDrag:(UILongPressGestureRecognizer*)gesture {
DCHECK([[gesture view] isKindOfClass:[TabView class]]);
// Stop disabling fullscreen.
_fullscreenDisabler = nullptr;
// 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;
_placeholderGapWebStateListIndex = WebStateList::kInvalidIndex;
_draggedTab = nil;
}
- (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 - URLDropDelegate
- (BOOL)canHandleURLDropInView:(UIView*)view {
return !_isReordering;
}
- (void)view:(UIView*)view didDropURL:(const GURL&)URL atPoint:(CGPoint)point {
CGPoint contentPoint = CGPointMake(point.x + _tabStripView.contentOffset.x,
point.y + _tabStripView.contentOffset.y);
for (TabView* tabView in _tabArray) {
if (CGRectContainsPoint(tabView.frame, contentPoint)) {
int index = [self webStateListIndexForTabView:tabView];
DCHECK_NE(WebStateList::kInvalidIndex, index);
if (index == WebStateList::kInvalidIndex)
return;
NSUInteger insertionIndex = base::checked_cast<NSUInteger>(index);
if (contentPoint.x > CGRectGetMidX(tabView.frame)) {
insertionIndex++;
}
[self insertNewItemAtIndex:insertionIndex withURL:URL];
return;
}
}
[self insertNewItemAtIndex:_webStateList->count() withURL:URL];
}
#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 - CRWWebStateObserver methods
- (void)webStateDidStartLoading:(web::WebState*)webState {
// webState can start loading before didInsertWebState is called, in that
// case early return as there is no view to update yet.
if (static_cast<NSUInteger>(_webStateList->count()) >
_tabArray.count - _closingTabs.count)
return;
if (IsVisibleURLNewTabPage(webState))
return;
TabView* view = [self tabViewForWebState:webState];
if (!view) {
DCHECK(false) << "Received start loading notification for a Webstate "
<< "that is not contained in the WebStateList";
return;
}
[view startProgressSpinner];
[view setNeedsDisplay];
}
- (void)webStateDidStopLoading:(web::WebState*)webState {
TabView* view = [self tabViewForWebState:webState];
if (!view) {
DCHECK(false) << "Received stop loading notification for a Webstate "
<< "that is not contained in the WebStateList";
return;
}
// In new Tab case WebState's DidChangeTitle is not called. Make sure to
// updated the title here to account for that.
[view setTitle:tab_util::GetTabTitle(webState)];
[view stopProgressSpinner];
[view setNeedsDisplay];
}
- (void)webStateDidChangeTitle:(web::WebState*)webState {
TabView* view = [self tabViewForWebState:webState];
if (!view) {
DCHECK(false) << "Received title change notification for a Webstate "
<< "that is not contained in the WebStateList";
return;
}
[view setTitle:tab_util::GetTabTitle(webState)];
[view setNeedsDisplay];
}
#pragma mark -
#pragma mark WebStateListObserving methods
// Observer method, active WebState changed.
- (void)webStateList:(WebStateList*)webStateList
didChangeActiveWebState:(web::WebState*)newWebState
oldWebState:(web::WebState*)oldWebState
atIndex:(int)atIndex
reason:(ActiveWebStateChangeReason)reason {
if (!newWebState)
return;
for (TabView* view in _tabArray) {
[view setSelected:NO];
}
NSUInteger index = [self indexForWebStateListIndex:atIndex];
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. |webState| moved in |webStateList|.
- (void)webStateList:(WebStateList*)webStateList
didMoveWebState:(web::WebState*)webState
fromIndex:(int)fromIndex
toIndex:(int)toIndex {
DCHECK(!_isReordering);
// Reorder the objects in _tabArray to keep in sync with the model ordering.
NSUInteger arrayIndex = [self indexForWebStateListIndex:fromIndex];
TabView* view = [_tabArray objectAtIndex:arrayIndex];
[_tabArray removeObject:view];
[_tabArray insertObject:view atIndex:toIndex];
[self setNeedsLayoutWithAnimation];
}
// Observer method, |webState| removed from |webStateList|.
- (void)webStateList:(WebStateList*)webStateList
didDetachWebState:(web::WebState*)webState
atIndex:(int)atIndex {
// Keep the actual view around while it is animating out. Once the animation
// is done, remove the view.
NSUInteger index = [self indexForWebStateListIndex:atIndex];
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. |webState| inserted on |webStateList|.
- (void)webStateList:(WebStateList*)webStateList
didInsertWebState:(web::WebState*)webState
atIndex:(int)index
activating:(BOOL)activating {
TabView* view = [self createTabViewForWebState:webState
isSelected:activating];
[_tabArray insertObject:view atIndex:[self indexForWebStateListIndex:index]];
[[self tabStripView] addSubview:view];
[self updateContentSizeAndRepositionViews];
[self setNeedsLayoutWithAnimation];
[self updateContentOffsetForWebStateIndex:index isNewWebState:YES];
}
// Observer method, WebState replaced in |webStateList|.
- (void)webStateList:(WebStateList*)webStateList
didReplaceWebState:(web::WebState*)oldWebState
withWebState:(web::WebState*)newWebState
atIndex:(int)atIndex {
TabView* view = [self tabViewForWebState:newWebState];
[self updateTabView:view withWebState:newWebState];
}
#pragma mark -
#pragma mark WebStateFaviconDriverObserver
// Observer method. |webState| got a favicon update.
- (void)faviconDriver:(favicon::FaviconDriver*)driver
didUpdateFaviconForWebState:(web::WebState*)webState {
if (!driver)
return;
int listIndex = _webStateList->GetIndexOfWebState(webState);
if (listIndex == WebStateList::kInvalidIndex) {
DCHECK(false) << "Received FavIcon update notification for webState that is"
" not in the WebStateList";
return;
}
NSUInteger index = [self indexForWebStateListIndex:listIndex];
TabView* view = [_tabArray objectAtIndex:index];
[view setFavicon:nil];
if (driver->FaviconIsValid()) {
gfx::Image favicon = driver->GetFavicon();
if (!favicon.IsEmpty())
[view setFavicon:favicon.ToUIImage()];
}
}
#pragma mark -
#pragma mark Views and Layout
- (TabView*)tabViewForWebState:(web::WebState*)webState {
int listIndex = _webStateList->GetIndexOfWebState(webState);
if (listIndex == WebStateList::kInvalidIndex)
return nil;
NSUInteger index = [self indexForWebStateListIndex:listIndex];
return [_tabArray objectAtIndex:index];
}
- (CGFloat)tabStripVisibleSpace {
CGFloat availableSpace = CGRectGetWidth([_tabStripView bounds]) -
CGRectGetWidth([_buttonNewTab frame]) +
kNewTabOverlap;
return availableSpace;
}
- (void)shiftTabStripSubviews:(CGPoint)oldContentOffset {
CGFloat dx = [_tabStripView contentOffset].x - oldContentOffset.x;
for (UIView* view in [_tabStripView subviews]) {
CGRect frame = [view frame];
frame.origin.x += dx;
[view setFrame:frame];
_targetFrames.AddFrame(view, frame);
}
}
- (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, [self maxTabWidth]);
_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.
CGPoint oldOffset = [_tabStripView contentOffset];
[_tabStripView setContentSize:contentSize];
[self shiftTabStripSubviews:oldOffset];
}
- (CGRect)scrollViewFrameForTab:(TabView*)view {
int index = [self webStateListIndexForTabView: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 - Unstacked layout
- (int)maxNumCollapsedTabs {
return self.useTabStacking ? kMaxNumCollapsedTabsStacked
: kMaxNumCollapsedTabsUnstacked;
}
- (CGFloat)tabOverlap {
return self.useTabStacking ? kTabOverlapStacked : kTabOverlapUnstacked;
}
- (CGFloat)maxTabWidth {
return self.useTabStacking ? kMaxTabWidthStacked : kMaxTabWidthUnstacked;
}
- (CGFloat)minTabWidth {
return self.useTabStacking ? kMinTabWidthStacked : kMinTabWidthUnstacked;
}
- (void)scrollTabToVisible:(int)tabIndex {
DCHECK_NE(WebStateList::kInvalidIndex, tabIndex);
// The following code calculates the amount of scroll needed to make
// |tabIndex| visible in the "virtual" coordinate system, where root is x=0
// and it contains all the tabs laid out as if the tabstrip was infinitely
// long. The amount of scroll is calculated as a desired length that it is
// just large enough to contain all the tabs to the left of |tabIndex|, with
// the standard overlap.
if (tabIndex == static_cast<int>([_tabArray count]) - 1) {
const CGFloat tabStripAvailableSpace =
_tabStripView.frame.size.width - _tabStripView.contentInset.right;
CGPoint oldOffset = [_tabStripView contentOffset];
if (_tabStripView.contentSize.width > tabStripAvailableSpace) {
CGFloat scrollToPoint =
_tabStripView.contentSize.width - tabStripAvailableSpace;
[_tabStripView setContentOffset:CGPointMake(scrollToPoint, 0)];
}
// To handle content offset change without making views appear to jump,
// shift all of the subviews by an amount equal to the size change.
[self shiftTabStripSubviews:oldOffset];
return;
}
NSUInteger numNonClosingTabsToLeft = 0;
int i = 0;
for (TabView* tab in _tabArray) {
if ([_closingTabs containsObject:tab])
++i;
if (i == tabIndex)
break;
++numNonClosingTabsToLeft;
++i;
}
const CGFloat tabHeight = CGRectGetHeight([_tabStripView bounds]);
CGRect scrollRect =
CGRectMake(_currentTabWidth * numNonClosingTabsToLeft -
([self tabOverlap] * (numNonClosingTabsToLeft - 1)),
0, _currentTabWidth, tabHeight);
[_tabStripView scrollRectToVisible:scrollRect animated:YES];
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification,
nil);
}
- (void)updateContentOffsetForWebStateIndex:(int)webStateIndex
isNewWebState:(BOOL)isNewWebState {
DCHECK_NE(WebStateList::kInvalidIndex, webStateIndex);
if (isNewWebState) {
[self scrollTabToVisible:webStateIndex];
return;
}
if (!self.useTabStacking) {
if (webStateIndex == static_cast<int>([_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:webStateIndex];
CGRect scrollRect =
CGRectInset(tabView.frame, -_tabStripView.contentInset.right, 0);
if (tabView)
[_tabStripView scrollRectToVisible:scrollRect animated:YES];
}
}
}
- (void)updateScrollViewFrameForTabSwitcherButton {
CGRect tabFrame = _tabStripView.frame;
tabFrame.size.width = _view.bounds.size.width;
[_tabStripView setFrame:tabFrame];
}
#pragma mark - TabStripViewLayoutDelegate
// Creates TabViews for each Tab in the WebStateList and positions them in the
// correct location onscreen.
- (void)layoutTabStripSubviews {
const int tabCount =
static_cast<int>([_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 unstacked mode the space used to layout the tabs is not constrained and
// uses the whole scroll view content size width. In stacked mode the
// available space is constrained to the visible space.
CGFloat availableSpace = self.useTabStacking
? [self tabStripVisibleSpace]
: _tabStripView.contentSize.width;
// The array and model indexes of the selected tab.
int selectedListIndex = _webStateList->active_index();
NSUInteger selectedArrayIndex =
[self indexForWebStateListIndex:selectedListIndex];
// 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 = self.useTabStacking ? [_tabStripView contentOffset].x : 0;
// 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.
int currentListIndex = [self webStateListIndexForIndex:arrayIndex];
if (currentListIndex == WebStateList::kInvalidIndex)
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(currentListIndex, [self maxNumCollapsedTabs]);
if (currentListIndex > selectedListIndex) {
// 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(selectedListIndex, [self maxNumCollapsedTabs]) +
std::min(currentListIndex - selectedListIndex,
[self maxNumCollapsedTabs]);
}
CGFloat realMinX =
offset + (numPossibleCollapsedTabsToLeft * kCollapsedTabOverlap);
// |realMaxX| is the furthest right the tab can be, in real coordinates.
int numPossibleCollapsedTabsToRight =
std::min(tabCount - currentListIndex - 1, [self maxNumCollapsedTabs]);
if (currentListIndex < selectedListIndex) {
// 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 - selectedListIndex - 1,
[self maxNumCollapsedTabs]) +
std::min(selectedListIndex - currentListIndex,
[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.
_placeholderGapWebStateListIndex = currentListIndex;
if ([self webStateListIndexForTabView:_draggedTab] < currentListIndex)
_placeholderGapWebStateListIndex--;
}
// |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
// _placeholderGapWebStateListIndex accordingly.
if (!hasPlaceholderGap && _isReordering)
_placeholderGapWebStateListIndex = _webStateList->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.presentationProvider isTabStripFullyVisible]) {
// Move the toolbar to visible and wait for the end of that animation to
// animate the appearance of the new tab.
delay = self.animationWaitDuration;
// 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)setNeedsLayoutWithAnimation {
_animateLayout = YES;
[_tabStripView setNeedsLayout];
}
#pragma mark - TabViewDelegate
// Called when the TabView was tapped.
- (void)tabViewTapped:(TabView*)tabView {
// Ignore taps while in reordering mode.
if ([self isReorderingTabs])
return;
int index = [self webStateListIndexForTabView:tabView];
DCHECK_NE(WebStateList::kInvalidIndex, index);
if (index == WebStateList::kInvalidIndex)
return;
if (IsIPadIdiom() &&
(_webStateList->active_index() != static_cast<int>(index))) {
SnapshotTabHelper::FromWebState(_webStateList->GetActiveWebState())
->UpdateSnapshotWithCallback(nil);
}
_webStateList->ActivateWebStateAt(static_cast<int>(index));
[self updateContentOffsetForWebStateIndex:index isNewWebState:NO];
}
// Called when the TabView's close button was tapped.
- (void)tabViewCloseButtonPressed:(TabView*)tabView {
// Ignore taps while in reordering mode.
// TODO(crbug.com/754287): We should just hide the close buttons instead.
if ([self isReorderingTabs])
return;
base::RecordAction(UserMetricsAction("MobileTabStripCloseTab"));
int webStateListIndex = [self webStateListIndexForTabView:tabView];
if (webStateListIndex != WebStateList::kInvalidIndex)
_webStateList->CloseWebStateAt(webStateListIndex,
WebStateList::CLOSE_USER_ACTION);
}
- (void)tabView:(TabView*)tabView receivedDroppedURL:(GURL)url {
int index = [self webStateListIndexForTabView:tabView];
DCHECK_NE(WebStateList::kInvalidIndex, index);
if (index == WebStateList::kInvalidIndex)
return;
web::WebState* webState = _webStateList->GetWebStateAt(index);
web::NavigationManager::WebLoadParams params(url);
params.transition_type = ui::PAGE_TRANSITION_GENERATED;
webState->GetNavigationManager()->LoadURLWithParams(params);
}
#pragma mark - Tab Stacking
- (BOOL)shouldUseTabStacking {
BOOL useTabStacking = !IsIPadIdiom() || !IsCompactWidth(self.view);
if (base::FeatureList::IsEnabled(kVoiceOverUnstackedTabstrip) &&
UIAccessibilityIsVoiceOverRunning()) {
useTabStacking = NO;
}
if (base::FeatureList::IsEnabled(kForceUnstackedTabstrip)) {
useTabStacking = NO;
}
return useTabStacking;
}
- (void)setUseTabStacking:(BOOL)useTabStacking {
if (_useTabStacking == useTabStacking) {
return;
}
_useTabStacking = useTabStacking;
[self updateScrollViewFrameForTabSwitcherButton];
[self updateContentSizeAndRepositionViews];
int selectedModelIndex = _webStateList->active_index();
if (selectedModelIndex != WebStateList::kInvalidIndex) {
[self updateContentOffsetForWebStateIndex:selectedModelIndex
isNewWebState:NO];
}
}
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
self.useTabStacking = [self shouldUseTabStacking];
}
- (void)voiceOverStatusDidChange {
self.useTabStacking = [self shouldUseTabStacking];
}
#pragma mark - ViewRevealingAnimatee
- (void)willAnimateViewReveal:(ViewRevealState)currentViewRevealState {
// Specifically when using the FullscreenProvider, the background of the view
// is non-clear to cover the WKWebView. In this case, make the tab strip
// background clear as soon as view revealing begins so any animations that
// should be visible behind the tab strip are visible. See the comment on
// |BackgroundColor()| for more details.
self.view.backgroundColor = UIColor.clearColor;
}
- (void)animateViewReveal:(ViewRevealState)nextViewRevealState {
// No-op.
}
- (void)didAnimateViewReveal:(ViewRevealState)viewRevealState {
if (viewRevealState == ViewRevealState::Hidden) {
// Reset the background color to cover up the WKWebView if it is behind
// the tab strip.
self.view.backgroundColor = BackgroundColor();
}
}
@end