chromium/ios/chrome/browser/ui/popup_menu/public/popup_menu_table_view_controller.mm

// Copyright 2018 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/popup_menu/public/popup_menu_table_view_controller.h"

#import "base/ios/ios_util.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "ios/chrome/browser/shared/ui/table_view/legacy_chrome_table_view_styler.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/popup_menu/popup_menu_metrics_handler.h"
#import "ios/chrome/browser/ui/popup_menu/public/cells/popup_menu_footer_item.h"
#import "ios/chrome/browser/ui/popup_menu/public/cells/popup_menu_item.h"
#import "ios/chrome/browser/ui/popup_menu/public/popup_menu_table_view_controller_delegate.h"
#import "ios/chrome/browser/ui/popup_menu/public/popup_menu_ui_constants.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/pointer_interaction_util.h"

namespace {
const CGFloat kFooterHeight = 21;
const CGFloat kPopupMenuVerticalInsets = 7;
const CGFloat kScrollIndicatorVerticalInsets = 11;
}  // namespace

@interface PopupMenuTableViewController ()
// Whether the -viewDidAppear: callback has been called.
@property(nonatomic, assign) BOOL viewDidAppear;
// A cached copy of `self.view.bounds`, made during calls to
// viewDidLayoutSubviews, and used to reduce the number of calls to
// calculatePreferredContentSize. which can be an expensive operation.
@property(nonatomic, assign) CGRect cachedBounds;
// Tracks reusable cells in memory, which has an upper limit. This is used to
// ensure that pointer interaction is added only once to a cell.
@property(nonatomic, strong) NSHashTable<UITableViewCell*>* cellsInMemory;
@end

@implementation PopupMenuTableViewController

@dynamic tableViewModel;
@synthesize baseViewController = _baseViewController;
@synthesize delegate = _delegate;
@synthesize itemToHighlight = _itemToHighlight;
@synthesize viewDidAppear = _viewDidAppear;

- (instancetype)init {
  self = [super initWithStyle:UITableViewStyleGrouped];
  if (self) {
    self.cellsInMemory = [NSHashTable<UITableViewCell*> weakObjectsHashTable];
    self.cachedBounds = CGRectZero;
  }
  return self;
}

#pragma mark - PopupMenuConsumer

- (void)setItemToHighlight:(TableViewItem<PopupMenuItem>*)itemToHighlight {
  DCHECK_GT(self.tableViewModel.numberOfSections, 0L);
  _itemToHighlight = itemToHighlight;
  if (itemToHighlight && self.viewDidAppear) {
    [self highlightItem:itemToHighlight repeat:YES];
  }
}

- (void)setPopupMenuItems:
    (NSArray<NSArray<TableViewItem<PopupMenuItem>*>*>*)items {
  [super loadModel];
  for (NSUInteger section = 0; section < items.count; section++) {
    NSInteger sectionIdentifier = kSectionIdentifierEnumZero + section;
    [self.tableViewModel addSectionWithIdentifier:sectionIdentifier];
    for (TableViewItem<PopupMenuItem>* item in items[section]) {
      [self.tableViewModel addItem:item
           toSectionWithIdentifier:sectionIdentifier];
    }

    if (section != items.count - 1) {
      // Add a footer for all sections except the last one.
      TableViewHeaderFooterItem* footer =
          [[PopupMenuFooterItem alloc] initWithType:kItemTypeEnumZero];
      [self.tableViewModel setFooter:footer
            forSectionWithIdentifier:sectionIdentifier];
    }
  }
  [self.tableView reloadData];
  self.preferredContentSize = [self calculatePreferredContentSize];
}

- (void)itemsHaveChanged:(NSArray<TableViewItem<PopupMenuItem>*>*)items {
  [self reconfigureCellsForItems:items];
}

#pragma mark - UIViewController

- (void)viewDidLoad {
  self.styler.tableViewBackgroundColor = nil;
  self.styler.cellBackgroundColor = [UIColor colorNamed:kBackgroundColor];
  [super viewDidLoad];
  self.tableView.contentInset = UIEdgeInsetsMake(kPopupMenuVerticalInsets, 0,
                                                 kPopupMenuVerticalInsets, 0);
  self.tableView.scrollIndicatorInsets = UIEdgeInsetsMake(
      kScrollIndicatorVerticalInsets, 0, kScrollIndicatorVerticalInsets, 0);
  self.tableView.rowHeight = 0;
  self.tableView.sectionHeaderHeight = 0;
  self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
  // Adding a tableHeaderView is needed to prevent a wide inset on top of the
  // collection.
  self.tableView.tableHeaderView = [[UIView alloc]
      initWithFrame:CGRectMake(0.0f, 0.0f, self.tableView.bounds.size.width,
                               0.01f)];

  self.view.layer.cornerRadius = kPopupMenuCornerRadius;
  self.view.layer.masksToBounds = YES;
}

- (void)viewDidAppear:(BOOL)animated {
  [super viewDidAppear:animated];
  self.viewDidAppear = YES;
  if (self.itemToHighlight) {
    [self highlightItem:self.itemToHighlight repeat:YES];
  }
}

- (void)viewDidLayoutSubviews {
  [super viewDidLayoutSubviews];

  // -calculatePreferredContentSize is a somewhat expensive operation and may
  // be responsible for some system hangs. Optimize away some calls by checking
  // if bounds changed first. See crbug.com/1257151 for more context.
  if (!CGRectEqualToRect(self.cachedBounds, self.view.bounds)) {
    self.preferredContentSize = [self calculatePreferredContentSize];
    self.cachedBounds = self.view.bounds;
  }
}

- (CGSize)calculatePreferredContentSize {
  CGFloat width = 0;
  CGFloat height = 0;
  for (NSInteger section = 0; section < [self.tableViewModel numberOfSections];
       section++) {
    NSInteger sectionIdentifier =
        [self.tableViewModel sectionIdentifierForSectionIndex:section];
    for (TableViewItem<PopupMenuItem>* item in
         [self.tableViewModel itemsInSectionWithIdentifier:sectionIdentifier]) {
      CGSize sizeForCell = [item cellSizeForWidth:self.view.bounds.size.width];
      width = MAX(width, ceil(sizeForCell.width));
      height += sizeForCell.height;
    }
    // Add the separator height (only available the non-final sections).
    height += [self tableView:self.tableView heightForFooterInSection:section];
  }
  height +=
      self.tableView.contentInset.top + self.tableView.contentInset.bottom;
  return CGSizeMake(width, ceil(height));
}

#pragma mark - UIScrollViewDelegate

- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
  [self.metricsHandler popupMenuScrolledVertically];
}

#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView*)tableView
    didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
  [self.metricsHandler popupMenuTookAction];
  UIView* cell = [self.tableView cellForRowAtIndexPath:indexPath];
  CGPoint center = [cell convertPoint:cell.center toView:nil];
  [self.delegate popupMenuTableViewController:self
                                didSelectItem:[self.tableViewModel
                                                  itemAtIndexPath:indexPath]
                                       origin:center];
}

- (CGFloat)tableView:(UITableView*)tableView
    heightForFooterInSection:(NSInteger)section {
  if (section == self.tableViewModel.numberOfSections - 1)
    return 0;
  return kFooterHeight;
}

- (CGFloat)tableView:(UITableView*)tableView
    heightForRowAtIndexPath:(NSIndexPath*)indexPath {
  TableViewItem<PopupMenuItem>* item =
      [self.tableViewModel itemAtIndexPath:indexPath];
  return [item cellSizeForWidth:self.view.bounds.size.width].height;
}

#pragma mark - UITableViewDataSource

- (UITableViewCell*)tableView:(UITableView*)tableView
        cellForRowAtIndexPath:(NSIndexPath*)indexPath {
  UITableViewCell* cell = [super tableView:tableView
                     cellForRowAtIndexPath:indexPath];
  if (![self.cellsInMemory containsObject:cell]) {
    [cell addInteraction:[[ViewPointerInteraction alloc] init]];
    [self.cellsInMemory addObject:cell];
  }
  return cell;
}

#pragma mark - Private

// Returns the index path identifying the the row at the position `point`.
// `point` must be in the window coordinates. Returns nil if `point` is outside
// the bounds of the table view.
- (NSIndexPath*)indexPathForInnerRowAtPoint:(CGPoint)point {
  CGPoint pointInTableViewCoordinates = [self.tableView convertPoint:point
                                                            fromView:nil];
  CGRect insetRect =
      CGRectInset(self.tableView.bounds, 0, kPopupMenuVerticalInsets);
  BOOL pointInTableViewBounds =
      CGRectContainsPoint(insetRect, pointInTableViewCoordinates);

  NSIndexPath* indexPath = nil;
  if (pointInTableViewBounds) {
    indexPath =
        [self.tableView indexPathForRowAtPoint:pointInTableViewCoordinates];
  }

  return indexPath;
}

// Highlights the `item` and `repeat` the highlighting once.
- (void)highlightItem:(TableViewItem<PopupMenuItem>*)item repeat:(BOOL)repeat {
  NSIndexPath* indexPath = [self.tableViewModel indexPathForItem:item];
  [self.tableView selectRowAtIndexPath:indexPath
                              animated:YES
                        scrollPosition:UITableViewScrollPositionNone];
  dispatch_after(
      dispatch_time(DISPATCH_TIME_NOW,
                    (int64_t)(kHighlightAnimationDuration * NSEC_PER_SEC)),
      dispatch_get_main_queue(), ^{
        [self unhighlightItem:item repeat:repeat];
      });
}

// Removes the highlight from `item` and `repeat` the highlighting once.
- (void)unhighlightItem:(TableViewItem<PopupMenuItem>*)item
                 repeat:(BOOL)repeat {
  NSIndexPath* indexPath = [self.tableViewModel indexPathForItem:item];
  [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
  if (!repeat)
    return;

  dispatch_after(
      dispatch_time(DISPATCH_TIME_NOW,
                    (int64_t)(kHighlightAnimationDuration * NSEC_PER_SEC)),
      dispatch_get_main_queue(), ^{
        [self highlightItem:item repeat:NO];
      });
}

@end