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