| Index: ios/chrome/browser/ui/history/history_collection_view_controller.mm
|
| diff --git a/ios/chrome/browser/ui/history/history_collection_view_controller.mm b/ios/chrome/browser/ui/history/history_collection_view_controller.mm
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..c2c010360832627a6b280f0861faedca1db39484
|
| --- /dev/null
|
| +++ b/ios/chrome/browser/ui/history/history_collection_view_controller.mm
|
| @@ -0,0 +1,791 @@
|
| +// Copyright 2016 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.
|
| +
|
| +#include "ios/chrome/browser/ui/history/history_collection_view_controller.h"
|
| +
|
| +#import <MobileCoreServices/MobileCoreServices.h>
|
| +
|
| +#include <memory>
|
| +
|
| +#import "base/ios/weak_nsobject.h"
|
| +#include "base/mac/foundation_util.h"
|
| +#import "base/mac/objc_property_releaser.h"
|
| +#include "base/mac/scoped_nsobject.h"
|
| +#include "base/strings/sys_string_conversions.h"
|
| +#include "base/strings/utf_string_conversions.h"
|
| +#include "components/browsing_data/core/history_notice_utils.h"
|
| +#include "components/strings/grit/components_strings.h"
|
| +#include "components/url_formatter/url_formatter.h"
|
| +#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
|
| +#include "ios/chrome/browser/chrome_url_constants.h"
|
| +#import "ios/chrome/browser/signin/authentication_service.h"
|
| +#include "ios/chrome/browser/signin/authentication_service_factory.h"
|
| +#import "ios/chrome/browser/ui/collection_view/cells/MDCCollectionViewCell+Chrome.h"
|
| +#import "ios/chrome/browser/ui/collection_view/cells/activity_indicator_cell.h"
|
| +#import "ios/chrome/browser/ui/collection_view/cells/collection_view_item.h"
|
| +#import "ios/chrome/browser/ui/collection_view/cells/collection_view_text_item.h"
|
| +#import "ios/chrome/browser/ui/collection_view/collection_view_model.h"
|
| +#import "ios/chrome/browser/ui/context_menu/context_menu_coordinator.h"
|
| +#include "ios/chrome/browser/ui/history/history_entries_status_item.h"
|
| +#include "ios/chrome/browser/ui/history/history_entry.h"
|
| +#include "ios/chrome/browser/ui/history/history_entry_inserter.h"
|
| +#import "ios/chrome/browser/ui/history/history_entry_item.h"
|
| +#include "ios/chrome/browser/ui/history/history_service_facade.h"
|
| +#include "ios/chrome/browser/ui/history/history_service_facade_delegate.h"
|
| +#include "ios/chrome/browser/ui/history/history_util.h"
|
| +#import "ios/chrome/browser/ui/url_loader.h"
|
| +#include "ios/chrome/grit/ios_strings.h"
|
| +#import "ios/third_party/material_components_ios/src/components/Collections/src/MaterialCollections.h"
|
| +#import "ios/third_party/material_components_ios/src/components/Palettes/src/MaterialPalettes.h"
|
| +#import "ios/web/public/referrer.h"
|
| +#import "ios/web/public/web_state/context_menu_params.h"
|
| +#import "net/base/mac/url_conversions.h"
|
| +#include "ui/base/l10n/l10n_util.h"
|
| +#include "ui/base/l10n/l10n_util_mac.h"
|
| +
|
| +namespace {
|
| +// Section identifier for the header (sync information) section.
|
| +const NSInteger kEntriesStatusSectionIdentifier = kSectionIdentifierEnumZero;
|
| +// Maximum number of entries to retrieve in a single query to history service.
|
| +const int kMaxFetchCount = 100;
|
| +// Horizontal inset for item separators.
|
| +const CGFloat kSeparatorInset = 10;
|
| +}
|
| +
|
| +@interface HistoryCollectionViewController ()<HistoryEntriesStatusItemDelegate,
|
| + HistoryEntryInserterDelegate,
|
| + HistoryEntryItemDelegate,
|
| + HistoryServiceFacadeDelegate> {
|
| + base::mac::ObjCPropertyReleaser
|
| + _propertyReleaser_HistoryCollectionViewController;
|
| + // Facade for communicating with HistoryService and WebHistoryService.
|
| + std::unique_ptr<HistoryServiceFacade> _historyServiceFacade;
|
| + // The main browser state. Not owned by HistoryCollectionViewController.
|
| + ios::ChromeBrowserState* _browserState;
|
| + // Backing ivar for delegate property.
|
| + base::WeakNSProtocol<id<HistoryCollectionViewControllerDelegate>> _delegate;
|
| + // Backing ivar for URLLoader property.
|
| + base::WeakNSProtocol<id<UrlLoader>> _URLLoader;
|
| +}
|
| +
|
| +// Object to manage insertion of history entries into the collection view model.
|
| +@property(nonatomic, retain) HistoryEntryInserter* entryInserter;
|
| +// Delegate for the history collection view.
|
| +@property(nonatomic, assign, readonly)
|
| + id<HistoryCollectionViewControllerDelegate>
|
| + delegate;
|
| +// UrlLoader for navigating to history entries.
|
| +@property(nonatomic, assign, readonly) id<UrlLoader> URLLoader;
|
| +// The current query for visible history entries.
|
| +@property(nonatomic, copy) NSString* currentQuery;
|
| +// Coordinator for displaying context menus for history entries.
|
| +@property(nonatomic, assign) ContextMenuCoordinator* contextMenuCoordinator;
|
| +// Type of displayed history entries. Entries can be synced or local, or there
|
| +// may be no history entries.
|
| +@property(nonatomic, assign) HistoryEntriesStatus entriesType;
|
| +// YES if the history panel should show a notice about additional forms of
|
| +// browsing history.
|
| +@property(nonatomic, assign)
|
| + BOOL shouldShowNoticeAboutOtherFormsOfBrowsingHistory;
|
| +// YES if there is an outstanding history query.
|
| +@property(nonatomic, assign, getter=isLoading) BOOL loading;
|
| +// YES if there are no more history entries to load.
|
| +@property(nonatomic, assign, getter=hasFinishedLoading) BOOL finishedLoading;
|
| +// YES if the collection should be filtered by the next received query result.
|
| +@property(nonatomic, assign) BOOL filterForNextQueryResult;
|
| +
|
| +// Fetches history prior to |time| for search text |query|. If |query| is nil or
|
| +// the empty string, all history is fetched.
|
| +- (void)fetchHistoryForQuery:(NSString*)query
|
| + priorToTime:(const base::Time&)time;
|
| +// Updates header section to provide relevant information about the currently
|
| +// displayed history entries.
|
| +- (void)updateEntriesStatusMessage;
|
| +// Removes selected items from the visible collection, but does not delete them
|
| +// from browser history.
|
| +- (void)removeSelectedItemsFromCollection;
|
| +// Removes all items in the collection that are not included in entries.
|
| +- (void)filterForHistoryEntries:(NSArray*)entries;
|
| +// Displays context menu on cell pressed with gestureRecognizer.
|
| +- (void)displayContextMenuInvokedByGestureRecognizer:
|
| + (UILongPressGestureRecognizer*)gestureRecognizer;
|
| +// Opens URL in the current tab and dismisses the history view.
|
| +- (void)openURL:(const GURL&)URL;
|
| +// Opens URL in a new non-incognito tab and dismisses the history view.
|
| +- (void)openURLInNewTab:(const GURL&)URL;
|
| +// Opens URL in a new incognito tab and dismisses the history view.
|
| +- (void)openURLInNewIncognitoTab:(const GURL&)URL;
|
| +// Copies URL to the clipboard.
|
| +- (void)copyURL:(const GURL&)URL;
|
| +@end
|
| +
|
| +@implementation HistoryCollectionViewController
|
| +
|
| +@synthesize searching = _searching;
|
| +@synthesize entryInserter = _entryInserter;
|
| +@synthesize currentQuery = _currentQuery;
|
| +@synthesize contextMenuCoordinator = _contextMenuCoordinator;
|
| +@synthesize entriesType = _entriesType;
|
| +@synthesize shouldShowNoticeAboutOtherFormsOfBrowsingHistory =
|
| + _shouldShowNoticeAboutOtherFormsOfBrowsingHistory;
|
| +@synthesize loading = _loading;
|
| +@synthesize finishedLoading = _finishedLoading;
|
| +@synthesize filterForNextQueryResult = _filterForNextQueryResult;
|
| +
|
| +- (instancetype)initWithLoader:(id<UrlLoader>)loader
|
| + browserState:(ios::ChromeBrowserState*)browserState
|
| + delegate:(id<HistoryCollectionViewControllerDelegate>)
|
| + delegate {
|
| + self = [super initWithStyle:CollectionViewControllerStyleDefault];
|
| + if (self) {
|
| + _propertyReleaser_HistoryCollectionViewController.Init(
|
| + self, [HistoryCollectionViewController class]);
|
| + _historyServiceFacade.reset(new HistoryServiceFacade(browserState, self));
|
| + _browserState = browserState;
|
| + _delegate.reset(delegate);
|
| + _URLLoader.reset(loader);
|
| + [self loadModel];
|
| + // Add initial info section as header.
|
| + [self.collectionViewModel
|
| + addSectionWithIdentifier:kEntriesStatusSectionIdentifier];
|
| + _entryInserter =
|
| + [[HistoryEntryInserter alloc] initWithModel:self.collectionViewModel];
|
| + _entryInserter.delegate = self;
|
| + _entriesType = NO_ENTRIES;
|
| + [self showHistoryMatchingQuery:nil];
|
| + }
|
| + return self;
|
| +}
|
| +
|
| +- (void)viewDidLoad {
|
| + [super viewDidLoad];
|
| + self.styler.cellLayoutType = MDCCollectionViewCellLayoutTypeList;
|
| + self.styler.separatorInset =
|
| + UIEdgeInsetsMake(0, kSeparatorInset, 0, kSeparatorInset);
|
| + self.styler.allowsItemInlay = NO;
|
| +
|
| + self.clearsSelectionOnViewWillAppear = NO;
|
| + self.collectionView.keyboardDismissMode =
|
| + UIScrollViewKeyboardDismissModeOnDrag;
|
| +
|
| + base::scoped_nsobject<UILongPressGestureRecognizer> longPressRecognizer([
|
| + [UILongPressGestureRecognizer alloc]
|
| + initWithTarget:self
|
| + action:@selector(displayContextMenuInvokedByGestureRecognizer:)]);
|
| + [self.collectionView addGestureRecognizer:longPressRecognizer];
|
| +}
|
| +
|
| +- (BOOL)isEditing {
|
| + return self.editor.isEditing;
|
| +}
|
| +
|
| +- (void)setEditing:(BOOL)editing {
|
| + [self.editor setEditing:editing animated:YES];
|
| +}
|
| +
|
| +- (void)setSearching:(BOOL)searching {
|
| + _searching = searching;
|
| + [self updateEntriesStatusMessage];
|
| +}
|
| +
|
| +- (BOOL)hasHistoryEntries {
|
| + return self.entriesType != NO_ENTRIES;
|
| +}
|
| +
|
| +- (BOOL)hasSelectedEntries {
|
| + return self.collectionView.indexPathsForSelectedItems.count;
|
| +}
|
| +
|
| +- (void)showHistoryMatchingQuery:(NSString*)query {
|
| + self.finishedLoading = NO;
|
| + self.currentQuery = query;
|
| + [self fetchHistoryForQuery:query priorToTime:base::Time::Now()];
|
| +}
|
| +
|
| +- (void)deleteSelectedItemsFromHistory {
|
| + NSArray* deletedIndexPaths = self.collectionView.indexPathsForSelectedItems;
|
| + std::vector<HistoryServiceFacade::RemovedEntry> entries;
|
| + for (NSIndexPath* indexPath in deletedIndexPaths) {
|
| + HistoryEntryItem* object = base::mac::ObjCCastStrict<HistoryEntryItem>(
|
| + [self.collectionViewModel itemAtIndexPath:indexPath]);
|
| + entries.push_back(
|
| + HistoryServiceFacade::RemovedEntry(object.URL, object.timestamp));
|
| + }
|
| + _historyServiceFacade->RemoveHistoryEntries(entries);
|
| + [self removeSelectedItemsFromCollection];
|
| +}
|
| +
|
| +- (id<HistoryCollectionViewControllerDelegate>)delegate {
|
| + return _delegate;
|
| +}
|
| +
|
| +- (id<UrlLoader>)URLLoader {
|
| + return _URLLoader;
|
| +}
|
| +
|
| +#pragma mark - MDCollectionViewController
|
| +
|
| +// TODO(crbug.com/653547): Remove this once the MDC adds an option for
|
| +// preventing the infobar from showing.
|
| +- (void)updateFooterInfoBarIfNecessary {
|
| + // No-op. This prevents the default infobar from showing.
|
| +}
|
| +
|
| +#pragma mark - HistoryEntriesStatusItemDelegate
|
| +
|
| +- (void)historyEntriesStatusItem:(HistoryEntriesStatusItem*)item
|
| + didRequestOpenURL:(const GURL&)URL {
|
| + [self openURL:URL];
|
| +}
|
| +
|
| +#pragma mark - HistoryEntryInserterDelegate
|
| +
|
| +- (void)historyEntryInserter:(HistoryEntryInserter*)inserter
|
| + didInsertItemAtIndexPath:(NSIndexPath*)indexPath {
|
| + [self.collectionView insertItemsAtIndexPaths:@[ indexPath ]];
|
| +}
|
| +
|
| +- (void)historyEntryInserter:(HistoryEntryInserter*)inserter
|
| + didInsertSectionAtIndex:(NSInteger)sectionIndex {
|
| + [self.collectionView
|
| + insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]];
|
| +}
|
| +
|
| +- (void)historyEntryInserter:(HistoryEntryInserter*)inserter
|
| + didRemoveSectionAtIndex:(NSInteger)sectionIndex {
|
| + [self.collectionView
|
| + deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]];
|
| +}
|
| +
|
| +#pragma mark - HistoryEntryItemDelegate
|
| +
|
| +- (void)historyEntryItemDidRequestOpen:(HistoryEntryItem*)item {
|
| + [self openURL:item.URL];
|
| +}
|
| +
|
| +- (void)historyEntryItemDidRequestDelete:(HistoryEntryItem*)item {
|
| + NSInteger sectionIdentifier =
|
| + [self.entryInserter sectionIdentifierForTimestamp:item.timestamp];
|
| + if ([self.collectionViewModel
|
| + hasSectionForSectionIdentifier:sectionIdentifier] &&
|
| + [self.collectionViewModel hasItem:item
|
| + inSectionWithIdentifier:sectionIdentifier]) {
|
| + NSIndexPath* indexPath =
|
| + [self.collectionViewModel indexPathForItem:item
|
| + inSectionWithIdentifier:sectionIdentifier];
|
| + [self.collectionView
|
| + selectItemAtIndexPath:indexPath
|
| + animated:NO
|
| + scrollPosition:UICollectionViewScrollPositionNone];
|
| + [self deleteSelectedItemsFromHistory];
|
| + }
|
| +}
|
| +
|
| +- (void)historyEntryItemDidRequestCopy:(HistoryEntryItem*)item {
|
| + [self copyURL:item.URL];
|
| +}
|
| +
|
| +- (void)historyEntryItemDidRequestOpenInNewTab:(HistoryEntryItem*)item {
|
| + [self openURLInNewTab:item.URL];
|
| +}
|
| +
|
| +- (void)historyEntryItemDidRequestOpenInNewIncognitoTab:
|
| + (HistoryEntryItem*)item {
|
| + [self openURLInNewIncognitoTab:item.URL];
|
| +}
|
| +
|
| +- (void)historyEntryItemShouldUpdateView:(HistoryEntryItem*)item {
|
| + NSInteger sectionIdentifier =
|
| + [self.entryInserter sectionIdentifierForTimestamp:item.timestamp];
|
| + // If the item is still in the model, reconfigure it.
|
| + if ([self.collectionViewModel
|
| + hasSectionForSectionIdentifier:sectionIdentifier] &&
|
| + [self.collectionViewModel hasItem:item
|
| + inSectionWithIdentifier:sectionIdentifier]) {
|
| + [self
|
| + reconfigureCellsForItems:@[ item ]
|
| + inSectionWithIdentifier:
|
| + [self.entryInserter sectionIdentifierForTimestamp:item.timestamp]];
|
| + }
|
| +}
|
| +
|
| +#pragma mark - HistoryServiceFacadeDelegate
|
| +
|
| +- (void)historyServiceFacade:(HistoryServiceFacade*)facade
|
| + didReceiveQueryResult:(HistoryServiceFacade::QueryResult)result {
|
| + self.loading = NO;
|
| + // Remove loading indicator.
|
| + CollectionViewItem* headerItem = [self.collectionViewModel
|
| + itemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
|
| + if ([headerItem.cellClass isSubclassOfClass:[ActivityIndicatorCell class]]) {
|
| + [self.collectionViewModel removeItemWithType:kItemTypeEnumZero
|
| + fromSectionWithIdentifier:kSectionIdentifierEnumZero];
|
| + [self.collectionView
|
| + deleteItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:0
|
| + inSection:0] ]];
|
| + }
|
| +
|
| + // If there are no results and no URLs have been loaded, report that no
|
| + // history entries were found.
|
| + if (result.entries.empty() && !self.hasHistoryEntries) {
|
| + DCHECK(self.entriesType == NO_ENTRIES);
|
| + [self updateEntriesStatusMessage];
|
| + [self.delegate historyCollectionViewControllerDidChangeEntries:self];
|
| + return;
|
| + }
|
| +
|
| + self.finishedLoading = result.has_synced_results
|
| + ? result.finished && result.sync_finished
|
| + : result.finished;
|
| + self.entriesType = result.has_synced_results ? SYNCED_ENTRIES : LOCAL_ENTRIES;
|
| + std::vector<history::HistoryEntry> entries = result.entries;
|
| +
|
| + // Header section should be updated outside of batch updates, otherwise
|
| + // loading indicator removal will not be observed.
|
| + [self updateEntriesStatusMessage];
|
| +
|
| + __block base::scoped_nsobject<NSMutableArray> searchResults(
|
| + [[NSMutableArray array] retain]);
|
| + __block base::scoped_nsobject<NSString> searchQuery(
|
| + [base::SysUTF16ToNSString(result.query) copy]);
|
| + [self.collectionView performBatchUpdates:^{
|
| + // There should always be at least a header section present.
|
| + DCHECK([[self collectionViewModel] numberOfSections]);
|
| + for (const history::HistoryEntry& entry : entries) {
|
| + HistoryEntryItem* item =
|
| + [[[HistoryEntryItem alloc] initWithType:kItemTypeEnumZero
|
| + historyEntry:entry
|
| + browserState:_browserState
|
| + delegate:self] autorelease];
|
| + [self.entryInserter insertHistoryEntryItem:item];
|
| + if ([self isSearching]) {
|
| + [searchResults addObject:item];
|
| + }
|
| + }
|
| + [self.delegate historyCollectionViewControllerDidChangeEntries:self];
|
| + }
|
| + completion:^(BOOL) {
|
| + if (([self isSearching] && [searchQuery length] > 0 &&
|
| + [self.currentQuery isEqualToString:searchQuery]) ||
|
| + self.filterForNextQueryResult) {
|
| + // If in search mode, filter out entries that are not
|
| + // part of the search result.
|
| + [self filterForHistoryEntries:searchResults];
|
| + self.filterForNextQueryResult = NO;
|
| + }
|
| + }];
|
| +}
|
| +
|
| +- (void)historyServiceFacade:(HistoryServiceFacade*)facade
|
| + shouldShowNoticeAboutOtherFormsOfBrowsingHistory:(BOOL)shouldShowNotice {
|
| + self.shouldShowNoticeAboutOtherFormsOfBrowsingHistory = shouldShowNotice;
|
| +}
|
| +
|
| +- (void)historyServiceFacadeDidObserveHistoryDeletion:
|
| + (HistoryServiceFacade*)facade {
|
| + // If history has been deleted, reload history filtering for the current
|
| + // results. This only observes local changes to history, i.e. removing
|
| + // history via the clear browsing data page.
|
| + self.filterForNextQueryResult = YES;
|
| + [self showHistoryMatchingQuery:nil];
|
| +}
|
| +
|
| +#pragma mark - MDCCollectionViewEditingDelegate
|
| +
|
| +- (BOOL)collectionViewAllowsEditing:(UICollectionView*)collectionView {
|
| + return YES;
|
| +}
|
| +
|
| +- (BOOL)collectionView:(UICollectionView*)collectionView
|
| + canEditItemAtIndexPath:(NSIndexPath*)indexPath {
|
| + // All items except those in the header section may be edited.
|
| + return indexPath.section;
|
| +}
|
| +
|
| +- (BOOL)collectionView:(UICollectionView*)collectionView
|
| + canSelectItemDuringEditingAtIndexPath:(NSIndexPath*)indexPath {
|
| + // All items except those in the header section may be edited.
|
| + return indexPath.section;
|
| +}
|
| +
|
| +#pragma mark - MDCCollectionViewStylingDelegate
|
| +
|
| +- (BOOL)collectionView:(UICollectionView*)collectionView
|
| + shouldHideItemBackgroundAtIndexPath:(NSIndexPath*)indexPath {
|
| + // Display the entries status section (always the first section) without any
|
| + // background image or shadowing.
|
| + return !indexPath.section;
|
| +}
|
| +
|
| +- (BOOL)collectionView:(UICollectionView*)collectionView
|
| + hidesInkViewAtIndexPath:(NSIndexPath*)indexPath {
|
| + return [indexPath isEqual:[NSIndexPath indexPathForItem:0 inSection:0]];
|
| +}
|
| +
|
| +- (CGFloat)collectionView:(UICollectionView*)collectionView
|
| + cellHeightAtIndexPath:(NSIndexPath*)indexPath {
|
| + if (indexPath.section) {
|
| + return MDCCellDefaultTwoLineHeight;
|
| + } else {
|
| + DCHECK([indexPath isEqual:[NSIndexPath indexPathForItem:0 inSection:0]]);
|
| + // Configure size for loading indicator and entries status cells.
|
| + CollectionViewItem* item =
|
| + [self.collectionViewModel itemAtIndexPath:indexPath];
|
| + if ([item isKindOfClass:[CollectionViewTextItem class]]) {
|
| + return MDCCellDefaultOneLineHeight;
|
| + }
|
| + CGFloat height = [[item cellClass]
|
| + cr_preferredHeightForWidth:CGRectGetWidth(collectionView.bounds)
|
| + forItem:item];
|
| + return height;
|
| + }
|
| +}
|
| +
|
| +- (MDCCollectionViewCellStyle)collectionView:(UICollectionView*)collectionView
|
| + cellStyleForSection:(NSInteger)section {
|
| + return section ? MDCCollectionViewCellStyleCard
|
| + : MDCCollectionViewCellStyleDefault;
|
| +}
|
| +
|
| +#pragma mark - UICollectionViewDelegate
|
| +
|
| +- (BOOL)collectionView:(UICollectionView*)collectionView
|
| + shouldSelectItemAtIndexPath:(NSIndexPath*)indexPath {
|
| + // The first section is not selectable.
|
| + return indexPath.section && [super collectionView:collectionView
|
| + shouldSelectItemAtIndexPath:indexPath];
|
| +}
|
| +
|
| +- (void)collectionView:(UICollectionView*)collectionView
|
| + didSelectItemAtIndexPath:(NSIndexPath*)indexPath {
|
| + [super collectionView:collectionView didSelectItemAtIndexPath:indexPath];
|
| +
|
| + if (self.isEditing) {
|
| + [self.delegate historyCollectionViewControllerDidChangeEntrySelection:self];
|
| + } else {
|
| + HistoryEntryItem* item = base::mac::ObjCCastStrict<HistoryEntryItem>(
|
| + [self.collectionViewModel itemAtIndexPath:indexPath]);
|
| + [self openURL:item.URL];
|
| + }
|
| +}
|
| +
|
| +- (void)collectionView:(UICollectionView*)collectionView
|
| + didDeselectItemAtIndexPath:(NSIndexPath*)indexPath {
|
| + [super collectionView:collectionView didDeselectItemAtIndexPath:indexPath];
|
| + [self.delegate historyCollectionViewControllerDidChangeEntrySelection:self];
|
| +}
|
| +
|
| +- (void)collectionView:(UICollectionView*)collectionView
|
| + didEndDisplayingCell:(UICollectionViewCell*)cell
|
| + forItemAtIndexPath:(NSIndexPath*)indexPath {
|
| + if ([cell isKindOfClass:[ActivityIndicatorCell class]]) {
|
| + [[base::mac::ObjCCast<ActivityIndicatorCell>(cell) activityIndicator]
|
| + stopAnimating];
|
| + }
|
| +}
|
| +#pragma mark - UIScrollViewDelegate
|
| +
|
| +- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
|
| + [super scrollViewDidScroll:scrollView];
|
| + // Adjust header shadow.
|
| + [self.delegate historyCollectionViewController:self
|
| + didScrollToOffset:scrollView.contentOffset];
|
| +
|
| + if (self.hasFinishedLoading)
|
| + return;
|
| +
|
| + CGFloat insetHeight =
|
| + scrollView.contentInset.top + scrollView.contentInset.bottom;
|
| + CGFloat contentViewHeight = scrollView.bounds.size.height - insetHeight;
|
| + CGFloat contentHeight = scrollView.contentSize.height;
|
| + CGFloat contentOffset = scrollView.contentOffset.y;
|
| + CGFloat buffer = contentViewHeight;
|
| + // If the scroll view is approaching the end of loaded history, try to fetch
|
| + // more history. Do so when the content offset is greater than the content
|
| + // height minus the view height, minus a buffer to start the fetch early.
|
| + if (contentOffset > (contentHeight - contentViewHeight) - buffer &&
|
| + !self.isLoading) {
|
| + // If at end, try to grab more history.
|
| + NSInteger lastSection = [self.collectionViewModel numberOfSections] - 1;
|
| + NSInteger lastItemIndex =
|
| + [self.collectionViewModel numberOfItemsInSection:lastSection] - 1;
|
| + if (lastSection == 0 || lastItemIndex < 0) {
|
| + return;
|
| + }
|
| + NSIndexPath* indexPath =
|
| + [NSIndexPath indexPathForItem:lastItemIndex inSection:lastSection];
|
| + HistoryEntryItem* lastItem = base::mac::ObjCCastStrict<HistoryEntryItem>(
|
| + [self.collectionViewModel itemAtIndexPath:indexPath]);
|
| + [self fetchHistoryForQuery:_currentQuery priorToTime:lastItem.timestamp];
|
| + }
|
| +}
|
| +
|
| +#pragma mark - Private methods
|
| +
|
| +- (void)fetchHistoryForQuery:(NSString*)query
|
| + priorToTime:(const base::Time&)time {
|
| + self.loading = YES;
|
| + // Add loading indicator if nothing else is shown.
|
| + if (!self.hasHistoryEntries && !self.isSearching) {
|
| + [self.collectionView performBatchUpdates:^{
|
| + NSIndexPath* indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
|
| + if ([self.collectionViewModel hasItemAtIndexPath:indexPath]) {
|
| + [self.collectionViewModel
|
| + removeItemWithType:kItemTypeEnumZero
|
| + fromSectionWithIdentifier:kSectionIdentifierEnumZero];
|
| + [self.collectionView deleteItemsAtIndexPaths:@[ indexPath ]];
|
| + }
|
| + CollectionViewItem* loadingIndicatorItem = [[[CollectionViewItem alloc]
|
| + initWithType:kItemTypeEnumZero] autorelease];
|
| + loadingIndicatorItem.cellClass = [ActivityIndicatorCell class];
|
| + [self.collectionViewModel addItem:loadingIndicatorItem
|
| + toSectionWithIdentifier:kEntriesStatusSectionIdentifier];
|
| + [self.collectionView insertItemsAtIndexPaths:@[ indexPath ]];
|
| + }
|
| + completion:nil];
|
| + }
|
| +
|
| + BOOL fetchAllHistory = !query || [query isEqualToString:@""];
|
| + base::string16 queryString =
|
| + fetchAllHistory ? base::string16() : base::SysNSStringToUTF16(query);
|
| + history::QueryOptions options;
|
| + options.end_time = time;
|
| + options.duplicate_policy =
|
| + fetchAllHistory ? history::QueryOptions::REMOVE_DUPLICATES_PER_DAY
|
| + : history::QueryOptions::REMOVE_ALL_DUPLICATES;
|
| + options.max_count = kMaxFetchCount;
|
| + options.matching_algorithm =
|
| + query_parser::MatchingAlgorithm::ALWAYS_PREFIX_SEARCH;
|
| + _historyServiceFacade->QueryOtherFormsOfBrowsingHistory();
|
| + _historyServiceFacade->QueryHistory(queryString, options);
|
| + // Also determine whether notice regarding other forms of browsing history
|
| + // should be shown.
|
| + _historyServiceFacade->QueryOtherFormsOfBrowsingHistory();
|
| +}
|
| +
|
| +- (void)updateEntriesStatusMessage {
|
| + CollectionViewItem* entriesStatusItem = nil;
|
| + if (!self.hasHistoryEntries) {
|
| + CollectionViewTextItem* noResultsItem = [[[CollectionViewTextItem alloc]
|
| + initWithType:kItemTypeEnumZero] autorelease];
|
| + noResultsItem.text =
|
| + self.isSearching ? l10n_util::GetNSString(IDS_HISTORY_NO_SEARCH_RESULTS)
|
| + : l10n_util::GetNSString(IDS_HISTORY_NO_RESULTS);
|
| + entriesStatusItem = noResultsItem;
|
| + } else {
|
| + HistoryEntriesStatusItem* historyEntriesStatusItem =
|
| + [[[HistoryEntriesStatusItem alloc] initWithType:kItemTypeEnumZero]
|
| + autorelease];
|
| + historyEntriesStatusItem.delegate = self;
|
| + AuthenticationService* authService =
|
| + AuthenticationServiceFactory::GetForBrowserState(_browserState);
|
| + BOOL signedIn = authService->IsAuthenticated();
|
| +
|
| + historyEntriesStatusItem.hidden =
|
| + self.isSearching || (!signedIn && self.hasHistoryEntries);
|
| + historyEntriesStatusItem.entriesStatus = self.entriesType;
|
| + historyEntriesStatusItem.showsOtherBrowsingDataNotice =
|
| + _shouldShowNoticeAboutOtherFormsOfBrowsingHistory;
|
| + entriesStatusItem = historyEntriesStatusItem;
|
| + }
|
| + // Replace the item in the first section, which is always present.
|
| + NSArray* items = [self.collectionViewModel
|
| + itemsInSectionWithIdentifier:kEntriesStatusSectionIdentifier];
|
| + if ([items count]) {
|
| + // There should only ever be one item in this section.
|
| + DCHECK([items count] == 1);
|
| + // Only update if the item has changed.
|
| + if ([items[0] isEqual:entriesStatusItem]) {
|
| + return;
|
| + }
|
| + }
|
| + [self.collectionView performBatchUpdates:^{
|
| + NSIndexPath* indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
|
| + if ([items count]) {
|
| + [self.collectionViewModel
|
| + removeItemWithType:kItemTypeEnumZero
|
| + fromSectionWithIdentifier:kEntriesStatusSectionIdentifier];
|
| + [self.collectionView deleteItemsAtIndexPaths:@[ indexPath ]];
|
| + }
|
| + [self.collectionViewModel addItem:entriesStatusItem
|
| + toSectionWithIdentifier:kEntriesStatusSectionIdentifier];
|
| + [self.collectionView insertItemsAtIndexPaths:@[ indexPath ]];
|
| + }
|
| + completion:nil];
|
| +}
|
| +
|
| +- (void)removeSelectedItemsFromCollection {
|
| + NSArray* deletedIndexPaths = self.collectionView.indexPathsForSelectedItems;
|
| + [self.collectionView performBatchUpdates:^{
|
| + [self collectionView:self.collectionView
|
| + willDeleteItemsAtIndexPaths:deletedIndexPaths];
|
| + [self.collectionView deleteItemsAtIndexPaths:deletedIndexPaths];
|
| +
|
| + // Remove any empty sections, except the header section.
|
| + for (int section = self.collectionView.numberOfSections - 1; section > 0;
|
| + --section) {
|
| + if (![self.collectionViewModel numberOfItemsInSection:section]) {
|
| + [self.entryInserter removeSection:section];
|
| + }
|
| + }
|
| + }
|
| + completion:^(BOOL) {
|
| + // If only the header section remains, there are no history entries.
|
| + if ([self.collectionViewModel numberOfSections] == 1) {
|
| + self.entriesType = NO_ENTRIES;
|
| + }
|
| + [self updateEntriesStatusMessage];
|
| + }];
|
| +}
|
| +
|
| +- (void)filterForHistoryEntries:(NSArray*)entries {
|
| + self.collectionView.allowsMultipleSelection = YES;
|
| + for (int section = 1; section < [self.collectionViewModel numberOfSections];
|
| + ++section) {
|
| + NSInteger sectionIdentifier =
|
| + [self.collectionViewModel sectionIdentifierForSection:section];
|
| + if ([self.collectionViewModel
|
| + hasSectionForSectionIdentifier:sectionIdentifier]) {
|
| + NSArray* items = [self.collectionViewModel
|
| + itemsInSectionWithIdentifier:sectionIdentifier];
|
| + for (id item in items) {
|
| + HistoryEntryItem* historyItem =
|
| + base::mac::ObjCCastStrict<HistoryEntryItem>(item);
|
| + if (![entries containsObject:historyItem]) {
|
| + NSIndexPath* indexPath =
|
| + [self.collectionViewModel indexPathForItem:historyItem
|
| + inSectionWithIdentifier:sectionIdentifier];
|
| + [self.collectionView
|
| + selectItemAtIndexPath:indexPath
|
| + animated:NO
|
| + scrollPosition:UICollectionViewScrollPositionNone];
|
| + }
|
| + }
|
| + }
|
| + }
|
| + [self removeSelectedItemsFromCollection];
|
| +}
|
| +
|
| +#pragma mark Context Menu
|
| +
|
| +- (void)displayContextMenuInvokedByGestureRecognizer:
|
| + (UILongPressGestureRecognizer*)gestureRecognizer {
|
| + if (gestureRecognizer.numberOfTouches != 1 || self.editing ||
|
| + gestureRecognizer.state != UIGestureRecognizerStateBegan) {
|
| + return;
|
| + }
|
| +
|
| + CGPoint touchLocation =
|
| + [gestureRecognizer locationOfTouch:0 inView:self.collectionView];
|
| + NSIndexPath* touchedItemIndexPath =
|
| + [self.collectionView indexPathForItemAtPoint:touchLocation];
|
| + // If there's no index path, or the index path is for the header item, do not
|
| + // display a contextual menu.
|
| + if (!touchedItemIndexPath ||
|
| + [touchedItemIndexPath
|
| + isEqual:[NSIndexPath indexPathForItem:0 inSection:0]])
|
| + return;
|
| +
|
| + HistoryEntryItem* entry = base::mac::ObjCCastStrict<HistoryEntryItem>(
|
| + [self.collectionViewModel itemAtIndexPath:touchedItemIndexPath]);
|
| +
|
| + base::WeakNSObject<HistoryCollectionViewController> weakSelf(self);
|
| + web::ContextMenuParams params;
|
| + params.location = touchLocation;
|
| + params.view.reset([self.collectionView retain]);
|
| + NSString* menuTitle =
|
| + base::SysUTF16ToNSString(url_formatter::FormatUrl(entry.URL));
|
| + params.menu_title.reset([menuTitle copy]);
|
| +
|
| + // Present sheet/popover using controller that is added to view hierarchy.
|
| + UIViewController* topController = [params.view window].rootViewController;
|
| + while (topController.presentedViewController)
|
| + topController = topController.presentedViewController;
|
| +
|
| + self.contextMenuCoordinator =
|
| + [[ContextMenuCoordinator alloc] initWithBaseViewController:topController
|
| + params:params];
|
| +
|
| + // TODO(crbug.com/606503): Refactor context menu creation code to be shared
|
| + // with BrowserViewController.
|
| + // Add "Open in New Tab" option.
|
| + NSString* openInNewTabTitle =
|
| + l10n_util::GetNSStringWithFixup(IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWTAB);
|
| + ProceduralBlock openInNewTabAction = ^{
|
| + [weakSelf openURLInNewTab:entry.URL];
|
| + };
|
| + [self.contextMenuCoordinator addItemWithTitle:openInNewTabTitle
|
| + action:openInNewTabAction];
|
| +
|
| + // Add "Open in New Incognito Tab" option.
|
| + NSString* openInNewIncognitoTabTitle = l10n_util::GetNSStringWithFixup(
|
| + IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWINCOGNITOTAB);
|
| + ProceduralBlock openInNewIncognitoTabAction = ^{
|
| + [weakSelf openURLInNewIncognitoTab:entry.URL];
|
| + };
|
| + [self.contextMenuCoordinator addItemWithTitle:openInNewIncognitoTabTitle
|
| + action:openInNewIncognitoTabAction];
|
| +
|
| + // Add "Copy URL" option.
|
| + NSString* copyURLTitle =
|
| + l10n_util::GetNSStringWithFixup(IDS_IOS_CONTENT_CONTEXT_COPY);
|
| + ProceduralBlock copyURLAction = ^{
|
| + [weakSelf copyURL:entry.URL];
|
| + };
|
| + [self.contextMenuCoordinator addItemWithTitle:copyURLTitle
|
| + action:copyURLAction];
|
| + [self.parentViewController.view endEditing:YES];
|
| + [self.contextMenuCoordinator start];
|
| +}
|
| +
|
| +- (void)openURL:(const GURL&)URL {
|
| + GURL copiedURL(URL);
|
| + [self.delegate historyCollectionViewController:self
|
| + shouldCloseWithCompletion:^{
|
| + [self.URLLoader
|
| + loadURL:copiedURL
|
| + referrer:web::Referrer()
|
| + transition:ui::PAGE_TRANSITION_AUTO_BOOKMARK
|
| + rendererInitiated:NO];
|
| + }];
|
| +}
|
| +
|
| +- (void)openURLInNewTab:(const GURL&)URL {
|
| + GURL copiedURL(URL);
|
| + [self.delegate historyCollectionViewController:self
|
| + shouldCloseWithCompletion:^{
|
| + [self.URLLoader webPageOrderedOpen:copiedURL
|
| + referrer:web::Referrer()
|
| + windowName:nil
|
| + inIncognito:NO
|
| + inBackground:NO
|
| + appendTo:kLastTab];
|
| + }];
|
| +}
|
| +
|
| +- (void)openURLInNewIncognitoTab:(const GURL&)URL {
|
| + GURL copiedURL(URL);
|
| + [self.delegate historyCollectionViewController:self
|
| + shouldCloseWithCompletion:^{
|
| + [self.URLLoader webPageOrderedOpen:copiedURL
|
| + referrer:web::Referrer()
|
| + windowName:nil
|
| + inIncognito:YES
|
| + inBackground:NO
|
| + appendTo:kLastTab];
|
| + }];
|
| +}
|
| +
|
| +- (void)copyURL:(const GURL&)URL {
|
| + DCHECK(URL.is_valid());
|
| + NSData* plainText = [base::SysUTF8ToNSString(URL.spec())
|
| + dataUsingEncoding:NSUTF8StringEncoding];
|
| + NSDictionary* copiedItem = @{
|
| + (NSString*)kUTTypeURL : net::NSURLWithGURL(URL),
|
| + (NSString*)kUTTypeUTF8PlainText : plainText,
|
| + };
|
| + [[UIPasteboard generalPasteboard] setItems:@[ copiedItem ]];
|
| +}
|
| +
|
| +@end
|
|
|