// Copyright 2022 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/bookmarks/ui_bundled/editor/bookmarks_editor_coordinator.h"
#import <MaterialComponents/MaterialSnackbar.h>
#import "base/memory/raw_ptr.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/strings/sys_string_conversions.h"
#import "components/bookmarks/browser/bookmark_node.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_model_factory.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/bookmark_utils_ios.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/editor/bookmarks_editor_coordinator_delegate.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/editor/bookmarks_editor_mediator.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/editor/bookmarks_editor_mediator_delegate.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/editor/bookmarks_editor_view_controller.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/folder_chooser/bookmarks_folder_chooser_coordinator.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/folder_editor/bookmarks_folder_editor_coordinator.h"
#import "ios/chrome/browser/shared/coordinator/alert/action_sheet_coordinator.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/public/commands/snackbar_commands.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_navigation_controller.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "url/gurl.h"
@interface BookmarksEditorCoordinator () <
BookmarksEditorViewControllerDelegate,
BookmarksEditorMediatorDelegate,
BookmarksFolderChooserCoordinatorDelegate> {
// BookmarkNode to edit.
raw_ptr<const bookmarks::BookmarkNode> _node;
// The editor view controller owned and presented by this coordinator.
// It is wrapped in a TableViewNavigationController.
BookmarksEditorViewController* _viewController;
// The editor mediator owned and presented by this coordinator.
// It is wrapped in a TableViewNavigationController.
BookmarksEditorMediator* _mediator;
// Receives commands to show a snackbar once a bookmark is edited or deleted.
id<SnackbarCommands> _snackbarCommandsHandler;
// The navigation controller that is being presented. The bookmark editor view
// controller is the child of this navigation controller.
UINavigationController* _navigationController;
// The folder chooser coordinator.
BookmarksFolderChooserCoordinator* _folderChooserCoordinator;
}
// The action sheet coordinator, if one is currently being shown.
@property(nonatomic, strong) ActionSheetCoordinator* actionSheetCoordinator;
@end
@implementation BookmarksEditorCoordinator
- (instancetype)initWithBaseViewController:(UIViewController*)viewController
browser:(Browser*)browser
node:(const bookmarks::BookmarkNode*)node
snackbarCommandsHandler:
(id<SnackbarCommands>)snackbarCommandsHandler {
self = [super initWithBaseViewController:viewController browser:browser];
if (self) {
_node = node;
_snackbarCommandsHandler = snackbarCommandsHandler;
}
return self;
}
- (void)start {
[super start];
_viewController = [[BookmarksEditorViewController alloc]
initWithName:bookmark_utils_ios::TitleForBookmarkNode(_node)
URL:base::SysUTF8ToNSString(_node->url().spec())
folderName:bookmark_utils_ios::TitleForBookmarkNode(_node->parent())];
_viewController.delegate = self;
ChromeBrowserState* browserState =
self.browser->GetBrowserState()->GetOriginalChromeBrowserState();
bookmarks::BookmarkModel* bookmarkModel =
ios::BookmarkModelFactory::GetForBrowserState(browserState);
syncer::SyncService* syncService =
SyncServiceFactory::GetForBrowserState(browserState);
_mediator = [[BookmarksEditorMediator alloc]
initWithBookmarkModel:bookmarkModel
bookmarkNode:_node
prefs:browserState->GetPrefs()
authenticationService:AuthenticationServiceFactory::GetForBrowserState(
browserState)
syncService:syncService
browserState:browserState];
_mediator.consumer = _viewController;
_mediator.delegate = self;
_mediator.snackbarCommandsHandler = _snackbarCommandsHandler;
_viewController.mutator = _mediator;
_navigationController =
[[TableViewNavigationController alloc] initWithTable:_viewController];
_navigationController.modalPresentationStyle = UIModalPresentationFormSheet;
_navigationController.toolbarHidden = YES;
_navigationController.presentationController.delegate = self;
[self.baseViewController presentViewController:_navigationController
animated:YES
completion:nil];
}
- (void)stop {
[super stop];
DCHECK(_navigationController);
[_mediator disconnect];
[self dismissActionSheetCoordinator];
_mediator.consumer = nil;
_mediator.snackbarCommandsHandler = nil;
_mediator = nil;
_viewController.delegate = nil;
_viewController.mutator = nil;
_viewController = nil;
_snackbarCommandsHandler = nil;
[_folderChooserCoordinator stop];
_folderChooserCoordinator.delegate = nil;
_folderChooserCoordinator = nil;
// animatedDismissal should have been explicitly set before calling stop.
[_navigationController dismissViewControllerAnimated:self.animatedDismissal
completion:nil];
_navigationController.presentationController.delegate = nil;
_navigationController = nil;
}
- (void)dealloc {
DCHECK(!_navigationController);
}
- (BOOL)canDismiss {
if (_viewController.edited) {
return NO;
}
if (_folderChooserCoordinator && ![_folderChooserCoordinator canDismiss]) {
return NO;
}
return YES;
}
#pragma mark - BookmarksEditorViewControllerDelegate
- (void)moveBookmark {
DCHECK(!_folderChooserCoordinator);
std::set<const bookmarks::BookmarkNode*> hiddenNodes{[_mediator bookmark]};
_folderChooserCoordinator = [[BookmarksFolderChooserCoordinator alloc]
initWithBaseNavigationController:_navigationController
browser:self.browser
hiddenNodes:hiddenNodes];
[_folderChooserCoordinator setSelectedFolder:_mediator.folder];
_folderChooserCoordinator.delegate = self;
[_folderChooserCoordinator start];
}
- (void)bookmarkEditorWantsDismissal:
(BookmarksEditorViewController*)controller {
[self.delegate bookmarksEditorCoordinatorShouldStop:self];
}
#pragma mark - UIAdaptivePresentationControllerDelegate
- (void)presentationControllerDidAttemptToDismiss:
(UIPresentationController*)presentationController {
if (!_viewController.canBeDismissed) {
return;
}
self.actionSheetCoordinator = [[ActionSheetCoordinator alloc]
initWithBaseViewController:_viewController
browser:self.browser
title:nil
message:nil
barButtonItem:_viewController.cancelItem];
__weak __typeof(self) weakSelf = self;
[self.actionSheetCoordinator
addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_VIEW_CONTROLLER_DISMISS_SAVE_CHANGES)
action:^{
[weakSelf dismissActionSheetCoordinator];
BookmarksEditorCoordinator* strongSelf = weakSelf;
if (strongSelf != nil) {
[strongSelf->_viewController save];
}
}
style:UIAlertActionStyleDefault];
[self.actionSheetCoordinator
addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_VIEW_CONTROLLER_DISMISS_DISCARD_CHANGES)
action:^{
[weakSelf dismissActionSheetCoordinator];
BookmarksEditorCoordinator* strongSelf = weakSelf;
if (strongSelf != nil) {
[strongSelf->_viewController cancel];
}
}
style:UIAlertActionStyleDestructive];
[self.actionSheetCoordinator
addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_VIEW_CONTROLLER_DISMISS_CANCEL_CHANGES)
action:^{
[weakSelf dismissActionSheetCoordinator];
BookmarksEditorCoordinator* strongSelf = weakSelf;
if (strongSelf != nil) {
[strongSelf->_viewController setNavigationItemsEnabled:YES];
}
}
style:UIAlertActionStyleCancel];
[_viewController setNavigationItemsEnabled:NO];
[self.actionSheetCoordinator start];
}
- (void)presentationControllerWillDismiss:
(UIPresentationController*)presentationController {
// Resign first responder if trying to dismiss the VC so the keyboard doesn't
// linger until the VC dismissal has completed.
[_viewController.view endEditing:YES];
}
- (void)presentationControllerDidDismiss:
(UIPresentationController*)presentationController {
base::RecordAction(
base::UserMetricsAction("IOSBookmarksEditorClosedWithSwipeDown"));
[_viewController dismissBookmarkEditorView];
}
- (BOOL)presentationControllerShouldDismiss:
(UIPresentationController*)presentationController {
return [self canDismiss];
}
#pragma mark - BookmarksEditorMediatorDelegate
- (void)bookmarkEditorMediatorWantsDismissal:
(BookmarksEditorMediator*)mediator {
[self.delegate bookmarksEditorCoordinatorShouldStop:self];
}
- (void)bookmarkDidMoveToParent:(const bookmarks::BookmarkNode*)newParent {
[_folderChooserCoordinator setSelectedFolder:newParent];
}
- (void)bookmarkEditorWillCommitTitleOrURLChange:
(BookmarksEditorMediator*)mediator {
[self.delegate bookmarkEditorWillCommitTitleOrURLChange:self];
}
#pragma mark - BookmarksFolderChooserCoordinatorDelegate
- (void)bookmarksFolderChooserCoordinatorDidConfirm:
(BookmarksFolderChooserCoordinator*)coordinator
withSelectedFolder:
(const bookmarks::BookmarkNode*)folder {
DCHECK(_folderChooserCoordinator);
DCHECK(folder);
[_folderChooserCoordinator stop];
_folderChooserCoordinator.delegate = nil;
_folderChooserCoordinator = nil;
[_mediator manuallyChangeFolder:folder];
}
- (void)bookmarksFolderChooserCoordinatorDidCancel:
(BookmarksFolderChooserCoordinator*)coordinator {
DCHECK(_folderChooserCoordinator);
[_folderChooserCoordinator stop];
_folderChooserCoordinator.delegate = nil;
_folderChooserCoordinator = nil;
if (!_navigationController.presentingViewController) {
// In this case the `_navigationController` itself was dismissed.
// TODO(crbug.com/40251259): Remove this if block when dismiss handling
// is done in coordinators.
[_viewController.view endEditing:YES];
[self.delegate bookmarksEditorCoordinatorShouldStop:self];
}
}
#pragma mark - Private
- (void)dismissActionSheetCoordinator {
[self.actionSheetCoordinator stop];
self.actionSheetCoordinator = nil;
}
@end