Index: ios/chrome/today_extension/interactive_label.mm |
diff --git a/ios/chrome/today_extension/interactive_label.mm b/ios/chrome/today_extension/interactive_label.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..cba913a80dc377b5f4b794b22f2d4784db9f64ef |
--- /dev/null |
+++ b/ios/chrome/today_extension/interactive_label.mm |
@@ -0,0 +1,269 @@ |
+// 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; |
+ base::mac::ScopedBlock<ProceduralBlock> _linkBlock; |
+ base::mac::ScopedBlock<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.reset(linkBlock, base::scoped_policy::RETAIN); |
+ |
+ _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.reset(buttonBlock, base::scoped_policy::RETAIN); |
+ _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]] retain]); |
+ _buttonLeftConstraint.reset([[[_activationButton leftAnchor] |
+ constraintEqualToAnchor:[self leftAnchor]] retain]); |
+ _buttonWidthConstraint.reset([[[_activationButton widthAnchor] |
+ constraintEqualToConstant:0] retain]); |
+ _buttonHeightConstraint.reset([[[_activationButton heightAnchor] |
+ constraintEqualToConstant:0] retain]); |
+ [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 { |
+ _linkBlock.get()(); |
+ return NO; |
+} |
+ |
+- (void)buttonPressed:(id)sender { |
+ _buttonBlock.get()(); |
+} |
+ |
+- (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 |