Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(85)

Unified Diff: ios/chrome/browser/ui/reading_list/reading_list_view_controller.mm

Issue 2589583003: Upstream Chrome on iOS source code [7/11]. (Closed)
Patch Set: Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: ios/chrome/browser/ui/reading_list/reading_list_view_controller.mm
diff --git a/ios/chrome/browser/ui/reading_list/reading_list_view_controller.mm b/ios/chrome/browser/ui/reading_list/reading_list_view_controller.mm
new file mode 100644
index 0000000000000000000000000000000000000000..c7e64f015999d850520013f2e6df640e22a4e99a
--- /dev/null
+++ b/ios/chrome/browser/ui/reading_list/reading_list_view_controller.mm
@@ -0,0 +1,998 @@
+// 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.
+
+#import "ios/chrome/browser/ui/reading_list/reading_list_view_controller.h"
+
+#include "base/bind.h"
+#include "base/logging.h"
+#import "base/mac/foundation_util.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/metrics/user_metrics.h"
+#include "base/metrics/user_metrics_action.h"
+#include "base/strings/sys_string_conversions.h"
+#include "base/time/time.h"
+#include "components/reading_list/ios/reading_list_entry.h"
+#include "components/reading_list/ios/reading_list_model.h"
+#import "components/reading_list/ios/reading_list_model_bridge_observer.h"
+#include "components/strings/grit/components_strings.h"
+#include "components/url_formatter/url_formatter.h"
+#include "ios/chrome/browser/reading_list/offline_url_utils.h"
+#include "ios/chrome/browser/reading_list/reading_list_download_service.h"
+#include "ios/chrome/browser/reading_list/reading_list_entry_loading_util.h"
+#import "ios/chrome/browser/tabs/tab.h"
+#import "ios/chrome/browser/tabs/tab_model.h"
+#import "ios/chrome/browser/ui/alert_coordinator/action_sheet_coordinator.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/favicon_view.h"
+#import "ios/chrome/browser/ui/material_components/utils.h"
+#import "ios/chrome/browser/ui/reading_list/reading_list_collection_view_item.h"
+#import "ios/chrome/browser/ui/reading_list/reading_list_toolbar.h"
+#import "ios/chrome/browser/ui/uikit_ui_util.h"
+#include "ios/chrome/browser/ui/url_loader.h"
+#include "ios/chrome/grit/ios_strings.h"
+#import "ios/third_party/material_components_ios/src/components/AppBar/src/MaterialAppBar.h"
+#import "ios/third_party/material_components_ios/src/components/Palettes/src/MaterialPalettes.h"
+#import "ios/third_party/material_roboto_font_loader_ios/src/src/MaterialRobotoFontLoader.h"
+#include "ios/web/public/referrer.h"
+#include "ios/web/public/web_state/web_state.h"
+#include "net/base/network_change_notifier.h"
+#include "ui/base/l10n/l10n_util_mac.h"
+#include "ui/base/window_open_disposition.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+namespace {
+
+NSString* const kEmptyReadingListBackgroundIcon = @"reading_list_icon";
+NSString* const kEmptyReadingListShareIcon = @"share_icon";
+NSString* const kShareIconMarker = @"SHARE_ICON";
+
+// Height of the toolbar.
+const int kToolbarHeight = 48;
+
+// Background view constants.
+const CGFloat kTextImageSpacing = 10;
+const CGFloat kTextHorizontalMargin = 32;
+const CGFloat kImageWidth = 60;
+const CGFloat kImageHeight = 44;
+const CGFloat kShareImageHeight = 14;
+const CGFloat kShareImageWidth = 10;
+const CGFloat kFontSize = 16;
+
+typedef NS_ENUM(NSInteger, SectionIdentifier) {
+ SectionIdentifierUnread = kSectionIdentifierEnumZero,
+ SectionIdentifierRead,
+};
+
+typedef NS_ENUM(NSInteger, ItemType) {
+ ItemTypeUnreadHeader = kItemTypeEnumZero,
+ ItemTypeUnread,
+ ItemTypeReadHeader,
+ ItemTypeRead,
+};
+
+// Typedef for a block taking a GURL as parameter and returning nothing.
+typedef void (^EntryUpdater)(const GURL&);
+
+// Type for map used to sort ReadingListEntry by timestamp. Multiple entries can
+// have the same timestamp.
+using ItemsMapByDate = std::multimap<int64_t, ReadingListCollectionViewItem*>;
+}
+
+@interface ReadingListViewController ()<ReadingListModelBridgeObserver> {
+ // Toolbar with the actions.
+ ReadingListToolbar* _toolbar;
+ // Action sheet presenting the subactions of the toolbar.
+ AlertCoordinator* _actionSheet;
+ std::unique_ptr<ReadingListModelBridge> _modelBridge;
+ UIView* _emptyCollectionBackground;
+}
+
+// Lazily instantiated.
+@property(nonatomic, strong, readonly)
+ FaviconAttributesProvider* attributesProvider;
+
+// Returns the UIView to be displayed when the reading list is empty.
+- (UIView*)emptyCollectionBackground;
+// Handles "Done" button touches.
+- (void)donePressed;
+// Loads all the items in all sections.
+- (void)loadItems;
+// Fills section |sectionIdentifier| with the items from |map| in reverse order
+// of the map key.
+- (void)loadItemsFromMap:(const ItemsMapByDate&)map
+ toSection:(SectionIdentifier)sectionIdentifier;
+// Convenience method to create cell items for reading list entries.
+- (ReadingListCollectionViewItem*)cellItemForReadingListEntry:
+ (const ReadingListEntry&)entry;
+// Returns whether there are elements in the section identified by
+// |sectionIdentifier|.
+- (BOOL)hasItemInSection:(SectionIdentifier)sectionIdentifier;
+// Adds the bottom toolbar with the edition options.
+- (void)addToolbar;
+// Updates the toolbar state according to the selected items.
+- (void)updateToolbarState;
+// Displays an action sheet to let the user choose to mark all the elements as
+// read or as unread. Used when nothing is selected.
+- (void)markAllItemsAs;
+// Displays an action sheet to let the user choose to mark all the selected
+// elements as read or as unread. Used if read and unread elements are selected.
+- (void)markMixedItemsAs;
+// Marks all items as read.
+- (void)markAllRead;
+// Marks all items as unread.
+- (void)markAllUnread;
+// Marks the selected items as read.
+- (void)markItemsRead;
+// Marks the selected items as unread.
+- (void)markItemsUnread;
+// Deletes all the read items.
+- (void)deleteAllReadItems;
+// Deletes all the selected items.
+- (void)deleteSelectedItems;
+// Initializes |_actionSheet| with |self| as base view controller, and the
+// toolbar's mark button as anchor point.
+- (void)initializeActionSheet;
+// Exits the editing mode and update the toolbar state with animation.
+- (void)exitEditingModeAnimated:(BOOL)animated;
+// Applies |action| to every cell in the section |identifier|.
+- (void)updateItemsInSectionIdentifier:(SectionIdentifier)identifier
+ usingEntryUpdater:(EntryUpdater)updater;
+// Applies |action| to every selected element of collection view.
+- (void)updateSelectedItemsWithEntryUpdater:(EntryUpdater)updater;
+// Logs the deletions histograms for the entry with |url|.
+- (void)logDeletionHistogramsForEntry:(const GURL&)url;
+// Move all the items from |sourceSectionIdentifier| to
+// |destinationSectionIdentifier| and removes the empty section from the
+// collection.
+- (void)moveItemsFromSection:(SectionIdentifier)sourceSectionIdentifier
+ toSection:(SectionIdentifier)destinationSectionIdentifier;
+// Move the currently selected elements to |sectionIdentifier| and removes the
+// empty sections.
+- (void)moveSelectedItems:(NSArray*)sortedIndexPaths
+ toSection:(SectionIdentifier)sectionIdentifier;
+// Makes sure |sectionIdentifier| exists with the correct header.
+// Returns the index of the new section in the collection view; NSIntegerMax if
+// no section has been created.
+- (NSInteger)initializeSection:(SectionIdentifier)sectionIdentifier;
+// Returns the header for the |sectionIdentifier|.
+- (CollectionViewTextItem*)headerForSection:
+ (SectionIdentifier)sectionIdentifier;
+// Removes the empty sections from the collection and the model.
+- (void)removeEmptySections;
+
+@end
+
+@implementation ReadingListViewController
+@synthesize readingListModel = _readingListModel;
+@synthesize tabModel = _tabModel;
+@synthesize largeIconService = _largeIconService;
+@synthesize readingListDownloadService = _readingListDownloadService;
+@synthesize attributesProvider = _attributesProvider;
+
+#pragma mark lifecycle
+
+- (instancetype)initWithModel:(ReadingListModel*)model
+ tabModel:(TabModel*)tabModel
+ largeIconService:(favicon::LargeIconService*)largeIconService
+ readingListDownloadService:
+ (ReadingListDownloadService*)readingListDownloadService {
+ self = [super initWithStyle:CollectionViewControllerStyleAppBar];
+ if (self) {
+ DCHECK(model);
+
+ // Configure modal presentation.
+ [self setModalPresentationStyle:UIModalPresentationFormSheet];
+ [self setModalTransitionStyle:UIModalTransitionStyleCoverVertical];
+
+ _readingListModel = model;
+ _tabModel = tabModel;
+ _largeIconService = largeIconService;
+ _readingListDownloadService = readingListDownloadService;
+ _emptyCollectionBackground = [self emptyCollectionBackground];
+ _toolbar = [[ReadingListToolbar alloc] initWithFrame:CGRectZero];
+
+ _modelBridge.reset(new ReadingListModelBridge(self, model));
+ }
+ return self;
+}
+
+#pragma mark - properties
+
+- (FaviconAttributesProvider*)attributesProvider {
+ if (_attributesProvider) {
+ return _attributesProvider;
+ }
+
+ _attributesProvider = [[FaviconAttributesProvider alloc]
+ initWithFaviconSize:kFaviconPreferredSize
+ minFaviconSize:kFaviconMinSize
+ largeIconService:self.largeIconService];
+ return _attributesProvider;
+}
+
+- (void)setToolbarState:(ReadingListToolbarState)toolbarState {
+ [_toolbar setState:toolbarState];
+}
+
+#pragma mark - UIViewController
+
+- (void)updateViewConstraints {
+ NSDictionary* views = @{ @"toolbar" : _toolbar };
+ NSDictionary* metrics = @{ @"toolbarHeight" : @(kToolbarHeight) };
+ NSArray* constraints =
+ @[ @"V:[toolbar(==toolbarHeight)]|", @"H:|[toolbar]|" ];
+ ApplyVisualConstraintsWithMetrics(constraints, views, metrics);
+ [super updateViewConstraints];
+}
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ [self addToolbar];
+
+ self.title = l10n_util::GetNSString(IDS_IOS_TOOLS_MENU_READING_LIST);
+
+ // Add "Done" button.
+ UIBarButtonItem* doneItem = [[UIBarButtonItem alloc]
+ initWithTitle:l10n_util::GetNSString(IDS_IOS_READING_LIST_DONE_BUTTON)
+ style:UIBarButtonItemStylePlain
+ target:self
+ action:@selector(donePressed)];
+ doneItem.accessibilityIdentifier = @"Done";
+ self.navigationItem.rightBarButtonItem = doneItem;
+
+ // Customize collection view settings.
+ self.styler.cellStyle = MDCCollectionViewCellStyleCard;
+ self.styler.separatorInset = UIEdgeInsetsMake(0, 16, 0, 16);
+}
+
+#pragma mark - UICollectionViewDelegate
+
+- (UICollectionReusableView*)collectionView:(UICollectionView*)collectionView
+ viewForSupplementaryElementOfKind:(NSString*)kind
+ atIndexPath:(NSIndexPath*)indexPath {
+ UICollectionReusableView* cell = [super collectionView:collectionView
+ viewForSupplementaryElementOfKind:kind
+ atIndexPath:indexPath];
+ MDCCollectionViewTextCell* textCell =
+ base::mac::ObjCCast<MDCCollectionViewTextCell>(cell);
+ if (textCell) {
+ textCell.textLabel.textColor = [[MDCPalette greyPalette] tint500];
+ }
+ return cell;
+};
+
+- (void)collectionView:(UICollectionView*)collectionView
+ didSelectItemAtIndexPath:(NSIndexPath*)indexPath {
+ [super collectionView:collectionView didSelectItemAtIndexPath:indexPath];
+
+ if (self.editor.editing) {
+ [self updateToolbarState];
+ } else {
+ [self openItemAtIndexPath:indexPath];
+ }
+}
+
+- (void)collectionView:(UICollectionView*)collectionView
+ didDeselectItemAtIndexPath:(NSIndexPath*)indexPath {
+ [super collectionView:collectionView didDeselectItemAtIndexPath:indexPath];
+ if (self.editor.editing) {
+ // When deselecting an item, if we are editing, we want to update the
+ // toolbar base on the selected items.
+ [self updateToolbarState];
+ }
+}
+
+#pragma mark - MDCCollectionViewController
+
+- (void)updateFooterInfoBarIfNecessary {
+ // No-op. This prevents the default infobar from showing.
+ // TODO(crbug.com/653547): Remove this once the MDC adds an option for
+ // preventing the infobar from showing.
+}
+
+#pragma mark - MDCCollectionViewStylingDelegate
+
+- (CGFloat)collectionView:(UICollectionView*)collectionView
+ cellHeightAtIndexPath:(NSIndexPath*)indexPath {
+ NSInteger type = [self.collectionViewModel itemTypeForIndexPath:indexPath];
+ if (type == ItemTypeUnread || type == ItemTypeRead)
+ return MDCCellDefaultTwoLineHeight;
+ else
+ return MDCCellDefaultOneLineHeight;
+}
+
+#pragma mark - MDCCollectionViewEditingDelegate
+
+- (BOOL)collectionViewAllowsEditing:(nonnull UICollectionView*)collectionView {
+ return YES;
+}
+
+#pragma mark - ReadingListModelBridgeObserver
+
+- (void)readingListModelLoaded:(const ReadingListModel*)model {
+ _readingListModel->ResetUnseenEntries();
+ [self loadModel];
+ UMA_HISTOGRAM_COUNTS_1000("ReadingList.Unread.Number", model->unread_size());
+ UMA_HISTOGRAM_COUNTS_1000("ReadingList.Read.Number",
+ model->size() - model->unread_size());
+ if ([self isViewLoaded]) {
+ [self.collectionView reloadData];
+ }
+}
+
+- (void)readingListModelDidApplyChanges:(const ReadingListModel*)model {
+ // Ignore model updates when the view controller is being edited or doing
+ // batch updates.
+ if (model->IsPerformingBatchUpdates() || [self.editor isEditing]) {
+ return;
+ }
+
+ [self reloadData];
+}
+
+- (void)readingListModelCompletedBatchUpdates:(const ReadingListModel*)model {
+ // Ignore model updates when the view controller is being edited.
+ if ([self.editor isEditing]) {
+ return;
+ }
+
+ [self reloadData];
+}
+
+#pragma mark - private methods
+
+- (UIView*)emptyCollectionBackground {
+ UIView* emptyCollectionBackground = [[UIView alloc] initWithFrame:CGRectZero];
+
+ NSString* emptyTitle =
+ l10n_util::GetNSString(IDS_IOS_READING_LIST_EMPTY_MESSAGE);
+ NSRange iconRange = [emptyTitle rangeOfString:kShareIconMarker];
+
+ NSTextAttachment* textAttachment = [[NSTextAttachment alloc] init];
+ textAttachment.image = [[UIImage imageNamed:kEmptyReadingListShareIcon]
+ imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+ textAttachment.bounds = CGRectMake(0, 0, kShareImageWidth, kShareImageHeight);
+
+ NSAttributedString* attachmentAttributedString =
+ [NSAttributedString attributedStringWithAttachment:textAttachment];
+
+ NSDictionary* attributes = @{
+ NSFontAttributeName :
+ [[MDFRobotoFontLoader sharedInstance] mediumFontOfSize:kFontSize],
+ NSForegroundColorAttributeName : [[MDCPalette greyPalette] tint700]
+ };
+
+ NSMutableAttributedString* labelAttributedString =
+ [[NSMutableAttributedString alloc] initWithString:emptyTitle
+ attributes:attributes];
+ [labelAttributedString replaceCharactersInRange:iconRange
+ withAttributedString:attachmentAttributedString];
+
+ UILabel* label = [[UILabel alloc] initWithFrame:CGRectZero];
+ label.attributedText = labelAttributedString;
+ label.lineBreakMode = NSLineBreakByWordWrapping;
+ label.numberOfLines = 0;
+ label.textAlignment = NSTextAlignmentCenter;
+ [label setTranslatesAutoresizingMaskIntoConstraints:NO];
+ [emptyCollectionBackground addSubview:label];
+
+ UIImageView* imageView = [[UIImageView alloc] init];
+ imageView.image = [UIImage imageNamed:kEmptyReadingListBackgroundIcon];
+ [imageView setTranslatesAutoresizingMaskIntoConstraints:NO];
+ [emptyCollectionBackground addSubview:imageView];
+
+ [NSLayoutConstraint activateConstraints:@[
+ [[imageView heightAnchor] constraintEqualToConstant:kImageHeight],
+ [[imageView widthAnchor] constraintEqualToConstant:kImageWidth],
+ [[emptyCollectionBackground centerXAnchor]
+ constraintEqualToAnchor:label.centerXAnchor],
+ [[emptyCollectionBackground centerXAnchor]
+ constraintEqualToAnchor:imageView.centerXAnchor],
+ [label.topAnchor constraintEqualToAnchor:imageView.bottomAnchor
+ constant:kTextImageSpacing]
+ ]];
+
+ // Position the top of the image at 40% from the top.
+ NSLayoutConstraint* verticalAlignment =
+ [NSLayoutConstraint constraintWithItem:imageView
+ attribute:NSLayoutAttributeTop
+ relatedBy:NSLayoutRelationEqual
+ toItem:emptyCollectionBackground
+ attribute:NSLayoutAttributeBottom
+ multiplier:0.4
+ constant:0];
+ [emptyCollectionBackground addConstraints:@[ verticalAlignment ]];
+
+ ApplyVisualConstraintsWithMetrics(@[ @"H:|-(margin)-[textLabel]-(margin)-|" ],
+ @{ @"textLabel" : label },
+ @{ @"margin" : @(kTextHorizontalMargin) });
+
+ return emptyCollectionBackground;
+}
+
+- (void)openItemAtIndexPath:(NSIndexPath*)indexPath {
+ ReadingListCollectionViewItem* readingListItem =
+ base::mac::ObjCCastStrict<ReadingListCollectionViewItem>(
+ [self.collectionViewModel itemAtIndexPath:indexPath]);
+ const ReadingListEntry* entry =
+ self.readingListModel->GetEntryByURL(readingListItem.url);
+ if (!entry) {
+ [self reloadData];
+ return;
+ }
+
+ base::RecordAction(base::UserMetricsAction("MobileReadingListOpen"));
+
+ // Reset observer to prevent further model update notifications.
+ _modelBridge.reset();
+
+ Tab* currentTab = _tabModel.currentTab;
+ DCHECK(currentTab);
+ reading_list::LoadReadingListEntry(*entry, self.readingListModel,
+ currentTab.webState);
+ [self dismiss];
+}
+
+- (void)donePressed {
+ [self dismiss];
+}
+
+- (void)dismiss {
+ // Reset observer to prevent further model update notifications.
+ _modelBridge.reset();
+ [_actionSheet stop];
+ [self.presentingViewController dismissViewControllerAnimated:YES
+ completion:nil];
+}
+
+- (void)loadModel {
+ [super loadModel];
+
+ if (self.readingListModel->size() == 0) {
+ // The collection is empty, add background.
+ self.collectionView.backgroundView = _emptyCollectionBackground;
+ [_toolbar setHidden:YES];
+ } else {
+ [self loadItems];
+ self.collectionView.backgroundView = nil;
+ [_toolbar setHidden:NO];
+ }
+}
+
+- (void)loadItemsFromMap:(const ItemsMapByDate&)map
+ toSection:(SectionIdentifier)sectionIdentifier {
+ if (map.size() == 0) {
+ return;
+ }
+ CollectionViewModel* model = self.collectionViewModel;
+ [model addSectionWithIdentifier:sectionIdentifier];
+ [model setHeader:[self headerForSection:sectionIdentifier]
+ forSectionWithIdentifier:sectionIdentifier];
+ // Reverse iterate to add newer entries at the top.
+ ItemsMapByDate::const_reverse_iterator iterator = map.rbegin();
+ for (; iterator != map.rend(); iterator++) {
+ [model addItem:iterator->second toSectionWithIdentifier:sectionIdentifier];
+ }
+}
+
+- (void)loadItems {
+ ItemsMapByDate read_map;
+ ItemsMapByDate unread_map;
+ for (const auto& url : self.readingListModel->Keys()) {
+ const ReadingListEntry* entry = self.readingListModel->GetEntryByURL(url);
+ ReadingListCollectionViewItem* item =
+ [self cellItemForReadingListEntry:*entry];
+ if (entry->IsRead()) {
+ read_map.insert(std::make_pair(entry->UpdateTime(), item));
+ } else {
+ unread_map.insert(std::make_pair(entry->UpdateTime(), item));
+ }
+ }
+ [self loadItemsFromMap:unread_map toSection:SectionIdentifierUnread];
+ [self loadItemsFromMap:read_map toSection:SectionIdentifierRead];
+
+ BOOL hasRead = read_map.size() > 0;
+ [_toolbar setHasReadItem:hasRead];
+}
+
+- (void)reloadData {
+ [self loadModel];
+ if ([self isViewLoaded]) {
+ [self.collectionView reloadData];
+ }
+}
+
+- (ReadingListCollectionViewItem*)cellItemForReadingListEntry:
+ (const ReadingListEntry&)entry {
+ GURL url = entry.URL();
+ ReadingListCollectionViewItem* item = [[ReadingListCollectionViewItem alloc]
+ initWithType:entry.IsRead() ? ItemTypeRead : ItemTypeUnread
+ attributesProvider:self.attributesProvider
+ url:url
+ distillationState:entry.DistilledState()];
+ base::string16 urlString = url_formatter::FormatUrl(url);
+ item.text = base::SysUTF8ToNSString(entry.Title());
+ item.detailText = base::SysUTF16ToNSString(urlString);
+ return item;
+}
+
+- (BOOL)hasItemInSection:(SectionIdentifier)sectionIdentifier {
+ if (![self.collectionViewModel
+ hasSectionForSectionIdentifier:sectionIdentifier]) {
+ // No section.
+ return NO;
+ }
+
+ NSInteger section =
+ [self.collectionViewModel sectionForSectionIdentifier:sectionIdentifier];
+ NSInteger numberOfItems =
+ [self.collectionViewModel numberOfItemsInSection:section];
+
+ return numberOfItems > 0;
+}
+
+#pragma mark - ReadingListToolbarDelegate
+
+- (void)markPressed {
+ switch ([_toolbar state]) {
+ case NoneSelected:
+ [self markAllItemsAs];
+ break;
+ case OnlyUnreadSelected:
+ [self markItemsRead];
+ break;
+ case OnlyReadSelected:
+ [self markItemsUnread];
+ break;
+ case MixedItemsSelected:
+ [self markMixedItemsAs];
+ break;
+ }
+}
+
+- (void)deletePressed {
+ if ([_toolbar state] == NoneSelected) {
+ [self deleteAllReadItems];
+ } else {
+ [self deleteSelectedItems];
+ }
+}
+- (void)enterEditingModePressed {
+ self.toolbarState = NoneSelected;
+ [self.editor setEditing:YES animated:YES];
+ [_toolbar setEditing:YES];
+}
+
+- (void)exitEditingModePressed {
+ [self exitEditingModeAnimated:YES];
+}
+
+#pragma mark - Private methods - Toolbar
+
+- (void)addToolbar {
+ [_toolbar setDelegate:self];
+ [_toolbar setTranslatesAutoresizingMaskIntoConstraints:NO];
+ [self.view addSubview:_toolbar];
+ UIEdgeInsets insets = self.collectionView.contentInset;
+ insets.bottom += kToolbarHeight + 8;
+ self.collectionView.contentInset = insets;
+ [_toolbar setHidden:YES];
+}
+
+- (void)updateToolbarState {
+ BOOL readSelected = NO;
+ BOOL unreadSelected = NO;
+
+ if ([self.collectionView.indexPathsForSelectedItems count] == 0) {
+ // No entry selected.
+ self.toolbarState = NoneSelected;
+ return;
+ }
+
+ // Sections for section identifiers.
+ NSInteger sectionRead = NSNotFound;
+ NSInteger sectionUnread = NSNotFound;
+
+ if ([self hasItemInSection:SectionIdentifierRead]) {
+ sectionRead = [self.collectionViewModel
+ sectionForSectionIdentifier:SectionIdentifierRead];
+ }
+ if ([self hasItemInSection:SectionIdentifierUnread]) {
+ sectionUnread = [self.collectionViewModel
+ sectionForSectionIdentifier:SectionIdentifierUnread];
+ }
+
+ // Check selected sections.
+ for (NSIndexPath* index in self.collectionView.indexPathsForSelectedItems) {
+ if (index.section == sectionRead) {
+ readSelected = YES;
+ } else if (index.section == sectionUnread) {
+ unreadSelected = YES;
+ }
+ }
+
+ // Update toolbar state.
+ if (readSelected) {
+ if (unreadSelected) {
+ // Read and Unread selected.
+ self.toolbarState = MixedItemsSelected;
+ } else {
+ // Read selected.
+ self.toolbarState = OnlyReadSelected;
+ }
+ } else if (unreadSelected) {
+ // Unread selected.
+ self.toolbarState = OnlyUnreadSelected;
+ }
+}
+
+- (void)markAllItemsAs {
+ [self initializeActionSheet];
+ __weak ReadingListViewController* weakSelf = self;
+ [_actionSheet addItemWithTitle:l10n_util::GetNSStringWithFixup(
+ IDS_IOS_READING_LIST_MARK_ALL_READ_ACTION)
+ action:^{
+ [weakSelf markAllRead];
+ }
+ style:UIAlertActionStyleDefault];
+ [_actionSheet
+ addItemWithTitle:l10n_util::GetNSStringWithFixup(
+ IDS_IOS_READING_LIST_MARK_ALL_UNREAD_ACTION)
+ action:^{
+ [weakSelf markAllUnread];
+ }
+ style:UIAlertActionStyleDefault];
+ [_actionSheet start];
+}
+
+- (void)markMixedItemsAs {
+ [self initializeActionSheet];
+ __weak ReadingListViewController* weakSelf = self;
+ [_actionSheet addItemWithTitle:l10n_util::GetNSStringWithFixup(
+ IDS_IOS_READING_LIST_MARK_READ_BUTTON)
+ action:^{
+ [weakSelf markItemsRead];
+ }
+ style:UIAlertActionStyleDefault];
+ [_actionSheet addItemWithTitle:l10n_util::GetNSStringWithFixup(
+ IDS_IOS_READING_LIST_MARK_UNREAD_BUTTON)
+ action:^{
+ [weakSelf markItemsUnread];
+ }
+ style:UIAlertActionStyleDefault];
+ [_actionSheet start];
+}
+
+- (void)initializeActionSheet {
+ _actionSheet = [_toolbar actionSheetForMarkWithBaseViewController:self];
+
+ [_actionSheet addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_CANCEL)
+ action:nil
+ style:UIAlertActionStyleCancel];
+}
+
+- (void)markAllRead {
+ if (![self hasItemInSection:SectionIdentifierUnread]) {
+ [self exitEditingModeAnimated:YES];
+ return;
+ }
+
+ [self updateItemsInSectionIdentifier:SectionIdentifierUnread
+ usingEntryUpdater:^(const GURL& url) {
+ [self readingListModel]->SetReadStatus(url, true);
+ }];
+ [self exitEditingModeAnimated:YES];
+ [self moveItemsFromSection:SectionIdentifierUnread
+ toSection:SectionIdentifierRead];
+}
+
+- (void)markAllUnread {
+ if (![self hasItemInSection:SectionIdentifierRead]) {
+ [self exitEditingModeAnimated:YES];
+ return;
+ }
+
+ [self updateItemsInSectionIdentifier:SectionIdentifierRead
+ usingEntryUpdater:^(const GURL& url) {
+ [self readingListModel]->SetReadStatus(url, false);
+ }];
+ [self exitEditingModeAnimated:YES];
+ [self moveItemsFromSection:SectionIdentifierRead
+ toSection:SectionIdentifierUnread];
+}
+
+- (void)markItemsRead {
+ base::RecordAction(base::UserMetricsAction("MobileReadingListMarkRead"));
+ [self updateSelectedItemsWithEntryUpdater:^(const GURL& url) {
+ [self readingListModel]->SetReadStatus(url, true);
+ }];
+
+ NSArray* sortedIndexPaths = [self.collectionView.indexPathsForSelectedItems
+ sortedArrayUsingSelector:@selector(compare:)];
+ [self exitEditingModeAnimated:YES];
+ [self moveSelectedItems:sortedIndexPaths toSection:SectionIdentifierRead];
+}
+
+- (void)markItemsUnread {
+ base::RecordAction(base::UserMetricsAction("MobileReadingListMarkUnread"));
+ [self updateSelectedItemsWithEntryUpdater:^(const GURL& url) {
+ [self readingListModel]->SetReadStatus(url, false);
+ }];
+
+ NSArray* sortedIndexPaths = [self.collectionView.indexPathsForSelectedItems
+ sortedArrayUsingSelector:@selector(compare:)];
+ [self exitEditingModeAnimated:YES];
+ [self moveSelectedItems:sortedIndexPaths toSection:SectionIdentifierUnread];
+}
+
+- (void)deleteAllReadItems {
+ base::RecordAction(base::UserMetricsAction("MobileReadingListDeleteRead"));
+ if (![self hasItemInSection:SectionIdentifierRead]) {
+ [self exitEditingModeAnimated:YES];
+ return;
+ }
+
+ [self updateItemsInSectionIdentifier:SectionIdentifierRead
+ usingEntryUpdater:^(const GURL& url) {
+ [self logDeletionHistogramsForEntry:url];
+ [self readingListModel]->RemoveEntryByURL(url);
+ }];
+
+ [self exitEditingModeAnimated:YES];
+ [self.collectionView performBatchUpdates:^{
+ NSInteger readSection = [self.collectionViewModel
+ sectionForSectionIdentifier:SectionIdentifierRead];
+ [self.collectionView
+ deleteSections:[NSIndexSet indexSetWithIndex:readSection]];
+ [self.collectionViewModel
+ removeSectionWithIdentifier:SectionIdentifierRead];
+ }
+ completion:^(BOOL) {
+ // Reload data to take into account possible sync events.
+ [self reloadData];
+ }];
+ // As we modified the section in the batch update block, remove the section in
+ // another block.
+ [self removeEmptySections];
+}
+
+- (void)deleteSelectedItems {
+ [self updateSelectedItemsWithEntryUpdater:^(const GURL& url) {
+ [self logDeletionHistogramsForEntry:url];
+ [self readingListModel]->RemoveEntryByURL(url);
+ }];
+
+ NSArray* indexPaths = [self.collectionView.indexPathsForSelectedItems copy];
+ [self exitEditingModeAnimated:YES];
+
+ [self.collectionView performBatchUpdates:^{
+ [self collectionView:self.collectionView
+ willDeleteItemsAtIndexPaths:indexPaths];
+
+ [self.collectionView deleteItemsAtIndexPaths:indexPaths];
+ }
+ completion:^(BOOL) {
+ // Reload data to take into account possible sync events.
+ [self reloadData];
+ }];
+ // As we modified the section in the batch update block, remove the section in
+ // another block.
+ [self removeEmptySections];
+}
+
+- (void)updateItemsInSectionIdentifier:(SectionIdentifier)identifier
+ usingEntryUpdater:(EntryUpdater)updater {
+ auto token = self.readingListModel->BeginBatchUpdates();
+ NSArray* readItems =
+ [self.collectionViewModel itemsInSectionWithIdentifier:identifier];
+ for (id item in readItems) {
+ ReadingListCollectionViewItem* readingListItem =
+ base::mac::ObjCCastStrict<ReadingListCollectionViewItem>(item);
+ if (updater)
+ updater(readingListItem.url);
+ }
+}
+
+- (void)updateSelectedItemsWithEntryUpdater:(EntryUpdater)updater {
+ auto token = self.readingListModel->BeginBatchUpdates();
+ for (NSIndexPath* index in self.collectionView.indexPathsForSelectedItems) {
+ CollectionViewItem* cell = [self.collectionViewModel itemAtIndexPath:index];
+ ReadingListCollectionViewItem* readingListItem =
+ base::mac::ObjCCastStrict<ReadingListCollectionViewItem>(cell);
+ if (updater)
+ updater(readingListItem.url);
+ }
+}
+
+- (void)logDeletionHistogramsForEntry:(const GURL&)url {
+ const ReadingListEntry* entry = [self readingListModel]->GetEntryByURL(url);
+
+ if (!entry)
+ return;
+
+ int64_t firstRead = entry->FirstReadTime();
+ if (firstRead > 0) {
+ // Log 0 if the entry has never been read.
+ firstRead = (base::Time::Now() - base::Time::UnixEpoch()).InMicroseconds() -
+ firstRead;
+ // Convert it to hours.
+ firstRead = firstRead / base::Time::kMicrosecondsPerHour;
+ }
+ UMA_HISTOGRAM_COUNTS_10000("ReadingList.FirstReadAgeOnDeletion", firstRead);
+
+ int64_t age = (base::Time::Now() - base::Time::UnixEpoch()).InMicroseconds() -
+ entry->CreationTime();
+ // Convert it to hours.
+ age = age / base::Time::kMicrosecondsPerHour;
+ if (entry->IsRead())
+ UMA_HISTOGRAM_COUNTS_10000("ReadingList.Read.AgeOnDeletion", age);
+ else
+ UMA_HISTOGRAM_COUNTS_10000("ReadingList.Unread.AgeOnDeletion", age);
+}
+
+- (void)moveItemsFromSection:(SectionIdentifier)sourceSectionIdentifier
+ toSection:(SectionIdentifier)destinationSectionIdentifier {
+ [self initializeSection:destinationSectionIdentifier];
+
+ NSInteger sourceSection = [self.collectionViewModel
+ sectionForSectionIdentifier:sourceSectionIdentifier];
+ NSInteger destinationSection = [self.collectionViewModel
+ sectionForSectionIdentifier:destinationSectionIdentifier];
+ NSInteger numberOfSourceItems =
+ [self.collectionViewModel numberOfItemsInSection:sourceSection];
+
+ [self.collectionView performBatchUpdates:^{
+ for (int index = 0; index < numberOfSourceItems; index++) {
+ NSIndexPath* firstItemIndex =
+ [NSIndexPath indexPathForItem:0 inSection:sourceSection];
+ NSIndexPath* sourceItemIndex =
+ [NSIndexPath indexPathForItem:index inSection:sourceSection];
+ NSIndexPath* destinationItemIndex =
+ [NSIndexPath indexPathForItem:index inSection:destinationSection];
+
+ // The collection view model gets updated instantaneously, the collection
+ // view does batch updates.
+ [self collectionView:self.collectionView
+ willMoveItemAtIndexPath:firstItemIndex
+ toIndexPath:destinationItemIndex];
+ [self.collectionView moveItemAtIndexPath:sourceItemIndex
+ toIndexPath:destinationItemIndex];
+ }
+ }
+ completion:^(BOOL) {
+ // Reload data to take into account possible sync events.
+ [self reloadData];
+ }];
+ // As we modified the section in the batch update block, remove the section in
+ // another block.
+ [self removeEmptySections];
+}
+
+- (void)moveSelectedItems:(NSArray*)sortedIndexPaths
+ toSection:(SectionIdentifier)sectionIdentifier {
+ NSInteger sectionCreatedIndex = [self initializeSection:sectionIdentifier];
+
+ [self.collectionView performBatchUpdates:^{
+ NSInteger section = [self.collectionViewModel
+ sectionForSectionIdentifier:sectionIdentifier];
+
+ NSInteger newItemIndex = 0;
+ // In order to make sure the we do not end modifying the wrong item, we have
+ // to take the items in the reverse order of the indexPaths.
+ for (NSIndexPath* index in [sortedIndexPaths reverseObjectEnumerator]) {
+ // The |sortedIndexPaths| is a copy of the index paths before the
+ // destination section has been added if necessary. The section part of
+ // the index potentially needs to be updated.
+ NSInteger updatedSection = index.section;
+ if (updatedSection >= sectionCreatedIndex)
+ updatedSection++;
+ if (updatedSection == section) {
+ // The item is already in the targeted section, there is no need to move
+ // it.
+ continue;
+ }
+
+ NSIndexPath* updatedIndex =
+ [NSIndexPath indexPathForItem:index.item inSection:updatedSection];
+
+ // Index of the item in the new section. The newItemIndex is the index of
+ // this item in the targeted section.
+ NSIndexPath* newIndexPath =
+ [NSIndexPath indexPathForItem:newItemIndex++ inSection:section];
+ [self collectionView:self.collectionView
+ willMoveItemAtIndexPath:updatedIndex
+ toIndexPath:newIndexPath];
+ [self.collectionView moveItemAtIndexPath:updatedIndex
+ toIndexPath:newIndexPath];
+ }
+ }
+ completion:^(BOOL) {
+ // Reload data to take into account possible sync events.
+ [self reloadData];
+ }];
+ // As we modified the section in the batch update block, remove the section in
+ // another block.
+ [self removeEmptySections];
+}
+
+- (NSInteger)initializeSection:(SectionIdentifier)sectionIdentifier {
+ if (![self.collectionViewModel
+ hasSectionForSectionIdentifier:sectionIdentifier]) {
+ // The new section IndexPath will be 1 if it is the read section with
+ // items in the unread section, 0 otherwise.
+ BOOL hasNonEmptySectionAbove =
+ sectionIdentifier == SectionIdentifierRead &&
+ [self hasItemInSection:SectionIdentifierUnread];
+ NSInteger sectionIndex = hasNonEmptySectionAbove ? 1 : 0;
+
+ [self.collectionView performBatchUpdates:^{
+ [self.collectionViewModel insertSectionWithIdentifier:sectionIdentifier
+ atIndex:sectionIndex];
+
+ [self.collectionViewModel
+ setHeader:[self headerForSection:sectionIdentifier]
+ forSectionWithIdentifier:sectionIdentifier];
+
+ [self.collectionView
+ insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]];
+ }
+ completion:nil];
+
+ return sectionIndex;
+ }
+ return NSIntegerMax;
+}
+
+- (CollectionViewTextItem*)headerForSection:
+ (SectionIdentifier)sectionIdentifier {
+ CollectionViewTextItem* header = nil;
+
+ switch (sectionIdentifier) {
+ case SectionIdentifierRead:
+ header = [[CollectionViewTextItem alloc] initWithType:ItemTypeReadHeader];
+ header.text = l10n_util::GetNSString(IDS_IOS_READING_LIST_READ_HEADER);
+ break;
+
+ case SectionIdentifierUnread:
+ header =
+ [[CollectionViewTextItem alloc] initWithType:ItemTypeUnreadHeader];
+ header.text = l10n_util::GetNSString(IDS_IOS_READING_LIST_UNREAD_HEADER);
+ break;
+ }
+ return header;
+}
+
+- (void)removeEmptySections {
+ [self.collectionView performBatchUpdates:^{
+
+ SectionIdentifier a[] = {SectionIdentifierRead, SectionIdentifierUnread};
+ for (size_t i = 0; i < arraysize(a); i++) {
+ SectionIdentifier sectionIdentifier = a[i];
+
+ if ([self.collectionViewModel
+ hasSectionForSectionIdentifier:sectionIdentifier] &&
+ ![self hasItemInSection:sectionIdentifier]) {
+ NSInteger section = [self.collectionViewModel
+ sectionForSectionIdentifier:sectionIdentifier];
+
+ [self.collectionView
+ deleteSections:[NSIndexSet indexSetWithIndex:section]];
+ [self.collectionViewModel
+ removeSectionWithIdentifier:sectionIdentifier];
+ }
+ }
+ }
+ completion:nil];
+}
+
+- (void)exitEditingModeAnimated:(BOOL)animated {
+ [self.editor setEditing:NO animated:animated];
+ [_toolbar setEditing:NO];
+}
+
+@end

Powered by Google App Engine
This is Rietveld 408576698