| 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
|
|
|