blob: 759eefb9803dbbc39e6ab2d5a1d5c8dc4942f0e6 [file] [log] [blame]
// Copyright 2018 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/list_model/list_model.h"
#include "base/logging.h"
#import "base/numerics/safe_conversions.h"
#import "ios/chrome/browser/ui/list_model/list_item.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
NSString* const kListModelCollapsedKey = @"ChromeListModelCollapsedSections";
namespace {
typedef NSMutableArray<ListItem*> SectionItems;
}
@implementation ListModel {
// Ordered list of section identifiers, one per section in the model.
NSMutableArray<NSNumber*>* _sectionIdentifiers;
// The lists of section items, one per section.
NSMutableArray<SectionItems*>* _sections;
// Maps from section identifier to header and footer.
NSMutableDictionary<NSNumber*, ListItem*>* _headers;
NSMutableDictionary<NSNumber*, ListItem*>* _footers;
// Maps from collapsed keys to section identifier.
NSMutableDictionary<NSNumber*, NSString*>* _collapsedKeys;
}
@synthesize collapsableMode = _collapsableMode;
- (instancetype)init {
if ((self = [super init])) {
_sectionIdentifiers = [[NSMutableArray alloc] init];
_sections = [[NSMutableArray alloc] init];
_headers = [[NSMutableDictionary alloc] init];
_footers = [[NSMutableDictionary alloc] init];
}
return self;
}
#pragma mark Modification methods
- (void)addSectionWithIdentifier:(NSInteger)sectionIdentifier {
DCHECK_GE(sectionIdentifier, kSectionIdentifierEnumZero);
DCHECK_EQ(base::checked_cast<NSUInteger>(NSNotFound),
[self internalSectionForIdentifier:sectionIdentifier]);
[_sectionIdentifiers addObject:@(sectionIdentifier)];
SectionItems* section = [[SectionItems alloc] init];
[_sections addObject:section];
}
- (void)insertSectionWithIdentifier:(NSInteger)sectionIdentifier
atIndex:(NSUInteger)index {
DCHECK_GE(sectionIdentifier, kSectionIdentifierEnumZero);
DCHECK_EQ(base::checked_cast<NSUInteger>(NSNotFound),
[self internalSectionForIdentifier:sectionIdentifier]);
DCHECK_LE(index, [_sections count]);
[_sectionIdentifiers insertObject:@(sectionIdentifier) atIndex:index];
SectionItems* section = [[SectionItems alloc] init];
[_sections insertObject:section atIndex:index];
}
- (void)addItem:(ListItem*)item
toSectionWithIdentifier:(NSInteger)sectionIdentifier {
DCHECK_GE(item.type, kItemTypeEnumZero);
NSInteger section = [self sectionForSectionIdentifier:sectionIdentifier];
SectionItems* items = [_sections objectAtIndex:section];
[items addObject:item];
}
- (void)insertItem:(ListItem*)item
inSectionWithIdentifier:(NSInteger)sectionIdentifier
atIndex:(NSUInteger)index {
DCHECK_GE(item.type, kItemTypeEnumZero);
NSInteger section = [self sectionForSectionIdentifier:sectionIdentifier];
SectionItems* items = [_sections objectAtIndex:section];
DCHECK(index <= [items count]);
[items insertObject:item atIndex:index];
}
- (void)removeItemWithType:(NSInteger)itemType
fromSectionWithIdentifier:(NSInteger)sectionIdentifier {
[self removeItemWithType:itemType
fromSectionWithIdentifier:sectionIdentifier
atIndex:0];
}
- (void)removeItemWithType:(NSInteger)itemType
fromSectionWithIdentifier:(NSInteger)sectionIdentifier
atIndex:(NSUInteger)index {
NSInteger section = [self sectionForSectionIdentifier:sectionIdentifier];
SectionItems* items = [_sections objectAtIndex:section];
NSInteger item =
[self itemForItemType:itemType inSectionItems:items atIndex:index];
DCHECK_NE(NSNotFound, item);
[items removeObjectAtIndex:item];
}
- (void)removeSectionWithIdentifier:(NSInteger)sectionIdentifier {
NSInteger section = [self sectionForSectionIdentifier:sectionIdentifier];
[_sectionIdentifiers removeObjectAtIndex:section];
[_sections removeObjectAtIndex:section];
[_collapsedKeys removeObjectForKey:@(sectionIdentifier)];
}
- (void)deleteAllItemsFromSectionWithIdentifier:(NSInteger)sectionIdentifier {
NSInteger section = [self sectionForSectionIdentifier:sectionIdentifier];
SectionItems* items = [_sections objectAtIndex:section];
[items removeAllObjects];
}
- (void)setHeader:(ListItem*)header
forSectionWithIdentifier:(NSInteger)sectionIdentifier {
NSNumber* key = [NSNumber numberWithInteger:sectionIdentifier];
if (header) {
[_headers setObject:header forKey:key];
} else {
[_headers removeObjectForKey:key];
}
}
- (void)setFooter:(ListItem*)footer
forSectionWithIdentifier:(NSInteger)sectionIdentifier {
NSNumber* key = [NSNumber numberWithInteger:sectionIdentifier];
if (footer) {
[_footers setObject:footer forKey:key];
} else {
[_footers removeObjectForKey:key];
}
}
#pragma mark Query model coordinates from index paths
- (NSInteger)sectionIdentifierForSection:(NSInteger)section {
DCHECK_LT(base::checked_cast<NSUInteger>(section),
[_sectionIdentifiers count]);
return [[_sectionIdentifiers objectAtIndex:section] integerValue];
}
- (NSInteger)itemTypeForIndexPath:(NSIndexPath*)indexPath {
return [self itemAtIndexPath:indexPath].type;
}
- (NSUInteger)indexInItemTypeForIndexPath:(NSIndexPath*)indexPath {
DCHECK_LT(base::checked_cast<NSUInteger>(indexPath.section),
[_sections count]);
SectionItems* items = [_sections objectAtIndex:indexPath.section];
ListItem* item = [self itemAtIndexPath:indexPath];
NSUInteger indexInItemType =
[self indexInItemTypeForItem:item inSectionItems:items];
return indexInItemType;
}
#pragma mark Query items from index paths
- (BOOL)hasItemAtIndexPath:(NSIndexPath*)indexPath {
if (!indexPath)
return NO;
if (base::checked_cast<NSUInteger>(indexPath.section) < [_sections count]) {
SectionItems* items = [_sections objectAtIndex:indexPath.section];
return base::checked_cast<NSUInteger>(indexPath.item) < [items count];
}
return NO;
}
- (ListItem*)itemAtIndexPath:(NSIndexPath*)indexPath {
DCHECK(indexPath);
DCHECK_LT(base::checked_cast<NSUInteger>(indexPath.section),
[_sections count]);
SectionItems* items = [_sections objectAtIndex:indexPath.section];
DCHECK_LT(base::checked_cast<NSUInteger>(indexPath.item), [items count]);
return [items objectAtIndex:indexPath.item];
}
- (ListItem*)headerForSection:(NSInteger)section {
NSInteger sectionIdentifier = [self sectionIdentifierForSection:section];
NSNumber* key = [NSNumber numberWithInteger:sectionIdentifier];
return [_headers objectForKey:key];
}
- (ListItem*)footerForSection:(NSInteger)section {
NSInteger sectionIdentifier = [self sectionIdentifierForSection:section];
NSNumber* key = [NSNumber numberWithInteger:sectionIdentifier];
return [_footers objectForKey:key];
}
- (NSArray<ListItem*>*)itemsInSectionWithIdentifier:
(NSInteger)sectionIdentifier {
NSInteger section = [self sectionForSectionIdentifier:sectionIdentifier];
DCHECK_LT(base::checked_cast<NSUInteger>(section), [_sections count]);
return [_sections objectAtIndex:section];
}
- (ListItem*)headerForSectionWithIdentifier:(NSInteger)sectionIdentifier {
NSNumber* key = [NSNumber numberWithInteger:sectionIdentifier];
return [_headers objectForKey:key];
}
- (ListItem*)footerForSectionWithIdentifier:(NSInteger)sectionIdentifier {
NSNumber* key = [NSNumber numberWithInteger:sectionIdentifier];
return [_footers objectForKey:key];
}
#pragma mark Query index paths from model coordinates
- (BOOL)hasSectionForSectionIdentifier:(NSInteger)sectionIdentifier {
NSUInteger section = [self internalSectionForIdentifier:sectionIdentifier];
return section != base::checked_cast<NSUInteger>(NSNotFound);
}
- (NSInteger)sectionForSectionIdentifier:(NSInteger)sectionIdentifier {
NSUInteger section = [self internalSectionForIdentifier:sectionIdentifier];
DCHECK_NE(base::checked_cast<NSUInteger>(NSNotFound), section);
return section;
}
- (BOOL)hasItemForItemType:(NSInteger)itemType
sectionIdentifier:(NSInteger)sectionIdentifier {
return [self hasItemForItemType:itemType
sectionIdentifier:sectionIdentifier
atIndex:0];
}
- (NSIndexPath*)indexPathForItemType:(NSInteger)itemType
sectionIdentifier:(NSInteger)sectionIdentifier {
return [self indexPathForItemType:itemType
sectionIdentifier:sectionIdentifier
atIndex:0];
}
- (BOOL)hasItemForItemType:(NSInteger)itemType
sectionIdentifier:(NSInteger)sectionIdentifier
atIndex:(NSUInteger)index {
if (![self hasSectionForSectionIdentifier:sectionIdentifier]) {
return NO;
}
NSInteger section = [self sectionForSectionIdentifier:sectionIdentifier];
SectionItems* items = [_sections objectAtIndex:section];
NSInteger item =
[self itemForItemType:itemType inSectionItems:items atIndex:index];
return item != NSNotFound;
}
- (NSIndexPath*)indexPathForItemType:(NSInteger)itemType
sectionIdentifier:(NSInteger)sectionIdentifier
atIndex:(NSUInteger)index {
NSInteger section = [self sectionForSectionIdentifier:sectionIdentifier];
SectionItems* items = [_sections objectAtIndex:section];
NSInteger item =
[self itemForItemType:itemType inSectionItems:items atIndex:index];
return [NSIndexPath indexPathForItem:item inSection:section];
}
- (NSIndexPath*)indexPathForItemType:(NSInteger)itemType {
__block NSIndexPath* indexPath = nil;
[_sections
enumerateObjectsUsingBlock:^(SectionItems* sectionItems,
NSUInteger sectionIndex, BOOL* sectionStop) {
[sectionItems enumerateObjectsUsingBlock:^(
ListItem* obj, NSUInteger itemIndex, BOOL* itemStop) {
if (obj.type == itemType) {
indexPath = [NSIndexPath indexPathForRow:itemIndex
inSection:sectionIndex];
*itemStop = YES;
}
}];
*sectionStop = (indexPath != nil);
}];
return indexPath;
}
#pragma mark Query index paths from items
- (BOOL)hasItem:(ListItem*)item
inSectionWithIdentifier:(NSInteger)sectionIdentifier {
return [[self itemsInSectionWithIdentifier:sectionIdentifier]
indexOfObject:item] != NSNotFound;
}
- (BOOL)hasItem:(ListItem*)item {
for (NSNumber* section in _sectionIdentifiers) {
if ([self hasItem:item inSectionWithIdentifier:[section integerValue]])
return YES;
}
return NO;
}
- (NSIndexPath*)indexPathForItem:(ListItem*)item {
for (NSUInteger section = 0; section < _sections.count; section++) {
NSInteger itemIndex = [_sections[section] indexOfObject:item];
if (itemIndex != NSNotFound) {
return [NSIndexPath indexPathForItem:itemIndex inSection:section];
}
}
NOTREACHED();
return nil;
}
#pragma mark Data sourcing
- (NSInteger)numberOfSections {
return [_sections count];
}
- (NSInteger)numberOfItemsInSection:(NSInteger)section {
DCHECK_LT(base::checked_cast<NSUInteger>(section), [_sections count]);
NSInteger sectionIdentifier = [self sectionIdentifierForSection:section];
SectionItems* items = [_sections objectAtIndex:section];
if ([self sectionIsCollapsed:sectionIdentifier]) {
switch (self.collapsableMode) {
case ListModelCollapsableModeHeader:
return 0;
case ListModelCollapsableModeFirstCell:
DCHECK_LT(0ul, items.count);
return 1;
}
NOTREACHED();
}
return items.count;
}
#pragma mark Collapsing methods.
- (void)setSectionIdentifier:(NSInteger)sectionIdentifier
collapsedKey:(NSString*)collapsedKey {
// Check that the sectionIdentifier exists.
DCHECK([self hasSectionForSectionIdentifier:sectionIdentifier]);
// Check that the collapsedKey is not being used already.
DCHECK(![self.collapsedKeys allKeysForObject:collapsedKey].count);
[self.collapsedKeys setObject:collapsedKey forKey:@(sectionIdentifier)];
}
- (void)setSection:(NSInteger)sectionIdentifier collapsed:(BOOL)collapsed {
// TODO(crbug.com/419346): Store in the browser state preference instead of
// NSUserDefaults.
DCHECK([self hasSectionForSectionIdentifier:sectionIdentifier]);
NSString* sectionKey = [self.collapsedKeys objectForKey:@(sectionIdentifier)];
DCHECK(sectionKey);
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
NSDictionary* collapsedSections =
[defaults dictionaryForKey:kListModelCollapsedKey];
NSMutableDictionary* newCollapsedSection =
[NSMutableDictionary dictionaryWithDictionary:collapsedSections];
NSNumber* value = [NSNumber numberWithBool:collapsed];
[newCollapsedSection setValue:value forKey:sectionKey];
[defaults setObject:newCollapsedSection forKey:kListModelCollapsedKey];
}
- (BOOL)sectionIsCollapsed:(NSInteger)sectionIdentifier {
// TODO(crbug.com/419346): Store in the profile's preference instead of the
// NSUserDefaults.
DCHECK([self hasSectionForSectionIdentifier:sectionIdentifier]);
NSString* sectionKey = [self.collapsedKeys objectForKey:@(sectionIdentifier)];
if (!sectionKey)
return NO;
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
NSDictionary* collapsedSections =
[defaults dictionaryForKey:kListModelCollapsedKey];
NSNumber* value = (NSNumber*)[collapsedSections valueForKey:sectionKey];
return [value boolValue];
}
// |_collapsedKeys| lazy instantiation.
- (NSMutableDictionary*)collapsedKeys {
if (!_collapsedKeys) {
_collapsedKeys = [[NSMutableDictionary alloc] init];
}
return _collapsedKeys;
}
#pragma mark Private methods
// Returns the section for the given section identifier. If the section
// identifier is not found, NSNotFound is returned.
- (NSUInteger)internalSectionForIdentifier:(NSInteger)sectionIdentifier {
return [_sectionIdentifiers indexOfObject:@(sectionIdentifier)];
}
// Returns the item for the given item type in the list of items, at the
// given index. If no item is found with the given type, NSNotFound is returned.
- (NSUInteger)itemForItemType:(NSInteger)itemType
inSectionItems:(SectionItems*)sectionItems
atIndex:(NSUInteger)index {
__block NSUInteger item = NSNotFound;
__block NSUInteger indexInItemType = 0;
[sectionItems
enumerateObjectsUsingBlock:^(ListItem* obj, NSUInteger idx, BOOL* stop) {
if (obj.type == itemType) {
if (indexInItemType == index) {
item = idx;
*stop = YES;
} else {
indexInItemType++;
}
}
}];
return item;
}
// Returns |item|'s index among all the items of the same type in the given
// section items. |item| must belong to |sectionItems|.
- (NSUInteger)indexInItemTypeForItem:(ListItem*)item
inSectionItems:(SectionItems*)sectionItems {
DCHECK([sectionItems containsObject:item]);
BOOL found = NO;
NSUInteger indexInItemType = 0;
for (ListItem* sectionItem in sectionItems) {
if (sectionItem == item) {
found = YES;
break;
}
if (sectionItem.type == item.type) {
indexInItemType++;
}
}
DCHECK(found);
return indexInItemType;
}
@end