| 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
|
|
|