blob: 022e3524842242fc9154ca9108c8a935b67c3afe [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// 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/tab_switcher/tab_grid/tab_grid_view_controller.h"
#import <objc/runtime.h>
#import "base/functional/bind.h"
#import "base/ios/ios_util.h"
#import "base/logging.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/histogram_macros.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/browser/crash_report/crash_keys_helper.h"
#import "ios/chrome/browser/default_browser/utils.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/popup_menu_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/table_view/chrome_table_view_styler.h"
#import "ios/chrome/browser/shared/ui/util/layout_guide_names.h"
#import "ios/chrome/browser/shared/ui/util/rtl_geometry.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/shared/ui/util/util_swift.h"
#import "ios/chrome/browser/tabs/features.h"
#import "ios/chrome/browser/tabs/inactive_tabs/features.h"
#import "ios/chrome/browser/ui/gestures/view_controller_trait_collection_observer.h"
#import "ios/chrome/browser/ui/gestures/view_revealing_vertical_pan_handler.h"
#import "ios/chrome/browser/ui/keyboard/UIKeyCommand+Chrome.h"
#import "ios/chrome/browser/ui/menu/action_factory.h"
#import "ios/chrome/browser/ui/recent_tabs/recent_tabs_table_view_controller.h"
#import "ios/chrome/browser/ui/recent_tabs/recent_tabs_table_view_controller_ui_delegate.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_collection_consumer.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_collection_drag_drop_handler.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/disabled_tab_view_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_commands.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_constants.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_view_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/pinned_tabs/pinned_tabs_constants.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/pinned_tabs/pinned_tabs_view_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/suggested_actions/suggested_actions_delegate.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_collection_commands.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_context_menu/tab_context_menu_provider.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_bottom_toolbar.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_constants.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_empty_state_view.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_new_tab_button.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_page_control.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_top_toolbar.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/thumb_strip_plus_sign_button.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/transitions/grid_transition_layout.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/thread/web_task_traits.h"
#import "ios/web/public/thread/web_thread.h"
#import "ui/base/l10n/l10n_util.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Not selected tabs opacity in thumbstrip.
const CGFloat kNotSelectedTabsOpacity = 0.8f;
// Types of configurations of this view controller.
typedef NS_ENUM(NSUInteger, TabGridConfiguration) {
TabGridConfigurationBottomToolbar = 1,
TabGridConfigurationFloatingButton,
};
// Key of the UMA IOS.TabSwitcher.PageChangeInteraction histogram.
const char kUMATabSwitcherPageChangeInteractionHistogram[] =
"IOS.TabSwitcher.PageChangeInteraction";
// Values of the UMA IOS.TabSwitcher.PageChangeInteraction histogram.
enum class TabSwitcherPageChangeInteraction {
kNone = 0,
kScrollDrag = 1,
kControlTap = 2,
kControlDrag = 3,
kItemDrag = 4,
kMaxValue = kItemDrag,
};
// Computes the page from the offset and width of `scrollView`.
TabGridPage GetPageFromScrollView(UIScrollView* scrollView) {
CGFloat pageWidth = scrollView.frame.size.width;
CGFloat offset = scrollView.contentOffset.x;
NSUInteger page = lround(offset / pageWidth);
// Fence `page` to valid values; page values of 3 (rounded up from 2.5) are
// possible, as are large int values if `pageWidth` is somehow very small.
page = page < TabGridPageIncognitoTabs ? TabGridPageIncognitoTabs : page;
page = page > TabGridPageRemoteTabs ? TabGridPageRemoteTabs : page;
if (UseRTLLayout()) {
// In RTL, page indexes are inverted, so subtract `page` from the highest-
// index TabGridPage value.
return static_cast<TabGridPage>(TabGridPageRemoteTabs - page);
}
return static_cast<TabGridPage>(page);
}
NSUInteger GetPageIndexFromPage(TabGridPage page) {
if (UseRTLLayout()) {
// In RTL, page indexes are inverted, so subtract `page` from the highest-
// index TabGridPage value.
return static_cast<NSUInteger>(TabGridPageRemoteTabs - page);
}
return static_cast<NSUInteger>(page);
}
} // namespace
@interface TabGridViewController () <DisabledTabViewControllerDelegate,
GridViewControllerDelegate,
LayoutSwitcher,
PinnedTabsViewControllerDelegate,
RecentTabsTableViewControllerUIDelegate,
SuggestedActionsDelegate,
UIGestureRecognizerDelegate,
UIScrollViewAccessibilityDelegate,
UISearchBarDelegate>
// Whether the view is visible. Bookkeeping is based on
// `-contentWillAppearAnimated:` and
// `-contentWillDisappearAnimated methods. Note that the `Did` methods are not
// reliably called (e.g., edge case in multitasking).
@property(nonatomic, assign) BOOL viewVisible;
// Child view controllers.
@property(nonatomic, strong) GridViewController* regularTabsViewController;
@property(nonatomic, strong) GridViewController* incognitoTabsViewController;
@property(nonatomic, strong) PinnedTabsViewController* pinnedTabsViewController;
// Disabled tab view controllers shown when a certain browser mode is disabled.
@property(nonatomic, strong)
DisabledTabViewController* incognitoDisabledTabViewController;
@property(nonatomic, strong)
DisabledTabViewController* regularDisabledTabViewController;
@property(nonatomic, strong)
DisabledTabViewController* recentDisabledTabViewController;
// Array holding the child page view controllers.
@property(nonatomic, strong) NSArray<UIViewController*>* pageViewControllers;
// Other UI components.
@property(nonatomic, weak) UIScrollView* scrollView;
@property(nonatomic, weak) UIView* scrollContentView;
// Scrim view to be presented when the search box in focused with no text.
@property(nonatomic, strong) UIControl* scrimView;
@property(nonatomic, weak) TabGridTopToolbar* topToolbar;
@property(nonatomic, weak) TabGridBottomToolbar* bottomToolbar;
@property(nonatomic, assign) TabGridConfiguration configuration;
// Setting the current page doesn't scroll the scroll view; use
// -scrollToPage:animated: for that.
@property(nonatomic, assign) TabGridPage currentPage;
// The UIViewController corresponding with `currentPage`.
@property(nonatomic, readonly) UIViewController* currentPageViewController;
// The frame of `self.view` when it initially appeared.
@property(nonatomic, assign) CGRect initialFrame;
// Whether the scroll view is animating its content offset to the current page.
@property(nonatomic, assign, getter=isScrollViewAnimatingContentOffset)
BOOL scrollViewAnimatingContentOffset;
// UIView whose background color changes to create a fade-in / fade-out effect
// when revealing / hiding the Thumb Strip.
@property(nonatomic, weak) UIView* foregroundView;
// Button with a plus sign that opens a new tab, located on the right side of
// the thumb strip, shown when the plus sign cell isn't visible.
@property(nonatomic, weak) ThumbStripPlusSignButton* plusSignButton;
// Bottom constraint for `plusSignButton`.
@property(nonatomic, weak) NSLayoutConstraint* plusSignButtonBottomConstraint;
// Constraints for the pinned tabs view.
@property(nonatomic, strong)
NSArray<NSLayoutConstraint*>* pinnedTabsConstraints;
// Bottom constraint for the regular tabs bottom message view.
@property(nonatomic, strong)
NSArray<NSLayoutConstraint*>* regularTabsBottomMessageConstraints;
// The current state of the tab grid when using the thumb strip.
@property(nonatomic, assign) ViewRevealState currentState;
// The configuration for tab grid pages.
@property(nonatomic, assign) TabGridPageConfiguration pageConfiguration;
// If the scrim view is being presented.
@property(nonatomic, assign) BOOL isScrimDisplayed;
// Wether there is a search being performed in the tab grid or not.
@property(nonatomic, assign) BOOL isPerformingSearch;
// Pan gesture for when the search results view is scrolled during the search
// mode.
@property(nonatomic, strong) UIPanGestureRecognizer* searchResultPanRecognizer;
@property(nonatomic, assign, getter=isDragSeesionInProgress)
BOOL dragSeesionInProgress;
// YES if it is possible to undo the close all conditions.
@property(nonatomic, assign) BOOL undoCloseAllAvailable;
// The timestamp of the user entering the tab grid.
@property(nonatomic, assign) base::TimeTicks tabGridEnterTime;
@end
@implementation TabGridViewController {
// Idle page status.
// Tracks whether the user closed the tab switcher without doing any
// meaningful action.
BOOL _idleRegularTabGrid;
BOOL _idleIncognitoTabGrid;
BOOL _idleRecentTabs;
TabGridPage _activePageWhenAppear;
}
// TabGridPaging property.
@synthesize activePage = _activePage;
@synthesize tabGridMode = _tabGridMode;
- (instancetype)initWithPageConfiguration:
(TabGridPageConfiguration)tabGridPageConfiguration {
self = [super initWithNibName:nil bundle:nil];
if (self) {
_pageConfiguration = tabGridPageConfiguration;
_dragSeesionInProgress = NO;
switch (_pageConfiguration) {
case TabGridPageConfiguration::kAllPagesEnabled:
_incognitoTabsViewController = [[GridViewController alloc] init];
_regularTabsViewController = [[GridViewController alloc] init];
_remoteTabsViewController =
[[RecentTabsTableViewController alloc] init];
_pageViewControllers = @[
_incognitoTabsViewController, _regularTabsViewController,
_remoteTabsViewController
];
break;
case TabGridPageConfiguration::kIncognitoPageDisabled:
_incognitoDisabledTabViewController = [[DisabledTabViewController alloc]
initWithPage:TabGridPageIncognitoTabs];
_regularTabsViewController = [[GridViewController alloc] init];
_remoteTabsViewController =
[[RecentTabsTableViewController alloc] init];
_pageViewControllers = @[
_incognitoDisabledTabViewController, _regularTabsViewController,
_remoteTabsViewController
];
break;
case TabGridPageConfiguration::kIncognitoPageOnly:
_incognitoTabsViewController = [[GridViewController alloc] init];
_regularDisabledTabViewController = [[DisabledTabViewController alloc]
initWithPage:TabGridPageRegularTabs];
_recentDisabledTabViewController = [[DisabledTabViewController alloc]
initWithPage:TabGridPageRemoteTabs];
_pageViewControllers = @[
_incognitoTabsViewController, _regularDisabledTabViewController,
_recentDisabledTabViewController
];
break;
}
if (IsPinnedTabsEnabled()) {
_pinnedTabsViewController = [[PinnedTabsViewController alloc] init];
}
}
return self;
}
#pragma mark - UIViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor colorNamed:kGridBackgroundColor];
[self setupScrollView];
switch (_pageConfiguration) {
case TabGridPageConfiguration::kAllPagesEnabled:
[self setupIncognitoTabsViewController];
[self setupRegularTabsViewController];
[self setupRemoteTabsViewController];
break;
case TabGridPageConfiguration::kIncognitoPageDisabled:
[self setupDisabledTabViewForPageType:TabGridPageIncognitoTabs];
[self setupRegularTabsViewController];
[self setupRemoteTabsViewController];
break;
case TabGridPageConfiguration::kIncognitoPageOnly:
[self setupIncognitoTabsViewController];
[self setupDisabledTabViewForPageType:TabGridPageRegularTabs];
[self setupDisabledTabViewForPageType:TabGridPageRemoteTabs];
break;
}
[self setupSearchUI];
[self setupTopToolbar];
[self setupBottomToolbar];
[self setupEditButton];
if (IsPinnedTabsEnabled()) {
[self setupPinnedTabsViewController];
}
// Hide the toolbars and the floating button, so they can fade in the first
// time there's a transition into this view controller.
[self hideToolbars];
// Mark the non-current page view controllers' contents as hidden for
// VoiceOver.
for (UIViewController* pageViewController in self.pageViewControllers) {
if (pageViewController != self.currentPageViewController) {
pageViewController.view.accessibilityElementsHidden = YES;
}
}
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
// Modify Incognito and Regular Tabs Insets.
[self setInsetForGridViews];
}
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:
(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
__weak TabGridViewController* weakSelf = self;
auto animate = ^(id<UIViewControllerTransitionCoordinatorContext> context) {
[weakSelf animateTransition:context];
};
[coordinator animateAlongsideTransition:animate completion:nil];
}
- (void)animateTransition:
(id<UIViewControllerTransitionCoordinatorContext>)context {
// Sync the scroll view offset to the current page value. Since this is
// invoked inside an animation block, the scrolling doesn't need to be
// animated.
[self scrollToPage:_currentPage animated:NO];
[self configureViewControllerForCurrentSizeClassesAndPage];
[self setInsetForRemoteTabs];
[self setInsetForGridViews];
}
- (UIStatusBarStyle)preferredStatusBarStyle {
return UIStatusBarStyleLightContent;
}
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
[self.traitCollectionObserver viewController:self
traitCollectionDidChange:previousTraitCollection];
if (IsPinnedTabsEnabled()) {
[self updatePinnedTabsViewControllerConstraints];
}
[self updateRegularTabsBottomMessageConstraintsIfExists];
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
if (scrollView.dragging || scrollView.decelerating) {
// Only when user initiates scroll through dragging.
CGFloat offsetWidth =
self.scrollView.contentSize.width - self.scrollView.frame.size.width;
CGFloat offset = scrollView.contentOffset.x / offsetWidth;
// In RTL, flip the offset.
if (UseRTLLayout())
offset = 1.0 - offset;
self.topToolbar.pageControl.sliderPosition = offset;
TabGridPage page = GetPageFromScrollView(scrollView);
if (page != self.currentPage) {
self.currentPage = page;
[self broadcastIncognitoContentVisibility];
[self configureButtonsForActiveAndCurrentPage];
// Records when the user drags the scrollView to switch pages.
[self recordActionSwitchingToPage:_currentPage
withInteration:TabSwitcherPageChangeInteraction::
kScrollDrag];
}
}
}
- (void)scrollViewWillBeginDragging:(UIScrollView*)scrollView {
// Disable the page control when the user drags on the scroll view since
// tapping on the page control during scrolling can result in erratic
// scrolling.
self.topToolbar.pageControl.userInteractionEnabled = NO;
}
- (void)scrollViewDidEndDragging:(UIScrollView*)scrollView
willDecelerate:(BOOL)decelerate {
// Re-enable the page control since the user isn't dragging anymore.
self.topToolbar.pageControl.userInteractionEnabled = YES;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView*)scrollView {
// Update currentPage if scroll view has moved to a new page. Especially
// important here for 3-finger accessibility swipes since it's not registered
// as dragging in scrollViewDidScroll:
TabGridPage page = GetPageFromScrollView(scrollView);
if (page != self.currentPage) {
self.currentPage = page;
[self broadcastIncognitoContentVisibility];
[self configureButtonsForActiveAndCurrentPage];
}
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView*)scrollView {
TabGridPage currentPage = GetPageFromScrollView(scrollView);
if (currentPage != self.currentPage && self.isDragSeesionInProgress) {
// This happens when the user drags an item from one scroll view into
// another.
[self recordActionSwitchingToPage:currentPage
withInteration:TabSwitcherPageChangeInteraction::
kItemDrag];
[self.topToolbar.pageControl setSelectedPage:currentPage animated:YES];
}
self.currentPage = currentPage;
self.scrollViewAnimatingContentOffset = NO;
[self broadcastIncognitoContentVisibility];
[self configureButtonsForActiveAndCurrentPage];
}
#pragma mark - UIScrollViewAccessibilityDelegate
- (NSString*)accessibilityScrollStatusForScrollView:(UIScrollView*)scrollView {
// This reads the new page whenever the user scrolls in VoiceOver.
int stringID;
switch (self.currentPage) {
case TabGridPageIncognitoTabs:
stringID = IDS_IOS_TAB_GRID_INCOGNITO_TABS_TITLE;
break;
case TabGridPageRegularTabs:
stringID = IDS_IOS_TAB_GRID_REGULAR_TABS_TITLE;
break;
case TabGridPageRemoteTabs:
stringID = IDS_IOS_TAB_GRID_REMOTE_TABS_TITLE;
break;
}
return l10n_util::GetNSString(stringID);
}
#pragma mark - GridTransitionAnimationLayoutProviding properties
- (BOOL)isSelectedCellVisible {
if (self.activePage != self.currentPage) {
return NO;
}
return [self isSelectedCellVisibleForPage:self.activePage];
}
- (BOOL)shouldReparentSelectedCell:(GridAnimationDirection)animationDirection {
switch (animationDirection) {
// For contracting animation only selected pinned cells should be
// reparented.
case GridAnimationDirectionContracting:
return [self isPinnedCellSelected];
// For expanding animation any selected cell should be reparented.
case GridAnimationDirectionExpanding:
return YES;
}
}
- (GridTransitionLayout*)transitionLayout:(TabGridPage)activePage {
GridTransitionLayout* layout = [self transitionLayoutForPage:activePage];
if (!layout) {
return nil;
}
layout.frameChanged = !CGRectEqualToRect(self.view.frame, self.initialFrame);
return layout;
}
- (UIView*)animationViewsContainer {
return self.view;
}
- (UIView*)animationViewsContainerBottomView {
return self.scrollView;
}
#pragma mark - Public Methods
- (void)prepareForAppearance {
[[self gridViewControllerForPage:self.activePage] prepareForAppearance];
}
- (void)contentWillAppearAnimated:(BOOL)animated {
[self resetIdlePageStatus];
self.viewVisible = YES;
[self.topToolbar.pageControl setSelectedPage:self.currentPage animated:NO];
_activePageWhenAppear = self.currentPage;
[self configureViewControllerForCurrentSizeClassesAndPage];
// The toolbars should be hidden (alpha 0.0) before the tab appears, so that
// they can be animated in. They can't be set to 0.0 here, because if
// `animated` is YES, this method is being called inside the animation block.
if (animated && self.transitionCoordinator) {
[self animateToolbarsForAppearance];
} else {
[self showToolbars];
}
[self broadcastIncognitoContentVisibility];
[self.incognitoTabsViewController contentWillAppearAnimated:animated];
[self.regularTabsViewController contentWillAppearAnimated:animated];
[self.pinnedTabsViewController contentWillAppearAnimated:animated];
self.remoteTabsViewController.session = self.view.window.windowScene.session;
self.remoteTabsViewController.preventUpdates = NO;
// Record when the tab switcher is presented.
self.tabGridEnterTime = base::TimeTicks::Now();
}
- (void)contentDidAppear {
self.initialFrame = self.view.frame;
// Modify Remote Tabs Insets when page appears and during rotation.
if (self.remoteTabsViewController) {
[self setInsetForRemoteTabs];
}
// Let the active grid view know the initial appearance is done.
[[self gridViewControllerForPage:self.activePage] contentDidAppear];
}
- (void)contentWillDisappearAnimated:(BOOL)animated {
[self recordIdlePageStatus];
self.undoCloseAllAvailable = NO;
if (self.tabGridMode != TabGridModeSearch || !animated) {
// Updating the mode reset the items on the grid, in that case of search
// mode the animation to show the tab will start from the tab cell after the
// reset instead of starting from the cell that triggered the navigation.
self.tabGridMode = TabGridModeNormal;
}
[self.regularTabsDelegate discardSavedClosedItems];
[self.inactiveTabsDelegate discardSavedClosedItems];
// When the view disappears, the toolbar alpha should be set to 0; either as
// part of the animation, or directly with -hideToolbars.
if (animated && self.transitionCoordinator) {
[self animateToolbarsForDisappearance];
} else {
[self hideToolbars];
}
self.viewVisible = NO;
[self.incognitoTabsViewController contentWillDisappear];
[self.regularTabsViewController contentWillDisappear];
[self.pinnedTabsViewController contentWillDisappear];
self.remoteTabsViewController.preventUpdates = YES;
self.tabGridEnterTime = base::TimeTicks();
}
- (void)dismissModals {
[self.regularTabsConsumer dismissModals];
[self.pinnedTabsConsumer dismissModals];
[self.incognitoTabsConsumer dismissModals];
[self.remoteTabsViewController dismissModals];
}
- (void)setCurrentPageAndPageControl:(TabGridPage)page animated:(BOOL)animated {
[self updatePageWithCurrentSearchTerms:page];
if (self.topToolbar.pageControl.selectedPage != page)
[self.topToolbar.pageControl setSelectedPage:page animated:animated];
if (self.currentPage != page) {
[self scrollToPage:page animated:animated];
}
}
// Sets the current search terms on `page`. This allows the content to update
// while the page is still hidden before the page change animation begins.
- (void)updatePageWithCurrentSearchTerms:(TabGridPage)page {
if (self.tabGridMode != TabGridModeSearch ||
self.currentPage == TabGridPageIncognitoTabs) {
// No need to update search term if not in search mode or currently on the
// incognito page.
return;
}
NSString* searchTerms = nil;
if (self.currentPage == TabGridPageRegularTabs) {
searchTerms = self.regularTabsViewController.searchText;
} else {
searchTerms = self.remoteTabsViewController.searchTerms;
}
if (page == TabGridPageRegularTabs) {
// Search terms will be non-empty when switching pages. This is important
// because `searchItemsWithText:` will show items from all windows. When no
// search terms exist, `resetToAllItems` is used instead.
DCHECK(searchTerms.length);
self.regularTabsViewController.searchText = searchTerms;
[self.regularTabsDelegate searchItemsWithText:searchTerms];
} else {
self.remoteTabsViewController.searchTerms = searchTerms;
}
}
#pragma mark - Public Properties
- (id<TabCollectionConsumer>)regularTabsConsumer {
return self.regularTabsViewController;
}
- (void)setPriceCardDataSource:(id<PriceCardDataSource>)priceCardDataSource {
self.regularTabsViewController.priceCardDataSource = priceCardDataSource;
_priceCardDataSource = priceCardDataSource;
}
- (id<TabCollectionConsumer>)pinnedTabsConsumer {
return self.pinnedTabsViewController;
}
- (id<TabCollectionConsumer>)incognitoTabsConsumer {
return self.incognitoTabsViewController;
}
- (id<RecentTabsConsumer>)remoteTabsConsumer {
return self.remoteTabsViewController;
}
- (void)setRegularTabsShareableItemsProvider:
(id<GridShareableItemsProvider>)provider {
self.regularTabsViewController.shareableItemsProvider = provider;
_regularTabsShareableItemsProvider = provider;
}
- (void)setIncognitoTabsShareableItemsProvider:
(id<GridShareableItemsProvider>)provider {
self.incognitoTabsViewController.shareableItemsProvider = provider;
_incognitoTabsShareableItemsProvider = provider;
}
- (void)setReauthHandler:(id<IncognitoReauthCommands>)reauthHandler {
if (_reauthHandler == reauthHandler)
return;
_reauthHandler = reauthHandler;
self.incognitoTabsViewController.reauthHandler = self.reauthHandler;
}
- (void)setRegularThumbStripHandler:(id<ThumbStripCommands>)handler {
if (_regularThumbStripHandler == handler)
return;
_regularThumbStripHandler = handler;
self.regularTabsViewController.thumbStripHandler =
self.regularThumbStripHandler;
}
- (void)setIncognitoThumbStripHandler:(id<ThumbStripCommands>)handler {
if (_incognitoThumbStripHandler == handler)
return;
_incognitoThumbStripHandler = handler;
self.regularTabsViewController.thumbStripHandler =
self.incognitoThumbStripHandler;
}
- (void)setRegularTabsContextMenuProvider:(id<TabContextMenuProvider>)provider {
if (_regularTabsContextMenuProvider == provider)
return;
_regularTabsContextMenuProvider = provider;
self.regularTabsViewController.menuProvider = provider;
if (IsPinnedTabsEnabled()) {
self.pinnedTabsViewController.menuProvider = provider;
}
}
- (void)setIncognitoTabsContextMenuProvider:
(id<TabContextMenuProvider>)provider {
if (_incognitoTabsContextMenuProvider == provider)
return;
_incognitoTabsContextMenuProvider = provider;
self.incognitoTabsViewController.menuProvider = provider;
}
- (void)setReauthAgent:(IncognitoReauthSceneAgent*)reauthAgent {
if (_reauthAgent) {
[_reauthAgent removeObserver:self];
}
_reauthAgent = reauthAgent;
[_reauthAgent addObserver:self];
}
- (void)setRegularTabsBottomMessage:(UIViewController*)bottomMessage {
if (_regularTabsBottomMessage == bottomMessage) {
return;
}
[_regularTabsBottomMessage willMoveToParentViewController:nil];
[_regularTabsBottomMessage.view removeFromSuperview];
[_regularTabsBottomMessage removeFromParentViewController];
_regularTabsBottomMessage = bottomMessage;
if (!_regularTabsBottomMessage) {
[self slideOutRegularTabsBottomMessage];
return;
}
[self addChildViewController:self.regularTabsBottomMessage];
[self.view addSubview:self.regularTabsBottomMessage.view];
[self.regularTabsBottomMessage didMoveToParentViewController:self];
[self initializeRegularTabsBottomMessageView];
}
#pragma mark - TabGridPaging
- (void)setActivePage:(TabGridPage)activePage {
[self scrollToPage:activePage animated:YES];
_activePage = activePage;
}
#pragma mark - TabGridMode
- (void)setTabGridMode:(TabGridMode)mode {
if (_tabGridMode == mode) {
return;
}
TabGridMode previousMode = _tabGridMode;
_tabGridMode = mode;
// Updating toolbars first before the controllers so when they set their
// content they will account for the updated insets of the toolbars.
self.topToolbar.mode = self.tabGridMode;
self.bottomToolbar.mode = self.tabGridMode;
// Resetting search state when leaving the search mode should happen before
// changing the mode in the controllers so when they do the cleanup for the
// new mode they will have the correct items (tabs).
if (previousMode == TabGridModeSearch) {
self.remoteTabsViewController.searchTerms = nil;
self.regularTabsViewController.searchText = nil;
self.incognitoTabsViewController.searchText = nil;
[self.regularTabsDelegate resetToAllItems];
[self.incognitoTabsDelegate resetToAllItems];
[self hideScrim];
}
// Reset the visibility of bottom message, if exists.
if (self.regularTabsBottomMessage) {
self.regularTabsBottomMessage.view.hidden =
self.tabGridMode != TabGridModeNormal;
}
[self setInsetForGridViews];
self.regularTabsViewController.mode = self.tabGridMode;
self.incognitoTabsViewController.mode = self.tabGridMode;
self.scrollView.scrollEnabled = (self.tabGridMode == TabGridModeNormal);
if (mode == TabGridModeSelection)
[self updateSelectionModeToolbars];
}
#pragma mark - LayoutSwitcherProvider
- (id<LayoutSwitcher>)layoutSwitcher {
return self;
}
#pragma mark - LayoutSwitcher
- (LayoutSwitcherState)currentLayoutSwitcherState {
GridViewController* gridViewController =
[self gridViewControllerForPage:self.currentPage];
return gridViewController.currentLayoutSwitcherState;
}
- (void)willTransitionToLayout:(LayoutSwitcherState)nextState
completion:
(void (^)(BOOL completed, BOOL finished))completion {
GridViewController* regularViewController =
[self gridViewControllerForPage:TabGridPageRegularTabs];
GridViewController* incognitoViewController =
[self gridViewControllerForPage:TabGridPageIncognitoTabs];
__block NSMutableArray<NSNumber*>* completeds = [[NSMutableArray alloc] init];
__block NSMutableArray<NSNumber*>* finisheds = [[NSMutableArray alloc] init];
void (^combinedCompletion)(BOOL, BOOL) = ^(BOOL completed, BOOL finished) {
[completeds addObject:[NSNumber numberWithBool:completed]];
[finisheds addObject:[NSNumber numberWithBool:finished]];
if ([completeds count] != 2) {
return;
}
DCHECK(completeds[0] == completeds[1]);
DCHECK(finisheds[0] == finisheds[1]);
completion(completeds[0], finisheds[0]);
};
// Each LayoutSwitcher method calls regular and incognito grid controller's
// corresponding method. Thus, attaching the completion to only one of the
// grid view controllers should suffice.
[regularViewController willTransitionToLayout:nextState
completion:combinedCompletion];
[incognitoViewController willTransitionToLayout:nextState
completion:combinedCompletion];
}
- (void)didUpdateTransitionLayoutProgress:(CGFloat)progress {
GridViewController* regularViewController =
[self gridViewControllerForPage:TabGridPageRegularTabs];
[regularViewController didUpdateTransitionLayoutProgress:progress];
GridViewController* incognitoViewController =
[self gridViewControllerForPage:TabGridPageIncognitoTabs];
[incognitoViewController didUpdateTransitionLayoutProgress:progress];
}
- (void)didTransitionToLayoutSuccessfully:(BOOL)success {
GridViewController* regularViewController =
[self gridViewControllerForPage:TabGridPageRegularTabs];
[regularViewController didTransitionToLayoutSuccessfully:success];
GridViewController* incognitoViewController =
[self gridViewControllerForPage:TabGridPageIncognitoTabs];
[incognitoViewController didTransitionToLayoutSuccessfully:success];
}
#pragma mark - ViewRevealingAnimatee
- (void)willAnimateViewRevealFromState:(ViewRevealState)currentViewRevealState
toState:(ViewRevealState)nextViewRevealState {
self.currentState = currentViewRevealState;
self.scrollView.scrollEnabled = NO;
[self updateNotSelectedTabCellOpacityForState:currentViewRevealState];
// Reset tab grid mode.
self.tabGridMode = TabGridModeNormal;
switch (currentViewRevealState) {
case ViewRevealState::Hidden: {
// If the tab grid is just showing up, make sure that the active page is
// current. This can happen when the user closes the tab grid using the
// done button on RecentTabs. The current page would stay RecentTabs, but
// the active page comes from the currently displayed BVC.
if (self.delegate) {
self.activePage =
[self.delegate activePageForTabGridViewController:self];
}
if (self.currentPage != self.activePage) {
[self scrollToPage:self.activePage animated:NO];
}
self.topToolbar.transform = CGAffineTransformMakeTranslation(
0, [self hiddenTopToolbarYTranslation]);
GridViewController* regularViewController =
[self gridViewControllerForPage:TabGridPageRegularTabs];
regularViewController.gridView.transform =
CGAffineTransformMakeTranslation(0, kThumbStripSlideInHeight);
GridViewController* incognitoViewController =
[self gridViewControllerForPage:TabGridPageIncognitoTabs];
incognitoViewController.gridView.transform =
CGAffineTransformMakeTranslation(0, kThumbStripSlideInHeight);
// Don't do any animation in the tab grid. All that animation will be
// controlled by the pan handler/-animateViewReveal:.
[self contentWillAppearAnimated:NO];
break;
}
case ViewRevealState::Peeked:
break;
case ViewRevealState::Revealed:
self.plusSignButton.alpha = 0;
break;
}
switch (nextViewRevealState) {
case ViewRevealState::Hidden:
case ViewRevealState::Peeked:
self.plusSignButtonBottomConstraint.constant = kThumbStripHeight;
break;
case ViewRevealState::Revealed:
// Increase height of button while hiding it, for a smooth animation.
self.plusSignButtonBottomConstraint.constant =
self.view.frame.size.height;
break;
}
}
- (void)animateViewReveal:(ViewRevealState)nextViewRevealState {
[self updateNotSelectedTabCellOpacityForState:nextViewRevealState];
GridViewController* regularViewController =
[self gridViewControllerForPage:TabGridPageRegularTabs];
GridViewController* incognitoViewController =
[self gridViewControllerForPage:TabGridPageIncognitoTabs];
switch (nextViewRevealState) {
case ViewRevealState::Hidden: {
self.foregroundView.alpha = 1;
self.topToolbar.transform = CGAffineTransformMakeTranslation(
0, [self hiddenTopToolbarYTranslation]);
regularViewController.gridView.transform =
CGAffineTransformMakeTranslation(0, kThumbStripSlideInHeight);
incognitoViewController.gridView.transform =
CGAffineTransformMakeTranslation(0, kThumbStripSlideInHeight);
self.topToolbar.alpha = 0;
GridViewController* currentGridViewController =
[self gridViewControllerForPage:self.currentPage];
[self showPlusSignButtonWithAlpha:1 - currentGridViewController
.fractionVisibleOfLastItem];
[self contentWillDisappearAnimated:YES];
self.plusSignButton.transform =
CGAffineTransformMakeTranslation(0, kThumbStripSlideInHeight);
break;
}
case ViewRevealState::Peeked: {
self.foregroundView.alpha = 0;
self.topToolbar.transform = CGAffineTransformMakeTranslation(
0, [self hiddenTopToolbarYTranslation]);
regularViewController.gridView.transform = CGAffineTransformIdentity;
incognitoViewController.gridView.transform = CGAffineTransformIdentity;
self.topToolbar.alpha = 0;
GridViewController* currentGridViewController =
[self gridViewControllerForPage:self.currentPage];
[self showPlusSignButtonWithAlpha:1 - currentGridViewController
.fractionVisibleOfLastItem];
break;
}
case ViewRevealState::Revealed: {
self.foregroundView.alpha = 0;
self.topToolbar.transform = CGAffineTransformIdentity;
regularViewController.gridView.transform =
CGAffineTransformMakeTranslation(
0, self.topToolbar.intrinsicContentSize.height);
incognitoViewController.gridView.transform =
CGAffineTransformMakeTranslation(
0, self.topToolbar.intrinsicContentSize.height);
self.topToolbar.alpha = 1;
[self hidePlusSignButton];
break;
}
}
}
- (void)didAnimateViewRevealFromState:(ViewRevealState)startViewRevealState
toState:(ViewRevealState)currentViewRevealState
trigger:(ViewRevealTrigger)trigger {
[self updateNotSelectedTabCellOpacityForState:currentViewRevealState];
self.currentState = currentViewRevealState;
// Update a11y visibility for browser and grid. Both should be visible
// when in Peeked mode, and only one visible in the other two modes.
BOOL updateAccessibility = NO;
switch (currentViewRevealState) {
case ViewRevealState::Hidden:
[self.delegate tabGridViewControllerDidDismiss:self];
self.view.accessibilityViewIsModal = NO;
[self.delegate setBVCAccessibilityViewModal:YES];
updateAccessibility = YES;
break;
case ViewRevealState::Peeked:
self.view.accessibilityViewIsModal = NO;
[self.delegate setBVCAccessibilityViewModal:NO];
updateAccessibility = startViewRevealState == ViewRevealState::Hidden;
break;
case ViewRevealState::Revealed:
self.scrollView.scrollEnabled = YES;
[self setInsetForRemoteTabs];
[self.delegate dismissBVC];
self.view.accessibilityViewIsModal = YES;
[self.delegate setBVCAccessibilityViewModal:NO];
updateAccessibility = startViewRevealState == ViewRevealState::Hidden;
break;
}
if (updateAccessibility) {
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
nil);
}
}
// Sets the expected opacity level for each view revealing state.
- (void)updateNotSelectedTabCellOpacityForState:(ViewRevealState)state {
GridViewController* regularViewController =
[self gridViewControllerForPage:TabGridPageRegularTabs];
GridViewController* incognitoViewController =
[self gridViewControllerForPage:TabGridPageIncognitoTabs];
switch (state) {
case ViewRevealState::Hidden:
case ViewRevealState::Peeked:
regularViewController.notSelectedTabCellOpacity = kNotSelectedTabsOpacity;
incognitoViewController.notSelectedTabCellOpacity =
kNotSelectedTabsOpacity;
break;
case ViewRevealState::Revealed:
regularViewController.notSelectedTabCellOpacity = 1.0f;
incognitoViewController.notSelectedTabCellOpacity = 1.0f;
break;
}
}
#pragma mark - Private
// Records the idle page status for the current `currentPage`.
- (void)recordIdlePageStatus {
if (!self.viewVisible) {
return;
}
// If the page has changed, the idle status of tab grid pages is `NO`.
BOOL onSamePage = self.currentPage == _activePageWhenAppear;
switch (self.currentPage) {
case TabGridPage::TabGridPageIncognitoTabs:
base::UmaHistogramBoolean(
kUMATabSwitcherIdleIncognitoTabGridPageHistogram,
_idleIncognitoTabGrid && onSamePage);
break;
case TabGridPage::TabGridPageRegularTabs:
base::UmaHistogramBoolean(kUMATabSwitcherIdleRegularTabGridPageHistogram,
_idleRegularTabGrid && onSamePage);
break;
case TabGridPage::TabGridPageRemoteTabs:
base::UmaHistogramBoolean(kUMATabSwitcherIdleRecentTabsHistogram,
_idleRecentTabs);
break;
}
}
// Sets the idle page status of the `currentPage`.
- (void)setCurrentIdlePageStatus:(BOOL)idlePageStatus {
if (!self.viewVisible) {
return;
}
switch (self.currentPage) {
case TabGridPage::TabGridPageIncognitoTabs:
_idleIncognitoTabGrid = idlePageStatus;
break;
case TabGridPage::TabGridPageRegularTabs:
_idleRegularTabGrid = idlePageStatus;
break;
case TabGridPage::TabGridPageRemoteTabs:
_idleRecentTabs = idlePageStatus;
break;
}
}
// Resets idle page status.
- (void)resetIdlePageStatus {
_idleIncognitoTabGrid = YES;
_idleRegularTabGrid = YES;
// `_idleRecentTabs` is set to 'YES' if the "Done" button has been tapped from
// the "TabGridPageRemoteTabs" or if the page has changed.
_idleRecentTabs = NO;
}
// Returns wether there is a selected pinned cell.
- (BOOL)isPinnedCellSelected {
if (!IsPinnedTabsEnabled() || self.currentPage != TabGridPageRegularTabs) {
return NO;
}
return [self.pinnedTabsViewController hasSelectedCell];
}
// Returns whether selcted cell is visible for the provided `page`.
- (BOOL)isSelectedCellVisibleForPage:(TabGridPage)page {
switch (page) {
case TabGridPageIncognitoTabs:
return self.incognitoTabsViewController.selectedCellVisible;
case TabGridPageRegularTabs:
return [self isSelectedCellVisibleForRegularTabsPage];
case TabGridPageRemoteTabs:
return NO;
}
}
// Returns whether selcted cell is visible for the regular tabs `page`.
- (BOOL)isSelectedCellVisibleForRegularTabsPage {
BOOL isSelectedCellVisible =
self.regularTabsViewController.selectedCellVisible;
if (IsPinnedTabsEnabled()) {
isSelectedCellVisible |= self.pinnedTabsViewController.selectedCellVisible;
}
return isSelectedCellVisible;
}
// Returns transition layout for the provided `page`.
- (GridTransitionLayout*)transitionLayoutForPage:(TabGridPage)page {
switch (page) {
case TabGridPageIncognitoTabs:
return [self.incognitoTabsViewController transitionLayout];
case TabGridPageRegularTabs:
return [self transitionLayoutForRegularTabsPage];
case TabGridPageRemoteTabs:
return nil;
}
}
// Returns transition layout provider for the regular tabs page.
- (GridTransitionLayout*)transitionLayoutForRegularTabsPage {
GridTransitionLayout* regularTabsTransitionLayout =
[self.regularTabsViewController transitionLayout];
if (IsPinnedTabsEnabled()) {
GridTransitionLayout* pinnedTabsTransitionLayout =
[self.pinnedTabsViewController transitionLayout];
return [self combineTransitionLayout:regularTabsTransitionLayout
withTransitionLayout:pinnedTabsTransitionLayout];
}
return regularTabsTransitionLayout;
}
// Combines two transition layouts into one. The `primaryLayout` has the
// priority over `secondaryLayout`. This means that in case there are two
// activeItems and/or two selectionItems available, only the ones from
// `primaryLayout` would be picked for a combined layout.
- (GridTransitionLayout*)
combineTransitionLayout:(GridTransitionLayout*)primaryLayout
withTransitionLayout:(GridTransitionLayout*)secondaryLayout {
NSArray<GridTransitionItem*>* primaryInactiveItems =
primaryLayout.inactiveItems;
NSArray<GridTransitionItem*>* secondaryInactiveItems =
secondaryLayout.inactiveItems;
NSArray<GridTransitionItem*>* inactiveItems =
[self combineInactiveItems:primaryInactiveItems
withInactiveItems:secondaryInactiveItems];
GridTransitionActiveItem* primaryActiveItem = primaryLayout.activeItem;
GridTransitionActiveItem* secondaryActiveItem = secondaryLayout.activeItem;
// Prefer primary active item.
GridTransitionActiveItem* activeItem =
primaryActiveItem ? primaryActiveItem : secondaryActiveItem;
GridTransitionItem* primarySelectionItem = primaryLayout.selectionItem;
GridTransitionItem* secondarySelectionItem = secondaryLayout.selectionItem;
// Prefer primary selection item.
GridTransitionItem* selectionItem =
primarySelectionItem ? primarySelectionItem : secondarySelectionItem;
return [GridTransitionLayout layoutWithInactiveItems:inactiveItems
activeItem:activeItem
selectionItem:selectionItem];
}
// Combines two arrays of inactive items into one. The `primaryInactiveItems`
// (if any) would be placed in the front of the resulting array, whether the
// `secondaryInactiveItems` would be placed in the back.
- (NSArray<GridTransitionItem*>*)
combineInactiveItems:(NSArray<GridTransitionItem*>*)primaryInactiveItems
withInactiveItems:(NSArray<GridTransitionItem*>*)secondaryInactiveItems {
if (primaryInactiveItems == nil) {
primaryInactiveItems = @[];
}
return [primaryInactiveItems
arrayByAddingObjectsFromArray:secondaryInactiveItems];
}
// Hides the thumb strip's plus sign button by translating it away and making it
// transparent.
- (void)hidePlusSignButton {
CGFloat xDistance = UseRTLLayout()
? -kThumbStripPlusSignButtonSlideOutDistance
: kThumbStripPlusSignButtonSlideOutDistance;
self.plusSignButton.transform =
CGAffineTransformMakeTranslation(xDistance, 0);
self.plusSignButton.alpha = 0;
}
// Show the thumb strip's plus sign button by translating it back into position
// and setting its alpha to `opacity`.
- (void)showPlusSignButtonWithAlpha:(CGFloat)opacity {
self.plusSignButton.transform = CGAffineTransformIdentity;
self.plusSignButton.alpha = opacity;
}
// Returns the ammount by which the top toolbar should be translated in the y
// direction when hidden. Used for the slide-in animation.
- (CGFloat)hiddenTopToolbarYTranslation {
return -self.topToolbar.frame.size.height -
self.scrollView.safeAreaInsets.top;
}
// Sets the proper insets for the Remote Tabs ViewController to accomodate for
// the safe area, toolbar, and status bar.
- (void)setInsetForRemoteTabs {
// Sync the scroll view offset to the current page value if the scroll view
// isn't scrolling. Don't animate this.
if (!self.scrollView.dragging && !self.scrollView.decelerating) {
[self scrollToPage:self.currentPage animated:NO];
}
// The content inset of the tab grids must be modified so that the toolbars
// do not obscure the tabs. This may change depending on orientation.
CGFloat bottomInset = self.bottomToolbar.intrinsicContentSize.height;
UIEdgeInsets inset = UIEdgeInsetsMake(
self.topToolbar.intrinsicContentSize.height, 0, bottomInset, 0);
// Left and right side could be missing correct safe area
// inset upon rotation. Manually correct it.
self.remoteTabsViewController.additionalSafeAreaInsets = UIEdgeInsetsZero;
UIEdgeInsets additionalSafeArea = inset;
UIEdgeInsets safeArea = self.scrollView.safeAreaInsets;
// If Remote Tabs isn't on the screen, it will not have the right safe area
// insets. Pass down the safe area insets of the scroll view.
if (self.currentPage != TabGridPageRemoteTabs) {
additionalSafeArea.right = safeArea.right;
additionalSafeArea.left = safeArea.left;
}
// Ensure that the View Controller doesn't have safe area inset that already
// covers the view's bounds. This can happen in tests.
if (!CGRectIsEmpty(UIEdgeInsetsInsetRect(
self.remoteTabsViewController.tableView.bounds,
self.remoteTabsViewController.tableView.safeAreaInsets))) {
self.remoteTabsViewController.additionalSafeAreaInsets = additionalSafeArea;
}
}
// Sets the proper insets for the Grid ViewControllers to accomodate for the
// safe area and toolbars.
- (void)setInsetForGridViews {
// Sync the scroll view offset to the current page value if the scroll view
// isn't scrolling. Don't animate this.
if (!self.scrollViewAnimatingContentOffset && !self.scrollView.dragging &&
!self.scrollView.decelerating) {
[self scrollToPage:self.currentPage animated:NO];
}
self.incognitoTabsViewController.gridView.contentInset =
[self calculateInsetForIncognitoGridView];
self.regularTabsViewController.gridView.contentInset =
[self calculateInsetForRegularGridView];
}
// Returns the corresponding GridViewController for `page`. Returns `nil` if
// page does not have a corresponding GridViewController.
- (GridViewController*)gridViewControllerForPage:(TabGridPage)page {
switch (page) {
case TabGridPageIncognitoTabs:
return self.incognitoTabsViewController;
case TabGridPageRegularTabs:
return self.regularTabsViewController;
case TabGridPageRemoteTabs:
return nil;
}
}
- (void)setCurrentPage:(TabGridPage)currentPage {
BOOL samePage = _currentPage == currentPage;
// Record the idle metric if the previous page was `TabGridPageRemoteTabs`.
if (!samePage && _currentPage == TabGridPageRemoteTabs) {
[self setCurrentIdlePageStatus:YES];
[self recordIdlePageStatus];
[self setCurrentIdlePageStatus:NO];
}
// Original current page is about to not be visible. Disable it from being
// focused by VoiceOver.
self.currentPageViewController.view.accessibilityElementsHidden = YES;
UIViewController* previousPageVC = self.currentPageViewController;
_currentPage = currentPage;
self.currentPageViewController.view.accessibilityElementsHidden = NO;
if (self.tabGridMode == TabGridModeSearch) {
// `UIAccessibilityLayoutChangedNotification` doesn't change the current
// item focused by the voiceOver if the notification argument provided with
// it is `nil`. In search mode, the item focused by the voiceOver needs to
// be reset and to do that `UIAccessibilityScreenChangedNotification` should
// be posted instead.
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
nil);
// If the search mode is active. the previous page should have the result
// gesture recognizer installed, make sure to move the gesture recognizer to
// the new page's view.
[previousPageVC.view
removeGestureRecognizer:self.searchResultPanRecognizer];
[self.currentPageViewController.view
addGestureRecognizer:self.searchResultPanRecognizer];
} else {
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification,
nil);
}
if (IsPinnedTabsEnabled()) {
const BOOL pinnedTabsAvailable =
currentPage == TabGridPage::TabGridPageRegularTabs &&
self.tabGridMode == TabGridModeNormal;
[self.pinnedTabsViewController pinnedTabsAvailable:pinnedTabsAvailable];
}
[self updateToolbarsAppearance];
[self setupEditButton];
// Make sure the current page becomes the first responder, so that it can
// register and handle key commands.
[self.currentPageViewController becomeFirstResponder];
}
// Sets the value of `currentPage`, adjusting the position of the scroll view
// to match. If `animated` is YES, the scroll view change may animate; if it is
// NO, it will never animate.
- (void)scrollToPage:(TabGridPage)targetPage animated:(BOOL)animated {
// This method should never early return if `targetPage` == `_currentPage`;
// the ivar may have been set before the scroll view could be updated. Calling
// this method should always update the scroll view's offset if possible.
// When VoiceOver is running, the animation can cause state to get out of
// sync. If the user swipes right during the animation, the VoiceOver cursor
// goes to the old page, instead of the new page. See crbug.com/978673 for
// more details.
if (UIAccessibilityIsVoiceOverRunning()) {
animated = NO;
}
// If the view isn't loaded yet, just do bookkeeping on `currentPage`.
if (!self.viewLoaded) {
self.currentPage = targetPage;
return;
}
CGFloat pageWidth = self.scrollView.frame.size.width;
NSUInteger pageIndex = GetPageIndexFromPage(targetPage);
CGPoint targetOffset = CGPointMake(pageIndex * pageWidth, 0);
BOOL changed = self.currentPage != targetPage;
BOOL scrolled =
!CGPointEqualToPoint(self.scrollView.contentOffset, targetOffset);
// If the view is visible and `animated` is YES, animate the change.
// Otherwise don't.
if (!self.viewVisible || !animated) {
[self.scrollView setContentOffset:targetOffset animated:NO];
self.currentPage = targetPage;
// Important updates (e.g., button configurations, incognito visibility) are
// made at the end of scrolling animations after `self.currentPage` is set.
// Since this codepath has no animations, updates must be called manually.
[self broadcastIncognitoContentVisibility];
[self configureButtonsForActiveAndCurrentPage];
} else {
// Only set `scrollViewAnimatingContentOffset` to YES if there's an actual
// change in the contentOffset, as `-scrollViewDidEndScrollingAnimation:` is
// never called if the animation does not occur.
if (scrolled) {
self.scrollViewAnimatingContentOffset = YES;
[self.scrollView setContentOffset:targetOffset animated:YES];
// `self.currentPage` is set in scrollViewDidEndScrollingAnimation:
} else {
self.currentPage = targetPage;
if (changed) {
// When there is no scrolling and the page changed, it can be due to
// the user dragging the slider and dropping it right on the spot.
// Something easy to reproduce with the two edges (incognito / recent
// tabs), but also possible with middle position (normal).
[self broadcastIncognitoContentVisibility];
[self configureButtonsForActiveAndCurrentPage];
}
}
}
// TODO(crbug.com/872303) : This is a workaround because TabRestoreService
// does not notify observers when entries are removed. When close all tabs
// removes entries, the remote tabs page in the tab grid are not updated. This
// ensures that the table is updated whenever scrolling to it.
if (targetPage == TabGridPageRemoteTabs && (changed || scrolled)) {
[self.remoteTabsViewController loadModel];
[self.remoteTabsViewController.tableView reloadData];
}
}
- (UIViewController*)currentPageViewController {
switch (self.currentPage) {
case TabGridPageIncognitoTabs:
return self.incognitoTabsViewController
? self.incognitoTabsViewController
: self.incognitoDisabledTabViewController;
case TabGridPageRegularTabs:
return self.regularTabsViewController
? self.regularTabsViewController
: self.regularDisabledTabViewController;
case TabGridPageRemoteTabs:
return self.remoteTabsViewController
? self.remoteTabsViewController
: self.recentDisabledTabViewController;
}
}
- (void)setScrollViewAnimatingContentOffset:
(BOOL)scrollViewAnimatingContentOffset {
if (_scrollViewAnimatingContentOffset == scrollViewAnimatingContentOffset)
return;
_scrollViewAnimatingContentOffset = scrollViewAnimatingContentOffset;
}
// Adds the scroll view and sets constraints.
- (void)setupScrollView {
UIScrollView* scrollView = [[UIScrollView alloc] init];
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
scrollView.scrollEnabled = YES;
scrollView.pagingEnabled = YES;
scrollView.delegate = self;
// Ensures that scroll view does not add additional margins based on safe
// areas.
scrollView.contentInsetAdjustmentBehavior =
UIScrollViewContentInsetAdjustmentNever;
UIView* contentView = [[UIView alloc] init];
contentView.translatesAutoresizingMaskIntoConstraints = NO;
[scrollView addSubview:contentView];
[self.view addSubview:scrollView];
self.scrollContentView = contentView;
self.scrollView = scrollView;
self.scrollView.scrollEnabled = !self.isThumbStripEnabled;
self.scrollView.accessibilityIdentifier = kTabGridScrollViewIdentifier;
NSArray* constraints = @[
[contentView.topAnchor constraintEqualToAnchor:scrollView.topAnchor],
[contentView.bottomAnchor constraintEqualToAnchor:scrollView.bottomAnchor],
[contentView.leadingAnchor
constraintEqualToAnchor:scrollView.leadingAnchor],
[contentView.trailingAnchor
constraintEqualToAnchor:scrollView.trailingAnchor],
[contentView.heightAnchor constraintEqualToAnchor:self.view.heightAnchor],
[scrollView.topAnchor constraintEqualToAnchor:self.view.topAnchor],
[scrollView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
[scrollView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[scrollView.trailingAnchor
constraintEqualToAnchor:self.view.trailingAnchor],
];
[NSLayoutConstraint activateConstraints:constraints];
}
// Adds the incognito tabs GridViewController as a contained view controller,
// and sets constraints.
- (void)setupIncognitoTabsViewController {
UIView* contentView = self.scrollContentView;
GridViewController* viewController = self.incognitoTabsViewController;
viewController.view.translatesAutoresizingMaskIntoConstraints = NO;
viewController.view.accessibilityIdentifier = kIncognitoTabGridIdentifier;
[self addChildViewController:viewController];
[contentView addSubview:viewController.view];
[viewController didMoveToParentViewController:self];
viewController.emptyStateView =
[[TabGridEmptyStateView alloc] initWithPage:TabGridPageIncognitoTabs];
viewController.emptyStateView.accessibilityIdentifier =
kTabGridIncognitoTabsEmptyStateIdentifier;
viewController.theme = GridThemeDark;
viewController.delegate = self;
viewController.dragDropHandler = self.incognitoTabsDragDropHandler;
NSArray* constraints = @[
[viewController.view.topAnchor
constraintEqualToAnchor:contentView.topAnchor],
[viewController.view.bottomAnchor
constraintEqualToAnchor:contentView.bottomAnchor],
[viewController.view.leadingAnchor
constraintEqualToAnchor:contentView.leadingAnchor],
[viewController.view.widthAnchor
constraintEqualToAnchor:self.view.widthAnchor]
];
[NSLayoutConstraint activateConstraints:constraints];
}
// Adds the regular tabs GridViewController as a contained view controller,
// and sets constraints.
- (void)setupRegularTabsViewController {
UIView* contentView = self.scrollContentView;
GridViewController* viewController = self.regularTabsViewController;
viewController.view.translatesAutoresizingMaskIntoConstraints = NO;
viewController.view.accessibilityIdentifier = kRegularTabGridIdentifier;
[self addChildViewController:viewController];
[contentView addSubview:viewController.view];
[viewController didMoveToParentViewController:self];
viewController.emptyStateView =
[[TabGridEmptyStateView alloc] initWithPage:TabGridPageRegularTabs];
viewController.emptyStateView.accessibilityIdentifier =
kTabGridRegularTabsEmptyStateIdentifier;
viewController.theme = GridThemeLight;
viewController.delegate = self;
viewController.dragDropHandler = self.regularTabsDragDropHandler;
viewController.suggestedActionsDelegate = self;
UIViewController* leadingSideViewController =
self.incognitoTabsViewController
? self.incognitoTabsViewController
: self.incognitoDisabledTabViewController;
NSArray* constraints = @[
[viewController.view.topAnchor
constraintEqualToAnchor:contentView.topAnchor],
[viewController.view.bottomAnchor
constraintEqualToAnchor:contentView.bottomAnchor],
[viewController.view.leadingAnchor
constraintEqualToAnchor:leadingSideViewController.view.trailingAnchor],
[viewController.view.widthAnchor
constraintEqualToAnchor:self.view.widthAnchor]
];
[NSLayoutConstraint activateConstraints:constraints];
}
// Adds the remote tabs view controller as a contained view controller, and
// sets constraints.
- (void)setupRemoteTabsViewController {
RecentTabsTableViewController* viewController = self.remoteTabsViewController;
viewController.UIDelegate = self;
// TODO(crbug.com/804589) : Dark style on remote tabs.
// The styler must be set before the view controller is loaded.
ChromeTableViewStyler* styler = [[ChromeTableViewStyler alloc] init];
styler.tableViewBackgroundColor = [UIColor colorNamed:kGridBackgroundColor];
viewController.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
viewController.styler = styler;
UIView* contentView = self.scrollContentView;
viewController.view.translatesAutoresizingMaskIntoConstraints = NO;
[self addChildViewController:viewController];
[contentView addSubview:viewController.view];
[viewController didMoveToParentViewController:self];
NSArray* constraints = @[
[viewController.view.topAnchor
constraintEqualToAnchor:contentView.topAnchor],
[viewController.view.bottomAnchor
constraintEqualToAnchor:contentView.bottomAnchor],
[viewController.view.leadingAnchor
constraintEqualToAnchor:self.regularTabsViewController.view
.trailingAnchor],
[viewController.view.trailingAnchor
constraintEqualToAnchor:contentView.trailingAnchor],
[viewController.view.widthAnchor
constraintEqualToAnchor:self.view.widthAnchor],
];
[NSLayoutConstraint activateConstraints:constraints];
}
// Adds a DisabledTabViewController as a contained view controller, and sets
// constraints.
- (void)setupDisabledTabViewForPageType:(TabGridPage)pageType {
UIView* contentView = self.scrollContentView;
DisabledTabViewController* viewController;
NSLayoutConstraint* leadingAnchorConstraint;
switch (pageType) {
case TabGridPage::TabGridPageIncognitoTabs:
viewController = self.incognitoDisabledTabViewController;
leadingAnchorConstraint = [viewController.view.leadingAnchor
constraintEqualToAnchor:contentView.leadingAnchor];
break;
case TabGridPage::TabGridPageRegularTabs:
viewController = self.regularDisabledTabViewController;
leadingAnchorConstraint = [viewController.view.leadingAnchor
constraintEqualToAnchor:self.incognitoTabsViewController.view
.trailingAnchor];
break;
case TabGridPage::TabGridPageRemoteTabs:
viewController = self.recentDisabledTabViewController;
leadingAnchorConstraint = [viewController.view.leadingAnchor
constraintEqualToAnchor:self.regularDisabledTabViewController.view
.trailingAnchor];
break;
}
viewController.view.translatesAutoresizingMaskIntoConstraints = NO;
[self addChildViewController:viewController];
[contentView addSubview:viewController.view];
[viewController didMoveToParentViewController:self];
viewController.delegate = self;
NSArray* constraints = @[
[viewController.view.topAnchor
constraintEqualToAnchor:contentView.topAnchor],
[viewController.view.bottomAnchor
constraintEqualToAnchor:contentView.bottomAnchor],
leadingAnchorConstraint,
[viewController.view.widthAnchor
constraintEqualToAnchor:self.view.widthAnchor]
];
[NSLayoutConstraint activateConstraints:constraints];
if (pageType == TabGridPage::TabGridPageRemoteTabs) {
NSLayoutConstraint* trailingConstraint = [viewController.view.trailingAnchor
constraintEqualToAnchor:contentView.trailingAnchor];
trailingConstraint.active = YES;
}
}
- (void)setupEditButton API_AVAILABLE(ios(14.0)) {
ActionFactory* actionFactory = [[ActionFactory alloc]
initWithScenario:MenuScenarioHistogram::kTabGridEdit];
__weak TabGridViewController* weakSelf = self;
NSMutableArray<UIMenuElement*>* menuElements =
[@[ [actionFactory actionToCloseAllTabsWithBlock:^{
[weakSelf closeAllButtonTapped:nil];
}] ] mutableCopy];
// Disable the "Select All" option from the edit button when there is no tabs
// in the regular tab grid. "Close All" can still be called if there is
// element in inactive tabs.
BOOL disabledSelectAll = self.currentPage == TabGridPageRegularTabs &&
self.regularTabsViewController.isGridEmpty;
if (!disabledSelectAll) {
[menuElements addObject:[actionFactory actionToSelectTabsWithBlock:^{
[weakSelf selectTabsButtonTapped:nil];
}]];
}
UIMenu* menu = [UIMenu menuWithChildren:menuElements];
[self.topToolbar setEditButtonMenu:menu];
[self.bottomToolbar setEditButtonMenu:menu];
}
// Adds the top toolbar and sets constraints.
- (void)setupTopToolbar {
// In iOS 13+, constraints break if the UIToolbar is initialized with a null
// or zero rect frame. An arbitrary non-zero frame fixes this issue.
TabGridTopToolbar* topToolbar =
[[TabGridTopToolbar alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
self.topToolbar = topToolbar;
topToolbar.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:topToolbar];
// Sets the leadingButton title during initialization allows the actionSheet
// to be correctly anchored. See: crbug.com/1140982.
[topToolbar setCloseAllButtonTarget:self
action:@selector(closeAllButtonTapped:)];
[topToolbar setDoneButtonTarget:self action:@selector(doneButtonTapped:)];
[topToolbar setNewTabButtonTarget:self action:@selector(newTabButtonTapped:)];
[topToolbar setSelectAllButtonTarget:self
action:@selector(selectAllButtonTapped:)];
[topToolbar setSearchButtonTarget:self action:@selector(searchButtonTapped:)];
[topToolbar setCancelSearchButtonTarget:self
action:@selector(cancelSearchButtonTapped:)];
[topToolbar setSearchBarDelegate:self];
// Configure and initialize the page control.
[topToolbar.pageControl addTarget:self
action:@selector(pageControlChangedValue:)
forControlEvents:UIControlEventValueChanged];
[topToolbar.pageControl addTarget:self
action:@selector(pageControlChangedPageByDrag:)
forControlEvents:TabGridPageChangeByDragEvent];
[topToolbar.pageControl addTarget:self
action:@selector(pageControlChangedPageByTap:)
forControlEvents:TabGridPageChangeByTapEvent];
[NSLayoutConstraint activateConstraints:@[
[topToolbar.topAnchor
constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],
[topToolbar.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[topToolbar.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor]
]];
}
// Adds the bottom toolbar and sets constraints.
- (void)setupBottomToolbar {
TabGridBottomToolbar* bottomToolbar = [[TabGridBottomToolbar alloc] init];
self.bottomToolbar = bottomToolbar;
bottomToolbar.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:bottomToolbar];
[NSLayoutConstraint activateConstraints:@[
[bottomToolbar.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
[bottomToolbar.leadingAnchor
constraintEqualToAnchor:self.view.leadingAnchor],
[bottomToolbar.trailingAnchor
constraintEqualToAnchor:self.view.trailingAnchor],
]];
[bottomToolbar setCloseAllButtonTarget:self
action:@selector(closeAllButtonTapped:)];
[bottomToolbar setDoneButtonTarget:self action:@selector(doneButtonTapped:)];
[bottomToolbar setNewTabButtonTarget:self
action:@selector(newTabButtonTapped:)];
[bottomToolbar setCloseTabsButtonTarget:self
action:@selector(closeSelectedTabs:)];
[bottomToolbar setShareTabsButtonTarget:self
action:@selector(shareSelectedTabs:)];
[self.layoutGuideCenter referenceView:bottomToolbar
underName:kTabGridBottomToolbarGuide];
}
// Adds the foreground view and sets constraints.
- (void)setupForegroundView {
UIView* foregroundView = [[UIView alloc] init];
self.foregroundView = foregroundView;
foregroundView.translatesAutoresizingMaskIntoConstraints = NO;
foregroundView.userInteractionEnabled = NO;
foregroundView.backgroundColor = [UIColor colorNamed:kGridBackgroundColor];
[self.view insertSubview:foregroundView aboveSubview:self.plusSignButton];
AddSameConstraints(foregroundView, self.view);
}
// Adds the PinnedTabsViewController and sets constraints.
- (void)setupPinnedTabsViewController {
PinnedTabsViewController* pinnedTabsViewController =
self.pinnedTabsViewController;
pinnedTabsViewController.delegate = self;
pinnedTabsViewController.dragDropHandler = self.pinnedTabsDragDropHandler;
[self addChildViewController:pinnedTabsViewController];
[self.view addSubview:pinnedTabsViewController.view];
[pinnedTabsViewController didMoveToParentViewController:self];
[self updatePinnedTabsViewControllerConstraints];
}
// Adds the thumb strip's plus sign button, which is visible when the plus sign
// cell isn't.
- (void)setupThumbStripPlusSignButton {
ThumbStripPlusSignButton* plusSignButton =
[[ThumbStripPlusSignButton alloc] init];
self.plusSignButton = plusSignButton;
plusSignButton.translatesAutoresizingMaskIntoConstraints = NO;
[plusSignButton addTarget:self
action:@selector(plusSignButtonTapped:)
forControlEvents:UIControlEventTouchUpInside];
DCHECK(self.bottomToolbar);
[self.view insertSubview:plusSignButton aboveSubview:self.bottomToolbar];
self.plusSignButtonBottomConstraint =
[plusSignButton.bottomAnchor constraintEqualToAnchor:self.view.topAnchor
constant:0];
NSArray* constraints = @[
[plusSignButton.topAnchor constraintEqualToAnchor:self.view.topAnchor],
self.plusSignButtonBottomConstraint,
[plusSignButton.trailingAnchor
constraintEqualToAnchor:self.view.trailingAnchor],
[plusSignButton.widthAnchor constraintEqualToConstant:kPlusSignButtonWidth],
];
[NSLayoutConstraint activateConstraints:constraints];
}
- (void)configureViewControllerForCurrentSizeClassesAndPage {
self.configuration = TabGridConfigurationFloatingButton;
if ([self shouldUseCompactLayout] ||
self.tabGridMode == TabGridModeSelection) {
// The bottom toolbar configuration is applied when the UI is narrow but
// vertically long or the selection mode is enabled.
self.configuration = TabGridConfigurationBottomToolbar;
}
[self configureButtonsForActiveAndCurrentPage];
}
- (void)configureButtonsForActiveAndCurrentPage {
self.bottomToolbar.page = self.currentPage;
self.topToolbar.page = self.currentPage;
self.bottomToolbar.mode = self.tabGridMode;
self.topToolbar.mode = self.tabGridMode;
[self configureAddToButtonMenuForSelectedItems];
BOOL incognitoTabsNeedsAuth =
(self.currentPage == TabGridPageIncognitoTabs &&
self.incognitoTabsViewController.contentNeedsAuthentication);
[self.bottomToolbar setAddToButtonEnabled:!incognitoTabsNeedsAuth];
// When current page is a remote tabs page.
if (self.currentPage == TabGridPageRemoteTabs) {
if (self.pageConfiguration ==
TabGridPageConfiguration::kIncognitoPageOnly) {
// Disable done button if showing a disabled tab view for recent tab.
[self configureDoneButtonOnDisabledPage];
} else {
[self configureDoneButtonBasedOnPage:self.activePage];
}
// Configure the "Close All" button on the recent tabs page.
[self configureCloseAllButtonForCurrentPageAndUndoAvailability];
return;
}
// When current page is a disabled tab page.
if ((self.currentPage == TabGridPageIncognitoTabs &&
!self.incognitoTabsViewController) ||
(self.currentPage == TabGridPageRegularTabs &&
!self.regularTabsViewController)) {
[self configureDoneButtonOnDisabledPage];
[self.bottomToolbar setNewTabButtonEnabled:NO];
[self.topToolbar setCloseAllButtonEnabled:NO];
[self.bottomToolbar setCloseAllButtonEnabled:NO];
[self.bottomToolbar setEditButtonEnabled:NO];
[self.topToolbar setEditButtonEnabled:NO];
return;
}
if (self.tabGridMode == TabGridModeSelection) {
[self updateSelectionModeToolbars];
}
[self configureDoneButtonBasedOnPage:self.currentPage];
[self configureNewTabButtonBasedOnContentPermissions];
[self configureCloseAllButtonForCurrentPageAndUndoAvailability];
}
// Updates the add to menu items with all the currently selected items.
- (void)configureAddToButtonMenuForSelectedItems {
GridViewController* gridViewController =
[self gridViewControllerForPage:self.currentPage];
NSArray<NSString*>* items =
gridViewController.selectedShareableItemIDsForEditing;
UIMenu* menu = nil;
switch (self.currentPage) {
case TabGridPageIncognitoTabs:
menu =
[UIMenu menuWithChildren:[self.incognitoTabsDelegate
addToButtonMenuElementsForItems:items]];
break;
case TabGridPageRegularTabs:
menu =
[UIMenu menuWithChildren:[self.regularTabsDelegate
addToButtonMenuElementsForItems:items]];
break;
case TabGridPageRemoteTabs:
// No-op, Add To button inaccessible in remote tabs page.
break;
}
[self.bottomToolbar setAddToButtonMenu:menu];
}
- (void)configureNewTabButtonBasedOnContentPermissions {
BOOL isRecentTabPage = self.currentPage == TabGridPageRemoteTabs;
BOOL allowedByContentAuthentication =
!((self.currentPage == TabGridPageIncognitoTabs) &&
self.incognitoTabsViewController.contentNeedsAuthentication);
BOOL allowNewTab = !isRecentTabPage && allowedByContentAuthentication;
[self.bottomToolbar setNewTabButtonEnabled:allowNewTab];
[self.topToolbar setNewTabButtonEnabled:allowNewTab];
}
- (void)configureDoneButtonBasedOnPage:(TabGridPage)page {
const BOOL tabsPresent = [self tabsPresentForPage:page];
self.topToolbar.pageControl.userInteractionEnabled = YES;
// The Done button should have the same behavior as the other buttons on the
// top Toolbar.
BOOL incognitoTabsNeedsAuth =
(self.currentPage == TabGridPageIncognitoTabs &&
self.incognitoTabsViewController.contentNeedsAuthentication);
BOOL doneEnabled = tabsPresent && !incognitoTabsNeedsAuth;
[self.topToolbar setDoneButtonEnabled:doneEnabled];
[self.bottomToolbar setDoneButtonEnabled:doneEnabled];
}
// YES if there are tabs present on `page`. For `TabGridPageRemoteTabs`, YES
// if there are tabs on either of the other pages.
- (BOOL)tabsPresentForPage:(TabGridPage)page {
switch (page) {
case TabGridPageRemoteTabs:
return !([self.regularTabsViewController isGridEmpty] &&
(!IsPinnedTabsEnabled() ||
[self.pinnedTabsViewController isCollectionEmpty]) &&
[self.incognitoTabsViewController isGridEmpty]);
case TabGridPageRegularTabs:
return !([self.regularTabsViewController isGridEmpty] &&
(!IsPinnedTabsEnabled() ||
[self.pinnedTabsViewController isCollectionEmpty]));
case TabGridPageIncognitoTabs:
return ![self.incognitoTabsViewController isGridEmpty];
}
}
// Disables the done button on bottom toolbar if a disabled tab view is
// presented.
- (void)configureDoneButtonOnDisabledPage {
self.topToolbar.pageControl.userInteractionEnabled = YES;
[self.bottomToolbar setDoneButtonEnabled:NO];
[self.topToolbar setDoneButtonEnabled:NO];
}
- (void)configureCloseAllButtonForCurrentPageAndUndoAvailability {
BOOL useUndo =
self.undoCloseAllAvailable && self.currentPage == TabGridPageRegularTabs;
[self.bottomToolbar useUndoCloseAll:useUndo];
[self.topToolbar useUndoCloseAll:useUndo];
if (useUndo)
return;
// Otherwise setup as a Close All button.
GridViewController* gridViewController =
[self gridViewControllerForPage:self.currentPage];
// "Close all" can be called if there is element in regular tab grid or in
// inactive tabs.
BOOL enabled =
gridViewController && (![gridViewController isGridEmpty] ||
![gridViewController isInactiveGridEmpty]);
BOOL incognitoTabsNeedsAuth =
(self.currentPage == TabGridPageIncognitoTabs &&
self.incognitoTabsViewController.contentNeedsAuthentication);
enabled = enabled && !incognitoTabsNeedsAuth && !self.isDragSeesionInProgress;
[self.topToolbar setCloseAllButtonEnabled:enabled];
[self.bottomToolbar setCloseAllButtonEnabled:enabled];
[self.bottomToolbar setEditButtonEnabled:enabled];
[self.topToolbar setEditButtonEnabled:enabled];
[self.topToolbar setNewTabButtonEnabled:enabled];
}
// Shows the two toolbars and the floating button. Suitable for use in
// animations.
- (void)showToolbars {
[self.topToolbar show];
if (self.thumbStripEnabled) {
GridViewController* gridViewController =
[self gridViewControllerForPage:self.currentPage];
// gridViewController can be null if page configuration disables the
// currentPage mode.
if (gridViewController) {
self.plusSignButton.alpha =
1 - gridViewController.fractionVisibleOfLastItem;
}
}
[self.bottomToolbar show];
}
// Hides the two toolbars. Suitable for use in animations.
- (void)hideToolbars {
[self.topToolbar hide];
[self.bottomToolbar hide];
}
// Translates the toolbar views offscreen and then animates them back in using
// the transition coordinator. Transitions are preferred here since they don't
// interact with the layout system at all.
- (void)animateToolbarsForAppearance {
DCHECK(self.transitionCoordinator);
// Unless reduce motion is enabled, hide the scroll view during the
// animation.
if (!UIAccessibilityIsReduceMotionEnabled()) {
self.scrollView.hidden = YES;
}
// Fade the toolbars in for the last 60% of the transition.
auto keyframe = ^{
[UIView addKeyframeWithRelativeStartTime:0.2
relativeDuration:0.6
animations:^{
[self showToolbars];
}];
};
// Animation block that does the keyframe animation.
auto animation = ^(id<UIViewControllerTransitionCoordinatorContext> context) {
[UIView animateKeyframesWithDuration:context.transitionDuration
delay:0
options:UIViewAnimationOptionLayoutSubviews
animations:keyframe
completion:nil];
};
// Restore the scroll view and toolbar opacities (in case the animation didn't
// complete) as part of the completion.
auto cleanup = ^(id<UIViewControllerTransitionCoordinatorContext> context) {
self.scrollView.hidden = NO;
[self showToolbars];
};
// Animate the toolbar alphas alongside the current transition.
[self.transitionCoordinator animateAlongsideTransition:animation
completion:cleanup];
}
// Translates the toolbar views offscreen using the transition coordinator.
- (void)animateToolbarsForDisappearance {
DCHECK(self.transitionCoordinator);
// Unless reduce motion is enabled, hide the scroll view during the
// animation.
if (!UIAccessibilityIsReduceMotionEnabled()) {
self.scrollView.hidden = YES;
}
// Fade the toolbars out in the first 66% of the transition.
auto keyframe = ^{
[UIView addKeyframeWithRelativeStartTime:0
relativeDuration:0.40
animations:^{
[self hideToolbars];
}];
};
// Animation block that does the keyframe animation.
auto animation = ^(id<UIViewControllerTransitionCoordinatorContext> context) {
[UIView animateKeyframesWithDuration:context.transitionDuration
delay:0
options:UIViewAnimationOptionLayoutSubviews
animations:keyframe
completion:nil];
};
// Hide the scroll view (and thus the tab grids) until the transition
// completes. Restore the toolbar opacity when the transition completes.
auto cleanup = ^(id<UIViewControllerTransitionCoordinatorContext> context) {
self.scrollView.hidden = NO;
};
// Animate the toolbar alphas alongside the current transition.
[self.transitionCoordinator animateAlongsideTransition:animation
completion:cleanup];
}
// Updates the labels and the buttons on the top and the bottom toolbars based
// based on the selected tabs count.
- (void)updateSelectionModeToolbars {
GridViewController* currentGridViewController =
[self gridViewControllerForPage:self.currentPage];
NSUInteger selectedItemsCount =
[currentGridViewController.selectedItemIDsForEditing count];
NSUInteger sharableSelectedItemsCount =
[currentGridViewController.selectedShareableItemIDsForEditing count];
self.topToolbar.selectedTabsCount = selectedItemsCount;
self.bottomToolbar.selectedTabsCount = selectedItemsCount;
BOOL incognitoTabsNeedsAuth =
(self.currentPage == TabGridPageIncognitoTabs &&
self.incognitoTabsViewController.contentNeedsAuthentication);
BOOL enableMultipleItemsSharing =
!incognitoTabsNeedsAuth && sharableSelectedItemsCount > 0;
[self.bottomToolbar setShareTabsButtonEnabled:enableMultipleItemsSharing];
[self.bottomToolbar setAddToButtonEnabled:enableMultipleItemsSharing];
[self.bottomToolbar
setCloseTabsButtonEnabled:!incognitoTabsNeedsAuth && selectedItemsCount];
[self.topToolbar setSelectAllButtonEnabled:!incognitoTabsNeedsAuth];
if (currentGridViewController.allItemsSelectedForEditing) {
[self.topToolbar configureDeselectAllButtonTitle];
} else {
[self.topToolbar configureSelectAllButtonTitle];
}
[self configureAddToButtonMenuForSelectedItems];
}
// Records when the user switches between incognito and regular pages in the tab
// grid. Switching to a different TabGridPage can either be driven by dragging
// the scrollView or tapping on the pageControl.
- (void)recordActionSwitchingToPage:(TabGridPage)page
withInteration:
(TabSwitcherPageChangeInteraction)interaction {
switch (page) {
case TabGridPageIncognitoTabs:
// There are duplicate metrics below that correspond to the previous
// separate implementations for iPhone and iPad. Having both allow for
// comparisons to the previous implementations.
base::RecordAction(
base::UserMetricsAction("MobileTabGridSelectIncognitoPanel"));
break;
case TabGridPageRegularTabs:
// There are duplicate metrics below that correspond to the previous
// separate implementations for iPhone and iPad. Having both allow for
// comparisons to the previous implementations.
base::RecordAction(
base::UserMetricsAction("MobileTabGridSelectRegularPanel"));
break;
case TabGridPageRemoteTabs:
base::RecordAction(
base::UserMetricsAction("MobileTabGridSelectRemotePanel"));
LogLikelyInterestedDefaultBrowserUserActivity(DefaultPromoTypeAllTabs);
break;
}
UMA_HISTOGRAM_ENUMERATION(kUMATabSwitcherPageChangeInteractionHistogram,
interaction);
}
// Tells the appropriate delegate to create a new item, and then tells the
// presentation delegate to show the new item.
- (void)openNewTabInPage:(TabGridPage)page focusOmnibox:(BOOL)focusOmnibox {
switch (page) {
case TabGridPageIncognitoTabs:
[self.incognitoTabsViewController prepareForDismissal];
[self.incognitoTabsDelegate addNewItem];
break;
case TabGridPageRegularTabs:
[self.regularTabsViewController prepareForDismissal];
[self.regularTabsDelegate addNewItem];
break;
case TabGridPageRemoteTabs:
NOTREACHED() << "It is invalid to have an active tab in remote tabs.";
break;
}
self.activePage = page;
[self.tabPresentationDelegate showActiveTabInPage:page
focusOmnibox:focusOmnibox
closeTabGrid:YES];
}
// Creates and shows a new regular tab.
- (void)openNewRegularTabForKeyboardCommand {
[self.handler dismissModalDialogs];
[self openNewTabInPage:TabGridPageRegularTabs focusOmnibox:YES];
base::RecordAction(
base::UserMetricsAction("MobileTabGridCreateRegularTabKeyboard"));
}
// Creates and shows a new incognito tab.
- (void)openNewIncognitoTabForKeyboardCommand {
[self.handler dismissModalDialogs];
[self openNewTabInPage:TabGridPageIncognitoTabs focusOmnibox:YES];
base::RecordAction(
base::UserMetricsAction("MobileTabGridCreateIncognitoTabKeyboard"));
}
// Creates and shows a new tab in the current page.
- (void)openNewTabInCurrentPageForKeyboardCommand {
switch (self.currentPage) {
case TabGridPageIncognitoTabs:
[self openNewIncognitoTabForKeyboardCommand];
break;
case TabGridPageRegularTabs:
[self openNewRegularTabForKeyboardCommand];
break;
case TabGridPageRemoteTabs:
NOTREACHED() << "It is invalid to have an active tab in remote tabs.";
break;
}
}
// Updates the views, buttons, toolbars as well as broadcasts incognito tabs
// visibility after the tab count has changed.
- (void)handleTabCountChangeWithTabCount:(NSUInteger)tabCount {
if (self.tabGridMode == TabGridModeSelection) {
// Exit selection mode if there are no more tabs.
if (tabCount == 0) {
self.tabGridMode = TabGridModeNormal;
}
[self updateSelectionModeToolbars];
}
if (tabCount > 0) {
// Undo is only available when the tab grid is empty.
self.undoCloseAllAvailable = NO;
}
[self configureButtonsForActiveAndCurrentPage];
[self broadcastIncognitoContentVisibility];
}
// Broadcasts whether incognito tabs are showing.
- (void)broadcastIncognitoContentVisibility {
// It is programmer error to broadcast incognito content visibility when the
// view is not visible.
if (!self.viewVisible)
return;
BOOL incognitoContentVisible =
(self.currentPage == TabGridPageIncognitoTabs &&
!self.incognitoTabsViewController.gridEmpty);
[self.handler setIncognitoContentVisible:incognitoContentVisible];
}
- (void)setupSearchUI {
self.scrimView = [[UIControl alloc] init];
self.scrimView.backgroundColor =
[UIColor colorNamed:kDarkerScrimBackgroundColor];
self.scrimView.translatesAutoresizingMaskIntoConstraints = NO;
self.scrimView.accessibilityIdentifier = kTabGridScrimIdentifier;
[self.scrimView addTarget:self
action:@selector(cancelSearchButtonTapped:)
forControlEvents:UIControlEventTouchUpInside];
// Add a gesture recognizer to identify when the user interactions with the
// search results.
self.searchResultPanRecognizer =
[[UIPanGestureRecognizer alloc] initWithTarget:self.view
action:@selector(endEditing:)];
self.searchResultPanRecognizer.cancelsTouchesInView = NO;
self.searchResultPanRecognizer.delegate = self;
}
// Shows scrim overlay.
- (void)showScrim {
self.scrimView.alpha = 0.0f;
if (!self.scrimView.superview) {
[self.scrollContentView addSubview:self.scrimView];
AddSameConstraints(self.scrimView, self.view.superview);
[self.view layoutIfNeeded];
}
self.currentPageViewController.accessibilityElementsHidden = YES;
__weak __typeof(self) weakSelf = self;
[UIView animateWithDuration:kAnimationDuration.InSecondsF()
animations:^{
TabGridViewController* strongSelf = weakSelf;
if (!strongSelf)
return;
strongSelf.scrimView.hidden = NO;
strongSelf.scrimView.alpha = 1.0f;
}
completion:^(BOOL finished) {
TabGridViewController* strongSelf = weakSelf;
if (!strongSelf)
return;
strongSelf.isScrimDisplayed = (strongSelf.scrimView.alpha > 0);
strongSelf.currentPageViewController.accessibilityElementsHidden = YES;
}];
}
// Hides scrim overlay.
- (void)hideScrim {
__weak TabGridViewController* weakSelf = self;
[UIView animateWithDuration:kAnimationDuration.InSecondsF()
animations:^{
TabGridViewController* strongSelf = weakSelf;
if (!strongSelf)
return;
strongSelf.scrimView.alpha = 0.0f;
strongSelf.scrimView.hidden = YES;
}
completion:^(BOOL finished) {
TabGridViewController* strongSelf = weakSelf;
if (!strongSelf)
return;
strongSelf.currentPageViewController.accessibilityElementsHidden = NO;
strongSelf.isScrimDisplayed = (strongSelf.scrimView.alpha > 0);
}];
}
// Updates the appearance of the toolbars based on the scroll position of the
// currently active Grid.
- (void)updateToolbarsAppearance {
UIScrollView* scrollView;
switch (self.currentPage) {
case TabGridPageIncognitoTabs:
scrollView = self.incognitoTabsViewController.gridView;
break;
case TabGridPageRegularTabs:
scrollView = self.regularTabsViewController.gridView;
break;
case TabGridPageRemoteTabs:
scrollView = self.remoteTabsViewController.tableView;
break;
}
BOOL gridScrolledToTop =
scrollView.contentOffset.y <= -scrollView.adjustedContentInset.top;
[self.topToolbar setScrollViewScrolledToEdge:gridScrolledToTop];
CGFloat scrollableHeight = scrollView.contentSize.height +
scrollView.adjustedContentInset.bottom -
scrollView.bounds.size.height;
BOOL gridScrolledToBottom = scrollView.contentOffset.y >= scrollableHeight;
[self.bottomToolbar setScrollViewScrolledToEdge:gridScrolledToBottom];
}
- (void)reportTabSelectionTime {
CHECK(!self.tabGridEnterTime.is_null());
base::TimeDelta duration = base::TimeTicks::Now() - self.tabGridEnterTime;
base::UmaHistogramLongTimes("IOS.TabSwitcher.TimeSpentOpeningExistingTab",
duration);
self.tabGridEnterTime = base::TimeTicks();
}
#pragma mark UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:
(UIGestureRecognizer*)otherGestureRecognizer {
if (gestureRecognizer == self.searchResultPanRecognizer)
return YES;
return NO;
}
#pragma mark UISearchBarDelegate
- (void)searchBarTextDidBeginEditing:(UISearchBar*)searchBar {
[self updateScrimVisibilityForText:searchBar.text];
[self.currentPageViewController.view
addGestureRecognizer:self.searchResultPanRecognizer];
}
- (void)searchBarTextDidEndEditing:(UISearchBar*)searchBar {
[self.currentPageViewController.view
removeGestureRecognizer:self.searchResultPanRecognizer];
}
- (void)searchBarSearchButtonClicked:(UISearchBar*)searchBar {
[searchBar resignFirstResponder];
}
- (void)searchBar:(UISearchBar*)searchBar textDidChange:(NSString*)searchText {
searchBar.searchTextField.accessibilityIdentifier =
[kTabGridSearchTextFieldIdentifierPrefix
stringByAppendingString:searchText];
[self updateScrimVisibilityForText:searchText];
switch (self.currentPage) {
case TabGridPageIncognitoTabs:
self.incognitoTabsViewController.searchText = searchText;
[self updateSearchGrid:self.incognitoTabsDelegate
withSearchText:searchText];
break;
case TabGridPageRegularTabs:
self.regularTabsViewController.searchText = searchText;
[self updateSearchGrid:self.regularTabsDelegate
withSearchText:searchText];
break;
case TabGridPageRemoteTabs:
self.remoteTabsViewController.searchTerms = searchText;
break;
}
}
- (void)updateSearchGrid:(id<GridCommands>)tabsDelegate
withSearchText:(NSString*)searchText {
if (searchText.length) {
[tabsDelegate searchItemsWithText:searchText];
} else {
// The expectation from searchItemsWithText is to search tabs from all
// the available windows to the app. However in the case of empy string
// the grid should revert back to its original state so it doesn't
// display all the tabs from all the available windows.
[tabsDelegate resetToAllItems];
}
}
- (void)updateScrimVisibilityForText:(NSString*)searchText {
if (_tabGridMode != TabGridModeSearch)
return;
if (searchText.length == 0) {
self.isPerformingSearch = NO;
[self showScrim];
} else if (!self.isPerformingSearch) {
self.isPerformingSearch = YES;
// If no results have been presented yet, then hide the scrim to present
// the results.
[self hideScrim];
}
}
// Calculates the proper insets for the Incognito Grid ViewController to
// accomodate for the safe area and toolbar.
- (UIEdgeInsets)calculateInsetForIncognitoGridView {
// The content inset of the tab grids must be modified so that the toolbars
// do not obscure the tabs. This may change depending on orientation.
CGFloat bottomInset = self.configuration == TabGridConfigurationBottomToolbar
? self.bottomToolbar.intrinsicContentSize.height
: 0;
BOOL showThumbStrip = self.thumbStripEnabled;
if (showThumbStrip) {
bottomInset += self.topToolbar.intrinsicContentSize.height;
}
CGFloat topInset =
showThumbStrip ? 0 : self.topToolbar.intrinsicContentSize.height;
UIEdgeInsets inset = UIEdgeInsetsMake(topInset, 0, bottomInset, 0);
inset.left = self.scrollView.safeAreaInsets.left;
inset.right = self.scrollView.safeAreaInsets.right;
inset.top += self.scrollView.safeAreaInsets.top;
inset.bottom += self.scrollView.safeAreaInsets.bottom;
return inset;
}
// Calculates the proper insets for the Regular Grid ViewController to
// accomodate for the safe area and toolbars.
- (UIEdgeInsets)calculateInsetForRegularGridView {
UIEdgeInsets inset = [self calculateInsetForIncognitoGridView];
if (self.regularTabsBottomMessage &&
!self.regularTabsBottomMessage.view.hidden) {
inset.bottom += self.regularTabsBottomMessage.view.bounds.size.height;
}
if (IsPinnedTabsEnabled() && self.pinnedTabsViewController.visible) {
CGFloat pinnedViewHeight =
self.pinnedTabsViewController.view.bounds.size.height;
inset.bottom += pinnedViewHeight + kPinnedViewBottomPadding;
}
return inset;
}
#pragma mark - RecentTabsTableViewControllerUIDelegate
- (void)recentTabsScrollViewDidScroll:
(RecentTabsTableViewController*)recentTabsTableViewController {
[self updateToolbarsAppearance];
}
#pragma mark - SuggestedActionsDelegate
- (void)fetchSearchHistoryResultsCountForText:(NSString*)searchText
completion:(void (^)(size_t))completion {
if (self.currentPage == TabGridPageIncognitoTabs) {
// History retrival shouldn't be done from incognito tabs page.
completion(0);
return;
}
[self.regularTabsDelegate fetchSearchHistoryResultsCountForText:searchText
completion:completion];
}
- (void)searchHistoryForText:(NSString*)searchText {
DCHECK(self.tabGridMode == TabGridModeSearch);
[self.delegate showHistoryFilteredBySearchText:searchText];
}
- (void)searchWebForText:(NSString*)searchText {
DCHECK(self.tabGridMode == TabGridModeSearch);
[self.delegate openSearchResultsPageForSearchText:searchText];
}
- (void)searchRecentTabsForText:(NSString*)searchText {
DCHECK(self.tabGridMode == TabGridModeSearch);
[self setCurrentPageAndPageControl:TabGridPageRemoteTabs animated:YES];
}
#pragma mark - ThumbStripSupporting
- (BOOL)isThumbStripEnabled {
return self.foregroundView != nil;
}
- (void)thumbStripEnabledWithPanHandler:
(ViewRevealingVerticalPanHandler*)panHandler {
DCHECK(!self.thumbStripEnabled);
self.scrollView.scrollEnabled = NO;
[self setupThumbStripPlusSignButton];
[self setupForegroundView];
[panHandler addAnimatee:self];
[self.regularTabsViewController thumbStripEnabledWithPanHandler:panHandler];
[self.incognitoTabsViewController thumbStripEnabledWithPanHandler:panHandler];
}
- (void)thumbStripDisabled {
[self.regularTabsViewController thumbStripDisabled];
[self.incognitoTabsViewController thumbStripDisabled];
DCHECK(self.thumbStripEnabled);
self.scrollView.scrollEnabled = YES;
[self.plusSignButton removeFromSuperview];
self.plusSignButton = nil;
[self.foregroundView removeFromSuperview];
self.foregroundView = nil;
self.topToolbar.transform = CGAffineTransformIdentity;
self.topToolbar.alpha = 1;
[self showToolbars];
self.regularTabsViewController.gridView.transform = CGAffineTransformIdentity;
self.incognitoTabsViewController.gridView.transform =
CGAffineTransformIdentity;
}
#pragma mark - PinnedTabsViewControllerDelegate
- (void)pinnedTabsViewController:
(PinnedTabsViewController*)pinnedTabsViewController
didSelectItemWithID:(NSString*)itemID {
// Record how long it took to select an item.
[self reportTabSelectionTime];
[self.pinnedTabsDelegate selectItemWithID:itemID];
self.activePage = self.currentPage;
[self setCurrentIdlePageStatus:NO];
[self.tabPresentationDelegate showActiveTabInPage:self.currentPage
focusOmnibox:NO
closeTabGrid:YES];
}
- (void)pinnedTabsViewController:
(PinnedTabsViewController*)pinnedTabsViewController
didChangeItemCount:(NSUInteger)count {
self.topToolbar.pageControl.pinnedTabCount = count;
const NSUInteger totalTabCount =
count + self.topToolbar.pageControl.regularTabCount;
crash_keys::SetRegularTabCount(totalTabCount);
[self handleTabCountChangeWithTabCount:totalTabCount];
}
- (void)pinnedTabsViewControllerVisibilityDidChange:
(PinnedTabsViewController*)pinnedTabsViewController {
UIEdgeInsets inset = [self calculateInsetForRegularGridView];
[UIView animateWithDuration:kPinnedViewInsetAnimationTime
animations:^{
self.regularTabsViewController.gridView.contentInset =
inset;
[self updateRegularTabsBottomMessageConstraintsIfExists];
}];
}
- (void)pinnedTabsViewController:
(PinnedTabsViewController*)pinnedTabsViewController
didMoveItemWithID:(NSString*)itemID {
[self setCurrentIdlePageStatus:NO];
}
- (void)pinnedTabsViewController:(GridViewController*)gridViewController
didRemoveItemWIthID:(NSString*)itemID {
[self setCurrentIdlePageStatus:NO];
}
- (void)pinnedViewControllerDropAnimationWillBegin:
(PinnedTabsViewController*)pinnedTabsViewController {
self.regularTabsViewController.dropAnimationInProgress = YES;
}
- (void)pinnedViewControllerDropAnimationDidEnd:
(PinnedTabsViewController*)pinnedTabsViewController {
self.regularTabsViewController.dropAnimationInProgress = NO;
}
- (void)pinnedViewControllerDragSessionDidEnd:
(PinnedTabsViewController*)pinnedTabsViewController {
self.dragSeesionInProgress = NO;
[self.topToolbar setSearchButtonEnabled:YES];
[self configureDoneButtonBasedOnPage:self.currentPage];
[self configureCloseAllButtonForCurrentPageAndUndoAvailability];
[self configureNewTabButtonBasedOnContentPermissions];
}
#pragma mark - GridViewControllerDelegate
- (void)gridViewController:(GridViewController*)gridViewController
didSelectItemWithID:(NSString*)itemID {
if (self.tabGridMode == TabGridModeSelection) {
[self updateSelectionModeToolbars];
return;
}
// Check if the tab being selected is already selected.
BOOL alreadySelected = NO;
id<GridCommands> tabsDelegate;
if (gridViewController == self.regularTabsViewController) {
tabsDelegate = self.regularTabsDelegate;
base::RecordAction(base::UserMetricsAction("MobileTabGridOpenRegularTab"));
if (self.tabGridMode == TabGridModeSearch) {
base::RecordAction(
base::UserMetricsAction("MobileTabGridOpenRegularTabSearchResult"));
}
} else if (gridViewController == self.incognitoTabsViewController) {
tabsDelegate = self.incognitoTabsDelegate;
base::RecordAction(
base::UserMetricsAction("MobileTabGridOpenIncognitoTab"));
if (self.tabGridMode == TabGridModeSearch) {
base::RecordAction(
base::UserMetricsAction("MobileTabGridOpenIncognitoTabSearchResult"));
}
}
// Record how long it took to select an item.
[self reportTabSelectionTime];
alreadySelected = [tabsDelegate isItemWithIDSelected:itemID];
if (!alreadySelected) {
[self setCurrentIdlePageStatus:NO];
}
[tabsDelegate selectItemWithID:itemID];
if (self.tabGridMode == TabGridModeSearch) {
if (![tabsDelegate isItemWithIDSelected:itemID]) {
// That can happen when the search result that was selected is from
// another window. In that case don't change the active page for this
// window and don't close the tab grid.
base::RecordAction(base::UserMetricsAction(
"MobileTabGridOpenSearchResultInAnotherWindow"));
return;
} else {
// Make sure that the keyboard is dismissed before starting the transition
// to the selected tab.
[self.view endEditing:YES];
}
}
self.activePage = self.currentPage;
// When the tab grid is peeked, selecting an item should not close the grid
// unless the user has selected an already selected tab.
BOOL closeTabGrid = !self.thumbStripEnabled || alreadySelected ||
self.currentState != ViewRevealState::Peeked;
[self.tabPresentationDelegate showActiveTabInPage:self.currentPage
focusOmnibox:NO
closeTabGrid:closeTabGrid];
}
- (void)gridViewController:(GridViewController*)gridViewController
didCloseItemWithID:(NSString*)itemID {
[self setCurrentIdlePageStatus:NO];
if (gridViewController == self.regularTabsViewController) {
[self.regularTabsDelegate closeItemWithID:itemID];
// Record when a regular tab is closed.
base::RecordAction(base::UserMetricsAction("MobileTabGridCloseRegularTab"));
} else if (gridViewController == self.incognitoTabsViewController) {
[self.incognitoTabsDelegate closeItemWithID:itemID];
// Record when an incognito tab is closed.
base::RecordAction(
base::UserMetricsAction("MobileTabGridCloseIncognitoTab"));
}
}
- (void)didTapPlusSignInGridViewController:
(GridViewController*)gridViewController {
[self setCurrentIdlePageStatus:NO];
[self plusSignButtonTapped:self];
[self.tabPresentationDelegate showActiveTabInPage:self.currentPage
focusOmnibox:NO
closeTabGrid:YES];
}
- (void)gridViewController:(GridViewController*)gridViewController
didMoveItemWithID:(NSString*)itemID
toIndex:(NSUInteger)destinationIndex {
[self setCurrentIdlePageStatus:NO];
if (gridViewController == self.regularTabsViewController) {
[self.regularTabsDelegate moveItemWithID:itemID toIndex:destinationIndex];
} else if (gridViewController == self.incognitoTabsViewController) {
[self.incognitoTabsDelegate moveItemWithID:itemID toIndex:destinationIndex];
}
}
- (void)gridViewController:(GridViewController*)gridViewController
didChangeItemCount:(NSUInteger)count {
if (gridViewController == self.regularTabsViewController) {
self.topToolbar.pageControl.regularTabCount = count;
const NSUInteger totalTabCount =
count + self.topToolbar.pageControl.pinnedTabCount;
crash_keys::SetRegularTabCount(totalTabCount);
[self handleTabCountChangeWithTabCount:totalTabCount];
} else if (gridViewController == self.incognitoTabsViewController) {
crash_keys::SetIncognitoTabCount(count);
[self handleTabCountChangeWithTabCount:count];
}
[self setupEditButton];
}
- (void)gridViewController:(GridViewController*)gridViewController
didRemoveItemWIthID:(NSString*)itemID {
[self setCurrentIdlePageStatus:NO];
}
- (void)didChangeLastItemVisibilityInGridViewController:
(GridViewController*)gridViewController {
self.plusSignButton.plusSignVerticalOffset =
gridViewController.gridView.adjustedContentInset.top -
kGridExpectedTopContentInset;
CGFloat lastItemVisiblity = gridViewController.fractionVisibleOfLastItem;
self.plusSignButton.alpha = 1 - lastItemVisiblity;
CGFloat xDistance = UseRTLLayout() ? -kScrollThresholdForPlusSignButtonHide
: kScrollThresholdForPlusSignButtonHide;
self.plusSignButton.plusSignImage.transform =
lastItemVisiblity < 1
? CGAffineTransformMakeTranslation(lastItemVisiblity * xDistance, 0)
: CGAffineTransformIdentity;
}
- (void)gridViewController:(GridViewController*)gridViewController
contentNeedsAuthenticationChanged:(BOOL)needsAuth {
[self configureButtonsForActiveAndCurrentPage];
}
- (void)gridViewControllerWillBeginDragging:
(GridViewController*)gridViewController {
if (!self.thumbStripEnabled) {
return;
}
[self.incognitoPopupMenuHandler dismissPopupMenuAnimated:YES];
[self.regularPopupMenuHandler dismissPopupMenuAnimated:YES];
}
- (void)gridViewControllerDragSessionWillBegin:
(GridViewController*)gridViewController {
self.dragSeesionInProgress = YES;
// Actions on both bars should be disabled during dragging.
[self.topToolbar setDoneButtonEnabled:NO];
self.topToolbar.pageControl.userInteractionEnabled = NO;
[self.bottomToolbar setDoneButtonEnabled:NO];
[self.topToolbar setNewTabButtonEnabled:NO];
[self.topToolbar setSelectAllButtonEnabled:NO];
[self.topToolbar setEditButtonEnabled:NO];
[self.topToolbar setSearchButtonEnabled:NO];
[self.bottomToolbar setEditButtonEnabled:NO];
[self.bottomToolbar setAddToButtonEnabled:NO];
[self.bottomToolbar setShareTabsButtonEnabled:NO];
[self.bottomToolbar setCloseTabsButtonEnabled:NO];
if (IsPinnedTabsEnabled()) {
[self.pinnedTabsViewController dragSessionEnabled:YES];
}
}
- (void)gridViewControllerDragSessionDidEnd:
(GridViewController*)gridViewController {
self.dragSeesionInProgress = NO;
[self.topToolbar setSearchButtonEnabled:YES];
// -configureDoneButtonBasedOnPage will enable the page control.
[self configureDoneButtonBasedOnPage:self.currentPage];
[self configureCloseAllButtonForCurrentPageAndUndoAvailability];
[self configureNewTabButtonBasedOnContentPermissions];
if (IsPinnedTabsEnabled()) {
[self.pinnedTabsViewController dragSessionEnabled:NO];
}
if (self.tabGridMode == TabGridModeSelection) {
[self updateSelectionModeToolbars];
}
}
- (void)gridViewControllerScrollViewDidScroll:
(GridViewController*)gridViewController {
[self updateToolbarsAppearance];
}
- (void)gridViewControllerDropAnimationWillBegin:
(GridViewController*)gridViewController {
if (IsPinnedTabsEnabled()) {
self.pinnedTabsViewController.dropAnimationInProgress = YES;
}
}
- (void)gridViewControllerDropAnimationDidEnd:
(GridViewController*)gridViewController {
if (IsPinnedTabsEnabled()) {
[self.pinnedTabsViewController dropAnimationDidEnd];
}
}
- (void)didTapInactiveTabsButtonInGridViewController:
(GridViewController*)gridViewController {
CHECK(IsInactiveTabsEnabled());
CHECK_EQ(self.currentPage, TabGridPageRegularTabs);
base::RecordAction(base::UserMetricsAction("MobileTabGridShowInactiveTabs"));
[self.delegate showInactiveTabs];
}
- (void)didTapInactiveTabsSettingsLinkInGridViewController:
(GridViewController*)gridViewController {
NOTREACHED();
}
#pragma mark - Control actions
- (void)doneButtonTapped:(id)sender {
// Tapping Done when in selection mode, should only return back to the normal
// mode.
if (self.tabGridMode == TabGridModeSelection) {
self.tabGridMode = TabGridModeNormal;
// Records action when user exit the selection mode.
base::RecordAction(base::UserMetricsAction("MobileTabGridSelectionDone"));
return;
}
TabGridPage newActivePage = self.currentPage;
if (self.currentPage == TabGridPageRemoteTabs) {
[self setCurrentIdlePageStatus:YES];
newActivePage = self.activePage;
}
self.activePage = newActivePage;
// Holding the done button down when it is enabled could result in done tap
// being triggered on release after tabs have been closed and the button
// disabled. Ensure that action is only taken on a valid state.
if ([self tabsPresentForPage:newActivePage]) {
[self.tabPresentationDelegate showActiveTabInPage:newActivePage
focusOmnibox:NO
closeTabGrid:YES];
// Record when users exit the tab grid to return to the current foreground
// tab.
base::RecordAction(base::UserMetricsAction("MobileTabGridDone"));
}
}
- (void)selectTabsButtonTapped:(id)sender {
self.tabGridMode = TabGridModeSelection;
base::RecordAction(base::UserMetricsAction("MobileTabGridSelectTabs"));
}
- (void)selectAllButtonTapped:(id)sender {
GridViewController* gridViewController =
[self gridViewControllerForPage:self.currentPage];
// Deselect all items if they are all already selected.
if (gridViewController.allItemsSelectedForEditing) {
base::RecordAction(
base::UserMetricsAction("MobileTabGridSelectionDeselectAll"));
[gridViewController deselectAllItemsForEditing];
} else {
base::RecordAction(
base::UserMetricsAction("MobileTabGridSelectionSelectAll"));
[gridViewController selectAllItemsForEditing];
}
[self updateSelectionModeToolbars];
}
- (void)closeAllButtonTapped:(id)sender {
switch (self.currentPage) {
case TabGridPageIncognitoTabs:
[self.incognitoTabsDelegate closeAllItems];
break;
case TabGridPageRegularTabs:
[self handleCloseAllButtonForRegularTabsWithAnchor:sender];
break;
case TabGridPageRemoteTabs:
NOTREACHED() << "It is invalid to call close all tabs on remote tabs.";
break;
}
}
- (void)handleCloseAllButtonForRegularTabsWithAnchor:(UIBarButtonItem*)anchor {
DCHECK_EQ(self.undoCloseAllAvailable,
(self.regularTabsViewController.gridEmpty &&
self.regularTabsViewController.inactiveGridEmpty));
if (self.undoCloseAllAvailable) {
[self undoCloseAllItemsForRegularTabs];
} else {
[self saveAndCloseAllItemsForRegularTabs];
}
}
- (void)undoCloseAllItemsForRegularTabs {
// This was saved as a stack: first save the inactive tabs, then the active
// tabs. So undo in the reverse order: first undo the active tabs, then the
// inactive tabs.
[self.regularTabsDelegate undoCloseAllItems];
[self.inactiveTabsDelegate undoCloseAllItems];
self.undoCloseAllAvailable = NO;
[self configureCloseAllButtonForCurrentPageAndUndoAvailability];
}
- (void)saveAndCloseAllItemsForRegularTabs {
// This was saved as a stack: first save the inactive tabs, then the active
// tabs. So undo in the reverse order: first undo the active tabs, then the
// inactive tabs.
[self.inactiveTabsDelegate saveAndCloseAllItems];
[self.regularTabsDelegate saveAndCloseAllItems];
self.undoCloseAllAvailable = YES;
[self configureCloseAllButtonForCurrentPageAndUndoAvailability];
}
- (void)searchButtonTapped:(id)sender {
self.tabGridMode = TabGridModeSearch;
base::RecordAction(base::UserMetricsAction("MobileTabGridSearchTabs"));
}
- (void)cancelSearchButtonTapped:(id)sender {
if (!self.isScrimDisplayed) {
// Only record search cancel event when an actual search happened.
base::RecordAction(
base::UserMetricsAction("MobileTabGridCancelSearchTabs"));
}
self.tabGridMode = TabGridModeNormal;
}
- (void)newTabButtonTapped:(id)sender {
[self setCurrentIdlePageStatus:NO];
base::RecordAction(base::UserMetricsAction("MobileTabNewTab"));
[self openNewTabInPage:self.currentPage focusOmnibox:NO];
// Record metrics for button taps
switch (self.currentPage) {
case TabGridPageIncognitoTabs:
base::RecordAction(
base::UserMetricsAction("MobileTabGridCreateIncognitoTab"));
break;
case TabGridPageRegularTabs:
base::RecordAction(
base::UserMetricsAction("MobileTabGridCreateRegularTab"));
break;
case TabGridPageRemoteTabs:
// No-op.
break;
}
}
- (void)plusSignButtonTapped:(id)sender {
switch (self.currentPage) {
case TabGridPageIncognitoTabs:
base::RecordAction(base::UserMetricsAction("MobileTabNewTab"));
[self.incognitoTabsDelegate addNewItem];
if (self.currentState == ViewRevealState::Peeked) {
base::RecordAction(
base::UserMetricsAction("MobileThumbstripCreateIncognitoTab"));
} else {
base::RecordAction(
base::UserMetricsAction("MobileTabGridCreateIncognitoTab"));
}
break;
case TabGridPageRegularTabs:
base::RecordAction(base::UserMetricsAction("MobileTabNewTab"));
[self.regularTabsDelegate addNewItem];
if (self.currentState == ViewRevealState::Peeked) {
base::RecordAction(
base::UserMetricsAction("MobileThumbstripCreateRegularTab"));
} else {
base::RecordAction(
base::UserMetricsAction("MobileTabGridCreateRegularTab"));
}
break;
case TabGridPageRemoteTabs:
// No-op.
break;
}
}
- (void)closeSelectedTabs:(id)sender {
GridViewController* gridViewController =
[self gridViewControllerForPage:self.currentPage];
NSArray<NSString*>* items = gridViewController.selectedItemIDsForEditing;
switch (self.currentPage) {
case TabGridPageIncognitoTabs:
[self.incognitoTabsDelegate
showCloseItemsConfirmationActionSheetWithItems:items
anchor:sender];
break;
case TabGridPageRegularTabs:
[self.regularTabsDelegate
showCloseItemsConfirmationActionSheetWithItems:items
anchor:sender];
break;
case TabGridPageRemoteTabs:
NOTREACHED()
<< "It is invalid to call close selected tabs on remote tabs.";
break;
}
}
- (void)shareSelectedTabs:(id)sender {
GridViewController* gridViewController =
[self gridViewControllerForPage:self.currentPage];
NSArray<NSString*>* items =
gridViewController.selectedShareableItemIDsForEditing;
switch (self.currentPage) {
case TabGridPageIncognitoTabs:
[self.incognitoTabsDelegate shareItems:items anchor:sender];
break;
case TabGridPageRegularTabs:
[self.regularTabsDelegate shareItems:items anchor:sender];
break;
case TabGridPageRemoteTabs:
NOTREACHED() << "Multiple tab selection invalid on remote tabs.";
break;
}
}
- (void)pageControlChangedValue:(id)sender {
// Map the page control slider position (in the range 0.0-1.0) to an
// x-offset for the scroll view.
CGFloat offset = self.topToolbar.pageControl.sliderPosition;
// In RTL, flip the offset.
if (UseRTLLayout())
offset = 1.0 - offset;
// Total space available for the scroll view to scroll (horizontally).
CGFloat offsetWidth =
self.scrollView.contentSize.width - self.scrollView.frame.size.width;
CGPoint contentOffset = self.scrollView.contentOffset;
// Find the final offset by using `offset` as a fraction of the available
// scroll width.
contentOffset.x = offsetWidth * offset;
self.scrollView.contentOffset = contentOffset;
}
- (void)pageControlChangedPageByDrag:(id)sender {
TabGridPage newPage = self.topToolbar.pageControl.selectedPage;
[self scrollToPage:newPage animated:YES];
// Records when the user uses the pageControl to switch pages.
if (self.currentPage != newPage) {
[self recordActionSwitchingToPage:newPage
withInteration:TabSwitcherPageChangeInteraction::
kControlDrag];
}
}
- (void)pageControlChangedPageByTap:(id)sender {
TabGridPage newPage = self.topToolbar.pageControl.selectedPage;
[self scrollToPage:newPage animated:YES];
// Records when the user uses the pageControl to switch pages.
if (self.currentPage != newPage) {
[self recordActionSwitchingToPage:newPage
withInteration:TabSwitcherPageChangeInteraction::
kControlTap];
}
}
#pragma mark - DisabledTabViewControllerDelegate
- (void)didTapLinkWithURL:(const GURL&)URL {
[self.delegate openLinkWithURL:URL];
}
#pragma mark - IncognitoReauthObserver
- (void)reauthAgent:(IncognitoReauthSceneAgent*)agent
didUpdateAuthenticationRequirement:(BOOL)isRequired {
if (isRequired) {
self.tabGridMode = TabGridModeNormal;
}
}
#pragma mark - UIResponder Helper
// Returns YES if "close all" can be performed. Conditions are:
// * Tab grid is currently displayed,
// * There are tabs to close in the current page,
// * Not in an undo scenario.
- (BOOL)canCloseAllTab {
return self.viewVisible && ((self.currentPage == TabGridPageIncognitoTabs &&
!self.incognitoTabsViewController.gridEmpty) ||
(self.currentPage == TabGridPageRegularTabs &&
!self.regularTabsViewController.gridEmpty &&
!self.undoCloseAllAvailable));
}
// Returns YES if "undo" the close all action can be performed.
- (BOOL)canUndoCloseAllTab {
return self.viewVisible && self.currentPage == TabGridPageRegularTabs &&
self.undoCloseAllAvailable;
}
#pragma mark - UIResponder
// To always be able to register key commands via -keyCommands, the VC must be
// able to become first responder.
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (NSArray<UIKeyCommand*>*)keyCommands {
// On iOS 15+, key commands visible in the app's menu are created in
// MenuBuilder.
if (@available(iOS 15, *)) {
// Return the key commands that are not already present in the menu.
return @[
UIKeyCommand.cr_openNewRegularTab,
UIKeyCommand.cr_undo,
UIKeyCommand.cr_close,
// TODO(crbug.com/1385469): Move it to the menu builder once we have the
// strings.
UIKeyCommand.cr_select2,
UIKeyCommand.cr_select3,
];
} else {
// Return all the commands supported by TabGridViewController.
return @[
UIKeyCommand.cr_openNewTab,
UIKeyCommand.cr_openNewIncognitoTab,
UIKeyCommand.cr_openNewRegularTab,
UIKeyCommand.cr_close,
];
}
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
if (sel_isEqual(action, @selector(keyCommand_openNewTab)) ||
sel_isEqual(action, @selector(keyCommand_openNewRegularTab)) ||
sel_isEqual(action, @selector(keyCommand_openNewIncognitoTab))) {
return self.currentPage != TabGridPageRemoteTabs;
}
if (sel_isEqual(action, @selector(keyCommand_find))) {
return self.viewVisible;
}
if (sel_isEqual(action, @selector(keyCommand_closeAll))) {
return [self canCloseAllTab];
}
if (sel_isEqual(action, @selector(keyCommand_undo))) {
return [self canUndoCloseAllTab];
}
return [super canPerformAction:action withSender:sender];
}
- (void)validateCommand:(UICommand*)command {
if (command.action == @selector(keyCommand_find)) {
command.discoverabilityTitle =
l10n_util::GetNSStringWithFixup(IDS_IOS_KEYBOARD_SEARCH_TABS);
} else {
// TODO(crbug.com/1385469): Add string for change pane's functions.
return [super validateCommand:command];
}
}
- (void)keyCommand_openNewTab {
base::RecordAction(base::UserMetricsAction("MobileKeyCommandOpenNewTab"));
[self openNewTabInCurrentPageForKeyboardCommand];
}
- (void)keyCommand_openNewRegularTab {
base::RecordAction(
base::UserMetricsAction("MobileKeyCommandOpenNewRegularTab"));
[self openNewRegularTabForKeyboardCommand];
}
- (void)keyCommand_openNewIncognitoTab {
base::RecordAction(
base::UserMetricsAction("MobileKeyCommandOpenNewIncognitoTab"));
[self openNewIncognitoTabForKeyboardCommand];
}
- (void)keyCommand_find {
base::RecordAction(base::UserMetricsAction("MobileKeyCommandSearchTabs"));
[self searchButtonTapped:nil];
}
- (void)keyCommand_select1 {
base::RecordAction(
base::UserMetricsAction("MobileKeyCommandGoToIncognitoTabGrid"));
[self setCurrentPageAndPageControl:TabGridPageIncognitoTabs animated:YES];
}
- (void)keyCommand_select2 {
base::RecordAction(
base::UserMetricsAction("MobileKeyCommandGoToRegularTabGrid"));
[self setCurrentPageAndPageControl:TabGridPageRegularTabs animated:YES];
}
- (void)keyCommand_select3 {
base::RecordAction(
base::UserMetricsAction("MobileKeyCommandGoToRemoteTabGrid"));
[self setCurrentPageAndPageControl:TabGridPageRemoteTabs animated:YES];
}
- (void)keyCommand_closeAll {
base::RecordAction(base::UserMetricsAction("MobileKeyCommandCloseAll"));
[self closeAllButtonTapped:nil];
}
- (void)keyCommand_undo {
base::RecordAction(base::UserMetricsAction("MobileKeyCommandUndo"));
// This function is also responsible for handling undo.
[self closeAllButtonTapped:nil];
}
- (void)keyCommand_close {
base::RecordAction(base::UserMetricsAction("MobileKeyCommandClose"));
if (self.tabGridMode == TabGridModeSearch) {
[self cancelSearchButtonTapped:nil];
} else {
[self doneButtonTapped:nil];
}
}
// Returns `YES` if should use compact layout.
- (BOOL)shouldUseCompactLayout {
return self.traitCollection.verticalSizeClass ==
UIUserInterfaceSizeClassRegular &&
self.traitCollection.horizontalSizeClass ==
UIUserInterfaceSizeClassCompact;
}
// Updates and sets constraints for `pinnedTabsViewController`.
- (void)updatePinnedTabsViewControllerConstraints {
if ([self.pinnedTabsConstraints count] > 0) {
[NSLayoutConstraint deactivateConstraints:self.pinnedTabsConstraints];
self.pinnedTabsConstraints = nil;
}
UIView* pinnedView = self.pinnedTabsViewController.view;
NSMutableArray<NSLayoutConstraint*>* pinnedTabsConstraints =
[[NSMutableArray alloc] init];
BOOL compactLayout = [self shouldUseCompactLayout];
if (compactLayout) {
[pinnedTabsConstraints addObjectsFromArray:@[
[pinnedView.leadingAnchor
constraintEqualToAnchor:self.view.leadingAnchor
constant:kPinnedViewHorizontalPadding],
[pinnedView.trailingAnchor
constraintEqualToAnchor:self.view.trailingAnchor
constant:-kPinnedViewHorizontalPadding],
[pinnedView.bottomAnchor
constraintEqualToAnchor:self.bottomToolbar.topAnchor
constant:-kPinnedViewBottomPadding],
]];
} else {
[pinnedTabsConstraints addObjectsFromArray:@[
[pinnedView.centerXAnchor
constraintEqualToAnchor:self.view.centerXAnchor],
[pinnedView.widthAnchor
constraintEqualToAnchor:self.view.widthAnchor
multiplier:kPinnedViewMaxWidthInPercent],
[pinnedView.topAnchor
constraintEqualToAnchor:self.bottomToolbar.topAnchor],
]];
}
self.pinnedTabsConstraints = pinnedTabsConstraints;
[NSLayoutConstraint activateConstraints:self.pinnedTabsConstraints];
}
// Updates the bottom constraint for the bottom message on the regular tabs.
- (void)updateRegularTabsBottomMessageConstraintsIfExists {
if (!self.regularTabsBottomMessage) {
return;
}
[NSLayoutConstraint
deactivateConstraints:self.regularTabsBottomMessageConstraints];
self.regularTabsBottomMessageConstraints = nil;
UIView* bottomMessageView = self.regularTabsBottomMessage.view;
[bottomMessageView invalidateIntrinsicContentSize];
NSMutableArray<NSLayoutConstraint*>* constraints =
[[NSMutableArray alloc] init];
// left and right anchors.
if ([self shouldUseCompactLayout]) {
[constraints addObjectsFromArray:@[
[bottomMessageView.widthAnchor
constraintEqualToAnchor:self.view.widthAnchor],
[bottomMessageView.centerXAnchor
constraintEqualToAnchor:self.regularTabsViewController.view
.centerXAnchor]
]];
} else {
// Make space on the right so that the message would NOT cover the new tab
// button.
CGFloat trailingMarginToShowNewTabButton =
kTabGridFloatingButtonHorizontalInset +
self.bottomToolbar.largeNewTabButton.intrinsicContentSize.width;
[constraints addObjectsFromArray:@[
[bottomMessageView.widthAnchor
constraintEqualToAnchor:self.view.widthAnchor
constant:self.regularTabsViewController.gridView
.contentOffset.x -
trailingMarginToShowNewTabButton],
[bottomMessageView.leadingAnchor
constraintEqualToAnchor:self.regularTabsViewController.view
.leadingAnchor]
]];
}
// Bottom and top anchors.
CGFloat topLayoutAnchorConstant =
[self shouldUseCompactLayout]
? self.topToolbar.intrinsicContentSize.height +
self.bottomToolbar.intrinsicContentSize.height
: self.topToolbar.intrinsicContentSize.height;
NSLayoutYAxisAnchor* bottomAnchor = [self shouldUseCompactLayout]
? self.bottomToolbar.topAnchor
: self.view.bottomAnchor;
if (IsPinnedTabsEnabled() && self.pinnedTabsViewController.visible) {
bottomAnchor = self.pinnedTabsViewController.view.topAnchor;
}
[constraints addObjectsFromArray:@[
[bottomMessageView.bottomAnchor constraintEqualToAnchor:bottomAnchor],
[bottomMessageView.topAnchor
constraintGreaterThanOrEqualToAnchor:self.view.topAnchor
constant:topLayoutAnchorConstant],
[bottomMessageView.heightAnchor
constraintLessThanOrEqualToConstant:bottomMessageView
.intrinsicContentSize.height],
]];
self.regularTabsBottomMessageConstraints = constraints;
[NSLayoutConstraint
activateConstraints:self.regularTabsBottomMessageConstraints];
}
// Sets up the view for `self.regularTabsBottomMessage`. This should be called
// when the bottom message is just set.
- (void)initializeRegularTabsBottomMessageView {
UIView* bottomMessageView = self.regularTabsBottomMessage.view;
bottomMessageView.translatesAutoresizingMaskIntoConstraints = NO;
// The bottom message should cover all grid cells but not cover the blocking
// view.
bottomMessageView.hidden = self.tabGridMode != TabGridModeNormal;
[self slideInRegularTabsBottomMessage];
}
// Slides `self.regularTabsBottomMessage` from the bottom edge into place. This
// should be called only when the bottom message is just set.
- (void)slideInRegularTabsBottomMessage {
UIView* bottomMessageView = self.regularTabsBottomMessage.view;
UIScrollView* regularTabsGridView = self.regularTabsViewController.gridView;
CGFloat scrollableHeight = regularTabsGridView.contentSize.height +
regularTabsGridView.adjustedContentInset.bottom -
regularTabsGridView.bounds.size.height;
// Slide if there are more active tabs that the screen could hold, and that
// the user has scrolled to the bottom.
BOOL shouldScrollAgainAfterSliding =
regularTabsGridView.contentSize.height >= self.view.bounds.size.height &&
regularTabsGridView.contentOffset.y >= scrollableHeight;
// Initial position of `bottomMessageView should be below the view, so that
// the animation slides it up from the bottom, instead of sliding it down from
// the top.
NSLayoutConstraint* initialConstraint = [bottomMessageView.topAnchor
constraintEqualToAnchor:self.view.bottomAnchor];
initialConstraint.active = YES;
[self.view layoutIfNeeded];
// Perform initial animation.
__weak TabGridViewController* weakSelf = self;
[UIView
animateWithDuration:kAnimationDuration.InSecondsF()
animations:^{
initialConstraint.active = NO;
[weakSelf updateRegularTabsBottomMessageConstraintsIfExists];
[weakSelf.view layoutIfNeeded];
if (shouldScrollAgainAfterSliding) {
CGFloat newScrollableHeight =
scrollableHeight + bottomMessageView.bounds.size.height;
[regularTabsGridView
setContentOffset:CGPointMake(0, newScrollableHeight)
animated:NO];
}
}];
}
// Slides an existing `self.regularTabsBottomMessage` out of the view. This
// should be called when the bottom message is just unset.
- (void)slideOutRegularTabsBottomMessage {
UIEdgeInsets inset = [self calculateInsetForRegularGridView];
[UIView animateWithDuration:kAnimationDuration.InSecondsF()
animations:^{
self.regularTabsViewController.gridView.contentInset =
inset;
}];
}
@end