blob: 954ea175ae541159381e16051280b8637e80e1e0 [file] [log] [blame]
// 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"
#include "base/logging.h"
#include "base/mac/foundation_util.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"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
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> {
MDCInkTouchController* _inkTouchController;
NSArray* _partitionedEntries;
NSArray* _sessionEntries;
}
@end
@implementation TabHistoryViewController
- (NSArray*)sessionEntries {
return _sessionEntries;
}
#pragma mark Public Methods
- (CGFloat)optimalHeight:(CGFloat)suggestedHeight {
DCHECK(suggestedHeight >= kCellHeight);
CGFloat optimalHeight = 0;
for (NSArray* sectionArray in _partitionedEntries) {
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 {
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 =
[[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 = sessionEntries;
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 = partitionedEntries;
}
#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