blob: 4b2a16a4b81bd8dc1d8021e97856d84a8203b35a [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_folder_collection_view.h"
#include "base/logging.h"
#include "base/strings/sys_string_conversions.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "ios/chrome/browser/bookmarks/bookmarks_utils.h"
#include "ios/chrome/browser/experimental_flags.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_collection_cells.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_promo_cell.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_utils_ios.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using bookmarks::BookmarkNode;
@interface BookmarkFolderCollectionView ()<BookmarkPromoCellDelegate> {
// A vector of folders to display in the collection view.
std::vector<const BookmarkNode*> _subFolders;
// A vector of bookmark urls to display in the collection view.
std::vector<const BookmarkNode*> _subItems;
// True if the promo is visible.
BOOL _promoVisible;
}
@property(nonatomic, assign) const bookmarks::BookmarkNode* folder;
// Section indices.
@property(nonatomic, readonly, assign) NSInteger promoSection;
@property(nonatomic, readonly, assign) NSInteger folderSection;
@property(nonatomic, readonly, assign) NSInteger itemsSection;
@property(nonatomic, readonly, assign) NSInteger sectionCount;
// Keep a reference to the promo cell to deregister as delegate.
@property(nonatomic, strong) BookmarkPromoCell* promoCell;
@end
@implementation BookmarkFolderCollectionView
@synthesize delegate = _delegate;
@synthesize folder = _folder;
@synthesize promoCell = _promoCell;
- (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState
frame:(CGRect)frame {
self = [super initWithBrowserState:browserState frame:frame];
if (self) {
[self updateCollectionView];
}
return self;
}
- (void)dealloc {
_promoCell.delegate = nil;
}
- (void)setDelegate:(id<BookmarkFolderCollectionViewDelegate>)delegate {
_delegate = delegate;
[self promoStateChangedAnimated:NO];
}
- (NSInteger)promoSection {
return [self shouldShowPromoCell] ? 0 : -1;
}
- (NSInteger)folderSection {
return [self shouldShowPromoCell] ? 1 : 0;
}
- (NSInteger)itemsSection {
return [self shouldShowPromoCell] ? 2 : 1;
}
- (NSInteger)sectionCount {
return [self shouldShowPromoCell] ? 3 : 2;
}
- (void)updateCollectionView {
if (!self.bookmarkModel->loaded())
return;
// Regenerate the list of all bookmarks.
_subFolders = std::vector<const BookmarkNode*>();
_subItems = std::vector<const BookmarkNode*>();
if (self.folder) {
int childCount = self.folder->child_count();
for (int i = 0; i < childCount; ++i) {
const BookmarkNode* node = self.folder->GetChild(i);
if (node->is_folder())
_subFolders.push_back(node);
else
_subItems.push_back(node);
}
bookmark_utils_ios::SortFolders(&_subFolders);
}
[self cancelAllFaviconLoads];
[self.collectionView reloadData];
}
#pragma mark - BookmarkModelBridgeObserver Callbacks
- (void)bookmarkModelLoaded {
[self updateCollectionView];
}
- (void)bookmarkNodeChanged:(const BookmarkNode*)bookmarkNode {
// The base folder changed. Do nothing.
if (bookmarkNode == self.folder)
return;
// A specific cell changed. Reload that cell.
NSIndexPath* indexPath = [self indexPathForNode:bookmarkNode];
if (indexPath) {
// TODO(crbug.com/603661): Ideally, we would only reload the relevant index
// path. However, calling reloadItemsAtIndexPaths:(0,0) immediately after
// reloadData results in a exception: NSInternalInconsistencyException
// 'request for index path for global index 2147483645 ...'
// One solution would be to keep track of whether we've just called
// reloadData, but that requires experimentation to determine how long we
// have to wait before we can safely call reloadItemsAtIndexPaths.
[self updateCollectionView];
}
}
- (void)bookmarkNodeFaviconChanged:
(const bookmarks::BookmarkNode*)bookmarkNode {
// Only urls have favicons.
DCHECK(bookmarkNode->is_url());
// Update image of corresponding cell.
NSIndexPath* indexPath = [self indexPathForNode:bookmarkNode];
if (!indexPath)
return;
// Check that this cell is visible.
NSArray* visiblePaths = [self.collectionView indexPathsForVisibleItems];
if (![visiblePaths containsObject:indexPath])
return;
[self loadFaviconAtIndexPath:indexPath];
}
- (NSIndexPath*)indexPathForNode:(const bookmarks::BookmarkNode*)bookmarkNode {
NSIndexPath* indexPath = nil;
if (bookmarkNode->is_folder()) {
std::vector<const BookmarkNode*>::iterator it =
std::find(_subFolders.begin(), _subFolders.end(), bookmarkNode);
if (it != _subFolders.end()) {
ptrdiff_t index = std::distance(_subFolders.begin(), it);
indexPath =
[NSIndexPath indexPathForRow:index inSection:self.folderSection];
}
} else if (bookmarkNode->is_url()) {
std::vector<const BookmarkNode*>::iterator it =
std::find(_subItems.begin(), _subItems.end(), bookmarkNode);
if (it != _subItems.end()) {
ptrdiff_t index = std::distance(_subItems.begin(), it);
indexPath =
[NSIndexPath indexPathForRow:index inSection:self.itemsSection];
}
}
return indexPath;
}
- (void)bookmarkNodeDeleted:(const BookmarkNode*)node
fromFolder:(const BookmarkNode*)folder {
if (self.folder == node) {
self.folder = nil;
[self updateCollectionView];
}
}
- (void)bookmarkNodeChildrenChanged:(const BookmarkNode*)bookmarkNode {
// The base folder's children changed. Reload everything.
if (bookmarkNode == self.folder) {
[self updateCollectionView];
return;
}
// A subfolder's children changed. Reload that cell.
std::vector<const BookmarkNode*>::iterator it =
std::find(_subFolders.begin(), _subFolders.end(), bookmarkNode);
if (it != _subFolders.end()) {
// TODO(crbug.com/603661): Ideally, we would only reload the relevant index
// path. However, calling reloadItemsAtIndexPaths:(0,0) immediately after
// reloadData results in a exception: NSInternalInconsistencyException
// 'request for index path for global index 2147483645 ...'
// One solution would be to keep track of whether we've just called
// reloadData, but that requires experimentation to determine how long we
// have to wait before we can safely call reloadItemsAtIndexPaths.
[self updateCollectionView];
}
}
- (void)bookmarkNode:(const BookmarkNode*)bookmarkNode
movedFromParent:(const BookmarkNode*)oldParent
toParent:(const BookmarkNode*)newParent {
if (oldParent == self.folder || newParent == self.folder) {
// A folder was added or removed from the base folder.
[self updateCollectionView];
}
}
- (void)bookmarkModelRemovedAllNodes {
self.folder = nil;
[self updateCollectionView];
}
#pragma mark - Parent class overrides that affect functionality
- (void)collectionView:(UICollectionView*)collectionView
willDisplayCell:(UICollectionViewCell*)cell
forItemAtIndexPath:(NSIndexPath*)indexPath {
if (indexPath.section == self.itemsSection) {
[self loadFaviconAtIndexPath:indexPath];
}
}
- (void)didAddCellForEditingAtIndexPath:(NSIndexPath*)indexPath {
const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
UICollectionViewCell* cell =
[self.collectionView cellForItemAtIndexPath:indexPath];
[self.delegate bookmarkCollectionView:self cell:cell addNodeForEditing:node];
}
- (void)didRemoveCellForEditingAtIndexPath:(NSIndexPath*)indexPath {
const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
UICollectionViewCell* cell =
[self.collectionView cellForItemAtIndexPath:indexPath];
[self.delegate bookmarkCollectionView:self
cell:cell
removeNodeForEditing:node];
}
- (void)didTapCellAtIndexPath:(NSIndexPath*)indexPath {
if (indexPath.section == self.promoSection) {
// User tapped inside promo cell but not on one of the buttons. Ignore it.
return;
}
const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
DCHECK(node);
if (indexPath.section == self.folderSection) {
[self.delegate bookmarkFolderCollectionView:self
selectedFolderForNavigation:node];
} else {
RecordBookmarkLaunch(BOOKMARK_LAUNCH_LOCATION_FOLDER);
[self.delegate bookmarkCollectionView:self
selectedUrlForNavigation:node->url()];
}
}
- (void)didTapMenuButtonAtIndexPath:(NSIndexPath*)indexPath
onView:(UIView*)view
forCell:(BookmarkItemCell*)cell {
[self.delegate bookmarkCollectionView:self
wantsMenuForBookmark:[self nodeAtIndexPath:indexPath]
onView:view
forCell:cell];
}
- (bookmark_cell::ButtonType)buttonTypeForCellAtIndexPath:
(NSIndexPath*)indexPath {
return self.editing ? bookmark_cell::ButtonNone : bookmark_cell::ButtonMenu;
}
- (BOOL)allowLongPressForCellAtIndexPath:(NSIndexPath*)indexPath {
return !self.editing;
}
- (void)didLongPressCell:(UICollectionViewCell*)cell
atIndexPath:(NSIndexPath*)indexPath {
if (indexPath.section == self.promoSection) {
// User long-pressed inside promo cell. Ignore it.
return;
}
[self.delegate bookmarkCollectionView:self
didLongPressCell:cell
forBookmark:[self nodeAtIndexPath:indexPath]];
}
- (BOOL)shouldSelectCellAtIndexPath:(NSIndexPath*)indexPath {
return YES;
}
- (BOOL)cellIsSelectedForEditingAtIndexPath:(NSIndexPath*)indexPath {
const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
const std::set<const BookmarkNode*>& editingNodes =
[self.delegate nodesBeingEdited];
return editingNodes.find(node) != editingNodes.end();
}
- (const BookmarkNode*)nodeAtIndexPath:(NSIndexPath*)indexPath {
if (indexPath.section == self.folderSection)
return _subFolders[indexPath.row];
if (indexPath.section == self.itemsSection)
return _subItems[indexPath.row];
NOTREACHED();
return nullptr;
}
- (void)resetFolder:(const BookmarkNode*)folder {
DCHECK(folder->is_folder());
self.folder = folder;
[self updateCollectionView];
}
- (CGFloat)interitemSpacingForSectionAtIndex:(NSInteger)section {
CGFloat interitemSpacing = 0;
SEL minimumInteritemSpacingSelector = @selector(collectionView:
layout:
minimumInteritemSpacingForSectionAtIndex:);
if ([self.collectionView.delegate
respondsToSelector:minimumInteritemSpacingSelector]) {
id collectionViewDelegate = static_cast<id>(self.collectionView.delegate);
interitemSpacing =
[collectionViewDelegate collectionView:self.collectionView
layout:self.collectionView
.collectionViewLayout
minimumInteritemSpacingForSectionAtIndex:section];
} else if ([self.collectionView.collectionViewLayout
isKindOfClass:[UICollectionViewFlowLayout class]]) {
UICollectionViewFlowLayout* flowLayout =
static_cast<UICollectionViewFlowLayout*>(
self.collectionView.collectionViewLayout);
interitemSpacing = flowLayout.minimumInteritemSpacing;
}
return interitemSpacing;
}
// Parent class override.
- (UIEdgeInsets)insetForSectionAtIndex:(NSInteger)section {
UIEdgeInsets insets = [super insetForSectionAtIndex:section];
if (section == self.folderSection)
insets.bottom = [self interitemSpacingForSectionAtIndex:section] / 2.;
else if (section == self.itemsSection)
insets.top = [self interitemSpacingForSectionAtIndex:section] / 2.;
else if (section == self.promoSection)
(void)0; // No insets to update.
else
NOTREACHED();
return insets;
}
// Parent class override.
- (UICollectionViewCell*)cellAtIndexPath:(NSIndexPath*)indexPath {
if (indexPath.section == self.promoSection) {
self.promoCell = [self.collectionView
dequeueReusableCellWithReuseIdentifier:[BookmarkPromoCell
reuseIdentifier]
forIndexPath:indexPath];
self.promoCell.delegate = self;
return self.promoCell;
}
const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
if (indexPath.section == self.folderSection)
return [self cellForFolder:node indexPath:indexPath];
BookmarkItemCell* cell = [self cellForBookmark:node indexPath:indexPath];
return cell;
}
- (BOOL)needsSectionHeaderForSection:(NSInteger)section {
// Only show header when there is at least one element in the previous
// section.
if (section == 0)
return NO;
if ([self numberOfItemsInSection:(section - 1)] == 0)
return NO;
return YES;
}
// Parent class override.
- (CGSize)headerSizeForSection:(NSInteger)section {
if ([self needsSectionHeaderForSection:section])
return CGSizeMake(self.bounds.size.width,
[BookmarkHeaderSeparatorView preferredHeight]);
return CGSizeZero;
}
// Parent class override.
- (UICollectionReusableView*)headerAtIndexPath:(NSIndexPath*)indexPath {
if (![self needsSectionHeaderForSection:indexPath.section])
return nil;
BookmarkHeaderSeparatorView* view = [self.collectionView
dequeueReusableSupplementaryViewOfKind:
UICollectionElementKindSectionHeader
withReuseIdentifier:[BookmarkHeaderSeparatorView
reuseIdentifier]
forIndexPath:indexPath];
view.backgroundColor = [UIColor colorWithWhite:1 alpha:1];
return view;
}
- (NSInteger)numberOfItemsInSection:(NSInteger)section {
if (section == self.folderSection)
return _subFolders.size();
if (section == self.itemsSection)
return _subItems.size();
if (section == self.promoSection)
return 1;
NOTREACHED();
return -1;
}
- (NSInteger)numberOfSections {
return self.sectionCount;
}
- (void)collectionViewScrolled {
[self.delegate bookmarkCollectionViewDidScroll:self];
}
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
[super setEditing:editing animated:animated];
[self promoStateChangedAnimated:animated];
}
- (void)promoStateChangedAnimated:(BOOL)animate {
BOOL newPromoState =
!self.editing && self.folder &&
self.folder->type() == BookmarkNode::MOBILE &&
[self.delegate bookmarkCollectionViewShouldShowPromoCell:self];
if (newPromoState != _promoVisible) {
// This is awful, but until the old code to do the refresh when switching
// in and out of edit mode is fixed, this is probably the cleanest thing to
// do.
_promoVisible = newPromoState;
[self.collectionView reloadData];
}
}
#pragma mark - BookmarkPromoCellDelegate
- (void)bookmarkPromoCellDidTapSignIn:(BookmarkPromoCell*)bookmarkPromoCell {
[self.delegate bookmarkCollectionViewShowSignIn:self];
}
- (void)bookmarkPromoCellDidTapDismiss:(BookmarkPromoCell*)bookmarkPromoCell {
[self.delegate bookmarkCollectionViewDismissPromo:self];
}
#pragma mark - Promo Cell
- (BOOL)shouldShowPromoCell {
return _promoVisible;
}
@end