[go: nahoru, domu]

blob: eef07178796963f5f43c2b5ba1220c994b502f41 [file] [log] [blame]
// Copyright 2014 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/bookmarks/folder_editor/bookmarks_folder_editor_view_controller.h"
#import <memory>
#import <set>
#import "base/apple/foundation_util.h"
#import "base/auto_reset.h"
#import "base/check_op.h"
#import "base/i18n/rtl.h"
#import "base/memory/weak_ptr.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/notreached.h"
#import "base/strings/sys_string_conversions.h"
#import "components/bookmarks/browser/bookmark_model.h"
#import "components/bookmarks/browser/bookmark_node.h"
#import "components/bookmarks/common/bookmark_features.h"
#import "components/bookmarks/common/bookmark_metrics.h"
#import "components/sync/base/features.h"
#import "ios/chrome/browser/bookmarks/bookmark_model_bridge_observer.h"
#import "ios/chrome/browser/bookmarks/bookmarks_utils.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/browser_state/chrome_browser_state.h"
#import "ios/chrome/browser/shared/public/commands/snackbar_commands.h"
#import "ios/chrome/browser/shared/ui/symbols/chrome_icon.h"
#import "ios/chrome/browser/shared/ui/table_view/chrome_table_view_styler.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_utils.h"
#import "ios/chrome/browser/shared/ui/util/rtl_geometry.h"
#import "ios/chrome/browser/signin/authentication_service_observer_bridge.h"
#import "ios/chrome/browser/sync/sync_observer_bridge.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_ui_constants.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_utils_ios.h"
#import "ios/chrome/browser/ui/bookmarks/cells/bookmark_parent_folder_item.h"
#import "ios/chrome/browser/ui/bookmarks/cells/bookmark_text_field_item.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.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"
using bookmarks::BookmarkModel;
using bookmarks::BookmarkNode;
namespace {
typedef NS_ENUM(NSInteger, SectionIdentifier) {
SectionIdentifierInfo = kSectionIdentifierEnumZero,
};
typedef NS_ENUM(NSInteger, ItemType) {
ItemTypeFolderTitle = kItemTypeEnumZero,
ItemTypeParentFolder,
};
} // namespace
@interface BookmarksFolderEditorViewController () <
AuthenticationServiceObserving,
BookmarkModelBridgeObserver,
BookmarkTextFieldItemDelegate,
SyncObserverModelBridge>
@end
@implementation BookmarksFolderEditorViewController {
// Model object for localOrSyncable bookmarks.
base::WeakPtr<BookmarkModel> _localOrSyncableBookmarkModel;
// Observer for `_localOrSyncableBookmarkModel` changes.
std::unique_ptr<BookmarkModelBridge> _localOrSyncableModelBridge;
// Model object for account bookmarks.
base::WeakPtr<BookmarkModel> _accountBookmarkModel;
// Observer for `_accountBookmarkModel` changes.
std::unique_ptr<BookmarkModelBridge> _accountModelBridge;
// Observer for signin status changes.
std::unique_ptr<AuthenticationServiceObserverBridge> _authServiceBridge;
syncer::SyncService* _syncService;
std::unique_ptr<SyncObserverBridge> _syncObserverModelBridge;
// The browser for this view controller.
base::WeakPtr<Browser> _browser;
ChromeBrowserState* _browserState;
// Parent folder to `_folder`. Should never be `nullptr`.
const BookmarkNode* _parentFolder;
// If `_folderNode` is `nullptr`, the user is adding a new folder. Otherwise
// the user is editing an existing folder.
const BookmarkNode* _folder;
BOOL _edited;
BOOL _editingExistingFolder;
// Flag to ignore bookmark model Move notifications when the move is performed
// by this class.
BOOL _ignoresOwnMove;
__weak UIBarButtonItem* _doneItem;
__strong BookmarkTextFieldItem* _titleItem;
__strong BookmarkParentFolderItem* _parentFolderItem;
// The action sheet coordinator, if one is currently being shown.
__strong ActionSheetCoordinator* _actionSheetCoordinator;
// Whether the user manually changed the folder. In which case it must be
// saved as last used folder on "save".
BOOL _manuallyChangedTheFolder;
}
#pragma mark - Initialization
- (instancetype)
initWithLocalOrSyncableBookmarkModel:
(BookmarkModel*)localOrSyncableBookmarkModel
accountBookmarkModel:(BookmarkModel*)accountBookmarkModel
folderNode:(const BookmarkNode*)folder
parentFolderNode:(const BookmarkNode*)parentFolder
authenticationService:(AuthenticationService*)authService
syncService:(syncer::SyncService*)syncService
browser:(Browser*)browser {
DCHECK(localOrSyncableBookmarkModel);
DCHECK(localOrSyncableBookmarkModel->loaded());
if (base::FeatureList::IsEnabled(syncer::kEnableBookmarksAccountStorage)) {
DCHECK(accountBookmarkModel);
DCHECK(accountBookmarkModel->loaded());
} else {
DCHECK(!accountBookmarkModel);
}
DCHECK(parentFolder);
if (folder) {
BookmarkModel* modelForFolder = bookmark_utils_ios::GetBookmarkModelForNode(
folder, localOrSyncableBookmarkModel, accountBookmarkModel);
DCHECK(!modelForFolder->is_permanent_node(folder));
}
DCHECK(browser);
UITableViewStyle style = ChromeTableViewStyle();
self = [super initWithStyle:style];
if (self) {
_localOrSyncableBookmarkModel = localOrSyncableBookmarkModel->AsWeakPtr();
_localOrSyncableModelBridge = std::make_unique<BookmarkModelBridge>(
self, _localOrSyncableBookmarkModel.get());
if (accountBookmarkModel) {
_accountBookmarkModel = accountBookmarkModel->AsWeakPtr();
_accountModelBridge = std::make_unique<BookmarkModelBridge>(
self, _accountBookmarkModel.get());
}
_folder = folder;
_parentFolder = parentFolder;
_editingExistingFolder = _folder != nullptr;
_browser = browser->AsWeakPtr();
_browserState = browser->GetBrowserState()->GetOriginalChromeBrowserState();
_authServiceBridge = std::make_unique<AuthenticationServiceObserverBridge>(
authService, self);
_syncService = syncService;
// Set up the bookmark model oberver.
_syncObserverModelBridge =
std::make_unique<SyncObserverBridge>(self, syncService);
}
return self;
}
- (void)disconnect {
_browserState = nullptr;
_localOrSyncableBookmarkModel.reset();
_localOrSyncableModelBridge.reset();
_accountBookmarkModel.reset();
_accountModelBridge.reset();
_folder = nullptr;
_parentFolder = nullptr;
_authServiceBridge.reset();
_syncService = nullptr;
_syncObserverModelBridge.reset();
_titleItem.delegate = nil;
[self dismissActionSheetCoordinator];
}
- (void)dealloc {
DCHECK(!_browserState);
}
#pragma mark - Public
- (void)presentationControllerDidAttemptToDismiss {
_actionSheetCoordinator = [[ActionSheetCoordinator alloc]
initWithBaseViewController:self
browser:_browser.get()
title:nil
message:nil
barButtonItem:self.navigationItem.leftBarButtonItem];
__weak __typeof(self) weakSelf = self;
[_actionSheetCoordinator
addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_VIEW_CONTROLLER_DISMISS_SAVE_CHANGES)
action:^{
[weakSelf saveFolder];
[weakSelf dismissActionSheetCoordinator];
}
style:UIAlertActionStyleDefault];
[_actionSheetCoordinator
addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_VIEW_CONTROLLER_DISMISS_DISCARD_CHANGES)
action:^{
[weakSelf dismiss];
[weakSelf dismissActionSheetCoordinator];
}
style:UIAlertActionStyleDestructive];
// IDS_IOS_NAVIGATION_BAR_CANCEL_BUTTON
[_actionSheetCoordinator
addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_VIEW_CONTROLLER_DISMISS_CANCEL_CHANGES)
action:^{
weakSelf.navigationItem.leftBarButtonItem.enabled = YES;
weakSelf.navigationItem.rightBarButtonItem.enabled = YES;
[weakSelf dismissActionSheetCoordinator];
}
style:UIAlertActionStyleCancel];
self.navigationItem.leftBarButtonItem.enabled = NO;
self.navigationItem.rightBarButtonItem.enabled = NO;
[_actionSheetCoordinator start];
}
// Whether the bookmarks folder editor can be dismissed.
- (BOOL)canDismiss {
return !_edited;
}
- (void)updateParentFolder:(const BookmarkNode*)parent {
DCHECK(parent);
_parentFolder = parent;
_manuallyChangedTheFolder = YES;
[self updateParentFolderState];
}
#pragma mark - UIViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.backgroundColor = self.styler.tableViewBackgroundColor;
self.tableView.estimatedRowHeight = 150.0;
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.sectionHeaderHeight = 0;
self.tableView.sectionFooterHeight = 0;
[self.tableView
setSeparatorInset:UIEdgeInsetsMake(0, kBookmarkCellHorizontalLeadingInset,
0, 0)];
// Add Done button.
UIBarButtonItem* doneItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
target:self
action:@selector(saveFolder)];
doneItem.accessibilityIdentifier =
kBookmarkFolderEditNavigationBarDoneButtonIdentifier;
self.navigationItem.rightBarButtonItem = doneItem;
_doneItem = doneItem;
if (_editingExistingFolder) {
// Add Cancel Button.
UIBarButtonItem* cancelItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
target:self
action:@selector(dismiss)];
cancelItem.accessibilityIdentifier = @"Cancel";
self.navigationItem.leftBarButtonItem = cancelItem;
[self addToolbar];
}
[self updateEditingState];
[self setupCollectionViewModel];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self updateSaveButtonState];
if (_editingExistingFolder) {
self.navigationController.toolbarHidden = NO;
} else {
self.navigationController.toolbarHidden = YES;
}
}
- (void)didMoveToParentViewController:(UIViewController*)parent {
[super didMoveToParentViewController:parent];
if (!parent) {
[self.delegate bookmarksFolderEditorDidDismiss:self];
}
}
#pragma mark - Accessibility
- (BOOL)accessibilityPerformEscape {
[self dismiss];
return YES;
}
#pragma mark - Actions
- (void)dismiss {
base::RecordAction(
base::UserMetricsAction("MobileBookmarksFolderEditorCanceled"));
[self.view endEditing:YES];
[self.delegate bookmarksFolderEditorDidCancel:self];
}
- (void)deleteFolder {
DCHECK(_editingExistingFolder);
DCHECK(_folder);
base::RecordAction(
base::UserMetricsAction("MobileBookmarksFolderEditorDeletedFolder"));
std::set<const BookmarkNode*> editedNodes;
editedNodes.insert(_folder);
BookmarkModel* modelForFolder = bookmark_utils_ios::GetBookmarkModelForNode(
_folder, _localOrSyncableBookmarkModel.get(),
_accountBookmarkModel.get());
[self.snackbarCommandsHandler
showSnackbarMessage:bookmark_utils_ios::DeleteBookmarksWithUndoToast(
editedNodes, {modelForFolder}, _browserState)];
[self.delegate bookmarksFolderEditorDidDeleteEditedFolder:self];
}
- (void)saveFolder {
DCHECK(_parentFolder);
BookmarkModel* modelForParentFolder =
bookmark_utils_ios::GetBookmarkModelForNode(
_parentFolder, _localOrSyncableBookmarkModel.get(),
_accountBookmarkModel.get());
base::RecordAction(
base::UserMetricsAction("MobileBookmarksFolderEditorSaved"));
NSString* folderString = _titleItem.text;
DCHECK(folderString.length > 0);
std::u16string folderTitle = base::SysNSStringToUTF16(folderString);
if (_editingExistingFolder) {
DCHECK(_folder);
// Tell delegate if folder title has been changed.
if (_folder->GetTitle() != folderTitle) {
[self.delegate bookmarksFolderEditorWillCommitTitleChange:self];
}
BookmarkModel* modelForFolder = bookmark_utils_ios::GetBookmarkModelForNode(
_folder, _localOrSyncableBookmarkModel.get(),
_accountBookmarkModel.get());
modelForFolder->SetTitle(_folder, folderTitle,
bookmarks::metrics::BookmarkEditSource::kUser);
if (_folder->parent() != _parentFolder) {
base::AutoReset<BOOL> autoReset(&_ignoresOwnMove, YES);
std::vector<const BookmarkNode*> bookmarksVector{_folder};
[self.snackbarCommandsHandler
showSnackbarMessage:bookmark_utils_ios::MoveBookmarksWithUndoToast(
bookmarksVector,
_localOrSyncableBookmarkModel.get(),
_accountBookmarkModel.get(), _parentFolder,
_browserState)];
// Move might change the pointer, grab the updated value.
CHECK_EQ(bookmarksVector.size(), 1u);
_folder = bookmarksVector[0];
}
} else {
DCHECK(!_folder);
_folder = modelForParentFolder->AddFolder(
_parentFolder, _parentFolder->children().size(), folderTitle);
}
if (_manuallyChangedTheFolder) {
bookmarks::StorageType type = bookmark_utils_ios::GetBookmarkModelType(
_parentFolder, _localOrSyncableBookmarkModel.get(),
_accountBookmarkModel.get());
SetLastUsedBookmarkFolder(_browserState->GetPrefs(), _parentFolder, type);
}
[self.view endEditing:YES];
[self.delegate bookmarksFolderEditor:self didFinishEditingFolder:_folder];
}
- (void)changeParentFolder {
base::RecordAction(base::UserMetricsAction(
"MobileBookmarksFolderEditorOpenedFolderChooser"));
std::set<const BookmarkNode*> hiddenNodes;
if (_folder) {
hiddenNodes.insert(_folder);
}
[self.delegate showBookmarksFolderChooserWithParentFolder:_parentFolder
hiddenNodes:hiddenNodes];
}
#pragma mark - AuthenticationServiceObserving
- (void)onServiceStatusChanged {
[self cancelIfParentFolderIsUnavailable];
}
#pragma mark - BookmarkModelBridgeObserver
- (void)bookmarkModelLoaded:(BookmarkModel*)model {
// The bookmark model is assumed to be loaded when this controller is created.
NOTREACHED();
}
- (void)bookmarkModel:(BookmarkModel*)model
didChangeNode:(const BookmarkNode*)bookmarkNode {
if (bookmarkNode == _parentFolder) {
[self updateParentFolderState];
}
}
- (void)bookmarkModel:(BookmarkModel*)model
didChangeChildrenForNode:(const BookmarkNode*)bookmarkNode {
// No-op.
}
- (void)bookmarkModel:(BookmarkModel*)model
didMoveNode:(const BookmarkNode*)bookmarkNode
fromParent:(const BookmarkNode*)oldParent
toParent:(const BookmarkNode*)newParent {
if (_ignoresOwnMove) {
return;
}
if (bookmarkNode == _folder) {
DCHECK(oldParent == _parentFolder);
_parentFolder = newParent;
[self updateParentFolderState];
}
}
- (void)bookmarkModel:(bookmarks::BookmarkModel*)model
willDeleteNode:(const bookmarks::BookmarkNode*)node
fromFolder:(const bookmarks::BookmarkNode*)folder {
if (_folder->HasAncestor(node)) {
_folder = nullptr;
if (_ignoresOwnMove) {
// `saveFolder` will dismiss this screen after finishing the move.
return;
}
[self dismiss];
} else if (_parentFolder->HasAncestor(node)) {
// This might happen when the user has changed `_parentFolder` but has not
// commited the changes by pressing done. And in the background the chosen
// folder was deleted.
_parentFolder = model->mobile_node();
[self updateParentFolderState];
}
}
- (void)bookmarkModel:(BookmarkModel*)model
didDeleteNode:(const BookmarkNode*)node
fromFolder:(const BookmarkNode*)folder {
// No-op. Bookmark deletion handled in
// `bookmarkModel:willDeleteNode:fromFolder:`
}
- (void)bookmarkModelRemovedAllNodes:(BookmarkModel*)model {
if (model->is_permanent_node(_parentFolder)) {
return; // The current parent folder is still valid.
}
[self dismiss];
}
#pragma mark - BookmarkTextFieldItemDelegate
- (void)textDidChangeForItem:(BookmarkTextFieldItem*)item {
_edited = YES;
[self updateSaveButtonState];
}
- (void)textFieldDidBeginEditing:(UITextField*)textField {
textField.textColor = [BookmarkTextFieldCell textColorForEditing:YES];
}
- (void)textFieldDidEndEditing:(UITextField*)textField {
textField.textColor = [BookmarkTextFieldCell textColorForEditing:NO];
}
- (BOOL)textFieldShouldReturn:(UITextField*)textField {
[textField resignFirstResponder];
return YES;
}
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView*)tableView
didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
DCHECK_EQ(tableView, self.tableView);
if ([self.tableViewModel itemTypeForIndexPath:indexPath] ==
ItemTypeParentFolder) {
[self changeParentFolder];
}
}
#pragma mark - Private
- (void)dismissActionSheetCoordinator {
[_actionSheetCoordinator stop];
_actionSheetCoordinator = nil;
}
// If `_parentFolder` belongs to `_accountBookmarkModel` and
// `accountBookmarkModel` becomes unavailable, this method will cancel folder
// editor. Returns whether folder editor was cancelled or not.
- (BOOL)cancelIfParentFolderIsUnavailable {
BookmarkModel* parentFolderModel =
bookmark_utils_ios::GetBookmarkModelForNode(
_parentFolder, _localOrSyncableBookmarkModel.get(),
_accountBookmarkModel.get());
if (!bookmark_utils_ios::IsAccountBookmarkStorageOptedIn(_syncService) &&
parentFolderModel == _accountBookmarkModel.get()) {
[self dismiss];
return YES;
}
return NO;
}
- (void)updateEditingState {
if (![self isViewLoaded]) {
return;
}
self.view.accessibilityIdentifier =
(_folder) ? kBookmarkFolderEditViewContainerIdentifier
: kBookmarkFolderCreateViewContainerIdentifier;
[self setTitle:(_folder)
? l10n_util::GetNSString(
IDS_IOS_BOOKMARK_NEW_GROUP_EDITOR_EDIT_TITLE)
: l10n_util::GetNSString(
IDS_IOS_BOOKMARK_NEW_GROUP_EDITOR_CREATE_TITLE)];
}
// Updates `_parentFolderItem` without updating the table view.
- (void)updateParentFolderItem {
CHECK(_parentFolderItem);
_parentFolderItem.title =
bookmark_utils_ios::TitleForBookmarkNode(_parentFolder);
bookmarks::StorageType type = bookmark_utils_ios::GetBookmarkModelType(
_parentFolder, _localOrSyncableBookmarkModel.get(),
_accountBookmarkModel.get());
switch (type) {
case bookmarks::StorageType::kLocalOrSyncable:
_parentFolderItem.shouldDisplayCloudSlashIcon =
bookmark_utils_ios::IsAccountBookmarkStorageOptedIn(_syncService);
break;
case bookmarks::StorageType::kAccount:
_parentFolderItem.shouldDisplayCloudSlashIcon = NO;
break;
}
}
// Updates `_parentFolderItem` and reload the table view cell.
- (void)updateParentFolderState {
if ([self cancelIfParentFolderIsUnavailable]) {
return;
}
[self updateParentFolderItem];
NSIndexPath* folderSelectionIndexPath =
[self.tableViewModel indexPathForItemType:ItemTypeParentFolder
sectionIdentifier:SectionIdentifierInfo];
[self.tableView reloadRowsAtIndexPaths:@[ folderSelectionIndexPath ]
withRowAnimation:UITableViewRowAnimationNone];
}
- (void)setupCollectionViewModel {
[self loadModel];
[self.tableViewModel addSectionWithIdentifier:SectionIdentifierInfo];
_titleItem = [[BookmarkTextFieldItem alloc] initWithType:ItemTypeFolderTitle];
_titleItem.text =
(_folder)
? bookmark_utils_ios::TitleForBookmarkNode(_folder)
: l10n_util::GetNSString(IDS_IOS_BOOKMARK_NEW_GROUP_DEFAULT_NAME);
_titleItem.placeholder =
l10n_util::GetNSString(IDS_IOS_BOOKMARK_NEW_EDITOR_NAME_LABEL);
_titleItem.accessibilityIdentifier = @"Title";
[self.tableViewModel addItem:_titleItem
toSectionWithIdentifier:SectionIdentifierInfo];
_titleItem.delegate = self;
_parentFolderItem =
[[BookmarkParentFolderItem alloc] initWithType:ItemTypeParentFolder];
[self updateParentFolderItem];
[self.tableViewModel addItem:_parentFolderItem
toSectionWithIdentifier:SectionIdentifierInfo];
}
// Adds delete button at the bottom.
- (void)addToolbar {
self.navigationController.toolbarHidden = NO;
NSString* titleString = l10n_util::GetNSString(IDS_IOS_BOOKMARK_GROUP_DELETE);
UIBarButtonItem* deleteButton =
[[UIBarButtonItem alloc] initWithTitle:titleString
style:UIBarButtonItemStylePlain
target:self
action:@selector(deleteFolder)];
deleteButton.accessibilityIdentifier =
kBookmarkFolderEditorDeleteButtonIdentifier;
UIBarButtonItem* spaceButton = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
target:nil
action:nil];
deleteButton.tintColor = [UIColor colorNamed:kRedColor];
[self setToolbarItems:@[ spaceButton, deleteButton, spaceButton ]
animated:NO];
}
- (void)updateSaveButtonState {
_doneItem.enabled = (_titleItem.text.length > 0);
}
#pragma mark - SyncObserverModelBridge
- (void)onSyncStateChanged {
[self updateParentFolderState];
}
@end