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/history/tab_history_view_controller.h" |
| 6 |
| 7 #import "base/ios/weak_nsobject.h" |
| 8 #include "base/logging.h" |
| 9 #include "base/mac/foundation_util.h" |
| 10 #include "base/mac/objc_property_releaser.h" |
| 11 #include "base/mac/scoped_nsobject.h" |
| 12 #include "base/strings/sys_string_conversions.h" |
| 13 #import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h" |
| 14 #include "ios/chrome/browser/ui/commands/ios_command_ids.h" |
| 15 #import "ios/chrome/browser/ui/history/tab_history_cell.h" |
| 16 #include "ios/chrome/browser/ui/rtl_geometry.h" |
| 17 #import "ios/third_party/material_components_ios/src/components/Ink/src/Material
Ink.h" |
| 18 #import "ios/web/navigation/crw_session_entry.h" |
| 19 #include "ios/web/public/favicon_status.h" |
| 20 #include "ios/web/public/navigation_item.h" |
| 21 #include "ui/gfx/image/image.h" |
| 22 |
| 23 namespace { |
| 24 |
| 25 // Visible percentage of the last visible row on the Tools menu if the |
| 26 // Tools menu is scrollable. |
| 27 const CGFloat kLastRowVisiblePercentage = 0.6; |
| 28 // Reuse identifier for cells. |
| 29 NSString* cellIdentifier = @"TabHistoryCell"; |
| 30 NSString* footerIdentifier = @"Footer"; |
| 31 NSString* headerIdentifier = @"Header"; |
| 32 // Height of rows. |
| 33 const CGFloat kCellHeight = 48.0; |
| 34 // Fraction height for partially visible row. |
| 35 const CGFloat kCellHeightLastRow = kCellHeight * kLastRowVisiblePercentage; |
| 36 // Width and leading for the header view. |
| 37 const CGFloat kHeaderLeadingInset = 0; |
| 38 const CGFloat kHeaderWidth = 48; |
| 39 // Leading and trailing insets for cell items. |
| 40 const CGFloat kCellLeadingInset = kHeaderLeadingInset + kHeaderWidth; |
| 41 const CGFloat kCellTrailingInset = 16; |
| 42 |
| 43 typedef std::vector<CGFloat> CGFloatVector; |
| 44 typedef std::vector<CGFloatVector> ItemOffsetVector; |
| 45 |
| 46 NS_INLINE CGFloat OffsetForPath(const ItemOffsetVector& offsets, |
| 47 NSInteger section, |
| 48 NSInteger item) { |
| 49 DCHECK(section < (NSInteger)offsets.size()); |
| 50 DCHECK(item < (NSInteger)offsets.at(section).size()); |
| 51 |
| 52 return offsets.at(section).at(item); |
| 53 } |
| 54 |
| 55 NS_INLINE CGFloat OffsetForPath(const ItemOffsetVector& offsets, |
| 56 NSIndexPath* path) { |
| 57 return OffsetForPath(offsets, [path section], [path item]); |
| 58 } |
| 59 |
| 60 NS_INLINE CGFloat OffsetForSection(const ItemOffsetVector& offsets, |
| 61 NSIndexPath* path) { |
| 62 DCHECK([path section] < (NSInteger)offsets.size()); |
| 63 DCHECK(offsets.at([path section]).size()); |
| 64 |
| 65 return offsets.at([path section]).at(0); |
| 66 } |
| 67 |
| 68 // Height for the footer view. |
| 69 NS_INLINE CGFloat FooterHeight() { |
| 70 return 1.0 / [[UIScreen mainScreen] scale]; |
| 71 } |
| 72 |
| 73 } // namespace |
| 74 |
| 75 @interface TabHistoryViewControllerLayout : UICollectionViewLayout |
| 76 @end |
| 77 |
| 78 @implementation TabHistoryViewControllerLayout { |
| 79 ItemOffsetVector _itemYOffsets; |
| 80 CGFloat _containerCalculatedHeight; |
| 81 CGFloat _containerWidth; |
| 82 CGFloat _cellItemWidth; |
| 83 CGFloat _footerWidth; |
| 84 } |
| 85 |
| 86 - (void)prepareLayout { |
| 87 [super prepareLayout]; |
| 88 |
| 89 UICollectionView* collectionView = [self collectionView]; |
| 90 CGFloat yOffset = 0; |
| 91 |
| 92 NSInteger numberOfSections = [collectionView numberOfSections]; |
| 93 _itemYOffsets.reserve(numberOfSections); |
| 94 |
| 95 for (NSInteger section = 0; section < numberOfSections; ++section) { |
| 96 NSInteger numberOfItems = [collectionView numberOfItemsInSection:section]; |
| 97 |
| 98 CGFloatVector dummy; |
| 99 _itemYOffsets.push_back(dummy); |
| 100 |
| 101 CGFloatVector& sectionYOffsets = _itemYOffsets.at(section); |
| 102 sectionYOffsets.reserve(numberOfItems); |
| 103 |
| 104 for (NSInteger item = 0; item < numberOfItems; ++item) { |
| 105 sectionYOffsets.push_back(yOffset); |
| 106 yOffset += kCellHeight; |
| 107 } |
| 108 |
| 109 // The last section should not have a footer. |
| 110 if (numberOfItems && (section + 1) < numberOfSections) { |
| 111 yOffset += FooterHeight(); |
| 112 } |
| 113 } |
| 114 |
| 115 CGRect containerBounds = [collectionView bounds]; |
| 116 _containerWidth = CGRectGetWidth(containerBounds); |
| 117 _cellItemWidth = _containerWidth - kCellLeadingInset - kCellTrailingInset; |
| 118 _footerWidth = _containerWidth - kCellLeadingInset; |
| 119 _containerCalculatedHeight = yOffset - kCellHeight / 2.0; |
| 120 } |
| 121 |
| 122 - (void)invalidateLayout { |
| 123 [super invalidateLayout]; |
| 124 _itemYOffsets.clear(); |
| 125 _containerCalculatedHeight = 0; |
| 126 _cellItemWidth = 0; |
| 127 _footerWidth = 0; |
| 128 } |
| 129 |
| 130 - (CGSize)collectionViewContentSize { |
| 131 return CGSizeMake(_containerWidth, _containerCalculatedHeight); |
| 132 } |
| 133 |
| 134 - (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect { |
| 135 UICollectionView* collectionView = [self collectionView]; |
| 136 NSMutableArray* array = [NSMutableArray array]; |
| 137 |
| 138 NSInteger numberOfSections = [collectionView numberOfSections]; |
| 139 for (NSInteger section = 0; section < numberOfSections; ++section) { |
| 140 NSInteger numberOfItems = [collectionView numberOfItemsInSection:section]; |
| 141 if (numberOfItems) { |
| 142 NSIndexPath* path = |
| 143 [NSIndexPath indexPathForItem:numberOfItems - 1 inSection:section]; |
| 144 [array addObject:[self layoutAttributesForSupplementaryViewOfKind: |
| 145 UICollectionElementKindSectionHeader |
| 146 atIndexPath:path]]; |
| 147 } |
| 148 |
| 149 for (NSInteger item = 0; item < numberOfItems; ++item) { |
| 150 NSIndexPath* path = [NSIndexPath indexPathForItem:item inSection:section]; |
| 151 [array addObject:[self layoutAttributesForItemAtIndexPath:path]]; |
| 152 } |
| 153 |
| 154 // The last section should not have a footer. |
| 155 if (numberOfItems && (section + 1) < numberOfSections) { |
| 156 NSIndexPath* path = |
| 157 [NSIndexPath indexPathForItem:numberOfItems - 1 inSection:section]; |
| 158 [array addObject:[self layoutAttributesForSupplementaryViewOfKind: |
| 159 UICollectionElementKindSectionFooter |
| 160 atIndexPath:path]]; |
| 161 } |
| 162 } |
| 163 |
| 164 return array; |
| 165 } |
| 166 |
| 167 - (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath: |
| 168 (NSIndexPath*)indexPath { |
| 169 CGFloat yOffset = OffsetForPath(_itemYOffsets, indexPath); |
| 170 |
| 171 UICollectionViewLayoutAttributes* attributes = |
| 172 [UICollectionViewLayoutAttributes |
| 173 layoutAttributesForCellWithIndexPath:indexPath]; |
| 174 LayoutRect cellLayout = LayoutRectMake(kCellLeadingInset, _containerWidth, |
| 175 yOffset, _cellItemWidth, kCellHeight); |
| 176 [attributes setFrame:LayoutRectGetRect(cellLayout)]; |
| 177 [attributes setZIndex:1]; |
| 178 |
| 179 return attributes; |
| 180 } |
| 181 |
| 182 - (UICollectionViewLayoutAttributes*) |
| 183 layoutAttributesForSupplementaryViewOfKind:(NSString*)kind |
| 184 atIndexPath:(NSIndexPath*)indexPath { |
| 185 CGFloat yOffset = OffsetForPath(_itemYOffsets, indexPath); |
| 186 |
| 187 UICollectionViewLayoutAttributes* attributes = |
| 188 [UICollectionViewLayoutAttributes |
| 189 layoutAttributesForSupplementaryViewOfKind:kind |
| 190 withIndexPath:indexPath]; |
| 191 |
| 192 if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { |
| 193 // The height is the yOffset of the first section minus the yOffset of the |
| 194 // last item. Additionally, the height of a cell needs to be added back in |
| 195 // since the _itemYOffsets stores Mid Y. |
| 196 CGFloat headerYOffset = OffsetForSection(_itemYOffsets, indexPath); |
| 197 CGFloat height = yOffset - headerYOffset + kCellHeight; |
| 198 |
| 199 if ([indexPath section]) |
| 200 headerYOffset += FooterHeight(); |
| 201 |
| 202 LayoutRect cellLayout = LayoutRectMake(kHeaderLeadingInset, _containerWidth, |
| 203 headerYOffset, kHeaderWidth, height); |
| 204 [attributes setFrame:LayoutRectGetRect(cellLayout)]; |
| 205 [attributes setZIndex:0]; |
| 206 } else if ([kind isEqualToString:UICollectionElementKindSectionFooter]) { |
| 207 LayoutRect cellLayout = |
| 208 LayoutRectMake(kCellLeadingInset, _containerWidth, yOffset, |
| 209 _footerWidth, FooterHeight()); |
| 210 [attributes setFrame:LayoutRectGetRect(cellLayout)]; |
| 211 [attributes setZIndex:0]; |
| 212 } |
| 213 |
| 214 return attributes; |
| 215 } |
| 216 |
| 217 @end |
| 218 |
| 219 @interface TabHistoryViewController ()<MDCInkTouchControllerDelegate> { |
| 220 base::scoped_nsobject<MDCInkTouchController> _inkTouchController; |
| 221 base::scoped_nsobject<NSArray> _partitionedEntries; |
| 222 base::scoped_nsobject<NSArray> _sessionEntries; |
| 223 } |
| 224 @end |
| 225 |
| 226 @implementation TabHistoryViewController |
| 227 |
| 228 - (NSArray*)sessionEntries { |
| 229 return [[_sessionEntries retain] autorelease]; |
| 230 } |
| 231 |
| 232 #pragma mark Public Methods |
| 233 |
| 234 - (CGFloat)optimalHeight:(CGFloat)suggestedHeight { |
| 235 DCHECK(suggestedHeight >= kCellHeight); |
| 236 CGFloat optimalHeight = 0; |
| 237 |
| 238 for (NSArray* sectionArray in _partitionedEntries.get()) { |
| 239 NSUInteger sectionItemCount = [sectionArray count]; |
| 240 for (NSUInteger i = 0; i < sectionItemCount; ++i) { |
| 241 CGFloat proposedHeight = optimalHeight + kCellHeight; |
| 242 |
| 243 if (proposedHeight > suggestedHeight) { |
| 244 CGFloat difference = proposedHeight - suggestedHeight; |
| 245 if (difference > kCellHeightLastRow) { |
| 246 return optimalHeight + kCellHeightLastRow; |
| 247 } else { |
| 248 return optimalHeight - kCellHeight + kCellHeightLastRow; |
| 249 } |
| 250 } |
| 251 |
| 252 optimalHeight = proposedHeight; |
| 253 } |
| 254 |
| 255 optimalHeight += FooterHeight(); |
| 256 } |
| 257 |
| 258 // If this point is reached, it means the entire content fits and this last |
| 259 // section should not include the footer. |
| 260 optimalHeight -= FooterHeight(); |
| 261 |
| 262 return optimalHeight; |
| 263 } |
| 264 |
| 265 - (instancetype)init { |
| 266 base::scoped_nsobject<TabHistoryViewControllerLayout> layout( |
| 267 [[TabHistoryViewControllerLayout alloc] init]); |
| 268 |
| 269 return [self initWithCollectionViewLayout:layout]; |
| 270 } |
| 271 |
| 272 - (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout*)layout { |
| 273 self = [super initWithCollectionViewLayout:layout]; |
| 274 if (self) { |
| 275 UICollectionView* collectionView = [self collectionView]; |
| 276 [collectionView setBackgroundColor:[UIColor whiteColor]]; |
| 277 |
| 278 [collectionView registerClass:[TabHistoryCell class] |
| 279 forCellWithReuseIdentifier:cellIdentifier]; |
| 280 |
| 281 [collectionView registerClass:[TabHistorySectionHeader class] |
| 282 forSupplementaryViewOfKind:UICollectionElementKindSectionHeader |
| 283 withReuseIdentifier:headerIdentifier]; |
| 284 |
| 285 [collectionView registerClass:[TabHistorySectionFooter class] |
| 286 forSupplementaryViewOfKind:UICollectionElementKindSectionFooter |
| 287 withReuseIdentifier:footerIdentifier]; |
| 288 |
| 289 _inkTouchController.reset( |
| 290 [[MDCInkTouchController alloc] initWithView:collectionView]); |
| 291 [_inkTouchController setDelegate:self]; |
| 292 [_inkTouchController addInkView]; |
| 293 } |
| 294 |
| 295 return self; |
| 296 } |
| 297 |
| 298 #pragma mark UICollectionViewDelegate |
| 299 |
| 300 - (void)collectionView:(UICollectionView*)collectionView |
| 301 didSelectItemAtIndexPath:(NSIndexPath*)indexPath { |
| 302 UICollectionViewCell* cell = |
| 303 [collectionView cellForItemAtIndexPath:indexPath]; |
| 304 [collectionView chromeExecuteCommand:cell]; |
| 305 } |
| 306 |
| 307 #pragma mark UICollectionViewDataSource |
| 308 |
| 309 - (CRWSessionEntry*)entryForIndexPath:(NSIndexPath*)indexPath { |
| 310 NSInteger section = [indexPath section]; |
| 311 NSInteger item = [indexPath item]; |
| 312 |
| 313 DCHECK(section < (NSInteger)[_partitionedEntries count]); |
| 314 DCHECK(item < (NSInteger)[[_partitionedEntries objectAtIndex:section] count]); |
| 315 NSArray* sectionedArray = [_partitionedEntries objectAtIndex:section]; |
| 316 |
| 317 return [sectionedArray objectAtIndex:item]; |
| 318 } |
| 319 |
| 320 - (NSInteger)collectionView:(UICollectionView*)collectionView |
| 321 numberOfItemsInSection:(NSInteger)section { |
| 322 DCHECK(section < (NSInteger)[_partitionedEntries count]); |
| 323 return [[_partitionedEntries objectAtIndex:section] count]; |
| 324 } |
| 325 |
| 326 - (UICollectionViewCell*)collectionView:(UICollectionView*)collectionView |
| 327 cellForItemAtIndexPath:(NSIndexPath*)indexPath { |
| 328 TabHistoryCell* cell = |
| 329 [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier |
| 330 forIndexPath:indexPath]; |
| 331 |
| 332 [cell setEntry:[self entryForIndexPath:indexPath]]; |
| 333 [cell setTag:IDC_BACK_FORWARD_IN_TAB_HISTORY]; |
| 334 |
| 335 return cell; |
| 336 } |
| 337 |
| 338 - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView*)view { |
| 339 return [_partitionedEntries count]; |
| 340 } |
| 341 |
| 342 - (UICollectionReusableView*)collectionView:(UICollectionView*)view |
| 343 viewForSupplementaryElementOfKind:(NSString*)kind |
| 344 atIndexPath:(NSIndexPath*)indexPath { |
| 345 if ([kind isEqualToString:UICollectionElementKindSectionFooter]) { |
| 346 return [view dequeueReusableSupplementaryViewOfKind:kind |
| 347 withReuseIdentifier:footerIdentifier |
| 348 forIndexPath:indexPath]; |
| 349 } |
| 350 |
| 351 DCHECK([kind isEqualToString:UICollectionElementKindSectionHeader]); |
| 352 CRWSessionEntry* sessionEntry = [self entryForIndexPath:indexPath]; |
| 353 web::NavigationItem* navigationItem = [sessionEntry navigationItem]; |
| 354 |
| 355 TabHistorySectionHeader* header = |
| 356 [view dequeueReusableSupplementaryViewOfKind:kind |
| 357 withReuseIdentifier:headerIdentifier |
| 358 forIndexPath:indexPath]; |
| 359 |
| 360 UIImage* iconImage = nil; |
| 361 const gfx::Image& image = navigationItem->GetFavicon().image; |
| 362 if (!image.IsEmpty()) |
| 363 iconImage = image.ToUIImage(); |
| 364 else |
| 365 iconImage = [UIImage imageNamed:@"default_favicon"]; |
| 366 |
| 367 [[header iconView] setImage:iconImage]; |
| 368 |
| 369 return header; |
| 370 } |
| 371 |
| 372 - (void)setSessionEntries:(NSArray*)sessionEntries { |
| 373 _sessionEntries.reset([sessionEntries retain]); |
| 374 |
| 375 std::string previousHost; |
| 376 |
| 377 NSMutableArray* sectionArray = [NSMutableArray array]; |
| 378 NSMutableArray* partitionedEntries = [NSMutableArray array]; |
| 379 |
| 380 NSInteger numberOfEntries = [_sessionEntries count]; |
| 381 for (NSInteger index = 0; index < numberOfEntries; ++index) { |
| 382 CRWSessionEntry* sessionEntry = [_sessionEntries objectAtIndex:index]; |
| 383 web::NavigationItem* navigationItem = [sessionEntry navigationItem]; |
| 384 |
| 385 std::string currentHost; |
| 386 if (navigationItem) |
| 387 currentHost = navigationItem->GetURL().host(); |
| 388 |
| 389 if (previousHost.empty()) |
| 390 previousHost = currentHost; |
| 391 |
| 392 // TODO: This should use some sort of Top Level Domain matching instead of |
| 393 // explicit host match so that images.googe.com matches shopping.google.com. |
| 394 if (previousHost == currentHost) { |
| 395 [sectionArray addObject:sessionEntry]; |
| 396 } else { |
| 397 [partitionedEntries addObject:sectionArray]; |
| 398 sectionArray = [NSMutableArray arrayWithObject:sessionEntry]; |
| 399 previousHost = currentHost; |
| 400 } |
| 401 } |
| 402 |
| 403 if ([sectionArray count]) |
| 404 [partitionedEntries addObject:sectionArray]; |
| 405 |
| 406 if (![partitionedEntries count]) |
| 407 partitionedEntries = nil; |
| 408 |
| 409 _partitionedEntries.reset([partitionedEntries retain]); |
| 410 } |
| 411 |
| 412 #pragma mark MDCInkTouchControllerDelegate |
| 413 |
| 414 - (BOOL)inkTouchController:(MDCInkTouchController*)inkTouchController |
| 415 shouldProcessInkTouchesAtTouchLocation:(CGPoint)location { |
| 416 NSIndexPath* indexPath = |
| 417 [self.collectionView indexPathForItemAtPoint:location]; |
| 418 TabHistoryCell* cell = base::mac::ObjCCastStrict<TabHistoryCell>( |
| 419 [self.collectionView cellForItemAtIndexPath:indexPath]); |
| 420 if (!cell) { |
| 421 return NO; |
| 422 } |
| 423 |
| 424 // Increase the size of the ink view to cover the collection view from edge |
| 425 // to edge. |
| 426 CGRect inkViewFrame = [cell frame]; |
| 427 inkViewFrame.origin.x = 0; |
| 428 inkViewFrame.size.width = CGRectGetWidth([self.collectionView bounds]); |
| 429 [[inkTouchController defaultInkView] setFrame:inkViewFrame]; |
| 430 return YES; |
| 431 } |
| 432 |
| 433 @end |
OLD | NEW |