blob: 614f350e878d327c85d16a244c994c79131e1f08 [file] [log] [blame]
// Copyright 2015 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/today_extension/interactive_label.h"
#import "base/mac/scoped_block.h"
#import "base/mac/scoped_nsobject.h"
#import "ios/chrome/common/string_util.h"
#include "ios/chrome/today_extension/transparent_button.h"
#include "ios/chrome/today_extension/ui_util.h"
@interface InteractiveLabel ()<UITextViewDelegate>
- (CGRect)frameOfTextRange:(NSRange)range;
@end
// The UITextView must have selection enabled for the link detection to work.
// The FooterLabels must not have selection enabled. Disable it by disabling
// becomeFirstResponder.
@interface NoSelectionUITextView : UITextView
@end
@implementation NoSelectionUITextView
- (BOOL)canBecomeFirstResponder {
return NO;
}
@end
@implementation InteractiveLabel {
base::scoped_nsobject<NSString> _labelString;
base::scoped_nsobject<NSString> _buttonString;
base::scoped_nsobject<UITextView> _label;
ProceduralBlock _linkBlock;
ProceduralBlock _buttonBlock;
base::scoped_nsobject<TransparentButton> _activationButton;
NSRange _buttonRange;
base::scoped_nsobject<NSMutableAttributedString> _attributedText;
UIEdgeInsets _insets;
CGFloat _currentWidth;
// These constraints set the position of the button inside
base::scoped_nsobject<NSLayoutConstraint> _buttonTopConstraint;
base::scoped_nsobject<NSLayoutConstraint> _buttonLeftConstraint;
base::scoped_nsobject<NSLayoutConstraint> _buttonHeightConstraint;
base::scoped_nsobject<NSLayoutConstraint> _buttonWidthConstraint;
}
- (instancetype)initWithFrame:(CGRect)frame
labelString:(NSString*)labelString
fontSize:(CGFloat)fontSize
labelAlignment:(NSTextAlignment)labelAlignment
insets:(UIEdgeInsets)insets
buttonString:(NSString*)buttonString
linkBlock:(ProceduralBlock)linkBlock
buttonBlock:(ProceduralBlock)buttonBlock {
self = [super initWithFrame:frame];
if (self) {
_insets = insets;
_currentWidth = frame.size.width;
// When the first character of the UITextView text as a NSLinkAttributeName
// attribute, the lineSpacing attribute of the paragraph style is ignored.
// Add a zero width space so the first character is never in a link.
NSString* prefixedString =
[NSString stringWithFormat:@"\u200B%@", labelString];
_labelString.reset([prefixedString copy]);
_linkBlock = [linkBlock copy];
_label.reset([[NoSelectionUITextView alloc] initWithFrame:CGRectZero]);
[_label setTranslatesAutoresizingMaskIntoConstraints:NO];
[_label setDelegate:self];
[_label setBackgroundColor:[UIColor clearColor]];
[_label setSelectable:YES];
[_label setEditable:NO];
// We want to get rid of the padding of the text in the UITextView.
[_label setTextContainerInset:UIEdgeInsetsZero];
[[_label textContainer] setLineFragmentPadding:0];
[self addSubview:_label];
[NSLayoutConstraint activateConstraints:@[
[[_label topAnchor] constraintEqualToAnchor:[self topAnchor]
constant:_insets.top],
[[_label bottomAnchor] constraintEqualToAnchor:[self bottomAnchor]
constant:_insets.bottom],
[[_label leadingAnchor] constraintEqualToAnchor:[self leadingAnchor]
constant:_insets.left],
[[_label trailingAnchor] constraintEqualToAnchor:[self trailingAnchor]
constant:-_insets.right]
]];
NSRange linkRange;
NSString* text = ParseStringWithLink(_labelString, &linkRange);
[_label setAccessibilityTraits:[_label accessibilityTraits] |
UIAccessibilityTraitStaticText];
NSString* buttonText = nil;
_buttonRange = NSMakeRange(0, 0);
BOOL showButton = buttonString && buttonBlock;
if (showButton) {
_buttonString.reset([buttonString copy]);
_buttonBlock = [buttonBlock copy];
_activationButton.reset(
[[TransparentButton alloc] initWithFrame:CGRectZero]);
[_activationButton addTarget:self
action:@selector(buttonPressed:)
forControlEvents:UIControlEventTouchUpInside];
[_activationButton setBackgroundColor:[UIColor clearColor]];
[_activationButton setInkColor:ui_util::InkColor()];
[_activationButton setCornerRadius:2];
[_activationButton setBorderWidth:1];
[_activationButton setBorderColor:ui_util::BorderColor()];
[self addSubview:_activationButton];
[self bringSubviewToFront:_activationButton];
_buttonTopConstraint.reset([[_activationButton topAnchor]
constraintEqualToAnchor:[self topAnchor]]);
_buttonLeftConstraint.reset([[_activationButton leftAnchor]
constraintEqualToAnchor:[self leftAnchor]]);
_buttonWidthConstraint.reset(
[[_activationButton widthAnchor] constraintEqualToConstant:0]);
_buttonHeightConstraint.reset(
[[_activationButton heightAnchor] constraintEqualToConstant:0]);
[NSLayoutConstraint activateConstraints:@[
_buttonTopConstraint, _buttonLeftConstraint, _buttonWidthConstraint,
_buttonHeightConstraint
]];
// Add two spaces before and after the button label to add padding to the
// button.
buttonText = [NSString stringWithFormat:@" %@ ", _buttonString.get()];
// Replace spaces by non breaking spaces to prevent buttons from wrapping.
buttonText = [buttonText stringByReplacingOccurrencesOfString:@" "
withString:@"\u00A0"];
// Add space between the text and the button as separator.
text = [text stringByAppendingString:@" "];
_buttonRange = NSMakeRange([text length], [buttonText length]);
text = [text stringByAppendingString:buttonText];
}
base::scoped_nsobject<NSMutableParagraphStyle> paragraphStyle(
[[NSMutableParagraphStyle alloc] init]);
UIFont* font = [UIFont fontWithName:@"Helvetica" size:fontSize];
[paragraphStyle setLineBreakMode:NSLineBreakByWordWrapping];
[paragraphStyle setLineSpacing:2];
NSDictionary* normalAttributes = @{
NSParagraphStyleAttributeName : paragraphStyle,
NSFontAttributeName : font,
NSForegroundColorAttributeName : ui_util::FooterTextColor(),
NSBackgroundColorAttributeName : [UIColor clearColor],
};
NSDictionary* linkAttributes = @{
NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle),
NSForegroundColorAttributeName : ui_util::FooterTextColor(),
};
NSDictionary* hiddenAttributes = @{
NSForegroundColorAttributeName : [UIColor clearColor],
};
_attributedText.reset(
[[NSMutableAttributedString alloc] initWithString:text]);
[_attributedText setAttributes:normalAttributes
range:NSMakeRange(0, text.length)];
NSDictionary* linkURLAttributes = @{
NSFontAttributeName : font,
NSLinkAttributeName :
[NSURL URLWithString:@"chrometodayextension://footerButtonPressed"]
};
[_attributedText setAttributes:linkURLAttributes range:linkRange];
[_attributedText setAttributes:hiddenAttributes range:_buttonRange];
[_label setLinkTextAttributes:linkAttributes];
[_label setAttributedText:_attributedText];
[_label setTextAlignment:labelAlignment];
if (showButton) {
base::scoped_nsobject<NSMutableAttributedString> buttonAttributedTitle(
[[NSMutableAttributedString alloc] initWithString:buttonText]);
[paragraphStyle setLineSpacing:0];
NSDictionary* buttonAttributes = @{
NSParagraphStyleAttributeName : paragraphStyle,
NSFontAttributeName : font,
NSForegroundColorAttributeName : ui_util::TextColor(),
NSBackgroundColorAttributeName : [UIColor clearColor],
};
[buttonAttributedTitle setAttributes:buttonAttributes
range:NSMakeRange(0, [buttonText length])];
[_activationButton setAttributedTitle:buttonAttributedTitle
forState:UIControlStateNormal];
}
}
return self;
}
- (CGRect)frameOfTextRange:(NSRange)range {
// Temporary set editable flag to access the |UITextInput firstRectForRange:|
// method.
BOOL editableState = [_label isEditable];
[_label setEditable:YES];
UITextPosition* beginning = [_label beginningOfDocument];
UITextPosition* start =
[_label positionFromPosition:beginning offset:range.location];
UITextPosition* end = [_label positionFromPosition:start offset:range.length];
UITextRange* textRange = [_label textRangeFromPosition:start toPosition:end];
CGRect textFrame = [_label firstRectForRange:textRange];
textFrame = [_label convertRect:textFrame fromView:[_label textInputView]];
[_label setEditable:editableState];
return textFrame;
}
- (void)layoutSubviews {
[super layoutSubviews];
if (self.frame.size.width != _currentWidth) {
_currentWidth = self.frame.size.width;
[self invalidateIntrinsicContentSize];
}
if (_activationButton) {
CGRect textFrame = [self frameOfTextRange:_buttonRange];
CGRect buttonFrame = [self convertRect:textFrame fromView:_label];
[_buttonTopConstraint setConstant:buttonFrame.origin.y];
[_buttonLeftConstraint setConstant:buttonFrame.origin.x];
[_buttonWidthConstraint setConstant:buttonFrame.size.width];
[_buttonHeightConstraint setConstant:buttonFrame.size.height];
}
}
- (BOOL)textView:(UITextView*)textView
shouldInteractWithURL:(NSURL*)URL
inRange:(NSRange)characterRange {
if (_linkBlock)
_linkBlock();
return NO;
}
- (void)buttonPressed:(id)sender {
if (_buttonBlock)
_buttonBlock();
}
- (CGSize)intrinsicContentSize {
return [self sizeThatFits:CGSizeMake(_currentWidth, CGFLOAT_MAX)];
}
- (CGSize)sizeThatFits:(CGSize)size {
if (![_attributedText length])
return CGSizeMake(size.width,
MIN(_insets.top + _insets.bottom, size.height));
// -sizeThatFits: doesn't behaves properly with UITextView.
// Therefore, we need to measure text size using
// -boundingRectWithSize:options:context:.
CGSize constraints =
CGSizeMake(size.width - _insets.left - _insets.right, CGFLOAT_MAX);
CGRect bounding = [_attributedText
boundingRectWithSize:constraints
options:static_cast<NSStringDrawingOptions>(
NSStringDrawingUsesLineFragmentOrigin |
NSStringDrawingUsesFontLeading)
context:nil];
return CGSizeMake(
size.width,
MIN(bounding.size.height + _insets.top + _insets.bottom, size.height));
}
@end