Index: ios/chrome/browser/ui/history/tab_history_view_controller.mm |
diff --git a/ios/chrome/browser/ui/history/tab_history_view_controller.mm b/ios/chrome/browser/ui/history/tab_history_view_controller.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..fcd1f851ac8422802ef7391d424df8895552f743 |
--- /dev/null |
+++ b/ios/chrome/browser/ui/history/tab_history_view_controller.mm |
@@ -0,0 +1,433 @@ |
+// 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/history/tab_history_view_controller.h" |
+ |
+#import "base/ios/weak_nsobject.h" |
+#include "base/logging.h" |
+#include "base/mac/foundation_util.h" |
+#include "base/mac/objc_property_releaser.h" |
+#include "base/mac/scoped_nsobject.h" |
+#include "base/strings/sys_string_conversions.h" |
+#import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h" |
+#include "ios/chrome/browser/ui/commands/ios_command_ids.h" |
+#import "ios/chrome/browser/ui/history/tab_history_cell.h" |
+#include "ios/chrome/browser/ui/rtl_geometry.h" |
+#import "ios/third_party/material_components_ios/src/components/Ink/src/MaterialInk.h" |
+#import "ios/web/navigation/crw_session_entry.h" |
+#include "ios/web/public/favicon_status.h" |
+#include "ios/web/public/navigation_item.h" |
+#include "ui/gfx/image/image.h" |
+ |
+namespace { |
+ |
+// Visible percentage of the last visible row on the Tools menu if the |
+// Tools menu is scrollable. |
+const CGFloat kLastRowVisiblePercentage = 0.6; |
+// Reuse identifier for cells. |
+NSString* cellIdentifier = @"TabHistoryCell"; |
+NSString* footerIdentifier = @"Footer"; |
+NSString* headerIdentifier = @"Header"; |
+// Height of rows. |
+const CGFloat kCellHeight = 48.0; |
+// Fraction height for partially visible row. |
+const CGFloat kCellHeightLastRow = kCellHeight * kLastRowVisiblePercentage; |
+// Width and leading for the header view. |
+const CGFloat kHeaderLeadingInset = 0; |
+const CGFloat kHeaderWidth = 48; |
+// Leading and trailing insets for cell items. |
+const CGFloat kCellLeadingInset = kHeaderLeadingInset + kHeaderWidth; |
+const CGFloat kCellTrailingInset = 16; |
+ |
+typedef std::vector<CGFloat> CGFloatVector; |
+typedef std::vector<CGFloatVector> ItemOffsetVector; |
+ |
+NS_INLINE CGFloat OffsetForPath(const ItemOffsetVector& offsets, |
+ NSInteger section, |
+ NSInteger item) { |
+ DCHECK(section < (NSInteger)offsets.size()); |
+ DCHECK(item < (NSInteger)offsets.at(section).size()); |
+ |
+ return offsets.at(section).at(item); |
+} |
+ |
+NS_INLINE CGFloat OffsetForPath(const ItemOffsetVector& offsets, |
+ NSIndexPath* path) { |
+ return OffsetForPath(offsets, [path section], [path item]); |
+} |
+ |
+NS_INLINE CGFloat OffsetForSection(const ItemOffsetVector& offsets, |
+ NSIndexPath* path) { |
+ DCHECK([path section] < (NSInteger)offsets.size()); |
+ DCHECK(offsets.at([path section]).size()); |
+ |
+ return offsets.at([path section]).at(0); |
+} |
+ |
+// Height for the footer view. |
+NS_INLINE CGFloat FooterHeight() { |
+ return 1.0 / [[UIScreen mainScreen] scale]; |
+} |
+ |
+} // namespace |
+ |
+@interface TabHistoryViewControllerLayout : UICollectionViewLayout |
+@end |
+ |
+@implementation TabHistoryViewControllerLayout { |
+ ItemOffsetVector _itemYOffsets; |
+ CGFloat _containerCalculatedHeight; |
+ CGFloat _containerWidth; |
+ CGFloat _cellItemWidth; |
+ CGFloat _footerWidth; |
+} |
+ |
+- (void)prepareLayout { |
+ [super prepareLayout]; |
+ |
+ UICollectionView* collectionView = [self collectionView]; |
+ CGFloat yOffset = 0; |
+ |
+ NSInteger numberOfSections = [collectionView numberOfSections]; |
+ _itemYOffsets.reserve(numberOfSections); |
+ |
+ for (NSInteger section = 0; section < numberOfSections; ++section) { |
+ NSInteger numberOfItems = [collectionView numberOfItemsInSection:section]; |
+ |
+ CGFloatVector dummy; |
+ _itemYOffsets.push_back(dummy); |
+ |
+ CGFloatVector& sectionYOffsets = _itemYOffsets.at(section); |
+ sectionYOffsets.reserve(numberOfItems); |
+ |
+ for (NSInteger item = 0; item < numberOfItems; ++item) { |
+ sectionYOffsets.push_back(yOffset); |
+ yOffset += kCellHeight; |
+ } |
+ |
+ // The last section should not have a footer. |
+ if (numberOfItems && (section + 1) < numberOfSections) { |
+ yOffset += FooterHeight(); |
+ } |
+ } |
+ |
+ CGRect containerBounds = [collectionView bounds]; |
+ _containerWidth = CGRectGetWidth(containerBounds); |
+ _cellItemWidth = _containerWidth - kCellLeadingInset - kCellTrailingInset; |
+ _footerWidth = _containerWidth - kCellLeadingInset; |
+ _containerCalculatedHeight = yOffset - kCellHeight / 2.0; |
+} |
+ |
+- (void)invalidateLayout { |
+ [super invalidateLayout]; |
+ _itemYOffsets.clear(); |
+ _containerCalculatedHeight = 0; |
+ _cellItemWidth = 0; |
+ _footerWidth = 0; |
+} |
+ |
+- (CGSize)collectionViewContentSize { |
+ return CGSizeMake(_containerWidth, _containerCalculatedHeight); |
+} |
+ |
+- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect { |
+ UICollectionView* collectionView = [self collectionView]; |
+ NSMutableArray* array = [NSMutableArray array]; |
+ |
+ NSInteger numberOfSections = [collectionView numberOfSections]; |
+ for (NSInteger section = 0; section < numberOfSections; ++section) { |
+ NSInteger numberOfItems = [collectionView numberOfItemsInSection:section]; |
+ if (numberOfItems) { |
+ NSIndexPath* path = |
+ [NSIndexPath indexPathForItem:numberOfItems - 1 inSection:section]; |
+ [array addObject:[self layoutAttributesForSupplementaryViewOfKind: |
+ UICollectionElementKindSectionHeader |
+ atIndexPath:path]]; |
+ } |
+ |
+ for (NSInteger item = 0; item < numberOfItems; ++item) { |
+ NSIndexPath* path = [NSIndexPath indexPathForItem:item inSection:section]; |
+ [array addObject:[self layoutAttributesForItemAtIndexPath:path]]; |
+ } |
+ |
+ // The last section should not have a footer. |
+ if (numberOfItems && (section + 1) < numberOfSections) { |
+ NSIndexPath* path = |
+ [NSIndexPath indexPathForItem:numberOfItems - 1 inSection:section]; |
+ [array addObject:[self layoutAttributesForSupplementaryViewOfKind: |
+ UICollectionElementKindSectionFooter |
+ atIndexPath:path]]; |
+ } |
+ } |
+ |
+ return array; |
+} |
+ |
+- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath: |
+ (NSIndexPath*)indexPath { |
+ CGFloat yOffset = OffsetForPath(_itemYOffsets, indexPath); |
+ |
+ UICollectionViewLayoutAttributes* attributes = |
+ [UICollectionViewLayoutAttributes |
+ layoutAttributesForCellWithIndexPath:indexPath]; |
+ LayoutRect cellLayout = LayoutRectMake(kCellLeadingInset, _containerWidth, |
+ yOffset, _cellItemWidth, kCellHeight); |
+ [attributes setFrame:LayoutRectGetRect(cellLayout)]; |
+ [attributes setZIndex:1]; |
+ |
+ return attributes; |
+} |
+ |
+- (UICollectionViewLayoutAttributes*) |
+layoutAttributesForSupplementaryViewOfKind:(NSString*)kind |
+ atIndexPath:(NSIndexPath*)indexPath { |
+ CGFloat yOffset = OffsetForPath(_itemYOffsets, indexPath); |
+ |
+ UICollectionViewLayoutAttributes* attributes = |
+ [UICollectionViewLayoutAttributes |
+ layoutAttributesForSupplementaryViewOfKind:kind |
+ withIndexPath:indexPath]; |
+ |
+ if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { |
+ // The height is the yOffset of the first section minus the yOffset of the |
+ // last item. Additionally, the height of a cell needs to be added back in |
+ // since the _itemYOffsets stores Mid Y. |
+ CGFloat headerYOffset = OffsetForSection(_itemYOffsets, indexPath); |
+ CGFloat height = yOffset - headerYOffset + kCellHeight; |
+ |
+ if ([indexPath section]) |
+ headerYOffset += FooterHeight(); |
+ |
+ LayoutRect cellLayout = LayoutRectMake(kHeaderLeadingInset, _containerWidth, |
+ headerYOffset, kHeaderWidth, height); |
+ [attributes setFrame:LayoutRectGetRect(cellLayout)]; |
+ [attributes setZIndex:0]; |
+ } else if ([kind isEqualToString:UICollectionElementKindSectionFooter]) { |
+ LayoutRect cellLayout = |
+ LayoutRectMake(kCellLeadingInset, _containerWidth, yOffset, |
+ _footerWidth, FooterHeight()); |
+ [attributes setFrame:LayoutRectGetRect(cellLayout)]; |
+ [attributes setZIndex:0]; |
+ } |
+ |
+ return attributes; |
+} |
+ |
+@end |
+ |
+@interface TabHistoryViewController ()<MDCInkTouchControllerDelegate> { |
+ base::scoped_nsobject<MDCInkTouchController> _inkTouchController; |
+ base::scoped_nsobject<NSArray> _partitionedEntries; |
+ base::scoped_nsobject<NSArray> _sessionEntries; |
+} |
+@end |
+ |
+@implementation TabHistoryViewController |
+ |
+- (NSArray*)sessionEntries { |
+ return [[_sessionEntries retain] autorelease]; |
+} |
+ |
+#pragma mark Public Methods |
+ |
+- (CGFloat)optimalHeight:(CGFloat)suggestedHeight { |
+ DCHECK(suggestedHeight >= kCellHeight); |
+ CGFloat optimalHeight = 0; |
+ |
+ for (NSArray* sectionArray in _partitionedEntries.get()) { |
+ NSUInteger sectionItemCount = [sectionArray count]; |
+ for (NSUInteger i = 0; i < sectionItemCount; ++i) { |
+ CGFloat proposedHeight = optimalHeight + kCellHeight; |
+ |
+ if (proposedHeight > suggestedHeight) { |
+ CGFloat difference = proposedHeight - suggestedHeight; |
+ if (difference > kCellHeightLastRow) { |
+ return optimalHeight + kCellHeightLastRow; |
+ } else { |
+ return optimalHeight - kCellHeight + kCellHeightLastRow; |
+ } |
+ } |
+ |
+ optimalHeight = proposedHeight; |
+ } |
+ |
+ optimalHeight += FooterHeight(); |
+ } |
+ |
+ // If this point is reached, it means the entire content fits and this last |
+ // section should not include the footer. |
+ optimalHeight -= FooterHeight(); |
+ |
+ return optimalHeight; |
+} |
+ |
+- (instancetype)init { |
+ base::scoped_nsobject<TabHistoryViewControllerLayout> layout( |
+ [[TabHistoryViewControllerLayout alloc] init]); |
+ |
+ return [self initWithCollectionViewLayout:layout]; |
+} |
+ |
+- (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout*)layout { |
+ self = [super initWithCollectionViewLayout:layout]; |
+ if (self) { |
+ UICollectionView* collectionView = [self collectionView]; |
+ [collectionView setBackgroundColor:[UIColor whiteColor]]; |
+ |
+ [collectionView registerClass:[TabHistoryCell class] |
+ forCellWithReuseIdentifier:cellIdentifier]; |
+ |
+ [collectionView registerClass:[TabHistorySectionHeader class] |
+ forSupplementaryViewOfKind:UICollectionElementKindSectionHeader |
+ withReuseIdentifier:headerIdentifier]; |
+ |
+ [collectionView registerClass:[TabHistorySectionFooter class] |
+ forSupplementaryViewOfKind:UICollectionElementKindSectionFooter |
+ withReuseIdentifier:footerIdentifier]; |
+ |
+ _inkTouchController.reset( |
+ [[MDCInkTouchController alloc] initWithView:collectionView]); |
+ [_inkTouchController setDelegate:self]; |
+ [_inkTouchController addInkView]; |
+ } |
+ |
+ return self; |
+} |
+ |
+#pragma mark UICollectionViewDelegate |
+ |
+- (void)collectionView:(UICollectionView*)collectionView |
+ didSelectItemAtIndexPath:(NSIndexPath*)indexPath { |
+ UICollectionViewCell* cell = |
+ [collectionView cellForItemAtIndexPath:indexPath]; |
+ [collectionView chromeExecuteCommand:cell]; |
+} |
+ |
+#pragma mark UICollectionViewDataSource |
+ |
+- (CRWSessionEntry*)entryForIndexPath:(NSIndexPath*)indexPath { |
+ NSInteger section = [indexPath section]; |
+ NSInteger item = [indexPath item]; |
+ |
+ DCHECK(section < (NSInteger)[_partitionedEntries count]); |
+ DCHECK(item < (NSInteger)[[_partitionedEntries objectAtIndex:section] count]); |
+ NSArray* sectionedArray = [_partitionedEntries objectAtIndex:section]; |
+ |
+ return [sectionedArray objectAtIndex:item]; |
+} |
+ |
+- (NSInteger)collectionView:(UICollectionView*)collectionView |
+ numberOfItemsInSection:(NSInteger)section { |
+ DCHECK(section < (NSInteger)[_partitionedEntries count]); |
+ return [[_partitionedEntries objectAtIndex:section] count]; |
+} |
+ |
+- (UICollectionViewCell*)collectionView:(UICollectionView*)collectionView |
+ cellForItemAtIndexPath:(NSIndexPath*)indexPath { |
+ TabHistoryCell* cell = |
+ [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier |
+ forIndexPath:indexPath]; |
+ |
+ [cell setEntry:[self entryForIndexPath:indexPath]]; |
+ [cell setTag:IDC_BACK_FORWARD_IN_TAB_HISTORY]; |
+ |
+ return cell; |
+} |
+ |
+- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView*)view { |
+ return [_partitionedEntries count]; |
+} |
+ |
+- (UICollectionReusableView*)collectionView:(UICollectionView*)view |
+ viewForSupplementaryElementOfKind:(NSString*)kind |
+ atIndexPath:(NSIndexPath*)indexPath { |
+ if ([kind isEqualToString:UICollectionElementKindSectionFooter]) { |
+ return [view dequeueReusableSupplementaryViewOfKind:kind |
+ withReuseIdentifier:footerIdentifier |
+ forIndexPath:indexPath]; |
+ } |
+ |
+ DCHECK([kind isEqualToString:UICollectionElementKindSectionHeader]); |
+ CRWSessionEntry* sessionEntry = [self entryForIndexPath:indexPath]; |
+ web::NavigationItem* navigationItem = [sessionEntry navigationItem]; |
+ |
+ TabHistorySectionHeader* header = |
+ [view dequeueReusableSupplementaryViewOfKind:kind |
+ withReuseIdentifier:headerIdentifier |
+ forIndexPath:indexPath]; |
+ |
+ UIImage* iconImage = nil; |
+ const gfx::Image& image = navigationItem->GetFavicon().image; |
+ if (!image.IsEmpty()) |
+ iconImage = image.ToUIImage(); |
+ else |
+ iconImage = [UIImage imageNamed:@"default_favicon"]; |
+ |
+ [[header iconView] setImage:iconImage]; |
+ |
+ return header; |
+} |
+ |
+- (void)setSessionEntries:(NSArray*)sessionEntries { |
+ _sessionEntries.reset([sessionEntries retain]); |
+ |
+ std::string previousHost; |
+ |
+ NSMutableArray* sectionArray = [NSMutableArray array]; |
+ NSMutableArray* partitionedEntries = [NSMutableArray array]; |
+ |
+ NSInteger numberOfEntries = [_sessionEntries count]; |
+ for (NSInteger index = 0; index < numberOfEntries; ++index) { |
+ CRWSessionEntry* sessionEntry = [_sessionEntries objectAtIndex:index]; |
+ web::NavigationItem* navigationItem = [sessionEntry navigationItem]; |
+ |
+ std::string currentHost; |
+ if (navigationItem) |
+ currentHost = navigationItem->GetURL().host(); |
+ |
+ if (previousHost.empty()) |
+ previousHost = currentHost; |
+ |
+ // TODO: This should use some sort of Top Level Domain matching instead of |
+ // explicit host match so that images.googe.com matches shopping.google.com. |
+ if (previousHost == currentHost) { |
+ [sectionArray addObject:sessionEntry]; |
+ } else { |
+ [partitionedEntries addObject:sectionArray]; |
+ sectionArray = [NSMutableArray arrayWithObject:sessionEntry]; |
+ previousHost = currentHost; |
+ } |
+ } |
+ |
+ if ([sectionArray count]) |
+ [partitionedEntries addObject:sectionArray]; |
+ |
+ if (![partitionedEntries count]) |
+ partitionedEntries = nil; |
+ |
+ _partitionedEntries.reset([partitionedEntries retain]); |
+} |
+ |
+#pragma mark MDCInkTouchControllerDelegate |
+ |
+- (BOOL)inkTouchController:(MDCInkTouchController*)inkTouchController |
+ shouldProcessInkTouchesAtTouchLocation:(CGPoint)location { |
+ NSIndexPath* indexPath = |
+ [self.collectionView indexPathForItemAtPoint:location]; |
+ TabHistoryCell* cell = base::mac::ObjCCastStrict<TabHistoryCell>( |
+ [self.collectionView cellForItemAtIndexPath:indexPath]); |
+ if (!cell) { |
+ return NO; |
+ } |
+ |
+ // Increase the size of the ink view to cover the collection view from edge |
+ // to edge. |
+ CGRect inkViewFrame = [cell frame]; |
+ inkViewFrame.origin.x = 0; |
+ inkViewFrame.size.width = CGRectGetWidth([self.collectionView bounds]); |
+ [[inkTouchController defaultInkView] setFrame:inkViewFrame]; |
+ return YES; |
+} |
+ |
+@end |