OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #import "ios/chrome/today_extension/interactive_label.h" |
| 6 |
| 7 #import "base/mac/scoped_block.h" |
| 8 #import "base/mac/scoped_nsobject.h" |
| 9 #import "ios/chrome/common/string_util.h" |
| 10 #include "ios/chrome/today_extension/transparent_button.h" |
| 11 #include "ios/chrome/today_extension/ui_util.h" |
| 12 |
| 13 @interface InteractiveLabel ()<UITextViewDelegate> |
| 14 |
| 15 - (CGRect)frameOfTextRange:(NSRange)range; |
| 16 |
| 17 @end |
| 18 |
| 19 // The UITextView must have selection enabled for the link detection to work. |
| 20 // The FooterLabels must not have selection enabled. Disable it by disabling |
| 21 // becomeFirstResponder. |
| 22 @interface NoSelectionUITextView : UITextView |
| 23 @end |
| 24 |
| 25 @implementation NoSelectionUITextView |
| 26 |
| 27 - (BOOL)canBecomeFirstResponder { |
| 28 return NO; |
| 29 } |
| 30 |
| 31 @end |
| 32 |
| 33 @implementation InteractiveLabel { |
| 34 base::scoped_nsobject<NSString> _labelString; |
| 35 base::scoped_nsobject<NSString> _buttonString; |
| 36 base::scoped_nsobject<UITextView> _label; |
| 37 base::mac::ScopedBlock<ProceduralBlock> _linkBlock; |
| 38 base::mac::ScopedBlock<ProceduralBlock> _buttonBlock; |
| 39 base::scoped_nsobject<TransparentButton> _activationButton; |
| 40 NSRange _buttonRange; |
| 41 base::scoped_nsobject<NSMutableAttributedString> _attributedText; |
| 42 UIEdgeInsets _insets; |
| 43 CGFloat _currentWidth; |
| 44 |
| 45 // These constraints set the position of the button inside |
| 46 base::scoped_nsobject<NSLayoutConstraint> _buttonTopConstraint; |
| 47 base::scoped_nsobject<NSLayoutConstraint> _buttonLeftConstraint; |
| 48 base::scoped_nsobject<NSLayoutConstraint> _buttonHeightConstraint; |
| 49 base::scoped_nsobject<NSLayoutConstraint> _buttonWidthConstraint; |
| 50 } |
| 51 |
| 52 - (instancetype)initWithFrame:(CGRect)frame |
| 53 labelString:(NSString*)labelString |
| 54 fontSize:(CGFloat)fontSize |
| 55 labelAlignment:(NSTextAlignment)labelAlignment |
| 56 insets:(UIEdgeInsets)insets |
| 57 buttonString:(NSString*)buttonString |
| 58 linkBlock:(ProceduralBlock)linkBlock |
| 59 buttonBlock:(ProceduralBlock)buttonBlock { |
| 60 self = [super initWithFrame:frame]; |
| 61 if (self) { |
| 62 _insets = insets; |
| 63 _currentWidth = frame.size.width; |
| 64 // When the first character of the UITextView text as a NSLinkAttributeName |
| 65 // attribute, the lineSpacing attribute of the paragraph style is ignored. |
| 66 // Add a zero width space so the first character is never in a link. |
| 67 NSString* prefixedString = |
| 68 [NSString stringWithFormat:@"\u200B%@", labelString]; |
| 69 _labelString.reset([prefixedString copy]); |
| 70 _linkBlock.reset(linkBlock, base::scoped_policy::RETAIN); |
| 71 |
| 72 _label.reset([[NoSelectionUITextView alloc] initWithFrame:CGRectZero]); |
| 73 [_label setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| 74 [_label setDelegate:self]; |
| 75 [_label setBackgroundColor:[UIColor clearColor]]; |
| 76 [_label setSelectable:YES]; |
| 77 [_label setEditable:NO]; |
| 78 // We want to get rid of the padding of the text in the UITextView. |
| 79 [_label setTextContainerInset:UIEdgeInsetsZero]; |
| 80 [[_label textContainer] setLineFragmentPadding:0]; |
| 81 [self addSubview:_label]; |
| 82 [NSLayoutConstraint activateConstraints:@[ |
| 83 [[_label topAnchor] constraintEqualToAnchor:[self topAnchor] |
| 84 constant:_insets.top], |
| 85 [[_label bottomAnchor] constraintEqualToAnchor:[self bottomAnchor] |
| 86 constant:_insets.bottom], |
| 87 [[_label leadingAnchor] constraintEqualToAnchor:[self leadingAnchor] |
| 88 constant:_insets.left], |
| 89 [[_label trailingAnchor] constraintEqualToAnchor:[self trailingAnchor] |
| 90 constant:-_insets.right] |
| 91 ]]; |
| 92 |
| 93 NSRange linkRange; |
| 94 NSString* text = ParseStringWithLink(_labelString, &linkRange); |
| 95 [_label setAccessibilityTraits:[_label accessibilityTraits] | |
| 96 UIAccessibilityTraitStaticText]; |
| 97 NSString* buttonText = nil; |
| 98 _buttonRange = NSMakeRange(0, 0); |
| 99 BOOL showButton = buttonString && buttonBlock; |
| 100 if (showButton) { |
| 101 _buttonString.reset([buttonString copy]); |
| 102 _buttonBlock.reset(buttonBlock, base::scoped_policy::RETAIN); |
| 103 _activationButton.reset( |
| 104 [[TransparentButton alloc] initWithFrame:CGRectZero]); |
| 105 [_activationButton addTarget:self |
| 106 action:@selector(buttonPressed:) |
| 107 forControlEvents:UIControlEventTouchUpInside]; |
| 108 [_activationButton setBackgroundColor:[UIColor clearColor]]; |
| 109 [_activationButton setInkColor:ui_util::InkColor()]; |
| 110 [_activationButton setCornerRadius:2]; |
| 111 [_activationButton setBorderWidth:1]; |
| 112 [_activationButton setBorderColor:ui_util::BorderColor()]; |
| 113 [self addSubview:_activationButton]; |
| 114 [self bringSubviewToFront:_activationButton]; |
| 115 _buttonTopConstraint.reset([[[_activationButton topAnchor] |
| 116 constraintEqualToAnchor:[self topAnchor]] retain]); |
| 117 _buttonLeftConstraint.reset([[[_activationButton leftAnchor] |
| 118 constraintEqualToAnchor:[self leftAnchor]] retain]); |
| 119 _buttonWidthConstraint.reset([[[_activationButton widthAnchor] |
| 120 constraintEqualToConstant:0] retain]); |
| 121 _buttonHeightConstraint.reset([[[_activationButton heightAnchor] |
| 122 constraintEqualToConstant:0] retain]); |
| 123 [NSLayoutConstraint activateConstraints:@[ |
| 124 _buttonTopConstraint, _buttonLeftConstraint, _buttonWidthConstraint, |
| 125 _buttonHeightConstraint |
| 126 ]]; |
| 127 |
| 128 // Add two spaces before and after the button label to add padding to the |
| 129 // button. |
| 130 buttonText = [NSString stringWithFormat:@" %@ ", _buttonString.get()]; |
| 131 // Replace spaces by non breaking spaces to prevent buttons from wrapping. |
| 132 buttonText = [buttonText stringByReplacingOccurrencesOfString:@" " |
| 133 withString:@"\u00A0"]; |
| 134 // Add space between the text and the button as separator. |
| 135 text = [text stringByAppendingString:@" "]; |
| 136 _buttonRange = NSMakeRange([text length], [buttonText length]); |
| 137 text = [text stringByAppendingString:buttonText]; |
| 138 } |
| 139 |
| 140 base::scoped_nsobject<NSMutableParagraphStyle> paragraphStyle( |
| 141 [[NSMutableParagraphStyle alloc] init]); |
| 142 UIFont* font = [UIFont fontWithName:@"Helvetica" size:fontSize]; |
| 143 [paragraphStyle setLineBreakMode:NSLineBreakByWordWrapping]; |
| 144 [paragraphStyle setLineSpacing:2]; |
| 145 |
| 146 NSDictionary* normalAttributes = @{ |
| 147 NSParagraphStyleAttributeName : paragraphStyle, |
| 148 NSFontAttributeName : font, |
| 149 NSForegroundColorAttributeName : ui_util::FooterTextColor(), |
| 150 NSBackgroundColorAttributeName : [UIColor clearColor], |
| 151 }; |
| 152 NSDictionary* linkAttributes = @{ |
| 153 NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle), |
| 154 NSForegroundColorAttributeName : ui_util::FooterTextColor(), |
| 155 }; |
| 156 NSDictionary* hiddenAttributes = @{ |
| 157 NSForegroundColorAttributeName : [UIColor clearColor], |
| 158 }; |
| 159 |
| 160 _attributedText.reset( |
| 161 [[NSMutableAttributedString alloc] initWithString:text]); |
| 162 |
| 163 [_attributedText setAttributes:normalAttributes |
| 164 range:NSMakeRange(0, text.length)]; |
| 165 |
| 166 NSDictionary* linkURLAttributes = @{ |
| 167 NSFontAttributeName : font, |
| 168 NSLinkAttributeName : |
| 169 [NSURL URLWithString:@"chrometodayextension://footerButtonPressed"] |
| 170 }; |
| 171 [_attributedText setAttributes:linkURLAttributes range:linkRange]; |
| 172 |
| 173 [_attributedText setAttributes:hiddenAttributes range:_buttonRange]; |
| 174 |
| 175 [_label setLinkTextAttributes:linkAttributes]; |
| 176 |
| 177 [_label setAttributedText:_attributedText]; |
| 178 [_label setTextAlignment:labelAlignment]; |
| 179 |
| 180 if (showButton) { |
| 181 base::scoped_nsobject<NSMutableAttributedString> buttonAttributedTitle( |
| 182 [[NSMutableAttributedString alloc] initWithString:buttonText]); |
| 183 [paragraphStyle setLineSpacing:0]; |
| 184 NSDictionary* buttonAttributes = @{ |
| 185 NSParagraphStyleAttributeName : paragraphStyle, |
| 186 NSFontAttributeName : font, |
| 187 NSForegroundColorAttributeName : ui_util::TextColor(), |
| 188 NSBackgroundColorAttributeName : [UIColor clearColor], |
| 189 }; |
| 190 [buttonAttributedTitle setAttributes:buttonAttributes |
| 191 range:NSMakeRange(0, [buttonText length])]; |
| 192 [_activationButton setAttributedTitle:buttonAttributedTitle |
| 193 forState:UIControlStateNormal]; |
| 194 } |
| 195 } |
| 196 return self; |
| 197 } |
| 198 |
| 199 - (CGRect)frameOfTextRange:(NSRange)range { |
| 200 // Temporary set editable flag to access the |UITextInput firstRectForRange:| |
| 201 // method. |
| 202 BOOL editableState = [_label isEditable]; |
| 203 [_label setEditable:YES]; |
| 204 UITextPosition* beginning = [_label beginningOfDocument]; |
| 205 UITextPosition* start = |
| 206 [_label positionFromPosition:beginning offset:range.location]; |
| 207 UITextPosition* end = [_label positionFromPosition:start offset:range.length]; |
| 208 UITextRange* textRange = [_label textRangeFromPosition:start toPosition:end]; |
| 209 CGRect textFrame = [_label firstRectForRange:textRange]; |
| 210 textFrame = [_label convertRect:textFrame fromView:[_label textInputView]]; |
| 211 [_label setEditable:editableState]; |
| 212 return textFrame; |
| 213 } |
| 214 |
| 215 - (void)layoutSubviews { |
| 216 [super layoutSubviews]; |
| 217 |
| 218 if (self.frame.size.width != _currentWidth) { |
| 219 _currentWidth = self.frame.size.width; |
| 220 [self invalidateIntrinsicContentSize]; |
| 221 } |
| 222 |
| 223 if (_activationButton) { |
| 224 CGRect textFrame = [self frameOfTextRange:_buttonRange]; |
| 225 CGRect buttonFrame = [self convertRect:textFrame fromView:_label]; |
| 226 [_buttonTopConstraint setConstant:buttonFrame.origin.y]; |
| 227 [_buttonLeftConstraint setConstant:buttonFrame.origin.x]; |
| 228 [_buttonWidthConstraint setConstant:buttonFrame.size.width]; |
| 229 [_buttonHeightConstraint setConstant:buttonFrame.size.height]; |
| 230 } |
| 231 } |
| 232 |
| 233 - (BOOL)textView:(UITextView*)textView |
| 234 shouldInteractWithURL:(NSURL*)URL |
| 235 inRange:(NSRange)characterRange { |
| 236 _linkBlock.get()(); |
| 237 return NO; |
| 238 } |
| 239 |
| 240 - (void)buttonPressed:(id)sender { |
| 241 _buttonBlock.get()(); |
| 242 } |
| 243 |
| 244 - (CGSize)intrinsicContentSize { |
| 245 return [self sizeThatFits:CGSizeMake(_currentWidth, CGFLOAT_MAX)]; |
| 246 } |
| 247 |
| 248 - (CGSize)sizeThatFits:(CGSize)size { |
| 249 if (![_attributedText length]) |
| 250 return CGSizeMake(size.width, |
| 251 MIN(_insets.top + _insets.bottom, size.height)); |
| 252 |
| 253 // -sizeThatFits: doesn't behaves properly with UITextView. |
| 254 // Therefore, we need to measure text size using |
| 255 // -boundingRectWithSize:options:context:. |
| 256 CGSize constraints = |
| 257 CGSizeMake(size.width - _insets.left - _insets.right, CGFLOAT_MAX); |
| 258 CGRect bounding = [_attributedText |
| 259 boundingRectWithSize:constraints |
| 260 options:static_cast<NSStringDrawingOptions>( |
| 261 NSStringDrawingUsesLineFragmentOrigin | |
| 262 NSStringDrawingUsesFontLeading) |
| 263 context:nil]; |
| 264 return CGSizeMake( |
| 265 size.width, |
| 266 MIN(bounding.size.height + _insets.top + _insets.bottom, size.height)); |
| 267 } |
| 268 |
| 269 @end |
OLD | NEW |