chromium/ios/chrome/browser/ui/tab_switcher/tab_grid/tab_context_menu/tab_context_menu_helper.mm

// Copyright 2021 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_context_menu/tab_context_menu_helper.h"

#import "base/check.h"
#import "base/metrics/histogram_functions.h"
#import "components/bookmarks/browser/bookmark_model.h"
#import "components/bookmarks/common/bookmark_pref_names.h"
#import "components/prefs/pref_service.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_model_factory.h"
#import "ios/chrome/browser/ntp/model/new_tab_page_util.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_list.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_factory.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/tab_group.h"
#import "ios/chrome/browser/shared/model/web_state_list/tab_group_utils.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/tabs/model/tab_title_util.h"
#import "ios/chrome/browser/ui/menu/action_factory.h"
#import "ios/chrome/browser/ui/menu/tab_context_menu_delegate.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_item_identifier.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_context_menu/tab_cell.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_context_menu/tab_item.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_group_item.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_switcher_item.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_utils.h"
#import "ios/web/public/web_state.h"

using PinnedState = WebStateSearchCriteria::PinnedState;

@interface TabContextMenuHelper ()

@property(nonatomic, weak) id<TabContextMenuDelegate> contextMenuDelegate;
@property(nonatomic, assign) BOOL incognito;

@end

@implementation TabContextMenuHelper

#pragma mark - TabContextMenuProvider

- (instancetype)initWithBrowserState:(ChromeBrowserState*)browserState
              tabContextMenuDelegate:
                  (id<TabContextMenuDelegate>)tabContextMenuDelegate {
  self = [super init];
  if (self) {
    _browserState = browserState;
    _contextMenuDelegate = tabContextMenuDelegate;
    _incognito = _browserState->IsOffTheRecord();
  }
  return self;
}

- (UIContextMenuConfiguration*)
    contextMenuConfigurationForTabCell:(TabCell*)cell
                          menuScenario:(MenuScenarioHistogram)scenario {
  __weak __typeof(self) weakSelf = self;

  UIContextMenuActionProvider actionProvider =
      ^(NSArray<UIMenuElement*>* suggestedActions) {
        TabContextMenuHelper* strongSelf = weakSelf;
        if (!strongSelf) {
          // Return an empty menu.
          return [UIMenu menuWithTitle:@"" children:@[]];
        }

        NSArray<UIMenuElement*>* menuElements =
            [strongSelf menuElementsForTabCell:cell menuScenario:scenario];
        return [UIMenu menuWithTitle:@"" children:menuElements];
      };

  return
      [UIContextMenuConfiguration configurationWithIdentifier:nil
                                              previewProvider:nil
                                               actionProvider:actionProvider];
}

- (UIContextMenuConfiguration*)
    contextMenuConfigurationForTabGroupCell:(TabCell*)cell
                               menuScenario:(MenuScenarioHistogram)scenario {
  __weak __typeof(self) weakSelf = self;

  UIContextMenuActionProvider actionProvider =
      ^(NSArray<UIMenuElement*>* suggestedActions) {
        TabContextMenuHelper* strongSelf = weakSelf;
        if (!strongSelf) {
          // Return an empty menu.
          return [UIMenu menuWithTitle:@"" children:@[]];
        }

        NSArray<UIMenuElement*>* menuElements =
            [strongSelf menuElementsForTabGroupCell:cell menuScenario:scenario];
        return [UIMenu menuWithTitle:@"" children:menuElements];
      };

  return
      [UIContextMenuConfiguration configurationWithIdentifier:nil
                                              previewProvider:nil
                                               actionProvider:actionProvider];
}

- (NSArray<UIMenuElement*>*)menuElementsForTabCell:(TabCell*)cell
                                      menuScenario:
                                          (MenuScenarioHistogram)scenario {
  CHECK(cell.itemIdentifier.type == GridItemType::kTab);
  // Record that this context menu was shown to the user.
  RecordMenuShown(scenario);

  web::WebStateID tabID = cell.itemIdentifier.tabSwitcherItem.identifier;

  __weak __typeof(self) weakSelf = self;

  ActionFactory* actionFactory =
      [[ActionFactory alloc] initWithScenario:scenario];
  const BOOL pinned = IsPinnedTabsEnabled() &&
                      [self isTabPinnedForIdentifier:cell.itemIdentifier];
  const BOOL tabSearchScenario =
      scenario == kMenuScenarioHistogramTabGridSearchResult;
  const BOOL inactive = scenario == kMenuScenarioHistogramInactiveTabsEntry;

  TabItem* item = [self tabItemForIdentifier:tabID];

  if (!item) {
    return @[];
  }

  UIMenuElement* pinAction;
  UIMenuElement* shareAction;
  UIMenuElement* addToReadingListAction;
  UIAction* bookmarkAction;
  UIMenuElement* selectAction;
  UIAction* closeTabAction;

  const BOOL isPinActionEnabled = IsPinnedTabsEnabled() && !self.incognito &&
                                  !inactive && !tabSearchScenario;
  if (isPinActionEnabled) {
    if (pinned) {
      pinAction = [actionFactory actionToUnpinTabWithBlock:^{
        [self.contextMenuDelegate unpinTabWithIdentifier:tabID];
      }];
    } else {
      pinAction = [actionFactory actionToPinTabWithBlock:^{
        [self.contextMenuDelegate pinTabWithIdentifier:tabID];
      }];
    }
  }

  if (!IsURLNewTabPage(item.URL)) {
    shareAction = [actionFactory actionToShareWithBlock:^{
      [self.contextMenuDelegate shareURL:item.URL
                                   title:item.title
                                scenario:SharingScenario::TabGridItem
                                fromView:cell];
    }];

    if (item.URL.SchemeIsHTTPOrHTTPS()) {
      addToReadingListAction =
          [actionFactory actionToAddToReadingListWithBlock:^{
            [self.contextMenuDelegate addToReadingListURL:item.URL
                                                    title:item.title];
          }];
    }

    if (_browserState) {
      const BOOL currentlyBookmarked = [self isTabItemBookmarked:item];
      if (currentlyBookmarked) {
        bookmarkAction = [actionFactory actionToEditBookmarkWithBlock:^{
          [self.contextMenuDelegate editBookmarkWithURL:item.URL];
        }];
      } else {
        bookmarkAction = [actionFactory actionToBookmarkWithBlock:^{
          [self.contextMenuDelegate bookmarkURL:item.URL title:item.title];
        }];
      }
      // Bookmarking can be disabled from prefs (from an enterprise policy),
      // if that's the case grey out the option in the menu.
      BOOL isEditBookmarksEnabled = _browserState->GetPrefs()->GetBoolean(
          bookmarks::prefs::kEditBookmarksEnabled);
      if (!isEditBookmarksEnabled && bookmarkAction) {
        bookmarkAction.attributes = UIMenuElementAttributesDisabled;
      }
    }
  }

  // Pinned tabs, inactive tabs and search results menus don't
  // support tab selection.
  BOOL scenarioDisablesSelection =
      scenario == kMenuScenarioHistogramTabGridSearchResult ||
      scenario == kMenuScenarioHistogramPinnedTabsEntry ||
      scenario == kMenuScenarioHistogramInactiveTabsEntry;
  if (!scenarioDisablesSelection) {
    selectAction = [actionFactory actionToSelectTabsWithBlock:^{
      [self.contextMenuDelegate selectTabs];
    }];
  }

  ProceduralBlock closeTabActionBlock = ^{
    [self.contextMenuDelegate closeTabWithIdentifier:tabID
                                           incognito:self.incognito];
  };

  if (IsPinnedTabsEnabled() && !self.incognito && pinned) {
    closeTabAction =
        [actionFactory actionToClosePinnedTabWithBlock:closeTabActionBlock];
  } else {
    closeTabAction =
        [actionFactory actionToCloseRegularTabWithBlock:closeTabActionBlock];
  }

  NSMutableArray<UIMenuElement*>* menuElements = [[NSMutableArray alloc] init];

  if (IsTabGroupInGridEnabled()) {
    std::set<const TabGroup*> groups =
        GetAllGroupsForBrowserState(_browserState);

    auto actionResult = ^(const TabGroup* group) {
      [weakSelf handleAddWebState:tabID toGroup:group];
    };

    const TabGroup* currentTabGroup = [self groupForWebState:tabID];
    UIMenuElement* groupAction;
    if (currentTabGroup) {
      ProceduralBlock removeBlock = ^{
        [weakSelf handleRemoveWebStateFromGroup:tabID];
      };
      groupAction =
          [actionFactory menuToMoveTabToGroupWithGroups:groups
                                           currentGroup:currentTabGroup
                                              moveBlock:actionResult
                                            removeBlock:removeBlock];
    } else {
      groupAction = [actionFactory menuToAddTabToGroupWithGroups:groups
                                                    numberOfTabs:1
                                                           block:actionResult];
    }

    // Hide the `shareAction` for tabs in groups.
    if (shareAction && !currentTabGroup) {
      UIMenu* shareMenu = [UIMenu menuWithTitle:@""
                                          image:nil
                                     identifier:nil
                                        options:UIMenuOptionsDisplayInline
                                       children:@[ shareAction ]];
      [menuElements addObject:shareMenu];
    }
    NSArray<UIMenuElement*>* tabActions =
        pinAction ? @[ pinAction, groupAction ] : @[ groupAction ];
    UIMenu* tabMenu = [UIMenu menuWithTitle:@""
                                      image:nil
                                 identifier:nil
                                    options:UIMenuOptionsDisplayInline
                                   children:tabActions];
    [menuElements addObject:tabMenu];

    NSMutableArray<UIMenuElement*>* collectionsActions = [NSMutableArray array];
    if (addToReadingListAction) {
      [collectionsActions addObject:addToReadingListAction];
    }
    if (bookmarkAction) {
      [collectionsActions addObject:bookmarkAction];
    }
    // Hide the `selectAction` for tabs in groups.
    if (selectAction && !currentTabGroup) {
      [collectionsActions addObject:selectAction];
    }
    if (closeTabAction) {
      [collectionsActions addObject:closeTabAction];
    }

    if (collectionsActions.count > 0) {
      UIMenu* collectionsMenu = [UIMenu menuWithTitle:@""
                                                image:nil
                                           identifier:nil
                                              options:UIMenuOptionsDisplayInline
                                             children:collectionsActions];
      [menuElements addObject:collectionsMenu];
    }

  } else {
    if (pinAction) {
      [menuElements addObject:pinAction];
    }
    if (shareAction) {
      [menuElements addObject:shareAction];
    }
    if (addToReadingListAction) {
      [menuElements addObject:addToReadingListAction];
    }
    if (bookmarkAction) {
      [menuElements addObject:bookmarkAction];
    }
    if (selectAction) {
      [menuElements addObject:selectAction];
    }
    if (closeTabAction) {
      [menuElements addObject:closeTabAction];
    }
  }

  return menuElements;
}

- (NSArray<UIMenuElement*>*)menuElementsForTabGroupCell:(TabCell*)cell
                                           menuScenario:
                                               (MenuScenarioHistogram)scenario {
  CHECK(cell.itemIdentifier.type == GridItemType::kGroup);
  // Record that this context menu was shown to the user.
  RecordMenuShown(scenario);

  ActionFactory* actionFactory =
      [[ActionFactory alloc] initWithScenario:scenario];

  const TabGroup* group = cell.itemIdentifier.tabGroupItem.tabGroup;
  base::WeakPtr<const TabGroup> weakGroup = group->GetWeakPtr();
  BOOL incognito = self.incognito;
  CHECK(group);
  __weak __typeof(self) weakSelf = self;

  NSMutableArray<UIMenuElement*>* menuElements = [[NSMutableArray alloc] init];

  [menuElements addObject:[actionFactory actionToRenameTabGroupWithBlock:^{
                  [weakSelf.contextMenuDelegate editTabGroup:weakGroup
                                                   incognito:incognito];
                }]];
  [menuElements addObject:[actionFactory actionToUngroupTabGroupWithBlock:^{
                  [weakSelf.contextMenuDelegate ungroupTabGroup:weakGroup
                                                      incognito:incognito
                                                     sourceView:cell];
                }]];

  if (IsTabGroupSyncEnabled()) {
    [menuElements addObject:[actionFactory actionToCloseTabGroupWithBlock:^{
                    [weakSelf.contextMenuDelegate closeTabGroup:weakGroup
                                                      incognito:incognito];
                  }]];
    if (!incognito) {
      [menuElements addObject:[actionFactory actionToDeleteTabGroupWithBlock:^{
                      [weakSelf.contextMenuDelegate deleteTabGroup:weakGroup
                                                         incognito:incognito
                                                        sourceView:cell];
                    }]];
    }
  } else {
    [menuElements addObject:[actionFactory actionToDeleteTabGroupWithBlock:^{
                    [weakSelf.contextMenuDelegate deleteTabGroup:weakGroup
                                                       incognito:incognito
                                                      sourceView:cell];
                  }]];
  }

  return menuElements;
}

#pragma mark - Private

// Returns `YES` if the tab `item` is already bookmarked.
- (BOOL)isTabItemBookmarked:(TabItem*)item {
  bookmarks::BookmarkModel* bookmarkModel =
      ios::BookmarkModelFactory::GetForBrowserState(_browserState);
  return item && bookmarkModel->IsBookmarked(item.URL);
}

// Returns `YES` if the tab for the given `identifier` is pinned.
- (BOOL)isTabPinnedForIdentifier:(GridItemIdentifier*)identifier {
  if (!identifier || (identifier.type != GridItemType::kTab)) {
    return NO;
  }

  BrowserList* browserList =
      BrowserListFactory::GetForBrowserState(_browserState);

  for (Browser* browser :
       browserList->BrowsersOfType(BrowserList::BrowserType::kRegular)) {
    WebStateList* webStateList = browser->GetWebStateList();
    web::WebState* webState = GetWebState(
        webStateList, WebStateSearchCriteria{
                          .identifier = identifier.tabSwitcherItem.identifier,
                          .pinned_state = PinnedState::kPinned,
                      });
    if (webState) {
      return YES;
    }
  }
  return NO;
}

// Returns the TabItem object representing the tab with `identifier.
- (TabItem*)tabItemForIdentifier:(web::WebStateID)identifier {
  BrowserList* browserList =
      BrowserListFactory::GetForBrowserState(_browserState);
  const BrowserList::BrowserType browser_types =
      _incognito ? BrowserList::BrowserType::kIncognito
                 : BrowserList::BrowserType::kRegularAndInactive;
  std::set<Browser*> browsers = browserList->BrowsersOfType(browser_types);
  for (Browser* browser : browsers) {
    WebStateList* webStateList = browser->GetWebStateList();
    TabItem* item = GetTabItem(
        webStateList, WebStateSearchCriteria{.identifier = identifier});
    if (item != nil) {
      return item;
    }
  }
  return nil;
}

// Handles the result of the add to group block.
- (void)handleAddWebState:(web::WebStateID)webStateID
                  toGroup:(const TabGroup*)group {
  if (group == nullptr) {
    [self.contextMenuDelegate createNewTabGroupWithIdentifier:webStateID
                                                    incognito:self.incognito];
  } else {
    MoveTabToGroup(webStateID, group, _browserState);
  }
}

// Handles the result of the remove from group block.
- (void)handleRemoveWebStateFromGroup:(web::WebStateID)webStateID {
  BrowserList* browserList =
      BrowserListFactory::GetForBrowserState(_browserState);

  for (Browser* browser :
       browserList->BrowsersOfType(BrowserList::BrowserType::kRegular)) {
    WebStateList* webStateList = browser->GetWebStateList();
    int index = GetWebStateIndex(
        webStateList,
        WebStateSearchCriteria{.identifier = webStateID,
                               .pinned_state = PinnedState::kNonPinned});
    if (index != WebStateList::kInvalidIndex) {
      webStateList->RemoveFromGroups({index});
      return;
    }
  }
}

// Returns the group of the given `webStateID`.
- (const TabGroup*)groupForWebState:(web::WebStateID)webStateID {
  BrowserList* browserList =
      BrowserListFactory::GetForBrowserState(_browserState);

  for (Browser* browser :
       browserList->BrowsersOfType(BrowserList::BrowserType::kRegular)) {
    WebStateList* webStateList = browser->GetWebStateList();
    int index = GetWebStateIndex(
        webStateList,
        WebStateSearchCriteria{.identifier = webStateID,
                               .pinned_state = PinnedState::kNonPinned});
    if (webStateList->ContainsIndex(index)) {
      return webStateList->GetGroupOfWebStateAt(index);
    }
  }
  return nil;
}

@end