OLD | NEW |
(Empty) | |
| 1 |
| 2 // Copyright 2012 The Chromium Authors. All rights reserved. |
| 3 // Use of this source code is governed by a BSD-style license that can be |
| 4 // found in the LICENSE file. |
| 5 |
| 6 #import "ios/chrome/browser/ui/tabs/tab_view.h" |
| 7 |
| 8 #include "base/i18n/rtl.h" |
| 9 #include "base/ios/ios_util.h" |
| 10 #include "base/logging.h" |
| 11 #include "base/mac/objc_property_releaser.h" |
| 12 #include "base/strings/sys_string_conversions.h" |
| 13 #import "ios/chrome/browser/ui/colors/MDCPalette+CrAdditions.h" |
| 14 #import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h" |
| 15 #import "ios/chrome/browser/ui/commands/generic_chrome_command.h" |
| 16 #import "ios/chrome/browser/ui/image_util.h" |
| 17 #include "ios/chrome/browser/ui/rtl_geometry.h" |
| 18 #include "ios/chrome/browser/ui/tabs/tab_util.h" |
| 19 #include "ios/chrome/browser/ui/ui_util.h" |
| 20 #import "ios/chrome/browser/ui/uikit_ui_util.h" |
| 21 #include "ios/chrome/grit/ios_strings.h" |
| 22 #import "ios/third_party/material_components_ios/src/components/ActivityIndicato
r/src/MaterialActivityIndicator.h" |
| 23 #import "ios/third_party/material_components_ios/src/components/Typography/src/M
aterialTypography.h" |
| 24 #include "third_party/google_toolbox_for_mac/src/iPhone/GTMFadeTruncatingLabel.h
" |
| 25 #include "ui/base/l10n/l10n_util.h" |
| 26 #include "ui/base/l10n/l10n_util_mac.h" |
| 27 #include "ui/base/resource/resource_bundle.h" |
| 28 #include "ui/gfx/image/image.h" |
| 29 #import "ui/gfx/ios/uikit_util.h" |
| 30 |
| 31 namespace { |
| 32 |
| 33 // Tab close button insets. |
| 34 const CGFloat kTabCloseTopInset = -1.0; |
| 35 const CGFloat kTabCloseLeftInset = 0.0; |
| 36 const CGFloat kTabCloseBottomInset = 0.0; |
| 37 const CGFloat kTabCloseRightInset = 0.0; |
| 38 const CGFloat kTabBackgroundLeftCapInset = 24.0; |
| 39 const CGFloat kFaviconLeftInset = 23.5; |
| 40 const CGFloat kFaviconVerticalOffset = 2.0; |
| 41 const CGFloat kTabStripLineMargin = 2.5; |
| 42 const CGFloat kTabStripLineHeight = 0.5; |
| 43 const CGFloat kCloseButtonHorizontalShift = 15; |
| 44 const CGFloat kCloseButtonVerticalShift = 4.0; |
| 45 const CGFloat kTitleLeftMargin = 8.0; |
| 46 const CGFloat kTitleRightMargin = 0.0; |
| 47 |
| 48 const CGFloat kCloseButtonSize = 24.0; |
| 49 const CGFloat kFaviconSize = 16.0; |
| 50 } |
| 51 |
| 52 @interface TabView () { |
| 53 // Close button for this tab. |
| 54 UIButton* _closeButton; |
| 55 |
| 56 // View that draws the tab title. |
| 57 GTMFadeTruncatingLabel* _titleLabel; |
| 58 |
| 59 // Background image for this tab. |
| 60 base::scoped_nsobject<UIImageView> _backgroundImageView; |
| 61 // This view is used to draw a separator line at the bottom of the tab view. |
| 62 // This view is hidden when the tab view is in a selected state. |
| 63 base::scoped_nsobject<UIView> _lineSeparator; |
| 64 BOOL _incognitoStyle; |
| 65 |
| 66 // Set to YES when the layout constraints have been initialized. |
| 67 BOOL _layoutConstraintsInitialized; |
| 68 |
| 69 // Image view used to draw the favicon and spinner. |
| 70 base::scoped_nsobject<UIImageView> _faviconView; |
| 71 |
| 72 // If |YES|, this view will adjust its appearance and draw as a collapsed tab. |
| 73 BOOL _collapsed; |
| 74 |
| 75 base::scoped_nsobject<MDCActivityIndicator> _activityIndicator; |
| 76 |
| 77 base::mac::ObjCPropertyReleaser _propertyReleaser_TabView; |
| 78 } |
| 79 @end |
| 80 |
| 81 @interface TabView (Private) |
| 82 |
| 83 // Creates the close button, favicon button, and title. |
| 84 - (void)createButtonsAndLabel; |
| 85 |
| 86 // Updates this tab's line separator color based on the current incognito style. |
| 87 - (void)updateLineSeparator; |
| 88 |
| 89 // Updates this tab's background image based on the value of |selected|. |
| 90 - (void)updateBackgroundImage:(BOOL)selected; |
| 91 |
| 92 // Updates this tab's close button image based on the current incognito style. |
| 93 - (void)updateCloseButtonImages; |
| 94 |
| 95 // Return the default favicon image based on the current incognito style. |
| 96 - (UIImage*)defaultFaviconImage; |
| 97 |
| 98 // Returns the rect in which to draw the favicon. |
| 99 - (CGRect)faviconRectForBounds:(CGRect)bounds; |
| 100 |
| 101 // Returns the rect in which to draw the tab title. |
| 102 - (CGRect)titleRectForBounds:(CGRect)bounds; |
| 103 |
| 104 // Returns the frame rect for the close button. |
| 105 - (CGRect)closeRectForBounds:(CGRect)bounds; |
| 106 |
| 107 @end |
| 108 |
| 109 @implementation TabView |
| 110 |
| 111 @synthesize closeButton = _closeButton; |
| 112 @synthesize titleLabel = _titleLabel; |
| 113 @synthesize collapsed = _collapsed; |
| 114 @synthesize background = background_; |
| 115 @synthesize incognitoStyle = _incognitoStyle; |
| 116 |
| 117 - (id)initWithEmptyView:(BOOL)emptyView selected:(BOOL)selected { |
| 118 if ((self = [super initWithFrame:CGRectZero])) { |
| 119 _propertyReleaser_TabView.Init(self, [TabView class]); |
| 120 [self setOpaque:NO]; |
| 121 [self createCommonViews]; |
| 122 // -setSelected only calls -updateBackgroundImage if the selected state |
| 123 // changes. |isSelected| defaults to NO, so if |selected| is also NO, |
| 124 // -updateBackgroundImage needs to be called explicitly. |
| 125 [self setSelected:selected]; |
| 126 [self updateLineSeparator]; |
| 127 [self updateBackgroundImage:selected]; |
| 128 if (!emptyView) |
| 129 [self createButtonsAndLabel]; |
| 130 } |
| 131 return self; |
| 132 } |
| 133 |
| 134 - (void)setSelected:(BOOL)selected { |
| 135 BOOL wasSelected = [self isSelected]; |
| 136 [super setSelected:selected]; |
| 137 |
| 138 [_lineSeparator setHidden:selected]; |
| 139 |
| 140 if (selected != wasSelected) |
| 141 [self updateBackgroundImage:selected]; |
| 142 |
| 143 // It would make more sense to set active/inactive on tab_view itself, but |
| 144 // tab_view is not an an accessible element, and making it one would add |
| 145 // several complicated layers to UIA. Instead, simply set active/inactive |
| 146 // here to be used by UIA. |
| 147 [_closeButton setAccessibilityValue:(selected ? @"active" : @"inactive")]; |
| 148 } |
| 149 |
| 150 - (void)setCollapsed:(BOOL)collapsed { |
| 151 if (_collapsed != collapsed) |
| 152 [_closeButton setHidden:collapsed]; |
| 153 |
| 154 _collapsed = collapsed; |
| 155 } |
| 156 |
| 157 - (void)setTitle:(NSString*)title { |
| 158 if ([_titleLabel.text isEqualToString:title]) |
| 159 return; |
| 160 if (base::i18n::GetStringDirection(base::SysNSStringToUTF16(title)) == |
| 161 base::i18n::RIGHT_TO_LEFT) { |
| 162 [_titleLabel setTruncateMode:GTMFadeTruncatingHead]; |
| 163 } else { |
| 164 [_titleLabel setTruncateMode:GTMFadeTruncatingTail]; |
| 165 } |
| 166 _titleLabel.text = title; |
| 167 } |
| 168 |
| 169 - (UIImage*)favicon { |
| 170 return [_faviconView image]; |
| 171 } |
| 172 |
| 173 - (void)setFavicon:(UIImage*)favicon { |
| 174 if (!favicon) |
| 175 favicon = [self defaultFaviconImage]; |
| 176 [_faviconView setImage:favicon]; |
| 177 } |
| 178 |
| 179 - (void)setIncognitoStyle:(BOOL)incognitoStyle { |
| 180 _incognitoStyle = incognitoStyle; |
| 181 _titleLabel.textColor = |
| 182 incognitoStyle ? [UIColor whiteColor] : [UIColor blackColor]; |
| 183 [_faviconView setImage:[self defaultFaviconImage]]; |
| 184 [self updateLineSeparator]; |
| 185 [self updateCloseButtonImages]; |
| 186 [self updateBackgroundImage:[self isSelected]]; |
| 187 } |
| 188 |
| 189 - (void)startProgressSpinner { |
| 190 [_activityIndicator startAnimating]; |
| 191 [_activityIndicator setHidden:NO]; |
| 192 [_faviconView setHidden:YES]; |
| 193 } |
| 194 |
| 195 - (void)stopProgressSpinner { |
| 196 [_activityIndicator stopAnimating]; |
| 197 [_activityIndicator setHidden:YES]; |
| 198 [_faviconView setHidden:NO]; |
| 199 } |
| 200 |
| 201 #pragma mark - UIView overrides |
| 202 |
| 203 - (void)setFrame:(CGRect)frame { |
| 204 const CGRect previousFrame = [self frame]; |
| 205 [super setFrame:frame]; |
| 206 // We are checking for a zero frame before triggering constraints updates in |
| 207 // order to prevent computation of constraints that will never be used for the |
| 208 // final layout. We could also initialize with a dummy frame but first this is |
| 209 // inefficient and second it's non trivial to compute the minimum valid frame |
| 210 // in regard to tweakable constants. |
| 211 if (CGRectEqualToRect(CGRectZero, previousFrame) && |
| 212 !_layoutConstraintsInitialized) { |
| 213 [self setNeedsUpdateConstraints]; |
| 214 } |
| 215 } |
| 216 |
| 217 - (void)updateConstraints { |
| 218 [super updateConstraints]; |
| 219 if (!_layoutConstraintsInitialized && |
| 220 !CGRectEqualToRect(CGRectZero, self.frame)) { |
| 221 _layoutConstraintsInitialized = YES; |
| 222 [self addCommonConstraints]; |
| 223 // Add buttons and labels constraints if needed. |
| 224 if (_closeButton) |
| 225 [self addButtonsAndLabelConstraints]; |
| 226 } |
| 227 } |
| 228 |
| 229 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { |
| 230 // Account for the trapezoidal shape of the tab. Inset the tab bounds by |
| 231 // (y = -2.2x + 56), determined empirically from looking at the tab background |
| 232 // images. |
| 233 CGFloat inset = MAX(0.0, (point.y - 56) / -2.2); |
| 234 return CGRectContainsPoint(CGRectInset([self bounds], inset, 0), point); |
| 235 } |
| 236 |
| 237 #pragma mark - Private |
| 238 |
| 239 - (void)createCommonViews { |
| 240 _backgroundImageView.reset([[UIImageView alloc] init]); |
| 241 [_backgroundImageView setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| 242 [self addSubview:_backgroundImageView]; |
| 243 |
| 244 _lineSeparator.reset([[UIView alloc] initWithFrame:CGRectZero]); |
| 245 [_lineSeparator setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| 246 [self addSubview:_lineSeparator]; |
| 247 } |
| 248 |
| 249 - (void)addCommonConstraints { |
| 250 NSDictionary* commonViewsDictionary = @{ |
| 251 @"backgroundImageView" : _backgroundImageView.get(), |
| 252 @"lineSeparator" : _lineSeparator.get() |
| 253 }; |
| 254 NSArray* commonConstraints = @[ |
| 255 @"H:|-0-[backgroundImageView]-0-|", |
| 256 @"V:|-0-[backgroundImageView]-0-|", |
| 257 @"H:|-tabStripLineMargin-[lineSeparator]-tabStripLineMargin-|", |
| 258 @"V:[lineSeparator(==tabStripLineHeight)]-0-|", |
| 259 ]; |
| 260 NSDictionary* commonMetrics = @{ |
| 261 @"tabStripLineMargin" : @(kTabStripLineMargin), |
| 262 @"tabStripLineHeight" : @(kTabStripLineHeight) |
| 263 }; |
| 264 ApplyVisualConstraintsWithMetrics(commonConstraints, commonViewsDictionary, |
| 265 commonMetrics, self); |
| 266 } |
| 267 |
| 268 - (void)createButtonsAndLabel { |
| 269 _closeButton = [[UIButton buttonWithType:UIButtonTypeCustom] retain]; |
| 270 [_closeButton setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| 271 [_closeButton setImage:[UIImage imageNamed:@"tabstrip_tab_close"] |
| 272 forState:UIControlStateNormal]; |
| 273 [_closeButton setImage:[UIImage imageNamed:@"tabstrip_tab_close_pressed"] |
| 274 forState:UIControlStateHighlighted]; |
| 275 [_closeButton setContentEdgeInsets:UIEdgeInsetsMake(kTabCloseTopInset, |
| 276 kTabCloseLeftInset, |
| 277 kTabCloseBottomInset, |
| 278 kTabCloseRightInset)]; |
| 279 [_closeButton setAccessibilityLabel:l10n_util::GetNSString( |
| 280 IDS_IOS_TOOLS_MENU_CLOSE_TAB)]; |
| 281 [self addSubview:_closeButton]; |
| 282 |
| 283 // Add fade truncating label. |
| 284 _titleLabel = [[GTMFadeTruncatingLabel alloc] initWithFrame:CGRectZero]; |
| 285 [_titleLabel setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| 286 [_titleLabel setFont:[MDCTypography body1Font]]; |
| 287 // Setting NSLineBreakByCharWrapping fixes an issue where the beginning of the |
| 288 // text is truncated for RTL text writing direction. Anyway since the label is |
| 289 // only one line and the end of the text is faded behind a gradient mask, it |
| 290 // is visually almost equivalent to NSLineBreakByClipping. |
| 291 [_titleLabel setLineBreakMode:NSLineBreakByCharWrapping]; |
| 292 |
| 293 [_titleLabel setTextAlignment:NSTextAlignmentNatural]; |
| 294 [self addSubview:_titleLabel]; |
| 295 |
| 296 CGRect faviconFrame = CGRectMake(0, 0, kFaviconSize, kFaviconSize); |
| 297 _faviconView.reset([[UIImageView alloc] initWithFrame:faviconFrame]); |
| 298 [_faviconView setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| 299 [_faviconView setContentMode:UIViewContentModeScaleAspectFit]; |
| 300 [_faviconView setImage:[self defaultFaviconImage]]; |
| 301 [_faviconView setAccessibilityIdentifier:@"Favicon"]; |
| 302 [self addSubview:_faviconView]; |
| 303 |
| 304 _activityIndicator.reset( |
| 305 [[MDCActivityIndicator alloc] initWithFrame:faviconFrame]); |
| 306 [_activityIndicator setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| 307 [_activityIndicator |
| 308 setCycleColors:@[ [[MDCPalette cr_bluePalette] tint500] ]]; |
| 309 [_activityIndicator setRadius:ui::AlignValueToUpperPixel(kFaviconSize / 2)]; |
| 310 [self addSubview:_activityIndicator]; |
| 311 } |
| 312 |
| 313 - (void)addButtonsAndLabelConstraints { |
| 314 // Constraints on the Top bar, snapshot view, and shadow view. |
| 315 NSDictionary* viewsDictionary = @{ |
| 316 @"close" : _closeButton, |
| 317 @"title" : _titleLabel, |
| 318 @"favicon" : _faviconView, |
| 319 }; |
| 320 NSArray* constraints = @[ |
| 321 @"H:|-faviconLeftInset-[favicon(faviconSize)]", |
| 322 @"V:|-faviconVerticalOffset-[favicon]-0-|", |
| 323 @"H:[close(==closeButtonSize)]-closeButtonHorizontalShift-|", |
| 324 @"V:|-closeButtonVerticalShift-[close]-0-|", |
| 325 @"H:[favicon]-titleLeftMargin-[title]-titleRightMargin-[close]", |
| 326 @"V:[title(==titleHeight)]", |
| 327 ]; |
| 328 NSDictionary* metrics = @{ |
| 329 @"closeButtonSize" : @(kCloseButtonSize), |
| 330 @"closeButtonHorizontalShift" : @(kCloseButtonHorizontalShift), |
| 331 @"closeButtonVerticalShift" : @(kCloseButtonVerticalShift), |
| 332 @"titleLeftMargin" : @(kTitleLeftMargin), |
| 333 @"titleRightMargin" : @(kTitleRightMargin), |
| 334 @"titleHeight" : @(kFaviconSize), |
| 335 @"faviconLeftInset" : @(AlignValueToPixel(kFaviconLeftInset)), |
| 336 @"faviconVerticalOffset" : @(kFaviconVerticalOffset), |
| 337 @"faviconSize" : @(kFaviconSize), |
| 338 }; |
| 339 ApplyVisualConstraintsWithMetrics(constraints, viewsDictionary, metrics, |
| 340 self); |
| 341 AddSameCenterXConstraint(self, _faviconView, _activityIndicator); |
| 342 AddSameCenterYConstraint(self, _faviconView, _activityIndicator); |
| 343 AddSameCenterYConstraint(self, _faviconView, _titleLabel); |
| 344 } |
| 345 |
| 346 - (void)updateLineSeparator { |
| 347 UIColor* separatorColor = |
| 348 _incognitoStyle ? [UIColor colorWithWhite:36 / 255.0 alpha:1.0] |
| 349 : [UIColor colorWithWhite:185 / 255.0 alpha:1.0]; |
| 350 [_lineSeparator setBackgroundColor:separatorColor]; |
| 351 } |
| 352 |
| 353 - (void)updateBackgroundImage:(BOOL)selected { |
| 354 NSString* state = (selected ? @"foreground" : @"background"); |
| 355 NSString* incognito = _incognitoStyle ? @"incognito_" : @""; |
| 356 NSString* imageName = |
| 357 [NSString stringWithFormat:@"tabstrip_%@%@_tab", incognito, state]; |
| 358 UIImage* backgroundImage = StretchableImageFromUIImage( |
| 359 [UIImage imageNamed:imageName], kTabBackgroundLeftCapInset, 0); |
| 360 [_backgroundImageView setImage:backgroundImage]; |
| 361 } |
| 362 |
| 363 - (void)updateCloseButtonImages { |
| 364 UIImage* normalImage = |
| 365 self.incognitoStyle ? [UIImage imageNamed:@"tabstrip_tab_close_incognito"] |
| 366 : [UIImage imageNamed:@"tabstrip_tab_close"]; |
| 367 UIImage* pressedImage = |
| 368 self.incognitoStyle |
| 369 ? [UIImage imageNamed:@"tabstrip_tab_close_incognito_pressed"] |
| 370 : [UIImage imageNamed:@"tabstrip_tab_close_pressed"]; |
| 371 [_closeButton setImage:normalImage forState:UIControlStateNormal]; |
| 372 [_closeButton setImage:pressedImage forState:UIControlStateHighlighted]; |
| 373 } |
| 374 |
| 375 - (UIImage*)defaultFaviconImage { |
| 376 return self.incognitoStyle ? [UIImage imageNamed:@"default_favicon_incognito"] |
| 377 : [UIImage imageNamed:@"default_favicon"]; |
| 378 } |
| 379 |
| 380 @end |
OLD | NEW |