chromium/ios/chrome/browser/ui/tab_switcher/tab_grid/toolbars/tab_grid_top_toolbar.mm

// 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/toolbars/tab_grid_top_toolbar.h"

#import <objc/runtime.h>

#import "base/check_op.h"
#import "base/feature_list.h"
#import "base/ios/ios_util.h"
#import "base/location.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/task/sequenced_task_runner.h"
#import "ios/chrome/browser/keyboard/ui_bundled/UIKeyCommand+Chrome.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_constants.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_constants.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/toolbars/tab_grid_page_control.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/toolbars/tab_grid_toolbars_grid_delegate.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/toolbars/tab_grid_toolbars_utils.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"

namespace {
// The space after the new tab toolbar button item. Calculated to have
// approximately 33 pts between the plus button and the done button.
const int kIconButtonAdditionalSpace = 20;
const int kSelectionModeButtonSize = 17;
const int kSearchBarTrailingSpace = 24;

// The size of top toolbar search symbol image.
const CGFloat kSymbolSearchImagePointSize = 22;

}  // namespace

@interface TabGridTopToolbar () <UIToolbarDelegate>
@end

@implementation TabGridTopToolbar {
  UIBarButtonItem* _leadingButton;
  UIBarButtonItem* _spaceItem;
  UIBarButtonItem* _iconButtonAdditionalSpaceItem;
  UIBarButtonItem* _selectionModeFixedSpace;
  UIBarButtonItem* _selectAllButton;
  UIBarButtonItem* _selectedTabsItem;
  UIBarButtonItem* _searchButton;
  UIBarButtonItem* _doneButton;
  UIBarButtonItem* _closeAllOrUndoButton;
  UIBarButtonItem* _editButton;
  UIBarButtonItem* _pageControlItem;
  // Search mode
  UISearchBar* _searchBar;
  UIBarButtonItem* _searchBarItem;
  UIBarButtonItem* _cancelSearchButton;
  UIView* _searchBarView;

  BOOL _undoActive;

  BOOL _scrolledToEdge;
  UIView* _scrolledBackgroundView;
  // Configures the responder following the receiver in the responder chain.
  UIResponder* _followingNextResponder;
}

- (instancetype)initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];
  if (self) {
    [self setupViews];
    [self setItemsForTraitCollection:self.traitCollection];
  }
  return self;
}

- (UIBarButtonItem*)anchorItem {
  return _leadingButton;
}

- (void)setPage:(TabGridPage)page {
  if (_page == page) {
    return;
  }
  _page = page;
  [self setItemsForTraitCollection:self.traitCollection];
}

- (void)setMode:(TabGridMode)mode {
  if (_mode == mode) {
    return;
  }
  // Reset search state when exiting search mode.
  if (_mode == TabGridMode::kSearch) {
    _searchBar.text = @"";
    [_searchBar resignFirstResponder];
  }
  _mode = mode;
  // Reset selected tabs count when mode changes.
  self.selectedTabsCount = 0;
  // Reset the Select All button to its default title.
  [self configureSelectAllButtonTitle];
  [self setItemsForTraitCollection:self.traitCollection];
  if (mode == TabGridMode::kSearch) {
    // Focus the search bar, and make it a first responder once the user enter
    // to search mode. Doing that here instead in `setItemsForTraitCollection`
    // makes sure it's only called once and allows VoiceOver to transition
    // smoothly and to say that there is a search field opened.
    // It is done on the next turn of the runloop as it has been seen to collide
    // with other animations on some devices.
    __weak __typeof(_searchBar) weakSearchBar = _searchBar;
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(^{
          [weakSearchBar becomeFirstResponder];
        }));
  }
}

- (void)setSelectedTabsCount:(int)count {
  _selectedTabsCount = count;
  if (_selectedTabsCount == 0) {
    _selectedTabsItem.title =
        l10n_util::GetNSString(IDS_IOS_TAB_GRID_SELECT_TABS_TITLE);
  } else {
    _selectedTabsItem.title = l10n_util::GetPluralNSStringF(
        IDS_IOS_TAB_GRID_SELECTED_TABS_TITLE, _selectedTabsCount);
  }
}

- (void)setSearchBarDelegate:(id<UISearchBarDelegate>)delegate {
  _searchBar.delegate = delegate;
}

- (void)setSearchButtonEnabled:(BOOL)enabled {
  _searchButton.enabled = enabled;
}

- (void)setCloseAllButtonEnabled:(BOOL)enabled {
  _closeAllOrUndoButton.enabled = enabled;
}

- (void)setSelectAllButtonEnabled:(BOOL)enabled {
  _selectAllButton.enabled = enabled;
}

- (void)setDoneButtonEnabled:(BOOL)enabled {
  _doneButton.enabled = enabled;
}

- (void)useUndoCloseAll:(BOOL)useUndo {
  _closeAllOrUndoButton.enabled = YES;
  if (useUndo) {
    _closeAllOrUndoButton.title =
        l10n_util::GetNSString(IDS_IOS_TAB_GRID_UNDO_CLOSE_ALL_BUTTON);
    // Setting the `accessibilityIdentifier` seems to trigger layout, which
    // causes an infinite loop.
    if (_closeAllOrUndoButton.accessibilityIdentifier !=
        kTabGridUndoCloseAllButtonIdentifier) {
      _closeAllOrUndoButton.accessibilityIdentifier =
          kTabGridUndoCloseAllButtonIdentifier;
    }
  } else {
    _closeAllOrUndoButton.title =
        l10n_util::GetNSString(IDS_IOS_TAB_GRID_CLOSE_ALL_BUTTON);
    // Setting the `accessibilityIdentifier` seems to trigger layout, which
    // causes an infinite loop.
    if (_closeAllOrUndoButton.accessibilityIdentifier !=
        kTabGridCloseAllButtonIdentifier) {
      _closeAllOrUndoButton.accessibilityIdentifier =
          kTabGridCloseAllButtonIdentifier;
    }
  }
  if (_undoActive != useUndo) {
    _undoActive = useUndo;
    [self setItemsForTraitCollection:self.traitCollection];
  }
}

- (void)configureDeselectAllButtonTitle {
  _selectAllButton.title =
      l10n_util::GetNSString(IDS_IOS_TAB_GRID_DESELECT_ALL_BUTTON);
}

- (void)configureSelectAllButtonTitle {
  _selectAllButton.title =
      l10n_util::GetNSString(IDS_IOS_TAB_GRID_SELECT_ALL_BUTTON);
}

- (void)highlightLastPageControl {
  [self.pageControl highlightLastPageControl];
}

- (void)resetLastPageControlHighlight {
  [self.pageControl resetLastPageControlHighlight];
}

- (void)hide {
  self.backgroundColor = UIColor.blackColor;
  self.pageControl.alpha = 0.0;
}

- (void)show {
  self.backgroundColor = UIColor.clearColor;
  self.pageControl.alpha = 1.0;
}

- (void)setScrollViewScrolledToEdge:(BOOL)scrolledToEdge {
  if (scrolledToEdge == _scrolledToEdge) {
    return;
  }

  _scrolledToEdge = scrolledToEdge;

  _scrolledBackgroundView.hidden = scrolledToEdge;
  [_pageControl setScrollViewScrolledToEdge:scrolledToEdge];
}

#pragma mark Edit Button

- (void)setEditButtonMenu:(UIMenu*)menu {
  _editButton.menu = menu;
}

- (void)setEditButtonEnabled:(BOOL)enabled {
  _editButton.enabled = enabled;
}

#pragma mark - UIView

- (CGSize)intrinsicContentSize {
  return CGSizeMake(UIViewNoIntrinsicMetric, kTabGridTopToolbarHeight);
}

- (void)setBounds:(CGRect)bounds {
  [super setBounds:bounds];
  if (_mode == TabGridMode::kSearch) {
    [self configureSearchModeForTraitCollection:self.traitCollection];
  }
}

- (void)didMoveToSuperview {
  if (_scrolledBackgroundView) {
    [self.superview.topAnchor
        constraintEqualToAnchor:_scrolledBackgroundView.topAnchor]
        .active = YES;
  }
  [super didMoveToSuperview];
}

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];
  [self setItemsForTraitCollection:self.traitCollection];
}

#pragma mark - UIBarPositioningDelegate

// Returns UIBarPositionTopAttached, otherwise the toolbar's translucent
// background won't extend below the status bar.
- (UIBarPosition)positionForBar:(id<UIBarPositioning>)bar {
  return UIBarPositionTopAttached;
}

#pragma mark - Private

- (void)configureSearchModeForTraitCollection:
    (UITraitCollection*)traitCollection {
  DCHECK_EQ(_mode, TabGridMode::kSearch);
  CGFloat widthModifier = 1;

  // In the landscape mode the search bar size should only span half of the
  // width of the toolbar.
  if (![self shouldUseCompactLayout:traitCollection]) {
    widthModifier = kTabGridSearchBarNonCompactWidthRatioModifier;
  }

  CGFloat cancelWidth = [_cancelSearchButton.title sizeWithAttributes:@{
                          NSFontAttributeName : [UIFont
                              preferredFontForTextStyle:UIFontTextStyleBody]
                        }]
                            .width;
  CGFloat barWidth =
      (self.bounds.size.width - kSearchBarTrailingSpace - cancelWidth) *
      kTabGridSearchBarWidthRatio * widthModifier;
  // Update the search bar size based on the container size.
  _searchBar.frame = CGRectMake(0, 0, barWidth, kTabGridSearchBarHeight);
  _searchBarView.frame = CGRectMake(0, 0, barWidth, kTabGridSearchBarHeight);
  [self setNeedsLayout];
  [self setItems:@[ _searchBarItem, _spaceItem, _cancelSearchButton ]
        animated:YES];
}

- (void)setItemsForTraitCollection:(UITraitCollection*)traitCollection {
  if (_mode == TabGridMode::kSearch) {
    [self configureSearchModeForTraitCollection:traitCollection];
    return;
  }
  UIBarButtonItem* centralItem = _pageControlItem;
  UIBarButtonItem* trailingButton = _doneButton;
  _selectionModeFixedSpace.width = 0;
  if ([self shouldUseCompactLayout:traitCollection]) {
    if (_mode == TabGridMode::kNormal) {
      _leadingButton = _searchButton;
    } else {
      _leadingButton = _spaceItem;
    }

    if (_mode == TabGridMode::kSelection) {
      // In the selection mode, Done button is much smaller than SelectAll
      // we need to calculate the difference on the width and use it as a
      // fixed space to make sure that the title is still centered.
      _selectionModeFixedSpace.width = [self selectionModeFixedSpaceWidth];
      [self setItems:@[
        _selectAllButton, _spaceItem, _selectedTabsItem, _spaceItem,
        _selectionModeFixedSpace, trailingButton
      ]];
    } else {
      trailingButton = _spaceItem;
      [self setItems:@[
        _leadingButton, _spaceItem, centralItem, _spaceItem, trailingButton
      ]];
    }
    return;
  }
  // In Landscape normal mode leading button is always "closeAll", or "Edit" if
  // bulk actions feature is enabled.
  if (!_undoActive) {
    _leadingButton = _editButton;
  } else {
    _leadingButton = _closeAllOrUndoButton;
  }

  if (_mode == TabGridMode::kSelection) {
    // In the selection mode, Done button is much smaller than SelectAll
    // we need to calculate the difference on the width and use it as a
    // fixed space to make sure that the title is still centered.
    _selectionModeFixedSpace.width = [self selectionModeFixedSpaceWidth];
    centralItem = _selectedTabsItem;
    _leadingButton = _selectAllButton;
  }

  // Build item list based on priority: tab search takes precedence over thumb
  // strip.

  BOOL animated = NO;
  NSMutableArray* items = [[NSMutableArray alloc] init];

  [items addObject:_leadingButton];

  if (_mode == TabGridMode::kNormal) {
    animated = YES;
    [items
        addObjectsFromArray:@[ _iconButtonAdditionalSpaceItem, _searchButton ]];
  }

  [items addObjectsFromArray:@[ _spaceItem, centralItem, _spaceItem ]];

  if (_mode != TabGridMode::kNormal) {
    [items addObject:_selectionModeFixedSpace];
  }

  [items addObject:trailingButton];

  [self setItems:items animated:animated];
}

// Calculates the space width to use for selection mode.
- (CGFloat)selectionModeFixedSpaceWidth {
  NSDictionary* selectAllFontAttrs = @{
    NSFontAttributeName : [UIFont systemFontOfSize:kSelectionModeButtonSize]
  };
  CGFloat selectAllTextWidth =
      [_selectAllButton.title sizeWithAttributes:selectAllFontAttrs].width;
  NSDictionary* DonefontAttr = @{
    NSFontAttributeName : [UIFont systemFontOfSize:kSelectionModeButtonSize
                                            weight:UIFontWeightSemibold]
  };
  return selectAllTextWidth -
         [_doneButton.title sizeWithAttributes:DonefontAttr].width;
}

- (void)setupViews {
  self.translatesAutoresizingMaskIntoConstraints = NO;
  self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
  [self createScrolledBackgrounds];
  self.delegate = self;
  [self setShadowImage:[[UIImage alloc] init]
      forToolbarPosition:UIBarPositionAny];

  _closeAllOrUndoButton = [[UIBarButtonItem alloc] init];
  _closeAllOrUndoButton.tintColor =
      UIColorFromRGB(kTabGridToolbarTextButtonColor);
  _closeAllOrUndoButton.target = self;
  _closeAllOrUndoButton.action = @selector(closeAllButtonTapped:);
  [self useUndoCloseAll:NO];

  // The segmented control has an intrinsic size.
  _pageControl = [[TabGridPageControl alloc] init];
  _pageControl.translatesAutoresizingMaskIntoConstraints = NO;
  [_pageControl setScrollViewScrolledToEdge:_scrolledToEdge];
  _pageControlItem = [[UIBarButtonItem alloc] initWithCustomView:_pageControl];

  _doneButton = [[UIBarButtonItem alloc] init];
  _doneButton.target = self;
  _doneButton.action = @selector(doneButtonTapped:);
  _doneButton.style = UIBarButtonItemStyleDone;
  _doneButton.tintColor = UIColorFromRGB(kTabGridToolbarTextButtonColor);
  _doneButton.accessibilityIdentifier = kTabGridDoneButtonIdentifier;
  _doneButton.title = l10n_util::GetNSString(IDS_IOS_TAB_GRID_DONE_BUTTON);

  _editButton = [[UIBarButtonItem alloc] init];
  _editButton.tintColor = UIColorFromRGB(kTabGridToolbarTextButtonColor);
  _editButton.title = l10n_util::GetNSString(IDS_IOS_TAB_GRID_EDIT_BUTTON);
  _editButton.accessibilityIdentifier = kTabGridEditButtonIdentifier;

  _selectAllButton = [[UIBarButtonItem alloc] init];
  _selectAllButton.target = self;
  _selectAllButton.action = @selector(selectAllButtonTapped:);
  _selectAllButton.tintColor = UIColorFromRGB(kTabGridToolbarTextButtonColor);
  _selectAllButton.title =
      l10n_util::GetNSString(IDS_IOS_TAB_GRID_SELECT_ALL_BUTTON);
  _selectAllButton.accessibilityIdentifier =
      kTabGridEditSelectAllButtonIdentifier;

  _selectedTabsItem = [[UIBarButtonItem alloc] init];
  _selectedTabsItem.title =
      l10n_util::GetNSString(IDS_IOS_TAB_GRID_SELECT_TABS_TITLE);
  _selectedTabsItem.tintColor = UIColorFromRGB(kTabGridToolbarTextButtonColor);
  _selectedTabsItem.action = nil;
  _selectedTabsItem.target = nil;
  _selectedTabsItem.enabled = NO;
  [_selectedTabsItem setTitleTextAttributes:@{
    NSForegroundColorAttributeName :
        UIColorFromRGB(kTabGridToolbarTextButtonColor),
    NSFontAttributeName :
        [[UIFontMetrics metricsForTextStyle:UIFontTextStyleBody]
            scaledFontForFont:[UIFont systemFontOfSize:kSelectionModeButtonSize
                                                weight:UIFontWeightSemibold]]
  }
                                   forState:UIControlStateDisabled];

  UIImage* searchImage =
      DefaultSymbolWithPointSize(kSearchSymbol, kSymbolSearchImagePointSize);
  _searchButton =
      [[UIBarButtonItem alloc] initWithImage:searchImage
                                       style:UIBarButtonItemStylePlain
                                      target:self
                                      action:@selector(searchButtonTapped:)];

  _searchButton.tintColor = UIColorFromRGB(kTabGridToolbarTextButtonColor);
  _searchButton.accessibilityIdentifier = kTabGridSearchButtonIdentifier;

  _searchBar = [[UISearchBar alloc] init];
  _searchBar.placeholder =
      l10n_util::GetNSString(IDS_IOS_TAB_GRID_SEARCHBAR_PLACEHOLDER);
  _searchBar.accessibilityIdentifier = kTabGridSearchBarIdentifier;
  // Cancel Button for the searchbar doesn't appear in ipadOS. Disable it and
  // create a custom cancel button.
  _searchBar.showsCancelButton = NO;
  _cancelSearchButton = [[UIBarButtonItem alloc] init];
  _cancelSearchButton.target = self;
  _cancelSearchButton.action = @selector(cancelSearchButtonTapped:);
  _cancelSearchButton.style = UIBarButtonItemStylePlain;
  _cancelSearchButton.tintColor =
      UIColorFromRGB(kTabGridToolbarTextButtonColor);
  _cancelSearchButton.accessibilityIdentifier = kTabGridCancelButtonIdentifier;
  _cancelSearchButton.title =
      l10n_util::GetNSString(IDS_IOS_TAB_GRID_CANCEL_BUTTON);
  _searchBarView = [[UIView alloc] initWithFrame:_searchBar.frame];
  [_searchBarView addSubview:_searchBar];
  [_searchBarView sizeToFit];
  _searchBarItem = [[UIBarButtonItem alloc] initWithCustomView:_searchBarView];

  _iconButtonAdditionalSpaceItem = [[UIBarButtonItem alloc]
      initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace
                           target:nil
                           action:nil];
  _iconButtonAdditionalSpaceItem.width = kIconButtonAdditionalSpace;

  _spaceItem = [[UIBarButtonItem alloc]
      initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
                           target:nil
                           action:nil];

  _selectionModeFixedSpace = [[UIBarButtonItem alloc]
      initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace
                           target:nil
                           action:nil];

  [self setItemsForTraitCollection:self.traitCollection];
}

// Creates and configures the two background for the scrolled in the
// middle/scrolled to the top states.
- (void)createScrolledBackgrounds {
  _scrolledToEdge = YES;

  // Background when the content is scrolled to the middle.
  _scrolledBackgroundView = CreateTabGridOverContentBackground();
  _scrolledBackgroundView.hidden = YES;
  _scrolledBackgroundView.translatesAutoresizingMaskIntoConstraints = NO;
  [self addSubview:_scrolledBackgroundView];
  AddSameConstraintsToSides(
      self, _scrolledBackgroundView,
      LayoutSides::kLeading | LayoutSides::kBottom | LayoutSides::kTrailing);

  // A non-nil UIImage has to be added in the background of the toolbar to avoid
  // having an additional blur effect.
  [self setBackgroundImage:[[UIImage alloc] init]
        forToolbarPosition:UIBarPositionAny
                barMetrics:UIBarMetricsDefault];
}

// Returns YES if should use compact bottom toolbar layout.
- (BOOL)shouldUseCompactLayout:(UITraitCollection*)traitCollection {
  return traitCollection.verticalSizeClass == UIUserInterfaceSizeClassRegular &&
         traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact;
}

#pragma mark - Public

- (void)unfocusSearchBar {
  [_searchBar resignFirstResponder];
}

- (void)respondBeforeResponder:(UIResponder*)nextResponder {
  _followingNextResponder = nextResponder;
}

#pragma mark - UIResponder

- (NSArray<UIKeyCommand*>*)keyCommands {
  return @[ UIKeyCommand.cr_undo ];
}

- (UIResponder*)nextResponder {
  return _followingNextResponder;
}

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
  if (sel_isEqual(action, @selector(keyCommand_closeAll))) {
    return !_undoActive && _closeAllOrUndoButton.enabled;
  }
  if (sel_isEqual(action, @selector(keyCommand_undo))) {
    return _undoActive;
  }
  if (sel_isEqual(action, @selector(keyCommand_close))) {
    return _doneButton.enabled || _mode == TabGridMode::kSearch;
  }
  if (sel_isEqual(action, @selector(keyCommand_find))) {
    return _searchButton.enabled;
  }
  return [super canPerformAction:action withSender:sender];
}

- (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.
  // TODO(crbug.com/40273478): This should be separated to avoid confusion.
  [self closeAllButtonTapped:nil];
}

- (void)keyCommand_close {
  base::RecordAction(base::UserMetricsAction("MobileKeyCommandClose"));
  if (_mode == TabGridMode::kSearch) {
    [self cancelSearchButtonTapped:nil];
  } else {
    [self doneButtonTapped:nil];
  }
}

- (void)keyCommand_find {
  base::RecordAction(base::UserMetricsAction("MobileKeyCommandSearchTabs"));
  [self searchButtonTapped:nil];
}

#pragma mark - Control actions

- (void)closeAllButtonTapped:(id)sender {
  if (_closeAllOrUndoButton.enabled) {
    [self.buttonsDelegate closeAllButtonTapped:sender];
  }
}

- (void)doneButtonTapped:(id)sender {
  if (_doneButton.enabled) {
    [self.buttonsDelegate doneButtonTapped:sender];
  }
}

- (void)selectAllButtonTapped:(id)sender {
  if (_selectAllButton.enabled) {
    [self.buttonsDelegate selectAllButtonTapped:sender];
  }
}

- (void)searchButtonTapped:(id)sender {
  if (_searchButton.enabled) {
    [self.buttonsDelegate searchButtonTapped:sender];
  }
}

- (void)cancelSearchButtonTapped:(id)sender {
  if (_cancelSearchButton.enabled) {
    [self.buttonsDelegate cancelSearchButtonTapped:sender];
  }
}

@end