OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #import "ios/chrome/browser/ui/infobars/infobar_view.h" |
| 6 |
| 7 #import <CoreGraphics/CoreGraphics.h> |
| 8 #import <QuartzCore/QuartzCore.h> |
| 9 |
| 10 #include "base/format_macros.h" |
| 11 #include "base/i18n/rtl.h" |
| 12 #include "base/ios/weak_nsobject.h" |
| 13 #include "base/logging.h" |
| 14 #include "base/mac/foundation_util.h" |
| 15 #include "base/strings/sys_string_conversions.h" |
| 16 #include "components/strings/grit/components_strings.h" |
| 17 #import "ios/chrome/browser/ui/colors/MDCPalette+CrAdditions.h" |
| 18 #include "ios/chrome/browser/ui/ui_util.h" |
| 19 #import "ios/chrome/browser/ui/uikit_ui_util.h" |
| 20 #import "ios/chrome/browser/ui/util/label_link_controller.h" |
| 21 #import "ios/public/provider/chrome/browser/ui/infobar_view_delegate.h" |
| 22 #import "ios/third_party/material_components_ios/src/components/Buttons/src/Mate
rialButtons.h" |
| 23 #import "ios/third_party/material_components_ios/src/components/Typography/src/M
aterialTypography.h" |
| 24 #include "ui/base/l10n/l10n_util.h" |
| 25 #import "ui/gfx/ios/NSString+CrStringDrawing.h" |
| 26 #import "ui/gfx/ios/uikit_util.h" |
| 27 #include "url/gurl.h" |
| 28 |
| 29 namespace { |
| 30 |
| 31 const char kChromeInfobarURL[] = "chromeinternal://infobar/"; |
| 32 |
| 33 // UX configuration constants for the shadow/rounded corners on the icon. |
| 34 const CGFloat kBaseSizeForEffects = 57.0; |
| 35 const CGFloat kCornerRadius = 10.0; |
| 36 const CGFloat kShadowVerticalOffset = 1.0; |
| 37 const CGFloat kShadowOpacity = 0.5; |
| 38 const CGFloat kShadowRadius = 0.8; |
| 39 |
| 40 // UX configuration for the layout of items. |
| 41 const CGFloat kLeftMarginOnFirstLineWhenIconAbsent = 20.0; |
| 42 const CGFloat kMinimumSpaceBetweenRightAndLeftAlignedWidgets = 30.0; |
| 43 const CGFloat kRightMargin = 10.0; |
| 44 const CGFloat kSpaceBetweenWidgets = 10.0; |
| 45 const CGFloat kCloseButtonInnerPadding = 16.0; |
| 46 const CGFloat kButtonHeight = 36.0; |
| 47 const CGFloat kButtonMargin = 16.0; |
| 48 const CGFloat kExtraButtonMarginOnSingleLine = 8.0; |
| 49 const CGFloat kButtonSpacing = 8.0; |
| 50 const CGFloat kButtonWidthUnits = 8.0; |
| 51 const CGFloat kButtonsTopMargin = kCloseButtonInnerPadding; |
| 52 const CGFloat kCloseButtonLeftMargin = 16.0; |
| 53 const CGFloat kLabelLineSpacing = 5.0; |
| 54 const CGFloat kLabelMarginBottom = 22.0; |
| 55 const CGFloat kExtraMarginBetweenLabelAndButton = 8.0; |
| 56 const CGFloat kLabelMarginTop = kButtonsTopMargin + 5.0; // Baseline lowered. |
| 57 const CGFloat kMinimumInfobarHeight = 68.0; |
| 58 |
| 59 const int kButton2TitleColor = 0x4285f4; |
| 60 |
| 61 enum InfoBarButtonPosition { ON_FIRST_LINE, CENTER, LEFT, RIGHT }; |
| 62 |
| 63 } // namespace |
| 64 |
| 65 // UIView containing a switch and a label. |
| 66 @interface SwitchView : BidiContainerView |
| 67 |
| 68 // Initialize the view's label with |labelText|. |
| 69 - (id)initWithLabel:(NSString*)labelText isOn:(BOOL)isOn; |
| 70 |
| 71 // Specifies the object, action, and tag used when the switch is toggled. |
| 72 - (void)setTag:(NSInteger)tag target:(id)target action:(SEL)action; |
| 73 |
| 74 // Returns the height taken by the view constrained by a width of |width|. |
| 75 // If |layout| is yes, it sets the frame of the label and the switch to fit |
| 76 // |width|. |
| 77 - (CGFloat)heightRequiredForSwitchWithWidth:(CGFloat)width layout:(BOOL)layout; |
| 78 |
| 79 // Returns the preferred width. A smaller width requires eliding the text. |
| 80 - (CGFloat)preferredWidth; |
| 81 |
| 82 @end |
| 83 |
| 84 @implementation SwitchView { |
| 85 base::scoped_nsobject<UILabel> label_; |
| 86 base::scoped_nsobject<UISwitch> switch_; |
| 87 CGFloat preferredTotalWidth_; |
| 88 CGFloat preferredLabelWidth_; |
| 89 } |
| 90 |
| 91 - (id)initWithLabel:(NSString*)labelText isOn:(BOOL)isOn { |
| 92 // Creates switch and label. |
| 93 base::scoped_nsobject<UILabel> tempLabel( |
| 94 [[UILabel alloc] initWithFrame:CGRectZero]); |
| 95 [tempLabel setTextAlignment:NSTextAlignmentNatural]; |
| 96 [tempLabel setFont:[MDCTypography body1Font]]; |
| 97 [tempLabel setText:labelText]; |
| 98 [tempLabel setBackgroundColor:[UIColor clearColor]]; |
| 99 [tempLabel setLineBreakMode:NSLineBreakByWordWrapping]; |
| 100 [tempLabel setNumberOfLines:0]; |
| 101 [tempLabel setAdjustsFontSizeToFitWidth:NO]; |
| 102 base::scoped_nsobject<UISwitch> tempSwitch( |
| 103 [[UISwitch alloc] initWithFrame:CGRectZero]); |
| 104 [tempSwitch setExclusiveTouch:YES]; |
| 105 [tempSwitch setAccessibilityLabel:labelText]; |
| 106 [tempSwitch setOnTintColor:[[MDCPalette cr_bluePalette] tint500]]; |
| 107 [tempSwitch setOn:isOn]; |
| 108 |
| 109 // Computes the size and initializes the view. |
| 110 CGSize maxSize = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX); |
| 111 CGSize labelSize = |
| 112 [[tempLabel text] cr_boundingSizeWithSize:maxSize font:[tempLabel font]]; |
| 113 CGSize switchSize = [tempSwitch frame].size; |
| 114 CGRect frameRect = CGRectMake( |
| 115 0, 0, labelSize.width + kSpaceBetweenWidgets + switchSize.width, |
| 116 std::max(labelSize.height, switchSize.height)); |
| 117 self = [super initWithFrame:frameRect]; |
| 118 if (!self) |
| 119 return nil; |
| 120 label_.reset([tempLabel retain]); |
| 121 switch_.reset([tempSwitch retain]); |
| 122 |
| 123 // Sets the position of the label and the switch. The label is left aligned |
| 124 // and the switch is right aligned. Both are vertically centered. |
| 125 CGRect labelFrame = |
| 126 CGRectMake(0, (self.frame.size.height - labelSize.height) / 2, |
| 127 labelSize.width, labelSize.height); |
| 128 CGRect switchFrame = |
| 129 CGRectMake(self.frame.size.width - switchSize.width, |
| 130 (self.frame.size.height - switchSize.height) / 2, |
| 131 switchSize.width, switchSize.height); |
| 132 |
| 133 labelFrame = AlignRectOriginAndSizeToPixels(labelFrame); |
| 134 switchFrame = AlignRectOriginAndSizeToPixels(switchFrame); |
| 135 |
| 136 [label_ setFrame:labelFrame]; |
| 137 [switch_ setFrame:switchFrame]; |
| 138 preferredTotalWidth_ = CGRectGetMaxX(switchFrame); |
| 139 preferredLabelWidth_ = CGRectGetMaxX(labelFrame); |
| 140 |
| 141 [self addSubview:label_]; |
| 142 [self addSubview:switch_]; |
| 143 return self; |
| 144 } |
| 145 |
| 146 - (void)setTag:(NSInteger)tag target:(id)target action:(SEL)action { |
| 147 [switch_ setTag:tag]; |
| 148 [switch_ addTarget:target |
| 149 action:action |
| 150 forControlEvents:UIControlEventValueChanged]; |
| 151 } |
| 152 |
| 153 - (CGFloat)heightRequiredForSwitchWithWidth:(CGFloat)width layout:(BOOL)layout { |
| 154 CGFloat widthLeftForLabel = |
| 155 width - [switch_ frame].size.width - kSpaceBetweenWidgets; |
| 156 CGSize maxSize = CGSizeMake(widthLeftForLabel, CGFLOAT_MAX); |
| 157 CGSize labelSize = |
| 158 [[label_ text] cr_boundingSizeWithSize:maxSize font:[label_ font]]; |
| 159 CGFloat viewHeight = std::max(labelSize.height, [switch_ frame].size.height); |
| 160 if (layout) { |
| 161 // Lays out the label and the switch to fit in {width, viewHeight}. |
| 162 CGRect newLabelFrame; |
| 163 newLabelFrame.origin.x = 0; |
| 164 newLabelFrame.origin.y = (viewHeight - labelSize.height) / 2; |
| 165 newLabelFrame.size = labelSize; |
| 166 newLabelFrame = AlignRectOriginAndSizeToPixels(newLabelFrame); |
| 167 [label_ setFrame:newLabelFrame]; |
| 168 CGRect newSwitchFrame; |
| 169 newSwitchFrame.origin.x = |
| 170 CGRectGetMaxX(newLabelFrame) + kSpaceBetweenWidgets; |
| 171 newSwitchFrame.origin.y = (viewHeight - [switch_ frame].size.height) / 2; |
| 172 newSwitchFrame.size = [switch_ frame].size; |
| 173 newSwitchFrame = AlignRectOriginAndSizeToPixels(newSwitchFrame); |
| 174 [switch_ setFrame:newSwitchFrame]; |
| 175 } |
| 176 return viewHeight; |
| 177 } |
| 178 |
| 179 - (CGFloat)preferredWidth { |
| 180 return preferredTotalWidth_; |
| 181 } |
| 182 |
| 183 @end |
| 184 |
| 185 @interface InfoBarView (Testing) |
| 186 // Returns the buttons' height. |
| 187 - (CGFloat)buttonsHeight; |
| 188 // Returns the button margin applied in some views. |
| 189 - (CGFloat)buttonMargin; |
| 190 // Returns the height of the infobar, and lays out the subviews if |layout| is |
| 191 // YES. |
| 192 - (CGFloat)computeRequiredHeightAndLayoutSubviews:(BOOL)layout; |
| 193 // Returns the height of the laid out buttons when not on the first line. |
| 194 // Either the buttons are narrow enough and they are on a single line next to |
| 195 // each other, or they are supperposed on top of each other. |
| 196 // Also lays out the buttons when |layout| is YES, in which case it uses |
| 197 // |heightOfFirstLine| to compute their vertical position. |
| 198 - (CGFloat)heightThatFitsButtonsUnderOtherWidgets:(CGFloat)heightOfFirstLine |
| 199 layout:(BOOL)layout; |
| 200 // The |button| is positioned with the right edge at the specified y-axis |
| 201 // position |rightEdge| and the top row at |y|. |
| 202 // Returns the left edge of the newly-positioned button. |
| 203 - (CGFloat)layoutWideButtonAlignRight:(UIButton*)button |
| 204 rightEdge:(CGFloat)rightEdge |
| 205 y:(CGFloat)y; |
| 206 // Returns the minimum height of infobars. |
| 207 - (CGFloat)minimumInfobarHeight; |
| 208 // Returns |string| stripped of the markers specifying the links and fills |
| 209 // |linkRanges_| with the ranges of the enclosed links. |
| 210 - (NSString*)stripMarkersFromString:(NSString*)string; |
| 211 // Returns the ranges of the links and the associated tags. |
| 212 - (const std::vector<std::pair<NSUInteger, NSRange>>&)linkRanges; |
| 213 @end |
| 214 |
| 215 @interface InfoBarView () |
| 216 |
| 217 // Returns the marker delimiting the start of a link. |
| 218 + (NSString*)openingMarkerForLink; |
| 219 // Returns the marker delimiting the end of a link. |
| 220 + (NSString*)closingMarkerForLink; |
| 221 |
| 222 @end |
| 223 |
| 224 @implementation InfoBarView { |
| 225 // Delegates UIView events. |
| 226 InfoBarViewDelegate* delegate_; // weak |
| 227 // The current height of this infobar (used for animations where part of the |
| 228 // infobar is hidden). |
| 229 CGFloat visibleHeight_; |
| 230 // The height of this infobar when fully visible. |
| 231 CGFloat targetHeight_; |
| 232 // View containing |imageView_|. Exists to apply drop shadows to the view. |
| 233 base::scoped_nsobject<UIView> imageViewContainer_; |
| 234 // View containing the icon. |
| 235 base::scoped_nsobject<UIImageView> imageView_; |
| 236 // Close button. |
| 237 base::scoped_nsobject<UIButton> closeButton_; |
| 238 // View containing the switch and its label. |
| 239 base::scoped_nsobject<SwitchView> switchView_; |
| 240 // We are using a LabelLinkController with an UILabel to be able to have |
| 241 // parts of the label underlined and clickable. This label_ may be nil if |
| 242 // the delegate returns an empty string for GetMessageText(). |
| 243 base::scoped_nsobject<LabelLinkController> labelLinkController_; |
| 244 UILabel* label_; // Weak. |
| 245 // Array of range information. The first element of the pair is the tag of |
| 246 // the action and the second element is the range defining the link. |
| 247 std::vector<std::pair<NSUInteger, NSRange>> linkRanges_; |
| 248 // Text for the label with link markers included. |
| 249 base::scoped_nsobject<NSString> markedLabel_; |
| 250 // Buttons. |
| 251 // button1_ is tagged with ConfirmInfoBarDelegate::BUTTON_OK . |
| 252 // button2_ is tagged with ConfirmInfoBarDelegate::BUTTON_CANCEL . |
| 253 base::scoped_nsobject<UIButton> button1_; |
| 254 base::scoped_nsobject<UIButton> button2_; |
| 255 // Drop shadow. |
| 256 base::scoped_nsobject<UIImageView> shadow_; |
| 257 } |
| 258 |
| 259 @synthesize visibleHeight = visibleHeight_; |
| 260 |
| 261 - (id)initWithFrame:(CGRect)frame delegate:(InfoBarViewDelegate*)delegate { |
| 262 self = [super initWithFrame:frame]; |
| 263 if (self) { |
| 264 delegate_ = delegate; |
| 265 // Make the drop shadow. |
| 266 UIImage* shadowImage = [UIImage imageNamed:@"infobar_shadow"]; |
| 267 shadow_.reset([[UIImageView alloc] initWithImage:shadowImage]); |
| 268 [self addSubview:shadow_]; |
| 269 [self setAutoresizingMask:UIViewAutoresizingFlexibleWidth | |
| 270 UIViewAutoresizingFlexibleHeight]; |
| 271 [self setAccessibilityViewIsModal:YES]; |
| 272 } |
| 273 return self; |
| 274 } |
| 275 |
| 276 - (void)dealloc { |
| 277 [super dealloc]; |
| 278 } |
| 279 |
| 280 - (NSString*)markedLabel { |
| 281 return markedLabel_; |
| 282 } |
| 283 |
| 284 - (void)resetDelegate { |
| 285 delegate_ = NULL; |
| 286 } |
| 287 |
| 288 // Returns the width reserved for the icon. |
| 289 - (CGFloat)leftMarginOnFirstLine { |
| 290 CGFloat leftMargin = 0; |
| 291 if (imageViewContainer_) { |
| 292 leftMargin += [self frameOfIcon].size.width; |
| 293 // The margin between the label and the icon is the same as the margin |
| 294 // between the edge of the screen and the icon. |
| 295 leftMargin += 2 * [self frameOfIcon].origin.x; |
| 296 } else { |
| 297 leftMargin += kLeftMarginOnFirstLineWhenIconAbsent; |
| 298 } |
| 299 return leftMargin; |
| 300 } |
| 301 |
| 302 // Returns the width reserved for the close button. |
| 303 - (CGFloat)rightMarginOnFirstLine { |
| 304 return |
| 305 [closeButton_ imageView].image.size.width + kCloseButtonInnerPadding * 2; |
| 306 } |
| 307 |
| 308 // Returns the horizontal space available between the icon and the close |
| 309 // button. |
| 310 - (CGFloat)horizontalSpaceAvailableOnFirstLine { |
| 311 return [self frame].size.width - [self leftMarginOnFirstLine] - |
| 312 [self rightMarginOnFirstLine]; |
| 313 } |
| 314 |
| 315 // Returns the height taken by a label constrained by a width of |width|. |
| 316 - (CGFloat)heightRequiredForLabelWithWidth:(CGFloat)width { |
| 317 return [label_ sizeThatFits:CGSizeMake(width, CGFLOAT_MAX)].height; |
| 318 } |
| 319 |
| 320 // Returns the width required by a label if it was displayed on a single line. |
| 321 - (CGFloat)widthOfLabelOnASingleLine { |
| 322 // |label_| can be nil when delegate returns "" for GetMessageText(). |
| 323 if (!label_) |
| 324 return 0.0; |
| 325 CGSize rect = [[label_ text] cr_pixelAlignedSizeWithFont:[label_ font]]; |
| 326 return rect.width; |
| 327 } |
| 328 |
| 329 // Returns the minimum size required by |button| to be properly displayed. |
| 330 - (CGFloat)narrowestWidthOfButton:(UIButton*)button { |
| 331 if (!button) |
| 332 return 0; |
| 333 // The button itself is queried for the size. The width is rounded up to be a |
| 334 // multiple of 8 to fit Material grid spacing requirements. |
| 335 CGFloat labelWidth = |
| 336 [button sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)].width; |
| 337 return ceil(labelWidth / kButtonWidthUnits) * kButtonWidthUnits; |
| 338 } |
| 339 |
| 340 // Returns the width of the buttons if they are laid out on the first line. |
| 341 - (CGFloat)widthOfButtonsOnFirstLine { |
| 342 CGFloat width = [self narrowestWidthOfButton:button1_] + |
| 343 [self narrowestWidthOfButton:button2_]; |
| 344 if (button1_ && button2_) { |
| 345 width += kSpaceBetweenWidgets; |
| 346 } |
| 347 return width; |
| 348 } |
| 349 |
| 350 // Returns the width needed for the switch. |
| 351 - (CGFloat)preferredWidthOfSwitch { |
| 352 return [switchView_ preferredWidth]; |
| 353 } |
| 354 |
| 355 // Returns the space required to separate the left aligned widgets (label) from |
| 356 // the right aligned widgets (switch, buttons), assuming they fit on one line. |
| 357 - (CGFloat)widthToSeparateRightAndLeftWidgets { |
| 358 BOOL leftWidgetsArePresent = (label_ != nil); |
| 359 BOOL rightWidgetsArePresent = button1_ || button2_ || switchView_; |
| 360 if (!leftWidgetsArePresent || !rightWidgetsArePresent) |
| 361 return 0; |
| 362 return kMinimumSpaceBetweenRightAndLeftAlignedWidgets; |
| 363 } |
| 364 |
| 365 // Returns the space required to separate the switch and the buttons. |
| 366 - (CGFloat)widthToSeparateSwitchAndButtons { |
| 367 BOOL buttonsArePresent = button1_ || button2_; |
| 368 BOOL switchIsPresent = (switchView_ != nil); |
| 369 if (!buttonsArePresent || !switchIsPresent) |
| 370 return 0; |
| 371 return kSpaceBetweenWidgets; |
| 372 } |
| 373 |
| 374 // Lays out |button| at the height |y| and in the position |position|. |
| 375 // Must only be used for wide buttons, i.e. buttons not on the first line. |
| 376 - (void)layoutWideButton:(UIButton*)button |
| 377 y:(CGFloat)y |
| 378 position:(InfoBarButtonPosition)position { |
| 379 CGFloat screenWidth = [self frame].size.width; |
| 380 CGFloat startPercentage = 0.0; |
| 381 CGFloat endPercentage = 0.0; |
| 382 switch (position) { |
| 383 case LEFT: |
| 384 startPercentage = 0.0; |
| 385 endPercentage = 0.5; |
| 386 break; |
| 387 case RIGHT: |
| 388 startPercentage = 0.5; |
| 389 endPercentage = 1.0; |
| 390 break; |
| 391 case CENTER: |
| 392 startPercentage = 0.0; |
| 393 endPercentage = 1.0; |
| 394 break; |
| 395 case ON_FIRST_LINE: |
| 396 NOTREACHED(); |
| 397 } |
| 398 DCHECK(startPercentage >= 0.0 && startPercentage <= 1.0); |
| 399 DCHECK(endPercentage >= 0.0 && endPercentage <= 1.0); |
| 400 DCHECK(startPercentage < endPercentage); |
| 401 // In Material the button is not stretched to fit the available space. It is |
| 402 // placed centrally in the allotted space. |
| 403 CGFloat minX = screenWidth * startPercentage; |
| 404 CGFloat maxX = screenWidth * endPercentage; |
| 405 CGFloat midpoint = (minX + maxX) / 2; |
| 406 CGFloat minWidth = |
| 407 std::min([self narrowestWidthOfButton:button], maxX - minX); |
| 408 CGFloat left = midpoint - minWidth / 2; |
| 409 CGRect frame = CGRectMake(left, y, minWidth, kButtonHeight); |
| 410 frame = AlignRectOriginAndSizeToPixels(frame); |
| 411 [button setFrame:frame]; |
| 412 } |
| 413 |
| 414 - (CGFloat)layoutWideButtonAlignRight:(UIButton*)button |
| 415 rightEdge:(CGFloat)rightEdge |
| 416 y:(CGFloat)y { |
| 417 CGFloat width = [self narrowestWidthOfButton:button]; |
| 418 CGFloat leftEdge = rightEdge - width; |
| 419 CGRect frame = CGRectMake(leftEdge, y, width, kButtonHeight); |
| 420 frame = AlignRectOriginAndSizeToPixels(frame); |
| 421 [button setFrame:frame]; |
| 422 return leftEdge; |
| 423 } |
| 424 |
| 425 - (CGFloat)heightThatFitsButtonsUnderOtherWidgets:(CGFloat)heightOfFirstLine |
| 426 layout:(BOOL)layout { |
| 427 if (button1_ && button2_) { |
| 428 CGFloat halfWidthOfScreen = [self frame].size.width / 2.0; |
| 429 if ([self narrowestWidthOfButton:button1_] <= halfWidthOfScreen && |
| 430 [self narrowestWidthOfButton:button2_] <= halfWidthOfScreen) { |
| 431 // Each button can fit in half the screen's width. |
| 432 if (layout) { |
| 433 // When there are two buttons on one line, they are positioned aligned |
| 434 // right in the available space, spaced apart by kButtonSpacing. |
| 435 CGFloat leftOfRightmostButton = |
| 436 [self layoutWideButtonAlignRight:button1_ |
| 437 rightEdge:CGRectGetWidth(self.bounds) - |
| 438 kButtonMargin |
| 439 y:heightOfFirstLine]; |
| 440 [self layoutWideButtonAlignRight:button2_ |
| 441 rightEdge:leftOfRightmostButton - kButtonSpacing |
| 442 y:heightOfFirstLine]; |
| 443 } |
| 444 return kButtonHeight; |
| 445 } else { |
| 446 // At least one of the two buttons is larger than half the screen's width, |
| 447 // so |button2_| is placed underneath |button1_|. |
| 448 if (layout) { |
| 449 [self layoutWideButton:button1_ y:heightOfFirstLine position:CENTER]; |
| 450 [self layoutWideButton:button2_ |
| 451 y:heightOfFirstLine + kButtonHeight |
| 452 position:CENTER]; |
| 453 } |
| 454 return 2 * kButtonHeight; |
| 455 } |
| 456 } |
| 457 // There is at most 1 button to layout. |
| 458 UIButton* button = button1_ ? button1_ : button2_; |
| 459 if (button) { |
| 460 if (layout) { |
| 461 // Where is there is just one button it is positioned aligned right in the |
| 462 // available space. |
| 463 [self |
| 464 layoutWideButtonAlignRight:button |
| 465 rightEdge:CGRectGetWidth(self.bounds) - kButtonMargin |
| 466 y:heightOfFirstLine]; |
| 467 } |
| 468 return kButtonHeight; |
| 469 } |
| 470 return 0; |
| 471 } |
| 472 |
| 473 - (CGFloat)computeRequiredHeightAndLayoutSubviews:(BOOL)layout { |
| 474 CGFloat requiredHeight = 0; |
| 475 CGFloat widthOfLabel = [self widthOfLabelOnASingleLine] + |
| 476 [self widthToSeparateRightAndLeftWidgets]; |
| 477 CGFloat widthOfButtons = [self widthOfButtonsOnFirstLine]; |
| 478 CGFloat preferredWidthOfSwitch = [self preferredWidthOfSwitch]; |
| 479 CGFloat widthOfScreen = [self frame].size.width; |
| 480 CGFloat rightMarginOnFirstLine = [self rightMarginOnFirstLine]; |
| 481 CGFloat spaceAvailableOnFirstLine = |
| 482 [self horizontalSpaceAvailableOnFirstLine]; |
| 483 CGFloat widthOfButtonAndSwitch = widthOfButtons + |
| 484 [self widthToSeparateSwitchAndButtons] + |
| 485 preferredWidthOfSwitch; |
| 486 // Tests if the label, switch, and buttons can fit on a single line. |
| 487 if (widthOfLabel + widthOfButtonAndSwitch < spaceAvailableOnFirstLine) { |
| 488 // The label, switch, and buttons can fit on a single line. |
| 489 requiredHeight = kMinimumInfobarHeight; |
| 490 if (layout) { |
| 491 // Lays out the close button. |
| 492 CGRect buttonFrame = [self frameOfCloseButton:YES]; |
| 493 [closeButton_ setFrame:buttonFrame]; |
| 494 // Lays out the label. |
| 495 CGFloat labelHeight = [self heightRequiredForLabelWithWidth:widthOfLabel]; |
| 496 CGRect frame = CGRectMake([self leftMarginOnFirstLine], |
| 497 (kMinimumInfobarHeight - labelHeight) / 2, |
| 498 [self widthOfLabelOnASingleLine], labelHeight); |
| 499 frame = AlignRectOriginAndSizeToPixels(frame); |
| 500 [label_ setFrame:frame]; |
| 501 // Layouts the buttons. |
| 502 CGFloat buttonMargin = |
| 503 rightMarginOnFirstLine + kExtraButtonMarginOnSingleLine; |
| 504 if (button1_) { |
| 505 CGFloat width = [self narrowestWidthOfButton:button1_]; |
| 506 CGFloat offset = width; |
| 507 frame = CGRectMake(widthOfScreen - buttonMargin - offset, |
| 508 (kMinimumInfobarHeight - kButtonHeight) / 2, width, |
| 509 kButtonHeight); |
| 510 frame = AlignRectOriginAndSizeToPixels(frame); |
| 511 [button1_ setFrame:frame]; |
| 512 } |
| 513 if (button2_) { |
| 514 CGFloat width = [self narrowestWidthOfButton:button2_]; |
| 515 CGFloat offset = widthOfButtons; |
| 516 frame = CGRectMake(widthOfScreen - buttonMargin - offset, |
| 517 (kMinimumInfobarHeight - kButtonHeight) / 2, width, |
| 518 frame.size.height = kButtonHeight); |
| 519 frame = AlignRectOriginAndSizeToPixels(frame); |
| 520 [button2_ setFrame:frame]; |
| 521 } |
| 522 // Lays out the switch view to the left of the buttons. |
| 523 if (switchView_) { |
| 524 frame = CGRectMake( |
| 525 widthOfScreen - buttonMargin - widthOfButtonAndSwitch, |
| 526 (kMinimumInfobarHeight - [switchView_ frame].size.height) / 2.0, |
| 527 preferredWidthOfSwitch, [switchView_ frame].size.height); |
| 528 frame = AlignRectOriginAndSizeToPixels(frame); |
| 529 [switchView_ setFrame:frame]; |
| 530 } |
| 531 } |
| 532 } else { |
| 533 // The widgets (label, switch, buttons) can't fit on a single line. Attempts |
| 534 // to lay out the label and switch on the first line, and the buttons |
| 535 // underneath. |
| 536 CGFloat heightOfLabelAndSwitch = 0; |
| 537 |
| 538 if (layout) { |
| 539 // Lays out the close button. |
| 540 CGRect buttonFrame = [self frameOfCloseButton:NO]; |
| 541 [closeButton_ setFrame:buttonFrame]; |
| 542 } |
| 543 if (widthOfLabel + preferredWidthOfSwitch < spaceAvailableOnFirstLine) { |
| 544 // The label and switch can fit on the first line. |
| 545 heightOfLabelAndSwitch = kMinimumInfobarHeight; |
| 546 if (layout) { |
| 547 CGFloat labelHeight = |
| 548 [self heightRequiredForLabelWithWidth:widthOfLabel]; |
| 549 CGRect labelFrame = |
| 550 CGRectMake([self leftMarginOnFirstLine], |
| 551 (heightOfLabelAndSwitch - labelHeight) / 2, |
| 552 [self widthOfLabelOnASingleLine], labelHeight); |
| 553 labelFrame = AlignRectOriginAndSizeToPixels(labelFrame); |
| 554 [label_ setFrame:labelFrame]; |
| 555 if (switchView_) { |
| 556 CGRect switchRect = CGRectMake( |
| 557 widthOfScreen - rightMarginOnFirstLine - preferredWidthOfSwitch, |
| 558 (heightOfLabelAndSwitch - [switchView_ frame].size.height) / 2, |
| 559 preferredWidthOfSwitch, [switchView_ frame].size.height); |
| 560 switchRect = AlignRectOriginAndSizeToPixels(switchRect); |
| 561 [switchView_ setFrame:switchRect]; |
| 562 } |
| 563 } |
| 564 } else { |
| 565 // The label and switch can't fit on the first line, so lay them out on |
| 566 // different lines. |
| 567 // Computes the height of the label, and optionally lays it out. |
| 568 CGFloat labelMarginBottom = kLabelMarginBottom; |
| 569 if (button1_ || button2_) { |
| 570 // Material features more padding between the label and the button than |
| 571 // the label and the bottom of the dialog when there is no button. |
| 572 labelMarginBottom += kExtraMarginBetweenLabelAndButton; |
| 573 } |
| 574 CGFloat heightOfLabelWithPadding = |
| 575 [self heightRequiredForLabelWithWidth:spaceAvailableOnFirstLine] + |
| 576 kLabelMarginTop + labelMarginBottom; |
| 577 if (layout) { |
| 578 CGRect labelFrame = CGRectMake( |
| 579 [self leftMarginOnFirstLine], kLabelMarginTop, |
| 580 spaceAvailableOnFirstLine, |
| 581 heightOfLabelWithPadding - kLabelMarginTop - labelMarginBottom); |
| 582 labelFrame = AlignRectOriginAndSizeToPixels(labelFrame); |
| 583 [label_ setFrame:labelFrame]; |
| 584 } |
| 585 // Computes the height of the switch view (if any), and optionally lays it |
| 586 // out. |
| 587 CGFloat heightOfSwitchWithPadding = 0; |
| 588 if (switchView_ != nil) { |
| 589 // The switch view is aligned with the first line's label, hence the |
| 590 // call to |leftMarginOnFirstLine|. |
| 591 CGFloat widthAvailableForSwitchView = [self frame].size.width - |
| 592 [self leftMarginOnFirstLine] - |
| 593 kRightMargin; |
| 594 CGFloat heightOfSwitch = [switchView_ |
| 595 heightRequiredForSwitchWithWidth:widthAvailableForSwitchView |
| 596 layout:layout]; |
| 597 // If there are buttons underneath the switch, add padding. |
| 598 if (button1_ || button2_) { |
| 599 heightOfSwitchWithPadding = heightOfSwitch + kSpaceBetweenWidgets + |
| 600 kExtraMarginBetweenLabelAndButton; |
| 601 } else { |
| 602 heightOfSwitchWithPadding = heightOfSwitch; |
| 603 } |
| 604 if (layout) { |
| 605 CGRect switchRect = |
| 606 CGRectMake([self leftMarginOnFirstLine], heightOfLabelWithPadding, |
| 607 widthAvailableForSwitchView, heightOfSwitch); |
| 608 switchRect = AlignRectOriginAndSizeToPixels(switchRect); |
| 609 [switchView_ setFrame:switchRect]; |
| 610 } |
| 611 } |
| 612 heightOfLabelAndSwitch = |
| 613 std::max(heightOfLabelWithPadding + heightOfSwitchWithPadding, |
| 614 kMinimumInfobarHeight); |
| 615 } |
| 616 // Lays out the button(s) under the label and switch. |
| 617 CGFloat heightOfButtons = |
| 618 [self heightThatFitsButtonsUnderOtherWidgets:heightOfLabelAndSwitch |
| 619 layout:layout]; |
| 620 requiredHeight = heightOfLabelAndSwitch; |
| 621 if (heightOfButtons > 0) |
| 622 requiredHeight += heightOfButtons + kButtonMargin; |
| 623 } |
| 624 return requiredHeight; |
| 625 } |
| 626 |
| 627 - (CGSize)sizeThatFits:(CGSize)size { |
| 628 CGFloat requiredHeight = [self computeRequiredHeightAndLayoutSubviews:NO]; |
| 629 return CGSizeMake([self frame].size.width, requiredHeight); |
| 630 } |
| 631 |
| 632 - (void)layoutSubviews { |
| 633 // Lays out the position of the icon. |
| 634 [imageViewContainer_ setFrame:[self frameOfIcon]]; |
| 635 targetHeight_ = [self computeRequiredHeightAndLayoutSubviews:YES]; |
| 636 |
| 637 if (delegate_) |
| 638 delegate_->SetInfoBarTargetHeight(targetHeight_); |
| 639 [self resetBackground]; |
| 640 |
| 641 // Asks the BidiContainerView to reposition of all the subviews. |
| 642 for (UIView* view in [self subviews]) |
| 643 [self setSubviewNeedsAdjustmentForRTL:view]; |
| 644 [super layoutSubviews]; |
| 645 } |
| 646 |
| 647 - (void)resetBackground { |
| 648 UIColor* color = [UIColor whiteColor]; |
| 649 [self setBackgroundColor:color]; |
| 650 CGFloat shadowY = 0; |
| 651 shadowY = -[shadow_ image].size.height; // Shadow above the infobar. |
| 652 [shadow_ setFrame:CGRectMake(0, shadowY, self.bounds.size.width, |
| 653 [shadow_ image].size.height)]; |
| 654 [shadow_ setAutoresizingMask:UIViewAutoresizingFlexibleWidth]; |
| 655 } |
| 656 |
| 657 - (void)addCloseButtonWithTag:(NSInteger)tag |
| 658 target:(id)target |
| 659 action:(SEL)action { |
| 660 DCHECK(!closeButton_); |
| 661 // TODO(jeanfrancoisg): Add IDR_ constant and use GetNativeImageNamed(). |
| 662 // crbug/228611 |
| 663 NSString* imagePath = |
| 664 [[NSBundle mainBundle] pathForResource:@"infobar_close" ofType:@"png"]; |
| 665 UIImage* image = [UIImage imageWithContentsOfFile:imagePath]; |
| 666 closeButton_.reset([[UIButton buttonWithType:UIButtonTypeCustom] retain]); |
| 667 [closeButton_ setExclusiveTouch:YES]; |
| 668 [closeButton_ setImage:image forState:UIControlStateNormal]; |
| 669 [closeButton_ addTarget:target |
| 670 action:action |
| 671 forControlEvents:UIControlEventTouchUpInside]; |
| 672 [closeButton_ setTag:tag]; |
| 673 [closeButton_ setAccessibilityLabel:l10n_util::GetNSString(IDS_CLOSE)]; |
| 674 [self addSubview:closeButton_]; |
| 675 } |
| 676 |
| 677 - (void)addSwitchWithLabel:(NSString*)label |
| 678 isOn:(BOOL)isOn |
| 679 tag:(NSInteger)tag |
| 680 target:(id)target |
| 681 action:(SEL)action { |
| 682 switchView_.reset([[SwitchView alloc] initWithLabel:label isOn:isOn]); |
| 683 [switchView_ setTag:tag target:target action:action]; |
| 684 [self addSubview:switchView_]; |
| 685 } |
| 686 |
| 687 - (void)addLeftIcon:(UIImage*)image { |
| 688 if (!imageViewContainer_) { |
| 689 imageViewContainer_.reset([[UIView alloc] init]); |
| 690 [self addSubview:imageViewContainer_]; |
| 691 } |
| 692 imageView_.reset([[UIImageView alloc] initWithImage:image]); |
| 693 [imageViewContainer_ addSubview:imageView_]; |
| 694 } |
| 695 |
| 696 - (void)addPlaceholderTransparentIcon:(CGSize const&)imageSize { |
| 697 UIGraphicsBeginImageContext(imageSize); |
| 698 UIImage* placeholder = UIGraphicsGetImageFromCurrentImageContext(); |
| 699 UIGraphicsEndImageContext(); |
| 700 [self addLeftIcon:placeholder]; |
| 701 } |
| 702 |
| 703 // Since shadows & rounded corners cannot be applied simultaneously to a |
| 704 // UIView, this method adds rounded corners to the UIImageView and then adds |
| 705 // drop shadow to the UIView containing the UIImageView. |
| 706 - (void)addLeftIconWithRoundedCornersAndShadow:(UIImage*)image { |
| 707 CGFloat effectScaleFactor = image.size.width / kBaseSizeForEffects; |
| 708 [self addLeftIcon:image]; |
| 709 CALayer* layer = [imageView_ layer]; |
| 710 [layer setMasksToBounds:YES]; |
| 711 [layer setCornerRadius:kCornerRadius * effectScaleFactor]; |
| 712 layer = [imageViewContainer_ layer]; |
| 713 [layer setShadowColor:[UIColor blackColor].CGColor]; |
| 714 [layer |
| 715 setShadowOffset:CGSizeMake(0, kShadowVerticalOffset * effectScaleFactor)]; |
| 716 [layer setShadowOpacity:kShadowOpacity]; |
| 717 [layer setShadowRadius:kShadowRadius * effectScaleFactor]; |
| 718 [imageViewContainer_ setClipsToBounds:NO]; |
| 719 } |
| 720 |
| 721 - (NSString*)stripMarkersFromString:(NSString*)string { |
| 722 linkRanges_.clear(); |
| 723 for (;;) { |
| 724 // Find the opening marker, followed by the tag between parentheses. |
| 725 NSRange startingRange = |
| 726 [string rangeOfString:[[InfoBarView openingMarkerForLink] |
| 727 stringByAppendingString:@"("]]; |
| 728 if (!startingRange.length) |
| 729 return [[string copy] autorelease]; |
| 730 // Read the tag. |
| 731 NSUInteger beginTag = NSMaxRange(startingRange); |
| 732 NSRange closingParenthesis = [string |
| 733 rangeOfString:@")" |
| 734 options:NSLiteralSearch |
| 735 range:NSMakeRange(beginTag, [string length] - beginTag)]; |
| 736 if (closingParenthesis.location == NSNotFound) |
| 737 return [[string copy] autorelease]; |
| 738 NSInteger tag = [[string |
| 739 substringWithRange:NSMakeRange(beginTag, closingParenthesis.location - |
| 740 beginTag)] integerValue]; |
| 741 // If the parsing fails, |tag| is 0. Negative values are not allowed. |
| 742 if (tag <= 0) |
| 743 return [[string copy] autorelease]; |
| 744 // Find the closing marker. |
| 745 startingRange.length = |
| 746 closingParenthesis.location - startingRange.location + 1; |
| 747 NSRange endingRange = |
| 748 [string rangeOfString:[InfoBarView closingMarkerForLink]]; |
| 749 DCHECK(endingRange.length); |
| 750 // Compute range of link in stripped string and add it to the array. |
| 751 NSRange rangeOfLinkInStrippedString = |
| 752 NSMakeRange(startingRange.location, |
| 753 endingRange.location - NSMaxRange(startingRange)); |
| 754 linkRanges_.push_back(std::make_pair(tag, rangeOfLinkInStrippedString)); |
| 755 // Creates a new string without the markers. |
| 756 NSString* beforeLink = [string substringToIndex:startingRange.location]; |
| 757 NSRange rangeOfLink = |
| 758 NSMakeRange(NSMaxRange(startingRange), |
| 759 endingRange.location - NSMaxRange(startingRange)); |
| 760 NSString* link = [string substringWithRange:rangeOfLink]; |
| 761 NSString* afterLink = [string substringFromIndex:NSMaxRange(endingRange)]; |
| 762 string = [NSString stringWithFormat:@"%@%@%@", beforeLink, link, afterLink]; |
| 763 } |
| 764 } |
| 765 |
| 766 - (void)addLabel:(NSString*)label { |
| 767 [self addLabel:label target:nil action:nil]; |
| 768 } |
| 769 |
| 770 - (void)addLabel:(NSString*)text target:(id)target action:(SEL)action { |
| 771 markedLabel_.reset([text copy]); |
| 772 if (target) |
| 773 text = [self stripMarkersFromString:text]; |
| 774 if ([label_ superview]) { |
| 775 [label_ removeFromSuperview]; |
| 776 } |
| 777 |
| 778 label_ = [[[UILabel alloc] initWithFrame:CGRectZero] autorelease]; |
| 779 |
| 780 UIFont* font = [MDCTypography subheadFont]; |
| 781 |
| 782 [label_ setBackgroundColor:[UIColor clearColor]]; |
| 783 |
| 784 NSMutableParagraphStyle* paragraphStyle = |
| 785 [[[NSMutableParagraphStyle alloc] init] autorelease]; |
| 786 paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping; |
| 787 paragraphStyle.lineSpacing = kLabelLineSpacing; |
| 788 NSDictionary* attributes = @{ |
| 789 NSParagraphStyleAttributeName : paragraphStyle, |
| 790 NSFontAttributeName : font, |
| 791 }; |
| 792 [label_ setNumberOfLines:0]; |
| 793 |
| 794 [label_ setAttributedText:[[[NSAttributedString alloc] |
| 795 initWithString:text |
| 796 attributes:attributes] autorelease]]; |
| 797 |
| 798 [self addSubview:label_]; |
| 799 |
| 800 if (linkRanges_.empty()) |
| 801 return; |
| 802 |
| 803 DCHECK([target respondsToSelector:action]); |
| 804 |
| 805 labelLinkController_.reset([[LabelLinkController alloc] |
| 806 initWithLabel:label_ |
| 807 action:^(const GURL& gurl) { |
| 808 NSUInteger actionTag = [base::SysUTF8ToNSString( |
| 809 gurl.ExtractFileName()) integerValue]; |
| 810 [target performSelector:action withObject:@(actionTag)]; |
| 811 }]); |
| 812 |
| 813 [labelLinkController_ setLinkUnderlineStyle:NSUnderlineStyleSingle]; |
| 814 [labelLinkController_ setLinkColor:[UIColor blackColor]]; |
| 815 |
| 816 std::vector<std::pair<NSUInteger, NSRange>>::const_iterator it; |
| 817 for (it = linkRanges_.begin(); it != linkRanges_.end(); ++it) { |
| 818 // The last part of the URL contains the tag, so it can be retrieved in the |
| 819 // callback. This tag is generally a command ID. |
| 820 std::string url = std::string(kChromeInfobarURL) + |
| 821 std::string(std::to_string((int)it->first)); |
| 822 [labelLinkController_ addLinkWithRange:it->second url:GURL(url)]; |
| 823 } |
| 824 } |
| 825 |
| 826 - (void)addButton1:(NSString*)title1 |
| 827 tag1:(NSInteger)tag1 |
| 828 button2:(NSString*)title2 |
| 829 tag2:(NSInteger)tag2 |
| 830 target:(id)target |
| 831 action:(SEL)action { |
| 832 button1_.reset([[self infoBarButton:title1 |
| 833 palette:[MDCPalette cr_bluePalette] |
| 834 customTitleColor:[UIColor whiteColor] |
| 835 tag:tag1 |
| 836 target:target |
| 837 action:action] retain]); |
| 838 [self addSubview:button1_]; |
| 839 |
| 840 button2_.reset([[self infoBarButton:title2 |
| 841 palette:nil |
| 842 customTitleColor:UIColorFromRGB(kButton2TitleColor) |
| 843 tag:tag2 |
| 844 target:target |
| 845 action:action] retain]); |
| 846 [self addSubview:button2_]; |
| 847 } |
| 848 |
| 849 - (void)addButton:(NSString*)title |
| 850 tag:(NSInteger)tag |
| 851 target:(id)target |
| 852 action:(SEL)action { |
| 853 if (![title length]) |
| 854 return; |
| 855 button1_.reset([[self infoBarButton:title |
| 856 palette:[MDCPalette cr_bluePalette] |
| 857 customTitleColor:[UIColor whiteColor] |
| 858 tag:tag |
| 859 target:target |
| 860 action:action] retain]); |
| 861 [self addSubview:button1_]; |
| 862 } |
| 863 |
| 864 // Initializes and returns a button for the infobar, with the specified |
| 865 // |message| and colors. |
| 866 - (UIButton*)infoBarButton:(NSString*)message |
| 867 palette:(MDCPalette*)palette |
| 868 customTitleColor:(UIColor*)customTitleColor |
| 869 tag:(NSInteger)tag |
| 870 target:(id)target |
| 871 action:(SEL)action { |
| 872 base::scoped_nsobject<MDCFlatButton> button([[MDCFlatButton alloc] init]); |
| 873 button.get().inkColor = [[palette tint300] colorWithAlphaComponent:0.5f]; |
| 874 [button setBackgroundColor:[palette tint500] forState:UIControlStateNormal]; |
| 875 [button setBackgroundColor:[UIColor colorWithWhite:0.8f alpha:1.0f] |
| 876 forState:UIControlStateDisabled]; |
| 877 if (palette) |
| 878 button.get().hasOpaqueBackground = YES; |
| 879 if (customTitleColor) |
| 880 button.get().customTitleColor = customTitleColor; |
| 881 button.get().titleLabel.adjustsFontSizeToFitWidth = YES; |
| 882 button.get().titleLabel.minimumScaleFactor = 0.6f; |
| 883 [button setTitle:message forState:UIControlStateNormal]; |
| 884 [button setTag:tag]; |
| 885 [button addTarget:target |
| 886 action:action |
| 887 forControlEvents:UIControlEventTouchUpInside]; |
| 888 // Without the call to layoutIfNeeded, |button| returns an incorrect |
| 889 // titleLabel the first time it is accessed in |narrowestWidthOfButton|. |
| 890 [button layoutIfNeeded]; |
| 891 return button.autorelease(); |
| 892 } |
| 893 |
| 894 - (CGRect)frameOfCloseButton:(BOOL)singleLineMode { |
| 895 DCHECK(closeButton_); |
| 896 // Add padding to increase the touchable area. |
| 897 CGSize closeButtonSize = [closeButton_ imageView].image.size; |
| 898 closeButtonSize.width += kCloseButtonInnerPadding * 2; |
| 899 closeButtonSize.height += kCloseButtonInnerPadding * 2; |
| 900 CGFloat x = CGRectGetMaxX(self.frame) - closeButtonSize.width; |
| 901 // Aligns the close button at the top (height includes touch padding). |
| 902 CGFloat y = 0; |
| 903 if (singleLineMode) { |
| 904 // On single-line mode the button is centered vertically. |
| 905 y = ui::AlignValueToUpperPixel( |
| 906 (kMinimumInfobarHeight - closeButtonSize.height) * 0.5); |
| 907 } |
| 908 return CGRectMake(x, y, closeButtonSize.width, closeButtonSize.height); |
| 909 } |
| 910 |
| 911 - (CGRect)frameOfIcon { |
| 912 CGSize iconSize = [imageView_ image].size; |
| 913 CGFloat y = kButtonsTopMargin; |
| 914 CGFloat x = kCloseButtonLeftMargin; |
| 915 return CGRectMake(AlignValueToPixel(x), AlignValueToPixel(y), iconSize.width, |
| 916 iconSize.height); |
| 917 } |
| 918 |
| 919 + (NSString*)openingMarkerForLink { |
| 920 return @"$LINK_START"; |
| 921 } |
| 922 |
| 923 + (NSString*)closingMarkerForLink { |
| 924 return @"$LINK_END"; |
| 925 } |
| 926 |
| 927 + (NSString*)stringAsLink:(NSString*)string tag:(NSUInteger)tag { |
| 928 DCHECK_NE(0u, tag); |
| 929 return [NSString stringWithFormat:@"%@(%" PRIuNS ")%@%@", |
| 930 [InfoBarView openingMarkerForLink], tag, |
| 931 string, [InfoBarView closingMarkerForLink]]; |
| 932 } |
| 933 |
| 934 #pragma mark - Testing |
| 935 |
| 936 - (CGFloat)minimumInfobarHeight { |
| 937 return kMinimumInfobarHeight; |
| 938 } |
| 939 |
| 940 - (CGFloat)buttonsHeight { |
| 941 return kButtonHeight; |
| 942 } |
| 943 |
| 944 - (CGFloat)buttonMargin { |
| 945 return kButtonMargin; |
| 946 } |
| 947 |
| 948 - (const std::vector<std::pair<NSUInteger, NSRange>>&)linkRanges { |
| 949 return linkRanges_; |
| 950 } |
| 951 |
| 952 @end |
OLD | NEW |