Index: ios/chrome/browser/ui/infobars/infobar_view.mm |
diff --git a/ios/chrome/browser/ui/infobars/infobar_view.mm b/ios/chrome/browser/ui/infobars/infobar_view.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..7248230b466f752345b418e3629180de1d7bcdf4 |
--- /dev/null |
+++ b/ios/chrome/browser/ui/infobars/infobar_view.mm |
@@ -0,0 +1,952 @@ |
+// Copyright (c) 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/infobars/infobar_view.h" |
+ |
+#import <CoreGraphics/CoreGraphics.h> |
+#import <QuartzCore/QuartzCore.h> |
+ |
+#include "base/format_macros.h" |
+#include "base/i18n/rtl.h" |
+#include "base/ios/weak_nsobject.h" |
+#include "base/logging.h" |
+#include "base/mac/foundation_util.h" |
+#include "base/strings/sys_string_conversions.h" |
+#include "components/strings/grit/components_strings.h" |
+#import "ios/chrome/browser/ui/colors/MDCPalette+CrAdditions.h" |
+#include "ios/chrome/browser/ui/ui_util.h" |
+#import "ios/chrome/browser/ui/uikit_ui_util.h" |
+#import "ios/chrome/browser/ui/util/label_link_controller.h" |
+#import "ios/public/provider/chrome/browser/ui/infobar_view_delegate.h" |
+#import "ios/third_party/material_components_ios/src/components/Buttons/src/MaterialButtons.h" |
+#import "ios/third_party/material_components_ios/src/components/Typography/src/MaterialTypography.h" |
+#include "ui/base/l10n/l10n_util.h" |
+#import "ui/gfx/ios/NSString+CrStringDrawing.h" |
+#import "ui/gfx/ios/uikit_util.h" |
+#include "url/gurl.h" |
+ |
+namespace { |
+ |
+const char kChromeInfobarURL[] = "chromeinternal://infobar/"; |
+ |
+// UX configuration constants for the shadow/rounded corners on the icon. |
+const CGFloat kBaseSizeForEffects = 57.0; |
+const CGFloat kCornerRadius = 10.0; |
+const CGFloat kShadowVerticalOffset = 1.0; |
+const CGFloat kShadowOpacity = 0.5; |
+const CGFloat kShadowRadius = 0.8; |
+ |
+// UX configuration for the layout of items. |
+const CGFloat kLeftMarginOnFirstLineWhenIconAbsent = 20.0; |
+const CGFloat kMinimumSpaceBetweenRightAndLeftAlignedWidgets = 30.0; |
+const CGFloat kRightMargin = 10.0; |
+const CGFloat kSpaceBetweenWidgets = 10.0; |
+const CGFloat kCloseButtonInnerPadding = 16.0; |
+const CGFloat kButtonHeight = 36.0; |
+const CGFloat kButtonMargin = 16.0; |
+const CGFloat kExtraButtonMarginOnSingleLine = 8.0; |
+const CGFloat kButtonSpacing = 8.0; |
+const CGFloat kButtonWidthUnits = 8.0; |
+const CGFloat kButtonsTopMargin = kCloseButtonInnerPadding; |
+const CGFloat kCloseButtonLeftMargin = 16.0; |
+const CGFloat kLabelLineSpacing = 5.0; |
+const CGFloat kLabelMarginBottom = 22.0; |
+const CGFloat kExtraMarginBetweenLabelAndButton = 8.0; |
+const CGFloat kLabelMarginTop = kButtonsTopMargin + 5.0; // Baseline lowered. |
+const CGFloat kMinimumInfobarHeight = 68.0; |
+ |
+const int kButton2TitleColor = 0x4285f4; |
+ |
+enum InfoBarButtonPosition { ON_FIRST_LINE, CENTER, LEFT, RIGHT }; |
+ |
+} // namespace |
+ |
+// UIView containing a switch and a label. |
+@interface SwitchView : BidiContainerView |
+ |
+// Initialize the view's label with |labelText|. |
+- (id)initWithLabel:(NSString*)labelText isOn:(BOOL)isOn; |
+ |
+// Specifies the object, action, and tag used when the switch is toggled. |
+- (void)setTag:(NSInteger)tag target:(id)target action:(SEL)action; |
+ |
+// Returns the height taken by the view constrained by a width of |width|. |
+// If |layout| is yes, it sets the frame of the label and the switch to fit |
+// |width|. |
+- (CGFloat)heightRequiredForSwitchWithWidth:(CGFloat)width layout:(BOOL)layout; |
+ |
+// Returns the preferred width. A smaller width requires eliding the text. |
+- (CGFloat)preferredWidth; |
+ |
+@end |
+ |
+@implementation SwitchView { |
+ base::scoped_nsobject<UILabel> label_; |
+ base::scoped_nsobject<UISwitch> switch_; |
+ CGFloat preferredTotalWidth_; |
+ CGFloat preferredLabelWidth_; |
+} |
+ |
+- (id)initWithLabel:(NSString*)labelText isOn:(BOOL)isOn { |
+ // Creates switch and label. |
+ base::scoped_nsobject<UILabel> tempLabel( |
+ [[UILabel alloc] initWithFrame:CGRectZero]); |
+ [tempLabel setTextAlignment:NSTextAlignmentNatural]; |
+ [tempLabel setFont:[MDCTypography body1Font]]; |
+ [tempLabel setText:labelText]; |
+ [tempLabel setBackgroundColor:[UIColor clearColor]]; |
+ [tempLabel setLineBreakMode:NSLineBreakByWordWrapping]; |
+ [tempLabel setNumberOfLines:0]; |
+ [tempLabel setAdjustsFontSizeToFitWidth:NO]; |
+ base::scoped_nsobject<UISwitch> tempSwitch( |
+ [[UISwitch alloc] initWithFrame:CGRectZero]); |
+ [tempSwitch setExclusiveTouch:YES]; |
+ [tempSwitch setAccessibilityLabel:labelText]; |
+ [tempSwitch setOnTintColor:[[MDCPalette cr_bluePalette] tint500]]; |
+ [tempSwitch setOn:isOn]; |
+ |
+ // Computes the size and initializes the view. |
+ CGSize maxSize = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX); |
+ CGSize labelSize = |
+ [[tempLabel text] cr_boundingSizeWithSize:maxSize font:[tempLabel font]]; |
+ CGSize switchSize = [tempSwitch frame].size; |
+ CGRect frameRect = CGRectMake( |
+ 0, 0, labelSize.width + kSpaceBetweenWidgets + switchSize.width, |
+ std::max(labelSize.height, switchSize.height)); |
+ self = [super initWithFrame:frameRect]; |
+ if (!self) |
+ return nil; |
+ label_.reset([tempLabel retain]); |
+ switch_.reset([tempSwitch retain]); |
+ |
+ // Sets the position of the label and the switch. The label is left aligned |
+ // and the switch is right aligned. Both are vertically centered. |
+ CGRect labelFrame = |
+ CGRectMake(0, (self.frame.size.height - labelSize.height) / 2, |
+ labelSize.width, labelSize.height); |
+ CGRect switchFrame = |
+ CGRectMake(self.frame.size.width - switchSize.width, |
+ (self.frame.size.height - switchSize.height) / 2, |
+ switchSize.width, switchSize.height); |
+ |
+ labelFrame = AlignRectOriginAndSizeToPixels(labelFrame); |
+ switchFrame = AlignRectOriginAndSizeToPixels(switchFrame); |
+ |
+ [label_ setFrame:labelFrame]; |
+ [switch_ setFrame:switchFrame]; |
+ preferredTotalWidth_ = CGRectGetMaxX(switchFrame); |
+ preferredLabelWidth_ = CGRectGetMaxX(labelFrame); |
+ |
+ [self addSubview:label_]; |
+ [self addSubview:switch_]; |
+ return self; |
+} |
+ |
+- (void)setTag:(NSInteger)tag target:(id)target action:(SEL)action { |
+ [switch_ setTag:tag]; |
+ [switch_ addTarget:target |
+ action:action |
+ forControlEvents:UIControlEventValueChanged]; |
+} |
+ |
+- (CGFloat)heightRequiredForSwitchWithWidth:(CGFloat)width layout:(BOOL)layout { |
+ CGFloat widthLeftForLabel = |
+ width - [switch_ frame].size.width - kSpaceBetweenWidgets; |
+ CGSize maxSize = CGSizeMake(widthLeftForLabel, CGFLOAT_MAX); |
+ CGSize labelSize = |
+ [[label_ text] cr_boundingSizeWithSize:maxSize font:[label_ font]]; |
+ CGFloat viewHeight = std::max(labelSize.height, [switch_ frame].size.height); |
+ if (layout) { |
+ // Lays out the label and the switch to fit in {width, viewHeight}. |
+ CGRect newLabelFrame; |
+ newLabelFrame.origin.x = 0; |
+ newLabelFrame.origin.y = (viewHeight - labelSize.height) / 2; |
+ newLabelFrame.size = labelSize; |
+ newLabelFrame = AlignRectOriginAndSizeToPixels(newLabelFrame); |
+ [label_ setFrame:newLabelFrame]; |
+ CGRect newSwitchFrame; |
+ newSwitchFrame.origin.x = |
+ CGRectGetMaxX(newLabelFrame) + kSpaceBetweenWidgets; |
+ newSwitchFrame.origin.y = (viewHeight - [switch_ frame].size.height) / 2; |
+ newSwitchFrame.size = [switch_ frame].size; |
+ newSwitchFrame = AlignRectOriginAndSizeToPixels(newSwitchFrame); |
+ [switch_ setFrame:newSwitchFrame]; |
+ } |
+ return viewHeight; |
+} |
+ |
+- (CGFloat)preferredWidth { |
+ return preferredTotalWidth_; |
+} |
+ |
+@end |
+ |
+@interface InfoBarView (Testing) |
+// Returns the buttons' height. |
+- (CGFloat)buttonsHeight; |
+// Returns the button margin applied in some views. |
+- (CGFloat)buttonMargin; |
+// Returns the height of the infobar, and lays out the subviews if |layout| is |
+// YES. |
+- (CGFloat)computeRequiredHeightAndLayoutSubviews:(BOOL)layout; |
+// Returns the height of the laid out buttons when not on the first line. |
+// Either the buttons are narrow enough and they are on a single line next to |
+// each other, or they are supperposed on top of each other. |
+// Also lays out the buttons when |layout| is YES, in which case it uses |
+// |heightOfFirstLine| to compute their vertical position. |
+- (CGFloat)heightThatFitsButtonsUnderOtherWidgets:(CGFloat)heightOfFirstLine |
+ layout:(BOOL)layout; |
+// The |button| is positioned with the right edge at the specified y-axis |
+// position |rightEdge| and the top row at |y|. |
+// Returns the left edge of the newly-positioned button. |
+- (CGFloat)layoutWideButtonAlignRight:(UIButton*)button |
+ rightEdge:(CGFloat)rightEdge |
+ y:(CGFloat)y; |
+// Returns the minimum height of infobars. |
+- (CGFloat)minimumInfobarHeight; |
+// Returns |string| stripped of the markers specifying the links and fills |
+// |linkRanges_| with the ranges of the enclosed links. |
+- (NSString*)stripMarkersFromString:(NSString*)string; |
+// Returns the ranges of the links and the associated tags. |
+- (const std::vector<std::pair<NSUInteger, NSRange>>&)linkRanges; |
+@end |
+ |
+@interface InfoBarView () |
+ |
+// Returns the marker delimiting the start of a link. |
++ (NSString*)openingMarkerForLink; |
+// Returns the marker delimiting the end of a link. |
++ (NSString*)closingMarkerForLink; |
+ |
+@end |
+ |
+@implementation InfoBarView { |
+ // Delegates UIView events. |
+ InfoBarViewDelegate* delegate_; // weak |
+ // The current height of this infobar (used for animations where part of the |
+ // infobar is hidden). |
+ CGFloat visibleHeight_; |
+ // The height of this infobar when fully visible. |
+ CGFloat targetHeight_; |
+ // View containing |imageView_|. Exists to apply drop shadows to the view. |
+ base::scoped_nsobject<UIView> imageViewContainer_; |
+ // View containing the icon. |
+ base::scoped_nsobject<UIImageView> imageView_; |
+ // Close button. |
+ base::scoped_nsobject<UIButton> closeButton_; |
+ // View containing the switch and its label. |
+ base::scoped_nsobject<SwitchView> switchView_; |
+ // We are using a LabelLinkController with an UILabel to be able to have |
+ // parts of the label underlined and clickable. This label_ may be nil if |
+ // the delegate returns an empty string for GetMessageText(). |
+ base::scoped_nsobject<LabelLinkController> labelLinkController_; |
+ UILabel* label_; // Weak. |
+ // Array of range information. The first element of the pair is the tag of |
+ // the action and the second element is the range defining the link. |
+ std::vector<std::pair<NSUInteger, NSRange>> linkRanges_; |
+ // Text for the label with link markers included. |
+ base::scoped_nsobject<NSString> markedLabel_; |
+ // Buttons. |
+ // button1_ is tagged with ConfirmInfoBarDelegate::BUTTON_OK . |
+ // button2_ is tagged with ConfirmInfoBarDelegate::BUTTON_CANCEL . |
+ base::scoped_nsobject<UIButton> button1_; |
+ base::scoped_nsobject<UIButton> button2_; |
+ // Drop shadow. |
+ base::scoped_nsobject<UIImageView> shadow_; |
+} |
+ |
+@synthesize visibleHeight = visibleHeight_; |
+ |
+- (id)initWithFrame:(CGRect)frame delegate:(InfoBarViewDelegate*)delegate { |
+ self = [super initWithFrame:frame]; |
+ if (self) { |
+ delegate_ = delegate; |
+ // Make the drop shadow. |
+ UIImage* shadowImage = [UIImage imageNamed:@"infobar_shadow"]; |
+ shadow_.reset([[UIImageView alloc] initWithImage:shadowImage]); |
+ [self addSubview:shadow_]; |
+ [self setAutoresizingMask:UIViewAutoresizingFlexibleWidth | |
+ UIViewAutoresizingFlexibleHeight]; |
+ [self setAccessibilityViewIsModal:YES]; |
+ } |
+ return self; |
+} |
+ |
+- (void)dealloc { |
+ [super dealloc]; |
+} |
+ |
+- (NSString*)markedLabel { |
+ return markedLabel_; |
+} |
+ |
+- (void)resetDelegate { |
+ delegate_ = NULL; |
+} |
+ |
+// Returns the width reserved for the icon. |
+- (CGFloat)leftMarginOnFirstLine { |
+ CGFloat leftMargin = 0; |
+ if (imageViewContainer_) { |
+ leftMargin += [self frameOfIcon].size.width; |
+ // The margin between the label and the icon is the same as the margin |
+ // between the edge of the screen and the icon. |
+ leftMargin += 2 * [self frameOfIcon].origin.x; |
+ } else { |
+ leftMargin += kLeftMarginOnFirstLineWhenIconAbsent; |
+ } |
+ return leftMargin; |
+} |
+ |
+// Returns the width reserved for the close button. |
+- (CGFloat)rightMarginOnFirstLine { |
+ return |
+ [closeButton_ imageView].image.size.width + kCloseButtonInnerPadding * 2; |
+} |
+ |
+// Returns the horizontal space available between the icon and the close |
+// button. |
+- (CGFloat)horizontalSpaceAvailableOnFirstLine { |
+ return [self frame].size.width - [self leftMarginOnFirstLine] - |
+ [self rightMarginOnFirstLine]; |
+} |
+ |
+// Returns the height taken by a label constrained by a width of |width|. |
+- (CGFloat)heightRequiredForLabelWithWidth:(CGFloat)width { |
+ return [label_ sizeThatFits:CGSizeMake(width, CGFLOAT_MAX)].height; |
+} |
+ |
+// Returns the width required by a label if it was displayed on a single line. |
+- (CGFloat)widthOfLabelOnASingleLine { |
+ // |label_| can be nil when delegate returns "" for GetMessageText(). |
+ if (!label_) |
+ return 0.0; |
+ CGSize rect = [[label_ text] cr_pixelAlignedSizeWithFont:[label_ font]]; |
+ return rect.width; |
+} |
+ |
+// Returns the minimum size required by |button| to be properly displayed. |
+- (CGFloat)narrowestWidthOfButton:(UIButton*)button { |
+ if (!button) |
+ return 0; |
+ // The button itself is queried for the size. The width is rounded up to be a |
+ // multiple of 8 to fit Material grid spacing requirements. |
+ CGFloat labelWidth = |
+ [button sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)].width; |
+ return ceil(labelWidth / kButtonWidthUnits) * kButtonWidthUnits; |
+} |
+ |
+// Returns the width of the buttons if they are laid out on the first line. |
+- (CGFloat)widthOfButtonsOnFirstLine { |
+ CGFloat width = [self narrowestWidthOfButton:button1_] + |
+ [self narrowestWidthOfButton:button2_]; |
+ if (button1_ && button2_) { |
+ width += kSpaceBetweenWidgets; |
+ } |
+ return width; |
+} |
+ |
+// Returns the width needed for the switch. |
+- (CGFloat)preferredWidthOfSwitch { |
+ return [switchView_ preferredWidth]; |
+} |
+ |
+// Returns the space required to separate the left aligned widgets (label) from |
+// the right aligned widgets (switch, buttons), assuming they fit on one line. |
+- (CGFloat)widthToSeparateRightAndLeftWidgets { |
+ BOOL leftWidgetsArePresent = (label_ != nil); |
+ BOOL rightWidgetsArePresent = button1_ || button2_ || switchView_; |
+ if (!leftWidgetsArePresent || !rightWidgetsArePresent) |
+ return 0; |
+ return kMinimumSpaceBetweenRightAndLeftAlignedWidgets; |
+} |
+ |
+// Returns the space required to separate the switch and the buttons. |
+- (CGFloat)widthToSeparateSwitchAndButtons { |
+ BOOL buttonsArePresent = button1_ || button2_; |
+ BOOL switchIsPresent = (switchView_ != nil); |
+ if (!buttonsArePresent || !switchIsPresent) |
+ return 0; |
+ return kSpaceBetweenWidgets; |
+} |
+ |
+// Lays out |button| at the height |y| and in the position |position|. |
+// Must only be used for wide buttons, i.e. buttons not on the first line. |
+- (void)layoutWideButton:(UIButton*)button |
+ y:(CGFloat)y |
+ position:(InfoBarButtonPosition)position { |
+ CGFloat screenWidth = [self frame].size.width; |
+ CGFloat startPercentage = 0.0; |
+ CGFloat endPercentage = 0.0; |
+ switch (position) { |
+ case LEFT: |
+ startPercentage = 0.0; |
+ endPercentage = 0.5; |
+ break; |
+ case RIGHT: |
+ startPercentage = 0.5; |
+ endPercentage = 1.0; |
+ break; |
+ case CENTER: |
+ startPercentage = 0.0; |
+ endPercentage = 1.0; |
+ break; |
+ case ON_FIRST_LINE: |
+ NOTREACHED(); |
+ } |
+ DCHECK(startPercentage >= 0.0 && startPercentage <= 1.0); |
+ DCHECK(endPercentage >= 0.0 && endPercentage <= 1.0); |
+ DCHECK(startPercentage < endPercentage); |
+ // In Material the button is not stretched to fit the available space. It is |
+ // placed centrally in the allotted space. |
+ CGFloat minX = screenWidth * startPercentage; |
+ CGFloat maxX = screenWidth * endPercentage; |
+ CGFloat midpoint = (minX + maxX) / 2; |
+ CGFloat minWidth = |
+ std::min([self narrowestWidthOfButton:button], maxX - minX); |
+ CGFloat left = midpoint - minWidth / 2; |
+ CGRect frame = CGRectMake(left, y, minWidth, kButtonHeight); |
+ frame = AlignRectOriginAndSizeToPixels(frame); |
+ [button setFrame:frame]; |
+} |
+ |
+- (CGFloat)layoutWideButtonAlignRight:(UIButton*)button |
+ rightEdge:(CGFloat)rightEdge |
+ y:(CGFloat)y { |
+ CGFloat width = [self narrowestWidthOfButton:button]; |
+ CGFloat leftEdge = rightEdge - width; |
+ CGRect frame = CGRectMake(leftEdge, y, width, kButtonHeight); |
+ frame = AlignRectOriginAndSizeToPixels(frame); |
+ [button setFrame:frame]; |
+ return leftEdge; |
+} |
+ |
+- (CGFloat)heightThatFitsButtonsUnderOtherWidgets:(CGFloat)heightOfFirstLine |
+ layout:(BOOL)layout { |
+ if (button1_ && button2_) { |
+ CGFloat halfWidthOfScreen = [self frame].size.width / 2.0; |
+ if ([self narrowestWidthOfButton:button1_] <= halfWidthOfScreen && |
+ [self narrowestWidthOfButton:button2_] <= halfWidthOfScreen) { |
+ // Each button can fit in half the screen's width. |
+ if (layout) { |
+ // When there are two buttons on one line, they are positioned aligned |
+ // right in the available space, spaced apart by kButtonSpacing. |
+ CGFloat leftOfRightmostButton = |
+ [self layoutWideButtonAlignRight:button1_ |
+ rightEdge:CGRectGetWidth(self.bounds) - |
+ kButtonMargin |
+ y:heightOfFirstLine]; |
+ [self layoutWideButtonAlignRight:button2_ |
+ rightEdge:leftOfRightmostButton - kButtonSpacing |
+ y:heightOfFirstLine]; |
+ } |
+ return kButtonHeight; |
+ } else { |
+ // At least one of the two buttons is larger than half the screen's width, |
+ // so |button2_| is placed underneath |button1_|. |
+ if (layout) { |
+ [self layoutWideButton:button1_ y:heightOfFirstLine position:CENTER]; |
+ [self layoutWideButton:button2_ |
+ y:heightOfFirstLine + kButtonHeight |
+ position:CENTER]; |
+ } |
+ return 2 * kButtonHeight; |
+ } |
+ } |
+ // There is at most 1 button to layout. |
+ UIButton* button = button1_ ? button1_ : button2_; |
+ if (button) { |
+ if (layout) { |
+ // Where is there is just one button it is positioned aligned right in the |
+ // available space. |
+ [self |
+ layoutWideButtonAlignRight:button |
+ rightEdge:CGRectGetWidth(self.bounds) - kButtonMargin |
+ y:heightOfFirstLine]; |
+ } |
+ return kButtonHeight; |
+ } |
+ return 0; |
+} |
+ |
+- (CGFloat)computeRequiredHeightAndLayoutSubviews:(BOOL)layout { |
+ CGFloat requiredHeight = 0; |
+ CGFloat widthOfLabel = [self widthOfLabelOnASingleLine] + |
+ [self widthToSeparateRightAndLeftWidgets]; |
+ CGFloat widthOfButtons = [self widthOfButtonsOnFirstLine]; |
+ CGFloat preferredWidthOfSwitch = [self preferredWidthOfSwitch]; |
+ CGFloat widthOfScreen = [self frame].size.width; |
+ CGFloat rightMarginOnFirstLine = [self rightMarginOnFirstLine]; |
+ CGFloat spaceAvailableOnFirstLine = |
+ [self horizontalSpaceAvailableOnFirstLine]; |
+ CGFloat widthOfButtonAndSwitch = widthOfButtons + |
+ [self widthToSeparateSwitchAndButtons] + |
+ preferredWidthOfSwitch; |
+ // Tests if the label, switch, and buttons can fit on a single line. |
+ if (widthOfLabel + widthOfButtonAndSwitch < spaceAvailableOnFirstLine) { |
+ // The label, switch, and buttons can fit on a single line. |
+ requiredHeight = kMinimumInfobarHeight; |
+ if (layout) { |
+ // Lays out the close button. |
+ CGRect buttonFrame = [self frameOfCloseButton:YES]; |
+ [closeButton_ setFrame:buttonFrame]; |
+ // Lays out the label. |
+ CGFloat labelHeight = [self heightRequiredForLabelWithWidth:widthOfLabel]; |
+ CGRect frame = CGRectMake([self leftMarginOnFirstLine], |
+ (kMinimumInfobarHeight - labelHeight) / 2, |
+ [self widthOfLabelOnASingleLine], labelHeight); |
+ frame = AlignRectOriginAndSizeToPixels(frame); |
+ [label_ setFrame:frame]; |
+ // Layouts the buttons. |
+ CGFloat buttonMargin = |
+ rightMarginOnFirstLine + kExtraButtonMarginOnSingleLine; |
+ if (button1_) { |
+ CGFloat width = [self narrowestWidthOfButton:button1_]; |
+ CGFloat offset = width; |
+ frame = CGRectMake(widthOfScreen - buttonMargin - offset, |
+ (kMinimumInfobarHeight - kButtonHeight) / 2, width, |
+ kButtonHeight); |
+ frame = AlignRectOriginAndSizeToPixels(frame); |
+ [button1_ setFrame:frame]; |
+ } |
+ if (button2_) { |
+ CGFloat width = [self narrowestWidthOfButton:button2_]; |
+ CGFloat offset = widthOfButtons; |
+ frame = CGRectMake(widthOfScreen - buttonMargin - offset, |
+ (kMinimumInfobarHeight - kButtonHeight) / 2, width, |
+ frame.size.height = kButtonHeight); |
+ frame = AlignRectOriginAndSizeToPixels(frame); |
+ [button2_ setFrame:frame]; |
+ } |
+ // Lays out the switch view to the left of the buttons. |
+ if (switchView_) { |
+ frame = CGRectMake( |
+ widthOfScreen - buttonMargin - widthOfButtonAndSwitch, |
+ (kMinimumInfobarHeight - [switchView_ frame].size.height) / 2.0, |
+ preferredWidthOfSwitch, [switchView_ frame].size.height); |
+ frame = AlignRectOriginAndSizeToPixels(frame); |
+ [switchView_ setFrame:frame]; |
+ } |
+ } |
+ } else { |
+ // The widgets (label, switch, buttons) can't fit on a single line. Attempts |
+ // to lay out the label and switch on the first line, and the buttons |
+ // underneath. |
+ CGFloat heightOfLabelAndSwitch = 0; |
+ |
+ if (layout) { |
+ // Lays out the close button. |
+ CGRect buttonFrame = [self frameOfCloseButton:NO]; |
+ [closeButton_ setFrame:buttonFrame]; |
+ } |
+ if (widthOfLabel + preferredWidthOfSwitch < spaceAvailableOnFirstLine) { |
+ // The label and switch can fit on the first line. |
+ heightOfLabelAndSwitch = kMinimumInfobarHeight; |
+ if (layout) { |
+ CGFloat labelHeight = |
+ [self heightRequiredForLabelWithWidth:widthOfLabel]; |
+ CGRect labelFrame = |
+ CGRectMake([self leftMarginOnFirstLine], |
+ (heightOfLabelAndSwitch - labelHeight) / 2, |
+ [self widthOfLabelOnASingleLine], labelHeight); |
+ labelFrame = AlignRectOriginAndSizeToPixels(labelFrame); |
+ [label_ setFrame:labelFrame]; |
+ if (switchView_) { |
+ CGRect switchRect = CGRectMake( |
+ widthOfScreen - rightMarginOnFirstLine - preferredWidthOfSwitch, |
+ (heightOfLabelAndSwitch - [switchView_ frame].size.height) / 2, |
+ preferredWidthOfSwitch, [switchView_ frame].size.height); |
+ switchRect = AlignRectOriginAndSizeToPixels(switchRect); |
+ [switchView_ setFrame:switchRect]; |
+ } |
+ } |
+ } else { |
+ // The label and switch can't fit on the first line, so lay them out on |
+ // different lines. |
+ // Computes the height of the label, and optionally lays it out. |
+ CGFloat labelMarginBottom = kLabelMarginBottom; |
+ if (button1_ || button2_) { |
+ // Material features more padding between the label and the button than |
+ // the label and the bottom of the dialog when there is no button. |
+ labelMarginBottom += kExtraMarginBetweenLabelAndButton; |
+ } |
+ CGFloat heightOfLabelWithPadding = |
+ [self heightRequiredForLabelWithWidth:spaceAvailableOnFirstLine] + |
+ kLabelMarginTop + labelMarginBottom; |
+ if (layout) { |
+ CGRect labelFrame = CGRectMake( |
+ [self leftMarginOnFirstLine], kLabelMarginTop, |
+ spaceAvailableOnFirstLine, |
+ heightOfLabelWithPadding - kLabelMarginTop - labelMarginBottom); |
+ labelFrame = AlignRectOriginAndSizeToPixels(labelFrame); |
+ [label_ setFrame:labelFrame]; |
+ } |
+ // Computes the height of the switch view (if any), and optionally lays it |
+ // out. |
+ CGFloat heightOfSwitchWithPadding = 0; |
+ if (switchView_ != nil) { |
+ // The switch view is aligned with the first line's label, hence the |
+ // call to |leftMarginOnFirstLine|. |
+ CGFloat widthAvailableForSwitchView = [self frame].size.width - |
+ [self leftMarginOnFirstLine] - |
+ kRightMargin; |
+ CGFloat heightOfSwitch = [switchView_ |
+ heightRequiredForSwitchWithWidth:widthAvailableForSwitchView |
+ layout:layout]; |
+ // If there are buttons underneath the switch, add padding. |
+ if (button1_ || button2_) { |
+ heightOfSwitchWithPadding = heightOfSwitch + kSpaceBetweenWidgets + |
+ kExtraMarginBetweenLabelAndButton; |
+ } else { |
+ heightOfSwitchWithPadding = heightOfSwitch; |
+ } |
+ if (layout) { |
+ CGRect switchRect = |
+ CGRectMake([self leftMarginOnFirstLine], heightOfLabelWithPadding, |
+ widthAvailableForSwitchView, heightOfSwitch); |
+ switchRect = AlignRectOriginAndSizeToPixels(switchRect); |
+ [switchView_ setFrame:switchRect]; |
+ } |
+ } |
+ heightOfLabelAndSwitch = |
+ std::max(heightOfLabelWithPadding + heightOfSwitchWithPadding, |
+ kMinimumInfobarHeight); |
+ } |
+ // Lays out the button(s) under the label and switch. |
+ CGFloat heightOfButtons = |
+ [self heightThatFitsButtonsUnderOtherWidgets:heightOfLabelAndSwitch |
+ layout:layout]; |
+ requiredHeight = heightOfLabelAndSwitch; |
+ if (heightOfButtons > 0) |
+ requiredHeight += heightOfButtons + kButtonMargin; |
+ } |
+ return requiredHeight; |
+} |
+ |
+- (CGSize)sizeThatFits:(CGSize)size { |
+ CGFloat requiredHeight = [self computeRequiredHeightAndLayoutSubviews:NO]; |
+ return CGSizeMake([self frame].size.width, requiredHeight); |
+} |
+ |
+- (void)layoutSubviews { |
+ // Lays out the position of the icon. |
+ [imageViewContainer_ setFrame:[self frameOfIcon]]; |
+ targetHeight_ = [self computeRequiredHeightAndLayoutSubviews:YES]; |
+ |
+ if (delegate_) |
+ delegate_->SetInfoBarTargetHeight(targetHeight_); |
+ [self resetBackground]; |
+ |
+ // Asks the BidiContainerView to reposition of all the subviews. |
+ for (UIView* view in [self subviews]) |
+ [self setSubviewNeedsAdjustmentForRTL:view]; |
+ [super layoutSubviews]; |
+} |
+ |
+- (void)resetBackground { |
+ UIColor* color = [UIColor whiteColor]; |
+ [self setBackgroundColor:color]; |
+ CGFloat shadowY = 0; |
+ shadowY = -[shadow_ image].size.height; // Shadow above the infobar. |
+ [shadow_ setFrame:CGRectMake(0, shadowY, self.bounds.size.width, |
+ [shadow_ image].size.height)]; |
+ [shadow_ setAutoresizingMask:UIViewAutoresizingFlexibleWidth]; |
+} |
+ |
+- (void)addCloseButtonWithTag:(NSInteger)tag |
+ target:(id)target |
+ action:(SEL)action { |
+ DCHECK(!closeButton_); |
+ // TODO(jeanfrancoisg): Add IDR_ constant and use GetNativeImageNamed(). |
+ // crbug/228611 |
+ NSString* imagePath = |
+ [[NSBundle mainBundle] pathForResource:@"infobar_close" ofType:@"png"]; |
+ UIImage* image = [UIImage imageWithContentsOfFile:imagePath]; |
+ closeButton_.reset([[UIButton buttonWithType:UIButtonTypeCustom] retain]); |
+ [closeButton_ setExclusiveTouch:YES]; |
+ [closeButton_ setImage:image forState:UIControlStateNormal]; |
+ [closeButton_ addTarget:target |
+ action:action |
+ forControlEvents:UIControlEventTouchUpInside]; |
+ [closeButton_ setTag:tag]; |
+ [closeButton_ setAccessibilityLabel:l10n_util::GetNSString(IDS_CLOSE)]; |
+ [self addSubview:closeButton_]; |
+} |
+ |
+- (void)addSwitchWithLabel:(NSString*)label |
+ isOn:(BOOL)isOn |
+ tag:(NSInteger)tag |
+ target:(id)target |
+ action:(SEL)action { |
+ switchView_.reset([[SwitchView alloc] initWithLabel:label isOn:isOn]); |
+ [switchView_ setTag:tag target:target action:action]; |
+ [self addSubview:switchView_]; |
+} |
+ |
+- (void)addLeftIcon:(UIImage*)image { |
+ if (!imageViewContainer_) { |
+ imageViewContainer_.reset([[UIView alloc] init]); |
+ [self addSubview:imageViewContainer_]; |
+ } |
+ imageView_.reset([[UIImageView alloc] initWithImage:image]); |
+ [imageViewContainer_ addSubview:imageView_]; |
+} |
+ |
+- (void)addPlaceholderTransparentIcon:(CGSize const&)imageSize { |
+ UIGraphicsBeginImageContext(imageSize); |
+ UIImage* placeholder = UIGraphicsGetImageFromCurrentImageContext(); |
+ UIGraphicsEndImageContext(); |
+ [self addLeftIcon:placeholder]; |
+} |
+ |
+// Since shadows & rounded corners cannot be applied simultaneously to a |
+// UIView, this method adds rounded corners to the UIImageView and then adds |
+// drop shadow to the UIView containing the UIImageView. |
+- (void)addLeftIconWithRoundedCornersAndShadow:(UIImage*)image { |
+ CGFloat effectScaleFactor = image.size.width / kBaseSizeForEffects; |
+ [self addLeftIcon:image]; |
+ CALayer* layer = [imageView_ layer]; |
+ [layer setMasksToBounds:YES]; |
+ [layer setCornerRadius:kCornerRadius * effectScaleFactor]; |
+ layer = [imageViewContainer_ layer]; |
+ [layer setShadowColor:[UIColor blackColor].CGColor]; |
+ [layer |
+ setShadowOffset:CGSizeMake(0, kShadowVerticalOffset * effectScaleFactor)]; |
+ [layer setShadowOpacity:kShadowOpacity]; |
+ [layer setShadowRadius:kShadowRadius * effectScaleFactor]; |
+ [imageViewContainer_ setClipsToBounds:NO]; |
+} |
+ |
+- (NSString*)stripMarkersFromString:(NSString*)string { |
+ linkRanges_.clear(); |
+ for (;;) { |
+ // Find the opening marker, followed by the tag between parentheses. |
+ NSRange startingRange = |
+ [string rangeOfString:[[InfoBarView openingMarkerForLink] |
+ stringByAppendingString:@"("]]; |
+ if (!startingRange.length) |
+ return [[string copy] autorelease]; |
+ // Read the tag. |
+ NSUInteger beginTag = NSMaxRange(startingRange); |
+ NSRange closingParenthesis = [string |
+ rangeOfString:@")" |
+ options:NSLiteralSearch |
+ range:NSMakeRange(beginTag, [string length] - beginTag)]; |
+ if (closingParenthesis.location == NSNotFound) |
+ return [[string copy] autorelease]; |
+ NSInteger tag = [[string |
+ substringWithRange:NSMakeRange(beginTag, closingParenthesis.location - |
+ beginTag)] integerValue]; |
+ // If the parsing fails, |tag| is 0. Negative values are not allowed. |
+ if (tag <= 0) |
+ return [[string copy] autorelease]; |
+ // Find the closing marker. |
+ startingRange.length = |
+ closingParenthesis.location - startingRange.location + 1; |
+ NSRange endingRange = |
+ [string rangeOfString:[InfoBarView closingMarkerForLink]]; |
+ DCHECK(endingRange.length); |
+ // Compute range of link in stripped string and add it to the array. |
+ NSRange rangeOfLinkInStrippedString = |
+ NSMakeRange(startingRange.location, |
+ endingRange.location - NSMaxRange(startingRange)); |
+ linkRanges_.push_back(std::make_pair(tag, rangeOfLinkInStrippedString)); |
+ // Creates a new string without the markers. |
+ NSString* beforeLink = [string substringToIndex:startingRange.location]; |
+ NSRange rangeOfLink = |
+ NSMakeRange(NSMaxRange(startingRange), |
+ endingRange.location - NSMaxRange(startingRange)); |
+ NSString* link = [string substringWithRange:rangeOfLink]; |
+ NSString* afterLink = [string substringFromIndex:NSMaxRange(endingRange)]; |
+ string = [NSString stringWithFormat:@"%@%@%@", beforeLink, link, afterLink]; |
+ } |
+} |
+ |
+- (void)addLabel:(NSString*)label { |
+ [self addLabel:label target:nil action:nil]; |
+} |
+ |
+- (void)addLabel:(NSString*)text target:(id)target action:(SEL)action { |
+ markedLabel_.reset([text copy]); |
+ if (target) |
+ text = [self stripMarkersFromString:text]; |
+ if ([label_ superview]) { |
+ [label_ removeFromSuperview]; |
+ } |
+ |
+ label_ = [[[UILabel alloc] initWithFrame:CGRectZero] autorelease]; |
+ |
+ UIFont* font = [MDCTypography subheadFont]; |
+ |
+ [label_ setBackgroundColor:[UIColor clearColor]]; |
+ |
+ NSMutableParagraphStyle* paragraphStyle = |
+ [[[NSMutableParagraphStyle alloc] init] autorelease]; |
+ paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping; |
+ paragraphStyle.lineSpacing = kLabelLineSpacing; |
+ NSDictionary* attributes = @{ |
+ NSParagraphStyleAttributeName : paragraphStyle, |
+ NSFontAttributeName : font, |
+ }; |
+ [label_ setNumberOfLines:0]; |
+ |
+ [label_ setAttributedText:[[[NSAttributedString alloc] |
+ initWithString:text |
+ attributes:attributes] autorelease]]; |
+ |
+ [self addSubview:label_]; |
+ |
+ if (linkRanges_.empty()) |
+ return; |
+ |
+ DCHECK([target respondsToSelector:action]); |
+ |
+ labelLinkController_.reset([[LabelLinkController alloc] |
+ initWithLabel:label_ |
+ action:^(const GURL& gurl) { |
+ NSUInteger actionTag = [base::SysUTF8ToNSString( |
+ gurl.ExtractFileName()) integerValue]; |
+ [target performSelector:action withObject:@(actionTag)]; |
+ }]); |
+ |
+ [labelLinkController_ setLinkUnderlineStyle:NSUnderlineStyleSingle]; |
+ [labelLinkController_ setLinkColor:[UIColor blackColor]]; |
+ |
+ std::vector<std::pair<NSUInteger, NSRange>>::const_iterator it; |
+ for (it = linkRanges_.begin(); it != linkRanges_.end(); ++it) { |
+ // The last part of the URL contains the tag, so it can be retrieved in the |
+ // callback. This tag is generally a command ID. |
+ std::string url = std::string(kChromeInfobarURL) + |
+ std::string(std::to_string((int)it->first)); |
+ [labelLinkController_ addLinkWithRange:it->second url:GURL(url)]; |
+ } |
+} |
+ |
+- (void)addButton1:(NSString*)title1 |
+ tag1:(NSInteger)tag1 |
+ button2:(NSString*)title2 |
+ tag2:(NSInteger)tag2 |
+ target:(id)target |
+ action:(SEL)action { |
+ button1_.reset([[self infoBarButton:title1 |
+ palette:[MDCPalette cr_bluePalette] |
+ customTitleColor:[UIColor whiteColor] |
+ tag:tag1 |
+ target:target |
+ action:action] retain]); |
+ [self addSubview:button1_]; |
+ |
+ button2_.reset([[self infoBarButton:title2 |
+ palette:nil |
+ customTitleColor:UIColorFromRGB(kButton2TitleColor) |
+ tag:tag2 |
+ target:target |
+ action:action] retain]); |
+ [self addSubview:button2_]; |
+} |
+ |
+- (void)addButton:(NSString*)title |
+ tag:(NSInteger)tag |
+ target:(id)target |
+ action:(SEL)action { |
+ if (![title length]) |
+ return; |
+ button1_.reset([[self infoBarButton:title |
+ palette:[MDCPalette cr_bluePalette] |
+ customTitleColor:[UIColor whiteColor] |
+ tag:tag |
+ target:target |
+ action:action] retain]); |
+ [self addSubview:button1_]; |
+} |
+ |
+// Initializes and returns a button for the infobar, with the specified |
+// |message| and colors. |
+- (UIButton*)infoBarButton:(NSString*)message |
+ palette:(MDCPalette*)palette |
+ customTitleColor:(UIColor*)customTitleColor |
+ tag:(NSInteger)tag |
+ target:(id)target |
+ action:(SEL)action { |
+ base::scoped_nsobject<MDCFlatButton> button([[MDCFlatButton alloc] init]); |
+ button.get().inkColor = [[palette tint300] colorWithAlphaComponent:0.5f]; |
+ [button setBackgroundColor:[palette tint500] forState:UIControlStateNormal]; |
+ [button setBackgroundColor:[UIColor colorWithWhite:0.8f alpha:1.0f] |
+ forState:UIControlStateDisabled]; |
+ if (palette) |
+ button.get().hasOpaqueBackground = YES; |
+ if (customTitleColor) |
+ button.get().customTitleColor = customTitleColor; |
+ button.get().titleLabel.adjustsFontSizeToFitWidth = YES; |
+ button.get().titleLabel.minimumScaleFactor = 0.6f; |
+ [button setTitle:message forState:UIControlStateNormal]; |
+ [button setTag:tag]; |
+ [button addTarget:target |
+ action:action |
+ forControlEvents:UIControlEventTouchUpInside]; |
+ // Without the call to layoutIfNeeded, |button| returns an incorrect |
+ // titleLabel the first time it is accessed in |narrowestWidthOfButton|. |
+ [button layoutIfNeeded]; |
+ return button.autorelease(); |
+} |
+ |
+- (CGRect)frameOfCloseButton:(BOOL)singleLineMode { |
+ DCHECK(closeButton_); |
+ // Add padding to increase the touchable area. |
+ CGSize closeButtonSize = [closeButton_ imageView].image.size; |
+ closeButtonSize.width += kCloseButtonInnerPadding * 2; |
+ closeButtonSize.height += kCloseButtonInnerPadding * 2; |
+ CGFloat x = CGRectGetMaxX(self.frame) - closeButtonSize.width; |
+ // Aligns the close button at the top (height includes touch padding). |
+ CGFloat y = 0; |
+ if (singleLineMode) { |
+ // On single-line mode the button is centered vertically. |
+ y = ui::AlignValueToUpperPixel( |
+ (kMinimumInfobarHeight - closeButtonSize.height) * 0.5); |
+ } |
+ return CGRectMake(x, y, closeButtonSize.width, closeButtonSize.height); |
+} |
+ |
+- (CGRect)frameOfIcon { |
+ CGSize iconSize = [imageView_ image].size; |
+ CGFloat y = kButtonsTopMargin; |
+ CGFloat x = kCloseButtonLeftMargin; |
+ return CGRectMake(AlignValueToPixel(x), AlignValueToPixel(y), iconSize.width, |
+ iconSize.height); |
+} |
+ |
++ (NSString*)openingMarkerForLink { |
+ return @"$LINK_START"; |
+} |
+ |
++ (NSString*)closingMarkerForLink { |
+ return @"$LINK_END"; |
+} |
+ |
++ (NSString*)stringAsLink:(NSString*)string tag:(NSUInteger)tag { |
+ DCHECK_NE(0u, tag); |
+ return [NSString stringWithFormat:@"%@(%" PRIuNS ")%@%@", |
+ [InfoBarView openingMarkerForLink], tag, |
+ string, [InfoBarView closingMarkerForLink]]; |
+} |
+ |
+#pragma mark - Testing |
+ |
+- (CGFloat)minimumInfobarHeight { |
+ return kMinimumInfobarHeight; |
+} |
+ |
+- (CGFloat)buttonsHeight { |
+ return kButtonHeight; |
+} |
+ |
+- (CGFloat)buttonMargin { |
+ return kButtonMargin; |
+} |
+ |
+- (const std::vector<std::pair<NSUInteger, NSRange>>&)linkRanges { |
+ return linkRanges_; |
+} |
+ |
+@end |