OLD | NEW |
(Empty) | |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #import "ios/chrome/browser/ui/bookmarks/bookmark_folder_collection_view.h" |
| 6 |
| 7 #include "base/logging.h" |
| 8 #include "base/mac/objc_property_releaser.h" |
| 9 #include "base/strings/sys_string_conversions.h" |
| 10 #include "components/bookmarks/browser/bookmark_model.h" |
| 11 #include "ios/chrome/browser/bookmarks/bookmarks_utils.h" |
| 12 #include "ios/chrome/browser/experimental_flags.h" |
| 13 #import "ios/chrome/browser/ui/bookmarks/bookmark_collection_cells.h" |
| 14 #import "ios/chrome/browser/ui/bookmarks/bookmark_promo_cell.h" |
| 15 #import "ios/chrome/browser/ui/bookmarks/bookmark_utils_ios.h" |
| 16 |
| 17 using bookmarks::BookmarkNode; |
| 18 |
| 19 @interface BookmarkFolderCollectionView ()<BookmarkPromoCellDelegate> { |
| 20 base::mac::ObjCPropertyReleaser |
| 21 _propertyReleaser_BookmarkFolderCollectionView; |
| 22 // A vector of folders to display in the collection view. |
| 23 std::vector<const BookmarkNode*> _subFolders; |
| 24 // A vector of bookmark urls to display in the collection view. |
| 25 std::vector<const BookmarkNode*> _subItems; |
| 26 |
| 27 // True if the promo is visible. |
| 28 BOOL _promoVisible; |
| 29 } |
| 30 @property(nonatomic, assign) const bookmarks::BookmarkNode* folder; |
| 31 |
| 32 // Section indices. |
| 33 @property(nonatomic, readonly, assign) NSInteger promoSection; |
| 34 @property(nonatomic, readonly, assign) NSInteger folderSection; |
| 35 @property(nonatomic, readonly, assign) NSInteger itemsSection; |
| 36 @property(nonatomic, readonly, assign) NSInteger sectionCount; |
| 37 |
| 38 // Keep a reference to the promo cell to deregister as delegate. |
| 39 @property(nonatomic, retain) BookmarkPromoCell* promoCell; |
| 40 |
| 41 @end |
| 42 |
| 43 @implementation BookmarkFolderCollectionView |
| 44 @synthesize delegate = _delegate; |
| 45 @synthesize folder = _folder; |
| 46 @synthesize promoCell = _promoCell; |
| 47 |
| 48 - (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState |
| 49 frame:(CGRect)frame { |
| 50 self = [super initWithBrowserState:browserState frame:frame]; |
| 51 if (self) { |
| 52 _propertyReleaser_BookmarkFolderCollectionView.Init( |
| 53 self, [BookmarkFolderCollectionView class]); |
| 54 |
| 55 [self updateCollectionView]; |
| 56 } |
| 57 return self; |
| 58 } |
| 59 |
| 60 - (void)dealloc { |
| 61 _promoCell.delegate = nil; |
| 62 [super dealloc]; |
| 63 } |
| 64 |
| 65 - (void)setDelegate:(id<BookmarkFolderCollectionViewDelegate>)delegate { |
| 66 _delegate = delegate; |
| 67 [self promoStateChangedAnimated:NO]; |
| 68 } |
| 69 |
| 70 - (NSInteger)promoSection { |
| 71 return [self shouldShowPromoCell] ? 0 : -1; |
| 72 } |
| 73 |
| 74 - (NSInteger)folderSection { |
| 75 return [self shouldShowPromoCell] ? 1 : 0; |
| 76 } |
| 77 |
| 78 - (NSInteger)itemsSection { |
| 79 return [self shouldShowPromoCell] ? 2 : 1; |
| 80 } |
| 81 |
| 82 - (NSInteger)sectionCount { |
| 83 return [self shouldShowPromoCell] ? 3 : 2; |
| 84 } |
| 85 |
| 86 - (void)updateCollectionView { |
| 87 if (!self.bookmarkModel->loaded()) |
| 88 return; |
| 89 |
| 90 // Regenerate the list of all bookmarks. |
| 91 _subFolders = std::vector<const BookmarkNode*>(); |
| 92 _subItems = std::vector<const BookmarkNode*>(); |
| 93 |
| 94 if (self.folder) { |
| 95 int childCount = self.folder->child_count(); |
| 96 for (int i = 0; i < childCount; ++i) { |
| 97 const BookmarkNode* node = self.folder->GetChild(i); |
| 98 if (node->is_folder()) |
| 99 _subFolders.push_back(node); |
| 100 else |
| 101 _subItems.push_back(node); |
| 102 } |
| 103 |
| 104 bookmark_utils_ios::SortFolders(&_subFolders); |
| 105 } |
| 106 |
| 107 [self cancelAllFaviconLoads]; |
| 108 [self.collectionView reloadData]; |
| 109 } |
| 110 |
| 111 #pragma mark - BookmarkModelBridgeObserver Callbacks |
| 112 |
| 113 - (void)bookmarkModelLoaded { |
| 114 [self updateCollectionView]; |
| 115 } |
| 116 |
| 117 - (void)bookmarkNodeChanged:(const BookmarkNode*)bookmarkNode { |
| 118 // The base folder changed. Do nothing. |
| 119 if (bookmarkNode == self.folder) |
| 120 return; |
| 121 |
| 122 // A specific cell changed. Reload that cell. |
| 123 NSIndexPath* indexPath = [self indexPathForNode:bookmarkNode]; |
| 124 |
| 125 if (indexPath) { |
| 126 // TODO(crbug.com/603661): Ideally, we would only reload the relevant index |
| 127 // path. However, calling reloadItemsAtIndexPaths:(0,0) immediately after |
| 128 // reloadData results in a exception: NSInternalInconsistencyException |
| 129 // 'request for index path for global index 2147483645 ...' |
| 130 // One solution would be to keep track of whether we've just called |
| 131 // reloadData, but that requires experimentation to determine how long we |
| 132 // have to wait before we can safely call reloadItemsAtIndexPaths. |
| 133 [self updateCollectionView]; |
| 134 } |
| 135 } |
| 136 |
| 137 - (void)bookmarkNodeFaviconChanged: |
| 138 (const bookmarks::BookmarkNode*)bookmarkNode { |
| 139 // Only urls have favicons. |
| 140 DCHECK(bookmarkNode->is_url()); |
| 141 |
| 142 // Update image of corresponding cell. |
| 143 NSIndexPath* indexPath = [self indexPathForNode:bookmarkNode]; |
| 144 |
| 145 if (!indexPath) |
| 146 return; |
| 147 |
| 148 // Check that this cell is visible. |
| 149 NSArray* visiblePaths = [self.collectionView indexPathsForVisibleItems]; |
| 150 if (![visiblePaths containsObject:indexPath]) |
| 151 return; |
| 152 |
| 153 [self loadFaviconAtIndexPath:indexPath]; |
| 154 } |
| 155 |
| 156 - (NSIndexPath*)indexPathForNode:(const bookmarks::BookmarkNode*)bookmarkNode { |
| 157 NSIndexPath* indexPath = nil; |
| 158 if (bookmarkNode->is_folder()) { |
| 159 std::vector<const BookmarkNode*>::iterator it = |
| 160 std::find(_subFolders.begin(), _subFolders.end(), bookmarkNode); |
| 161 if (it != _subFolders.end()) { |
| 162 ptrdiff_t index = std::distance(_subFolders.begin(), it); |
| 163 indexPath = |
| 164 [NSIndexPath indexPathForRow:index inSection:self.folderSection]; |
| 165 } |
| 166 } else if (bookmarkNode->is_url()) { |
| 167 std::vector<const BookmarkNode*>::iterator it = |
| 168 std::find(_subItems.begin(), _subItems.end(), bookmarkNode); |
| 169 if (it != _subItems.end()) { |
| 170 ptrdiff_t index = std::distance(_subItems.begin(), it); |
| 171 indexPath = |
| 172 [NSIndexPath indexPathForRow:index inSection:self.itemsSection]; |
| 173 } |
| 174 } |
| 175 return indexPath; |
| 176 } |
| 177 |
| 178 - (void)bookmarkNodeDeleted:(const BookmarkNode*)node |
| 179 fromFolder:(const BookmarkNode*)folder { |
| 180 if (self.folder == node) { |
| 181 self.folder = nil; |
| 182 [self updateCollectionView]; |
| 183 } |
| 184 } |
| 185 |
| 186 - (void)bookmarkNodeChildrenChanged:(const BookmarkNode*)bookmarkNode { |
| 187 // The base folder's children changed. Reload everything. |
| 188 if (bookmarkNode == self.folder) { |
| 189 [self updateCollectionView]; |
| 190 return; |
| 191 } |
| 192 |
| 193 // A subfolder's children changed. Reload that cell. |
| 194 std::vector<const BookmarkNode*>::iterator it = |
| 195 std::find(_subFolders.begin(), _subFolders.end(), bookmarkNode); |
| 196 if (it != _subFolders.end()) { |
| 197 // TODO(crbug.com/603661): Ideally, we would only reload the relevant index |
| 198 // path. However, calling reloadItemsAtIndexPaths:(0,0) immediately after |
| 199 // reloadData results in a exception: NSInternalInconsistencyException |
| 200 // 'request for index path for global index 2147483645 ...' |
| 201 // One solution would be to keep track of whether we've just called |
| 202 // reloadData, but that requires experimentation to determine how long we |
| 203 // have to wait before we can safely call reloadItemsAtIndexPaths. |
| 204 [self updateCollectionView]; |
| 205 } |
| 206 } |
| 207 |
| 208 - (void)bookmarkNode:(const BookmarkNode*)bookmarkNode |
| 209 movedFromParent:(const BookmarkNode*)oldParent |
| 210 toParent:(const BookmarkNode*)newParent { |
| 211 if (oldParent == self.folder || newParent == self.folder) { |
| 212 // A folder was added or removed from the base folder. |
| 213 [self updateCollectionView]; |
| 214 } |
| 215 } |
| 216 |
| 217 - (void)bookmarkModelRemovedAllNodes { |
| 218 self.folder = nil; |
| 219 [self updateCollectionView]; |
| 220 } |
| 221 |
| 222 #pragma mark - Parent class overrides that affect functionality |
| 223 |
| 224 - (void)collectionView:(UICollectionView*)collectionView |
| 225 willDisplayCell:(UICollectionViewCell*)cell |
| 226 forItemAtIndexPath:(NSIndexPath*)indexPath { |
| 227 if (indexPath.section == self.itemsSection) { |
| 228 [self loadFaviconAtIndexPath:indexPath]; |
| 229 } |
| 230 } |
| 231 |
| 232 - (void)didAddCellForEditingAtIndexPath:(NSIndexPath*)indexPath { |
| 233 const BookmarkNode* node = [self nodeAtIndexPath:indexPath]; |
| 234 UICollectionViewCell* cell = |
| 235 [self.collectionView cellForItemAtIndexPath:indexPath]; |
| 236 [self.delegate bookmarkCollectionView:self cell:cell addNodeForEditing:node]; |
| 237 } |
| 238 |
| 239 - (void)didRemoveCellForEditingAtIndexPath:(NSIndexPath*)indexPath { |
| 240 const BookmarkNode* node = [self nodeAtIndexPath:indexPath]; |
| 241 UICollectionViewCell* cell = |
| 242 [self.collectionView cellForItemAtIndexPath:indexPath]; |
| 243 [self.delegate bookmarkCollectionView:self |
| 244 cell:cell |
| 245 removeNodeForEditing:node]; |
| 246 } |
| 247 |
| 248 - (void)didTapCellAtIndexPath:(NSIndexPath*)indexPath { |
| 249 if (indexPath.section == self.promoSection) { |
| 250 // User tapped inside promo cell but not on one of the buttons. Ignore it. |
| 251 return; |
| 252 } |
| 253 |
| 254 const BookmarkNode* node = [self nodeAtIndexPath:indexPath]; |
| 255 DCHECK(node); |
| 256 |
| 257 if (indexPath.section == self.folderSection) { |
| 258 [self.delegate bookmarkFolderCollectionView:self |
| 259 selectedFolderForNavigation:node]; |
| 260 } else { |
| 261 RecordBookmarkLaunch(BOOKMARK_LAUNCH_LOCATION_FOLDER); |
| 262 [self.delegate bookmarkCollectionView:self |
| 263 selectedUrlForNavigation:node->url()]; |
| 264 } |
| 265 } |
| 266 |
| 267 - (void)didTapMenuButtonAtIndexPath:(NSIndexPath*)indexPath |
| 268 onView:(UIView*)view |
| 269 forCell:(BookmarkItemCell*)cell { |
| 270 [self.delegate bookmarkCollectionView:self |
| 271 wantsMenuForBookmark:[self nodeAtIndexPath:indexPath] |
| 272 onView:view |
| 273 forCell:cell]; |
| 274 } |
| 275 |
| 276 - (bookmark_cell::ButtonType)buttonTypeForCellAtIndexPath: |
| 277 (NSIndexPath*)indexPath { |
| 278 return self.editing ? bookmark_cell::ButtonNone : bookmark_cell::ButtonMenu; |
| 279 } |
| 280 |
| 281 - (BOOL)allowLongPressForCellAtIndexPath:(NSIndexPath*)indexPath { |
| 282 return !self.editing; |
| 283 } |
| 284 |
| 285 - (void)didLongPressCell:(UICollectionViewCell*)cell |
| 286 atIndexPath:(NSIndexPath*)indexPath { |
| 287 if (indexPath.section == self.promoSection) { |
| 288 // User long-pressed inside promo cell. Ignore it. |
| 289 return; |
| 290 } |
| 291 |
| 292 [self.delegate bookmarkCollectionView:self |
| 293 didLongPressCell:cell |
| 294 forBookmark:[self nodeAtIndexPath:indexPath]]; |
| 295 } |
| 296 |
| 297 - (BOOL)shouldSelectCellAtIndexPath:(NSIndexPath*)indexPath { |
| 298 return YES; |
| 299 } |
| 300 |
| 301 - (BOOL)cellIsSelectedForEditingAtIndexPath:(NSIndexPath*)indexPath { |
| 302 const BookmarkNode* node = [self nodeAtIndexPath:indexPath]; |
| 303 const std::set<const BookmarkNode*>& editingNodes = |
| 304 [self.delegate nodesBeingEdited]; |
| 305 return editingNodes.find(node) != editingNodes.end(); |
| 306 } |
| 307 |
| 308 - (const BookmarkNode*)nodeAtIndexPath:(NSIndexPath*)indexPath { |
| 309 if (indexPath.section == self.folderSection) |
| 310 return _subFolders[indexPath.row]; |
| 311 if (indexPath.section == self.itemsSection) |
| 312 return _subItems[indexPath.row]; |
| 313 |
| 314 NOTREACHED(); |
| 315 return nullptr; |
| 316 } |
| 317 |
| 318 - (void)resetFolder:(const BookmarkNode*)folder { |
| 319 DCHECK(folder->is_folder()); |
| 320 self.folder = folder; |
| 321 [self updateCollectionView]; |
| 322 } |
| 323 |
| 324 - (CGFloat)interitemSpacingForSectionAtIndex:(NSInteger)section { |
| 325 CGFloat interitemSpacing = 0; |
| 326 SEL minimumInteritemSpacingSelector = @selector(collectionView: |
| 327 layout: |
| 328 minimumInteritemSpacingForSectionAtIndex:); |
| 329 if ([self.collectionView.delegate |
| 330 respondsToSelector:minimumInteritemSpacingSelector]) { |
| 331 id collectionViewDelegate = static_cast<id>(self.collectionView.delegate); |
| 332 interitemSpacing = |
| 333 [collectionViewDelegate collectionView:self.collectionView |
| 334 layout:self.collectionView |
| 335 .collectionViewLayout |
| 336 minimumInteritemSpacingForSectionAtIndex:section]; |
| 337 } else if ([self.collectionView.collectionViewLayout |
| 338 isKindOfClass:[UICollectionViewFlowLayout class]]) { |
| 339 UICollectionViewFlowLayout* flowLayout = |
| 340 static_cast<UICollectionViewFlowLayout*>( |
| 341 self.collectionView.collectionViewLayout); |
| 342 interitemSpacing = flowLayout.minimumInteritemSpacing; |
| 343 } |
| 344 return interitemSpacing; |
| 345 } |
| 346 |
| 347 // Parent class override. |
| 348 - (UIEdgeInsets)insetForSectionAtIndex:(NSInteger)section { |
| 349 UIEdgeInsets insets = [super insetForSectionAtIndex:section]; |
| 350 if (section == self.folderSection) |
| 351 insets.bottom = [self interitemSpacingForSectionAtIndex:section] / 2.; |
| 352 else if (section == self.itemsSection) |
| 353 insets.top = [self interitemSpacingForSectionAtIndex:section] / 2.; |
| 354 else if (section == self.promoSection) |
| 355 (void)0; // No insets to update. |
| 356 else |
| 357 NOTREACHED(); |
| 358 return insets; |
| 359 } |
| 360 |
| 361 // Parent class override. |
| 362 - (UICollectionViewCell*)cellAtIndexPath:(NSIndexPath*)indexPath { |
| 363 if (indexPath.section == self.promoSection) { |
| 364 self.promoCell = [self.collectionView |
| 365 dequeueReusableCellWithReuseIdentifier:[BookmarkPromoCell |
| 366 reuseIdentifier] |
| 367 forIndexPath:indexPath]; |
| 368 self.promoCell.delegate = self; |
| 369 return self.promoCell; |
| 370 } |
| 371 const BookmarkNode* node = [self nodeAtIndexPath:indexPath]; |
| 372 |
| 373 if (indexPath.section == self.folderSection) |
| 374 return [self cellForFolder:node indexPath:indexPath]; |
| 375 |
| 376 BookmarkItemCell* cell = [self cellForBookmark:node indexPath:indexPath]; |
| 377 return cell; |
| 378 } |
| 379 |
| 380 - (BOOL)needsSectionHeaderForSection:(NSInteger)section { |
| 381 // Only show header when there is at least one element in the previous |
| 382 // section. |
| 383 if (section == 0) |
| 384 return NO; |
| 385 |
| 386 if ([self numberOfItemsInSection:(section - 1)] == 0) |
| 387 return NO; |
| 388 |
| 389 return YES; |
| 390 } |
| 391 |
| 392 // Parent class override. |
| 393 - (CGSize)headerSizeForSection:(NSInteger)section { |
| 394 if ([self needsSectionHeaderForSection:section]) |
| 395 return CGSizeMake(self.bounds.size.width, |
| 396 [BookmarkHeaderSeparatorView preferredHeight]); |
| 397 |
| 398 return CGSizeZero; |
| 399 } |
| 400 |
| 401 // Parent class override. |
| 402 - (UICollectionReusableView*)headerAtIndexPath:(NSIndexPath*)indexPath { |
| 403 if (![self needsSectionHeaderForSection:indexPath.section]) |
| 404 return nil; |
| 405 |
| 406 BookmarkHeaderSeparatorView* view = [self.collectionView |
| 407 dequeueReusableSupplementaryViewOfKind: |
| 408 UICollectionElementKindSectionHeader |
| 409 withReuseIdentifier:[BookmarkHeaderSeparatorView |
| 410 reuseIdentifier] |
| 411 forIndexPath:indexPath]; |
| 412 view.backgroundColor = [UIColor colorWithWhite:1 alpha:1]; |
| 413 return view; |
| 414 } |
| 415 |
| 416 - (NSInteger)numberOfItemsInSection:(NSInteger)section { |
| 417 if (section == self.folderSection) |
| 418 return _subFolders.size(); |
| 419 if (section == self.itemsSection) |
| 420 return _subItems.size(); |
| 421 if (section == self.promoSection) |
| 422 return 1; |
| 423 |
| 424 NOTREACHED(); |
| 425 return -1; |
| 426 } |
| 427 |
| 428 - (NSInteger)numberOfSections { |
| 429 return self.sectionCount; |
| 430 } |
| 431 |
| 432 - (void)collectionViewScrolled { |
| 433 [self.delegate bookmarkCollectionViewDidScroll:self]; |
| 434 } |
| 435 |
| 436 - (void)setEditing:(BOOL)editing animated:(BOOL)animated { |
| 437 [super setEditing:editing animated:animated]; |
| 438 [self promoStateChangedAnimated:animated]; |
| 439 } |
| 440 |
| 441 - (void)promoStateChangedAnimated:(BOOL)animate { |
| 442 if (experimental_flags::IsAllBookmarksEnabled()) |
| 443 return; // The promo is not shown if All Bookmarks is enabled. |
| 444 |
| 445 BOOL newPromoState = |
| 446 !self.editing && self.folder && |
| 447 self.folder->type() == BookmarkNode::MOBILE && |
| 448 [self.delegate bookmarkCollectionViewShouldShowPromoCell:self]; |
| 449 if (newPromoState != _promoVisible) { |
| 450 // This is awful, but until the old code to do the refresh when switching |
| 451 // in and out of edit mode is fixed, this is probably the cleanest thing to |
| 452 // do. |
| 453 _promoVisible = newPromoState; |
| 454 [self.collectionView reloadData]; |
| 455 } |
| 456 } |
| 457 |
| 458 #pragma mark - BookmarkPromoCellDelegate |
| 459 |
| 460 - (void)bookmarkPromoCellDidTapSignIn:(BookmarkPromoCell*)bookmarkPromoCell { |
| 461 [self.delegate bookmarkCollectionViewShowSignIn:self]; |
| 462 } |
| 463 |
| 464 - (void)bookmarkPromoCellDidTapDismiss:(BookmarkPromoCell*)bookmarkPromoCell { |
| 465 [self.delegate bookmarkCollectionViewDismissPromo:self]; |
| 466 } |
| 467 |
| 468 #pragma mark - Promo Cell |
| 469 |
| 470 - (BOOL)shouldShowPromoCell { |
| 471 return _promoVisible; |
| 472 } |
| 473 |
| 474 @end |
OLD | NEW |