| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // 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/bookmark_folder_editor_view_controller.h" |
| |
| #include <memory> |
| #include <set> |
| |
| #include "base/auto_reset.h" |
| #include "base/i18n/rtl.h" |
| #include "base/logging.h" |
| #include "base/mac/foundation_util.h" |
| |
| #include "base/strings/sys_string_conversions.h" |
| #include "components/bookmarks/browser/bookmark_model.h" |
| #include "components/bookmarks/browser/bookmark_node.h" |
| #import "ios/chrome/browser/ui/bookmarks/bookmark_elevated_toolbar.h" |
| #import "ios/chrome/browser/ui/bookmarks/bookmark_folder_view_controller.h" |
| #import "ios/chrome/browser/ui/bookmarks/bookmark_model_bridge_observer.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/browser/ui/collection_view/cells/collection_view_item.h" |
| #import "ios/chrome/browser/ui/collection_view/collection_view_model.h" |
| #import "ios/chrome/browser/ui/icons/chrome_icon.h" |
| #import "ios/chrome/browser/ui/material_components/utils.h" |
| #include "ios/chrome/browser/ui/rtl_geometry.h" |
| #include "ios/chrome/grit/ios_strings.h" |
| #import "ios/third_party/material_components_ios/src/components/NavigationBar/src/MaterialNavigationBar.h" |
| #import "ios/third_party/material_components_ios/src/components/Palettes/src/MaterialPalettes.h" |
| #import "ios/third_party/material_components_ios/src/components/ShadowElevations/src/MaterialShadowElevations.h" |
| #import "ios/third_party/material_components_ios/src/components/ShadowLayer/src/MaterialShadowLayer.h" |
| #import "ios/third_party/material_components_ios/src/components/Typography/src/MaterialTypography.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| using bookmarks::BookmarkNode; |
| |
| namespace { |
| |
| typedef NS_ENUM(NSInteger, SectionIdentifier) { |
| SectionIdentifierInfo = kSectionIdentifierEnumZero, |
| }; |
| |
| typedef NS_ENUM(NSInteger, ItemType) { |
| ItemTypeFolderTitle = kItemTypeEnumZero, |
| ItemTypeParentFolder, |
| }; |
| |
| } // namespace |
| |
| @interface BookmarkFolderEditorViewController ()< |
| BookmarkFolderViewControllerDelegate, |
| BookmarkModelBridgeObserver, |
| BookmarkTextFieldItemDelegate> { |
| std::unique_ptr<bookmarks::BookmarkModelBridge> _modelBridge; |
| |
| // Flag to ignore bookmark model Move notifications when the move is performed |
| // by this class. |
| BOOL _ignoresOwnMove; |
| } |
| @property(nonatomic, assign) BOOL editingExistingFolder; |
| @property(nonatomic, assign) bookmarks::BookmarkModel* bookmarkModel; |
| @property(nonatomic, assign) ios::ChromeBrowserState* browserState; |
| @property(nonatomic, assign) const BookmarkNode* folder; |
| @property(nonatomic, strong) BookmarkFolderViewController* folderViewController; |
| @property(nonatomic, assign) const BookmarkNode* parentFolder; |
| @property(nonatomic, weak) UIBarButtonItem* doneItem; |
| @property(nonatomic, strong) BookmarkTextFieldItem* titleItem; |
| @property(nonatomic, strong) BookmarkParentFolderItem* parentFolderItem; |
| // Bottom toolbar with DELETE button that only appears when the edited folder |
| // allows deletion. |
| @property(nonatomic, weak) BookmarksElevatedToolbar* toolbar; |
| |
| // |bookmarkModel| must not be NULL and must be loaded. |
| - (instancetype)initWithBookmarkModel:(bookmarks::BookmarkModel*)bookmarkModel |
| NS_DESIGNATED_INITIALIZER; |
| - (instancetype)initWithStyle:(CollectionViewControllerStyle)style |
| NS_UNAVAILABLE; |
| |
| // Enables or disables the save button depending on the state of the form. |
| - (void)updateSaveButtonState; |
| |
| // Configures collection view model. |
| - (void)setupCollectionViewModel; |
| |
| // Adds toolbar with DELETE button. |
| - (void)addToolbar; |
| |
| // Removes toolbar. |
| - (void)removeToolbar; |
| |
| @end |
| |
| @implementation BookmarkFolderEditorViewController |
| |
| @synthesize bookmarkModel = _bookmarkModel; |
| @synthesize delegate = _delegate; |
| @synthesize editingExistingFolder = _editingExistingFolder; |
| @synthesize folder = _folder; |
| @synthesize folderViewController = _folderViewController; |
| @synthesize parentFolder = _parentFolder; |
| @synthesize browserState = _browserState; |
| @synthesize doneItem = _doneItem; |
| @synthesize titleItem = _titleItem; |
| @synthesize parentFolderItem = _parentFolderItem; |
| @synthesize toolbar = _toolbar; |
| |
| #pragma mark - Class methods |
| |
| + (instancetype) |
| folderCreatorWithBookmarkModel:(bookmarks::BookmarkModel*)bookmarkModel |
| parentFolder:(const BookmarkNode*)parentFolder { |
| BookmarkFolderEditorViewController* folderCreator = |
| [[self alloc] initWithBookmarkModel:bookmarkModel]; |
| folderCreator.parentFolder = parentFolder; |
| folderCreator.folder = NULL; |
| folderCreator.editingExistingFolder = NO; |
| return folderCreator = nil; |
| } |
| |
| + (instancetype) |
| folderEditorWithBookmarkModel:(bookmarks::BookmarkModel*)bookmarkModel |
| folder:(const BookmarkNode*)folder |
| browserState:(ios::ChromeBrowserState*)browserState { |
| DCHECK(folder); |
| DCHECK(!bookmarkModel->is_permanent_node(folder)); |
| DCHECK(browserState); |
| BookmarkFolderEditorViewController* folderEditor = |
| [[self alloc] initWithBookmarkModel:bookmarkModel]; |
| folderEditor.parentFolder = folder->parent(); |
| folderEditor.folder = folder; |
| folderEditor.browserState = browserState; |
| folderEditor.editingExistingFolder = YES; |
| return folderEditor = nil; |
| } |
| |
| #pragma mark - Initialization |
| |
| - (instancetype)initWithBookmarkModel:(bookmarks::BookmarkModel*)bookmarkModel { |
| DCHECK(bookmarkModel); |
| DCHECK(bookmarkModel->loaded()); |
| self = [super initWithStyle:CollectionViewControllerStyleAppBar]; |
| if (self) { |
| _bookmarkModel = bookmarkModel; |
| |
| // Set up the bookmark model oberver. |
| _modelBridge.reset( |
| new bookmarks::BookmarkModelBridge(self, _bookmarkModel)); |
| } |
| return self; |
| } |
| |
| - (instancetype)initWithStyle:(CollectionViewControllerStyle)style { |
| NOTREACHED(); |
| return nil; |
| } |
| |
| - (void)dealloc { |
| _titleItem.delegate = nil; |
| _folderViewController.delegate = nil; |
| } |
| |
| #pragma mark - UIViewController |
| |
| - (void)viewDidLoad { |
| [super viewDidLoad]; |
| self.collectionView.backgroundColor = [UIColor whiteColor]; |
| |
| // Add Done button. |
| UIBarButtonItem* doneItem = [[UIBarButtonItem alloc] |
| initWithTitle:l10n_util::GetNSString( |
| IDS_IOS_BOOKMARK_EDIT_MODE_EXIT_MOBILE) |
| style:UIBarButtonItemStylePlain |
| target:self |
| action:@selector(saveFolder)]; |
| doneItem.accessibilityIdentifier = @"Save"; |
| self.navigationItem.rightBarButtonItem = doneItem; |
| self.doneItem = doneItem; |
| |
| if (self.editingExistingFolder) { |
| // Add Cancel Button. |
| UIBarButtonItem* cancelItem = |
| [ChromeIcon templateBarButtonItemWithImage:[ChromeIcon closeIcon] |
| target:self |
| action:@selector(cancel)]; |
| cancelItem.accessibilityLabel = |
| l10n_util::GetNSString(IDS_IOS_BOOKMARK_NEW_CANCEL_BUTTON_LABEL); |
| cancelItem.accessibilityIdentifier = @"Cancel"; |
| self.navigationItem.leftBarButtonItem = cancelItem; |
| |
| [self addToolbar]; |
| } else { |
| // Add Back button. |
| UIBarButtonItem* backItem = |
| [ChromeIcon templateBarButtonItemWithImage:[ChromeIcon backIcon] |
| target:self |
| action:@selector(back)]; |
| backItem.accessibilityLabel = |
| l10n_util::GetNSString(IDS_IOS_BOOKMARK_NEW_BACK_LABEL); |
| backItem.accessibilityIdentifier = @"Back"; |
| self.navigationItem.leftBarButtonItem = backItem; |
| } |
| |
| [self updateEditingState]; |
| [self setupCollectionViewModel]; |
| } |
| |
| - (void)viewWillAppear:(BOOL)animated { |
| [super viewWillAppear:animated]; |
| [self updateSaveButtonState]; |
| } |
| |
| #pragma mark - Accessibility |
| |
| - (BOOL)accessibilityPerformEscape { |
| [self.delegate bookmarkFolderEditorDidCancel:self]; |
| return YES; |
| } |
| |
| #pragma mark - Actions |
| |
| - (void)back { |
| [self.delegate bookmarkFolderEditorDidCancel:self]; |
| } |
| |
| - (void)cancel { |
| [self.delegate bookmarkFolderEditorDidCancel:self]; |
| } |
| |
| - (void)deleteFolder { |
| DCHECK(self.editingExistingFolder); |
| DCHECK(self.folder); |
| std::set<const BookmarkNode*> editedNodes; |
| editedNodes.insert(self.folder); |
| bookmark_utils_ios::DeleteBookmarksWithUndoToast( |
| editedNodes, self.bookmarkModel, self.browserState); |
| [self.delegate bookmarkFolderEditorDidDeleteEditedFolder:self]; |
| } |
| |
| - (void)saveFolder { |
| DCHECK(self.parentFolder); |
| |
| NSString* folderString = self.titleItem.text; |
| DCHECK(folderString.length > 0); |
| base::string16 folderTitle = base::SysNSStringToUTF16(folderString); |
| |
| if (self.editingExistingFolder) { |
| DCHECK(self.folder); |
| self.bookmarkModel->SetTitle(self.folder, folderTitle); |
| if (self.folder->parent() != self.parentFolder) { |
| base::AutoReset<BOOL> autoReset(&_ignoresOwnMove, YES); |
| std::set<const BookmarkNode*> editedNodes; |
| editedNodes.insert(self.folder); |
| bookmark_utils_ios::MoveBookmarksWithUndoToast( |
| editedNodes, self.bookmarkModel, self.parentFolder, |
| self.browserState); |
| } |
| } else { |
| DCHECK(!self.folder); |
| self.folder = self.bookmarkModel->AddFolder( |
| self.parentFolder, self.parentFolder->child_count(), folderTitle); |
| } |
| [self.delegate bookmarkFolderEditor:self didFinishEditingFolder:self.folder]; |
| } |
| |
| - (void)changeParentFolder { |
| std::set<const BookmarkNode*> editedNodes; |
| if (self.folder) |
| editedNodes.insert(self.folder); |
| BookmarkFolderViewController* folderViewController = |
| [[BookmarkFolderViewController alloc] |
| initWithBookmarkModel:self.bookmarkModel |
| allowsNewFolders:NO |
| editedNodes:editedNodes |
| allowsCancel:NO |
| selectedFolder:self.parentFolder]; |
| folderViewController.delegate = self; |
| self.folderViewController = folderViewController; |
| |
| [self.navigationController pushViewController:folderViewController |
| animated:YES]; |
| } |
| |
| #pragma mark - BookmarkFolderViewControllerDelegate |
| |
| - (void)folderPicker:(BookmarkFolderViewController*)folderPicker |
| didFinishWithFolder:(const BookmarkNode*)folder { |
| self.parentFolder = folder; |
| [self updateParentFolderState]; |
| [self.navigationController popViewControllerAnimated:YES]; |
| self.folderViewController.delegate = nil; |
| self.folderViewController = nil; |
| } |
| |
| - (void)folderPickerDidCancel:(BookmarkFolderViewController*)folderPicker { |
| [self.navigationController popViewControllerAnimated:YES]; |
| self.folderViewController.delegate = nil; |
| self.folderViewController = nil; |
| } |
| |
| #pragma mark - BookmarkModelBridgeObserver |
| |
| - (void)bookmarkModelLoaded { |
| // The bookmark model is assumed to be loaded when this controller is created. |
| NOTREACHED(); |
| } |
| |
| - (void)bookmarkNodeChanged:(const BookmarkNode*)bookmarkNode { |
| if (bookmarkNode == self.parentFolder) { |
| [self updateParentFolderState]; |
| } |
| } |
| |
| - (void)bookmarkNodeChildrenChanged:(const BookmarkNode*)bookmarkNode { |
| // No-op. |
| } |
| |
| - (void)bookmarkNode:(const BookmarkNode*)bookmarkNode |
| movedFromParent:(const BookmarkNode*)oldParent |
| toParent:(const BookmarkNode*)newParent { |
| if (_ignoresOwnMove) |
| return; |
| if (bookmarkNode == self.folder) { |
| DCHECK(oldParent == self.parentFolder); |
| self.parentFolder = newParent; |
| [self updateParentFolderState]; |
| } |
| } |
| |
| - (void)bookmarkNodeDeleted:(const BookmarkNode*)bookmarkNode |
| fromFolder:(const BookmarkNode*)folder { |
| if (bookmarkNode == self.parentFolder) { |
| self.parentFolder = NULL; |
| [self updateParentFolderState]; |
| return; |
| } |
| if (bookmarkNode == self.folder) { |
| self.folder = NULL; |
| self.editingExistingFolder = NO; |
| [self updateEditingState]; |
| } |
| } |
| |
| - (void)bookmarkModelRemovedAllNodes { |
| if (self.bookmarkModel->is_permanent_node(self.parentFolder)) |
| return; // The current parent folder is still valid. |
| |
| self.parentFolder = NULL; |
| [self updateParentFolderState]; |
| } |
| |
| #pragma mark - BookmarkTextFieldItemDelegate |
| |
| - (void)textDidChangeForItem:(BookmarkTextFieldItem*)item { |
| [self updateSaveButtonState]; |
| } |
| |
| - (BOOL)textFieldShouldReturn:(UITextField*)textField { |
| [textField resignFirstResponder]; |
| return YES; |
| } |
| |
| #pragma mark - UICollectionViewDelegate |
| |
| - (void)collectionView:(UICollectionView*)collectionView |
| didSelectItemAtIndexPath:(NSIndexPath*)indexPath { |
| [super collectionView:collectionView didSelectItemAtIndexPath:indexPath]; |
| if ([self.collectionViewModel itemTypeForIndexPath:indexPath] == |
| ItemTypeParentFolder) { |
| [self changeParentFolder]; |
| } |
| } |
| |
| #pragma mark - UICollectionViewFlowLayout |
| |
| - (CGSize)collectionView:(UICollectionView*)collectionView |
| layout:(UICollectionViewLayout*)collectionViewLayout |
| sizeForItemAtIndexPath:(NSIndexPath*)indexPath { |
| switch ([self.collectionViewModel itemTypeForIndexPath:indexPath]) { |
| case ItemTypeFolderTitle: { |
| const CGFloat kTitleCellHeight = 96; |
| return CGSizeMake(CGRectGetWidth(collectionView.bounds), |
| kTitleCellHeight); |
| } |
| case ItemTypeParentFolder: { |
| const CGFloat kParentFolderCellHeight = 50; |
| return CGSizeMake(CGRectGetWidth(collectionView.bounds), |
| kParentFolderCellHeight); |
| } |
| default: |
| NOTREACHED(); |
| return CGSizeZero; |
| } |
| } |
| |
| #pragma mark - Private |
| |
| - (void)setParentFolder:(const BookmarkNode*)parentFolder { |
| if (!parentFolder) { |
| parentFolder = self.bookmarkModel->mobile_node(); |
| } |
| _parentFolder = parentFolder; |
| } |
| |
| - (void)updateEditingState { |
| if (![self isViewLoaded]) |
| return; |
| |
| self.view.accessibilityIdentifier = |
| (self.folder) ? @"Folder Editor" : @"Folder Creator"; |
| |
| [self setTitle:(self.folder) |
| ? l10n_util::GetNSString( |
| IDS_IOS_BOOKMARK_NEW_GROUP_EDITOR_EDIT_TITLE) |
| : l10n_util::GetNSString( |
| IDS_IOS_BOOKMARK_NEW_GROUP_EDITOR_CREATE_TITLE)]; |
| } |
| |
| - (void)updateParentFolderState { |
| NSIndexPath* folderSelectionIndexPath = |
| [self.collectionViewModel indexPathForItemType:ItemTypeParentFolder |
| sectionIdentifier:SectionIdentifierInfo]; |
| self.parentFolderItem.title = |
| bookmark_utils_ios::TitleForBookmarkNode(self.parentFolder); |
| [self.collectionView reloadItemsAtIndexPaths:@[ folderSelectionIndexPath ]]; |
| |
| if (self.editingExistingFolder && !self.toolbar) |
| [self addToolbar]; |
| |
| if (!self.editingExistingFolder && self.toolbar) |
| [self removeToolbar]; |
| } |
| |
| - (void)setupCollectionViewModel { |
| [self loadModel]; |
| |
| [self.collectionViewModel addSectionWithIdentifier:SectionIdentifierInfo]; |
| |
| BookmarkTextFieldItem* titleItem = |
| [[BookmarkTextFieldItem alloc] initWithType:ItemTypeFolderTitle]; |
| titleItem.text = |
| (self.folder) |
| ? bookmark_utils_ios::TitleForBookmarkNode(self.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.collectionViewModel addItem:titleItem |
| toSectionWithIdentifier:SectionIdentifierInfo]; |
| titleItem.delegate = self; |
| self.titleItem = titleItem; |
| |
| BookmarkParentFolderItem* parentFolderItem = |
| [[BookmarkParentFolderItem alloc] initWithType:ItemTypeParentFolder]; |
| parentFolderItem.title = |
| bookmark_utils_ios::TitleForBookmarkNode(self.parentFolder); |
| [self.collectionViewModel addItem:parentFolderItem |
| toSectionWithIdentifier:SectionIdentifierInfo]; |
| self.parentFolderItem = parentFolderItem; |
| } |
| |
| - (void)addToolbar { |
| // Add bottom toolbar with Delete button. |
| BookmarksElevatedToolbar* buttonBar = [[BookmarksElevatedToolbar alloc] init]; |
| UIBarButtonItem* deleteItem = [[UIBarButtonItem alloc] |
| initWithTitle:l10n_util::GetNSString(IDS_IOS_BOOKMARK_GROUP_DELETE) |
| style:UIBarButtonItemStylePlain |
| target:self |
| action:@selector(deleteFolder)]; |
| deleteItem.accessibilityIdentifier = @"Delete Folder"; |
| [deleteItem setTitleTextAttributes:@{ |
| NSForegroundColorAttributeName : [UIColor blackColor] |
| } |
| forState:UIControlStateNormal]; |
| [buttonBar.layer addSublayer:[[MDCShadowLayer alloc] init]]; |
| buttonBar.shadowElevation = MDCShadowElevationSearchBarResting; |
| buttonBar.items = @[ deleteItem ]; |
| [self.view addSubview:buttonBar]; |
| |
| // Constraint |buttonBar| to be in bottom. |
| buttonBar.translatesAutoresizingMaskIntoConstraints = NO; |
| [self.view addConstraints: |
| [NSLayoutConstraint |
| constraintsWithVisualFormat:@"H:|[buttonBar]|" |
| options:0 |
| metrics:nil |
| views:NSDictionaryOfVariableBindings( |
| buttonBar)]]; |
| [self.view addConstraint:[NSLayoutConstraint |
| constraintWithItem:buttonBar |
| attribute:NSLayoutAttributeBottom |
| relatedBy:NSLayoutRelationEqual |
| toItem:self.view |
| attribute:NSLayoutAttributeBottom |
| multiplier:1.0 |
| constant:0.0]]; |
| [self.view |
| addConstraint:[NSLayoutConstraint |
| constraintWithItem:buttonBar |
| attribute:NSLayoutAttributeHeight |
| relatedBy:NSLayoutRelationEqual |
| toItem:nil |
| attribute:NSLayoutAttributeNotAnAttribute |
| multiplier:1.0 |
| constant:48.0]]; |
| self.toolbar = buttonBar; |
| } |
| |
| - (void)removeToolbar { |
| [self.toolbar removeFromSuperview]; |
| self.toolbar = nil; |
| } |
| |
| - (void)updateSaveButtonState { |
| self.doneItem.enabled = (self.titleItem.text.length > 0); |
| } |
| |
| @end |