Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(92)

Unified Diff: ios/chrome/browser/ui/util/label_link_controller.mm

Issue 2588733002: Upstream Chrome on iOS source code [9/11]. (Closed)
Patch Set: Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: ios/chrome/browser/ui/util/label_link_controller.mm
diff --git a/ios/chrome/browser/ui/util/label_link_controller.mm b/ios/chrome/browser/ui/util/label_link_controller.mm
new file mode 100644
index 0000000000000000000000000000000000000000..7820ead3e6c3416786bfd0f6997bf849eb50c5e9
--- /dev/null
+++ b/ios/chrome/browser/ui/util/label_link_controller.mm
@@ -0,0 +1,451 @@
+// 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/browser/ui/util/label_link_controller.h"
+
+#include <map>
+#include <vector>
+
+#include "base/ios/ios_util.h"
+#include "base/ios/weak_nsobject.h"
+#include "base/logging.h"
+#include "base/mac/foundation_util.h"
+#include "base/mac/scoped_block.h"
+#import "base/mac/scoped_nsobject.h"
+#import "base/strings/sys_string_conversions.h"
+#include "ios/chrome/browser/ui/ui_util.h"
+#import "ios/chrome/browser/ui/util/label_observer.h"
+#import "ios/chrome/browser/ui/util/text_region_mapper.h"
+#import "ios/chrome/browser/ui/util/transparent_link_button.h"
+#import "net/base/mac/url_conversions.h"
+#include "url/gurl.h"
+
+#pragma mark - LinkLayout
+
+// Object encapsulating the range of a link and the frames corresponding with
+// that range.
+@interface LinkLayout : NSObject {
+ // Backing objects for properties of same name.
+ base::scoped_nsobject<NSArray> _frames;
+}
+
+// Designated initializer.
+- (instancetype)initWithRange:(NSRange)range NS_DESIGNATED_INITIALIZER;
+- (instancetype)init NS_UNAVAILABLE;
+
+// The range passed on initialization.
+@property(nonatomic, readonly) NSRange range;
+
+// The frames calculated for |_range|.
+@property(nonatomic, retain) NSArray* frames;
+
+@end
+
+@implementation LinkLayout
+
+@synthesize range = _range;
+
+- (instancetype)initWithRange:(NSRange)range {
+ if ((self = [super init])) {
+ DCHECK_NE(range.location, static_cast<NSUInteger>(NSNotFound));
+ DCHECK_NE(range.length, 0U);
+ _range = range;
+ }
+ return self;
+}
+
+#pragma mark - Accessors
+
+- (void)setFrames:(NSArray*)frames {
+ _frames.reset([frames retain]);
+}
+
+- (NSArray*)frames {
+ return _frames.get();
+}
+
+@end
+
+#pragma mark - LabelLinkController
+
+@interface LabelLinkController ()
+// Private property exposed publically in testing interface.
+@property(nonatomic, assign) Class textMapperClass;
+
+// The original attributed text set on the label. This may be different from
+// the label's |attributedText| property, as additional style attributes may be
+// introduced for links.
+@property(nonatomic, readonly) NSAttributedString* originalLabelText;
+
+// The array of TransparentLinkButtons inserted above the label.
+@property(nonatomic, readonly) NSArray* linkButtons;
+
+// Adds LabelObserverActions to the LabelObserver corresponding to |_label|.
+- (void)addLabelObserverActions;
+
+// Clears all defined links and any data associated with them. Update the
+// original attributed text from the controlled label.
+- (void)reset;
+
+// Handle a change to the label that changes the positioning of glyphs but not
+// any styling of those glyphs. Forces a recomputation of the tap regions, and
+// recreates any tap buttons.
+- (void)labelLayoutInvalidated;
+
+// Handle a change to the label that changes the glyph style. This forces all of
+// the link-specific styling applied by this class to be regenerated (which
+// itself will again re-trigger this method), and because any kind of style
+// change may alter the position of glyphs, this forces a layout invalidation.
+- (void)labelStyleInvalidated;
+
+// Updates the attributed string content of the controlled label to
+// have the designated link colors and styles.
+// No-op if no links are defined.
+- (void)updateStyles;
+
+// If the controlled label's bounds have changed from the last time tap rects
+// were updated, determine which regions in the label should be tappable.
+- (void)updateTapRects;
+
+// Creates a new text mapper instance with the current label bounds and
+// attributed text.
+- (void)resetTextMapper;
+
+// Clear any tap buttons that have been created, removing them from their
+// superview if necessary.
+- (void)clearTapButtons;
+
+// Updates the tap buttons as detailed below. This method is called every time
+// tap rects are updated, as well as every time |_label|'s superview changes.
+// If there are no tap buttons defined, but there are known tap rects, and
+// |_label| has a superview, then tap buttons are created and added to that
+// view.
+// If there are tap buttons, and |_label| has no superview, then the tap buttons
+// are cleared.
+// If there are tap buttons, but they are not subviews of |_label|'s superview
+// (if _label's superview has changed since the buttons were created), then
+// the tap buttons are migrated into the new superview.
+- (void)updateTapButtons;
+
+@end
+
+@implementation LabelLinkController {
+ // Ivars immutable for the lifetime of the object.
+ base::mac::ScopedBlock<ProceduralBlockWithURL> _action;
+ base::WeakNSObject<UILabel> _label; // weak
+ base::scoped_nsobject<UITapGestureRecognizer> _linkTapRecognizer;
+
+ // Ivas backing properties.
+ base::scoped_nsobject<UIColor> _linkColor;
+ base::scoped_nsobject<UIFont> _linkFont;
+
+ // Ivars that reset when label text changes.
+ base::scoped_nsobject<NSMutableDictionary> _layoutsForURLs;
+ base::scoped_nsobject<NSAttributedString> _originalLabelText;
+ CGRect _lastLabelFrame;
+
+ // Ivars that reset when text or bounds change.
+ base::scoped_nsprotocol<id<TextRegionMapper>> _textMapper;
+
+ // Internal tracking.
+ BOOL _justUpdatedStyles;
+ base::scoped_nsobject<NSMutableArray> _linkButtons;
+}
+
+@synthesize showTapAreas = _showTapAreas;
+@synthesize textMapperClass = _textMapperClass;
+@synthesize linkUnderlineStyle = _linkUnderlineStyle;
+
+- (instancetype)initWithLabel:(UILabel*)label
+ action:(ProceduralBlockWithURL)action {
+ if ((self = [super init])) {
+ DCHECK(label);
+ _label.reset(label);
+ _action.reset(action, base::scoped_policy::RETAIN);
+ _linkUnderlineStyle = NSUnderlineStyleNone;
+ [self reset];
+
+ [self addLabelObserverActions];
+
+ self.textMapperClass = [CoreTextRegionMapper class];
+ _linkButtons.reset([[NSMutableArray alloc] init]);
+ }
+ return self;
+}
+
+- (NSAttributedString*)originalLabelText {
+ return _originalLabelText.get();
+}
+
+- (NSArray*)linkButtons {
+ return _linkButtons.get();
+}
+
+- (void)addLabelObserverActions {
+ LabelObserver* observer = [LabelObserver observerForLabel:_label];
+ base::WeakNSObject<LabelLinkController> weakSelf(self);
+ [observer addStyleChangedAction:^(UILabel* label) {
+ // One of the style properties has been changed, which will silently
+ // update the label's attributedText.
+ if (!weakSelf)
+ return;
+ base::scoped_nsobject<LabelLinkController> strongSelf([weakSelf retain]);
+ [strongSelf labelStyleInvalidated];
+ }];
+ [observer addTextChangedAction:^(UILabel* label) {
+ if (!weakSelf)
+ return;
+ base::scoped_nsobject<LabelLinkController> strongSelf([weakSelf retain]);
+ NSString* originalText = [[strongSelf originalLabelText] string];
+ if ([label.text isEqualToString:originalText]) {
+ // The actual text of the label didn't change, so this was a change to
+ // the string attributes only.
+ [strongSelf labelStyleInvalidated];
+ } else {
+ // The label text has changed, so start everything from scratch.
+ [strongSelf reset];
+ }
+ }];
+ [observer addLayoutChangedAction:^(UILabel* label) {
+ if (!weakSelf)
+ return;
+ base::scoped_nsobject<LabelLinkController> strongSelf([weakSelf retain]);
+ [strongSelf labelLayoutInvalidated];
+ NSArray* linkButtons = [strongSelf linkButtons];
+ // If this layout change corresponds to |label|'s moving to a new superview,
+ // update the tap buttons so that they are inserted above |label| in the new
+ // hierarchy.
+ if (linkButtons.count && label.superview != [linkButtons[0] superview])
+ [strongSelf updateTapButtons];
+ }];
+}
+
+- (void)dealloc {
+ [self clearTapButtons];
+ [super dealloc];
+}
+
+- (void)addLinkWithRange:(NSRange)range url:(GURL)url {
+ DCHECK(url.is_valid());
+ if (!_layoutsForURLs)
+ _layoutsForURLs.reset([[NSMutableDictionary alloc] init]);
+ NSURL* key = net::NSURLWithGURL(url);
+ base::scoped_nsobject<LinkLayout> layout(
+ [[LinkLayout alloc] initWithRange:range]);
+ [_layoutsForURLs setObject:layout forKey:key];
+ [self updateStyles];
+}
+
+- (UIColor*)linkColor {
+ return _linkColor.get();
+}
+
+- (void)setLinkColor:(UIColor*)linkColor {
+ _linkColor.reset([linkColor copy]);
+ [self updateStyles];
+}
+
+- (void)setLinkUnderlineStyle:(NSUnderlineStyle)underlineStyle {
+ _linkUnderlineStyle = underlineStyle;
+ [self updateStyles];
+}
+
+- (UIFont*)linkFont {
+ return _linkFont.get();
+}
+
+- (void)setLinkFont:(UIFont*)linkFont {
+ _linkFont.reset([linkFont retain]);
+ [self updateStyles];
+}
+
+- (void)setShowTapAreas:(BOOL)showTapAreas {
+#ifndef NDEBUG
+ for (TransparentLinkButton* button in _linkButtons.get()) {
+ button.debug = showTapAreas;
+ }
+#endif // NDEBUG
+ _showTapAreas = showTapAreas;
+}
+
+#pragma mark - internal methods
+
+- (void)reset {
+ _originalLabelText.reset([[_label attributedText] copy]);
+ _textMapper.reset();
+ _lastLabelFrame = CGRectZero;
+ _layoutsForURLs.reset();
+}
+
+- (void)labelLayoutInvalidated {
+ _textMapper.reset();
+ [self updateTapRects];
+}
+
+- (void)labelStyleInvalidated {
+ // If the style invalidation was triggered by this class updating link styles,
+ // then the original label text is still correct, but the tap rects still need
+ // to be updated. Otherwise, update the original label text, and then update
+ // styles. This will set |_justUpdatedStyles| and trigger another call to
+ // this method via KVO.
+ if (_justUpdatedStyles) {
+ // TODO(crbug.com/664648): Remove _justUpdatedStyles due to bug that
+ // prevents proper style updates after successive label format changes.
+ _justUpdatedStyles = NO;
+ } else if (![_originalLabelText isEqual:[_label attributedText]]) {
+ _originalLabelText.reset([[_label attributedText] copy]);
+ [self updateStyles];
+ }
+ _lastLabelFrame = CGRectZero;
+ [self labelLayoutInvalidated];
+}
+
+- (void)updateStyles {
+ if (![_layoutsForURLs count])
+ return;
+
+ __block base::scoped_nsobject<NSMutableAttributedString> labelText(
+ [_originalLabelText mutableCopy]);
+ [_layoutsForURLs enumerateKeysAndObjectsUsingBlock:^(
+ NSURL* key, LinkLayout* layout, BOOL* stop) {
+ if (_linkColor) {
+ [labelText addAttribute:NSForegroundColorAttributeName
+ value:_linkColor
+ range:layout.range];
+ }
+ if (_linkUnderlineStyle != NSUnderlineStyleNone) {
+ [labelText addAttribute:NSUnderlineStyleAttributeName
+ value:@(_linkUnderlineStyle)
+ range:layout.range];
+ }
+ if (_linkFont) {
+ [labelText addAttribute:NSFontAttributeName
+ value:_linkFont
+ range:layout.range];
+ }
+ }];
+ _justUpdatedStyles = YES;
+ [_label setAttributedText:labelText];
+ _textMapper.reset();
+}
+
+- (void)updateTapRects {
+ // Don't update if the label hasn't changed size or position.
+ if (CGRectEqualToRect([_label frame], _lastLabelFrame))
+ return;
+ // Don't update if there are no links.
+ if (![_layoutsForURLs count])
+ return;
+
+ _lastLabelFrame = [_label frame];
+ [self clearTapButtons];
+
+ // If the label bounds are zero in either dimension, no rects are possible.
+ if (0.0 == _lastLabelFrame.size.width || 0.0 == _lastLabelFrame.size.height)
+ return;
+
+ if (!_textMapper)
+ [self resetTextMapper];
+
+ for (LinkLayout* layout in [_layoutsForURLs allValues]) {
+ base::scoped_nsobject<NSMutableArray> frames([[NSMutableArray alloc] init]);
+ NSArray* rects = [_textMapper rectsForRange:layout.range];
+ for (NSUInteger rectIdx = 0; rectIdx < [rects count]; ++rectIdx) {
+ CGRect frame = [rects[rectIdx] CGRectValue];
+ frame = [[_label superview] convertRect:frame fromView:_label];
+ [frames addObject:[NSValue valueWithCGRect:frame]];
+ }
+ layout.frames = frames;
+ }
+ [self updateTapButtons];
+}
+
+- (void)resetTextMapper {
+ DCHECK([self.textMapperClass conformsToProtocol:@protocol(TextRegionMapper)]);
+ _textMapper.reset([[self.textMapperClass alloc]
+ initWithAttributedString:[_label attributedText]
+ bounds:[_label bounds]]);
+}
+
+- (void)clearTapButtons {
+ for (TransparentLinkButton* button in _linkButtons.get()) {
+ [button removeFromSuperview];
+ }
+ [_linkButtons removeAllObjects];
+}
+
+- (void)updateTapButtons {
+ // If the label has no superview, clear any existing buttons.
+ if (![_label superview]) {
+ [self clearTapButtons];
+ return;
+ } else if ([_linkButtons count]) {
+ // If the buttons are currently in some view other than the label's
+ // superview, repatriate them.
+ if (base::mac::ObjCCast<TransparentLinkButton>(_linkButtons[0]).superview !=
+ [_label superview]) {
+ for (TransparentLinkButton* button in _linkButtons.get()) {
+ CGRect newFrame =
+ [[_label superview] convertRect:button.frame fromView:button];
+ [[_label superview] insertSubview:button aboveSubview:_label];
+ button.frame = newFrame;
+ }
+ }
+ }
+ // If there are no buttons, make some and put them in the label's superview.
+ if (![_linkButtons count] && _layoutsForURLs) {
+ [_layoutsForURLs enumerateKeysAndObjectsUsingBlock:^(
+ NSURL* key, LinkLayout* layout, BOOL* stop) {
+ GURL URL = net::GURLWithNSURL(key);
+ NSString* accessibilityLabel =
+ [[_label text] substringWithRange:layout.range];
+ NSArray* buttons =
+ [TransparentLinkButton buttonsForLinkFrames:layout.frames
+ URL:URL
+ accessibilityLabel:accessibilityLabel];
+ for (TransparentLinkButton* button in buttons) {
+#ifndef NDEBUG
+ button.debug = self.showTapAreas;
+#endif // NDEBUG
+ [button addTarget:self
+ action:@selector(linkButtonTapped:)
+ forControlEvents:UIControlEventTouchUpInside];
+ [[_label superview] insertSubview:button aboveSubview:_label];
+ [_linkButtons addObject:button];
+ }
+ }];
+ }
+}
+
+#pragma mark - Tap Handlers
+
+- (void)linkButtonTapped:(id)sender {
+ TransparentLinkButton* button =
+ base::mac::ObjCCast<TransparentLinkButton>(sender);
+ _action.get()(button.URL);
+}
+
+#pragma mark - Test facilitators
+
+- (NSArray*)tapRectsForURL:(GURL)url {
+ NSURL* key = net::NSURLWithGURL(url);
+ LinkLayout* layout = [_layoutsForURLs objectForKey:key];
+ return layout.frames;
+}
+
+- (void)tapLabelAtPoint:(CGPoint)point {
+ [_layoutsForURLs enumerateKeysAndObjectsUsingBlock:^(
+ NSURL* key, LinkLayout* layout, BOOL* stop) {
+ for (NSValue* frameValue in layout.frames) {
+ CGRect frame = [frameValue CGRectValue];
+ if (CGRectContainsPoint(frame, point)) {
+ _action.get()(net::GURLWithNSURL(key));
+ *stop = YES;
+ break;
+ }
+ }
+ }];
+}
+
+@end
« no previous file with comments | « ios/chrome/browser/ui/util/label_link_controller.h ('k') | ios/chrome/browser/ui/util/label_link_controller_unittest.mm » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698