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_collection_view.h" |
| 6 |
| 7 #import <UIKit/UIGestureRecognizerSubclass.h> |
| 8 #include <algorithm> |
| 9 #include <map> |
| 10 #include <memory> |
| 11 |
| 12 #include "base/ios/weak_nsobject.h" |
| 13 #include "base/mac/bind_objc_block.h" |
| 14 #include "base/mac/foundation_util.h" |
| 15 #include "base/mac/objc_property_releaser.h" |
| 16 #include "base/mac/scoped_nsobject.h" |
| 17 #include "base/strings/sys_string_conversions.h" |
| 18 #include "components/bookmarks/browser/bookmark_model.h" |
| 19 #include "components/bookmarks/browser/bookmark_model_observer.h" |
| 20 #include "components/favicon/core/fallback_url_util.h" |
| 21 #include "components/favicon/core/large_icon_service.h" |
| 22 #include "components/favicon_base/fallback_icon_style.h" |
| 23 #include "components/favicon_base/favicon_types.h" |
| 24 #include "ios/chrome/browser/bookmarks/bookmark_model_factory.h" |
| 25 #include "ios/chrome/browser/bookmarks/bookmarks_utils.h" |
| 26 #include "ios/chrome/browser/favicon/ios_chrome_large_icon_service_factory.h" |
| 27 #import "ios/chrome/browser/ui/bookmarks/bookmark_collection_cells.h" |
| 28 #import "ios/chrome/browser/ui/bookmarks/bookmark_collection_view_background.h" |
| 29 #import "ios/chrome/browser/ui/bookmarks/bookmark_promo_cell.h" |
| 30 #import "ios/chrome/browser/ui/bookmarks/bookmark_utils_ios.h" |
| 31 #include "ios/chrome/browser/ui/ui_util.h" |
| 32 #import "ios/chrome/browser/ui/uikit_ui_util.h" |
| 33 #include "ios/chrome/grit/ios_strings.h" |
| 34 #include "skia/ext/skia_utils_ios.h" |
| 35 #include "ui/base/l10n/l10n_util_mac.h" |
| 36 |
| 37 using bookmarks::BookmarkNode; |
| 38 |
| 39 namespace { |
| 40 |
| 41 // Used to store a pair of NSIntegers when storing a NSIndexPath in C++ |
| 42 // collections. |
| 43 using IntegerPair = std::pair<NSInteger, NSInteger>; |
| 44 |
| 45 // The margin between the side of the view and the first and last tile. |
| 46 CGFloat rowMarginTablet = 24.0; |
| 47 CGFloat rowHeight = 48.0; |
| 48 // Minimal acceptable favicon size, in points. |
| 49 CGFloat minFaviconSizePt = 16; |
| 50 |
| 51 // Delay in seconds to which the empty background view will be shown when the |
| 52 // collection view is empty. |
| 53 // This delay should not be too small to let enough time to load bookmarks |
| 54 // from network. |
| 55 const NSTimeInterval kShowEmptyBookmarksBackgroundRefreshDelay = 1.0; |
| 56 |
| 57 } // namespace |
| 58 |
| 59 @interface BookmarkCollectionView ()<UICollectionViewDataSource, |
| 60 UICollectionViewDelegateFlowLayout, |
| 61 UIGestureRecognizerDelegate> { |
| 62 std::unique_ptr<bookmarks::BookmarkModelBridge> _modelBridge; |
| 63 ios::ChromeBrowserState* _browserState; |
| 64 |
| 65 base::mac::ObjCPropertyReleaser _propertyReleaser_BookmarkCollectionView; |
| 66 |
| 67 // Map of favicon load tasks for each index path. Used to keep track of |
| 68 // pending favicon load operations so that they can be cancelled upon cell |
| 69 // reuse. Keys are (section, item) pairs of cell index paths. |
| 70 std::map<IntegerPair, base::CancelableTaskTracker::TaskId> _faviconLoadTasks; |
| 71 // Task tracker used for async favicon loads. |
| 72 base::CancelableTaskTracker _faviconTaskTracker; |
| 73 } |
| 74 |
| 75 // Redefined to be readwrite. |
| 76 @property(nonatomic, assign) bookmarks::BookmarkModel* bookmarkModel; |
| 77 // Redefined to be readwrite. |
| 78 @property(nonatomic, retain) UICollectionView* collectionView; |
| 79 // Redefined to be readwrite. |
| 80 @property(nonatomic, assign) BOOL editing; |
| 81 // Detects a long press on a cell. |
| 82 @property(nonatomic, retain) UILongPressGestureRecognizer* longPressRecognizer; |
| 83 // Background view of the collection view shown when there is no items. |
| 84 @property(nonatomic, retain) |
| 85 BookmarkCollectionViewBackground* emptyCollectionBackgroundView; |
| 86 // Shadow to display over the content. |
| 87 @property(nonatomic, retain) UIView* shadow; |
| 88 |
| 89 // Updates the editing state for the cell. |
| 90 - (void)updateEditingStateOfCell:(BookmarkCell*)cell |
| 91 atIndexPath:(NSIndexPath*)indexPath |
| 92 animateMenuVisibility:(BOOL)animateMenuVisibility |
| 93 animateSelectedState:(BOOL)animateSelectedState; |
| 94 |
| 95 // Callback received when the user taps the menu button on the cell. |
| 96 - (void)didTapMenuButton:(BookmarkItemCell*)cell view:(UIView*)view; |
| 97 |
| 98 // In landscape mode, there are 2 widths: 480pt and 568pt. Returns YES if the |
| 99 // width is 568pt. |
| 100 - (BOOL)wideLandscapeMode; |
| 101 |
| 102 // Schedules showing or hiding the empty bookmarks background view if the |
| 103 // collection view is empty by calling showEmptyBackgroundIfNeeded after |
| 104 // kShowEmptyBookmarksBackgroundRefreshDelay. |
| 105 // Multiple call to this method will cancel previous scheduled call to |
| 106 // showEmptyBackgroundIfNeeded before scheduling a new one. |
| 107 - (void)scheduleEmptyBackgroundVisibilityUpdate; |
| 108 // Shows/hides empty bookmarks background view if the collections view is empty. |
| 109 - (void)updateEmptyBackgroundVisibility; |
| 110 // Shows/hides empty bookmarks background view with an animation. |
| 111 - (void)setEmptyBackgroundVisible:(BOOL)visible; |
| 112 @end |
| 113 |
| 114 @implementation BookmarkCollectionView |
| 115 @synthesize bookmarkModel = _bookmarkModel; |
| 116 @synthesize collectionView = _collectionView; |
| 117 @synthesize editing = _editing; |
| 118 @synthesize emptyCollectionBackgroundView = _emptyCollectionBackgroundView; |
| 119 @synthesize loader = _loader; |
| 120 @synthesize longPressRecognizer = _longPressRecognizer; |
| 121 @synthesize browserState = _browserState; |
| 122 @synthesize shadow = _shadow; |
| 123 |
| 124 #pragma mark - Initialization |
| 125 |
| 126 - (id)init { |
| 127 NOTREACHED(); |
| 128 return nil; |
| 129 } |
| 130 |
| 131 - (id)initWithFrame:(CGRect)frame { |
| 132 NOTREACHED(); |
| 133 return nil; |
| 134 } |
| 135 |
| 136 - (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState |
| 137 frame:(CGRect)frame { |
| 138 self = [super initWithFrame:frame]; |
| 139 if (self) { |
| 140 _propertyReleaser_BookmarkCollectionView.Init( |
| 141 self, [BookmarkCollectionView class]); |
| 142 |
| 143 _browserState = browserState; |
| 144 |
| 145 // Set up connection to the BookmarkModel. |
| 146 _bookmarkModel = |
| 147 ios::BookmarkModelFactory::GetForBrowserState(browserState); |
| 148 |
| 149 // Set up observers. |
| 150 _modelBridge.reset( |
| 151 new bookmarks::BookmarkModelBridge(self, _bookmarkModel)); |
| 152 |
| 153 [self setupViews]; |
| 154 } |
| 155 return self; |
| 156 } |
| 157 |
| 158 - (void)dealloc { |
| 159 _collectionView.dataSource = nil; |
| 160 _collectionView.delegate = nil; |
| 161 UIView* moi = _collectionView; |
| 162 dispatch_async(dispatch_get_main_queue(), ^{ |
| 163 // A collection view with a layout that uses a dynamic animator (aka |
| 164 // something that changes the layout over time) will crash if it is |
| 165 // deallocated while the animation is currently playing. |
| 166 // Apparently if a tick has been dispatched it will execute, invoking a |
| 167 // method on the deallocated collection. |
| 168 // The only purpose of this block is to retain the collection view for a |
| 169 // while, giving the layout a chance to perform its last tick. |
| 170 [moi self]; |
| 171 }); |
| 172 _faviconTaskTracker.TryCancelAll(); |
| 173 [super dealloc]; |
| 174 } |
| 175 |
| 176 - (void)setupViews { |
| 177 self.backgroundColor = bookmark_utils_ios::mainBackgroundColor(); |
| 178 base::scoped_nsobject<UICollectionViewFlowLayout> layout( |
| 179 [[UICollectionViewFlowLayout alloc] init]); |
| 180 |
| 181 base::scoped_nsobject<UICollectionView> collectionView( |
| 182 [[UICollectionView alloc] initWithFrame:self.bounds |
| 183 collectionViewLayout:layout]); |
| 184 self.collectionView = collectionView; |
| 185 self.collectionView.backgroundColor = [UIColor clearColor]; |
| 186 self.collectionView.autoresizingMask = |
| 187 UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; |
| 188 self.collectionView.alwaysBounceVertical = YES; |
| 189 self.collectionView.delegate = self; |
| 190 self.collectionView.dataSource = self; |
| 191 [self.collectionView registerClass:[BookmarkFolderCell class] |
| 192 forCellWithReuseIdentifier:[BookmarkFolderCell reuseIdentifier]]; |
| 193 [self.collectionView registerClass:[BookmarkItemCell class] |
| 194 forCellWithReuseIdentifier:[BookmarkItemCell reuseIdentifier]]; |
| 195 [self.collectionView registerClass:[BookmarkHeaderView class] |
| 196 forSupplementaryViewOfKind:UICollectionElementKindSectionHeader |
| 197 withReuseIdentifier:[BookmarkHeaderView reuseIdentifier]]; |
| 198 [self.collectionView |
| 199 registerClass:[BookmarkHeaderSeparatorView class] |
| 200 forSupplementaryViewOfKind:UICollectionElementKindSectionHeader |
| 201 withReuseIdentifier:[BookmarkHeaderSeparatorView reuseIdentifier]]; |
| 202 [self.collectionView registerClass:[BookmarkPromoCell class] |
| 203 forCellWithReuseIdentifier:[BookmarkPromoCell reuseIdentifier]]; |
| 204 |
| 205 [self addSubview:self.collectionView]; |
| 206 |
| 207 // Set up the background view shown when the collection is empty. |
| 208 base::scoped_nsobject<BookmarkCollectionViewBackground> |
| 209 emptyCollectionBackgroundView( |
| 210 [[BookmarkCollectionViewBackground alloc] initWithFrame:CGRectZero]); |
| 211 self.emptyCollectionBackgroundView = emptyCollectionBackgroundView; |
| 212 self.emptyCollectionBackgroundView.autoresizingMask = |
| 213 UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; |
| 214 self.emptyCollectionBackgroundView.alpha = 0; |
| 215 self.emptyCollectionBackgroundView.text = [self textWhenCollectionIsEmpty]; |
| 216 |
| 217 self.emptyCollectionBackgroundView.frame = self.collectionView.bounds; |
| 218 self.collectionView.backgroundView = self.emptyCollectionBackgroundView; |
| 219 |
| 220 [self updateShadow]; |
| 221 |
| 222 self.longPressRecognizer = |
| 223 base::scoped_nsobject<UILongPressGestureRecognizer>( |
| 224 [[UILongPressGestureRecognizer alloc] |
| 225 initWithTarget:self |
| 226 action:@selector(longPress:)]); |
| 227 self.longPressRecognizer.delegate = self; |
| 228 [self.collectionView addGestureRecognizer:self.longPressRecognizer]; |
| 229 } |
| 230 |
| 231 - (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection { |
| 232 [self updateShadow]; |
| 233 } |
| 234 |
| 235 - (void)updateShadow { |
| 236 // Remove the current one, if any. |
| 237 [self.shadow removeFromSuperview]; |
| 238 |
| 239 if (IsCompact(self)) { |
| 240 self.shadow = |
| 241 bookmark_utils_ios::dropShadowWithWidth(CGRectGetWidth(self.bounds)); |
| 242 } else { |
| 243 self.shadow = [[[UIView alloc] |
| 244 initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.bounds), |
| 245 1 / [[UIScreen mainScreen] scale])] |
| 246 autorelease]; |
| 247 self.shadow.backgroundColor = [UIColor colorWithWhite:0.0 alpha:.12]; |
| 248 } |
| 249 |
| 250 [self updateShadowFrame]; |
| 251 self.shadow.autoresizingMask = UIViewAutoresizingFlexibleWidth; |
| 252 if (self.editing) |
| 253 self.shadow.alpha = 0.0; |
| 254 |
| 255 // Add the new shadow. |
| 256 [self addSubview:self.shadow]; |
| 257 } |
| 258 |
| 259 - (void)updateShadowFrame { |
| 260 CGFloat shadowHeight = CGRectGetHeight(self.shadow.frame); |
| 261 CGFloat y = std::min<CGFloat>( |
| 262 0.0, self.collectionView.contentOffset.y - shadowHeight); |
| 263 self.shadow.frame = |
| 264 CGRectMake(0, y, CGRectGetWidth(self.bounds), shadowHeight); |
| 265 } |
| 266 |
| 267 #pragma mark - UIScrollViewDelegate |
| 268 |
| 269 - (void)scrollViewDidScroll:(UIScrollView*)scrollView { |
| 270 [self updateShadowFrame]; |
| 271 [self collectionViewScrolled]; |
| 272 } |
| 273 |
| 274 #pragma mark - empty background |
| 275 |
| 276 - (void)scheduleEmptyBackgroundVisibilityUpdate { |
| 277 [NSObject |
| 278 cancelPreviousPerformRequestsWithTarget:self |
| 279 selector: |
| 280 @selector( |
| 281 updateEmptyBackgroundVisibility) |
| 282 object:nil]; |
| 283 [self performSelector:@selector(updateEmptyBackgroundVisibility) |
| 284 withObject:nil |
| 285 afterDelay:kShowEmptyBookmarksBackgroundRefreshDelay]; |
| 286 } |
| 287 |
| 288 - (BOOL)isCollectionViewEmpty { |
| 289 BOOL collectionViewIsEmpty = YES; |
| 290 const NSInteger numberOfSections = [self numberOfSections]; |
| 291 NSInteger section = [self shouldShowPromoCell] ? 1 : 0; |
| 292 for (; collectionViewIsEmpty && section < numberOfSections; ++section) { |
| 293 const NSInteger numberOfItemsInSection = |
| 294 [self numberOfItemsInSection:section]; |
| 295 collectionViewIsEmpty = numberOfItemsInSection == 0; |
| 296 } |
| 297 return collectionViewIsEmpty; |
| 298 } |
| 299 |
| 300 - (void)updateEmptyBackgroundVisibility { |
| 301 const BOOL showEmptyBackground = |
| 302 [self isCollectionViewEmpty] && ![self shouldShowPromoCell]; |
| 303 [self setEmptyBackgroundVisible:showEmptyBackground]; |
| 304 } |
| 305 |
| 306 - (void)setEmptyBackgroundVisible:(BOOL)emptyBackgroundVisible { |
| 307 [UIView beginAnimations:@"alpha" context:NULL]; |
| 308 self.emptyCollectionBackgroundView.alpha = emptyBackgroundVisible ? 1 : 0; |
| 309 [UIView commitAnimations]; |
| 310 } |
| 311 |
| 312 #pragma mark - UICollectionViewDataSource |
| 313 |
| 314 - (NSInteger)collectionView:(UICollectionView*)collectionView |
| 315 numberOfItemsInSection:(NSInteger)section { |
| 316 const NSInteger numberOfItemsInSection = |
| 317 [self numberOfItemsInSection:section]; |
| 318 const BOOL isCollectionViewEmpty = [self isCollectionViewEmpty]; |
| 319 self.collectionView.scrollEnabled = !isCollectionViewEmpty; |
| 320 if (isCollectionViewEmpty) { |
| 321 [self scheduleEmptyBackgroundVisibilityUpdate]; |
| 322 } else { |
| 323 // Hide empty bookmarks now. |
| 324 [self setEmptyBackgroundVisible:NO]; |
| 325 } |
| 326 return numberOfItemsInSection; |
| 327 } |
| 328 |
| 329 - (NSInteger)numberOfSectionsInCollectionView: |
| 330 (UICollectionView*)collectionView { |
| 331 const NSInteger numberOfSections = [self numberOfSections]; |
| 332 const BOOL collectionViewIsEmpty = 0 == numberOfSections; |
| 333 self.collectionView.scrollEnabled = !collectionViewIsEmpty; |
| 334 if (collectionViewIsEmpty) { |
| 335 [self scheduleEmptyBackgroundVisibilityUpdate]; |
| 336 } else { |
| 337 // Hide empty bookmarks now. |
| 338 [self setEmptyBackgroundVisible:NO]; |
| 339 } |
| 340 return numberOfSections; |
| 341 } |
| 342 |
| 343 - (UICollectionViewCell*)collectionView:(UICollectionView*)collectionView |
| 344 cellForItemAtIndexPath:(NSIndexPath*)indexPath { |
| 345 return [self cellAtIndexPath:indexPath]; |
| 346 } |
| 347 |
| 348 - (UICollectionReusableView*)collectionView:(UICollectionView*)collectionView |
| 349 viewForSupplementaryElementOfKind:(NSString*)kind |
| 350 atIndexPath:(NSIndexPath*)indexPath { |
| 351 return [self headerAtIndexPath:indexPath]; |
| 352 } |
| 353 |
| 354 - (BOOL)collectionView:(UICollectionView*)collectionView |
| 355 shouldSelectItemAtIndexPath:(NSIndexPath*)indexPath { |
| 356 return [self shouldSelectCellAtIndexPath:indexPath]; |
| 357 } |
| 358 |
| 359 - (void)collectionView:(UICollectionView*)collectionView |
| 360 didSelectItemAtIndexPath:(NSIndexPath*)indexPath { |
| 361 if (self.editing) |
| 362 [self toggleSelectedForEditingAtIndexPath:indexPath]; |
| 363 else |
| 364 [self didTapCellAtIndexPath:indexPath]; |
| 365 } |
| 366 |
| 367 - (void)toggleSelectedForEditingAtIndexPath:(NSIndexPath*)indexPath { |
| 368 BOOL selected = [self cellIsSelectedForEditingAtIndexPath:indexPath]; |
| 369 if (selected) |
| 370 [self didRemoveCellForEditingAtIndexPath:indexPath]; |
| 371 else |
| 372 [self didAddCellForEditingAtIndexPath:indexPath]; |
| 373 |
| 374 [self updateEditingStateOfCellAtIndexPath:indexPath |
| 375 animateMenuVisibility:NO |
| 376 animateSelectedState:YES]; |
| 377 } |
| 378 |
| 379 #pragma mark - UICollectionViewDelegate |
| 380 |
| 381 - (void)collectionView:(UICollectionView*)collectionView |
| 382 didEndDisplayingCell:(UICollectionViewCell*)cell |
| 383 forItemAtIndexPath:(NSIndexPath*)indexPath { |
| 384 _faviconTaskTracker.TryCancel( |
| 385 _faviconLoadTasks[IntegerPair(indexPath.section, indexPath.item)]); |
| 386 } |
| 387 |
| 388 #pragma mark - UICollectionViewDelegateFlowLayout |
| 389 |
| 390 - (UIEdgeInsets)collectionView:(UICollectionView*)collectionView |
| 391 layout:(UICollectionViewLayout*)collectionViewLayout |
| 392 insetForSectionAtIndex:(NSInteger)section { |
| 393 return [self insetForSectionAtIndex:section]; |
| 394 } |
| 395 |
| 396 - (CGFloat)collectionView:(UICollectionView*)collectionView |
| 397 layout:(UICollectionViewLayout*) |
| 398 collectionViewLayout |
| 399 minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { |
| 400 return [self minimumInteritemSpacingForSectionAtIndex:section]; |
| 401 } |
| 402 |
| 403 - (CGFloat)collectionView:(UICollectionView*)collectionView |
| 404 layout:(UICollectionViewLayout*)layout |
| 405 minimumLineSpacingForSectionAtIndex:(NSInteger)section { |
| 406 return [self minimumLineSpacingForSectionAtIndex:section]; |
| 407 } |
| 408 |
| 409 - (CGSize)collectionView:(UICollectionView*)collectionView |
| 410 layout:(UICollectionViewLayout*)collectionViewLayout |
| 411 sizeForItemAtIndexPath:(NSIndexPath*)indexPath { |
| 412 return [self cellSizeForIndexPath:indexPath]; |
| 413 } |
| 414 |
| 415 - (CGSize)collectionView:(UICollectionView*)collectionView |
| 416 layout: |
| 417 (UICollectionViewLayout*)collectionViewLayout |
| 418 referenceSizeForHeaderInSection:(NSInteger)section { |
| 419 return [self headerSizeForSection:section]; |
| 420 } |
| 421 |
| 422 #pragma mark - BookmarkItemCell callbacks |
| 423 |
| 424 - (void)didTapMenuButton:(BookmarkItemCell*)cell view:(UIView*)view { |
| 425 [self didTapMenuButtonAtIndexPath:[self.collectionView indexPathForCell:cell] |
| 426 onView:view |
| 427 forCell:cell]; |
| 428 } |
| 429 |
| 430 #pragma mark - Convenience methods for subclasses |
| 431 |
| 432 - (void)updateCellAtIndexPath:(NSIndexPath*)indexPath |
| 433 withImage:(UIImage*)image |
| 434 backgroundColor:(UIColor*)backgroundColor |
| 435 textColor:(UIColor*)textColor |
| 436 fallbackText:(NSString*)text { |
| 437 BookmarkItemCell* cell = base::mac::ObjCCast<BookmarkItemCell>( |
| 438 [self.collectionView cellForItemAtIndexPath:indexPath]); |
| 439 if (!cell) |
| 440 return; |
| 441 |
| 442 if (image) { |
| 443 [cell setImage:image]; |
| 444 } else { |
| 445 [cell setPlaceholderText:text |
| 446 textColor:textColor |
| 447 backgroundColor:backgroundColor]; |
| 448 } |
| 449 } |
| 450 |
| 451 - (BookmarkItemCell*)cellForBookmark:(const BookmarkNode*)node |
| 452 indexPath:(NSIndexPath*)indexPath { |
| 453 DCHECK(![self isPromoSection:indexPath.section]); |
| 454 |
| 455 BOOL selected = [self cellIsSelectedForEditingAtIndexPath:indexPath]; |
| 456 |
| 457 BookmarkItemCell* cell = [self.collectionView |
| 458 dequeueReusableCellWithReuseIdentifier:[BookmarkItemCell reuseIdentifier] |
| 459 forIndexPath:indexPath]; |
| 460 |
| 461 [cell updateWithTitle:bookmark_utils_ios::TitleForBookmarkNode(node)]; |
| 462 [cell setSelectedForEditing:selected animated:NO]; |
| 463 [cell setButtonTarget:self action:@selector(didTapMenuButton:view:)]; |
| 464 |
| 465 [self updateEditingStateOfCell:cell |
| 466 atIndexPath:indexPath |
| 467 animateMenuVisibility:NO |
| 468 animateSelectedState:NO]; |
| 469 |
| 470 [self loadFaviconAtIndexPath:indexPath]; |
| 471 |
| 472 return cell; |
| 473 } |
| 474 |
| 475 - (void)cancelAllFaviconLoads { |
| 476 _faviconTaskTracker.TryCancelAll(); |
| 477 } |
| 478 |
| 479 - (void)cancelLoadingFaviconAtIndexPath:(NSIndexPath*)indexPath { |
| 480 _faviconTaskTracker.TryCancel( |
| 481 _faviconLoadTasks[IntegerPair(indexPath.section, indexPath.item)]); |
| 482 } |
| 483 |
| 484 - (void)loadFaviconAtIndexPath:(NSIndexPath*)indexPath { |
| 485 // Cancel previous load attempts. |
| 486 [self cancelLoadingFaviconAtIndexPath:indexPath]; |
| 487 |
| 488 // Start loading a favicon. |
| 489 base::WeakNSObject<BookmarkCollectionView> weakSelf(self); |
| 490 const bookmarks::BookmarkNode* node = [self nodeAtIndexPath:indexPath]; |
| 491 GURL blockURL(node->url()); |
| 492 void (^faviconBlock)(const favicon_base::LargeIconResult&) = ^( |
| 493 const favicon_base::LargeIconResult& result) { |
| 494 base::scoped_nsobject<BookmarkCollectionView> strongSelf([weakSelf retain]); |
| 495 if (!strongSelf) |
| 496 return; |
| 497 UIImage* favIcon = nil; |
| 498 UIColor* backgroundColor = nil; |
| 499 UIColor* textColor = nil; |
| 500 NSString* fallbackText = nil; |
| 501 if (result.bitmap.is_valid()) { |
| 502 scoped_refptr<base::RefCountedMemory> data = |
| 503 result.bitmap.bitmap_data.get(); |
| 504 favIcon = [UIImage imageWithData:[NSData dataWithBytes:data->front() |
| 505 length:data->size()]]; |
| 506 } else if (result.fallback_icon_style) { |
| 507 backgroundColor = skia::UIColorFromSkColor( |
| 508 result.fallback_icon_style->background_color); |
| 509 textColor = |
| 510 skia::UIColorFromSkColor(result.fallback_icon_style->text_color); |
| 511 |
| 512 fallbackText = |
| 513 base::SysUTF16ToNSString(favicon::GetFallbackIconText(blockURL)); |
| 514 } |
| 515 |
| 516 [strongSelf updateCellAtIndexPath:indexPath |
| 517 withImage:favIcon |
| 518 backgroundColor:backgroundColor |
| 519 textColor:textColor |
| 520 fallbackText:fallbackText]; |
| 521 }; |
| 522 |
| 523 CGFloat scale = [UIScreen mainScreen].scale; |
| 524 CGFloat preferredSize = scale * [BookmarkItemCell preferredImageSize]; |
| 525 CGFloat minSize = scale * minFaviconSizePt; |
| 526 |
| 527 base::CancelableTaskTracker::TaskId taskId = |
| 528 IOSChromeLargeIconServiceFactory::GetForBrowserState(self.browserState) |
| 529 ->GetLargeIconOrFallbackStyle(node->url(), minSize, preferredSize, |
| 530 base::BindBlock(faviconBlock), |
| 531 &_faviconTaskTracker); |
| 532 _faviconLoadTasks[IntegerPair(indexPath.section, indexPath.item)] = taskId; |
| 533 } |
| 534 |
| 535 - (BookmarkFolderCell*)cellForFolder:(const BookmarkNode*)node |
| 536 indexPath:(NSIndexPath*)indexPath { |
| 537 DCHECK(![self isPromoSection:indexPath.section]); |
| 538 BookmarkFolderCell* cell = [self.collectionView |
| 539 dequeueReusableCellWithReuseIdentifier:[BookmarkFolderCell |
| 540 reuseIdentifier] |
| 541 forIndexPath:indexPath]; |
| 542 [self updateEditingStateOfCell:cell |
| 543 atIndexPath:indexPath |
| 544 animateMenuVisibility:NO |
| 545 animateSelectedState:NO]; |
| 546 [cell updateWithTitle:bookmark_utils_ios::TitleForBookmarkNode(node)]; |
| 547 [cell setButtonTarget:self action:@selector(didTapMenuButton:view:)]; |
| 548 |
| 549 return cell; |
| 550 } |
| 551 |
| 552 - (void)updateEditingStateOfCell:(BookmarkCell*)cell |
| 553 atIndexPath:(NSIndexPath*)indexPath |
| 554 animateMenuVisibility:(BOOL)animateMenuVisibility |
| 555 animateSelectedState:(BOOL)animateSelectedState { |
| 556 BOOL selected = [self cellIsSelectedForEditingAtIndexPath:indexPath]; |
| 557 [cell setSelectedForEditing:selected animated:animateSelectedState]; |
| 558 BookmarkItemCell* itemCell = static_cast<BookmarkItemCell*>(cell); |
| 559 [itemCell showButtonOfType:[self buttonTypeForCellAtIndexPath:indexPath] |
| 560 animated:animateMenuVisibility]; |
| 561 } |
| 562 |
| 563 - (void)updateEditingStateOfCellAtIndexPath:(NSIndexPath*)indexPath |
| 564 animateMenuVisibility:(BOOL)animateMenuVisibility |
| 565 animateSelectedState:(BOOL)animateSelectedState { |
| 566 BookmarkCell* cell = base::mac::ObjCCast<BookmarkCell>( |
| 567 [self.collectionView cellForItemAtIndexPath:indexPath]); |
| 568 if (!cell) |
| 569 return; |
| 570 |
| 571 [self updateEditingStateOfCell:cell |
| 572 atIndexPath:indexPath |
| 573 animateMenuVisibility:animateMenuVisibility |
| 574 animateSelectedState:animateSelectedState]; |
| 575 } |
| 576 |
| 577 #pragma mark - BookmarkModelObserver Callbacks |
| 578 |
| 579 - (void)bookmarkModelLoaded { |
| 580 NOTREACHED(); |
| 581 } |
| 582 |
| 583 - (void)bookmarkNodeChanged:(const BookmarkNode*)bookmarkNode { |
| 584 NOTREACHED(); |
| 585 } |
| 586 |
| 587 - (void)bookmarkNodeChildrenChanged:(const BookmarkNode*)bookmarkNode { |
| 588 NOTREACHED(); |
| 589 } |
| 590 |
| 591 - (void)bookmarkNode:(const BookmarkNode*)bookmarkNode |
| 592 movedFromParent:(const BookmarkNode*)oldParent |
| 593 toParent:(const BookmarkNode*)newParent { |
| 594 NOTREACHED(); |
| 595 } |
| 596 |
| 597 - (void)bookmarkNodeDeleted:(const BookmarkNode*)node |
| 598 fromFolder:(const BookmarkNode*)folder { |
| 599 NOTREACHED(); |
| 600 } |
| 601 |
| 602 - (void)bookmarkModelRemovedAllNodes { |
| 603 NOTREACHED(); |
| 604 } |
| 605 |
| 606 #pragma mark - Public Methods That Must Be Overridden |
| 607 |
| 608 - (BOOL)shouldSelectCellAtIndexPath:(NSIndexPath*)indexPath { |
| 609 NOTREACHED(); |
| 610 return NO; |
| 611 } |
| 612 |
| 613 - (void)didTapCellAtIndexPath:(NSIndexPath*)indexPath { |
| 614 NOTREACHED(); |
| 615 } |
| 616 |
| 617 - (void)didAddCellForEditingAtIndexPath:(NSIndexPath*)indexPath { |
| 618 NOTREACHED(); |
| 619 } |
| 620 |
| 621 - (void)didRemoveCellForEditingAtIndexPath:(NSIndexPath*)indexPath { |
| 622 NOTREACHED(); |
| 623 } |
| 624 |
| 625 - (void)didTapMenuButtonAtIndexPath:(NSIndexPath*)indexPath |
| 626 onView:(UIView*)view |
| 627 forCell:(BookmarkItemCell*)cell { |
| 628 NOTREACHED(); |
| 629 } |
| 630 |
| 631 - (bookmark_cell::ButtonType)buttonTypeForCellAtIndexPath: |
| 632 (NSIndexPath*)indexPath { |
| 633 NOTREACHED(); |
| 634 return bookmark_cell::ButtonNone; |
| 635 } |
| 636 |
| 637 - (BOOL)allowLongPressForCellAtIndexPath:(NSIndexPath*)indexPath { |
| 638 NOTREACHED(); |
| 639 return NO; |
| 640 } |
| 641 |
| 642 - (void)didLongPressCell:(UICollectionViewCell*)cell |
| 643 atIndexPath:(NSIndexPath*)indexPath { |
| 644 NOTREACHED(); |
| 645 } |
| 646 |
| 647 - (BOOL)cellIsSelectedForEditingAtIndexPath:(NSIndexPath*)indexPath { |
| 648 NOTREACHED(); |
| 649 return NO; |
| 650 } |
| 651 |
| 652 - (CGSize)headerSizeForSection:(NSInteger)section { |
| 653 NOTREACHED(); |
| 654 return CGSizeZero; |
| 655 } |
| 656 |
| 657 - (UICollectionViewCell*)cellAtIndexPath:(NSIndexPath*)indexPath { |
| 658 NOTREACHED(); |
| 659 return nil; |
| 660 } |
| 661 |
| 662 - (UICollectionReusableView*)headerAtIndexPath:(NSIndexPath*)indexPath { |
| 663 NOTREACHED(); |
| 664 return nil; |
| 665 } |
| 666 |
| 667 - (NSInteger)numberOfItemsInSection:(NSInteger)section { |
| 668 NOTREACHED(); |
| 669 return 0; |
| 670 } |
| 671 - (NSInteger)numberOfSections { |
| 672 NOTREACHED(); |
| 673 return 0; |
| 674 } |
| 675 |
| 676 - (void)updateCollectionView { |
| 677 NOTREACHED(); |
| 678 } |
| 679 |
| 680 - (const bookmarks::BookmarkNode*)nodeAtIndexPath:(NSIndexPath*)indexPath { |
| 681 NOTREACHED(); |
| 682 return nullptr; |
| 683 } |
| 684 |
| 685 #pragma mark - Methods that subclasses can override (UI) |
| 686 |
| 687 - (UIEdgeInsets)insetForSectionAtIndex:(NSInteger)section { |
| 688 if ([self isPromoSection:section]) |
| 689 return UIEdgeInsetsZero; |
| 690 |
| 691 if (IsIPadIdiom()) { |
| 692 return UIEdgeInsetsMake(10, rowMarginTablet, 0, rowMarginTablet); |
| 693 } else { |
| 694 return UIEdgeInsetsZero; |
| 695 } |
| 696 } |
| 697 |
| 698 - (CGSize)cellSizeForIndexPath:(NSIndexPath*)indexPath { |
| 699 if ([self isPromoSection:indexPath.section]) { |
| 700 CGRect estimatedFrame = CGRectMake(0, 0, CGRectGetWidth(self.bounds), 100); |
| 701 UICollectionViewCell* cell = |
| 702 [self.collectionView cellForItemAtIndexPath:indexPath]; |
| 703 if (!cell) { |
| 704 cell = [[[BookmarkPromoCell alloc] initWithFrame:estimatedFrame] |
| 705 autorelease]; |
| 706 } |
| 707 cell.frame = estimatedFrame; |
| 708 [cell layoutIfNeeded]; |
| 709 return [cell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; |
| 710 } |
| 711 |
| 712 UIEdgeInsets insets = [self insetForSectionAtIndex:indexPath.section]; |
| 713 return CGSizeMake(self.bounds.size.width - (insets.right + insets.left), |
| 714 rowHeight); |
| 715 } |
| 716 |
| 717 - (CGFloat)minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { |
| 718 return 0; |
| 719 } |
| 720 |
| 721 - (CGFloat)minimumLineSpacingForSectionAtIndex:(NSInteger)section { |
| 722 return 0; |
| 723 } |
| 724 |
| 725 - (NSString*)textWhenCollectionIsEmpty { |
| 726 return l10n_util::GetNSString(IDS_IOS_BOOKMARK_NO_BOOKMARKS_LABEL); |
| 727 } |
| 728 |
| 729 #pragma mark - Public Methods That Can Be Overridden |
| 730 |
| 731 - (void)collectionViewScrolled { |
| 732 } |
| 733 |
| 734 #pragma mark - Editing |
| 735 |
| 736 - (void)setEditing:(BOOL)editing animated:(BOOL)animated { |
| 737 if (self.editing == editing) |
| 738 return; |
| 739 |
| 740 _editing = editing; |
| 741 [UIView animateWithDuration:animated ? 0.2 : 0.0 |
| 742 delay:0 |
| 743 options:UIViewAnimationOptionBeginFromCurrentState |
| 744 animations:^{ |
| 745 self.shadow.alpha = editing ? 0.0 : 1.0; |
| 746 } |
| 747 completion:nil]; |
| 748 |
| 749 // If the promo is active this means that it is removed and added as the edit |
| 750 // mode changes, making reloading the data mandatory. |
| 751 if ([self isPromoActive]) { |
| 752 [self.collectionView reloadData]; |
| 753 [self.collectionView.collectionViewLayout invalidateLayout]; |
| 754 } else { |
| 755 // Update the visual state of the bookmark cells without reloading the |
| 756 // section. |
| 757 // This prevents flickering of images that need to be asynchronously |
| 758 // reloaded. |
| 759 NSArray* indexPaths = [self.collectionView indexPathsForVisibleItems]; |
| 760 for (NSIndexPath* indexPath in indexPaths) { |
| 761 [self updateEditingStateOfCellAtIndexPath:indexPath |
| 762 animateMenuVisibility:animated |
| 763 animateSelectedState:NO]; |
| 764 } |
| 765 } |
| 766 } |
| 767 |
| 768 #pragma mark - Public Methods |
| 769 |
| 770 - (void)changeOrientation:(UIInterfaceOrientation)orientation { |
| 771 [self updateCollectionView]; |
| 772 } |
| 773 |
| 774 - (CGFloat)contentPositionInPortraitOrientation { |
| 775 if (IsPortrait()) |
| 776 return self.collectionView.contentOffset.y; |
| 777 |
| 778 // In short landscape mode and portrait mode, there are 2 cells per row. |
| 779 if ([self wideLandscapeMode]) |
| 780 return self.collectionView.contentOffset.y; |
| 781 |
| 782 // In wide landscape mode, there are 3 cells per row. |
| 783 return self.collectionView.contentOffset.y * 3 / 2.0; |
| 784 } |
| 785 |
| 786 - (void)applyContentPosition:(CGFloat)position { |
| 787 if (IsLandscape() && [self wideLandscapeMode]) { |
| 788 position = position * 2 / 3.0; |
| 789 } |
| 790 |
| 791 CGFloat y = |
| 792 MIN(position, |
| 793 [self.collectionView.collectionViewLayout collectionViewContentSize] |
| 794 .height); |
| 795 self.collectionView.contentOffset = |
| 796 CGPointMake(self.collectionView.contentOffset.x, y); |
| 797 } |
| 798 |
| 799 - (void)setScrollsToTop:(BOOL)scrollsToTop { |
| 800 self.collectionView.scrollsToTop = scrollsToTop; |
| 801 } |
| 802 |
| 803 #pragma mark - Private Methods |
| 804 |
| 805 - (BOOL)wideLandscapeMode { |
| 806 return self.frame.size.width > 567; |
| 807 } |
| 808 |
| 809 #pragma mark - UIGestureRecognizer Callbacks |
| 810 |
| 811 - (void)longPress:(UILongPressGestureRecognizer*)recognizer { |
| 812 if (self.longPressRecognizer.numberOfTouches != 1 || self.editing) |
| 813 return; |
| 814 |
| 815 if (self.longPressRecognizer.state == UIGestureRecognizerStateRecognized || |
| 816 self.longPressRecognizer.state == UIGestureRecognizerStateBegan) { |
| 817 CGPoint point = |
| 818 [self.longPressRecognizer locationOfTouch:0 inView:self.collectionView]; |
| 819 NSIndexPath* indexPath = |
| 820 [self.collectionView indexPathForItemAtPoint:point]; |
| 821 if (!indexPath) |
| 822 return; |
| 823 |
| 824 UICollectionViewCell* cell = |
| 825 [self.collectionView cellForItemAtIndexPath:indexPath]; |
| 826 |
| 827 // Notify the subclass that long press has been received. |
| 828 if (cell) |
| 829 [self didLongPressCell:cell atIndexPath:indexPath]; |
| 830 } |
| 831 } |
| 832 |
| 833 #pragma mark - UIGestureRecognizerDelegate |
| 834 |
| 835 - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer*)gestureRecognizer { |
| 836 DCHECK(gestureRecognizer == self.longPressRecognizer); |
| 837 CGPoint point = |
| 838 [gestureRecognizer locationOfTouch:0 inView:self.collectionView]; |
| 839 NSIndexPath* indexPath = [self.collectionView indexPathForItemAtPoint:point]; |
| 840 if (!indexPath) |
| 841 return NO; |
| 842 return [self allowLongPressForCellAtIndexPath:indexPath]; |
| 843 } |
| 844 |
| 845 #pragma mark - Promo Cell |
| 846 |
| 847 - (BOOL)isPromoSection:(NSInteger)section { |
| 848 return section == 0 && [self shouldShowPromoCell]; |
| 849 } |
| 850 |
| 851 - (BOOL)shouldShowPromoCell { |
| 852 return NO; |
| 853 } |
| 854 |
| 855 - (BOOL)isPromoActive { |
| 856 return NO; |
| 857 } |
| 858 |
| 859 @end |
OLD | NEW |