blob: cc4e76bd2299f56b0820b1cc057ed8a9ec6450f0 [file] [log] [blame]
// 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_home_handset_view_controller.h"
#include <memory>
#include "base/logging.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/strings/sys_string_conversions.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/strings/grit/components_strings.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "ios/chrome/browser/bookmarks/bookmarks_utils.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
#import "ios/chrome/browser/ui/alert_coordinator/action_sheet_coordinator.h"
#import "ios/chrome/browser/ui/bookmarks/bars/bookmark_editing_bar.h"
#import "ios/chrome/browser/ui/bookmarks/bars/bookmark_navigation_bar.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_collection_cells.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_edit_view_controller.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_folder_collection_view.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_folder_editor_view_controller.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_folder_view_controller.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_home_primary_view.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_home_waiting_view.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_menu_item.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_menu_view.h"
#include "ios/chrome/browser/ui/bookmarks/bookmark_model_bridge_observer.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_navigation_controller.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_panel_view.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_position_cache.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_promo_controller.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_utils_ios.h"
#include "ios/chrome/browser/ui/ui_util.h"
#import "ios/chrome/browser/ui/uikit_ui_util.h"
#include "ios/chrome/grit/ios_strings.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/l10n_util_mac.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using bookmarks::BookmarkNode;
namespace {
const CGFloat kBookmarkMenuWidth = 264;
} // namespace
@interface BookmarkHomeHandsetViewController ()<
BookmarkEditViewControllerDelegate,
BookmarkFolderCollectionViewDelegate,
BookmarkFolderEditorViewControllerDelegate,
BookmarkFolderViewControllerDelegate,
BookmarkMenuViewDelegate,
BookmarkModelBridgeObserver,
BookmarkPanelViewDelegate,
BookmarkPromoControllerDelegate> {
// Bridge to register for bookmark changes.
std::unique_ptr<bookmarks::BookmarkModelBridge> _bridge;
}
// This views holds the primary content of this view controller. At any point in
// time, it contains exactly one of the BookmarkCollectionView subclasses.
@property(nonatomic, strong) UIView* contentView;
// The possible views that can be shown from the menu.
@property(nonatomic, strong) BookmarkFolderCollectionView* folderView;
// This view is created and used if the model is not fully loaded yet by the
// time this controller starts.
@property(nonatomic, strong) BookmarkHomeWaitingView* waitForModelView;
// The menu with all the folders and special entries.
@property(nonatomic, strong) BookmarkMenuView* menuView;
// At any point in time, there is exactly one collection view whose view is part
// of the view hierarchy. This property determine which collection view is
// visible. Not by accident, this property also reflects the selected menu item
// in the BookmarkMenuView.
@property(nonatomic, strong) BookmarkMenuItem* primaryMenuItem;
// When the view is first shown on the screen, this property represents the
// cached value of the y of the content offset of the primary view. This
// property is set to nil after it is used.
@property(nonatomic, strong) NSNumber* cachedContentPosition;
// The navigation bar sits on top of the main content.
@property(nonatomic, strong) BookmarkNavigationBar* navigationBar;
// The layout code in this class relies on the assumption that the editingBar
// has the same frame as the navigationBar.
@property(nonatomic, strong) BookmarkEditingBar* editingBar;
// The action sheet coordinator used when trying to edit a single bookmark.
@property(nonatomic, strong) ActionSheetCoordinator* actionSheetCoordinator;
// The view controller used to view and edit a single bookmark.
@property(nonatomic, strong) BookmarkEditViewController* editViewController;
// The view controller used to pick a folder in which to move the selected
// bookmarks.
@property(nonatomic, strong) BookmarkFolderViewController* folderSelector;
// The view controller to present when editing the current folder.
@property(nonatomic, strong) BookmarkFolderEditorViewController* folderEditor;
#pragma mark Specific to this class.
// The panel view slides on top of the content to display the menu.
@property(nonatomic, strong) BookmarkPanelView* panelView;
// Either the menu or the primaryView can scrollToTop.
@property(nonatomic, assign) BOOL scrollingMenuToTop;
// The controller managing the display of the promo cell and the promo view
// controller.
@property(nonatomic, strong) BookmarkPromoController* bookmarkPromoController;
#pragma mark View loading and switching
// This method is called if the view needs to be loaded and the model is not
// ready yet.
- (void)loadWaitingView;
// This method should be called at most once in the life-cycle of the
// class. It should be called at the soonest possible time after the
// view has been loaded, and the bookmark model is loaded.
- (void)loadBookmarkViews;
// If the view doesn't exist, create it.
- (void)ensureFolderViewExists;
// Updates the property 'primaryMenuItem'.
// Updates the UI to reflect the new state of 'primaryMenuItem'.
- (void)updatePrimaryMenuItem:(BookmarkMenuItem*)menuItem
animated:(BOOL)animated;
#pragma mark Editing related methods
// This method statelessly updates the editing top bar from |_editNodes| and
// |editing|.
- (void)updateEditingStateAnimated:(BOOL)animated;
// Shows or hides the editing bar.
- (void)showEditingBarAnimated:(BOOL)animated;
- (void)hideEditingBarAnimated:(BOOL)animated;
// Instaneously updates the shadow of the edit bar.
// This method should be called anytime:
// (1)|editing| property changes.
// (2)The primary view changes.
// (3)The primary view's collection view is scrolled.
// (2) is not necessary right now, as it is only possible to switch primary
// views when |editing| is NO. When |editing| is NO, the shadow is never shown.
- (void)updateEditBarShadow;
#pragma mark Editing bar callbacks
// The cancel button was tapped on the editing bar.
- (void)editingBarCancel;
// The move button was tapped on the editing bar.
- (void)editingBarMove;
// The delete button was tapped on the editing bar.
- (void)editingBarDelete;
// The edit button was tapped on the editing bar.
- (void)editingBarEdit;
#pragma mark Action sheet callbacks
// Enters into edit mode by selecting the given node and cell.
- (void)selectFirstNode:(const BookmarkNode*)node
withCell:(UICollectionViewCell*)cell;
// Opens the editor on the given node with a transition from cell.
- (void)editNode:(const BookmarkNode*)node;
// Opens the folder move editor for the given node.
- (void)moveNodes:(const std::set<const BookmarkNode*>&)nodes;
// Deletes the current node.
- (void)deleteNodes:(const std::set<const BookmarkNode*>&)nodes;
#pragma mark private utility methods
// Deletes the nodes, and presents a toast with an undo button.
- (void)deleteSelectedNodes;
#pragma mark Navigation bar
// Updates the UI of the navigation bar with the primaryMenuItem.
// This method should be called anytime:
// (1)The primary view changes.
// (2)The menu is shown or hidden.
// (3)The primary view has type folder, and the relevant folder has changed.
// (4)The interface orientation changes.
// (5)viewWillAppear, as the interface orientation may have changed.
- (void)updateNavigationBarAnimated:(BOOL)animated
orientation:(UIInterfaceOrientation)orientation;
- (void)updateNavigationBarWithDuration:(CGFloat)duration
orientation:(UIInterfaceOrientation)orientation;
// Whether the edit button on the navigation bar should be shown.
- (BOOL)shouldShowEditButtonWithMenuVisibility:(BOOL)visible;
// Whether the back button on the navigation bar should be preferred to the menu
// button.
- (BOOL)shouldShowBackButtonInsteadOfMenuButton;
// Called when the edit button is pressed on the navigation bar.
- (void)navigationBarWantsEditing:(id)sender;
// Called when the cancel button is pressed on the navigation bar.
- (void)navigationBarCancel:(id)sender;
// Called when the menu button is pressed on the navigation bar.
- (void)navigationBarToggledMenu:(id)sender;
// Called when the back button is pressed on the navigation bar.
- (void)navigationBarBack:(id)sender;
#pragma mark private methods
// The active collection view that corresponds to primaryMenuItem.
// This must be implemented by subclass.
- (UIView<BookmarkHomePrimaryView>*)primaryView;
// Returns the size of the primary view.
- (CGRect)frameForPrimaryView;
// Updates the UI to reflect the given orientation, with an animation lasting
// |duration|.
- (void)updateUIForInterfaceOrientation:(UIInterfaceOrientation)orientation
duration:(NSTimeInterval)duration;
// Shows or hides the menu.
- (void)showMenuAnimated:(BOOL)animated;
- (void)hideMenuAnimated:(BOOL)animated updateNavigationBar:(BOOL)update;
// Saves the current position and asks the delegate to open the url.
- (void)delegateDismiss:(const GURL&)url;
// TODO(crbug.com/450646): This should not be needed but require refactoring of
// the BookmarkCollectionViewDelegate.
- (NSIndexPath*)indexPathForCell:(UICollectionViewCell*)cell;
@end
@implementation BookmarkHomeHandsetViewController
@synthesize contentView = _contentView;
@synthesize folderView = _folderView;
@synthesize waitForModelView = _waitForModelView;
@synthesize menuView = _menuView;
@synthesize primaryMenuItem = _primaryMenuItem;
@synthesize cachedContentPosition = _cachedContentPosition;
@synthesize navigationBar = _navigationBar;
@synthesize editingBar = _editingBar;
@synthesize actionSheetCoordinator = _actionSheetCoordinator;
@synthesize editViewController = _editViewController;
@synthesize folderSelector = _folderSelector;
@synthesize folderEditor = _folderEditor;
@synthesize panelView = _panelView;
@synthesize scrollingMenuToTop = _scrollingMenuToTop;
@synthesize bookmarkPromoController = _bookmarkPromoController;
- (instancetype)initWithLoader:(id<UrlLoader>)loader
browserState:(ios::ChromeBrowserState*)browserState {
self = [super initWithLoader:loader browserState:browserState];
if (self) {
_bridge.reset(new bookmarks::BookmarkModelBridge(self, self.bookmarks));
// It is important to initialize the promo controller with the browser state
// passed in, as it could be incognito.
_bookmarkPromoController =
[[BookmarkPromoController alloc] initWithBrowserState:browserState
delegate:self];
}
return self;
}
- (void)dealloc {
_folderView.delegate = nil;
_menuView.delegate = nil;
_editViewController.delegate = nil;
_folderSelector.delegate = nil;
_panelView.delegate = nil;
}
- (void)removeEditNode:(const BookmarkNode*)node
cell:(UICollectionViewCell*)cell {
[super removeEditNode:node atIndexPath:[self indexPathForCell:cell]];
if (_editNodes.size() == 0)
[self setEditing:NO animated:YES];
else
[self updateEditingStateAnimated:YES];
}
#pragma mark - UIViewController methods
- (void)viewDidLoad {
[super viewDidLoad];
BookmarkNavigationBar* bar =
[[BookmarkNavigationBar alloc] initWithFrame:[self navigationBarFrame]];
self.navigationBar = bar;
[self.navigationBar setEditTarget:self
action:@selector(navigationBarWantsEditing:)];
[self.navigationBar setMenuTarget:self
action:@selector(navigationBarToggledMenu:)];
[self.navigationBar setBackTarget:self action:@selector(navigationBarBack:)];
[self.navigationBar setCancelTarget:self
action:@selector(navigationBarCancel:)];
[self.view addSubview:self.navigationBar];
[self.view bringSubviewToFront:self.navigationBar];
if (self.bookmarks->loaded())
[self loadBookmarkViews];
else
[self loadWaitingView];
}
- (BOOL)prefersStatusBarHidden {
return NO;
}
#pragma mark - Accessibility
- (BOOL)accessibilityPerformEscape {
[self delegateDismiss:GURL()];
return YES;
}
#pragma mark - Methods duplicated from BookmarkHomeTabletNTPController.
- (void)loadWaitingView {
DCHECK(!self.waitForModelView);
DCHECK([self isViewLoaded]);
// Present a waiting view.
BookmarkHomeWaitingView* waitingView =
[[BookmarkHomeWaitingView alloc] initWithFrame:self.view.bounds];
self.waitForModelView = waitingView;
[self.view addSubview:self.waitForModelView];
[self.waitForModelView startWaiting];
}
- (void)loadBookmarkViews {
DCHECK(self.bookmarks->loaded());
DCHECK([self isViewLoaded]);
self.panelView =
[[BookmarkPanelView alloc] initWithFrame:[self frameForPrimaryView]
menuViewWidth:kBookmarkMenuWidth];
self.panelView.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.panelView.delegate = self;
[self.view insertSubview:self.panelView atIndex:0];
self.contentView = [[UIView alloc] init];
self.contentView.frame = self.panelView.contentView.bounds;
self.contentView.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.panelView.contentView addSubview:self.contentView];
// The user can swipe the BookmarkPanelView to show the menuView.
// Therefore, it must be created here.
self.menuView = [[BookmarkMenuView alloc]
initWithBrowserState:self.browserState
frame:self.panelView.menuView.bounds];
self.menuView.delegate = self;
[self.panelView.menuView addSubview:self.menuView];
self.menuView.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
// Load the last primary menu item which the user had active.
BookmarkMenuItem* item = nil;
CGFloat position = 0;
BOOL found =
bookmark_utils_ios::GetPositionCache(self.bookmarks, &item, &position);
if (!found)
item = [self.menuView defaultMenuItem];
[self updatePrimaryMenuItem:item animated:NO];
if (found) {
// If the view has already been laid out, then immediately apply the content
// position.
if (self.view.window) {
[[self primaryView] applyContentPosition:position];
} else {
// Otherwise, save the position to be applied once the view has been laid
// out.
self.cachedContentPosition = [NSNumber numberWithFloat:position];
}
}
}
- (void)ensureFolderViewExists {
if (self.folderView)
return;
BookmarkFolderCollectionView* view = [[BookmarkFolderCollectionView alloc]
initWithBrowserState:self.browserState
frame:[self frameForPrimaryView]];
self.folderView = view;
self.folderView.delegate = self;
[self.folderView setEditing:self.editing animated:NO];
self.folderView.autoresizingMask =
UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
}
- (void)updatePrimaryMenuItem:(BookmarkMenuItem*)menuItem
animated:(BOOL)animated {
DCHECK(menuItem.type == bookmarks::MenuItemFolder);
if ([self.primaryMenuItem isEqual:menuItem])
return;
// Disable editing on previous primary view before dismissing it. No need to
// animate because this view is immediately removed from hierarchy.
if ([[self primaryMenuItem] supportsEditing])
[self.primaryView setEditing:NO animated:NO];
[[self primaryView] removeFromSuperview];
self.primaryMenuItem = menuItem;
[self ensureFolderViewExists];
[self.folderView resetFolder:self.primaryMenuItem.folder];
[self.folderView promoStateChangedAnimated:NO];
UIView* primaryView = [self primaryView];
[[self primaryView] changeOrientation:GetInterfaceOrientation()];
[[self primaryView] setScrollsToTop:!self.scrollingMenuToTop];
[self.contentView insertSubview:primaryView atIndex:0];
primaryView.frame = self.contentView.bounds;
[self updateNavigationBarAnimated:animated
orientation:GetInterfaceOrientation()];
[self.menuView updatePrimaryMenuItem:self.primaryMenuItem];
}
- (UIView<BookmarkHomePrimaryView>*)primaryView {
if (self.primaryMenuItem.type == bookmarks::MenuItemFolder)
return self.folderView;
return nil;
}
#pragma mark - Editing bar methods.
- (void)updateEditingStateAnimated:(BOOL)animated {
if (!self.editing) {
[self hideEditingBarAnimated:animated];
[self updateEditBarShadow];
[self.panelView enableSideSwiping:YES];
return;
}
if (!self.editingBar) {
self.editingBar =
[[BookmarkEditingBar alloc] initWithFrame:self.navigationBar.frame];
[self.editingBar setCancelTarget:self action:@selector(editingBarCancel)];
[self.editingBar setDeleteTarget:self action:@selector(editingBarDelete)];
[self.editingBar setMoveTarget:self action:@selector(editingBarMove)];
[self.editingBar setEditTarget:self action:@selector(editingBarEdit)];
[self.view addSubview:self.editingBar];
self.editingBar.alpha = 0;
}
int bookmarkCount = 0;
int folderCount = 0;
for (auto* node : _editNodes) {
if (node->is_url())
++bookmarkCount;
else
++folderCount;
}
[self.editingBar updateUIWithBookmarkCount:bookmarkCount
folderCount:folderCount];
[self showEditingBarAnimated:animated];
[self updateEditBarShadow];
[self.panelView enableSideSwiping:NO];
}
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
[super setEditing:editing animated:animated];
[self updateEditingStateAnimated:animated];
if ([[self primaryMenuItem] supportsEditing])
[[self primaryView] setEditing:editing animated:animated];
if (editing)
self.bookmarkPromoController.promoState = NO;
else
[self.bookmarkPromoController updatePromoState];
}
- (void)showEditingBarAnimated:(BOOL)animated {
CGRect frame = self.navigationBar.frame;
self.editingBar.hidden = NO;
if ([self respondsToSelector:@selector(setNeedsStatusBarAppearanceUpdate)])
[self setNeedsStatusBarAppearanceUpdate];
[UIView animateWithDuration:animated ? 0.2 : 0
delay:0
options:UIViewAnimationOptionBeginFromCurrentState
animations:^{
self.editingBar.alpha = 1;
self.editingBar.frame = frame;
}
completion:^(BOOL finished) {
if (finished) {
self.navigationBar.hidden = YES;
} else {
if ([self respondsToSelector:@selector(
setNeedsStatusBarAppearanceUpdate)])
[self setNeedsStatusBarAppearanceUpdate];
}
}];
}
- (void)hideEditingBarAnimated:(BOOL)animated {
CGRect frame = self.navigationBar.frame;
self.navigationBar.hidden = NO;
if ([self respondsToSelector:@selector(setNeedsStatusBarAppearanceUpdate)])
[self setNeedsStatusBarAppearanceUpdate];
[UIView animateWithDuration:animated ? 0.2 : 0
delay:0
options:UIViewAnimationOptionBeginFromCurrentState
animations:^{
self.editingBar.alpha = 0;
self.editingBar.frame = frame;
}
completion:^(BOOL finished) {
if (finished) {
self.editingBar.hidden = YES;
} else {
if ([self respondsToSelector:@selector(
setNeedsStatusBarAppearanceUpdate)])
[self setNeedsStatusBarAppearanceUpdate];
}
}];
}
- (void)updateEditBarShadow {
[self.editingBar showShadow:self.editing];
}
#pragma mark Editing Bar Callbacks
- (void)editingBarCancel {
[self setEditing:NO animated:YES];
}
- (void)editingBarMove {
[self moveNodes:_editNodes];
}
- (void)editingBarDelete {
[self deleteSelectedNodes];
[self setEditing:NO animated:YES];
}
- (void)editingBarEdit {
DCHECK_EQ(_editNodes.size(), 1u);
const BookmarkNode* node = *(_editNodes.begin());
[self editNode:node];
}
#pragma mark - BookmarkMenuViewDelegate
- (void)bookmarkMenuView:(BookmarkMenuView*)view
selectedMenuItem:(BookmarkMenuItem*)menuItem {
BOOL menuItemChanged = ![[self primaryMenuItem] isEqual:menuItem];
// If the primary menu item has changed, then updatePrimaryMenuItem: will
// update the navigation bar, and there's no need for hideMenuAnimated: to do
// so.
[self hideMenuAnimated:YES updateNavigationBar:!menuItemChanged];
if (menuItemChanged)
[self updatePrimaryMenuItem:menuItem animated:YES];
}
#pragma mark - BookmarkCollectionViewDelegate
// This class owns multiple views that have a delegate that conforms to
// BookmarkCollectionViewDelegate, or a subprotocol of
// BookmarkCollectionViewDelegate.
- (void)bookmarkCollectionView:(BookmarkCollectionView*)view
cell:(UICollectionViewCell*)cell
addNodeForEditing:(const BookmarkNode*)node {
[self insertEditNode:node atIndexPath:[self indexPathForCell:cell]];
[self updateEditingStateAnimated:YES];
}
- (void)bookmarkCollectionView:(BookmarkCollectionView*)view
cell:(UICollectionViewCell*)cell
removeNodeForEditing:(const BookmarkNode*)node {
[self removeEditNode:node atIndexPath:[self indexPathForCell:cell]];
if (_editNodes.size() == 0)
[self setEditing:NO animated:YES];
else
[self updateEditingStateAnimated:YES];
}
- (const std::set<const BookmarkNode*>&)nodesBeingEdited {
return _editNodes;
}
- (void)bookmarkCollectionViewDidScroll:(BookmarkCollectionView*)view {
[self updateEditBarShadow];
}
- (void)bookmarkCollectionView:(BookmarkCollectionView*)view
selectedUrlForNavigation:(const GURL&)url {
[self delegateDismiss:url];
}
- (void)bookmarkCollectionView:(BookmarkCollectionView*)collectionView
wantsMenuForBookmark:(const BookmarkNode*)node
onView:(UIView*)view
forCell:(BookmarkItemCell*)cell {
DCHECK(!self.editViewController);
DCHECK(!self.actionSheetCoordinator);
self.actionSheetCoordinator =
[[ActionSheetCoordinator alloc] initWithBaseViewController:self
title:nil
message:nil
rect:CGRectZero
view:nil];
__weak BookmarkHomeHandsetViewController* weakSelf = self;
// Select action.
[self.actionSheetCoordinator
addItemWithTitle:l10n_util::GetNSString(IDS_IOS_BOOKMARK_ACTION_SELECT)
action:^{
[weakSelf selectFirstNode:node withCell:cell];
weakSelf.actionSheetCoordinator = nil;
}
style:UIAlertActionStyleDefault];
// Edit action.
[self.actionSheetCoordinator
addItemWithTitle:l10n_util::GetNSString(IDS_IOS_BOOKMARK_ACTION_EDIT)
action:^{
[weakSelf editNode:node];
weakSelf.actionSheetCoordinator = nil;
}
style:UIAlertActionStyleDefault];
// Move action.
[self.actionSheetCoordinator
addItemWithTitle:l10n_util::GetNSString(IDS_IOS_BOOKMARK_ACTION_MOVE)
action:^{
std::set<const BookmarkNode*> nodes;
nodes.insert(node);
[weakSelf moveNodes:nodes];
weakSelf.actionSheetCoordinator = nil;
}
style:UIAlertActionStyleDefault];
// Delete action.
[self.actionSheetCoordinator
addItemWithTitle:l10n_util::GetNSString(IDS_IOS_BOOKMARK_ACTION_DELETE)
action:^{
std::set<const BookmarkNode*> nodes;
nodes.insert(node);
[weakSelf deleteNodes:nodes];
weakSelf.actionSheetCoordinator = nil;
}
style:UIAlertActionStyleDestructive];
// Cancel action.
[self.actionSheetCoordinator
addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL)
action:^{
weakSelf.actionSheetCoordinator = nil;
}
style:UIAlertActionStyleCancel];
[self.actionSheetCoordinator start];
}
- (void)bookmarkCollectionView:(BookmarkCollectionView*)view
didLongPressCell:(UICollectionViewCell*)cell
forBookmark:(const BookmarkNode*)node {
[self selectFirstNode:node withCell:cell];
}
- (BOOL)bookmarkCollectionViewShouldShowPromoCell:
(BookmarkCollectionView*)collectionView {
return self.bookmarkPromoController.promoState;
}
- (void)bookmarkCollectionViewShowSignIn:(BookmarkCollectionView*)view {
[self.bookmarkPromoController showSignIn];
}
- (void)bookmarkCollectionViewDismissPromo:(BookmarkCollectionView*)view {
[self.bookmarkPromoController hidePromoCell];
}
#pragma mark Action Sheet Callbacks
- (void)selectFirstNode:(const BookmarkNode*)node
withCell:(UICollectionViewCell*)cell {
DCHECK(!self.editing);
[self insertEditNode:node atIndexPath:[self indexPathForCell:cell]];
[self setEditing:YES animated:YES];
}
- (void)editNode:(const BookmarkNode*)node {
DCHECK(!self.editViewController);
DCHECK(!self.folderEditor);
UIViewController* editorController = nil;
if (node->is_folder()) {
BookmarkFolderEditorViewController* folderEditor =
[BookmarkFolderEditorViewController
folderEditorWithBookmarkModel:self.bookmarks
folder:node
browserState:self.browserState];
folderEditor.delegate = self;
self.folderEditor = folderEditor;
editorController = folderEditor;
} else {
BookmarkEditViewController* controller =
[[BookmarkEditViewController alloc] initWithBookmark:node
browserState:self.browserState];
self.editViewController = controller;
self.editViewController.delegate = self;
editorController = self.editViewController;
}
DCHECK(editorController);
UINavigationController* navController = [[BookmarkNavigationController alloc]
initWithRootViewController:editorController];
[navController setModalPresentationStyle:UIModalPresentationFormSheet];
[self presentViewController:navController animated:YES completion:NULL];
}
- (void)moveNodes:(const std::set<const BookmarkNode*>&)nodes {
DCHECK(!self.folderSelector);
DCHECK(nodes.size() > 0);
const BookmarkNode* editedNode = *(nodes.begin());
const BookmarkNode* selectedFolder = editedNode->parent();
self.folderSelector = [[BookmarkFolderViewController alloc]
initWithBookmarkModel:self.bookmarks
allowsNewFolders:YES
editedNodes:nodes
allowsCancel:YES
selectedFolder:selectedFolder];
self.folderSelector.delegate = self;
UINavigationController* navController = [[BookmarkNavigationController alloc]
initWithRootViewController:self.folderSelector];
[navController setModalPresentationStyle:UIModalPresentationFormSheet];
[self presentViewController:navController animated:YES completion:NULL];
}
- (void)deleteNodes:(const std::set<const BookmarkNode*>&)nodes {
DCHECK_GE(nodes.size(), 1u);
bookmark_utils_ios::DeleteBookmarksWithUndoToast(nodes, self.bookmarks,
self.browserState);
}
#pragma mark - BookmarkFolderCollectionViewDelegate
- (void)bookmarkFolderCollectionView:(BookmarkFolderCollectionView*)view
selectedFolderForNavigation:(const BookmarkNode*)folder {
BookmarkMenuItem* menuItem = nil;
if (view == self.folderView) {
const BookmarkNode* parent = RootLevelFolderForNode(folder, self.bookmarks);
menuItem =
[BookmarkMenuItem folderMenuItemForNode:folder rootAncestor:parent];
} else {
NOTREACHED();
return;
}
[self updatePrimaryMenuItem:menuItem animated:YES];
}
#pragma mark - BookmarkEditViewControllerDelegate
- (BOOL)bookmarkEditor:(BookmarkEditViewController*)controller
shoudDeleteAllOccurencesOfBookmark:(const BookmarkNode*)bookmark {
return NO;
}
- (void)bookmarkEditorWantsDismissal:(BookmarkEditViewController*)controller {
self.editViewController.delegate = nil;
self.editViewController = nil;
[self dismissViewControllerAnimated:YES completion:NULL];
// The editViewController can be displayed from the menu button, or from the
// edit button in edit mode. Either way, after it's dismissed, edit mode
// should be off.
[self setEditing:NO animated:NO];
}
#pragma mark - BookmarkFolderViewControllerDelegate
- (void)folderPicker:(BookmarkFolderViewController*)folderPicker
didFinishWithFolder:(const BookmarkNode*)folder {
DCHECK(folder);
DCHECK(!folder->is_url());
DCHECK_GE(folderPicker.editedNodes.size(), 1u);
bookmark_utils_ios::MoveBookmarksWithUndoToast(
folderPicker.editedNodes, self.bookmarks, folder, self.browserState);
[self setEditing:NO animated:NO];
[self dismissViewControllerAnimated:YES completion:NULL];
self.folderSelector.delegate = nil;
self.folderSelector = nil;
}
- (void)folderPickerDidCancel:(BookmarkFolderViewController*)folderPicker {
[self setEditing:NO animated:NO];
[self dismissViewControllerAnimated:YES completion:NULL];
self.folderSelector.delegate = nil;
self.folderSelector = nil;
}
#pragma mark - BookmarkFolderEditorViewControllerDelegate
- (void)bookmarkFolderEditor:(BookmarkFolderEditorViewController*)folderEditor
didFinishEditingFolder:(const BookmarkNode*)folder {
DCHECK(folder);
[self dismissViewControllerAnimated:YES completion:nil];
self.folderEditor.delegate = nil;
self.folderEditor = nil;
}
- (void)bookmarkFolderEditorDidDeleteEditedFolder:
(BookmarkFolderEditorViewController*)folderEditor {
[self dismissViewControllerAnimated:YES completion:nil];
self.folderEditor.delegate = nil;
self.folderEditor = nil;
}
- (void)bookmarkFolderEditorDidCancel:
(BookmarkFolderEditorViewController*)folderEditor {
[self dismissViewControllerAnimated:YES completion:nil];
self.folderEditor.delegate = nil;
self.folderEditor = nil;
}
#pragma mark - Internal Utility Methods
- (void)deleteSelectedNodes {
[self deleteNodes:_editNodes];
}
#pragma mark - Navigation bar
- (CGRect)navigationBarFrame {
return CGRectMake(0, 0, CGRectGetWidth(self.view.bounds),
[BookmarkNavigationBar expectedContentViewHeight] +
[self.topLayoutGuide length]);
}
- (void)updateNavigationBarAnimated:(BOOL)animated
orientation:(UIInterfaceOrientation)orientation {
CGFloat duration = animated ? bookmark_utils_ios::menuAnimationDuration : 0;
[self updateNavigationBarWithDuration:duration orientation:orientation];
}
- (void)updateNavigationBarWithDuration:(CGFloat)duration
orientation:(UIInterfaceOrientation)orientation {
if ([self.panelView userDrivenAnimationInProgress])
return;
[self.navigationBar setTitle:[self.primaryMenuItem titleForNavigationBar]];
BOOL isMenuVisible = self.panelView.showingMenu;
if ([self shouldShowEditButtonWithMenuVisibility:isMenuVisible])
[self.navigationBar showEditButtonWithAnimationDuration:duration];
else
[self.navigationBar hideEditButtonWithAnimationDuration:duration];
if ([self shouldShowBackButtonInsteadOfMenuButton])
[self.navigationBar showBackButtonInsteadOfMenuButton:duration];
else
[self.navigationBar showMenuButtonInsteadOfBackButton:duration];
}
- (BOOL)shouldShowEditButtonWithMenuVisibility:(BOOL)visible {
if (visible)
return NO;
if (self.primaryMenuItem.type != bookmarks::MenuItemFolder)
return NO;
// The type is MenuItemFolder, so it is safe to access |folder|.
return !self.bookmarks->is_permanent_node(self.primaryMenuItem.folder);
}
- (BOOL)shouldShowBackButtonInsteadOfMenuButton {
if (self.primaryMenuItem.type != bookmarks::MenuItemFolder)
return NO;
// The type is MenuItemFolder, so it is safe to access |folder|.
const BookmarkNode* folder = self.primaryMenuItem.folder;
// Show the back button iff the folder or its immediate parent is a permanent
// primary folder.
BOOL isTopFolder = IsPrimaryPermanentNode(folder, self.bookmarks) ||
IsPrimaryPermanentNode(folder->parent(), self.bookmarks);
return !isTopFolder;
}
#pragma mark Navigation Bar Callbacks
- (void)navigationBarWantsEditing:(id)sender {
DCHECK(self.primaryMenuItem.type == bookmarks::MenuItemFolder);
const BookmarkNode* folder = self.primaryMenuItem.folder;
BookmarkFolderEditorViewController* folderEditor =
[BookmarkFolderEditorViewController
folderEditorWithBookmarkModel:self.bookmarks
folder:folder
browserState:self.browserState];
folderEditor.delegate = self;
self.folderEditor = folderEditor;
BookmarkNavigationController* navController =
[[BookmarkNavigationController alloc]
initWithRootViewController:self.folderEditor];
[navController setModalPresentationStyle:UIModalPresentationFormSheet];
[self presentViewController:navController animated:YES completion:NULL];
}
- (void)navigationBarCancel:(id)sender {
[self delegateDismiss:GURL()];
}
- (void)navigationBarToggledMenu:(id)sender {
if ([self.panelView userDrivenAnimationInProgress])
return;
if (self.panelView.showingMenu)
[self hideMenuAnimated:YES updateNavigationBar:YES];
else
[self showMenuAnimated:YES];
}
- (void)navigationBarBack:(id)sender {
DCHECK([self shouldShowBackButtonInsteadOfMenuButton]);
// Go to the parent folder.
DCHECK(self.primaryMenuItem.type == bookmarks::MenuItemFolder);
const BookmarkNode* parentFolder = self.primaryMenuItem.folder->parent();
const BookmarkNode* rootAncestor =
RootLevelFolderForNode(parentFolder, self.bookmarks);
BookmarkMenuItem* menuItem =
[BookmarkMenuItem folderMenuItemForNode:parentFolder
rootAncestor:rootAncestor];
[self updatePrimaryMenuItem:menuItem animated:YES];
}
#pragma mark - Other internal methods.
- (CGRect)frameForPrimaryView {
CGFloat margin;
if (self.editing)
margin = CGRectGetMaxY(self.editingBar.frame);
else
margin = CGRectGetMaxY(self.navigationBar.frame);
CGFloat height = CGRectGetHeight(self.view.frame) - margin;
CGFloat width = CGRectGetWidth(self.view.frame);
return CGRectMake(0, margin, width, height);
}
- (void)updateUIForInterfaceOrientation:(UIInterfaceOrientation)orientation
duration:(NSTimeInterval)duration {
[[self primaryView] changeOrientation:orientation];
[self updateNavigationBarWithDuration:duration orientation:orientation];
}
- (void)showMenuAnimated:(BOOL)animated {
[self.menuView setScrollsToTop:YES];
[[self primaryView] setScrollsToTop:NO];
self.scrollingMenuToTop = YES;
[self.panelView showMenuAnimated:animated];
[self updateNavigationBarAnimated:animated
orientation:GetInterfaceOrientation()];
}
- (void)hideMenuAnimated:(BOOL)animated updateNavigationBar:(BOOL)update {
[self.menuView setScrollsToTop:NO];
[[self primaryView] setScrollsToTop:YES];
self.scrollingMenuToTop = NO;
[self.panelView hideMenuAnimated:animated];
if (update) {
UIInterfaceOrientation orient = GetInterfaceOrientation();
[self updateNavigationBarAnimated:animated orientation:orient];
}
}
#pragma mark - BookmarkHomeViewController
- (void)dismissModals:(BOOL)animated {
[self.actionSheetCoordinator stop];
self.actionSheetCoordinator = nil;
}
#pragma mark - BookmarkPanelViewDelegate
- (void)bookmarkPanelView:(BookmarkPanelView*)view
willShowMenu:(BOOL)showMenu
withAnimationDuration:(CGFloat)duration {
if (showMenu) {
[self.menuView setScrollsToTop:YES];
[[self primaryView] setScrollsToTop:NO];
self.scrollingMenuToTop = YES;
} else {
[self.menuView setScrollsToTop:NO];
[[self primaryView] setScrollsToTop:YES];
self.scrollingMenuToTop = NO;
}
if ([self shouldShowEditButtonWithMenuVisibility:showMenu])
[self.navigationBar showEditButtonWithAnimationDuration:duration];
else
[self.navigationBar hideEditButtonWithAnimationDuration:duration];
}
- (void)bookmarkPanelView:(BookmarkPanelView*)view
updatedMenuVisibility:(CGFloat)visibility {
// As the menu becomes more visible, the edit button will always fade out.
if ([self shouldShowEditButtonWithMenuVisibility:NO])
[self.navigationBar updateEditButtonVisibility:1 - visibility];
else
[self.navigationBar updateEditButtonVisibility:0];
}
#pragma mark - BookmarkModelBridgeObserver
- (void)bookmarkModelLoaded {
if (![self isViewLoaded])
return;
DCHECK(self.waitForModelView);
__weak BookmarkHomeHandsetViewController* weakSelf = self;
[self.waitForModelView stopWaitingWithCompletion:^{
BookmarkHomeHandsetViewController* strongSelf = weakSelf;
// Early return if the controller has been deallocated.
if (!strongSelf)
return;
[UIView animateWithDuration:0.2
animations:^{
strongSelf.waitForModelView.alpha = 0.0;
}
completion:^(BOOL finished) {
[strongSelf.waitForModelView removeFromSuperview];
strongSelf.waitForModelView = nil;
}];
[strongSelf loadBookmarkViews];
}];
}
- (void)bookmarkNodeChanged:(const BookmarkNode*)node {
// The title of the folder may have changed.
if (self.primaryMenuItem.type == bookmarks::MenuItemFolder &&
self.primaryMenuItem.folder == node) {
UIInterfaceOrientation orient = GetInterfaceOrientation();
[self updateNavigationBarAnimated:NO orientation:orient];
}
}
- (void)bookmarkNodeChildrenChanged:(const BookmarkNode*)bookmarkNode {
// The node has not changed, but the ordering and existence of its children
// have changed.
}
- (void)bookmarkNode:(const BookmarkNode*)bookmarkNode
movedFromParent:(const BookmarkNode*)oldParent
toParent:(const BookmarkNode*)newParent {
// The node has moved to a new parent folder.
}
- (void)bookmarkNodeDeleted:(const BookmarkNode*)node
fromFolder:(const BookmarkNode*)folder {
[self removeEditNode:node atIndexPath:nil];
}
- (void)bookmarkModelRemovedAllNodes {
// All non-permanent nodes have been removed.
[self setEditing:NO animated:YES];
}
- (UIStatusBarStyle)preferredStatusBarStyle {
return self.editing ? UIStatusBarStyleLightContent : UIStatusBarStyleDefault;
}
// There are 2 UIViewController methods that could be overridden. This one and
// willAnimateRotationToInterfaceOrientation:duration:. The latter is called
// during an "animation block", and its unclear that reloading a collection view
// will always work duratin an "animation block", although it appears to work at
// first glance.
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orientation
duration:(NSTimeInterval)duration {
[self updateUIForInterfaceOrientation:orientation duration:duration];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
UIInterfaceOrientation orient = GetInterfaceOrientation();
[self updateUIForInterfaceOrientation:orient duration:0];
if (self.cachedContentPosition) {
[[self primaryView]
applyContentPosition:[self.cachedContentPosition floatValue]];
self.cachedContentPosition = nil;
}
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
// Invalidate the layout of the collection view, as its frame might have
// changed. Normally, this can be done automatically when the collection view
// layout returns YES to -shouldInvalidateLayoutForBoundsChange:.
// Unfortunately, it doesn't happen for all bounds changes. E.g. on iPhone 6
// Plus landscape, the width of the collection is too large when created, then
// resized down before being presented. Yet, the bounds change doesn't yield a
// call to the flow layout, thus not invalidating the layout correctly.
[[self primaryView].collectionView.collectionViewLayout invalidateLayout];
self.navigationBar.frame = [self navigationBarFrame];
self.editingBar.frame = [self navigationBarFrame];
[self.panelView setFrame:[self frameForPrimaryView]];
}
#pragma mark - Internal non-UI Methods
- (void)delegateDismiss:(const GURL&)url {
if ([self primaryView]) {
bookmark_utils_ios::CachePosition(
[[self primaryView] contentPositionInPortraitOrientation],
[self primaryMenuItem]);
}
[self.delegate bookmarkHomeViewControllerWantsDismissal:self
navigationToUrl:url];
}
#pragma mark - BookmarkPromoControllerDelegate
- (void)promoStateChanged:(BOOL)promoEnabled {
[self.folderView
promoStateChangedAnimated:self.folderView == [self primaryView]];
}
- (NSIndexPath*)indexPathForCell:(UICollectionViewCell*)cell {
DCHECK([self primaryView].collectionView);
NSIndexPath* indexPath =
[[self primaryView].collectionView indexPathForCell:cell];
return indexPath;
}
@end