| Index: ios/chrome/browser/ui/bookmarks/bookmark_all_collection_view.mm
|
| diff --git a/ios/chrome/browser/ui/bookmarks/bookmark_all_collection_view.mm b/ios/chrome/browser/ui/bookmarks/bookmark_all_collection_view.mm
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..b1aacd6ae4b6e08a97a44fc1fc3b283d3104f4fb
|
| --- /dev/null
|
| +++ b/ios/chrome/browser/ui/bookmarks/bookmark_all_collection_view.mm
|
| @@ -0,0 +1,414 @@
|
| +// 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_all_collection_view.h"
|
| +
|
| +#include "base/logging.h"
|
| +#include "base/mac/objc_property_releaser.h"
|
| +#include "base/mac/scoped_nsobject.h"
|
| +#include "base/strings/sys_string_conversions.h"
|
| +#include "components/bookmarks/browser/bookmark_model.h"
|
| +#include "ios/chrome/browser/bookmarks/bookmarks_utils.h"
|
| +#import "ios/chrome/browser/ui/bookmarks/bookmark_collection_cells.h"
|
| +#include "ios/chrome/browser/ui/bookmarks/bookmark_promo_cell.h"
|
| +#import "ios/chrome/browser/ui/bookmarks/bookmark_utils_ios.h"
|
| +#include "ui/base/l10n/l10n_util.h"
|
| +#include "ui/base/models/tree_node_iterator.h"
|
| +
|
| +using bookmarks::BookmarkNode;
|
| +
|
| +namespace {
|
| +typedef std::vector<const BookmarkNode*> NodeVector;
|
| +// Each section of the collection view corresponds to a NodesSection object,
|
| +// sorted by creation date.
|
| +using bookmark_utils_ios::NodesSection;
|
| +
|
| +// There is sometimes a flurry of events from the bookmark model. When this
|
| +// happens the data the collection view is displaying needs to be recalculated
|
| +// and the collection view needs to be reloaded. The recalculation of the data
|
| +// is expensive, so instead of doing the update over and over again the update
|
| +// is done once and all other updates received during the same runloop event are
|
| +// deferred to the next turn of the runloop (if everything is deferred this
|
| +// unfortunately triggers a bug in the UICollectionView sometimes).
|
| +// This enum tracks the state of the refresh. See -collectionViewNeedsUpdate
|
| +// for the state machine.
|
| +typedef enum { kNoUpdate = 0, kOneUpdateDone, kUpdateScheduled } UpdateState;
|
| +} // namespace
|
| +
|
| +@interface BookmarkAllCollectionView ()<BookmarkPromoCellDelegate> {
|
| + // A vector of vectors. Url nodes are segregated by month of creation.
|
| + ScopedVector<NodesSection> _nodesSectionVector;
|
| + // To avoid refreshing the internal model too often.
|
| + UpdateState _updateScheduled;
|
| + base::mac::ObjCPropertyReleaser _propertyReleaser_BookmarkAllCollectionView;
|
| +}
|
| +
|
| +// Keep a reference to the promo cell to deregister as delegate.
|
| +@property(nonatomic, retain) BookmarkPromoCell* promoCell;
|
| +
|
| +// Triggers an update of the collection, but delayed in order to coallesce a lot
|
| +// of events into one update.
|
| +- (void)collectionViewNeedsUpdate;
|
| +
|
| +@end
|
| +
|
| +@implementation BookmarkAllCollectionView
|
| +
|
| +@synthesize delegate = _delegate;
|
| +@synthesize promoCell = _promoCell;
|
| +
|
| +- (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState
|
| + frame:(CGRect)frame {
|
| + self = [super initWithBrowserState:browserState frame:frame];
|
| + if (self) {
|
| + _propertyReleaser_BookmarkAllCollectionView.Init(
|
| + self, [BookmarkAllCollectionView class]);
|
| + self.accessibilityIdentifier = @"bookmark_all_collection_view";
|
| + [self updateCollectionView];
|
| + }
|
| + return self;
|
| +}
|
| +
|
| +- (void)dealloc {
|
| + _promoCell.delegate = nil;
|
| + [super dealloc];
|
| +}
|
| +
|
| +- (void)updateCollectionView {
|
| + if (!self.bookmarkModel->loaded())
|
| + return;
|
| +
|
| + // Regenerate the list of all bookmarks.
|
| + NodeVector allItems;
|
| + ui::TreeNodeIterator<const BookmarkNode> iterator(
|
| + self.bookmarkModel->root_node());
|
| +
|
| + while (iterator.has_next()) {
|
| + const BookmarkNode* bookmark = iterator.Next();
|
| +
|
| + if (bookmark->is_url())
|
| + allItems.push_back(bookmark);
|
| + }
|
| +
|
| + // Perform segregation.
|
| + bookmark_utils_ios::segregateNodes(allItems, _nodesSectionVector);
|
| +
|
| + [self cancelAllFaviconLoads];
|
| + [self.collectionView reloadData];
|
| +}
|
| +
|
| +- (void)collectionViewNeedsUpdate {
|
| + switch (_updateScheduled) {
|
| + case kNoUpdate:
|
| + // If the collection view was not updated recently, update it now.
|
| + [self updateCollectionView];
|
| + _updateScheduled = kOneUpdateDone;
|
| + // And reset the state when going back to the main loop.
|
| + dispatch_async(dispatch_get_main_queue(), ^{
|
| + _updateScheduled = kNoUpdate;
|
| + });
|
| + break;
|
| + case kOneUpdateDone:
|
| + // An update was already done on this turn of the main loop, schedule the
|
| + // next update for later.
|
| + _updateScheduled = kUpdateScheduled;
|
| + dispatch_async(dispatch_get_main_queue(), ^{
|
| + if (_updateScheduled == kNoUpdate)
|
| + [self updateCollectionView];
|
| + });
|
| + break;
|
| + case kUpdateScheduled:
|
| + // Nothing to do.
|
| + break;
|
| + }
|
| +}
|
| +
|
| +#pragma mark - BookmarkModelBridgeObserver Callbacks
|
| +
|
| +- (void)bookmarkModelLoaded {
|
| + [self updateCollectionView];
|
| +}
|
| +
|
| +- (void)bookmarkNodeChanged:(const BookmarkNode*)bookmarkNode {
|
| + if (bookmarkNode->is_folder())
|
| + return;
|
| +
|
| + // 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];
|
| +}
|
| +
|
| +- (void)bookmarkNodeChildrenChanged:(const BookmarkNode*)bookmarkNode {
|
| + [self collectionViewNeedsUpdate];
|
| +}
|
| +
|
| +- (void)bookmarkNode:(const BookmarkNode*)bookmarkNode
|
| + movedFromParent:(const BookmarkNode*)oldParent
|
| + toParent:(const BookmarkNode*)newParent {
|
| + [self updateCollectionView];
|
| +}
|
| +
|
| +- (void)bookmarkNodeDeleted:(const BookmarkNode*)node
|
| + fromFolder:(const BookmarkNode*)folder {
|
| + // Only remove the node from the list of all nodes. Since we also receive a
|
| + // 'bookmarkNodeChildrenChanged' callback, the collection view will be updated
|
| + // there.
|
| + for (NodesSection* nodesSection : _nodesSectionVector) {
|
| + NodeVector nodeVector = nodesSection->vector;
|
| + // If the node was in _nodesSectionVector, it is now invalid. In that case,
|
| + // remove it from _nodesSectionVector.
|
| + auto it = std::find(nodeVector.begin(), nodeVector.end(), node);
|
| + if (it != nodeVector.end()) {
|
| + nodeVector.erase(it);
|
| + nodesSection->vector = nodeVector;
|
| + break;
|
| + }
|
| + }
|
| +}
|
| +
|
| +- (void)bookmarkModelRemovedAllNodes {
|
| + [self updateCollectionView];
|
| +}
|
| +
|
| +#pragma mark - Parent class overrides that affect functionality
|
| +
|
| +- (void)collectionView:(UICollectionView*)collectionView
|
| + willDisplayCell:(UICollectionViewCell*)cell
|
| + forItemAtIndexPath:(NSIndexPath*)indexPath {
|
| + auto node = [self nodeAtIndexPath:indexPath];
|
| + if (node && node->type() == bookmarks::BookmarkNode::URL) {
|
| + [self loadFaviconAtIndexPath:indexPath];
|
| + }
|
| +}
|
| +
|
| +- (void)didAddCellForEditingAtIndexPath:(NSIndexPath*)indexPath {
|
| + DCHECK(![self isPromoSection:indexPath.section]);
|
| + const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
|
| + UICollectionViewCell* cell =
|
| + [self.collectionView cellForItemAtIndexPath:indexPath];
|
| + [self.delegate bookmarkCollectionView:self cell:cell addNodeForEditing:node];
|
| +}
|
| +
|
| +- (void)didRemoveCellForEditingAtIndexPath:(NSIndexPath*)indexPath {
|
| + DCHECK(![self isPromoSection:indexPath.section]);
|
| + const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
|
| + UICollectionViewCell* cell =
|
| + [self.collectionView cellForItemAtIndexPath:indexPath];
|
| + [self.delegate bookmarkCollectionView:self
|
| + cell:cell
|
| + removeNodeForEditing:node];
|
| +}
|
| +
|
| +- (BOOL)shouldSelectCellAtIndexPath:(NSIndexPath*)indexPath {
|
| + return ![self isPromoSection:indexPath.section];
|
| +}
|
| +
|
| +- (void)didTapCellAtIndexPath:(NSIndexPath*)indexPath {
|
| + DCHECK(![self isPromoSection:indexPath.section]);
|
| + const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
|
| + DCHECK(node);
|
| + RecordBookmarkLaunch(BOOKMARK_LAUNCH_LOCATION_ALL_ITEMS);
|
| + [self.delegate bookmarkCollectionView:self
|
| + selectedUrlForNavigation:node->url()];
|
| +}
|
| +
|
| +- (void)didTapMenuButtonAtIndexPath:(NSIndexPath*)indexPath
|
| + onView:(UIView*)view
|
| + forCell:(BookmarkItemCell*)cell {
|
| + DCHECK(![self isPromoSection:indexPath.section]);
|
| + [self.delegate bookmarkCollectionView:self
|
| + wantsMenuForBookmark:[self nodeAtIndexPath:indexPath]
|
| + onView:view
|
| + forCell:cell];
|
| +}
|
| +
|
| +- (bookmark_cell::ButtonType)buttonTypeForCellAtIndexPath:
|
| + (NSIndexPath*)indexPath {
|
| + DCHECK(![self isPromoSection:indexPath.section]);
|
| + if (self.editing)
|
| + return bookmark_cell::ButtonNone;
|
| + return bookmark_cell::ButtonMenu;
|
| +}
|
| +
|
| +- (BOOL)allowLongPressForCellAtIndexPath:(NSIndexPath*)indexPath {
|
| + return [self isPromoSection:indexPath.section] ? NO : !self.editing;
|
| +}
|
| +
|
| +- (void)didLongPressCell:(UICollectionViewCell*)cell
|
| + atIndexPath:(NSIndexPath*)indexPath {
|
| + DCHECK(![self isPromoSection:indexPath.section]);
|
| + [self.delegate bookmarkCollectionView:self
|
| + didLongPressCell:cell
|
| + forBookmark:[self nodeAtIndexPath:indexPath]];
|
| +}
|
| +
|
| +- (BOOL)cellIsSelectedForEditingAtIndexPath:(NSIndexPath*)indexPath {
|
| + DCHECK(![self isPromoSection:indexPath.section]);
|
| +
|
| + 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 {
|
| + NSInteger section = indexPath.section;
|
| + if ([self isPromoSection:section])
|
| + return nullptr;
|
| +
|
| + if ([self shouldShowPromoCell])
|
| + --section;
|
| + return _nodesSectionVector[section]->vector[indexPath.row];
|
| +}
|
| +
|
| +- (NSIndexPath*)indexPathForNode:(const BookmarkNode*)bookmarkNode {
|
| + NSInteger section = 0;
|
| +
|
| + // When showing promo cell, bookmarks start with section 1.
|
| + if ([self shouldShowPromoCell])
|
| + section = 1;
|
| +
|
| + for (NodesSection* nodesSection : _nodesSectionVector) {
|
| + NodeVector nodeVector = nodesSection->vector;
|
| + NSInteger item = 0;
|
| + for (const BookmarkNode* node : nodeVector) {
|
| + if (bookmarkNode == node) {
|
| + return [NSIndexPath indexPathForItem:item inSection:section];
|
| + }
|
| + ++item;
|
| + }
|
| + ++section;
|
| + }
|
| +
|
| + return nil;
|
| +}
|
| +
|
| +#pragma mark - Parent class overrides that change UI
|
| +
|
| +// Parent class override.
|
| +- (UIEdgeInsets)insetForSectionAtIndex:(NSInteger)section {
|
| + // Only return insets for non-empty sections.
|
| + NSInteger count = [self numberOfItemsInSection:section];
|
| + if (count == 0)
|
| + return UIEdgeInsetsZero;
|
| +
|
| + // The last section needs special treatment.
|
| + UIEdgeInsets insets = [super insetForSectionAtIndex:section];
|
| + NSInteger sectionCount = [self.collectionView.dataSource
|
| + numberOfSectionsInCollectionView:self.collectionView];
|
| +
|
| + if (section == sectionCount - 1) {
|
| + insets.top = 0;
|
| + return insets;
|
| + }
|
| +
|
| + insets.top = 0;
|
| + insets.bottom = 0;
|
| + return insets;
|
| +}
|
| +
|
| +// Parent class override.
|
| +- (UICollectionViewCell*)cellAtIndexPath:(NSIndexPath*)indexPath {
|
| + if ([self isPromoSection:indexPath.section]) {
|
| + self.promoCell = [self.collectionView
|
| + dequeueReusableCellWithReuseIdentifier:[BookmarkPromoCell
|
| + reuseIdentifier]
|
| + forIndexPath:indexPath];
|
| + self.promoCell.delegate = self;
|
| + return self.promoCell;
|
| + }
|
| +
|
| + return [self cellForBookmark:[self nodeAtIndexPath:indexPath]
|
| + indexPath:indexPath];
|
| +}
|
| +
|
| +// Parent class override.
|
| +- (CGSize)headerSizeForSection:(NSInteger)section {
|
| + if ([self isPromoSection:section])
|
| + return CGSizeZero;
|
| +
|
| + return CGSizeMake(self.bounds.size.width, [BookmarkHeaderView handsetHeight]);
|
| +}
|
| +
|
| +- (UICollectionReusableView*)headerAtIndexPath:(NSIndexPath*)indexPath {
|
| + NSInteger section = indexPath.section;
|
| + if ([self isPromoSection:section])
|
| + return nil;
|
| +
|
| + if ([self shouldShowPromoCell])
|
| + --section;
|
| +
|
| + BookmarkHeaderView* view = [self.collectionView
|
| + dequeueReusableSupplementaryViewOfKind:
|
| + UICollectionElementKindSectionHeader
|
| + withReuseIdentifier:[BookmarkHeaderView
|
| + reuseIdentifier]
|
| + forIndexPath:indexPath];
|
| +
|
| + NSString* title =
|
| + base::SysUTF8ToNSString(_nodesSectionVector[section]->timeRepresentation);
|
| + [view setTitle:title];
|
| + return view;
|
| +}
|
| +
|
| +- (NSInteger)numberOfItemsInSection:(NSInteger)section {
|
| + if ([self isPromoSection:section])
|
| + return 1;
|
| +
|
| + if ([self shouldShowPromoCell])
|
| + --section;
|
| + return _nodesSectionVector[section]->vector.size();
|
| +}
|
| +
|
| +- (NSInteger)numberOfSections {
|
| + const BOOL showPromo = [self shouldShowPromoCell];
|
| + const NSInteger nodeSectionsCount = _nodesSectionVector.size();
|
| + return showPromo ? nodeSectionsCount + 1 : nodeSectionsCount;
|
| +}
|
| +
|
| +#pragma mark - BookmarkPromoCellDelegate
|
| +
|
| +- (void)bookmarkPromoCellDidTapSignIn:(BookmarkPromoCell*)bookmarkPromoCell {
|
| + [self.delegate bookmarkCollectionViewShowSignIn:self];
|
| +}
|
| +
|
| +- (void)bookmarkPromoCellDidTapDismiss:(BookmarkPromoCell*)bookmarkPromoCell {
|
| + [self.delegate bookmarkCollectionViewDismissPromo:self];
|
| +}
|
| +
|
| +#pragma mark - Promo Cell
|
| +
|
| +- (BOOL)isPromoActive {
|
| + return [self.delegate bookmarkCollectionViewShouldShowPromoCell:self];
|
| +}
|
| +
|
| +- (BOOL)shouldShowPromoCell {
|
| + // The promo cell is not shown in edit mode.
|
| + return !self.editing && [self isPromoActive];
|
| +}
|
| +
|
| +@end
|
|
|