1// Copyright 2018 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "ios/chrome/browser/ui/popup_menu/public/popup_menu_table_view_controller.h" 6 7#include "base/feature_list.h" 8#include "base/ios/ios_util.h" 9#include "base/metrics/user_metrics.h" 10#include "base/metrics/user_metrics_action.h" 11#import "ios/chrome/browser/ui/popup_menu/public/cells/popup_menu_footer_item.h" 12#import "ios/chrome/browser/ui/popup_menu/public/cells/popup_menu_item.h" 13#import "ios/chrome/browser/ui/popup_menu/public/popup_menu_table_view_controller_delegate.h" 14#import "ios/chrome/browser/ui/popup_menu/public/popup_menu_ui_constants.h" 15#import "ios/chrome/browser/ui/table_view/chrome_table_view_styler.h" 16#include "ios/chrome/browser/ui/ui_feature_flags.h" 17#import "ios/chrome/browser/ui/util/uikit_ui_util.h" 18#import "ios/chrome/common/ui/util/pointer_interaction_util.h" 19 20#if !defined(__has_feature) || !__has_feature(objc_arc) 21#error "This file requires ARC support." 22#endif 23 24namespace { 25const CGFloat kFooterHeight = 21; 26const CGFloat kPopupMenuVerticalInsets = 7; 27const CGFloat kScrollIndicatorVerticalInsets = 11; 28} // namespace 29 30@interface PopupMenuTableViewController () 31// Whether the -viewDidAppear: callback has been called. 32@property(nonatomic, assign) BOOL viewDidAppear; 33#if defined(__IPHONE_13_4) 34// Tracks reusable cells in memory, which has an upper limit. This is used to 35// ensure that pointer interaction is added only once to a cell. 36@property(nonatomic, strong) 37 NSHashTable<UITableViewCell*>* cellsInMemory API_AVAILABLE(ios(13.4)); 38#endif // defined(__IPHONE_13_4) 39@end 40 41@implementation PopupMenuTableViewController 42 43@dynamic tableViewModel; 44@synthesize baseViewController = _baseViewController; 45@synthesize delegate = _delegate; 46@synthesize itemToHighlight = _itemToHighlight; 47@synthesize viewDidAppear = _viewDidAppear; 48 49- (instancetype)init { 50 self = [super initWithStyle:UITableViewStyleGrouped]; 51 if (self) { 52#if defined(__IPHONE_13_4) 53 if (@available(iOS 13.4, *)) { 54 if (base::FeatureList::IsEnabled(kPointerSupport)) { 55 self.cellsInMemory = 56 [NSHashTable<UITableViewCell*> weakObjectsHashTable]; 57 } 58 } 59#endif // defined(__IPHONE_13_4) 60 } 61 return self; 62} 63 64- (void)selectRowAtPoint:(CGPoint)point { 65 NSIndexPath* rowIndexPath = [self indexPathForInnerRowAtPoint:point]; 66 if (!rowIndexPath) 67 return; 68 69 UITableViewCell* cell = [self.tableView cellForRowAtIndexPath:rowIndexPath]; 70 if (!cell.userInteractionEnabled) 71 return; 72 73 base::RecordAction(base::UserMetricsAction("MobilePopupMenuSwipeToSelect")); 74 [self.delegate popupMenuTableViewController:self 75 didSelectItem:[self.tableViewModel 76 itemAtIndexPath:rowIndexPath] 77 origin:[cell convertPoint:cell.center 78 toView:nil]]; 79} 80 81- (void)focusRowAtPoint:(CGPoint)point { 82 NSIndexPath* rowIndexPath = [self indexPathForInnerRowAtPoint:point]; 83 84 BOOL rowAlreadySelected = NO; 85 NSArray<NSIndexPath*>* selectedRows = 86 [self.tableView indexPathsForSelectedRows]; 87 for (NSIndexPath* selectedIndexPath in selectedRows) { 88 if (selectedIndexPath == rowIndexPath) { 89 rowAlreadySelected = YES; 90 continue; 91 } 92 [self.tableView deselectRowAtIndexPath:selectedIndexPath animated:NO]; 93 } 94 95 if (!rowAlreadySelected && rowIndexPath) { 96 [self.tableView selectRowAtIndexPath:rowIndexPath 97 animated:NO 98 scrollPosition:UITableViewScrollPositionNone]; 99 TriggerHapticFeedbackForSelectionChange(); 100 } 101} 102 103#pragma mark - PopupMenuConsumer 104 105- (void)setItemToHighlight:(TableViewItem<PopupMenuItem>*)itemToHighlight { 106 DCHECK_GT(self.tableViewModel.numberOfSections, 0L); 107 _itemToHighlight = itemToHighlight; 108 if (itemToHighlight && self.viewDidAppear) { 109 [self highlightItem:itemToHighlight repeat:YES]; 110 } 111} 112 113- (void)setPopupMenuItems: 114 (NSArray<NSArray<TableViewItem<PopupMenuItem>*>*>*)items { 115 [super loadModel]; 116 for (NSUInteger section = 0; section < items.count; section++) { 117 NSInteger sectionIdentifier = kSectionIdentifierEnumZero + section; 118 [self.tableViewModel addSectionWithIdentifier:sectionIdentifier]; 119 for (TableViewItem<PopupMenuItem>* item in items[section]) { 120 [self.tableViewModel addItem:item 121 toSectionWithIdentifier:sectionIdentifier]; 122 } 123 124 if (section != items.count - 1) { 125 // Add a footer for all sections except the last one. 126 TableViewHeaderFooterItem* footer = 127 [[PopupMenuFooterItem alloc] initWithType:kItemTypeEnumZero]; 128 [self.tableViewModel setFooter:footer 129 forSectionWithIdentifier:sectionIdentifier]; 130 } 131 } 132 [self.tableView reloadData]; 133 self.preferredContentSize = [self calculatePreferredContentSize]; 134} 135 136- (void)itemsHaveChanged:(NSArray<TableViewItem<PopupMenuItem>*>*)items { 137 [self reconfigureCellsForItems:items]; 138} 139 140#pragma mark - UIViewController 141 142- (void)viewDidLoad { 143 self.styler.tableViewBackgroundColor = nil; 144 [super viewDidLoad]; 145 self.tableView.contentInset = UIEdgeInsetsMake(kPopupMenuVerticalInsets, 0, 146 kPopupMenuVerticalInsets, 0); 147 self.tableView.scrollIndicatorInsets = UIEdgeInsetsMake( 148 kScrollIndicatorVerticalInsets, 0, kScrollIndicatorVerticalInsets, 0); 149 self.tableView.rowHeight = 0; 150 self.tableView.sectionHeaderHeight = 0; 151 self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; 152 // Adding a tableHeaderView is needed to prevent a wide inset on top of the 153 // collection. 154 self.tableView.tableHeaderView = [[UIView alloc] 155 initWithFrame:CGRectMake(0.0f, 0.0f, self.tableView.bounds.size.width, 156 0.01f)]; 157 158 self.view.layer.cornerRadius = kPopupMenuCornerRadius; 159 self.view.layer.masksToBounds = YES; 160} 161 162- (void)viewDidAppear:(BOOL)animated { 163 [super viewDidAppear:animated]; 164 self.viewDidAppear = YES; 165 if (self.itemToHighlight) { 166 [self highlightItem:self.itemToHighlight repeat:YES]; 167 } 168} 169 170- (void)viewDidLayoutSubviews { 171 [super viewDidLayoutSubviews]; 172 173 self.preferredContentSize = [self calculatePreferredContentSize]; 174} 175 176- (CGSize)calculatePreferredContentSize { 177 CGFloat width = 0; 178 CGFloat height = 0; 179 for (NSInteger section = 0; section < [self.tableViewModel numberOfSections]; 180 section++) { 181 NSInteger sectionIdentifier = 182 [self.tableViewModel sectionIdentifierForSection:section]; 183 for (TableViewItem<PopupMenuItem>* item in 184 [self.tableViewModel itemsInSectionWithIdentifier:sectionIdentifier]) { 185 CGSize sizeForCell = [item cellSizeForWidth:self.view.bounds.size.width]; 186 width = MAX(width, ceil(sizeForCell.width)); 187 height += sizeForCell.height; 188 } 189 // Add the separator height (only available the non-final sections). 190 height += [self tableView:self.tableView heightForFooterInSection:section]; 191 } 192 height += 193 self.tableView.contentInset.top + self.tableView.contentInset.bottom; 194 return CGSizeMake(width, ceil(height)); 195} 196 197#pragma mark - UITableViewDelegate 198 199- (void)tableView:(UITableView*)tableView 200 didSelectRowAtIndexPath:(NSIndexPath*)indexPath { 201 UIView* cell = [self.tableView cellForRowAtIndexPath:indexPath]; 202 CGPoint center = [cell convertPoint:cell.center toView:nil]; 203 [self.delegate popupMenuTableViewController:self 204 didSelectItem:[self.tableViewModel 205 itemAtIndexPath:indexPath] 206 origin:center]; 207} 208 209- (CGFloat)tableView:(UITableView*)tableView 210 heightForFooterInSection:(NSInteger)section { 211 if (section == self.tableViewModel.numberOfSections - 1) 212 return 0; 213 return kFooterHeight; 214} 215 216- (CGFloat)tableView:(UITableView*)tableView 217 heightForRowAtIndexPath:(NSIndexPath*)indexPath { 218 TableViewItem<PopupMenuItem>* item = 219 [self.tableViewModel itemAtIndexPath:indexPath]; 220 return [item cellSizeForWidth:self.view.bounds.size.width].height; 221} 222 223#pragma mark - UITableViewDataSource 224 225- (UITableViewCell*)tableView:(UITableView*)tableView 226 cellForRowAtIndexPath:(NSIndexPath*)indexPath { 227 UITableViewCell* cell = [super tableView:tableView 228 cellForRowAtIndexPath:indexPath]; 229#if defined(__IPHONE_13_4) 230 if (@available(iOS 13.4, *)) { 231 if (base::FeatureList::IsEnabled(kPointerSupport)) { 232 if (![self.cellsInMemory containsObject:cell]) { 233 [cell addInteraction:[[ViewPointerInteraction alloc] init]]; 234 [self.cellsInMemory addObject:cell]; 235 } 236 } 237 } 238#endif // defined(__IPHONE_13_4) 239 return cell; 240} 241 242#pragma mark - Private 243 244// Returns the index path identifying the the row at the position |point|. 245// |point| must be in the window coordinates. Returns nil if |point| is outside 246// the bounds of the table view. 247- (NSIndexPath*)indexPathForInnerRowAtPoint:(CGPoint)point { 248 CGPoint pointInTableViewCoordinates = [self.tableView convertPoint:point 249 fromView:nil]; 250 CGRect insetRect = 251 CGRectInset(self.tableView.bounds, 0, kPopupMenuVerticalInsets); 252 BOOL pointInTableViewBounds = 253 CGRectContainsPoint(insetRect, pointInTableViewCoordinates); 254 255 NSIndexPath* indexPath = nil; 256 if (pointInTableViewBounds) { 257 indexPath = 258 [self.tableView indexPathForRowAtPoint:pointInTableViewCoordinates]; 259 } 260 261 return indexPath; 262} 263 264// Highlights the |item| and |repeat| the highlighting once. 265- (void)highlightItem:(TableViewItem<PopupMenuItem>*)item repeat:(BOOL)repeat { 266 NSIndexPath* indexPath = [self.tableViewModel indexPathForItem:item]; 267 [self.tableView selectRowAtIndexPath:indexPath 268 animated:YES 269 scrollPosition:UITableViewScrollPositionNone]; 270 dispatch_after( 271 dispatch_time(DISPATCH_TIME_NOW, 272 (int64_t)(kHighlightAnimationDuration * NSEC_PER_SEC)), 273 dispatch_get_main_queue(), ^{ 274 [self unhighlightItem:item repeat:repeat]; 275 }); 276} 277 278// Removes the highlight from |item| and |repeat| the highlighting once. 279- (void)unhighlightItem:(TableViewItem<PopupMenuItem>*)item 280 repeat:(BOOL)repeat { 281 NSIndexPath* indexPath = [self.tableViewModel indexPathForItem:item]; 282 [self.tableView deselectRowAtIndexPath:indexPath animated:YES]; 283 if (!repeat) 284 return; 285 286 dispatch_after( 287 dispatch_time(DISPATCH_TIME_NOW, 288 (int64_t)(kHighlightAnimationDuration * NSEC_PER_SEC)), 289 dispatch_get_main_queue(), ^{ 290 [self highlightItem:item repeat:NO]; 291 }); 292} 293 294@end 295