Index: ios/chrome/browser/ui/bookmarks/bookmark_folder_collection_view.mm |
diff --git a/ios/chrome/browser/ui/bookmarks/bookmark_folder_collection_view.mm b/ios/chrome/browser/ui/bookmarks/bookmark_folder_collection_view.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..7f68a85e91e340eb4a49d2ddf5d073ca9b62a86a |
--- /dev/null |
+++ b/ios/chrome/browser/ui/bookmarks/bookmark_folder_collection_view.mm |
@@ -0,0 +1,474 @@ |
+// 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/mac/objc_property_releaser.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" |
+ |
+using bookmarks::BookmarkNode; |
+ |
+@interface BookmarkFolderCollectionView ()<BookmarkPromoCellDelegate> { |
+ base::mac::ObjCPropertyReleaser |
+ _propertyReleaser_BookmarkFolderCollectionView; |
+ // 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, retain) 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) { |
+ _propertyReleaser_BookmarkFolderCollectionView.Init( |
+ self, [BookmarkFolderCollectionView class]); |
+ |
+ [self updateCollectionView]; |
+ } |
+ return self; |
+} |
+ |
+- (void)dealloc { |
+ _promoCell.delegate = nil; |
+ [super dealloc]; |
+} |
+ |
+- (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 { |
+ if (experimental_flags::IsAllBookmarksEnabled()) |
+ return; // The promo is not shown if All Bookmarks is enabled. |
+ |
+ 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 |