[go: nahoru, domu]

blob: a033dd9ea41ed93dac7d3c170498d3c399000af2 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// 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/tab_switcher/tab_grid/pinned_tabs/pinned_tabs_view_controller.h"
#import "base/apple/foundation_util.h"
#import "base/dcheck_is_on.h"
#import "base/ios/block_types.h"
#import "base/ios/ios_util.h"
#import "base/metrics/histogram_functions.h"
#import "base/notreached.h"
#import "base/numerics/safe_conversions.h"
#import "ios/chrome/browser/shared/ui/util/rtl_geometry.h"
#import "ios/chrome/browser/tabs/features.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_collection_drag_drop_handler.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_collection_drag_drop_metrics.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/pinned_tabs/pinned_cell.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/pinned_tabs/pinned_tabs_constants.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/pinned_tabs/pinned_tabs_layout.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_context_menu/tab_context_menu_provider.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/transitions/legacy_grid_transition_layout.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_switcher_item.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/public/provider/chrome/browser/modals/modals_api.h"
#import "ui/base/l10n/l10n_util_mac.h"
namespace {
// The number of sections for the pinned collection view.
NSInteger kNumberOfSectionsInPinnedCollection = 1;
// Pinned cell identifier.
NSString* const kCellIdentifier = @"PinnedCellIdentifier";
// Creates an NSIndexPath with `index` in section 0.
NSIndexPath* CreateIndexPath(NSInteger index) {
return [NSIndexPath indexPathForItem:index inSection:0];
}
} // namespace
@interface PinnedTabsViewController () <UICollectionViewDragDelegate,
UICollectionViewDropDelegate>
// Index of the selected item in `_items`.
@property(nonatomic, readonly) NSUInteger selectedIndex;
@end
@implementation PinnedTabsViewController {
// The local model backing the collection view.
NSMutableArray<TabSwitcherItem*>* _items;
// Identifier of the selected item.
NSString* _selectedItemID;
// Identifier of the lastest dragged item. This property is set when the item
// is long pressed which does not always result in a drag action.
NSString* _draggedItemID;
// Identifier of the last item to be inserted. This is used to track if the
// active tab was newly created when building the animation layout for
// transitions.
NSString* _lastInsertedItemID;
// Constraints used to update the view during drag and drop actions.
NSLayoutConstraint* _dragEnabledConstraint;
NSLayoutConstraint* _defaultConstraint;
// Background color of the view.
UIColor* _backgroundColor;
// View displayed during an external drag action.
UIView* _dropOverlayView;
// Tracks if the view is available.
BOOL _available;
// Tracks if a drag action is in progress.
BOOL _dragSessionEnabled;
BOOL _localDragActionInProgress;
// YES if the dragged tab moved to a new index.
BOOL _dragEndAtNewIndex;
// YES if view controller's content has appeared.
BOOL _contentAppeared;
// Tracks if there is a scroll in progress.
BOOL _scrollInProgress;
}
- (instancetype)init {
PinnedTabsLayout* layout = [[PinnedTabsLayout alloc] init];
if (self = [super initWithCollectionViewLayout:layout]) {
}
return self;
}
#pragma mark - UICollectionViewController
- (void)viewDidLoad {
[super viewDidLoad];
_available = YES;
_visible = YES;
_dragSessionEnabled = NO;
_localDragActionInProgress = NO;
_dropAnimationInProgress = NO;
_contentAppeared = NO;
_scrollInProgress = NO;
[self configureCollectionView];
[self configureDropOverlayView];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self contentWillAppearAnimated:animated];
}
#pragma mark - Public
- (void)contentWillAppearAnimated:(BOOL)animated {
[self.collectionView reloadData];
[self deselectAllCollectionViewItemsAnimated:NO];
[self selectCollectionViewItemWithID:_selectedItemID animated:NO];
[self scrollCollectionViewToSelectedItemAnimated:NO];
// Update the delegate, in case it wasn't set when `items` was populated.
[self.delegate pinnedTabsViewController:self didChangeItemCount:_items.count];
_lastInsertedItemID = nil;
_contentAppeared = YES;
}
- (void)contentWillDisappear {
_contentAppeared = NO;
}
- (void)dragSessionEnabled:(BOOL)enabled {
if (_dropAnimationInProgress || (_dragSessionEnabled == enabled)) {
return;
}
_dragSessionEnabled = enabled;
__weak __typeof(self) weakSelf = self;
[UIView animateWithDuration:kPinnedViewDragAnimationTime
animations:^{
self->_dragEnabledConstraint.active = enabled;
self->_defaultConstraint.active = !enabled;
[self updateDropOverlayViewVisibility];
[self resetViewBackgrounds];
[self.view.superview layoutIfNeeded];
[self.view layoutIfNeeded];
}
completion:^(BOOL finished) {
[weakSelf popLastInsertedItem];
}];
}
- (void)pinnedTabsAvailable:(BOOL)available {
_available = available;
// The view is visible if `_items` is not empty or if a drag action is in
// progress.
bool visible = _available && (_items.count || _dragSessionEnabled);
if (visible == _visible) {
return;
}
_visible = visible;
// Show the view if `visible` is true to ensure smooth animation.
if (visible) {
[self updateDropOverlayViewVisibility];
self.view.hidden = NO;
}
// Tell the delegate that the visibility has changed in order to update the
// tab grid view inset before hiding the pinned view.
[self.delegate pinnedTabsViewControllerVisibilityDidChange:self];
__weak __typeof(self) weakSelf = self;
[UIView animateWithDuration:kPinnedViewFadeInTime
animations:^{
self.view.alpha = visible ? 1.0 : 0.0;
}
completion:^(BOOL finished) {
[weakSelf updatePinnedTabsVisibilityAfterAnimation];
}];
}
- (void)dropAnimationDidEnd {
// If a local drag action is in progress, `dragSessionDidEnd:` will end the
// drag session.
if (_localDragActionInProgress) {
return;
}
_dropAnimationInProgress = NO;
[self dragSessionEnabled:NO];
}
- (LegacyGridTransitionLayout*)transitionLayout {
[self.collectionView layoutIfNeeded];
LegacyGridTransitionActiveItem* activeItem;
LegacyGridTransitionItem* selectionItem;
NSIndexPath* selectedItemIndexPath =
self.collectionView.indexPathsForSelectedItems.firstObject;
PinnedCell* selectedCell = base::apple::ObjCCastStrict<PinnedCell>(
[self.collectionView cellForItemAtIndexPath:selectedItemIndexPath]);
if ([selectedCell hasIdentifier:_selectedItemID]) {
UICollectionViewLayoutAttributes* attributes = [self.collectionView
layoutAttributesForItemAtIndexPath:selectedItemIndexPath];
// Normalize frame to window coordinates. The attributes class applies this
// change to the other properties such as center, bounds, etc.
attributes.frame = [self.collectionView convertRect:attributes.frame
toView:nil];
PinnedTransitionCell* activeCell =
[PinnedTransitionCell transitionCellFromCell:selectedCell];
activeItem = [LegacyGridTransitionActiveItem itemWithCell:activeCell
center:attributes.center
size:attributes.size];
// If the active item is the last inserted item, it needs to be animated
// differently.
if ([selectedCell hasIdentifier:_lastInsertedItemID]) {
activeItem.isAppearing = YES;
}
selectionItem = [LegacyGridTransitionItem
itemWithCell:[PinnedCell transitionSelectionCellFromCell:selectedCell]
center:attributes.center];
}
return [LegacyGridTransitionLayout layoutWithInactiveItems:@[]
activeItem:activeItem
selectionItem:selectionItem];
}
- (BOOL)isCollectionEmpty {
return _items.count == 0;
}
- (BOOL)isSelectedCellVisible {
// The collection view's selected item may not have updated yet, so use the
// selected index.
NSUInteger selectedIndex = self.selectedIndex;
if (selectedIndex == NSNotFound) {
return NO;
}
NSIndexPath* selectedIndexPath = CreateIndexPath(selectedIndex);
return [self.collectionView.indexPathsForVisibleItems
containsObject:selectedIndexPath];
}
- (BOOL)hasSelectedCell {
return self.selectedIndex != NSNotFound;
}
#pragma mark - TabCollectionConsumer
- (void)populateItems:(NSArray<TabSwitcherItem*>*)items
selectedItemID:(NSString*)selectedItemID {
#if DCHECK_IS_ON()
// Consistency check: ensure no IDs are duplicated.
NSMutableSet<NSString*>* identifiers = [[NSMutableSet alloc] init];
for (TabSwitcherItem* item in items) {
[identifiers addObject:item.identifier];
}
DCHECK_EQ(identifiers.count, items.count);
#endif
_items = [items mutableCopy];
_selectedItemID = selectedItemID;
[self updatePinnedTabsVisibility];
[self.delegate pinnedTabsViewController:self didChangeItemCount:items.count];
[self.collectionView reloadData];
[self deselectAllCollectionViewItemsAnimated:YES];
[self selectCollectionViewItemWithID:_selectedItemID animated:YES];
[self scrollCollectionViewToSelectedItemAnimated:YES];
}
- (void)insertItem:(TabSwitcherItem*)item
atIndex:(NSUInteger)index
selectedItemID:(NSString*)selectedItemID {
// Consistency check: `item`'s ID is not in `_items`.
DCHECK([self indexOfItemWithID:item.identifier] == NSNotFound);
__weak __typeof(self) weakSelf = self;
[self.collectionView
performBatchUpdates:^{
[weakSelf performBatchUpdateForInsertingItem:item
atIndex:index
selectedItemID:selectedItemID];
}
completion:^(BOOL completed) {
[weakSelf handleItemInsertionCompletion];
}];
}
- (void)removeItemWithID:(NSString*)removedItemID
selectedItemID:(NSString*)selectedItemID {
NSUInteger index = [self indexOfItemWithID:removedItemID];
if (index == NSNotFound) {
return;
}
__weak __typeof(self) weakSelf = self;
[self.collectionView
performBatchUpdates:^{
[weakSelf performBatchUpdateForRemovingItemAtIndex:index
selectedItemID:selectedItemID];
}
completion:^(BOOL completed) {
[weakSelf handleItemRemovalCompletion];
[weakSelf.delegate pinnedTabsViewController:weakSelf
didRemoveItemWIthID:removedItemID];
}];
}
- (void)selectItemWithID:(NSString*)selectedItemID {
if ([_selectedItemID isEqualToString:selectedItemID]) {
return;
}
[self deselectAllCollectionViewItemsAnimated:NO];
_selectedItemID = selectedItemID;
[self selectCollectionViewItemWithID:_selectedItemID animated:NO];
[self scrollCollectionViewToSelectedItemAnimated:NO];
}
- (void)replaceItemID:(NSString*)itemID withItem:(TabSwitcherItem*)item {
DCHECK([item.identifier isEqualToString:itemID] ||
[self indexOfItemWithID:item.identifier] == NSNotFound);
NSUInteger index = [self indexOfItemWithID:itemID];
_items[index] = item;
PinnedCell* cell = base::apple::ObjCCastStrict<PinnedCell>(
[self.collectionView cellForItemAtIndexPath:CreateIndexPath(index)]);
// `cell` may be nil if it is scrolled offscreen.
if (cell) {
[self configureCell:cell withItem:item];
}
}
- (void)moveItemWithID:(NSString*)itemID toIndex:(NSUInteger)toIndex {
NSUInteger fromIndex = [self indexOfItemWithID:itemID];
if (fromIndex == toIndex || toIndex == NSNotFound ||
fromIndex == NSNotFound) {
return;
}
ProceduralBlock modelUpdates = ^{
TabSwitcherItem* item = self->_items[fromIndex];
[self->_items removeObjectAtIndex:fromIndex];
[self->_items insertObject:item atIndex:toIndex];
};
ProceduralBlock collectionViewUpdates = ^{
[self.collectionView moveItemAtIndexPath:CreateIndexPath(fromIndex)
toIndexPath:CreateIndexPath(toIndex)];
};
__weak __typeof(self) weakSelf = self;
ProceduralBlock collectionViewUpdatesCompletion = ^{
[weakSelf updateCollectionViewAfterMovingItemToIndex:toIndex];
[weakSelf.delegate pinnedTabsViewController:weakSelf
didMoveItemWithID:itemID];
};
[self.collectionView
performBatchUpdates:^{
modelUpdates();
collectionViewUpdates();
}
completion:^(BOOL completed) {
collectionViewUpdatesCompletion();
}];
}
- (void)dismissModals {
ios::provider::DismissModalsForCollectionView(self.collectionView);
}
#pragma mark - UICollectionViewDataSource
- (NSInteger)numberOfSectionsInCollectionView:
(UICollectionView*)collectionView {
return kNumberOfSectionsInPinnedCollection;
}
- (NSInteger)collectionView:(UICollectionView*)collectionView
numberOfItemsInSection:(NSInteger)section {
return _items.count;
}
- (UICollectionViewCell*)collectionView:(UICollectionView*)collectionView
cellForItemAtIndexPath:(NSIndexPath*)indexPath {
NSUInteger itemIndex = base::checked_cast<NSUInteger>(indexPath.item);
// TODO(crbug.com/1068136): Remove this when the issue is closed.
// This is a preventive fix related to the issue above.
// Presumably this is a race condition where an item has been deleted at the
// same time as the collection is doing layout. The assumption is that there
// will be another, correct layout shortly after the incorrect one.
if (itemIndex >= _items.count) {
itemIndex = _items.count - 1;
}
TabSwitcherItem* item = _items[itemIndex];
PinnedCell* cell = base::apple::ObjCCastStrict<PinnedCell>([collectionView
dequeueReusableCellWithReuseIdentifier:kPinnedCellIdentifier
forIndexPath:indexPath]);
[self configureCell:cell withItem:item];
return cell;
}
#pragma mark - UICollectionViewDelegate
- (void)collectionView:(UICollectionView*)collectionView
didSelectItemAtIndexPath:(NSIndexPath*)indexPath {
if (@available(iOS 16, *)) {
// This is handled by
// `collectionView:performPrimaryActionForItemAtIndexPath:` on iOS 16.
} else {
[self tappedItemAtIndexPath:indexPath];
}
}
- (void)collectionView:(UICollectionView*)collectionView
performPrimaryActionForItemAtIndexPath:(NSIndexPath*)indexPath {
[self tappedItemAtIndexPath:indexPath];
}
- (UIContextMenuConfiguration*)collectionView:(UICollectionView*)collectionView
contextMenuConfigurationForItemAtIndexPath:(NSIndexPath*)indexPath
point:(CGPoint)point {
PinnedCell* cell = base::apple::ObjCCastStrict<PinnedCell>(
[self.collectionView cellForItemAtIndexPath:indexPath]);
return [self.menuProvider
contextMenuConfigurationForTabCell:cell
menuScenario:MenuScenarioHistogram::
kPinnedTabsEntry];
}
- (void)collectionView:(UICollectionView*)collectionView
didEndDisplayingCell:(UICollectionViewCell*)cell
forItemAtIndexPath:(NSIndexPath*)indexPath {
if ([cell isKindOfClass:[PinnedCell class]]) {
// Stop animation of PinnedCells when removing them from the collection
// view. This is important to prevent cells from animating indefinitely.
// This is safe because the animation state of GridCells is set in
// `configureCell:withItem:` whenever a cell is used.
[base::apple::ObjCCastStrict<PinnedCell>(cell) hideActivityIndicator];
}
}
#pragma mark - UICollectionViewDragDelegate
- (void)collectionView:(UICollectionView*)collectionView
dragSessionWillBegin:(id<UIDragSession>)session {
[self.dragDropHandler dragWillBeginForItemWithID:_draggedItemID];
_dragEndAtNewIndex = NO;
_localDragActionInProgress = YES;
base::UmaHistogramEnumeration(kUmaPinnedViewDragDropTabs,
DragDropTabs::kDragBegin);
[self dragSessionEnabled:YES];
}
- (void)collectionView:(UICollectionView*)collectionView
dragSessionDidEnd:(id<UIDragSession>)session {
_localDragActionInProgress = NO;
DragDropTabs dragEvent = _dragEndAtNewIndex
? DragDropTabs::kDragEndAtNewIndex
: DragDropTabs::kDragEndAtSameIndex;
// If a drop animation is in progress and the drag didn't end at a new index,
// that means the item has been dropped outside of its collection view.
if (_dropAnimationInProgress && !_dragEndAtNewIndex) {
dragEvent = DragDropTabs::kDragEndInOtherCollection;
}
base::UmaHistogramEnumeration(kUmaPinnedViewDragDropTabs, dragEvent);
[self.dragDropHandler dragSessionDidEnd];
[self.delegate pinnedViewControllerDragSessionDidEnd:self];
[self dragSessionEnabled:NO];
}
- (NSArray<UIDragItem*>*)collectionView:(UICollectionView*)collectionView
itemsForBeginningDragSession:(id<UIDragSession>)session
atIndexPath:(NSIndexPath*)indexPath {
TabSwitcherItem* item = _items[indexPath.item];
_draggedItemID = item.identifier;
UIDragItem* dragItem =
[self.dragDropHandler dragItemForItemWithID:_draggedItemID];
return [NSArray arrayWithObjects:dragItem, nil];
}
- (NSArray<UIDragItem*>*)collectionView:(UICollectionView*)collectionView
itemsForAddingToDragSession:(id<UIDragSession>)session
atIndexPath:(NSIndexPath*)indexPath
point:(CGPoint)point {
// Prevent more items from getting added to the drag session.
return @[];
}
- (UIDragPreviewParameters*)collectionView:(UICollectionView*)collectionView
dragPreviewParametersForItemAtIndexPath:(NSIndexPath*)indexPath {
PinnedCell* pinedCell = base::apple::ObjCCastStrict<PinnedCell>(
[self.collectionView cellForItemAtIndexPath:indexPath]);
return pinedCell.dragPreviewParameters;
}
#pragma mark - UICollectionViewDropDelegate
- (void)collectionView:(UICollectionView*)collectionView
dropSessionDidEnter:(id<UIDropSession>)session {
_dropOverlayView.backgroundColor = [UIColor colorNamed:kBlueColor];
self.collectionView.backgroundColor = [UIColor colorNamed:kBlueColor];
self.collectionView.backgroundView.hidden = YES;
}
- (void)collectionView:(UICollectionView*)collectionView
dropSessionDidExit:(id<UIDropSession>)session {
[self resetViewBackgrounds];
}
- (void)collectionView:(UICollectionView*)collectionView
dropSessionDidEnd:(id<UIDropSession>)session {
[self.delegate pinnedViewControllerDropAnimationDidEnd:self];
[self dropAnimationDidEnd];
}
- (UICollectionViewDropProposal*)
collectionView:(UICollectionView*)collectionView
dropSessionDidUpdate:(id<UIDropSession>)session
withDestinationIndexPath:(NSIndexPath*)destinationIndexPath {
UIDropOperation dropOperation =
[self.dragDropHandler dropOperationForDropSession:session];
UICollectionViewDropIntent intent =
_localDragActionInProgress
? UICollectionViewDropIntentInsertAtDestinationIndexPath
: UICollectionViewDropIntentUnspecified;
return
[[UICollectionViewDropProposal alloc] initWithDropOperation:dropOperation
intent:intent];
}
- (void)collectionView:(UICollectionView*)collectionView
performDropWithCoordinator:
(id<UICollectionViewDropCoordinator>)coordinator {
NSArray<id<UICollectionViewDropItem>>* items = coordinator.items;
for (id<UICollectionViewDropItem> item in items) {
// Append to the end of the collection, unless drop is from the same
// collection view and its index is specified.
// The sourceIndexPath is nil if the drop item is not from the same
// collection view. Set the destinationIndex to reflect the addition of an
// item.
NSUInteger destinationIndex =
item.sourceIndexPath ? _items.count - 1 : _items.count;
if (coordinator.destinationIndexPath && item.sourceIndexPath) {
destinationIndex =
base::checked_cast<NSUInteger>(coordinator.destinationIndexPath.item);
}
_dragEndAtNewIndex = YES;
NSIndexPath* dropIndexPath = CreateIndexPath(destinationIndex);
// Drop synchronously if local object is available.
if (item.dragItem.localObject) {
_dropAnimationInProgress = YES;
[self.delegate pinnedViewControllerDropAnimationWillBegin:self];
if (_localDragActionInProgress) {
__weak __typeof(self) weakSelf = self;
[[coordinator dropItem:item.dragItem toItemAtIndexPath:dropIndexPath]
addCompletion:^(UIViewAnimatingPosition finalPosition) {
[weakSelf dropAnimationDidEnd];
}];
}
// The sourceIndexPath is non-nil if the drop item is from this same
// collection view.
[self.dragDropHandler dropItem:item.dragItem
toIndex:destinationIndex
fromSameCollection:(item.sourceIndexPath != nil)];
} else {
// Drop asynchronously if local object is not available.
UICollectionViewDropPlaceholder* placeholder =
[[UICollectionViewDropPlaceholder alloc]
initWithInsertionIndexPath:dropIndexPath
reuseIdentifier:kCellIdentifier];
placeholder.previewParametersProvider =
^UIDragPreviewParameters*(UICollectionViewCell* placeholderCell) {
PinnedCell* pinnedCell =
base::apple::ObjCCastStrict<PinnedCell>(placeholderCell);
return pinnedCell.dragPreviewParameters;
};
id<UICollectionViewDropPlaceholderContext> context =
[coordinator dropItem:item.dragItem toPlaceholder:placeholder];
[self.dragDropHandler dropItemFromProvider:item.dragItem.itemProvider
toIndex:destinationIndex
placeholderContext:context];
}
}
}
- (BOOL)collectionView:(UICollectionView*)collectionView
canHandleDropSession:(id<UIDropSession>)session {
return _available;
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
_scrollInProgress = YES;
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView*)scrollView {
_scrollInProgress = NO;
[self popLastInsertedItem];
}
#pragma mark - Private properties
- (NSUInteger)selectedIndex {
return [self indexOfItemWithID:_selectedItemID];
}
#pragma mark - Private
// Animates the lastest inserted item (if any) with a pop animation.
// This method is called when :
// - The pinned overlay is hidden.
// - A scroll animation ends.
- (void)popLastInsertedItem {
if (_dragSessionEnabled || !_lastInsertedItemID) {
return;
}
NSUInteger itemIndex = [self indexOfItemWithID:_lastInsertedItemID];
// Check `itemIndex` boundaries in order to filter out possible race
// conditions while mutating the collection.
if (itemIndex == NSNotFound || itemIndex >= _items.count) {
return;
}
PinnedCell* pinnedCell = base::apple::ObjCCastStrict<PinnedCell>(
[self.collectionView cellForItemAtIndexPath:CreateIndexPath(itemIndex)]);
CGAffineTransform originalTransform = pinnedCell.transform;
// Initial attributes.
pinnedCell.alpha = 0;
pinnedCell.hidden = NO;
pinnedCell.transform =
CGAffineTransformScale(pinnedCell.transform, kPinnedCellPopInitialScale,
kPinnedCellPopInitialScale);
const BOOL isSelectedItem = _lastInsertedItemID == _selectedItemID;
_lastInsertedItemID = nil;
__weak __typeof(self) weakSelf = self;
[UIView animateWithDuration:kPinnedViewPopAnimationTime
animations:^{
pinnedCell.alpha = 1;
pinnedCell.transform = originalTransform;
[self.view layoutIfNeeded];
}
completion:^(BOOL finished) {
if (isSelectedItem) {
PinnedTabsViewController* strongSelf = weakSelf;
[strongSelf selectCollectionViewItemWithID:strongSelf->_selectedItemID
animated:NO];
}
}];
}
// Updates the visibility of the pinned view.
- (void)updatePinnedTabsVisibility {
[self pinnedTabsAvailable:_available];
}
// Performs (in batch) all the actions needed to insert an `item` at the
// specified `index` into the collection view and updates its appearance.
// `selectedItemID` is saved to an instance variable.
- (void)performBatchUpdateForInsertingItem:(TabSwitcherItem*)item
atIndex:(NSUInteger)index
selectedItemID:(NSString*)selectedItemID {
[_items insertObject:item atIndex:index];
_selectedItemID = [selectedItemID copy];
_lastInsertedItemID = [item.identifier copy];
[self.delegate pinnedTabsViewController:self didChangeItemCount:_items.count];
[self.collectionView insertItemsAtIndexPaths:@[ CreateIndexPath(index) ]];
}
// Performs (in batch) all the actions needed to remove an item at the
// specified `index` from the collection view and updates its appearance.
// `selectedItemID` is saved to an instance variable.
- (void)performBatchUpdateForRemovingItemAtIndex:(NSUInteger)index
selectedItemID:(NSString*)selectedItemID {
[_items removeObjectAtIndex:index];
_selectedItemID = selectedItemID;
[self.delegate pinnedTabsViewController:self didChangeItemCount:_items.count];
[self.collectionView deleteItemsAtIndexPaths:@[ CreateIndexPath(index) ]];
}
// Handles the completion of item insertion into the collection view.
- (void)handleItemInsertionCompletion {
[self updateCollectionViewAfterItemInsertion];
[self.delegate pinnedTabsViewController:self didChangeItemCount:_items.count];
}
// Handles the completion of item removal into the collection view.
- (void)handleItemRemovalCompletion {
[self updateCollectionViewAfterItemDeletion];
[self.delegate pinnedTabsViewController:self didChangeItemCount:_items.count];
}
// Configures the collectionView.
- (void)configureCollectionView {
self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
UICollectionView* collectionView = self.collectionView;
[collectionView registerClass:[PinnedCell class]
forCellWithReuseIdentifier:kPinnedCellIdentifier];
collectionView.layer.cornerRadius = kPinnedViewCornerRadius;
collectionView.translatesAutoresizingMaskIntoConstraints = NO;
collectionView.delegate = self;
collectionView.dragDelegate = self;
collectionView.dropDelegate = self;
collectionView.dragInteractionEnabled = YES;
collectionView.showsHorizontalScrollIndicator = NO;
collectionView.accessibilityIdentifier = kPinnedViewIdentifier;
self.view = collectionView;
UIView* backgroundView;
// Only apply the blur if transparency effects are not disabled.
if (!UIAccessibilityIsReduceTransparencyEnabled()) {
_backgroundColor = [UIColor clearColor];
UIBlurEffect* blurEffect =
[UIBlurEffect effectWithStyle:UIBlurEffectStyleSystemThinMaterialDark];
backgroundView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
} else {
_backgroundColor = [UIColor colorNamed:kPrimaryBackgroundColor];
backgroundView = [[UIView alloc] init];
}
backgroundView.frame = collectionView.bounds;
backgroundView.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
collectionView.backgroundView = backgroundView;
collectionView.backgroundColor = _backgroundColor;
_dragEnabledConstraint = [collectionView.heightAnchor
constraintEqualToConstant:kPinnedViewDragEnabledHeight];
_defaultConstraint = [collectionView.heightAnchor
constraintEqualToConstant:kPinnedViewDefaultHeight];
_defaultConstraint.active = YES;
}
// Configures `dropOverlayView`.
- (void)configureDropOverlayView {
_dropOverlayView = [[UIView alloc] init];
_dropOverlayView.translatesAutoresizingMaskIntoConstraints = NO;
_dropOverlayView.backgroundColor =
[UIColor colorNamed:kPrimaryBackgroundColor];
[self.view addSubview:_dropOverlayView];
UILabel* label = [[UILabel alloc] init];
label.numberOfLines = 0;
label.textAlignment = NSTextAlignmentCenter;
label.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
label.adjustsFontForContentSizeCategory = YES;
label.adjustsFontSizeToFitWidth = YES;
label.textColor = [UIColor colorNamed:kTextPrimaryColor];
label.text = l10n_util::GetNSString(IDS_IOS_PINNED_TABS_DRAG_TO_PIN_LABEL);
label.translatesAutoresizingMaskIntoConstraints = NO;
// Mirror the label for RTL (see crbug.com/1426256).
if (base::i18n::IsRTL()) {
label.transform = CGAffineTransformScale(label.transform, -1, 1);
}
[_dropOverlayView addSubview:label];
AddSameConstraints(_dropOverlayView, self.collectionView.backgroundView);
[NSLayoutConstraint activateConstraints:@[
[label.centerYAnchor
constraintEqualToAnchor:_dropOverlayView.centerYAnchor],
[label.centerXAnchor
constraintEqualToAnchor:_dropOverlayView.centerXAnchor],
[label.leadingAnchor
constraintGreaterThanOrEqualToAnchor:_dropOverlayView.leadingAnchor
constant:kPinnedViewHorizontalPadding],
[label.trailingAnchor
constraintLessThanOrEqualToAnchor:_dropOverlayView.trailingAnchor
constant:-kPinnedViewHorizontalPadding],
[label.bottomAnchor constraintEqualToAnchor:_dropOverlayView.bottomAnchor],
[label.topAnchor constraintEqualToAnchor:_dropOverlayView.topAnchor],
]];
[self updateDropOverlayViewVisibility];
}
// Configures `cell`'s identifier and title synchronously, and favicon and
// snapshot asynchronously from `item`.
- (void)configureCell:(PinnedCell*)cell withItem:(TabSwitcherItem*)item {
if (item) {
cell.itemIdentifier = item.identifier;
cell.title = item.title;
[item fetchFavicon:^(TabSwitcherItem* innerItem, UIImage* icon) {
// Only update the icon if the cell is not already reused for another
// item.
if ([cell hasIdentifier:innerItem.identifier]) {
cell.icon = icon;
}
}];
[item fetchSnapshot:^(TabSwitcherItem* innerItem, UIImage* snapshot) {
// Only update the icon if the cell is not already reused for another
// item.
if ([cell hasIdentifier:innerItem.identifier]) {
cell.snapshot = snapshot;
}
}];
}
cell.accessibilityIdentifier =
[NSString stringWithFormat:@"%@%ld", kPinnedCellIdentifier,
[self indexOfItemWithID:cell.itemIdentifier]];
if (item.showsActivity) {
[cell showActivityIndicator];
} else {
[cell hideActivityIndicator];
}
if (_contentAppeared && cell.itemIdentifier == _lastInsertedItemID) {
cell.hidden = YES;
}
}
// Returns the index in `_items` of the first item whose identifier is
// `identifier`.
- (NSUInteger)indexOfItemWithID:(NSString*)identifier {
// Check that identifier exists and not empty.
if (identifier.length == 0) {
return NSNotFound;
}
auto selectedTest =
^BOOL(TabSwitcherItem* item, NSUInteger index, BOOL* stop) {
return [item.identifier isEqualToString:identifier];
};
return [_items indexOfObjectPassingTest:selectedTest];
}
// Updates the pinned tabs view visibility after an animation.
- (void)updatePinnedTabsVisibilityAfterAnimation {
if (!_visible) {
self.view.hidden = YES;
}
// Don't call the delegate if the pinned view is hidden after a tab grid page
// change.
if (!_visible && _items.count > 0) {
return;
}
if (_visible && _items.count == 1) {
[self popLastInsertedItem];
}
}
// Shows `_dropOverlayView` when a external drag action is in progress.
- (void)updateDropOverlayViewVisibility {
BOOL visible = _dragSessionEnabled && !_localDragActionInProgress;
_dropOverlayView.alpha = visible ? 1 : 0;
}
// Updates the collection view after an item insertion.
- (void)updateCollectionViewAfterItemInsertion {
[self deselectAllCollectionViewItemsAnimated:NO];
[self selectCollectionViewItemWithID:_selectedItemID animated:NO];
// Scroll the collection view to the newly added item, so it doesn't
// disappear from the user's sight.
[self scrollCollectionViewToLastItemAnimated:YES];
[self updatePinnedTabsVisibility];
}
// Updates the collection view after an item deletion.
- (void)updateCollectionViewAfterItemDeletion {
if (_items.count > 0) {
[self deselectAllCollectionViewItemsAnimated:NO];
[self selectCollectionViewItemWithID:_selectedItemID animated:NO];
} else {
[self pinnedTabsAvailable:_available];
}
}
// Updates the collection view after moving an item to the given `index`.
- (void)updateCollectionViewAfterMovingItemToIndex:(NSUInteger)index {
// Bring back selected halo only for the moved cell, which lost it during
// the move (drag & drop).
if (self.selectedIndex != index) {
[self scrollCollectionViewToItemWithIndex:index animated:YES];
return;
}
// Force reload of the selected cell now to avoid extra delay for the
// blue halo to appear.
[UIView
animateWithDuration:kPinnedViewMoveAnimationTime
animations:^{
[self.collectionView reloadItemsAtIndexPaths:@[
CreateIndexPath(self.selectedIndex)
]];
[self deselectAllCollectionViewItemsAnimated:NO];
[self selectCollectionViewItemWithID:self->_selectedItemID
animated:NO];
}
completion:nil];
}
// Tells the delegate that the user tapped the item with identifier
// corresponding to `indexPath`.
- (void)tappedItemAtIndexPath:(NSIndexPath*)indexPath {
// Do not track item taps during tab grid transitions.
if (!_contentAppeared) {
return;
}
NSUInteger index = base::checked_cast<NSUInteger>(indexPath.item);
DCHECK_LT(index, _items.count);
NSString* itemID = _items[index].identifier;
[self.delegate pinnedTabsViewController:self didSelectItemWithID:itemID];
}
// Resets view backgrounds.
- (void)resetViewBackgrounds {
_dropOverlayView.backgroundColor =
[UIColor colorNamed:kPrimaryBackgroundColor];
self.collectionView.backgroundColor = _backgroundColor;
self.collectionView.backgroundView.hidden = NO;
}
// Selects the collection view's item with `itemID`.
- (void)selectCollectionViewItemWithID:(NSString*)itemID
animated:(BOOL)animated {
NSUInteger itemIndex = [self indexOfItemWithID:itemID];
// Check `itemIndex` boundaries in order to filter out possible race
// conditions while mutating the collection.
if (itemIndex == NSNotFound || itemIndex >= _items.count) {
return;
}
NSIndexPath* itemIndexPath = CreateIndexPath(itemIndex);
[self.collectionView
selectItemAtIndexPath:itemIndexPath
animated:animated
scrollPosition:UICollectionViewScrollPositionNone];
}
// Deselects all the collection view items.
- (void)deselectAllCollectionViewItemsAnimated:(BOOL)animated {
NSArray<NSIndexPath*>* indexPathsForSelectedItems =
[self.collectionView indexPathsForSelectedItems];
for (NSIndexPath* itemIndexPath in indexPathsForSelectedItems) {
[self.collectionView deselectItemAtIndexPath:itemIndexPath
animated:animated];
}
}
// Scrolls the collection view to the currently selected item.
- (void)scrollCollectionViewToSelectedItemAnimated:(BOOL)animated {
[self scrollCollectionViewToItemWithIndex:self.selectedIndex
animated:animated];
}
// Scrolls the collection view to the last item.
- (void)scrollCollectionViewToLastItemAnimated:(BOOL)animated {
[self scrollCollectionViewToItemWithIndex:_items.count - 1 animated:animated];
}
// Scrolls the collection view to the item with specified `itemIndex`.
- (void)scrollCollectionViewToItemWithIndex:(NSUInteger)itemIndex
animated:(BOOL)animated {
// Check `itemIndex` boundaries in order to filter out possible race
// conditions while mutating the collection.
if (itemIndex == NSNotFound || itemIndex >= _items.count) {
return;
}
NSIndexPath* itemIndexPath = CreateIndexPath(itemIndex);
[self.collectionView
scrollToItemAtIndexPath:itemIndexPath
atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally
animated:animated];
}
@end