blob: dae90753accdd5be4159fdcdbba6fd42f975b9b3 [file] [log] [blame]
// Copyright (c) 2019 The Chromium Authors. All rights reserved.
// 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/omnibox/popup/omnibox_popup_legacy_view_controller.h"
#import "ios/chrome/browser/ui/omnibox/popup/omnibox_popup_base_view_controller+internal.h"
#include <memory>
#include "base/ios/ios_util.h"
#include "base/metrics/histogram_macros.h"
#import "ios/chrome/browser/ui/elements/fade_truncating_label.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_constants.h"
#import "ios/chrome/browser/ui/omnibox/popup/image_retriever.h"
#import "ios/chrome/browser/ui/omnibox/popup/omnibox_popup_row.h"
#import "ios/chrome/browser/ui/omnibox/popup/self_sizing_table_view.h"
#import "ios/chrome/browser/ui/toolbar/buttons/toolbar_configuration.h"
#include "ios/chrome/browser/ui/util/animation_util.h"
#include "ios/chrome/browser/ui/util/rtl_geometry.h"
#include "ios/chrome/browser/ui/util/ui_util.h"
#import "ios/chrome/browser/ui/util/uikit_ui_util.h"
#include "ios/chrome/common/ui_util/constraints_ui_util.h"
#include "ios/chrome/grit/ios_strings.h"
#include "ios/chrome/grit/ios_theme_resources.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
const int kRowCount = 12;
const CGFloat kRowHeight = 48.0;
const CGFloat kAnswerRowHeight = 64.0;
} // namespace
@interface OmniboxPopupLegacyViewController () <
OmniboxPopupRowAccessibilityDelegate> {
// Array containing the OmniboxPopupRow objects displayed in the view.
NSArray* _rows;
}
// A flag to track if since the last viewWillAppear, the view ever adopted a
// non-zero size. This is a pretty sad workaround for the new iOS 13 behaviour
// where the half-autolayout, half-manual layout code of this legacy class ends
// up sizing cells to a zero width because -layoutRows is never called on the
// first appearance. This should be removed, together with this class, when the
// non-legacy OmniboxPopupViewController becomes the default.
@property(nonatomic, assign) BOOL viewHadNonZeroWidth;
@end
@implementation OmniboxPopupLegacyViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Cache fonts needed for omnibox attributed string.
NSMutableArray* rowsBuilder = [[NSMutableArray alloc] init];
for (int i = 0; i < kRowCount; i++) {
OmniboxPopupRow* row =
[[OmniboxPopupRow alloc] initWithIncognito:self.incognito];
row.accessibilityIdentifier =
[NSString stringWithFormat:@"omnibox suggestion %i", i];
row.autoresizingMask = UIViewAutoresizingFlexibleWidth;
[rowsBuilder addObject:row];
[row.trailingButton addTarget:self
action:@selector(trailingButtonTapped:)
forControlEvents:UIControlEventTouchUpInside];
[row.trailingButton setTag:i];
row.rowNumber = i;
row.delegate = self;
row.rowHeight = kRowHeight;
}
_rows = [rowsBuilder copy];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
if (![self isViewLoaded]) {
_rows = nil;
}
}
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
[self layoutRows];
}
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:
(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[coordinator
animateAlongsideTransition:^(
id<UIViewControllerTransitionCoordinatorContext> context) {
[self layoutRows];
}
completion:nil];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (self.view.bounds.size.width == 0) {
self.viewHadNonZeroWidth = NO;
}
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
// This method will be called multiple times, including after the self-sizing
// table view will have taken its final (non-zero) size. Calling -layoutRows
// will result in another viewDidLayoutSubviews call, so a flag is necessary
// to avoid an infinite loop.
if (self.view.bounds.size.width > 0 && !self.viewHadNonZeroWidth) {
self.viewHadNonZeroWidth = YES;
[self layoutRows];
}
}
#pragma mark -
#pragma mark Updating data and UI
- (void)updateRow:(OmniboxPopupRow*)row
withMatch:(id<AutocompleteSuggestion>)match {
CGFloat kTextCellLeadingPadding =
[self showsLeadingIcons] ? ([self useRegularWidthOffset] ? 192 : 100)
: 16;
kTextCellLeadingPadding = [self showsLeadingIcons] ? 221 : 24;
const CGFloat kTextCellTopPadding = 6;
const CGFloat kDetailCellTopPadding = 26;
const CGFloat kTextLabelHeight = 24;
const CGFloat kTextDetailLabelHeight = 22;
const CGFloat kTrailingButtonWidth = 40;
const CGFloat kAnswerLabelHeight = 36;
const CGFloat kAnswerImageWidth = 30;
const CGFloat kAnswerImageLeftPadding = -1;
const CGFloat kAnswerImageRightPadding = 4;
const CGFloat kAnswerImageTopPadding = 2;
const BOOL alignmentRight = self.alignment == NSTextAlignmentRight;
BOOL LTRTextInRTLLayout =
self.alignment == NSTextAlignmentLeft && UseRTLLayout();
row.rowHeight = match.hasAnswer ? kAnswerRowHeight : kRowHeight;
// Fetch the answer image if specified. Currently, no answer types specify an
// image on the first line so for now we only look at the second line.
if (match.hasImage) {
[self.imageRetriever fetchImage:match.imageURL
completion:^(UIImage* image) {
row.answerImageView.image = image;
}];
// Answers in suggest do not support RTL, left align only.
CGFloat imageLeftPadding =
kTextCellLeadingPadding + kAnswerImageLeftPadding;
if (alignmentRight) {
imageLeftPadding =
row.frame.size.width - (kAnswerImageWidth + kTrailingButtonWidth);
}
CGFloat imageTopPadding = kDetailCellTopPadding + kAnswerImageTopPadding;
row.answerImageView.frame =
CGRectMake(imageLeftPadding, imageTopPadding, kAnswerImageWidth,
kAnswerImageWidth);
row.answerImageView.hidden = NO;
} else {
row.answerImageView.hidden = YES;
}
// DetailTextLabel and textLabel are fading labels placed in each row. The
// textLabel is laid out above the detailTextLabel, and vertically centered
// if the detailTextLabel is empty.
// For the detail text label, we use either the regular detail label, which
// truncates by fading, or the answer label, which uses UILabel's standard
// truncation by ellipse for the multi-line text sometimes shown in answers.
row.detailTruncatingLabel.hidden = match.hasAnswer;
row.detailAnswerLabel.hidden = !match.hasAnswer;
// URLs have have special layout requirements that need to be invoked here.
row.detailTruncatingLabel.displayAsURL = match.isURL;
// TODO(crbug.com/697647): The complexity of managing these two separate
// labels could probably be encapusulated in the row class if we moved the
// layout logic there.
UILabel* detailTextLabel =
match.hasAnswer ? row.detailAnswerLabel : row.detailTruncatingLabel;
[detailTextLabel setTextAlignment:self.alignment];
// The width must be positive for CGContextRef to be valid.
UIEdgeInsets safeAreaInsets = self.view.safeAreaInsets;
CGRect rowBounds = UIEdgeInsetsInsetRect(self.view.bounds, safeAreaInsets);
CGFloat labelWidth =
MAX(40, floorf(rowBounds.size.width) - kTextCellLeadingPadding);
CGFloat labelHeight =
match.hasAnswer ? kAnswerLabelHeight : kTextDetailLabelHeight;
CGFloat answerImagePadding = kAnswerImageWidth + kAnswerImageRightPadding;
CGFloat leadingPadding =
(match.hasImage && !alignmentRight ? answerImagePadding : 0) +
kTextCellLeadingPadding;
LayoutRect detailTextLabelLayout =
LayoutRectMake(leadingPadding, CGRectGetWidth(rowBounds),
kDetailCellTopPadding, labelWidth, labelHeight);
detailTextLabel.frame = LayoutRectGetRect(detailTextLabelLayout);
detailTextLabel.attributedText = match.detailText;
// Set detail text label number of lines
if (match.hasAnswer) {
detailTextLabel.numberOfLines = match.numberOfLines;
}
[detailTextLabel setNeedsDisplay];
FadeTruncatingLabel* textLabel = row.textTruncatingLabel;
[textLabel setTextAlignment:self.alignment];
LayoutRect textLabelLayout =
LayoutRectMake(kTextCellLeadingPadding, CGRectGetWidth(rowBounds), 0,
labelWidth, kTextLabelHeight);
textLabel.frame = LayoutRectGetRect(textLabelLayout);
// Set the text.
textLabel.attributedText = match.text;
// Center the textLabel if detailLabel is empty.
if (!match.hasAnswer && [match.detailText length] == 0) {
textLabel.center = CGPointMake(textLabel.center.x, floor(kRowHeight / 2));
textLabel.frame = AlignRectToPixel(textLabel.frame);
} else {
CGRect frame = textLabel.frame;
frame.origin.y = kTextCellTopPadding;
textLabel.frame = frame;
}
// The leading image (e.g. magnifying glass, star, clock) is only shown on
// iPad.
if ([self showsLeadingIcons]) {
UIImage* image = nil;
image = match.suggestionTypeIcon;
DCHECK(image);
[row updateLeadingImage:image];
}
row.tabMatch = match.isTabMatch;
// Show append button for search history/search suggestions as the right
// control element (aka an accessory element of a table view cell).
BOOL hasVisibleTrailingButton = match.isAppendable || match.isTabMatch;
row.trailingButton.hidden = !hasVisibleTrailingButton;
[row.trailingButton cancelTrackingWithEvent:nil];
// If a right accessory element is present or the text alignment is right
// aligned, adjust the width to align with the accessory element.
if (hasVisibleTrailingButton || alignmentRight) {
LayoutRect layout =
LayoutRectForRectInBoundingRect(textLabel.frame, self.view.frame);
layout.size.width -= kTrailingButtonWidth;
textLabel.frame = LayoutRectGetRect(layout);
layout =
LayoutRectForRectInBoundingRect(detailTextLabel.frame, self.view.frame);
layout.size.width -=
kTrailingButtonWidth + (match.hasImage ? answerImagePadding : 0);
detailTextLabel.frame = LayoutRectGetRect(layout);
}
// Since it's a common use case to type in a left-to-right URL while the
// device is set to a native RTL language, make sure the left alignment looks
// good by anchoring the leading edge to the left.
if (LTRTextInRTLLayout) {
// This is really a left padding, not a leading padding.
const CGFloat kLTRTextInRTLLayoutLeftPadding =
[self showsLeadingIcons] ? ([self useRegularWidthOffset] ? 176 : 94)
: 94;
CGRect frame = textLabel.frame;
frame.size.width -= kLTRTextInRTLLayoutLeftPadding - frame.origin.x;
frame.origin.x = kLTRTextInRTLLayoutLeftPadding;
textLabel.frame = frame;
frame = detailTextLabel.frame;
frame.size.width -= kLTRTextInRTLLayoutLeftPadding - frame.origin.x;
frame.origin.x = kLTRTextInRTLLayoutLeftPadding;
detailTextLabel.frame = frame;
}
NSString* trailingButtonActionName =
row.tabMatch
? l10n_util::GetNSString(IDS_IOS_OMNIBOX_POPUP_SWITCH_TO_OPEN_TAB)
: l10n_util::GetNSString(IDS_IOS_OMNIBOX_POPUP_APPEND);
UIAccessibilityCustomAction* trailingButtonAction =
[[UIAccessibilityCustomAction alloc]
initWithName:trailingButtonActionName
target:row
selector:@selector(accessibilityTrailingButtonTapped)];
row.accessibilityCustomActions =
hasVisibleTrailingButton ? @[ trailingButtonAction ] : nil;
[textLabel setNeedsDisplay];
}
- (void)updateTableViewWithAnimation:(BOOL)animation {
[self layoutRows];
size_t size = self.currentResult.count;
if (animation && size > 0) {
[self fadeInRows];
}
}
- (void)layoutRows {
size_t size = self.currentResult.count;
[self.tableView reloadData];
for (size_t i = 0; i < kRowCount; i++) {
OmniboxPopupRow* row = _rows[i];
if (i < size) {
[self updateRow:row withMatch:self.currentResult[i]];
row.hidden = NO;
} else {
row.hidden = YES;
}
}
if (IsIPadIdiom())
[self updateContentInsetForKeyboard];
}
- (void)fadeInRows {
[CATransaction begin];
[CATransaction
setAnimationTimingFunction:[CAMediaTimingFunction
functionWithControlPoints:
0:0:0.2:1]];
for (size_t i = 0; i < kRowCount; i++) {
OmniboxPopupRow* row = _rows[i];
CGFloat beginTime = (i + 1) * .05;
CABasicAnimation* transformAnimation =
[CABasicAnimation animationWithKeyPath:@"transform"];
[transformAnimation
setFromValue:[NSValue
valueWithCATransform3D:CATransform3DMakeTranslation(
0, -20, 0)]];
[transformAnimation
setToValue:[NSValue valueWithCATransform3D:CATransform3DIdentity]];
[transformAnimation setDuration:0.5];
[transformAnimation setBeginTime:beginTime];
CAAnimation* fadeAnimation = OpacityAnimationMake(0, 1);
[fadeAnimation setDuration:0.5];
[fadeAnimation setBeginTime:beginTime];
[[row layer]
addAnimation:AnimationGroupMake(@[ transformAnimation, fadeAnimation ])
forKey:@"animateIn"];
}
[CATransaction commit];
}
#pragma mark -
#pragma mark UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
[super scrollViewDidScroll:scrollView];
// TODO(crbug.com/733650): Default to the dragging check once it's been tested
// on trunk.
if (!scrollView.dragging)
return;
for (OmniboxPopupRow* row in _rows) {
row.highlighted = NO;
}
}
#pragma mark -
#pragma mark Table view data source
- (CGFloat)tableView:(UITableView*)tableView
heightForRowAtIndexPath:(NSIndexPath*)indexPath {
DCHECK_EQ(0U, (NSUInteger)indexPath.section);
DCHECK_LT((NSUInteger)indexPath.row, self.currentResult.count);
return ((OmniboxPopupRow*)(_rows[indexPath.row])).rowHeight;
}
// Customize the appearance of table view cells.
- (UITableViewCell*)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath {
DCHECK_EQ(0U, (NSUInteger)indexPath.section);
DCHECK_LT((NSUInteger)indexPath.row, self.currentResult.count);
return _rows[indexPath.row];
}
#pragma mark - private
- (BOOL)showsLeadingIcons {
return IsRegularXRegularSizeClass();
}
- (BOOL)useRegularWidthOffset {
return [self showsLeadingIcons] && !IsCompactWidth();
}
#pragma mark - OmniboxPopupRowAccessibilityDelegate
- (void)accessibilityTrailingButtonTappedOmniboxPopupRow:(OmniboxPopupRow*)row {
[self.delegate autocompleteResultConsumer:self
didTapTrailingButtonForRow:row.rowNumber];
}
@end