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

Side by Side 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 unified diff | Download patch
OLDNEW
(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/browser/ui/util/label_link_controller.h"
6
7 #include <map>
8 #include <vector>
9
10 #include "base/ios/ios_util.h"
11 #include "base/ios/weak_nsobject.h"
12 #include "base/logging.h"
13 #include "base/mac/foundation_util.h"
14 #include "base/mac/scoped_block.h"
15 #import "base/mac/scoped_nsobject.h"
16 #import "base/strings/sys_string_conversions.h"
17 #include "ios/chrome/browser/ui/ui_util.h"
18 #import "ios/chrome/browser/ui/util/label_observer.h"
19 #import "ios/chrome/browser/ui/util/text_region_mapper.h"
20 #import "ios/chrome/browser/ui/util/transparent_link_button.h"
21 #import "net/base/mac/url_conversions.h"
22 #include "url/gurl.h"
23
24 #pragma mark - LinkLayout
25
26 // Object encapsulating the range of a link and the frames corresponding with
27 // that range.
28 @interface LinkLayout : NSObject {
29 // Backing objects for properties of same name.
30 base::scoped_nsobject<NSArray> _frames;
31 }
32
33 // Designated initializer.
34 - (instancetype)initWithRange:(NSRange)range NS_DESIGNATED_INITIALIZER;
35 - (instancetype)init NS_UNAVAILABLE;
36
37 // The range passed on initialization.
38 @property(nonatomic, readonly) NSRange range;
39
40 // The frames calculated for |_range|.
41 @property(nonatomic, retain) NSArray* frames;
42
43 @end
44
45 @implementation LinkLayout
46
47 @synthesize range = _range;
48
49 - (instancetype)initWithRange:(NSRange)range {
50 if ((self = [super init])) {
51 DCHECK_NE(range.location, static_cast<NSUInteger>(NSNotFound));
52 DCHECK_NE(range.length, 0U);
53 _range = range;
54 }
55 return self;
56 }
57
58 #pragma mark - Accessors
59
60 - (void)setFrames:(NSArray*)frames {
61 _frames.reset([frames retain]);
62 }
63
64 - (NSArray*)frames {
65 return _frames.get();
66 }
67
68 @end
69
70 #pragma mark - LabelLinkController
71
72 @interface LabelLinkController ()
73 // Private property exposed publically in testing interface.
74 @property(nonatomic, assign) Class textMapperClass;
75
76 // The original attributed text set on the label. This may be different from
77 // the label's |attributedText| property, as additional style attributes may be
78 // introduced for links.
79 @property(nonatomic, readonly) NSAttributedString* originalLabelText;
80
81 // The array of TransparentLinkButtons inserted above the label.
82 @property(nonatomic, readonly) NSArray* linkButtons;
83
84 // Adds LabelObserverActions to the LabelObserver corresponding to |_label|.
85 - (void)addLabelObserverActions;
86
87 // Clears all defined links and any data associated with them. Update the
88 // original attributed text from the controlled label.
89 - (void)reset;
90
91 // Handle a change to the label that changes the positioning of glyphs but not
92 // any styling of those glyphs. Forces a recomputation of the tap regions, and
93 // recreates any tap buttons.
94 - (void)labelLayoutInvalidated;
95
96 // Handle a change to the label that changes the glyph style. This forces all of
97 // the link-specific styling applied by this class to be regenerated (which
98 // itself will again re-trigger this method), and because any kind of style
99 // change may alter the position of glyphs, this forces a layout invalidation.
100 - (void)labelStyleInvalidated;
101
102 // Updates the attributed string content of the controlled label to
103 // have the designated link colors and styles.
104 // No-op if no links are defined.
105 - (void)updateStyles;
106
107 // If the controlled label's bounds have changed from the last time tap rects
108 // were updated, determine which regions in the label should be tappable.
109 - (void)updateTapRects;
110
111 // Creates a new text mapper instance with the current label bounds and
112 // attributed text.
113 - (void)resetTextMapper;
114
115 // Clear any tap buttons that have been created, removing them from their
116 // superview if necessary.
117 - (void)clearTapButtons;
118
119 // Updates the tap buttons as detailed below. This method is called every time
120 // tap rects are updated, as well as every time |_label|'s superview changes.
121 // If there are no tap buttons defined, but there are known tap rects, and
122 // |_label| has a superview, then tap buttons are created and added to that
123 // view.
124 // If there are tap buttons, and |_label| has no superview, then the tap buttons
125 // are cleared.
126 // If there are tap buttons, but they are not subviews of |_label|'s superview
127 // (if _label's superview has changed since the buttons were created), then
128 // the tap buttons are migrated into the new superview.
129 - (void)updateTapButtons;
130
131 @end
132
133 @implementation LabelLinkController {
134 // Ivars immutable for the lifetime of the object.
135 base::mac::ScopedBlock<ProceduralBlockWithURL> _action;
136 base::WeakNSObject<UILabel> _label; // weak
137 base::scoped_nsobject<UITapGestureRecognizer> _linkTapRecognizer;
138
139 // Ivas backing properties.
140 base::scoped_nsobject<UIColor> _linkColor;
141 base::scoped_nsobject<UIFont> _linkFont;
142
143 // Ivars that reset when label text changes.
144 base::scoped_nsobject<NSMutableDictionary> _layoutsForURLs;
145 base::scoped_nsobject<NSAttributedString> _originalLabelText;
146 CGRect _lastLabelFrame;
147
148 // Ivars that reset when text or bounds change.
149 base::scoped_nsprotocol<id<TextRegionMapper>> _textMapper;
150
151 // Internal tracking.
152 BOOL _justUpdatedStyles;
153 base::scoped_nsobject<NSMutableArray> _linkButtons;
154 }
155
156 @synthesize showTapAreas = _showTapAreas;
157 @synthesize textMapperClass = _textMapperClass;
158 @synthesize linkUnderlineStyle = _linkUnderlineStyle;
159
160 - (instancetype)initWithLabel:(UILabel*)label
161 action:(ProceduralBlockWithURL)action {
162 if ((self = [super init])) {
163 DCHECK(label);
164 _label.reset(label);
165 _action.reset(action, base::scoped_policy::RETAIN);
166 _linkUnderlineStyle = NSUnderlineStyleNone;
167 [self reset];
168
169 [self addLabelObserverActions];
170
171 self.textMapperClass = [CoreTextRegionMapper class];
172 _linkButtons.reset([[NSMutableArray alloc] init]);
173 }
174 return self;
175 }
176
177 - (NSAttributedString*)originalLabelText {
178 return _originalLabelText.get();
179 }
180
181 - (NSArray*)linkButtons {
182 return _linkButtons.get();
183 }
184
185 - (void)addLabelObserverActions {
186 LabelObserver* observer = [LabelObserver observerForLabel:_label];
187 base::WeakNSObject<LabelLinkController> weakSelf(self);
188 [observer addStyleChangedAction:^(UILabel* label) {
189 // One of the style properties has been changed, which will silently
190 // update the label's attributedText.
191 if (!weakSelf)
192 return;
193 base::scoped_nsobject<LabelLinkController> strongSelf([weakSelf retain]);
194 [strongSelf labelStyleInvalidated];
195 }];
196 [observer addTextChangedAction:^(UILabel* label) {
197 if (!weakSelf)
198 return;
199 base::scoped_nsobject<LabelLinkController> strongSelf([weakSelf retain]);
200 NSString* originalText = [[strongSelf originalLabelText] string];
201 if ([label.text isEqualToString:originalText]) {
202 // The actual text of the label didn't change, so this was a change to
203 // the string attributes only.
204 [strongSelf labelStyleInvalidated];
205 } else {
206 // The label text has changed, so start everything from scratch.
207 [strongSelf reset];
208 }
209 }];
210 [observer addLayoutChangedAction:^(UILabel* label) {
211 if (!weakSelf)
212 return;
213 base::scoped_nsobject<LabelLinkController> strongSelf([weakSelf retain]);
214 [strongSelf labelLayoutInvalidated];
215 NSArray* linkButtons = [strongSelf linkButtons];
216 // If this layout change corresponds to |label|'s moving to a new superview,
217 // update the tap buttons so that they are inserted above |label| in the new
218 // hierarchy.
219 if (linkButtons.count && label.superview != [linkButtons[0] superview])
220 [strongSelf updateTapButtons];
221 }];
222 }
223
224 - (void)dealloc {
225 [self clearTapButtons];
226 [super dealloc];
227 }
228
229 - (void)addLinkWithRange:(NSRange)range url:(GURL)url {
230 DCHECK(url.is_valid());
231 if (!_layoutsForURLs)
232 _layoutsForURLs.reset([[NSMutableDictionary alloc] init]);
233 NSURL* key = net::NSURLWithGURL(url);
234 base::scoped_nsobject<LinkLayout> layout(
235 [[LinkLayout alloc] initWithRange:range]);
236 [_layoutsForURLs setObject:layout forKey:key];
237 [self updateStyles];
238 }
239
240 - (UIColor*)linkColor {
241 return _linkColor.get();
242 }
243
244 - (void)setLinkColor:(UIColor*)linkColor {
245 _linkColor.reset([linkColor copy]);
246 [self updateStyles];
247 }
248
249 - (void)setLinkUnderlineStyle:(NSUnderlineStyle)underlineStyle {
250 _linkUnderlineStyle = underlineStyle;
251 [self updateStyles];
252 }
253
254 - (UIFont*)linkFont {
255 return _linkFont.get();
256 }
257
258 - (void)setLinkFont:(UIFont*)linkFont {
259 _linkFont.reset([linkFont retain]);
260 [self updateStyles];
261 }
262
263 - (void)setShowTapAreas:(BOOL)showTapAreas {
264 #ifndef NDEBUG
265 for (TransparentLinkButton* button in _linkButtons.get()) {
266 button.debug = showTapAreas;
267 }
268 #endif // NDEBUG
269 _showTapAreas = showTapAreas;
270 }
271
272 #pragma mark - internal methods
273
274 - (void)reset {
275 _originalLabelText.reset([[_label attributedText] copy]);
276 _textMapper.reset();
277 _lastLabelFrame = CGRectZero;
278 _layoutsForURLs.reset();
279 }
280
281 - (void)labelLayoutInvalidated {
282 _textMapper.reset();
283 [self updateTapRects];
284 }
285
286 - (void)labelStyleInvalidated {
287 // If the style invalidation was triggered by this class updating link styles,
288 // then the original label text is still correct, but the tap rects still need
289 // to be updated. Otherwise, update the original label text, and then update
290 // styles. This will set |_justUpdatedStyles| and trigger another call to
291 // this method via KVO.
292 if (_justUpdatedStyles) {
293 // TODO(crbug.com/664648): Remove _justUpdatedStyles due to bug that
294 // prevents proper style updates after successive label format changes.
295 _justUpdatedStyles = NO;
296 } else if (![_originalLabelText isEqual:[_label attributedText]]) {
297 _originalLabelText.reset([[_label attributedText] copy]);
298 [self updateStyles];
299 }
300 _lastLabelFrame = CGRectZero;
301 [self labelLayoutInvalidated];
302 }
303
304 - (void)updateStyles {
305 if (![_layoutsForURLs count])
306 return;
307
308 __block base::scoped_nsobject<NSMutableAttributedString> labelText(
309 [_originalLabelText mutableCopy]);
310 [_layoutsForURLs enumerateKeysAndObjectsUsingBlock:^(
311 NSURL* key, LinkLayout* layout, BOOL* stop) {
312 if (_linkColor) {
313 [labelText addAttribute:NSForegroundColorAttributeName
314 value:_linkColor
315 range:layout.range];
316 }
317 if (_linkUnderlineStyle != NSUnderlineStyleNone) {
318 [labelText addAttribute:NSUnderlineStyleAttributeName
319 value:@(_linkUnderlineStyle)
320 range:layout.range];
321 }
322 if (_linkFont) {
323 [labelText addAttribute:NSFontAttributeName
324 value:_linkFont
325 range:layout.range];
326 }
327 }];
328 _justUpdatedStyles = YES;
329 [_label setAttributedText:labelText];
330 _textMapper.reset();
331 }
332
333 - (void)updateTapRects {
334 // Don't update if the label hasn't changed size or position.
335 if (CGRectEqualToRect([_label frame], _lastLabelFrame))
336 return;
337 // Don't update if there are no links.
338 if (![_layoutsForURLs count])
339 return;
340
341 _lastLabelFrame = [_label frame];
342 [self clearTapButtons];
343
344 // If the label bounds are zero in either dimension, no rects are possible.
345 if (0.0 == _lastLabelFrame.size.width || 0.0 == _lastLabelFrame.size.height)
346 return;
347
348 if (!_textMapper)
349 [self resetTextMapper];
350
351 for (LinkLayout* layout in [_layoutsForURLs allValues]) {
352 base::scoped_nsobject<NSMutableArray> frames([[NSMutableArray alloc] init]);
353 NSArray* rects = [_textMapper rectsForRange:layout.range];
354 for (NSUInteger rectIdx = 0; rectIdx < [rects count]; ++rectIdx) {
355 CGRect frame = [rects[rectIdx] CGRectValue];
356 frame = [[_label superview] convertRect:frame fromView:_label];
357 [frames addObject:[NSValue valueWithCGRect:frame]];
358 }
359 layout.frames = frames;
360 }
361 [self updateTapButtons];
362 }
363
364 - (void)resetTextMapper {
365 DCHECK([self.textMapperClass conformsToProtocol:@protocol(TextRegionMapper)]);
366 _textMapper.reset([[self.textMapperClass alloc]
367 initWithAttributedString:[_label attributedText]
368 bounds:[_label bounds]]);
369 }
370
371 - (void)clearTapButtons {
372 for (TransparentLinkButton* button in _linkButtons.get()) {
373 [button removeFromSuperview];
374 }
375 [_linkButtons removeAllObjects];
376 }
377
378 - (void)updateTapButtons {
379 // If the label has no superview, clear any existing buttons.
380 if (![_label superview]) {
381 [self clearTapButtons];
382 return;
383 } else if ([_linkButtons count]) {
384 // If the buttons are currently in some view other than the label's
385 // superview, repatriate them.
386 if (base::mac::ObjCCast<TransparentLinkButton>(_linkButtons[0]).superview !=
387 [_label superview]) {
388 for (TransparentLinkButton* button in _linkButtons.get()) {
389 CGRect newFrame =
390 [[_label superview] convertRect:button.frame fromView:button];
391 [[_label superview] insertSubview:button aboveSubview:_label];
392 button.frame = newFrame;
393 }
394 }
395 }
396 // If there are no buttons, make some and put them in the label's superview.
397 if (![_linkButtons count] && _layoutsForURLs) {
398 [_layoutsForURLs enumerateKeysAndObjectsUsingBlock:^(
399 NSURL* key, LinkLayout* layout, BOOL* stop) {
400 GURL URL = net::GURLWithNSURL(key);
401 NSString* accessibilityLabel =
402 [[_label text] substringWithRange:layout.range];
403 NSArray* buttons =
404 [TransparentLinkButton buttonsForLinkFrames:layout.frames
405 URL:URL
406 accessibilityLabel:accessibilityLabel];
407 for (TransparentLinkButton* button in buttons) {
408 #ifndef NDEBUG
409 button.debug = self.showTapAreas;
410 #endif // NDEBUG
411 [button addTarget:self
412 action:@selector(linkButtonTapped:)
413 forControlEvents:UIControlEventTouchUpInside];
414 [[_label superview] insertSubview:button aboveSubview:_label];
415 [_linkButtons addObject:button];
416 }
417 }];
418 }
419 }
420
421 #pragma mark - Tap Handlers
422
423 - (void)linkButtonTapped:(id)sender {
424 TransparentLinkButton* button =
425 base::mac::ObjCCast<TransparentLinkButton>(sender);
426 _action.get()(button.URL);
427 }
428
429 #pragma mark - Test facilitators
430
431 - (NSArray*)tapRectsForURL:(GURL)url {
432 NSURL* key = net::NSURLWithGURL(url);
433 LinkLayout* layout = [_layoutsForURLs objectForKey:key];
434 return layout.frames;
435 }
436
437 - (void)tapLabelAtPoint:(CGPoint)point {
438 [_layoutsForURLs enumerateKeysAndObjectsUsingBlock:^(
439 NSURL* key, LinkLayout* layout, BOOL* stop) {
440 for (NSValue* frameValue in layout.frames) {
441 CGRect frame = [frameValue CGRectValue];
442 if (CGRectContainsPoint(frame, point)) {
443 _action.get()(net::GURLWithNSURL(key));
444 *stop = YES;
445 break;
446 }
447 }
448 }];
449 }
450
451 @end
OLDNEW
« 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