| |
| // Copyright 2012 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/tabs/tab_view.h" |
| |
| #include "base/i18n/rtl.h" |
| #include "base/ios/ios_util.h" |
| #include "base/logging.h" |
| |
| #include "base/strings/sys_string_conversions.h" |
| #import "ios/chrome/browser/ui/colors/MDCPalette+CrAdditions.h" |
| #import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h" |
| #import "ios/chrome/browser/ui/commands/generic_chrome_command.h" |
| #import "ios/chrome/browser/ui/image_util.h" |
| #include "ios/chrome/browser/ui/rtl_geometry.h" |
| #include "ios/chrome/browser/ui/ui_util.h" |
| #import "ios/chrome/browser/ui/uikit_ui_util.h" |
| #include "ios/chrome/grit/ios_strings.h" |
| #import "ios/third_party/material_components_ios/src/components/ActivityIndicator/src/MaterialActivityIndicator.h" |
| #import "ios/third_party/material_components_ios/src/components/Typography/src/MaterialTypography.h" |
| #include "third_party/google_toolbox_for_mac/src/iPhone/GTMFadeTruncatingLabel.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/l10n/l10n_util_mac.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/gfx/image/image.h" |
| #import "ui/gfx/ios/uikit_util.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| namespace { |
| |
| // Tab close button insets. |
| const CGFloat kTabCloseTopInset = -1.0; |
| const CGFloat kTabCloseLeftInset = 0.0; |
| const CGFloat kTabCloseBottomInset = 0.0; |
| const CGFloat kTabCloseRightInset = 0.0; |
| const CGFloat kTabBackgroundLeftCapInset = 24.0; |
| const CGFloat kFaviconLeftInset = 23.5; |
| const CGFloat kFaviconVerticalOffset = 2.0; |
| const CGFloat kTabStripLineMargin = 2.5; |
| const CGFloat kTabStripLineHeight = 0.5; |
| const CGFloat kCloseButtonHorizontalShift = 15; |
| const CGFloat kCloseButtonVerticalShift = 4.0; |
| const CGFloat kTitleLeftMargin = 8.0; |
| const CGFloat kTitleRightMargin = 0.0; |
| |
| const CGFloat kCloseButtonSize = 24.0; |
| const CGFloat kFaviconSize = 16.0; |
| } |
| |
| @interface TabView () { |
| // Close button for this tab. |
| UIButton* _closeButton; |
| |
| // View that draws the tab title. |
| GTMFadeTruncatingLabel* _titleLabel; |
| |
| // Background image for this tab. |
| UIImageView* _backgroundImageView; |
| // This view is used to draw a separator line at the bottom of the tab view. |
| // This view is hidden when the tab view is in a selected state. |
| UIView* _lineSeparator; |
| BOOL _incognitoStyle; |
| |
| // Set to YES when the layout constraints have been initialized. |
| BOOL _layoutConstraintsInitialized; |
| |
| // Image view used to draw the favicon and spinner. |
| UIImageView* _faviconView; |
| |
| // If |YES|, this view will adjust its appearance and draw as a collapsed tab. |
| BOOL _collapsed; |
| |
| MDCActivityIndicator* _activityIndicator; |
| } |
| @end |
| |
| @interface TabView (Private) |
| |
| // Creates the close button, favicon button, and title. |
| - (void)createButtonsAndLabel; |
| |
| // Updates this tab's line separator color based on the current incognito style. |
| - (void)updateLineSeparator; |
| |
| // Updates this tab's background image based on the value of |selected|. |
| - (void)updateBackgroundImage:(BOOL)selected; |
| |
| // Updates this tab's close button image based on the current incognito style. |
| - (void)updateCloseButtonImages; |
| |
| // Return the default favicon image based on the current incognito style. |
| - (UIImage*)defaultFaviconImage; |
| |
| // Returns the rect in which to draw the favicon. |
| - (CGRect)faviconRectForBounds:(CGRect)bounds; |
| |
| // Returns the rect in which to draw the tab title. |
| - (CGRect)titleRectForBounds:(CGRect)bounds; |
| |
| // Returns the frame rect for the close button. |
| - (CGRect)closeRectForBounds:(CGRect)bounds; |
| |
| @end |
| |
| @implementation TabView |
| |
| @synthesize closeButton = _closeButton; |
| @synthesize titleLabel = _titleLabel; |
| @synthesize collapsed = _collapsed; |
| @synthesize background = background_; |
| @synthesize incognitoStyle = _incognitoStyle; |
| |
| - (id)initWithEmptyView:(BOOL)emptyView selected:(BOOL)selected { |
| if ((self = [super initWithFrame:CGRectZero])) { |
| [self setOpaque:NO]; |
| [self createCommonViews]; |
| // -setSelected only calls -updateBackgroundImage if the selected state |
| // changes. |isSelected| defaults to NO, so if |selected| is also NO, |
| // -updateBackgroundImage needs to be called explicitly. |
| [self setSelected:selected]; |
| [self updateLineSeparator]; |
| [self updateBackgroundImage:selected]; |
| if (!emptyView) |
| [self createButtonsAndLabel]; |
| } |
| return self; |
| } |
| |
| - (void)setSelected:(BOOL)selected { |
| BOOL wasSelected = [self isSelected]; |
| [super setSelected:selected]; |
| |
| [_lineSeparator setHidden:selected]; |
| |
| if (selected != wasSelected) |
| [self updateBackgroundImage:selected]; |
| |
| // It would make more sense to set active/inactive on tab_view itself, but |
| // tab_view is not an an accessible element, and making it one would add |
| // several complicated layers to UIA. Instead, simply set active/inactive |
| // here to be used by UIA. |
| [_closeButton setAccessibilityValue:(selected ? @"active" : @"inactive")]; |
| } |
| |
| - (void)setCollapsed:(BOOL)collapsed { |
| if (_collapsed != collapsed) |
| [_closeButton setHidden:collapsed]; |
| |
| _collapsed = collapsed; |
| } |
| |
| - (void)setTitle:(NSString*)title { |
| if ([_titleLabel.text isEqualToString:title]) |
| return; |
| if (base::i18n::GetStringDirection(base::SysNSStringToUTF16(title)) == |
| base::i18n::RIGHT_TO_LEFT) { |
| [_titleLabel setTruncateMode:GTMFadeTruncatingHead]; |
| } else { |
| [_titleLabel setTruncateMode:GTMFadeTruncatingTail]; |
| } |
| _titleLabel.text = title; |
| } |
| |
| - (UIImage*)favicon { |
| return [_faviconView image]; |
| } |
| |
| - (void)setFavicon:(UIImage*)favicon { |
| if (!favicon) |
| favicon = [self defaultFaviconImage]; |
| [_faviconView setImage:favicon]; |
| } |
| |
| - (void)setIncognitoStyle:(BOOL)incognitoStyle { |
| _incognitoStyle = incognitoStyle; |
| _titleLabel.textColor = |
| incognitoStyle ? [UIColor whiteColor] : [UIColor blackColor]; |
| [_faviconView setImage:[self defaultFaviconImage]]; |
| [self updateLineSeparator]; |
| [self updateCloseButtonImages]; |
| [self updateBackgroundImage:[self isSelected]]; |
| } |
| |
| - (void)startProgressSpinner { |
| [_activityIndicator startAnimating]; |
| [_activityIndicator setHidden:NO]; |
| [_faviconView setHidden:YES]; |
| } |
| |
| - (void)stopProgressSpinner { |
| [_activityIndicator stopAnimating]; |
| [_activityIndicator setHidden:YES]; |
| [_faviconView setHidden:NO]; |
| } |
| |
| #pragma mark - UIView overrides |
| |
| - (void)setFrame:(CGRect)frame { |
| const CGRect previousFrame = [self frame]; |
| [super setFrame:frame]; |
| // We are checking for a zero frame before triggering constraints updates in |
| // order to prevent computation of constraints that will never be used for the |
| // final layout. We could also initialize with a dummy frame but first this is |
| // inefficient and second it's non trivial to compute the minimum valid frame |
| // in regard to tweakable constants. |
| if (CGRectEqualToRect(CGRectZero, previousFrame) && |
| !_layoutConstraintsInitialized) { |
| [self setNeedsUpdateConstraints]; |
| } |
| } |
| |
| - (void)updateConstraints { |
| [super updateConstraints]; |
| if (!_layoutConstraintsInitialized && |
| !CGRectEqualToRect(CGRectZero, self.frame)) { |
| _layoutConstraintsInitialized = YES; |
| [self addCommonConstraints]; |
| // Add buttons and labels constraints if needed. |
| if (_closeButton) |
| [self addButtonsAndLabelConstraints]; |
| } |
| } |
| |
| - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { |
| // Account for the trapezoidal shape of the tab. Inset the tab bounds by |
| // (y = -2.2x + 56), determined empirically from looking at the tab background |
| // images. |
| CGFloat inset = MAX(0.0, (point.y - 56) / -2.2); |
| return CGRectContainsPoint(CGRectInset([self bounds], inset, 0), point); |
| } |
| |
| #pragma mark - Private |
| |
| - (void)createCommonViews { |
| _backgroundImageView = [[UIImageView alloc] init]; |
| [_backgroundImageView setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| [self addSubview:_backgroundImageView]; |
| |
| _lineSeparator = [[UIView alloc] initWithFrame:CGRectZero]; |
| [_lineSeparator setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| [self addSubview:_lineSeparator]; |
| } |
| |
| - (void)addCommonConstraints { |
| NSDictionary* commonViewsDictionary = @{ |
| @"backgroundImageView" : _backgroundImageView, |
| @"lineSeparator" : _lineSeparator |
| }; |
| NSArray* commonConstraints = @[ |
| @"H:|-0-[backgroundImageView]-0-|", |
| @"V:|-0-[backgroundImageView]-0-|", |
| @"H:|-tabStripLineMargin-[lineSeparator]-tabStripLineMargin-|", |
| @"V:[lineSeparator(==tabStripLineHeight)]-0-|", |
| ]; |
| NSDictionary* commonMetrics = @{ |
| @"tabStripLineMargin" : @(kTabStripLineMargin), |
| @"tabStripLineHeight" : @(kTabStripLineHeight) |
| }; |
| ApplyVisualConstraintsWithMetrics(commonConstraints, commonViewsDictionary, |
| commonMetrics, self); |
| } |
| |
| - (void)createButtonsAndLabel { |
| _closeButton = [UIButton buttonWithType:UIButtonTypeCustom]; |
| [_closeButton setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| [_closeButton setImage:[UIImage imageNamed:@"tabstrip_tab_close"] |
| forState:UIControlStateNormal]; |
| [_closeButton setImage:[UIImage imageNamed:@"tabstrip_tab_close_pressed"] |
| forState:UIControlStateHighlighted]; |
| [_closeButton setContentEdgeInsets:UIEdgeInsetsMake(kTabCloseTopInset, |
| kTabCloseLeftInset, |
| kTabCloseBottomInset, |
| kTabCloseRightInset)]; |
| [_closeButton setAccessibilityLabel:l10n_util::GetNSString( |
| IDS_IOS_TOOLS_MENU_CLOSE_TAB)]; |
| [self addSubview:_closeButton]; |
| |
| // Add fade truncating label. |
| _titleLabel = [[GTMFadeTruncatingLabel alloc] initWithFrame:CGRectZero]; |
| [_titleLabel setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| [_titleLabel setFont:[MDCTypography body1Font]]; |
| // Setting NSLineBreakByCharWrapping fixes an issue where the beginning of the |
| // text is truncated for RTL text writing direction. Anyway since the label is |
| // only one line and the end of the text is faded behind a gradient mask, it |
| // is visually almost equivalent to NSLineBreakByClipping. |
| [_titleLabel setLineBreakMode:NSLineBreakByCharWrapping]; |
| |
| [_titleLabel setTextAlignment:NSTextAlignmentNatural]; |
| [self addSubview:_titleLabel]; |
| |
| CGRect faviconFrame = CGRectMake(0, 0, kFaviconSize, kFaviconSize); |
| _faviconView = [[UIImageView alloc] initWithFrame:faviconFrame]; |
| [_faviconView setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| [_faviconView setContentMode:UIViewContentModeScaleAspectFit]; |
| [_faviconView setImage:[self defaultFaviconImage]]; |
| [_faviconView setAccessibilityIdentifier:@"Favicon"]; |
| [self addSubview:_faviconView]; |
| |
| _activityIndicator = |
| [[MDCActivityIndicator alloc] initWithFrame:faviconFrame]; |
| [_activityIndicator setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| [_activityIndicator |
| setCycleColors:@[ [[MDCPalette cr_bluePalette] tint500] ]]; |
| [_activityIndicator setRadius:ui::AlignValueToUpperPixel(kFaviconSize / 2)]; |
| [self addSubview:_activityIndicator]; |
| } |
| |
| - (void)addButtonsAndLabelConstraints { |
| // Constraints on the Top bar, snapshot view, and shadow view. |
| NSDictionary* viewsDictionary = @{ |
| @"close" : _closeButton, |
| @"title" : _titleLabel, |
| @"favicon" : _faviconView, |
| }; |
| NSArray* constraints = @[ |
| @"H:|-faviconLeftInset-[favicon(faviconSize)]", |
| @"V:|-faviconVerticalOffset-[favicon]-0-|", |
| @"H:[close(==closeButtonSize)]-closeButtonHorizontalShift-|", |
| @"V:|-closeButtonVerticalShift-[close]-0-|", |
| @"H:[favicon]-titleLeftMargin-[title]-titleRightMargin-[close]", |
| @"V:[title(==titleHeight)]", |
| ]; |
| NSDictionary* metrics = @{ |
| @"closeButtonSize" : @(kCloseButtonSize), |
| @"closeButtonHorizontalShift" : @(kCloseButtonHorizontalShift), |
| @"closeButtonVerticalShift" : @(kCloseButtonVerticalShift), |
| @"titleLeftMargin" : @(kTitleLeftMargin), |
| @"titleRightMargin" : @(kTitleRightMargin), |
| @"titleHeight" : @(kFaviconSize), |
| @"faviconLeftInset" : @(AlignValueToPixel(kFaviconLeftInset)), |
| @"faviconVerticalOffset" : @(kFaviconVerticalOffset), |
| @"faviconSize" : @(kFaviconSize), |
| }; |
| ApplyVisualConstraintsWithMetrics(constraints, viewsDictionary, metrics, |
| self); |
| AddSameCenterXConstraint(self, _faviconView, _activityIndicator); |
| AddSameCenterYConstraint(self, _faviconView, _activityIndicator); |
| AddSameCenterYConstraint(self, _faviconView, _titleLabel); |
| } |
| |
| - (void)updateLineSeparator { |
| UIColor* separatorColor = |
| _incognitoStyle ? [UIColor colorWithWhite:36 / 255.0 alpha:1.0] |
| : [UIColor colorWithWhite:185 / 255.0 alpha:1.0]; |
| [_lineSeparator setBackgroundColor:separatorColor]; |
| } |
| |
| - (void)updateBackgroundImage:(BOOL)selected { |
| NSString* state = (selected ? @"foreground" : @"background"); |
| NSString* incognito = _incognitoStyle ? @"incognito_" : @""; |
| NSString* imageName = |
| [NSString stringWithFormat:@"tabstrip_%@%@_tab", incognito, state]; |
| UIImage* backgroundImage = StretchableImageFromUIImage( |
| [UIImage imageNamed:imageName], kTabBackgroundLeftCapInset, 0); |
| [_backgroundImageView setImage:backgroundImage]; |
| } |
| |
| - (void)updateCloseButtonImages { |
| UIImage* normalImage = |
| self.incognitoStyle ? [UIImage imageNamed:@"tabstrip_tab_close_incognito"] |
| : [UIImage imageNamed:@"tabstrip_tab_close"]; |
| UIImage* pressedImage = |
| self.incognitoStyle |
| ? [UIImage imageNamed:@"tabstrip_tab_close_incognito_pressed"] |
| : [UIImage imageNamed:@"tabstrip_tab_close_pressed"]; |
| [_closeButton setImage:normalImage forState:UIControlStateNormal]; |
| [_closeButton setImage:pressedImage forState:UIControlStateHighlighted]; |
| } |
| |
| - (UIImage*)defaultFaviconImage { |
| return self.incognitoStyle ? [UIImage imageNamed:@"default_favicon_incognito"] |
| : [UIImage imageNamed:@"default_favicon"]; |
| } |
| |
| @end |