// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <MaterialComponents/MaterialSnackbar.h>
#import "base/check.h"
#import "base/strings/sys_string_conversions.h"
#import "components/feature_engagement/public/feature_constants.h"
#import "components/feature_engagement/public/tracker.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/feature_engagement/model/tracker_factory.h"
#import "ios/chrome/browser/shared/model/browser/browser.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/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/snackbar_commands.h"
#import "ios/chrome/browser/shared/public/commands/tab_grid_commands.h"
#import "ios/chrome/browser/shared/public/commands/tab_grid_toolbar_commands.h"
#import "ios/chrome/browser/shared/public/commands/tab_group_confirmation_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/util/snackbar_util.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/base_grid_coordinator+subclassing.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/base_grid_mediator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/base_grid_view_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_container_view_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_item_identifier.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/tab_group_grid_view_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/create_tab_group_coordinator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/tab_group_coordinator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/tab_group_view_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/transitions/legacy_grid_transition_layout.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_group_action_type.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_group_confirmation_coordinator.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/web_state.h"
#import "ui/base/l10n/l10n_util.h"
@implementation BaseGridCoordinator {
// Mutator that handle toolbars changes.
__weak id<GridToolbarsMutator> _toolbarsMutator;
// Delegate to handle presenting the action sheet.
__weak id<GridMediatorDelegate> _gridMediatorDelegate;
// Tab Groups Coordinator used to display the tab group UI;
TabGroupCoordinator* _tabGroupCoordinator;
// Handles the creation of a new tab group.
CreateTabGroupCoordinator* _tabGroupCreator;
// The coordinator to handle the confirmation dialog for the action taken for
// a tab group.
TabGroupConfirmationCoordinator* _tabGroupConfirmationCoordinator;
}
#pragma mark - Public
- (instancetype)initWithBaseViewController:(UIViewController*)baseViewController
browser:(Browser*)browser
toolbarsMutator:
(id<GridToolbarsMutator>)toolbarsMutator
gridMediatorDelegate:(id<GridMediatorDelegate>)delegate {
CHECK(baseViewController);
CHECK(browser);
if ((self = [super initWithBaseViewController:baseViewController
browser:browser])) {
CHECK(toolbarsMutator);
CHECK(delegate);
_toolbarsMutator = toolbarsMutator;
_gridMediatorDelegate = delegate;
}
return self;
}
- (BaseGridMediator*)mediator {
NOTREACHED() << "This should be implemented in subclasses.";
}
- (BaseGridViewController*)gridViewController {
NOTREACHED() << "This should be implemented in subclasses.";
}
- (void)showTabGroupForTabGridOpening:(const TabGroup*)tabGroup {
[self showTabGroup:tabGroup forTabGridOpening:YES];
}
- (BOOL)bringTabGroupIntoViewIfPresent:(const TabGroup*)tabGroup
animated:(BOOL)animated {
WebStateList* webStateList = self.browser->GetWebStateList();
if (!webStateList->ContainsGroup(tabGroup)) {
return NO;
}
GridItemIdentifier* groupIdentifier =
[GridItemIdentifier groupIdentifier:tabGroup
withWebStateList:webStateList];
[self.gridViewController bringItemIntoView:groupIdentifier animated:animated];
return YES;
}
- (LegacyGridTransitionLayout*)transitionLayout {
NOTREACHED() << "This should be implemented in subclasses.";
}
- (BOOL)isSelectedCellVisible {
if (IsTabGroupInGridEnabled()) {
if (_tabGroupCoordinator) {
return _tabGroupCoordinator.viewController.gridViewController
.selectedCellVisible;
}
}
return self.gridViewController.selectedCellVisible;
}
- (UIView*)gridView {
if (IsTabGroupInGridEnabled()) {
if (_tabGroupCoordinator) {
return _tabGroupCoordinator.viewController.gridViewController.view;
}
}
return self.gridContainerViewController.view;
}
- (UIView*)gridContainerForAnimation {
if (IsTabGroupInGridEnabled()) {
if (_tabGroupCoordinator) {
return _tabGroupCoordinator.viewController.gridViewController.view;
}
}
return nil;
}
- (void)stopChildCoordinators {
if (_tabGroupConfirmationCoordinator) {
[_tabGroupConfirmationCoordinator stop];
_tabGroupConfirmationCoordinator = nil;
}
[self hideTabGroupCreationAnimated:NO];
[self.tabGroupCoordinator stopChildCoordinators];
[self.gridViewController dismissModals];
}
#pragma mark - Subclassing properties
- (id<GridToolbarsMutator>)toolbarsMutator {
return _toolbarsMutator;
}
- (id<GridMediatorDelegate>)gridMediatorDelegate {
return _gridMediatorDelegate;
}
- (TabGroupCoordinator*)tabGroupCoordinator {
return _tabGroupCoordinator;
}
- (LegacyGridTransitionLayout*)
combineTransitionLayout:(LegacyGridTransitionLayout*)primaryLayout
withTransitionLayout:(LegacyGridTransitionLayout*)secondaryLayout {
NSArray<LegacyGridTransitionItem*>* primaryInactiveItems =
primaryLayout.inactiveItems;
NSArray<LegacyGridTransitionItem*>* secondaryInactiveItems =
secondaryLayout.inactiveItems;
NSArray<LegacyGridTransitionItem*>* inactiveItems =
[self combineInactiveItems:primaryInactiveItems
withInactiveItems:secondaryInactiveItems];
LegacyGridTransitionActiveItem* primaryActiveItem = primaryLayout.activeItem;
LegacyGridTransitionActiveItem* secondaryActiveItem =
secondaryLayout.activeItem;
// Prefer primary active item.
LegacyGridTransitionActiveItem* activeItem =
primaryActiveItem ? primaryActiveItem : secondaryActiveItem;
LegacyGridTransitionItem* primarySelectionItem = primaryLayout.selectionItem;
LegacyGridTransitionItem* secondarySelectionItem =
secondaryLayout.selectionItem;
// Prefer primary selection item.
LegacyGridTransitionItem* selectionItem =
primarySelectionItem ? primarySelectionItem : secondarySelectionItem;
return [LegacyGridTransitionLayout layoutWithInactiveItems:inactiveItems
activeItem:activeItem
selectionItem:selectionItem];
}
#pragma mark - ChromeCoordinator
- (void)start {
CommandDispatcher* dispatcher = self.browser->GetCommandDispatcher();
[dispatcher startDispatchingToTarget:self
forProtocol:@protocol(TabGroupsCommands)];
self.mediator.tabGroupsHandler = self;
if (!self.browser->GetBrowserState()->IsOffTheRecord()) {
self.mediator.tabGridToolbarHandler =
HandlerForProtocol(dispatcher, TabGridToolbarCommands);
}
self.mediator.browser = self.browser;
self.mediator.delegate = self.gridMediatorDelegate;
self.mediator.toolbarsMutator = self.toolbarsMutator;
self.mediator.tabGridHandler =
HandlerForProtocol(self.browser->GetCommandDispatcher(), TabGridCommands);
self.gridViewController.tabGridHandler =
HandlerForProtocol(dispatcher, TabGridCommands);
}
- (void)stop {
[self.browser->GetCommandDispatcher() stopDispatchingToTarget:self];
if (_tabGroupCoordinator) {
[self hideTabGroup];
}
[self.mediator disconnect];
}
#pragma mark - TabGroupsCommands
- (void)showTabGroup:(const TabGroup*)tabGroup {
if (_tabGroupCoordinator) {
[self hideTabGroup];
}
// When entering the tab group, disable scrolls-to-top gesture for the
// view controller that is going to stay behind the screen being presented.
self.gridViewController.gridScrollsToTopEnabled = NO;
[self showTabGroup:tabGroup forTabGridOpening:NO];
}
- (void)hideTabGroup {
// When the tab group is hidden, re-enable the scrolls-to-top gesture on the
// regular grid view controller.
self.gridViewController.gridScrollsToTopEnabled = YES;
[_tabGroupCoordinator stop];
_tabGroupCoordinator = nil;
}
- (void)showTabGroupCreationForTabs:
(const std::set<web::WebStateID>&)identifiers {
CHECK(IsTabGroupInGridEnabled())
<< "You should not be able to create a tab group outside the Tab Groups "
"experiment.";
CHECK(!_tabGroupCreator) << "There is an atemps to create a tab group when a "
"creation process is still running.";
_tabGroupCreator = [[CreateTabGroupCoordinator alloc]
initTabGroupCreationWithBaseViewController:self.baseViewController
browser:self.browser
selectedTabs:identifiers];
_tabGroupCreator.delegate = self;
[_tabGroupCreator start];
}
- (void)hideTabGroupCreationAnimated:(BOOL)animated {
_tabGroupCreator.animatedDismissal = animated;
_tabGroupCreator.delegate = nil;
[_tabGroupCreator stop];
_tabGroupCreator = nil;
}
- (void)showTabGroupEditionForGroup:(const TabGroup*)tabGroup {
CHECK(IsTabGroupInGridEnabled())
<< "You should not be able to edit a tab group outside the Tab Groups "
"experiment.";
CHECK(!_tabGroupCreator) << "There is an attempt to edit a tab group when a "
"creation process is still running.";
CHECK(tabGroup) << "To edit a tab group you should pass a group.";
UIViewController* backgroundView = _tabGroupCoordinator
? _tabGroupCoordinator.viewController
: self.baseViewController;
_tabGroupCreator = [[CreateTabGroupCoordinator alloc]
initTabGroupEditionWithBaseViewController:backgroundView
browser:self.browser
tabGroup:tabGroup];
_tabGroupCreator.delegate = self;
[_tabGroupCreator start];
}
- (void)showActiveTab {
[self.mediator displayActiveTab];
}
- (void)showTabGroupConfirmationForAction:(TabGroupActionType)actionType
group:
(base::WeakPtr<const TabGroup>)tabGroup
sourceView:(UIView*)sourceView {
_tabGroupConfirmationCoordinator = [[TabGroupConfirmationCoordinator alloc]
initWithBaseViewController:self.baseViewController
browser:self.browser
actionType:actionType
sourceView:sourceView];
__weak BaseGridCoordinator* weakSelf = self;
_tabGroupConfirmationCoordinator.action = ^{
[weakSelf takeActionForActionType:actionType weakGroup:tabGroup];
};
[_tabGroupConfirmationCoordinator start];
self.gridViewController.tabGroupConfirmationHandler =
_tabGroupConfirmationCoordinator;
}
- (void)showTabGroupConfirmationForAction:(TabGroupActionType)actionType
group:
(base::WeakPtr<const TabGroup>)tabGroup
sourceButtonItem:(UIBarButtonItem*)sourceButtonItem {
_tabGroupConfirmationCoordinator = [[TabGroupConfirmationCoordinator alloc]
initWithBaseViewController:self.baseViewController
browser:self.browser
actionType:actionType
sourceButtonItem:sourceButtonItem];
__weak BaseGridCoordinator* weakSelf = self;
_tabGroupConfirmationCoordinator.action = ^{
[weakSelf takeActionForActionType:actionType weakGroup:tabGroup];
};
[_tabGroupConfirmationCoordinator start];
self.gridViewController.tabGroupConfirmationHandler =
_tabGroupConfirmationCoordinator;
}
- (void)showTabGridTabGroupSnackbarAfterClosingGroups:
(int)numberOfClosedGroups {
if (!IsTabGroupSyncEnabled() ||
self.browser->GetBrowserState()->IsOffTheRecord()) {
return;
}
// Don't show the snackbar if the IPH will be presented.
feature_engagement::Tracker* tracker =
feature_engagement::TrackerFactory::GetForBrowserState(
self.browser->GetBrowserState());
if (tracker->WouldTriggerHelpUI(
feature_engagement::kIPHiOSSavedTabGroupClosed)) {
return;
}
// Create the "Open Tab Groups" action.
CommandDispatcher* dispatcher = self.browser->GetCommandDispatcher();
__weak id<TabGridCommands> tabGridHandler =
HandlerForProtocol(dispatcher, TabGridCommands);
void (^openTabGroupPanelAction)() = ^{
[tabGridHandler showTabGroupsPanelAnimated:YES];
};
// Create and config the snackbar.
NSString* messageLabel =
base::SysUTF16ToNSString(l10n_util::GetPluralStringFUTF16(
IDS_IOS_TAB_GROUP_SNACKBAR_LABEL, numberOfClosedGroups));
MDCSnackbarMessage* message = CreateSnackbarMessage(messageLabel);
MDCSnackbarMessageAction* action = [[MDCSnackbarMessageAction alloc] init];
action.handler = openTabGroupPanelAction;
action.title = l10n_util::GetNSString(IDS_IOS_TAB_GROUP_SNACKBAR_ACTION);
message.action = action;
id<SnackbarCommands> snackbarCommandsHandler =
HandlerForProtocol(dispatcher, SnackbarCommands);
[snackbarCommandsHandler showSnackbarMessage:message];
}
#pragma mark - CreateOrEditTabGroupCoordinatorDelegate
- (void)createOrEditTabGroupCoordinatorDidDismiss:
(CreateTabGroupCoordinator*)coordinator
animated:(BOOL)animated {
CHECK(coordinator == _tabGroupCreator);
[self hideTabGroupCreationAnimated:animated];
}
#pragma mark - Private
// Shows the `tabGroup` with animations for `tabGridOpening` or not.
- (void)showTabGroup:(const TabGroup*)tabGroup
forTabGridOpening:(BOOL)tabGridOpening {
CHECK(IsTabGroupInGridEnabled())
<< "You should not be able to show a tab group UI outside the "
"Tab Groups experiment.";
if (_tabGroupCoordinator) {
// There is an attempt to display a tab group when one is already presented.
return;
}
// TODO(crbug.com/40942154): Replace base view controller by view controller
// when the base grid coordinator will have access to the grid view
// controller.
_tabGroupCoordinator = [[TabGroupCoordinator alloc]
initWithBaseViewController:self.baseViewController
browser:self.browser
tabGroup:tabGroup];
_tabGroupCoordinator.tabContextMenuDelegate = self.tabContextMenuDelegate;
_tabGroupCoordinator.animatedPresentation = !tabGridOpening;
_tabGroupCoordinator.tabGroupPositioner = self.tabGroupPositioner;
_tabGroupCoordinator.tabGridIdleStatusHandler =
self.mediator.tabGridIdleStatusHandler;
_tabGroupCoordinator.modeHolder = self.modeHolder;
[_tabGroupCoordinator start];
}
// 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<LegacyGridTransitionItem*>*)
combineInactiveItems:
(NSArray<LegacyGridTransitionItem*>*)primaryInactiveItems
withInactiveItems:
(NSArray<LegacyGridTransitionItem*>*)secondaryInactiveItems {
if (primaryInactiveItems == nil) {
primaryInactiveItems = @[];
}
return [primaryInactiveItems
arrayByAddingObjectsFromArray:secondaryInactiveItems];
}
// Helper method to execute a corresponded action to `actionType` and dismiss
// the confirmation coordinator.
- (void)takeActionForActionType:(TabGroupActionType)actionType
weakGroup:(base::WeakPtr<const TabGroup>)weakGroup {
switch (actionType) {
case TabGroupActionType::kUngroupTabGroup:
if (weakGroup) {
[self.mediator ungroupTabGroup:weakGroup.get()];
}
break;
case TabGroupActionType::kDeleteTabGroup:
if (weakGroup) {
[self.mediator closeTabGroup:weakGroup.get() andDeleteGroup:YES];
}
break;
}
if (_tabGroupCoordinator) {
[self hideTabGroup];
}
[_tabGroupConfirmationCoordinator stop];
_tabGroupConfirmationCoordinator = nil;
}
@end