// 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_detail_icon_item.h"
#import "base/check.h"
#import "base/notreached.h"
#import "ios/chrome/browser/shared/ui/elements/new_feature_badge_view.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/settings/cells/settings_cells_constants.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/table_view/table_view_cells_constants.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util_mac.h"
namespace {
// Proportion of Cell's textLabel/detailTextLabel. This guarantees that the
// textLabel occupies 75% of the row space and detailTextLabel occupies 25%.
constexpr CGFloat kCellLabelsWidthProportion = 3.f;
// Minimum cell height when the cell has 2 lines.
constexpr CGFloat kChromeTableViewTwoLinesCellHeight = 58.f;
// kDotSize represents the size of the dot (i.e. its height and width).
constexpr CGFloat kDotSize = 10.f;
// kMarginAroundDot represents the amount of space before and after the badge,
// between itself and the surrounding UILabels `text` and `detailText`.
constexpr CGFloat kMarginAroundBadge = 5.0;
// The size of the "new" IPH badge.
constexpr CGFloat kNewIPHBadgeSize = 20.0;
// The font size of the "N" in the "new" IPH badge.
constexpr CGFloat kNewIPHBadgeFontSize = 10.0;
// kDefaultTextLabelSpacing represents the default spacing between the text
// labels when no dot is present.
constexpr CGFloat kDefaultTextLabelSpacing = 4;
// By default, the maximum number of lines to be displayed for the detail text
// should be one.
const NSInteger kDefaultDetailTextNumberOfLines = 1;
// The extra vertical spacing of the icon when it's top aligned with the text
// labels.
constexpr CGFloat kIconTopAlignmentVerticalSpacing = 2.0;
// Returns the notification dot view for `TableViewDetailIconCell`.
UIView* NotificationDotView() {
UIView* notificationDotUIView = [[UIView alloc] init];
notificationDotUIView.translatesAutoresizingMaskIntoConstraints = NO;
UIView* dotUIView = [[UIView alloc] init];
dotUIView.translatesAutoresizingMaskIntoConstraints = NO;
dotUIView.layer.cornerRadius = kDotSize / 2;
dotUIView.backgroundColor = [UIColor colorNamed:kBlue600Color];
[notificationDotUIView addSubview:dotUIView];
[NSLayoutConstraint activateConstraints:@[
[notificationDotUIView.widthAnchor
constraintGreaterThanOrEqualToConstant:kDotSize],
[dotUIView.widthAnchor constraintEqualToConstant:kDotSize],
[dotUIView.heightAnchor constraintEqualToConstant:kDotSize],
[dotUIView.leadingAnchor
constraintEqualToAnchor:notificationDotUIView.leadingAnchor],
[dotUIView.centerYAnchor
constraintEqualToAnchor:notificationDotUIView.centerYAnchor],
]];
return notificationDotUIView;
}
// Returns the "new" ("N") IPH badge view for `TableViewDetailIconCell`.
NewFeatureBadgeView* NewIPHBadgeView() {
NewFeatureBadgeView* newIPHBadgeView =
[[NewFeatureBadgeView alloc] initWithBadgeSize:kNewIPHBadgeSize
fontSize:kNewIPHBadgeFontSize];
newIPHBadgeView.translatesAutoresizingMaskIntoConstraints = NO;
return newIPHBadgeView;
}
} // namespace
@implementation TableViewDetailIconItem
- (instancetype)initWithType:(NSInteger)type {
self = [super initWithType:type];
if (self) {
self.cellClass = [TableViewDetailIconCell class];
self.badgeType = BadgeType::kNone;
_detailTextNumberOfLines = kDefaultDetailTextNumberOfLines;
_iconCenteredVertically = YES;
}
return self;
}
#pragma mark TableViewItem
- (void)configureCell:(TableViewDetailIconCell*)cell
withStyler:(ChromeTableViewStyler*)styler {
[super configureCell:cell withStyler:styler];
cell.textLabel.text = self.text;
[cell setDetailText:self.detailText];
[cell setIconImage:self.iconImage
tintColor:self.iconTintColor
backgroundColor:self.iconBackgroundColor
cornerRadius:self.iconCornerRadius];
[cell setTextLayoutConstraintAxis:self.textLayoutConstraintAxis];
[cell setBadgeType:self.badgeType];
[cell setDetailTextNumberOfLines:self.detailTextNumberOfLines];
[cell setIconCenteredVertically:self.iconCenteredVertically];
}
@end
#pragma mark - TableViewDetailIconCell
@interface TableViewDetailIconCell ()
// View containing UILabels `text` and `detailText`.
@property(nonatomic, strong) UIStackView* textStackView;
// Padding layout constraints.
@property(nonatomic, strong)
NSArray<NSLayoutConstraint*>* verticalPaddingConstraints;
// Constraint to set the cell minimum height.
@property(nonatomic, strong) NSLayoutConstraint* minimumCellHeightConstraint;
// Text width constraint between the title and the detail text.
@property(nonatomic, strong) NSLayoutConstraint* textWidthConstraint;
// Detail text. Can be nil if no text is set.
@property(nonatomic, strong) UILabel* detailTextLabel;
@end
@implementation TableViewDetailIconCell {
UIView* _iconBackground;
UIImageView* _iconImageView;
NSLayoutConstraint* _iconHiddenConstraint;
NSLayoutConstraint* _iconVisibleConstraint;
NSLayoutConstraint* _iconCenterAlignment;
NSLayoutConstraint* _iconTopAlignment;
NSLayoutConstraint* _iconBackgroundDefaultWidthConstraint;
NSLayoutConstraint* _iconBackgroundCustomWidthConstraint;
// View representing the current badge view.
UIView* _badgeView;
// Badge type of current badge view.
BadgeType _badgeType;
}
@synthesize detailTextLabel = _detailTextLabel;
@synthesize textLabel = _textLabel;
- (instancetype)initWithStyle:(UITableViewCellStyle)style
reuseIdentifier:(NSString*)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
_detailTextNumberOfLines = kDefaultDetailTextNumberOfLines;
_iconCenteredVertically = YES;
self.isAccessibilityElement = YES;
UIView* contentView = self.contentView;
_iconBackground = [[UIView alloc] init];
_iconBackground.translatesAutoresizingMaskIntoConstraints = NO;
_iconBackground.hidden = YES;
[contentView addSubview:_iconBackground];
_iconImageView = [[UIImageView alloc] init];
_iconImageView.translatesAutoresizingMaskIntoConstraints = NO;
_iconImageView.contentMode = UIViewContentModeCenter;
[_iconBackground addSubview:_iconImageView];
AddSameCenterConstraints(_iconBackground, _iconImageView);
_textLabel = [[UILabel alloc] init];
_textLabel.translatesAutoresizingMaskIntoConstraints = NO;
_textLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
_textLabel.adjustsFontForContentSizeCategory = YES;
_textLabel.textColor = [UIColor colorNamed:kTextPrimaryColor];
_textLabel.backgroundColor = UIColor.clearColor;
_textLabel.numberOfLines = 2;
_textStackView =
[[UIStackView alloc] initWithArrangedSubviews:@[ _textLabel ]];
_textStackView.translatesAutoresizingMaskIntoConstraints = NO;
_textStackView.spacing = kDefaultTextLabelSpacing;
[contentView addSubview:_textStackView];
// Set up the constraints for when the icon is visible and hidden. One of
// these will be active at a time, defaulting to hidden.
_iconHiddenConstraint = [_textStackView.leadingAnchor
constraintEqualToAnchor:contentView.leadingAnchor
constant:kTableViewHorizontalSpacing];
_iconVisibleConstraint = [_textStackView.leadingAnchor
constraintEqualToAnchor:_iconBackground.trailingAnchor
constant:kTableViewImagePadding];
_minimumCellHeightConstraint = [contentView.heightAnchor
constraintGreaterThanOrEqualToConstant:kChromeTableViewCellHeight];
// Lower the priority for transition. The content view has autoresizing mask
// to have the same height than the cell. To avoid breaking the constaints
// while updating the minimum height constant, the constraint has to have
// a lower priority.
_minimumCellHeightConstraint.priority = UILayoutPriorityDefaultHigh - 1;
_minimumCellHeightConstraint.active = YES;
// Set up the constrains for the icon's vertical alignment. One of these
// will be active at the time, defaulting to center alignment.
_iconCenterAlignment = [_iconBackground.centerYAnchor
constraintEqualToAnchor:contentView.centerYAnchor];
_iconTopAlignment = [_iconBackground.topAnchor
constraintEqualToAnchor:_textStackView.topAnchor
constant:kIconTopAlignmentVerticalSpacing];
[self updateIconAlignment];
_iconBackgroundDefaultWidthConstraint = [_iconBackground.widthAnchor
constraintEqualToConstant:kTableViewIconImageSize];
_iconBackgroundDefaultWidthConstraint.active = YES;
[_iconImageView
setContentCompressionResistancePriority:UILayoutPriorityRequired - 1
forAxis:
UILayoutConstraintAxisHorizontal];
_iconBackgroundCustomWidthConstraint = [_iconBackground.widthAnchor
constraintEqualToAnchor:_iconImageView.widthAnchor];
_iconBackgroundCustomWidthConstraint.active = NO;
[NSLayoutConstraint activateConstraints:@[
// Icon.
[_iconBackground.leadingAnchor
constraintEqualToAnchor:contentView.leadingAnchor
constant:kTableViewHorizontalSpacing],
[_iconBackground.heightAnchor
constraintEqualToAnchor:_iconBackground.widthAnchor],
// Text labels.
[_textStackView.trailingAnchor
constraintEqualToAnchor:contentView.trailingAnchor
constant:-kTableViewHorizontalSpacing],
[_textStackView.centerYAnchor
constraintEqualToAnchor:contentView.centerYAnchor],
_iconHiddenConstraint,
// Leading constraint for `customSeparator`.
[self.customSeparator.leadingAnchor
constraintEqualToAnchor:_textStackView.leadingAnchor],
]];
_verticalPaddingConstraints = AddOptionalVerticalPadding(
contentView, _textStackView, kTableViewTwoLabelsCellVerticalSpacing);
[self updateCellForAccessibilityContentSizeCategory:
UIContentSizeCategoryIsAccessibilityCategory(
self.traitCollection.preferredContentSizeCategory)];
}
return self;
}
- (void)setIconImage:(UIImage*)image
tintColor:(UIColor*)tintColor
backgroundColor:(UIColor*)backgroundColor
cornerRadius:(CGFloat)cornerRadius {
if (image == nil && _iconImageView.image == nil) {
return;
}
if (tintColor) {
image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
}
_iconImageView.image = image;
_iconImageView.tintColor = tintColor;
_iconBackground.backgroundColor = backgroundColor;
_iconBackground.layer.cornerRadius = cornerRadius;
BOOL hidden = (image == nil);
_iconBackground.hidden = hidden;
if (hidden) {
_iconVisibleConstraint.active = NO;
_iconHiddenConstraint.active = YES;
} else {
_iconHiddenConstraint.active = NO;
_iconVisibleConstraint.active = YES;
}
}
#pragma mark - Properties
- (void)setIconCenteredVertically:(BOOL)iconCenteredVertically {
_iconCenteredVertically = iconCenteredVertically;
[self updateIconAlignment];
}
- (void)setDetailTextNumberOfLines:(NSInteger)detailTextNumberOfLines {
_detailTextNumberOfLines = detailTextNumberOfLines;
[self updateCellForAccessibilityContentSizeCategory:
UIContentSizeCategoryIsAccessibilityCategory(
self.traitCollection.preferredContentSizeCategory)];
}
- (void)setTextLayoutConstraintAxis:
(UILayoutConstraintAxis)textLayoutConstraintAxis {
CGFloat verticalPaddingConstant = 0;
switch (textLayoutConstraintAxis) {
case UILayoutConstraintAxisVertical:
verticalPaddingConstant = kTableViewTwoLabelsCellVerticalSpacing;
self.minimumCellHeightConstraint.constant =
kChromeTableViewTwoLinesCellHeight;
DCHECK(self.detailTextLabel);
break;
case UILayoutConstraintAxisHorizontal:
verticalPaddingConstant = kTableViewOneLabelCellVerticalSpacing;
self.minimumCellHeightConstraint.constant = kChromeTableViewCellHeight;
break;
}
DCHECK(verticalPaddingConstant);
for (NSLayoutConstraint* constraint in self.verticalPaddingConstraints) {
constraint.constant = verticalPaddingConstant;
}
self.textStackView.axis = textLayoutConstraintAxis;
[self updateCellForAccessibilityContentSizeCategory:
UIContentSizeCategoryIsAccessibilityCategory(
self.traitCollection.preferredContentSizeCategory)];
}
- (UILayoutConstraintAxis)textLayoutConstraintAxis {
return self.textStackView.axis;
}
- (void)setDetailText:(NSString*)detailText {
if (detailText.length > 0) {
if (!self.detailTextLabel) {
[self createDetailTextLabel];
}
self.detailTextLabel.text = detailText;
} else if (self.detailTextLabel) {
[self removeDetailTextLabel];
}
}
- (void)setBadgeType:(BadgeType)badgeType {
if (badgeType == BadgeType::kNone) {
[self removeBadgeView];
} else if (_badgeType != badgeType) {
[self removeBadgeView];
[self addBadgeViewOfType:badgeType];
}
_badgeType = badgeType;
}
- (void)updateIconBackgroundWidthToFitContent:(BOOL)useCustomWidth {
if (useCustomWidth) {
_iconBackgroundDefaultWidthConstraint.active = NO;
_iconBackgroundCustomWidthConstraint.active = YES;
return;
}
_iconBackgroundCustomWidthConstraint.active = NO;
_iconBackgroundDefaultWidthConstraint.active = YES;
}
#pragma mark - UIView
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
BOOL isCurrentCategoryAccessibility =
UIContentSizeCategoryIsAccessibilityCategory(
self.traitCollection.preferredContentSizeCategory);
if (isCurrentCategoryAccessibility !=
UIContentSizeCategoryIsAccessibilityCategory(
previousTraitCollection.preferredContentSizeCategory)) {
[self updateCellForAccessibilityContentSizeCategory:
isCurrentCategoryAccessibility];
}
}
#pragma mark - UITableViewCell
- (void)prepareForReuse {
[super prepareForReuse];
[self setTextLayoutConstraintAxis:UILayoutConstraintAxisHorizontal];
[self setIconImage:nil tintColor:nil backgroundColor:nil cornerRadius:0];
[self setDetailText:nil];
[self setBadgeType:BadgeType::kNone];
[self updateIconBackgroundWidthToFitContent:NO];
}
#pragma mark - Private
- (void)updateIconAlignment {
if (_iconCenteredVertically) {
_iconTopAlignment.active = NO;
_iconCenterAlignment.active = YES;
} else {
_iconCenterAlignment.active = NO;
_iconTopAlignment.active = YES;
}
}
- (void)createDetailTextLabel {
if (self.detailTextLabel) {
return;
}
self.detailTextLabel = [[UILabel alloc] init];
self.detailTextLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.detailTextLabel.font =
[UIFont preferredFontForTextStyle:UIFontTextStyleBody];
self.detailTextLabel.adjustsFontForContentSizeCategory = YES;
self.detailTextLabel.textColor = [UIColor colorNamed:kTextSecondaryColor];
self.detailTextLabel.backgroundColor = UIColor.clearColor;
[self.textStackView addArrangedSubview:self.detailTextLabel];
// In case the two labels don't fit in width, have the `textLabel` be 3
// times the width of the `detailTextLabel` (so 75% / 25%).
self.textWidthConstraint = [self.textLabel.widthAnchor
constraintEqualToAnchor:self.detailTextLabel.widthAnchor
multiplier:kCellLabelsWidthProportion];
// Set low priority to the proportion constraint between `self.textLabel` and
// `self.detailTextLabel`, so that it won't break other layouts.
self.textWidthConstraint.priority = UILayoutPriorityDefaultLow;
[self updateCellForAccessibilityContentSizeCategory:
UIContentSizeCategoryIsAccessibilityCategory(
self.traitCollection.preferredContentSizeCategory)];
}
- (void)removeDetailTextLabel {
if (!self.detailTextLabel) {
return;
}
[self.detailTextLabel removeFromSuperview];
self.detailTextLabel = nil;
self.textWidthConstraint = nil;
self.textLayoutConstraintAxis = UILayoutConstraintAxisHorizontal;
}
// Returns the view corresponding to `badgeType`. Should not be called with
// BadgeType::kNone.
- (UIView*)badgeViewOfType:(BadgeType)badgeType {
switch (badgeType) {
case BadgeType::kNotificationDot:
return NotificationDotView();
case BadgeType::kNew:
return NewIPHBadgeView();
case BadgeType::kNone: {
NOTREACHED_IN_MIGRATION();
return nil;
}
}
}
// Creates the badge of `badgeType` and insert it after the `textLabel`.
- (void)addBadgeViewOfType:(BadgeType)badgeType {
CHECK(badgeType != BadgeType::kNone);
if (_badgeView) {
// Previous badge view shoulb be removed before adding a new type of badge
// view.
CHECK(badgeType == _badgeType);
return;
}
// Since we're inserting the view at index 1, this CHECK checks to make sure
// the main text's UILabel subview is there.
CHECK(self.textStackView.subviews.count > 0);
// Since only the horizontal axis is supported for the badge, make
// sure we currently have the right axis.
CHECK([self textLayoutConstraintAxis] == UILayoutConstraintAxisHorizontal);
// Make sure the badge is always snug next to the main text.
[self.textLabel setContentHuggingPriority:UILayoutPriorityDefaultHigh
forAxis:UILayoutConstraintAxisHorizontal];
_badgeView = [self badgeViewOfType:badgeType];
[self.textStackView insertArrangedSubview:_badgeView atIndex:1];
[self.textStackView setCustomSpacing:kMarginAroundBadge
afterView:self.textLabel];
[self.textStackView setCustomSpacing:kMarginAroundBadge afterView:_badgeView];
[NSLayoutConstraint activateConstraints:@[
[_badgeView.centerYAnchor
constraintEqualToAnchor:self.textLabel.centerYAnchor],
]];
}
// Removes the badge view from the UI.
- (void)removeBadgeView {
if (!_badgeView) {
return;
}
[self.textStackView setCustomSpacing:UIStackViewSpacingUseDefault
afterView:self.textLabel];
[_badgeView removeFromSuperview];
_badgeView = nil;
}
// Updates the cell such as it is layouted correctly with regard to the
// preferred content size category, if it is an
// `accessibilityContentSizeCategory` or not.
- (void)updateCellForAccessibilityContentSizeCategory:
(BOOL)accessibilityContentSizeCategory {
if (accessibilityContentSizeCategory) {
_textWidthConstraint.active = NO;
// detailTextLabel is laid below textLabel with accessibility content size
// category.
_detailTextLabel.textAlignment = NSTextAlignmentNatural;
_detailTextLabel.numberOfLines = 0;
_textLabel.numberOfLines = 0;
} else {
_textWidthConstraint.active = YES;
// detailTextLabel is laid after textLabel and should have a trailing text
// alignment with non-accessibility content size category if in horizontal
// axis layout.
if (_textStackView.axis == UILayoutConstraintAxisHorizontal) {
_detailTextLabel.textAlignment =
self.effectiveUserInterfaceLayoutDirection ==
UIUserInterfaceLayoutDirectionLeftToRight
? NSTextAlignmentRight
: NSTextAlignmentLeft;
_detailTextLabel.numberOfLines = kDefaultDetailTextNumberOfLines;
} else {
_detailTextLabel.textAlignment = NSTextAlignmentNatural;
_detailTextLabel.numberOfLines = _detailTextNumberOfLines;
}
_textLabel.numberOfLines = 2;
}
UIFontTextStyle preferredFont =
_textStackView.axis == UILayoutConstraintAxisVertical
? UIFontTextStyleFootnote
: UIFontTextStyleBody;
_detailTextLabel.font = [UIFont preferredFontForTextStyle:preferredFont];
}
- (NSString*)accessibilityLabel {
if (self.customAccessibilityLabel) {
return self.customAccessibilityLabel;
}
if (_badgeView) {
switch (_badgeType) {
case BadgeType::kNotificationDot:
return [NSString
stringWithFormat:@"%@, %@", self.textLabel.text,
l10n_util::GetNSString(
IDS_IOS_NEW_ITEM_ACCESSIBILITY_HINT)];
case BadgeType::kNew:
return [NSString
stringWithFormat:@"%@, %@", self.textLabel.text,
l10n_util::GetNSString(
IDS_IOS_NEW_FEATURE_ACCESSIBILITY_HINT)];
case BadgeType::kNone:
NOTREACHED_IN_MIGRATION();
break;
}
}
return self.textLabel.text;
}
- (NSString*)accessibilityValue {
if (self.customAccessibilityLabel) {
// If the cell already has an accessibility label for the whole cell, remove
// the one specifically for the detail text, so that information is not
// repeated by voice over.
return @"";
}
return self.detailTextLabel.text;
}
- (NSArray<NSString*>*)accessibilityUserInputLabels {
if (_badgeView) {
switch (_badgeType) {
case BadgeType::kNotificationDot:
return @[ [NSString
stringWithFormat:@"%@, %@", self.textLabel.text,
l10n_util::GetNSString(
IDS_IOS_NEW_ITEM_ACCESSIBILITY_HINT)] ];
case BadgeType::kNew:
return @[ [NSString
stringWithFormat:@"%@, %@", self.textLabel.text,
l10n_util::GetNSString(
IDS_IOS_NEW_FEATURE_ACCESSIBILITY_HINT)] ];
case BadgeType::kNone:
NOTREACHED_IN_MIGRATION();
break;
}
}
return @[ self.textLabel.text ];
}
@end