| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/chrome/browser/ui/tab_grid/tab_grid_view_controller.h" |
| |
| #include "base/metrics/user_metrics.h" |
| #include "base/metrics/user_metrics_action.h" |
| #include "base/strings/sys_string_conversions.h" |
| #import "ios/chrome/browser/ui/commands/application_commands.h" |
| #import "ios/chrome/browser/ui/recent_tabs/recent_tabs_table_view_controller.h" |
| #import "ios/chrome/browser/ui/rtl_geometry.h" |
| #import "ios/chrome/browser/ui/tab_grid/grid/grid_commands.h" |
| #import "ios/chrome/browser/ui/tab_grid/grid/grid_constants.h" |
| #import "ios/chrome/browser/ui/tab_grid/grid/grid_consumer.h" |
| #import "ios/chrome/browser/ui/tab_grid/grid/grid_image_data_source.h" |
| #import "ios/chrome/browser/ui/tab_grid/grid/grid_view_controller.h" |
| #import "ios/chrome/browser/ui/tab_grid/tab_grid_bottom_toolbar.h" |
| #import "ios/chrome/browser/ui/tab_grid/tab_grid_constants.h" |
| #import "ios/chrome/browser/ui/tab_grid/tab_grid_empty_state_view.h" |
| #import "ios/chrome/browser/ui/tab_grid/tab_grid_new_tab_button.h" |
| #import "ios/chrome/browser/ui/tab_grid/tab_grid_page_control.h" |
| #import "ios/chrome/browser/ui/tab_grid/tab_grid_top_toolbar.h" |
| #import "ios/chrome/browser/ui/table_view/chrome_table_view_styler.h" |
| #import "ios/chrome/browser/ui/uikit_ui_util.h" |
| #import "ios/chrome/common/ui_util/constraints_ui_util.h" |
| #include "ios/chrome/grit/ios_strings.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| namespace { |
| // Types of configurations of this view controller. |
| typedef NS_ENUM(NSUInteger, TabGridConfiguration) { |
| TabGridConfigurationBottomToolbar = 1, |
| TabGridConfigurationFloatingButton, |
| }; |
| |
| // Computes the page from the offset and width of |scrollView|. |
| TabGridPage GetPageFromScrollView(UIScrollView* scrollView) { |
| CGFloat pageWidth = scrollView.frame.size.width; |
| NSUInteger page = lround(scrollView.contentOffset.x / pageWidth); |
| 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 ()<GridViewControllerDelegate, |
| UIScrollViewAccessibilityDelegate> |
| // It is programmer error to broadcast incognito content visibility when the |
| // view is not visible. Bookkeeping is based on |-viewWillAppear:| and |
| // |-viewWillDisappear methods. Note that the |Did| methods are not reliably |
| // called (e.g., edge case in multitasking). |
| @property(nonatomic, assign) BOOL broadcasting; |
| // Child view controllers. |
| @property(nonatomic, strong) GridViewController* regularTabsViewController; |
| @property(nonatomic, strong) GridViewController* incognitoTabsViewController; |
| // Other UI components. |
| @property(nonatomic, weak) UIScrollView* scrollView; |
| @property(nonatomic, weak) UIView* scrollContentView; |
| @property(nonatomic, weak) TabGridTopToolbar* topToolbar; |
| @property(nonatomic, weak) TabGridBottomToolbar* bottomToolbar; |
| @property(nonatomic, weak) UIButton* doneButton; |
| @property(nonatomic, weak) UIButton* closeAllButton; |
| @property(nonatomic, assign) BOOL undoCloseAllAvailable; |
| // Clang does not allow property getters to start with the reserved word "new", |
| // but provides a workaround. The getter must be set before the property is |
| // declared. |
| - (TabGridNewTabButton*)newTabButton __attribute__((objc_method_family(none))); |
| @property(nonatomic, weak) TabGridNewTabButton* newTabButton; |
| @property(nonatomic, weak) TabGridNewTabButton* floatingButton; |
| @property(nonatomic, assign) TabGridConfiguration configuration; |
| // Setting the current page will adjust the scroll view to the correct position. |
| @property(nonatomic, assign) TabGridPage currentPage; |
| @end |
| |
| @implementation TabGridViewController |
| // Public properties. |
| @synthesize dispatcher = _dispatcher; |
| @synthesize tabPresentationDelegate = _tabPresentationDelegate; |
| @synthesize regularTabsDelegate = _regularTabsDelegate; |
| @synthesize incognitoTabsDelegate = _incognitoTabsDelegate; |
| @synthesize regularTabsImageDataSource = _regularTabsImageDataSource; |
| @synthesize incognitoTabsImageDataSource = _incognitoTabsImageDataSource; |
| // TabGridPaging property. |
| @synthesize activePage = _activePage; |
| // Private properties. |
| @synthesize broadcasting = _broadcasting; |
| @synthesize regularTabsViewController = _regularTabsViewController; |
| @synthesize incognitoTabsViewController = _incognitoTabsViewController; |
| @synthesize remoteTabsViewController = _remoteTabsViewController; |
| @synthesize scrollView = _scrollView; |
| @synthesize scrollContentView = _scrollContentView; |
| @synthesize topToolbar = _topToolbar; |
| @synthesize bottomToolbar = _bottomToolbar; |
| @synthesize doneButton = _doneButton; |
| @synthesize closeAllButton = _closeAllButton; |
| @synthesize undoCloseAllAvailable = _undoCloseAllAvailable; |
| @synthesize newTabButton = _newTabButton; |
| @synthesize floatingButton = _floatingButton; |
| @synthesize configuration = _configuration; |
| @synthesize currentPage = _currentPage; |
| |
| - (instancetype)init { |
| if (self = [super init]) { |
| _regularTabsViewController = [[GridViewController alloc] init]; |
| _incognitoTabsViewController = [[GridViewController alloc] init]; |
| _remoteTabsViewController = [[RecentTabsTableViewController alloc] init]; |
| } |
| return self; |
| } |
| |
| #pragma mark - UIViewController |
| |
| - (void)viewDidLoad { |
| [super viewDidLoad]; |
| self.view.backgroundColor = UIColorFromRGB(kGridBackgroundColor); |
| [self setupScrollView]; |
| [self setupIncognitoTabsViewController]; |
| [self setupRegularTabsViewController]; |
| [self setupRemoteTabsViewController]; |
| [self setupTopToolbar]; |
| [self setupBottomToolbar]; |
| [self setupFloatingButton]; |
| |
| // 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]; |
| } |
| |
| - (void)viewWillAppear:(BOOL)animated { |
| self.broadcasting = YES; |
| [self.topToolbar.pageControl setSelectedPage:self.currentPage animated:YES]; |
| [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]; |
| [super viewWillAppear:animated]; |
| } |
| |
| - (void)viewWillDisappear:(BOOL)animated { |
| self.undoCloseAllAvailable = NO; |
| [self.regularTabsDelegate 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.broadcasting = NO; |
| [super viewWillDisappear:animated]; |
| } |
| |
| - (void)viewWillLayoutSubviews { |
| [super viewWillLayoutSubviews]; |
| // Call the current page setter to 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.currentPage = _currentPage; |
| } |
| } |
| |
| - (void)viewDidLayoutSubviews { |
| [super viewDidLayoutSubviews]; |
| // 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; |
| UIEdgeInsets contentInset = UIEdgeInsetsMake( |
| self.topToolbar.intrinsicContentSize.height, 0, bottomInset, 0); |
| |
| [self setInsetForRemoteTabs:contentInset]; |
| [self setInsetForGridViews:contentInset]; |
| } |
| |
| - (void)viewWillTransitionToSize:(CGSize)size |
| withTransitionCoordinator: |
| (id<UIViewControllerTransitionCoordinator>)coordinator { |
| [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; |
| auto animate = ^(id<UIViewControllerTransitionCoordinatorContext> context) { |
| // Call the current page setter to sync the scroll view offset to the |
| // current page value. |
| self.currentPage = _currentPage; |
| [self configureViewControllerForCurrentSizeClassesAndPage]; |
| }; |
| [coordinator animateAlongsideTransition:animate completion:nil]; |
| } |
| |
| - (UIStatusBarStyle)preferredStatusBarStyle { |
| return UIStatusBarStyleLightContent; |
| } |
| |
| #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 != _currentPage) { |
| _currentPage = page; |
| [self broadcastIncognitoContentVisibility]; |
| [self configureButtonsForActiveAndCurrentPage]; |
| // Records when the user drags the scrollView to switch pages. |
| [self recordActionSwitchingToPage:_currentPage]; |
| } |
| } |
| } |
| |
| - (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)scrollViewDidEndScrollingAnimation:(UIScrollView*)scrollView { |
| _currentPage = GetPageFromScrollView(scrollView); |
| [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 - GridTransitionStateProviding properties |
| |
| - (BOOL)isSelectedCellVisible { |
| if (self.activePage != self.currentPage) |
| return NO; |
| GridViewController* gridViewController = |
| [self gridViewControllerForPage:self.activePage]; |
| return gridViewController == nil ? NO |
| : gridViewController.selectedCellVisible; |
| } |
| |
| - (GridTransitionLayout*)layoutForTransitionContext: |
| (id<UIViewControllerContextTransitioning>)context { |
| GridViewController* gridViewController = |
| [self gridViewControllerForPage:self.activePage]; |
| return gridViewController == nil ? nil |
| : [gridViewController transitionLayout]; |
| } |
| |
| - (UIView*)proxyContainerForTransitionContext: |
| (id<UIViewControllerContextTransitioning>)context { |
| return self.view; |
| } |
| |
| - (UIView*)proxyPositionForTransitionContext: |
| (id<UIViewControllerContextTransitioning>)context { |
| return self.floatingButton; |
| } |
| |
| #pragma mark - Public |
| |
| - (id<GridConsumer>)regularTabsConsumer { |
| return self.regularTabsViewController; |
| } |
| |
| - (void)setRegularTabsImageDataSource: |
| (id<GridImageDataSource>)regularTabsImageDataSource { |
| self.regularTabsViewController.imageDataSource = regularTabsImageDataSource; |
| _regularTabsImageDataSource = regularTabsImageDataSource; |
| } |
| |
| - (id<GridConsumer>)incognitoTabsConsumer { |
| return self.incognitoTabsViewController; |
| } |
| |
| - (void)setIncognitoTabsImageDataSource: |
| (id<GridImageDataSource>)incognitoTabsImageDataSource { |
| self.incognitoTabsViewController.imageDataSource = |
| incognitoTabsImageDataSource; |
| _incognitoTabsImageDataSource = incognitoTabsImageDataSource; |
| } |
| |
| - (id<RecentTabsTableConsumer>)remoteTabsConsumer { |
| return self.remoteTabsViewController; |
| } |
| |
| #pragma mark - TabGridPaging |
| |
| - (void)setActivePage:(TabGridPage)activePage { |
| [self setCurrentPage:activePage animated:YES]; |
| _activePage = activePage; |
| } |
| |
| #pragma mark - Private |
| |
| // Sets the proper insets for the Remote Tabs ViewController to accomodate for |
| // the safe area, toolbar, and status bar. |
| - (void)setInsetForRemoteTabs:(UIEdgeInsets)inset { |
| // TableView uses SafeArea to adjust insets, so no need to add them to |
| // contentInset. |
| if (@available(iOS 11, *)) { |
| // Left or right side (depending on rtl) is missing correct safe area |
| // inset upon rotation. Manually correct it. This is because the leading |
| // constraint of the tableView is not directly bound to the scroll view's |
| // margins. |
| UIEdgeInsets safeArea = self.scrollView.safeAreaInsets; |
| if (UseRTLLayout()) { |
| inset.right += |
| safeArea.right - |
| self.remoteTabsViewController.tableView.safeAreaInsets.right; |
| } else { |
| inset.left += safeArea.left - |
| self.remoteTabsViewController.tableView.safeAreaInsets.left; |
| } |
| // Ensure that the View Controller doesn't have safe area inset that already |
| // covers the view's bounds. |
| DCHECK(!CGRectIsEmpty(UIEdgeInsetsInsetRect( |
| self.remoteTabsViewController.tableView.bounds, |
| self.remoteTabsViewController.tableView.safeAreaInsets))); |
| self.remoteTabsViewController.additionalSafeAreaInsets = inset; |
| } else { |
| // Must manually account for status bar in pre-iOS 11. |
| inset.top += self.topLayoutGuide.length; |
| self.remoteTabsViewController.tableView.contentInset = inset; |
| } |
| } |
| |
| // Sets the proper insets for the Grid ViewControllers to accomodate for the |
| // safe area and toolbar. |
| - (void)setInsetForGridViews:(UIEdgeInsets)inset { |
| if (@available(iOS 11, *)) { |
| inset.left = self.scrollView.safeAreaInsets.left; |
| inset.right = self.scrollView.safeAreaInsets.right; |
| inset.top += self.scrollView.safeAreaInsets.top; |
| inset.bottom += self.scrollView.safeAreaInsets.bottom; |
| } else { |
| // Must manually account for status bar in pre-iOS 11. |
| inset.top += self.topLayoutGuide.length; |
| } |
| self.incognitoTabsViewController.gridView.contentInset = inset; |
| self.regularTabsViewController.gridView.contentInset = inset; |
| } |
| |
| // 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 { |
| // Setting the current page will adjust the scroll view to the correct |
| // position. |
| [self setCurrentPage:currentPage animated:NO]; |
| } |
| |
| // 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)setCurrentPage:(TabGridPage)currentPage animated:(BOOL)animated { |
| // This method should never early return if |currentPage| == |_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. |
| |
| // If the view isn't loaded yet, just do bookkeeping on _currentPage. |
| if (!self.viewLoaded) { |
| _currentPage = currentPage; |
| return; |
| } |
| CGFloat pageWidth = self.scrollView.frame.size.width; |
| NSUInteger pageIndex = GetPageIndexFromPage(currentPage); |
| CGPoint offset = CGPointMake(pageIndex * pageWidth, 0); |
| // If the view is visible and |animated| is YES, animate the change. |
| // Otherwise don't. |
| if (self.view.window == nil || !animated) { |
| [self.scrollView setContentOffset:offset animated:NO]; |
| _currentPage = currentPage; |
| } else { |
| [self.scrollView setContentOffset:offset animated:YES]; |
| // _currentPage is set in scrollViewDidEndScrollingAnimation: |
| } |
| } |
| |
| // 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; |
| if (@available(iOS 11, *)) { |
| // 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; |
| 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; |
| [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; |
| 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; |
| [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; |
| NSArray* constraints = @[ |
| [viewController.view.topAnchor |
| constraintEqualToAnchor:contentView.topAnchor], |
| [viewController.view.bottomAnchor |
| constraintEqualToAnchor:contentView.bottomAnchor], |
| [viewController.view.leadingAnchor |
| constraintEqualToAnchor:self.incognitoTabsViewController.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 { |
| // 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.tableViewSectionHeaderBlurEffect = nil; |
| styler.tableViewBackgroundColor = UIColorFromRGB(kGridBackgroundColor); |
| styler.cellTitleColor = UIColorFromRGB(kGridDarkThemeCellTitleColor); |
| styler.headerFooterTitleColor = UIColorFromRGB(kGridDarkThemeCellTitleColor); |
| self.remoteTabsViewController.styler = styler; |
| |
| UIView* contentView = self.scrollContentView; |
| RecentTabsTableViewController* viewController = self.remoteTabsViewController; |
| 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 the top toolbar and sets constraints. |
| - (void)setupTopToolbar { |
| TabGridTopToolbar* topToolbar = [[TabGridTopToolbar alloc] init]; |
| topToolbar.translatesAutoresizingMaskIntoConstraints = NO; |
| [self.view addSubview:topToolbar]; |
| self.topToolbar = topToolbar; |
| // Configure and initialize the page control. |
| [self.topToolbar.pageControl addTarget:self |
| action:@selector(pageControlChangedValue:) |
| forControlEvents:UIControlEventValueChanged]; |
| [self.topToolbar.pageControl addTarget:self |
| action:@selector(pageControlChangedPage:) |
| forControlEvents:UIControlEventTouchUpInside]; |
| |
| NSArray* constraints = @[ |
| [topToolbar.topAnchor constraintEqualToAnchor:self.view.topAnchor], |
| [topToolbar.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], |
| [topToolbar.trailingAnchor |
| constraintEqualToAnchor:self.view.trailingAnchor], |
| ]; |
| [NSLayoutConstraint activateConstraints:constraints]; |
| // Set the height of the toolbar, including unsafe areas. |
| if (@available(iOS 11, *)) { |
| // SafeArea is only available in iOS 11+. |
| [topToolbar.bottomAnchor |
| constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor |
| constant:topToolbar.intrinsicContentSize.height] |
| .active = YES; |
| } else { |
| // Top and bottom layout guides are deprecated starting in iOS 11. |
| [topToolbar.bottomAnchor |
| constraintEqualToAnchor:self.topLayoutGuide.bottomAnchor |
| constant:topToolbar.intrinsicContentSize.height] |
| .active = YES; |
| } |
| } |
| |
| // Adds the bottom toolbar and sets constraints. |
| - (void)setupBottomToolbar { |
| TabGridBottomToolbar* bottomToolbar = [[TabGridBottomToolbar alloc] init]; |
| bottomToolbar.translatesAutoresizingMaskIntoConstraints = NO; |
| [self.view addSubview:bottomToolbar]; |
| self.bottomToolbar = bottomToolbar; |
| NSArray* constraints = @[ |
| [bottomToolbar.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor], |
| [bottomToolbar.leadingAnchor |
| constraintEqualToAnchor:self.view.leadingAnchor], |
| [bottomToolbar.trailingAnchor |
| constraintEqualToAnchor:self.view.trailingAnchor], |
| ]; |
| [NSLayoutConstraint activateConstraints:constraints]; |
| // Adds the height of the toolbar above the bottom safe area. |
| if (@available(iOS 11, *)) { |
| // SafeArea is only available in iOS 11+. |
| [self.view.safeAreaLayoutGuide.bottomAnchor |
| constraintEqualToAnchor:bottomToolbar.topAnchor |
| constant:bottomToolbar.intrinsicContentSize.height] |
| .active = YES; |
| } else { |
| // Top and bottom layout guides are deprecated starting in iOS 11. |
| [bottomToolbar.topAnchor |
| constraintEqualToAnchor:self.bottomLayoutGuide.topAnchor |
| constant:-bottomToolbar.intrinsicContentSize.height] |
| .active = YES; |
| } |
| } |
| |
| // Adds floating button and constraints. |
| - (void)setupFloatingButton { |
| TabGridNewTabButton* button = [TabGridNewTabButton |
| buttonWithSizeClass:TabGridNewTabButtonSizeClassLarge]; |
| button.translatesAutoresizingMaskIntoConstraints = NO; |
| // Position the floating button over the scroll view, so transition animations |
| // can be above the button but below the toolbars. |
| [self.view insertSubview:button aboveSubview:self.scrollView]; |
| self.floatingButton = button; |
| CGFloat verticalInset = kTabGridFloatingButtonVerticalInsetSmall; |
| if (self.traitCollection.verticalSizeClass == |
| UIUserInterfaceSizeClassRegular && |
| self.traitCollection.horizontalSizeClass == |
| UIUserInterfaceSizeClassRegular) { |
| verticalInset = kTabGridFloatingButtonVerticalInsetLarge; |
| } |
| id<LayoutGuideProvider> safeAreaGuide = SafeAreaLayoutGuideForView(self.view); |
| [NSLayoutConstraint activateConstraints:@[ |
| [button.trailingAnchor |
| constraintEqualToAnchor:safeAreaGuide.trailingAnchor |
| constant:-kTabGridFloatingButtonHorizontalInset], |
| [button.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor |
| constant:-verticalInset] |
| ]]; |
| } |
| |
| - (void)configureViewControllerForCurrentSizeClassesAndPage { |
| self.configuration = TabGridConfigurationFloatingButton; |
| if (self.traitCollection.verticalSizeClass == |
| UIUserInterfaceSizeClassRegular && |
| self.traitCollection.horizontalSizeClass == |
| UIUserInterfaceSizeClassCompact) { |
| // The only bottom toolbar configuration is when the UI is narrow but |
| // vertically long. |
| self.configuration = TabGridConfigurationBottomToolbar; |
| } |
| switch (self.configuration) { |
| case TabGridConfigurationBottomToolbar: |
| self.topToolbar.leadingButton.hidden = YES; |
| self.topToolbar.trailingButton.hidden = YES; |
| self.bottomToolbar.hidden = NO; |
| self.floatingButton.hidden = YES; |
| self.doneButton = self.bottomToolbar.trailingButton; |
| self.closeAllButton = self.bottomToolbar.leadingButton; |
| self.newTabButton = self.bottomToolbar.centerButton; |
| break; |
| case TabGridConfigurationFloatingButton: |
| self.topToolbar.leadingButton.hidden = NO; |
| self.topToolbar.trailingButton.hidden = NO; |
| self.bottomToolbar.hidden = YES; |
| self.floatingButton.hidden = NO; |
| self.doneButton = self.topToolbar.trailingButton; |
| self.closeAllButton = self.topToolbar.leadingButton; |
| self.newTabButton = self.floatingButton; |
| break; |
| } |
| |
| [self.doneButton setTitle:l10n_util::GetNSString(IDS_IOS_TAB_GRID_DONE_BUTTON) |
| forState:UIControlStateNormal]; |
| self.doneButton.titleLabel.font = |
| [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]; |
| self.closeAllButton.titleLabel.font = |
| [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; |
| self.doneButton.titleLabel.adjustsFontForContentSizeCategory = YES; |
| self.closeAllButton.titleLabel.adjustsFontForContentSizeCategory = YES; |
| self.doneButton.accessibilityIdentifier = kTabGridDoneButtonIdentifier; |
| [self.doneButton addTarget:self |
| action:@selector(doneButtonTapped:) |
| forControlEvents:UIControlEventTouchUpInside]; |
| [self.closeAllButton addTarget:self |
| action:@selector(closeAllButtonTapped:) |
| forControlEvents:UIControlEventTouchUpInside]; |
| [self.newTabButton addTarget:self |
| action:@selector(newTabButtonTapped:) |
| forControlEvents:UIControlEventTouchUpInside]; |
| [self configureButtonsForActiveAndCurrentPage]; |
| } |
| |
| - (void)configureButtonsForActiveAndCurrentPage { |
| self.newTabButton.page = self.currentPage; |
| if (self.currentPage == TabGridPageRemoteTabs) { |
| [self configureDoneButtonBasedOnPage:self.activePage]; |
| } else { |
| [self configureDoneButtonBasedOnPage:self.currentPage]; |
| } |
| [self configureCloseAllButtonForCurrentPageAndUndoAvailability]; |
| } |
| |
| - (void)configureDoneButtonBasedOnPage:(TabGridPage)page { |
| GridViewController* gridViewController = |
| [self gridViewControllerForPage:page]; |
| if (!gridViewController) { |
| NOTREACHED() << "The done button should not be configured based on the " |
| "contents of the recent tabs page."; |
| } |
| self.doneButton.enabled = !gridViewController.gridEmpty; |
| } |
| |
| - (void)configureCloseAllButtonForCurrentPageAndUndoAvailability { |
| if (self.undoCloseAllAvailable && |
| self.currentPage == TabGridPageRegularTabs) { |
| // Setup closeAllButton as undo button. |
| self.closeAllButton.enabled = YES; |
| [self.closeAllButton |
| setTitle:l10n_util::GetNSString(IDS_IOS_TAB_GRID_UNDO_CLOSE_ALL_BUTTON) |
| forState:UIControlStateNormal]; |
| self.closeAllButton.accessibilityIdentifier = |
| kTabGridUndoCloseAllButtonIdentifier; |
| return; |
| } |
| // Otherwise setup as a Close All button. |
| GridViewController* gridViewController = |
| [self gridViewControllerForPage:self.currentPage]; |
| self.closeAllButton.enabled = |
| gridViewController == nil ? NO : !gridViewController.gridEmpty; |
| [self.closeAllButton |
| setTitle:l10n_util::GetNSString(IDS_IOS_TAB_GRID_CLOSE_ALL_BUTTON) |
| forState:UIControlStateNormal]; |
| self.closeAllButton.accessibilityIdentifier = |
| kTabGridCloseAllButtonIdentifier; |
| } |
| |
| // Shows (by setting the alpha to 1.0) the two toolbar views and the floating |
| // button. Suitable for use in animations. |
| - (void)showToolbars { |
| self.topToolbar.alpha = 1.0; |
| self.bottomToolbar.alpha = 1.0; |
| self.floatingButton.alpha = 1.0; |
| } |
| |
| // Hides (by setting the alpha to 0.0) the two toolbar views and the floating |
| // button. Suitable for use in animations. |
| - (void)hideToolbars { |
| self.topToolbar.alpha = 0.0; |
| self.bottomToolbar.alpha = 0.0; |
| self.floatingButton.alpha = 0.0; |
| } |
| |
| // 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]; |
| } |
| |
| // 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 { |
| 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. |
| // TODO(crbug.com/856965) : Consolidate and rename metrics. |
| base::RecordAction( |
| base::UserMetricsAction("MobileStackViewIncognitoMode")); |
| base::RecordAction(base::UserMetricsAction( |
| "MobileTabSwitcherHeaderViewSelectIncognitoPanel")); |
| 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. |
| // TODO(crbug.com/856965) : Consolidate and rename metrics. |
| base::RecordAction(base::UserMetricsAction("MobileStackViewNormalMode")); |
| base::RecordAction(base::UserMetricsAction( |
| "MobileTabSwitcherHeaderViewSelectNonIncognitoPanel")); |
| break; |
| case TabGridPageRemoteTabs: |
| // TODO(crbug.com/856965) : Rename metrics. |
| base::RecordAction(base::UserMetricsAction( |
| "MobileTabSwitcherHeaderViewSelectDistantSessionPanel")); |
| break; |
| } |
| } |
| |
| // Tells the appropriate delegate to create a new item, and then tells the |
| // presentation delegate to show the new item. |
| - (void)openNewTabInPage:(TabGridPage)page { |
| switch (page) { |
| case TabGridPageIncognitoTabs: |
| [self.incognitoTabsDelegate addNewItem]; |
| // Record when new incognito tab is created. |
| // TODO(crbug.com/856965) : Rename metrics. |
| base::RecordAction( |
| base::UserMetricsAction("MobileTabSwitcherCreateIncognitoTab")); |
| break; |
| case TabGridPageRegularTabs: |
| [self.regularTabsDelegate addNewItem]; |
| // Record when new regular tab is created. |
| // TODO(crbug.com/856965) : Rename metrics. |
| base::RecordAction( |
| base::UserMetricsAction("MobileTabSwitcherCreateNonIncognitoTab")); |
| break; |
| case TabGridPageRemoteTabs: |
| // This is intended as NO-OP since the user can ⌘-t while on this page. |
| break; |
| } |
| self.activePage = page; |
| [self.tabPresentationDelegate showActiveTabInPage:page]; |
| } |
| |
| // Creates and shows a new regular tab. |
| - (void)openNewRegularTab { |
| [self openNewTabInPage:TabGridPageRegularTabs]; |
| } |
| |
| // Creates and shows a new incognito tab. |
| - (void)openNewIncognitoTab { |
| [self openNewTabInPage:TabGridPageIncognitoTabs]; |
| } |
| |
| // Creates and shows a new tab in the current page. |
| - (void)openNewTabInCurrentPage { |
| [self openNewTabInPage:self.currentPage]; |
| } |
| |
| // Broadcasts whether incognito tabs are showing. |
| - (void)broadcastIncognitoContentVisibility { |
| if (!self.broadcasting) |
| return; |
| BOOL incognitoContentVisible = |
| (self.currentPage == TabGridPageIncognitoTabs && |
| !self.incognitoTabsViewController.gridEmpty); |
| [self.dispatcher setIncognitoContentVisible:incognitoContentVisible]; |
| } |
| |
| #pragma mark - GridViewControllerDelegate |
| |
| - (void)gridViewController:(GridViewController*)gridViewController |
| didSelectItemWithID:(NSString*)itemID { |
| // Update the model with the tab selection, but don't have the grid view |
| // controller display the new selection, since there will be a transition |
| // away from it immediately afterwards. |
| gridViewController.showsSelectionUpdates = NO; |
| if (gridViewController == self.regularTabsViewController) { |
| [self.regularTabsDelegate selectItemWithID:itemID]; |
| // Record when a regular tab is opened. |
| // TODO(crbug.com/856965) : Rename metrics. |
| base::RecordAction( |
| base::UserMetricsAction("MobileTabSwitcherOpenNonIncognitoTab")); |
| } else if (gridViewController == self.incognitoTabsViewController) { |
| [self.incognitoTabsDelegate selectItemWithID:itemID]; |
| // Record when an incognito tab is opened. |
| // TODO(crbug.com/856965) : Rename metrics. |
| base::RecordAction( |
| base::UserMetricsAction("MobileTabSwitcherOpenIncognitoTab")); |
| } |
| self.activePage = self.currentPage; |
| [self.tabPresentationDelegate showActiveTabInPage:self.currentPage]; |
| gridViewController.showsSelectionUpdates = YES; |
| } |
| |
| - (void)gridViewController:(GridViewController*)gridViewController |
| didCloseItemWithID:(NSString*)itemID { |
| if (gridViewController == self.regularTabsViewController) { |
| [self.regularTabsDelegate closeItemWithID:itemID]; |
| // Record when a regular tab is closed. |
| // TODO(crbug.com/856965) : Rename metrics. |
| base::RecordAction( |
| base::UserMetricsAction("MobileTabSwitcherCloseNonIncognitoTab")); |
| } else if (gridViewController == self.incognitoTabsViewController) { |
| [self.incognitoTabsDelegate closeItemWithID:itemID]; |
| // Record when an incognito tab is closed. |
| // TODO(crbug.com/856965) : Rename metrics. |
| base::RecordAction( |
| base::UserMetricsAction("MobileTabSwitcherCloseIncognitoTab")); |
| } |
| } |
| |
| - (void)gridViewController:(GridViewController*)gridViewController |
| didMoveItemWithID:(NSString*)itemID |
| toIndex:(NSUInteger)destinationIndex { |
| 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 { |
| [self configureButtonsForActiveAndCurrentPage]; |
| if (gridViewController == self.regularTabsViewController) { |
| self.topToolbar.pageControl.regularTabCount = count; |
| } |
| [self broadcastIncognitoContentVisibility]; |
| } |
| |
| #pragma mark - Control actions |
| |
| - (void)doneButtonTapped:(id)sender { |
| TabGridPage newActivePage = self.currentPage; |
| if (self.currentPage == TabGridPageRemoteTabs) { |
| 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 gridViewControllerForPage:newActivePage] isGridEmpty]) { |
| [self.tabPresentationDelegate showActiveTabInPage:newActivePage]; |
| // Record when users exit the tab grid to return to the current foreground |
| // tab. |
| // TODO(crbug.com/856965) : Rename metrics. |
| base::RecordAction( |
| base::UserMetricsAction("MobileTabReturnedToCurrentTab")); |
| } |
| } |
| |
| - (void)closeAllButtonTapped:(id)sender { |
| switch (self.currentPage) { |
| case TabGridPageIncognitoTabs: |
| [self.incognitoTabsDelegate closeAllItems]; |
| break; |
| case TabGridPageRegularTabs: |
| DCHECK_EQ(self.undoCloseAllAvailable, |
| self.regularTabsViewController.gridEmpty); |
| if (self.undoCloseAllAvailable) { |
| [self.regularTabsDelegate undoCloseAllItems]; |
| } else { |
| [self.regularTabsDelegate saveAndCloseAllItems]; |
| } |
| self.undoCloseAllAvailable = !self.undoCloseAllAvailable; |
| [self configureCloseAllButtonForCurrentPageAndUndoAvailability]; |
| break; |
| case TabGridPageRemoteTabs: |
| NOTREACHED() << "It is invalid to call close all tabs on remote tabs."; |
| break; |
| } |
| } |
| |
| - (void)newTabButtonTapped:(id)sender { |
| [self openNewTabInCurrentPage]; |
| // Record only when a new tab is created through the + button. |
| // TODO(crbug.com/856965) : Rename metrics. |
| base::RecordAction(base::UserMetricsAction("MobileToolbarStackViewNewTab")); |
| } |
| |
| - (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)pageControlChangedPage:(id)sender { |
| TabGridPage newPage = self.topToolbar.pageControl.selectedPage; |
| [self setCurrentPage:newPage animated:YES]; |
| // Records when the user taps on the pageControl to switch pages. |
| [self recordActionSwitchingToPage:newPage]; |
| } |
| |
| #pragma mark - UIResponder |
| |
| - (NSArray*)keyCommands { |
| UIKeyCommand* newWindowShortcut = |
| [UIKeyCommand keyCommandWithInput:@"n" |
| modifierFlags:UIKeyModifierCommand |
| action:@selector(openNewRegularTab) |
| discoverabilityTitle:l10n_util::GetNSStringWithFixup( |
| IDS_IOS_TOOLS_MENU_NEW_TAB)]; |
| UIKeyCommand* newIncognitoWindowShortcut = [UIKeyCommand |
| keyCommandWithInput:@"n" |
| modifierFlags:UIKeyModifierCommand | UIKeyModifierShift |
| action:@selector(openNewIncognitoTab) |
| discoverabilityTitle:l10n_util::GetNSStringWithFixup( |
| IDS_IOS_TOOLS_MENU_NEW_INCOGNITO_TAB)]; |
| UIKeyCommand* newTabShortcut = |
| [UIKeyCommand keyCommandWithInput:@"t" |
| modifierFlags:UIKeyModifierCommand |
| action:@selector(openNewTabInCurrentPage) |
| discoverabilityTitle:l10n_util::GetNSStringWithFixup( |
| IDS_IOS_TOOLS_MENU_NEW_TAB)]; |
| return @[ newWindowShortcut, newIncognitoWindowShortcut, newTabShortcut ]; |
| } |
| |
| @end |