chromium/ios/chrome/browser/shared/ui/table_view/cells/table_view_url_item.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/shared/ui/table_view/cells/table_view_url_item.h"

#import "base/apple/foundation_util.h"
#import "base/strings/sys_string_conversions.h"
#import "components/url_formatter/elide_url.h"
#import "ios/chrome/browser/net/model/crurl.h"
#import "ios/chrome/browser/shared/public/features/features.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/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/favicon/favicon_container_view.h"
#import "ios/chrome/common/ui/favicon/favicon_view.h"
#import "ios/chrome/common/ui/table_view/table_view_cells_constants.h"
#import "ios/chrome/common/ui/table_view/table_view_url_cell_favicon_badge_view.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"

namespace {
// Default delimiter to use between the hostname and the supplemental URL text
// if text is specified but not the delimiter.
const char kDefaultSupplementalURLTextDelimiter[] = "•";
// The max number of lines for the cell title label.
const int kMaxNumberOfLinesForCellTitleLabel = 2;
}  // namespace

#pragma mark - TableViewURLItem

@implementation TableViewURLItem

- (instancetype)initWithType:(NSInteger)type {
  self = [super initWithType:type];
  if (self) {
    self.cellClass = [TableViewURLCell class];
  }
  return self;
}

- (void)configureCell:(TableViewCell*)tableCell
           withStyler:(ChromeTableViewStyler*)styler {
  [super configureCell:tableCell withStyler:styler];

  TableViewURLCell* cell =
      base::apple::ObjCCastStrict<TableViewURLCell>(tableCell);
  cell.titleLabel.text = [self titleLabelText];
  cell.URLLabel.text = [self URLLabelText];
  cell.thirdRowLabel.text = self.thirdRowText;
  cell.faviconBadgeView.image = self.badgeImage;
  cell.metadataLabel.text = self.metadata;
  cell.metadataImage.image = self.metadataImage;
  cell.metadataImage.tintColor = self.metadataImageColor;
  cell.cellUniqueIdentifier = self.uniqueIdentifier;
  cell.accessibilityTraits |= UIAccessibilityTraitButton;

  if (styler.cellTitleColor) {
    cell.titleLabel.textColor = styler.cellTitleColor;
  }
  if (self.thirdRowTextColor) {
    cell.thirdRowLabel.textColor = self.thirdRowTextColor;
  }

  [cell configureUILayout];
}

- (NSString*)uniqueIdentifier {
  if (!self.URL) {
    return @"";
  }
  return base::SysUTF8ToNSString(self.URL.gurl.host());
}

#pragma mark Private

// Returns the text to use when configuring a TableViewURLCell's title label.
- (NSString*)titleLabelText {
  if (self.title.length) {
    return self.title;
  }
  if (!self.URL) {
    return @"";
  }
  NSString* hostname = [self displayedURL];
  if (hostname.length) {
    return hostname;
  }
  // Backup in case host returns nothing (e.g. about:blank).
  return base::SysUTF8ToNSString(self.URL.gurl.spec());
}

// Returns the text to use when configuring a TableViewURLCell's URL label.
- (NSString*)URLLabelText {
  // Use detail text instead of the URL if there is one set.
  if (self.detailText) {
    return self.detailText;
  }
  // If there's no title text, the URL is used as the cell title.  Add the
  // supplemental text to the URL label below if it exists.
  if (!self.title.length) {
    return self.supplementalURLText;
  }

  // Append the hostname with the supplemental text.
  if (!self.URL) {
    return @"";
  }

  NSString* hostname = [self displayedURL];
  if (self.supplementalURLText.length) {
    NSString* delimeter =
        self.supplementalURLTextDelimiter.length
            ? self.supplementalURLTextDelimiter
            : base::SysUTF8ToNSString(kDefaultSupplementalURLTextDelimiter);
    return [NSString stringWithFormat:@"%@ %@ %@", hostname, delimeter,
                                      self.supplementalURLText];
  } else {
    return hostname;
  }
}

- (NSString*)displayedURL {
  return base::SysUTF16ToNSString(
      url_formatter::
          FormatUrlForDisplayOmitSchemePathTrivialSubdomainsAndMobilePrefix(
              self.URL.gurl));
}

@end

#pragma mark - TableViewURLCell

@interface TableViewURLCell ()
// If the cell's accessibility label has not been manually set via
// `-setAccessibilityLabel:`, this property will be YES, and
// `-accessibilityLabel` will return a lazily created label based on the
// text values of the UILabel subviews.
@property(nonatomic, assign) BOOL shouldGenerateAccessibilityLabel;
// Container View for the faviconView.
@property(nonatomic, strong) FaviconContainerView* faviconContainerView;
// Activity indicator (spinner) used for indicating an in-flight request related
// to the item represented by this cell.
@property(nonatomic, strong) UIActivityIndicatorView* activityIndicatorView;
@end

@implementation TableViewURLCell {
  // Constraint defining the distance between the title vertical stack view and
  // the metadata image.
  NSLayoutConstraint* _titleMetadataImageSpacingConstraint;
  // Constraint defining the distance between the metadata label and the
  // metadata image views.
  NSLayoutConstraint* _metadataViewsSpacingConstraint;
}

- (instancetype)initWithStyle:(UITableViewCellStyle)style
              reuseIdentifier:(NSString*)reuseIdentifier {
  self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];

  if (self) {
    _faviconContainerView = [[FaviconContainerView alloc] init];

    _faviconBadgeView = [[TableViewURLCellFaviconBadgeView alloc] init];
    _titleLabel = [[UILabel alloc] init];
    _URLLabel = [[UILabel alloc] init];
    _thirdRowLabel = [[UILabel alloc] init];
    _metadataImage = [[UIImageView alloc] init];
    _metadataLabel = [[UILabel alloc] init];

    // Set font sizes using dynamic type.
    _titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
    _titleLabel.adjustsFontForContentSizeCategory = YES;
    // Sometimes a very long url is used as the cell title, so be sure it will
    // not stretch the cell to an unlimited number of lines.
    _titleLabel.numberOfLines = kMaxNumberOfLinesForCellTitleLabel;
    _URLLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote];
    _URLLabel.adjustsFontForContentSizeCategory = YES;
    _URLLabel.textColor = [UIColor colorNamed:kTextSecondaryColor];
    _URLLabel.hidden = YES;
    _URLLabel.numberOfLines = 0;
    _thirdRowLabel.font =
        [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote];
    _thirdRowLabel.adjustsFontForContentSizeCategory = YES;
    _thirdRowLabel.textColor = [UIColor colorNamed:kTextSecondaryColor];
    _thirdRowLabel.numberOfLines = 0;
    _thirdRowLabel.lineBreakMode = NSLineBreakByWordWrapping;
    _thirdRowLabel.hidden = YES;
    _metadataLabel.font =
        [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote];
    _metadataLabel.textColor = [UIColor colorNamed:kTextSecondaryColor];
    _metadataLabel.adjustsFontForContentSizeCategory = YES;
    _metadataLabel.hidden = YES;
    _metadataImage.contentMode = UIViewContentModeCenter;
    _metadataImage.accessibilityIdentifier = kTableViewURLCellMetadataImageID;

    // Use stack views to layout the subviews except for the favicon.
    UIStackView* verticalStack = [[UIStackView alloc]
        initWithArrangedSubviews:@[ _titleLabel, _URLLabel, _thirdRowLabel ]];
    verticalStack.axis = UILayoutConstraintAxisVertical;
    [_metadataLabel setContentHuggingPriority:UILayoutPriorityDefaultHigh
                                      forAxis:UILayoutConstraintAxisHorizontal];
    [_metadataLabel
        setContentCompressionResistancePriority:UILayoutPriorityRequired
                                        forAxis:
                                            UILayoutConstraintAxisHorizontal];
    [_metadataImage setContentHuggingPriority:UILayoutPriorityDefaultHigh
                                      forAxis:UILayoutConstraintAxisHorizontal];
    [_metadataImage
        setContentCompressionResistancePriority:UILayoutPriorityRequired
                                        forAxis:
                                            UILayoutConstraintAxisHorizontal];

    // Horizontal view holds vertical stack view and metadata views.
    UIView* horizontalView = [[UIView alloc] init];
    [horizontalView addSubview:verticalStack];
    [horizontalView addSubview:_metadataImage];
    [horizontalView addSubview:_metadataLabel];

    UIView* contentView = self.contentView;
    _faviconContainerView.translatesAutoresizingMaskIntoConstraints = NO;
    _faviconBadgeView.translatesAutoresizingMaskIntoConstraints = NO;
    horizontalView.translatesAutoresizingMaskIntoConstraints = NO;
    verticalStack.translatesAutoresizingMaskIntoConstraints = NO;
    _metadataImage.translatesAutoresizingMaskIntoConstraints = NO;
    _metadataLabel.translatesAutoresizingMaskIntoConstraints = NO;
    [contentView addSubview:_faviconContainerView];
    [contentView addSubview:_faviconBadgeView];
    [contentView addSubview:horizontalView];

    NSLayoutConstraint* heightConstraint = [self.contentView.heightAnchor
        constraintGreaterThanOrEqualToConstant:kChromeTableViewCellHeight];
    // Don't set the priority to required to avoid clashing with the estimated
    // height.
    heightConstraint.priority = UILayoutPriorityRequired - 1;

    _titleMetadataImageSpacingConstraint = [_metadataImage.leadingAnchor
        constraintEqualToAnchor:verticalStack.trailingAnchor
                       constant:0];
    _metadataViewsSpacingConstraint = [_metadataLabel.leadingAnchor
        constraintEqualToAnchor:_metadataImage.trailingAnchor
                       constant:0];

    [NSLayoutConstraint activateConstraints:@[
      [_faviconContainerView.leadingAnchor
          constraintEqualToAnchor:self.contentView.leadingAnchor
                         constant:kTableViewHorizontalSpacing],
      [_faviconContainerView.centerYAnchor
          constraintEqualToAnchor:self.contentView.centerYAnchor],

      // The favicon badge view is aligned with the top-trailing corner of the
      // favicon container.
      [_faviconBadgeView.centerXAnchor
          constraintEqualToAnchor:_faviconContainerView.trailingAnchor],
      [_faviconBadgeView.centerYAnchor
          constraintEqualToAnchor:_faviconContainerView.topAnchor],

      // The stack view fills the remaining space, has an intrinsic height, and
      // is centered vertically.
      [horizontalView.leadingAnchor
          constraintEqualToAnchor:_faviconContainerView.trailingAnchor
                         constant:kTableViewSubViewHorizontalSpacing],
      [horizontalView.trailingAnchor
          constraintEqualToAnchor:self.contentView.trailingAnchor
                         constant:-kTableViewHorizontalSpacing],
      [horizontalView.centerYAnchor
          constraintEqualToAnchor:self.contentView.centerYAnchor],
      [verticalStack.topAnchor
          constraintEqualToAnchor:horizontalView.topAnchor],
      [verticalStack.bottomAnchor
          constraintEqualToAnchor:horizontalView.bottomAnchor],
      [verticalStack.leadingAnchor
          constraintEqualToAnchor:horizontalView.leadingAnchor],
      [_metadataImage.centerYAnchor
          constraintEqualToAnchor:horizontalView.centerYAnchor],
      [_metadataImage.heightAnchor
          constraintLessThanOrEqualToAnchor:horizontalView.heightAnchor],
      [_metadataLabel.firstBaselineAnchor
          constraintEqualToAnchor:verticalStack.firstBaselineAnchor],
      [_metadataLabel.trailingAnchor
          constraintEqualToAnchor:horizontalView.trailingAnchor],
      [_metadataLabel.heightAnchor
          constraintLessThanOrEqualToAnchor:horizontalView.heightAnchor],
      [horizontalView.topAnchor
          constraintGreaterThanOrEqualToAnchor:self.contentView.topAnchor
                                      constant:
                                          kTableViewTwoLabelsCellVerticalSpacing],
      [horizontalView.bottomAnchor
          constraintLessThanOrEqualToAnchor:self.contentView.bottomAnchor
                                   constant:
                                       -kTableViewTwoLabelsCellVerticalSpacing],
      _titleMetadataImageSpacingConstraint, _metadataViewsSpacingConstraint,
      heightConstraint
    ]];
  }
  return self;
}

- (FaviconView*)faviconView {
  return self.faviconContainerView.faviconView;
}

- (void)setFaviconContainerBackgroundColor:(UIColor*)backgroundColor {
  [self.faviconContainerView setFaviconBackgroundColor:backgroundColor];
}

- (void)setFaviconContainerBorderColor:(UIColor*)borderColor {
  [self.faviconContainerView setFaviconBorderColor:borderColor];
}

// Hide or show the metadata and URL labels depending on the presence of text.
// Align the horizontal stack properly depending on if the metadata label will
// be present or not.
- (void)configureUILayout {
  if ([self.metadataLabel.text length]) {
    self.metadataLabel.hidden = NO;
    _metadataViewsSpacingConstraint.constant =
        kTableViewTwoLabelsCellVerticalSpacing;
  } else {
    self.metadataLabel.hidden = YES;
    _metadataViewsSpacingConstraint.constant = 0;
  }
  if ([self.metadataImage image]) {
    self.metadataImage.hidden = NO;
    _titleMetadataImageSpacingConstraint.constant =
        kTableViewTwoLabelsCellVerticalSpacing;
  } else {
    self.metadataImage.hidden = YES;
    _titleMetadataImageSpacingConstraint.constant = 0;
  }
  if ([self.URLLabel.text length]) {
    self.URLLabel.hidden = NO;
  } else {
    self.URLLabel.hidden = YES;
  }
  if ([self.thirdRowLabel.text length] && !self.URLLabel.hidden) {
    self.thirdRowLabel.hidden = NO;
  } else {
    // There shouldn't be a third row if the second row isn't even shown.
    self.thirdRowLabel.hidden = YES;
  }
}

- (void)prepareForReuse {
  [super prepareForReuse];
  [self.faviconView configureWithAttributes:nil];
  [self setFaviconContainerBackgroundColor:nil];
  [self setFaviconContainerBorderColor:nil];
  self.faviconBadgeView.image = nil;
  self.metadataLabel.hidden = YES;
  self.metadataImage.image = nil;
  self.URLLabel.hidden = YES;
  self.thirdRowLabel.hidden = YES;
}

- (void)setAccessibilityLabel:(NSString*)accessibilityLabel {
  self.shouldGenerateAccessibilityLabel = !accessibilityLabel.length;
  [super setAccessibilityLabel:accessibilityLabel];
}

- (NSString*)accessibilityLabel {
  if (self.shouldGenerateAccessibilityLabel) {
    NSString* accessibilityLabel = self.titleLabel.text;
    if (self.URLLabel.text.length > 0) {
      accessibilityLabel = [NSString
          stringWithFormat:@"%@, %@", accessibilityLabel, self.URLLabel.text];
    }
    if (self.thirdRowLabel.text.length > 0) {
      accessibilityLabel =
          [NSString stringWithFormat:@"%@, %@", accessibilityLabel,
                                     self.thirdRowLabel.text];
    }
    if (self.metadataLabel.text.length > 0) {
      accessibilityLabel =
          [NSString stringWithFormat:@"%@, %@", accessibilityLabel,
                                     self.metadataLabel.text];
    }
    return accessibilityLabel;
  } else {
    return [super accessibilityLabel];
  }
}

- (NSArray<NSString*>*)accessibilityUserInputLabels {
  NSMutableArray<NSString*>* userInputLabels = [[NSMutableArray alloc] init];
  if (self.titleLabel.text) {
    [userInputLabels addObject:self.titleLabel.text];
  }

  return userInputLabels;
}

- (NSString*)accessibilityIdentifier {
  return self.titleLabel.text;
}

- (BOOL)isAccessibilityElement {
  return YES;
}

- (void)startAnimatingActivityIndicator {
  // It may be an edge case if the activity indicator is spinning when we don't
  // expect it. But it's okay to leave indicator spinning instead of crashing.
  if (self.activityIndicatorView != nil) {
    return;
  }

  self.activityIndicatorView = [[UIActivityIndicatorView alloc] init];
  UIActivityIndicatorView* activityView = self.activityIndicatorView;
  activityView.translatesAutoresizingMaskIntoConstraints = NO;
  [self.faviconContainerView addSubview:activityView];
  [NSLayoutConstraint activateConstraints:@[
    [activityView.centerXAnchor
        constraintEqualToAnchor:self.faviconContainerView.centerXAnchor],
    [activityView.centerYAnchor
        constraintEqualToAnchor:self.faviconContainerView.centerYAnchor],
  ]];
  [activityView startAnimating];
  activityView.backgroundColor = self.faviconContainerView.backgroundColor;
}

- (void)stopAnimatingActivityIndicator {
  [self.activityIndicatorView stopAnimating];
  [self.activityIndicatorView removeFromSuperview];
  self.activityIndicatorView = nil;
}

@end