|
|
@@ -1,1797 +0,0 @@
|
|
|
-
|
|
|
-// Created by Tencent on 2023/06/09.
|
|
|
-// Copyright © 2023 Tencent. All rights reserved.
|
|
|
-// TUIAttributedLabel.m
|
|
|
-
|
|
|
-#import "TUIAttributedLabel.h"
|
|
|
-
|
|
|
-#import <Availability.h>
|
|
|
-#import <QuartzCore/QuartzCore.h>
|
|
|
-#import <objc/runtime.h>
|
|
|
-
|
|
|
-#define kTUILineBreakWordWrapTextWidthScalingFactor (M_PI / M_E)
|
|
|
-
|
|
|
-static CGFloat const TUIFLOAT_MAX = 100000;
|
|
|
-
|
|
|
-NSString *const kTUIStrikeOutAttributeName = @"TUIStrikeOutAttribute";
|
|
|
-NSString *const kTUIBackgroundFillColorAttributeName = @"TUIBackgroundFillColor";
|
|
|
-NSString *const kTUIBackgroundFillPaddingAttributeName = @"TUIBackgroundFillPadding";
|
|
|
-NSString *const kTUIBackgroundStrokeColorAttributeName = @"TUIBackgroundStrokeColor";
|
|
|
-NSString *const kTUIBackgroundLineWidthAttributeName = @"TUIBackgroundLineWidth";
|
|
|
-NSString *const kTUIBackgroundCornerRadiusAttributeName = @"TUIBackgroundCornerRadius";
|
|
|
-
|
|
|
-const NSTextAlignment TUITextAlignmentLeft = NSTextAlignmentLeft;
|
|
|
-const NSTextAlignment TUITextAlignmentCenter = NSTextAlignmentCenter;
|
|
|
-const NSTextAlignment TUITextAlignmentRight = NSTextAlignmentRight;
|
|
|
-const NSTextAlignment TUITextAlignmentJustified = NSTextAlignmentJustified;
|
|
|
-const NSTextAlignment TUITextAlignmentNatural = NSTextAlignmentNatural;
|
|
|
-
|
|
|
-const NSLineBreakMode TUILineBreakByWordWrapping = NSLineBreakByWordWrapping;
|
|
|
-const NSLineBreakMode TUILineBreakByCharWrapping = NSLineBreakByCharWrapping;
|
|
|
-const NSLineBreakMode TUILineBreakByClipping = NSLineBreakByClipping;
|
|
|
-const NSLineBreakMode TUILineBreakByTruncatingHead = NSLineBreakByTruncatingHead;
|
|
|
-const NSLineBreakMode TUILineBreakByTruncatingMiddle = NSLineBreakByTruncatingMiddle;
|
|
|
-const NSLineBreakMode TUILineBreakByTruncatingTail = NSLineBreakByTruncatingTail;
|
|
|
-
|
|
|
-typedef NSTextAlignment TUITextAlignment;
|
|
|
-typedef NSLineBreakMode TUILineBreakMode;
|
|
|
-
|
|
|
-static inline CGFLOAT_TYPE formatCGFloatCeil(CGFLOAT_TYPE cgfloat) {
|
|
|
-#if CGFLOAT_IS_DOUBLE
|
|
|
- return ceil(cgfloat);
|
|
|
-#else
|
|
|
- return ceilf(cgfloat);
|
|
|
-#endif
|
|
|
-}
|
|
|
-
|
|
|
-static inline CGFLOAT_TYPE formatCGFloatFloor(CGFLOAT_TYPE cgfloat) {
|
|
|
-#if CGFLOAT_IS_DOUBLE
|
|
|
- return floor(cgfloat);
|
|
|
-#else
|
|
|
- return floorf(cgfloat);
|
|
|
-#endif
|
|
|
-}
|
|
|
-
|
|
|
-static inline CGFLOAT_TYPE formatCGFloatRound(CGFLOAT_TYPE cgfloat) {
|
|
|
-#if CGFLOAT_IS_DOUBLE
|
|
|
- return round(cgfloat);
|
|
|
-#else
|
|
|
- return roundf(cgfloat);
|
|
|
-#endif
|
|
|
-}
|
|
|
-
|
|
|
-static inline CGFLOAT_TYPE formatCGFloatSqrt(CGFLOAT_TYPE cgfloat) {
|
|
|
-#if CGFLOAT_IS_DOUBLE
|
|
|
- return sqrt(cgfloat);
|
|
|
-#else
|
|
|
- return sqrtf(cgfloat);
|
|
|
-#endif
|
|
|
-}
|
|
|
-
|
|
|
-static inline CGFloat flushFactorForTextAlignment(NSTextAlignment textAlignment) {
|
|
|
- switch (textAlignment) {
|
|
|
- case TUITextAlignmentCenter:
|
|
|
- return 0.5f;
|
|
|
- case TUITextAlignmentRight:
|
|
|
- return 1.0f;
|
|
|
- case TUITextAlignmentLeft:
|
|
|
- default:
|
|
|
- return 0.0f;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-static inline NSDictionary *formatNSAttributedStringAttributesFromLabel(TUIAttributedLabel *label) {
|
|
|
- NSMutableDictionary *mutableAttributes = [NSMutableDictionary dictionary];
|
|
|
-
|
|
|
- [mutableAttributes setObject:label.font forKey:(NSString *)kCTFontAttributeName];
|
|
|
- [mutableAttributes setObject:label.textColor forKey:(NSString *)kCTForegroundColorAttributeName];
|
|
|
- [mutableAttributes setObject:@(label.kern) forKey:(NSString *)kCTKernAttributeName];
|
|
|
-
|
|
|
- NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
|
|
|
- paragraphStyle.alignment = label.textAlignment;
|
|
|
- paragraphStyle.lineSpacing = label.lineSpacing;
|
|
|
- paragraphStyle.minimumLineHeight = label.minimumLineHeight > 0 ? label.minimumLineHeight : label.font.lineHeight * label.lineHeightMultiple;
|
|
|
- paragraphStyle.maximumLineHeight = label.maximumLineHeight > 0 ? label.maximumLineHeight : label.font.lineHeight * label.lineHeightMultiple;
|
|
|
- paragraphStyle.lineHeightMultiple = label.lineHeightMultiple;
|
|
|
- paragraphStyle.firstLineHeadIndent = label.firstLineIndent;
|
|
|
-
|
|
|
- if (label.numberOfLines == 1) {
|
|
|
- paragraphStyle.lineBreakMode = label.lineBreakMode;
|
|
|
- } else {
|
|
|
- paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
|
|
|
- }
|
|
|
-
|
|
|
- [mutableAttributes setObject:paragraphStyle forKey:(NSString *)kCTParagraphStyleAttributeName];
|
|
|
-
|
|
|
- return [NSDictionary dictionaryWithDictionary:mutableAttributes];
|
|
|
-}
|
|
|
-
|
|
|
-static inline CGColorRef formatCGColorRefFromColor(id color);
|
|
|
-static inline NSDictionary *convertNSAttributedStringAttributesToCTAttributes(NSDictionary *attributes);
|
|
|
-
|
|
|
-static inline NSAttributedString *formatNSAttributedStringByScalingFontSize(NSAttributedString *attributedString, CGFloat scale) {
|
|
|
- NSMutableAttributedString *mutableAttributedString = [attributedString mutableCopy];
|
|
|
- [mutableAttributedString enumerateAttribute:(NSString *)kCTFontAttributeName
|
|
|
- inRange:NSMakeRange(0, [mutableAttributedString length])
|
|
|
- options:0
|
|
|
- usingBlock:^(id value, NSRange range, BOOL *__unused stop) {
|
|
|
- UIFont *font = (UIFont *)value;
|
|
|
- if (font) {
|
|
|
- NSString *fontName;
|
|
|
- CGFloat pointSize;
|
|
|
-
|
|
|
- if ([font isKindOfClass:[UIFont class]]) {
|
|
|
- fontName = font.fontName;
|
|
|
- pointSize = font.pointSize;
|
|
|
- } else {
|
|
|
- fontName = (NSString *)CFBridgingRelease(CTFontCopyName((__bridge CTFontRef)font, kCTFontPostScriptNameKey));
|
|
|
- pointSize = CTFontGetSize((__bridge CTFontRef)font);
|
|
|
- }
|
|
|
-
|
|
|
- [mutableAttributedString removeAttribute:(NSString *)kCTFontAttributeName range:range];
|
|
|
- CTFontRef fontRef =
|
|
|
- CTFontCreateWithName((__bridge CFStringRef)fontName, formatCGFloatFloor(pointSize * scale), NULL);
|
|
|
- [mutableAttributedString addAttribute:(NSString *)kCTFontAttributeName value:(__bridge id)fontRef range:range];
|
|
|
- CFRelease(fontRef);
|
|
|
- }
|
|
|
- }];
|
|
|
-
|
|
|
- return mutableAttributedString;
|
|
|
-}
|
|
|
-
|
|
|
-static inline NSAttributedString *formatNSAttributedStringBySettingColorFromContext(NSAttributedString *attributedString, UIColor *color) {
|
|
|
- if (!color) {
|
|
|
- return attributedString;
|
|
|
- }
|
|
|
-
|
|
|
- NSMutableAttributedString *mutableAttributedString = [attributedString mutableCopy];
|
|
|
- [mutableAttributedString enumerateAttribute:(NSString *)kCTForegroundColorFromContextAttributeName
|
|
|
- inRange:NSMakeRange(0, [mutableAttributedString length])
|
|
|
- options:0
|
|
|
- usingBlock:^(id value, NSRange range, __unused BOOL *stop) {
|
|
|
- BOOL usesColorFromContext = (BOOL)value;
|
|
|
- if (usesColorFromContext) {
|
|
|
- [mutableAttributedString
|
|
|
- setAttributes:[NSDictionary dictionaryWithObject:color forKey:(NSString *)kCTForegroundColorAttributeName]
|
|
|
- range:range];
|
|
|
- [mutableAttributedString removeAttribute:(NSString *)kCTForegroundColorFromContextAttributeName range:range];
|
|
|
- }
|
|
|
- }];
|
|
|
-
|
|
|
- return mutableAttributedString;
|
|
|
-}
|
|
|
-
|
|
|
-static inline CGSize formatCTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(CTFramesetterRef framesetter, NSAttributedString *attributedString,
|
|
|
- CGSize size, NSUInteger numberOfLines) {
|
|
|
- CFRange rangeToSize = CFRangeMake(0, (CFIndex)[attributedString length]);
|
|
|
- CGSize constraints = CGSizeMake(size.width, TUIFLOAT_MAX);
|
|
|
-
|
|
|
- if (numberOfLines == 1) {
|
|
|
- // If there is one line, the size that fits is the full width of the line
|
|
|
- constraints = CGSizeMake(TUIFLOAT_MAX, TUIFLOAT_MAX);
|
|
|
- } else if (numberOfLines > 0) {
|
|
|
- // If the line count of the label more than 1, limit the range to size to the number of lines that have been set
|
|
|
- CGMutablePathRef path = CGPathCreateMutable();
|
|
|
- CGPathAddRect(path, NULL, CGRectMake(0.0f, 0.0f, constraints.width, TUIFLOAT_MAX));
|
|
|
- CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
|
|
|
- CFArrayRef lines = CTFrameGetLines(frame);
|
|
|
-
|
|
|
- if (CFArrayGetCount(lines) > 0) {
|
|
|
- NSInteger lastVisibleLineIndex = MIN((CFIndex)numberOfLines, CFArrayGetCount(lines)) - 1;
|
|
|
- CTLineRef lastVisibleLine = CFArrayGetValueAtIndex(lines, lastVisibleLineIndex);
|
|
|
-
|
|
|
- CFRange rangeToLayout = CTLineGetStringRange(lastVisibleLine);
|
|
|
- rangeToSize = CFRangeMake(0, rangeToLayout.location + rangeToLayout.length);
|
|
|
- }
|
|
|
-
|
|
|
- CFRelease(frame);
|
|
|
- CGPathRelease(path);
|
|
|
- }
|
|
|
-
|
|
|
- CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, rangeToSize, NULL, constraints, NULL);
|
|
|
-
|
|
|
- return CGSizeMake(formatCGFloatCeil(suggestedSize.width), formatCGFloatCeil(suggestedSize.height));
|
|
|
-}
|
|
|
-
|
|
|
-@interface TUIAccessibilityElement : UIAccessibilityElement
|
|
|
-@property(nonatomic, weak) UIView *superview;
|
|
|
-@property(nonatomic, assign) CGRect boundingRect;
|
|
|
-@end
|
|
|
-
|
|
|
-@implementation TUIAccessibilityElement
|
|
|
-
|
|
|
-- (CGRect)accessibilityFrame {
|
|
|
- return UIAccessibilityConvertFrameToScreenCoordinates(self.boundingRect, self.superview);
|
|
|
-}
|
|
|
-
|
|
|
-@end
|
|
|
-
|
|
|
-@interface TUIAttributedLabel ()
|
|
|
-@property(readwrite, nonatomic, copy) NSAttributedString *inactiveAttributedText;
|
|
|
-@property(readwrite, nonatomic, copy) NSAttributedString *renderedAttributedText;
|
|
|
-@property(readwrite, atomic, strong) NSDataDetector *dataDetector;
|
|
|
-@property(readwrite, nonatomic, strong) NSArray *linkModels;
|
|
|
-@property(readwrite, nonatomic, strong) TUIAttributedLabelLink *activeLink;
|
|
|
-@property(readwrite, nonatomic, strong) NSArray *accessibilityElements;
|
|
|
-
|
|
|
-- (void)longPressGestureDidFire:(UILongPressGestureRecognizer *)sender;
|
|
|
-@end
|
|
|
-
|
|
|
-@implementation TUIAttributedLabel {
|
|
|
-@private
|
|
|
- BOOL _needsFramesetter;
|
|
|
- CTFramesetterRef _framesetter;
|
|
|
- CTFramesetterRef _highlightFramesetter;
|
|
|
-}
|
|
|
-
|
|
|
-@dynamic text;
|
|
|
-@synthesize attributedText = _attributedText;
|
|
|
-
|
|
|
-#ifndef kCFCoreFoundationVersionNumber_iOS_7_0
|
|
|
-#define kCFCoreFoundationVersionNumber_iOS_7_0 847.2
|
|
|
-#endif
|
|
|
-
|
|
|
-+ (void)load {
|
|
|
- static dispatch_once_t onceToken;
|
|
|
- dispatch_once(&onceToken, ^{
|
|
|
- if (kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iOS_7_0) {
|
|
|
- Class class = [self class];
|
|
|
- Class superclass = class_getSuperclass(class);
|
|
|
-
|
|
|
- NSArray *strings = @[
|
|
|
- NSStringFromSelector(@selector(isAccessibilityElement)),
|
|
|
- NSStringFromSelector(@selector(accessibilityElementCount)),
|
|
|
- NSStringFromSelector(@selector(accessibilityElementAtIndex:)),
|
|
|
- NSStringFromSelector(@selector(indexOfAccessibilityElement:)),
|
|
|
- ];
|
|
|
-
|
|
|
- for (NSString *string in strings) {
|
|
|
- SEL selector = NSSelectorFromString(string);
|
|
|
- IMP superImplementation = class_getMethodImplementation(superclass, selector);
|
|
|
- Method method = class_getInstanceMethod(class, selector);
|
|
|
- const char *types = method_getTypeEncoding(method);
|
|
|
- class_replaceMethod(class, selector, superImplementation, types);
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-- (instancetype)initWithFrame:(CGRect)frame {
|
|
|
- self = [super initWithFrame:frame];
|
|
|
- if (!self) {
|
|
|
- return nil;
|
|
|
- }
|
|
|
-
|
|
|
- [self commonInit];
|
|
|
-
|
|
|
- return self;
|
|
|
-}
|
|
|
-
|
|
|
-- (void)commonInit {
|
|
|
- self.userInteractionEnabled = YES;
|
|
|
-#if !TARGET_OS_TV
|
|
|
- self.multipleTouchEnabled = NO;
|
|
|
-#endif
|
|
|
-
|
|
|
- self.textInsets = UIEdgeInsetsZero;
|
|
|
- self.lineHeightMultiple = 1.0f;
|
|
|
-
|
|
|
- self.linkModels = [NSArray array];
|
|
|
-
|
|
|
- self.linkBackgroundEdgeInset = UIEdgeInsetsMake(0.0f, -1.0f, 0.0f, -1.0f);
|
|
|
-
|
|
|
- NSMutableDictionary *mutableLinkAttributes = [NSMutableDictionary dictionary];
|
|
|
- [mutableLinkAttributes setObject:[NSNumber numberWithBool:YES] forKey:(NSString *)kCTUnderlineStyleAttributeName];
|
|
|
-
|
|
|
- NSMutableDictionary *mutableActiveLinkAttributes = [NSMutableDictionary dictionary];
|
|
|
- [mutableActiveLinkAttributes setObject:[NSNumber numberWithBool:NO] forKey:(NSString *)kCTUnderlineStyleAttributeName];
|
|
|
-
|
|
|
- NSMutableDictionary *mutableInactiveLinkAttributes = [NSMutableDictionary dictionary];
|
|
|
- [mutableInactiveLinkAttributes setObject:[NSNumber numberWithBool:NO] forKey:(NSString *)kCTUnderlineStyleAttributeName];
|
|
|
-
|
|
|
- if ([NSMutableParagraphStyle class]) {
|
|
|
- [mutableLinkAttributes setObject:[UIColor blueColor] forKey:(NSString *)kCTForegroundColorAttributeName];
|
|
|
- [mutableActiveLinkAttributes setObject:[UIColor redColor] forKey:(NSString *)kCTForegroundColorAttributeName];
|
|
|
- [mutableInactiveLinkAttributes setObject:[UIColor grayColor] forKey:(NSString *)kCTForegroundColorAttributeName];
|
|
|
- } else {
|
|
|
- [mutableLinkAttributes setObject:(__bridge id)[[UIColor blueColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName];
|
|
|
- [mutableActiveLinkAttributes setObject:(__bridge id)[[UIColor redColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName];
|
|
|
- [mutableInactiveLinkAttributes setObject:(__bridge id)[[UIColor grayColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName];
|
|
|
- }
|
|
|
-
|
|
|
- self.linkAttributes = [NSDictionary dictionaryWithDictionary:mutableLinkAttributes];
|
|
|
- self.activeLinkAttributes = [NSDictionary dictionaryWithDictionary:mutableActiveLinkAttributes];
|
|
|
- self.inactiveLinkAttributes = [NSDictionary dictionaryWithDictionary:mutableInactiveLinkAttributes];
|
|
|
- _extendsLinkTouchArea = NO;
|
|
|
- _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGestureDidFire:)];
|
|
|
- self.longPressGestureRecognizer.delegate = self;
|
|
|
- [self addGestureRecognizer:self.longPressGestureRecognizer];
|
|
|
-}
|
|
|
-
|
|
|
-- (void)dealloc {
|
|
|
- if (_framesetter) {
|
|
|
- CFRelease(_framesetter);
|
|
|
- }
|
|
|
-
|
|
|
- if (_highlightFramesetter) {
|
|
|
- CFRelease(_highlightFramesetter);
|
|
|
- }
|
|
|
-
|
|
|
- if (_longPressGestureRecognizer) {
|
|
|
- [self removeGestureRecognizer:_longPressGestureRecognizer];
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-#pragma mark -
|
|
|
-
|
|
|
-+ (CGSize)sizeThatFitsAttributedString:(NSAttributedString *)attributedString withConstraints:(CGSize)size limitedToNumberOfLines:(NSUInteger)numberOfLines {
|
|
|
- if (!attributedString || attributedString.length == 0) {
|
|
|
- return CGSizeZero;
|
|
|
- }
|
|
|
-
|
|
|
- CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)attributedString);
|
|
|
-
|
|
|
- CGSize calculatedSize = formatCTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(framesetter, attributedString, size, numberOfLines);
|
|
|
-
|
|
|
- CFRelease(framesetter);
|
|
|
-
|
|
|
- return calculatedSize;
|
|
|
-}
|
|
|
-
|
|
|
-#pragma mark -
|
|
|
-
|
|
|
-- (void)setAttributedText:(NSAttributedString *)text {
|
|
|
- if ([text isEqualToAttributedString:_attributedText]) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- _attributedText = [text copy];
|
|
|
-
|
|
|
- [self setNeedsFramesetter];
|
|
|
- [self setNeedsDisplay];
|
|
|
-
|
|
|
- if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) {
|
|
|
- [self invalidateIntrinsicContentSize];
|
|
|
- }
|
|
|
-
|
|
|
- [super setText:[self.attributedText string]];
|
|
|
-}
|
|
|
-
|
|
|
-- (NSAttributedString *)renderedAttributedText {
|
|
|
- if (!_renderedAttributedText) {
|
|
|
- NSMutableAttributedString *fullString = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText];
|
|
|
-
|
|
|
- if (self.attributedTruncationToken) {
|
|
|
- [fullString appendAttributedString:self.attributedTruncationToken];
|
|
|
- }
|
|
|
-
|
|
|
- NSAttributedString *string = [[NSAttributedString alloc] initWithAttributedString:fullString];
|
|
|
- self.renderedAttributedText = formatNSAttributedStringBySettingColorFromContext(string, self.textColor);
|
|
|
- }
|
|
|
-
|
|
|
- return _renderedAttributedText;
|
|
|
-}
|
|
|
-
|
|
|
-- (NSArray *)links {
|
|
|
- return [_linkModels valueForKey:@"result"];
|
|
|
-}
|
|
|
-
|
|
|
-- (void)setLinkModels:(NSArray *)linkModels {
|
|
|
- _linkModels = linkModels;
|
|
|
-
|
|
|
- self.accessibilityElements = nil;
|
|
|
-}
|
|
|
-
|
|
|
-- (void)setNeedsFramesetter {
|
|
|
- // Reset the rendered attributed text so it has a chance to regenerate
|
|
|
- self.renderedAttributedText = nil;
|
|
|
-
|
|
|
- _needsFramesetter = YES;
|
|
|
-}
|
|
|
-
|
|
|
-- (CTFramesetterRef)framesetter {
|
|
|
- if (_needsFramesetter) {
|
|
|
- @synchronized(self) {
|
|
|
- CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)self.renderedAttributedText);
|
|
|
- [self setFramesetter:framesetter];
|
|
|
- [self setHighlightFramesetter:nil];
|
|
|
- _needsFramesetter = NO;
|
|
|
-
|
|
|
- if (framesetter) {
|
|
|
- CFRelease(framesetter);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return _framesetter;
|
|
|
-}
|
|
|
-
|
|
|
-- (void)setFramesetter:(CTFramesetterRef)framesetter {
|
|
|
- if (framesetter) {
|
|
|
- CFRetain(framesetter);
|
|
|
- }
|
|
|
-
|
|
|
- if (_framesetter) {
|
|
|
- CFRelease(_framesetter);
|
|
|
- }
|
|
|
-
|
|
|
- _framesetter = framesetter;
|
|
|
-}
|
|
|
-
|
|
|
-- (CTFramesetterRef)highlightFramesetter {
|
|
|
- return _highlightFramesetter;
|
|
|
-}
|
|
|
-
|
|
|
-- (void)setHighlightFramesetter:(CTFramesetterRef)highlightFramesetter {
|
|
|
- if (highlightFramesetter) {
|
|
|
- CFRetain(highlightFramesetter);
|
|
|
- }
|
|
|
-
|
|
|
- if (_highlightFramesetter) {
|
|
|
- CFRelease(_highlightFramesetter);
|
|
|
- }
|
|
|
-
|
|
|
- _highlightFramesetter = highlightFramesetter;
|
|
|
-}
|
|
|
-
|
|
|
-#pragma mark -
|
|
|
-
|
|
|
-- (void)setEnabledTextCheckingTypes:(NSTextCheckingTypes)enabledTextCheckingTypes {
|
|
|
- if (self.enabledTextCheckingTypes == enabledTextCheckingTypes) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- _enabledTextCheckingTypes = enabledTextCheckingTypes;
|
|
|
-
|
|
|
- // one detector instance per type (combination), fast reuse e.g. in cells
|
|
|
- static NSMutableDictionary *dataDetectorsByType = nil;
|
|
|
-
|
|
|
- if (!dataDetectorsByType) {
|
|
|
- dataDetectorsByType = [NSMutableDictionary dictionary];
|
|
|
- }
|
|
|
-
|
|
|
- if (enabledTextCheckingTypes) {
|
|
|
- if (![dataDetectorsByType objectForKey:@(enabledTextCheckingTypes)]) {
|
|
|
- NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:enabledTextCheckingTypes error:nil];
|
|
|
- if (detector) {
|
|
|
- [dataDetectorsByType setObject:detector forKey:@(enabledTextCheckingTypes)];
|
|
|
- }
|
|
|
- }
|
|
|
- self.dataDetector = [dataDetectorsByType objectForKey:@(enabledTextCheckingTypes)];
|
|
|
- } else {
|
|
|
- self.dataDetector = nil;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-- (void)addLink:(TUIAttributedLabelLink *)link {
|
|
|
- [self addLinks:@[ link ]];
|
|
|
-}
|
|
|
-
|
|
|
-- (void)addLinks:(NSArray *)links {
|
|
|
- NSMutableArray *mutableLinkModels = [NSMutableArray arrayWithArray:self.linkModels];
|
|
|
-
|
|
|
- NSMutableAttributedString *mutableAttributedString = [self.attributedText mutableCopy];
|
|
|
-
|
|
|
- for (TUIAttributedLabelLink *link in links) {
|
|
|
- if (link.attributes) {
|
|
|
- [mutableAttributedString addAttributes:link.attributes range:link.result.range];
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- self.attributedText = mutableAttributedString;
|
|
|
- [self setNeedsDisplay];
|
|
|
-
|
|
|
- [mutableLinkModels addObjectsFromArray:links];
|
|
|
-
|
|
|
- self.linkModels = [NSArray arrayWithArray:mutableLinkModels];
|
|
|
-}
|
|
|
-
|
|
|
-- (TUIAttributedLabelLink *)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result attributes:(NSDictionary *)attributes {
|
|
|
- return [self addLinksWithTextCheckingResults:@[ result ] attributes:attributes].firstObject;
|
|
|
-}
|
|
|
-
|
|
|
-- (NSArray *)addLinksWithTextCheckingResults:(NSArray *)results attributes:(NSDictionary *)attributes {
|
|
|
- NSMutableArray *links = [NSMutableArray array];
|
|
|
-
|
|
|
- for (NSTextCheckingResult *result in results) {
|
|
|
- NSDictionary *activeAttributes = attributes ? self.activeLinkAttributes : nil;
|
|
|
- NSDictionary *inactiveAttributes = attributes ? self.inactiveLinkAttributes : nil;
|
|
|
-
|
|
|
- TUIAttributedLabelLink *link = [[TUIAttributedLabelLink alloc] initWithAttributes:attributes
|
|
|
- activeAttributes:activeAttributes
|
|
|
- inactiveAttributes:inactiveAttributes
|
|
|
- textCheckingResult:result];
|
|
|
-
|
|
|
- [links addObject:link];
|
|
|
- }
|
|
|
-
|
|
|
- [self addLinks:links];
|
|
|
-
|
|
|
- return links;
|
|
|
-}
|
|
|
-
|
|
|
-- (TUIAttributedLabelLink *)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result {
|
|
|
- return [self addLinkWithTextCheckingResult:result attributes:self.linkAttributes];
|
|
|
-}
|
|
|
-
|
|
|
-- (TUIAttributedLabelLink *)addLinkToURL:(NSURL *)url withRange:(NSRange)range {
|
|
|
- return [self addLinkWithTextCheckingResult:[NSTextCheckingResult linkCheckingResultWithRange:range URL:url]];
|
|
|
-}
|
|
|
-
|
|
|
-- (TUIAttributedLabelLink *)addLinkToAddress:(NSDictionary *)addressComponents withRange:(NSRange)range {
|
|
|
- return [self addLinkWithTextCheckingResult:[NSTextCheckingResult addressCheckingResultWithRange:range components:addressComponents]];
|
|
|
-}
|
|
|
-
|
|
|
-- (TUIAttributedLabelLink *)addLinkToPhoneNumber:(NSString *)phoneNumber withRange:(NSRange)range {
|
|
|
- return [self addLinkWithTextCheckingResult:[NSTextCheckingResult phoneNumberCheckingResultWithRange:range phoneNumber:phoneNumber]];
|
|
|
-}
|
|
|
-
|
|
|
-- (TUIAttributedLabelLink *)addLinkToDate:(NSDate *)date withRange:(NSRange)range {
|
|
|
- return [self addLinkWithTextCheckingResult:[NSTextCheckingResult dateCheckingResultWithRange:range date:date]];
|
|
|
-}
|
|
|
-
|
|
|
-- (TUIAttributedLabelLink *)addLinkToDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone duration:(NSTimeInterval)duration withRange:(NSRange)range {
|
|
|
- return [self addLinkWithTextCheckingResult:[NSTextCheckingResult dateCheckingResultWithRange:range date:date timeZone:timeZone duration:duration]];
|
|
|
-}
|
|
|
-
|
|
|
-- (TUIAttributedLabelLink *)addLinkToTransitInformation:(NSDictionary *)components withRange:(NSRange)range {
|
|
|
- return [self addLinkWithTextCheckingResult:[NSTextCheckingResult transitInformationCheckingResultWithRange:range components:components]];
|
|
|
-}
|
|
|
-
|
|
|
-#pragma mark -
|
|
|
-
|
|
|
-- (BOOL)containslinkAtPoint:(CGPoint)point {
|
|
|
- return [self linkAtPoint:point] != nil;
|
|
|
-}
|
|
|
-
|
|
|
-- (TUIAttributedLabelLink *)linkAtPoint:(CGPoint)point {
|
|
|
- // Stop quickly if none of the points to be tested are in the bounds.
|
|
|
- if (!CGRectContainsPoint(CGRectInset(self.bounds, -15.f, -15.f), point) || self.links.count == 0) {
|
|
|
- return nil;
|
|
|
- }
|
|
|
-
|
|
|
- TUIAttributedLabelLink *result = [self linkAtCharacterIndex:[self characterIndexAtPoint:point]];
|
|
|
-
|
|
|
- if (!result && self.extendsLinkTouchArea) {
|
|
|
- result = [self linkAtRadius:2.5f aroundPoint:point]
|
|
|
- ?: [self linkAtRadius:5.f aroundPoint:point]
|
|
|
- ?: [self linkAtRadius:7.5f aroundPoint:point]
|
|
|
- ?: [self linkAtRadius:12.5f aroundPoint:point]
|
|
|
- ?: [self linkAtRadius:15.f aroundPoint:point];
|
|
|
- }
|
|
|
-
|
|
|
- return result;
|
|
|
-}
|
|
|
-
|
|
|
-- (TUIAttributedLabelLink *)linkAtRadius:(const CGFloat)radius aroundPoint:(CGPoint)point {
|
|
|
- const CGFloat diagonal = formatCGFloatSqrt(2 * radius * radius);
|
|
|
- const CGPoint deltas[] = {
|
|
|
- CGPointMake(0, -radius), CGPointMake(0, radius), // Above and below
|
|
|
- CGPointMake(-radius, 0), CGPointMake(radius, 0), // Beside
|
|
|
- CGPointMake(-diagonal, -diagonal), CGPointMake(-diagonal, diagonal), CGPointMake(diagonal, diagonal), CGPointMake(diagonal, -diagonal) // Diagonal
|
|
|
- };
|
|
|
- const size_t count = sizeof(deltas) / sizeof(CGPoint);
|
|
|
-
|
|
|
- TUIAttributedLabelLink *link = nil;
|
|
|
-
|
|
|
- for (NSUInteger i = 0; i < count && link.result == nil; i++) {
|
|
|
- CGPoint currentPoint = CGPointMake(point.x + deltas[i].x, point.y + deltas[i].y);
|
|
|
- link = [self linkAtCharacterIndex:[self characterIndexAtPoint:currentPoint]];
|
|
|
- }
|
|
|
-
|
|
|
- return link;
|
|
|
-}
|
|
|
-
|
|
|
-- (TUIAttributedLabelLink *)linkAtCharacterIndex:(CFIndex)idx {
|
|
|
- // Do not enumerate if the index is outside of the bounds of the text.
|
|
|
- if (!NSLocationInRange((NSUInteger)idx, NSMakeRange(0, self.attributedText.length))) {
|
|
|
- return nil;
|
|
|
- }
|
|
|
-
|
|
|
- NSEnumerator *enumerator = [self.linkModels reverseObjectEnumerator];
|
|
|
- TUIAttributedLabelLink *link = nil;
|
|
|
- while ((link = [enumerator nextObject])) {
|
|
|
- if (NSLocationInRange((NSUInteger)idx, link.result.range)) {
|
|
|
- return link;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return nil;
|
|
|
-}
|
|
|
-
|
|
|
-- (CFIndex)characterIndexAtPoint:(CGPoint)p {
|
|
|
- if (!CGRectContainsPoint(self.bounds, p)) {
|
|
|
- return NSNotFound;
|
|
|
- }
|
|
|
-
|
|
|
- CGRect textRect = [self textRectForBounds:self.bounds limitedToNumberOfLines:self.numberOfLines];
|
|
|
- if (!CGRectContainsPoint(textRect, p)) {
|
|
|
- return NSNotFound;
|
|
|
- }
|
|
|
-
|
|
|
- // Offset tap coordinates by textRect origin to make them relative to the origin of frame
|
|
|
- p = CGPointMake(p.x - textRect.origin.x, p.y - textRect.origin.y);
|
|
|
- // Convert tap coordinates (start at top left) to CT coordinates (start at bottom left)
|
|
|
- p = CGPointMake(p.x, textRect.size.height - p.y);
|
|
|
-
|
|
|
- CGMutablePathRef path = CGPathCreateMutable();
|
|
|
- CGPathAddRect(path, NULL, textRect);
|
|
|
- CTFrameRef frame = CTFramesetterCreateFrame([self framesetter], CFRangeMake(0, (CFIndex)[self.attributedText length]), path, NULL);
|
|
|
- if (frame == NULL) {
|
|
|
- CGPathRelease(path);
|
|
|
- return NSNotFound;
|
|
|
- }
|
|
|
-
|
|
|
- CFArrayRef lines = CTFrameGetLines(frame);
|
|
|
- NSInteger numberOfLines = self.numberOfLines > 0 ? MIN(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines);
|
|
|
- if (numberOfLines == 0) {
|
|
|
- CFRelease(frame);
|
|
|
- CGPathRelease(path);
|
|
|
- return NSNotFound;
|
|
|
- }
|
|
|
-
|
|
|
- CFIndex idx = NSNotFound;
|
|
|
-
|
|
|
- CGPoint lineOrigins[numberOfLines];
|
|
|
- CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);
|
|
|
-
|
|
|
- for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) {
|
|
|
- CGPoint lineOrigin = lineOrigins[lineIndex];
|
|
|
- CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
|
|
|
-
|
|
|
- // Get bounding information of line
|
|
|
- CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
|
|
|
- CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
|
|
|
- CGFloat yMin = (CGFloat)floor(lineOrigin.y - descent);
|
|
|
- CGFloat yMax = (CGFloat)ceil(lineOrigin.y + ascent);
|
|
|
-
|
|
|
- // Apply penOffset using flushFactor for horizontal alignment to set lineOrigin since this is the horizontal offset from drawFramesetter
|
|
|
- CGFloat flushFactor = flushFactorForTextAlignment(self.textAlignment);
|
|
|
- CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, textRect.size.width);
|
|
|
- lineOrigin.x = penOffset;
|
|
|
-
|
|
|
- // Check if we've already passed the line
|
|
|
- if (p.y > yMax) {
|
|
|
- break;
|
|
|
- }
|
|
|
- // Check if the point is within this line vertically
|
|
|
- if (p.y >= yMin) {
|
|
|
- // Check if the point is within this line horizontally
|
|
|
- if (p.x >= lineOrigin.x && p.x <= lineOrigin.x + width) {
|
|
|
- // Convert CT coordinates to line-relative coordinates
|
|
|
- CGPoint relativePoint = CGPointMake(p.x - lineOrigin.x, p.y - lineOrigin.y);
|
|
|
- idx = CTLineGetStringIndexForPosition(line, relativePoint);
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- CFRelease(frame);
|
|
|
- CGPathRelease(path);
|
|
|
-
|
|
|
- return idx;
|
|
|
-}
|
|
|
-
|
|
|
-- (CGRect)boundingRectForCharacterRange:(NSRange)range {
|
|
|
- NSMutableAttributedString *mutableAttributedString = [self.attributedText mutableCopy];
|
|
|
-
|
|
|
- NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:mutableAttributedString];
|
|
|
-
|
|
|
- NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
|
|
|
- [textStorage addLayoutManager:layoutManager];
|
|
|
-
|
|
|
- NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:self.bounds.size];
|
|
|
- [layoutManager addTextContainer:textContainer];
|
|
|
-
|
|
|
- NSRange glyphRange;
|
|
|
- [layoutManager characterRangeForGlyphRange:range actualGlyphRange:&glyphRange];
|
|
|
-
|
|
|
- return [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];
|
|
|
-}
|
|
|
-
|
|
|
-- (void)drawFramesetter:(CTFramesetterRef)framesetter
|
|
|
- attributedString:(NSAttributedString *)attributedString
|
|
|
- textRange:(CFRange)textRange
|
|
|
- inRect:(CGRect)rect
|
|
|
- context:(CGContextRef)c {
|
|
|
- CGMutablePathRef path = CGPathCreateMutable();
|
|
|
- CGPathAddRect(path, NULL, rect);
|
|
|
- CTFrameRef frame = CTFramesetterCreateFrame(framesetter, textRange, path, NULL);
|
|
|
-
|
|
|
- [self drawBackground:frame inRect:rect context:c];
|
|
|
-
|
|
|
- CFArrayRef lines = CTFrameGetLines(frame);
|
|
|
- NSInteger numberOfLines = self.numberOfLines > 0 ? MIN(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines);
|
|
|
- BOOL truncateLastLine = (self.lineBreakMode == TUILineBreakByTruncatingHead || self.lineBreakMode == TUILineBreakByTruncatingMiddle ||
|
|
|
- self.lineBreakMode == TUILineBreakByTruncatingTail);
|
|
|
-
|
|
|
- CGPoint lineOrigins[numberOfLines];
|
|
|
- CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);
|
|
|
-
|
|
|
- for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) {
|
|
|
- CGPoint lineOrigin = lineOrigins[lineIndex];
|
|
|
- CGContextSetTextPosition(c, lineOrigin.x, lineOrigin.y);
|
|
|
- CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
|
|
|
-
|
|
|
- CGFloat descent = 0.0f;
|
|
|
- CTLineGetTypographicBounds((CTLineRef)line, NULL, &descent, NULL);
|
|
|
-
|
|
|
- // Adjust pen offset for flush depending on text alignment
|
|
|
- CGFloat flushFactor = flushFactorForTextAlignment(self.textAlignment);
|
|
|
-
|
|
|
- if (lineIndex == numberOfLines - 1 && truncateLastLine) {
|
|
|
- // Check if the range of text in the last line reaches the end of the full attributed string
|
|
|
- CFRange lastLineRange = CTLineGetStringRange(line);
|
|
|
-
|
|
|
- if (!(lastLineRange.length == 0 && lastLineRange.location == 0) &&
|
|
|
- lastLineRange.location + lastLineRange.length < textRange.location + textRange.length) {
|
|
|
- // Get correct truncationType and attribute position
|
|
|
- CTLineTruncationType truncationType;
|
|
|
- CFIndex truncationAttributePosition = lastLineRange.location;
|
|
|
- TUILineBreakMode lineBreakMode = self.lineBreakMode;
|
|
|
-
|
|
|
- // Multiple lines, only use UILineBreakModeTailTruncation
|
|
|
- if (numberOfLines != 1) {
|
|
|
- lineBreakMode = TUILineBreakByTruncatingTail;
|
|
|
- }
|
|
|
-
|
|
|
- switch (lineBreakMode) {
|
|
|
- case TUILineBreakByTruncatingHead:
|
|
|
- truncationType = kCTLineTruncationStart;
|
|
|
- break;
|
|
|
- case TUILineBreakByTruncatingMiddle:
|
|
|
- truncationType = kCTLineTruncationMiddle;
|
|
|
- truncationAttributePosition += (lastLineRange.length / 2);
|
|
|
- break;
|
|
|
- case TUILineBreakByTruncatingTail:
|
|
|
- default:
|
|
|
- truncationType = kCTLineTruncationEnd;
|
|
|
- truncationAttributePosition += (lastLineRange.length - 1);
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- NSAttributedString *attributedTruncationString = self.attributedTruncationToken;
|
|
|
- if (!attributedTruncationString) {
|
|
|
- NSString *truncationTokenString = @"\u2026"; // Unicode Character 'HORIZONTAL ELLIPSIS' (U+2026)
|
|
|
-
|
|
|
- NSDictionary *truncationTokenStringAttributes = truncationTokenStringAttributes =
|
|
|
- [attributedString attributesAtIndex:(NSUInteger)truncationAttributePosition effectiveRange:NULL];
|
|
|
-
|
|
|
- attributedTruncationString = [[NSAttributedString alloc] initWithString:truncationTokenString attributes:truncationTokenStringAttributes];
|
|
|
- }
|
|
|
- CTLineRef truncationToken = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)attributedTruncationString);
|
|
|
-
|
|
|
- // Append truncationToken to the string
|
|
|
- // because if string isn't too long, CT won't add the truncationToken on its own.
|
|
|
- // There is no chance of a double truncationToken because CT only adds the
|
|
|
- // token if it removes characters (and the one we add will go first)
|
|
|
- NSMutableAttributedString *truncationString = [[NSMutableAttributedString alloc]
|
|
|
- initWithAttributedString:[attributedString attributedSubstringFromRange:NSMakeRange((NSUInteger)lastLineRange.location,
|
|
|
- (NSUInteger)lastLineRange.length)]];
|
|
|
- if (lastLineRange.length > 0) {
|
|
|
- // Remove any newline at the end (we don't want newline space between the text and the truncation token). There can only be one, because the
|
|
|
- // second would be on the next line.
|
|
|
- unichar lastCharacter = [[truncationString string] characterAtIndex:(NSUInteger)(lastLineRange.length - 1)];
|
|
|
- if ([[NSCharacterSet newlineCharacterSet] characterIsMember:lastCharacter]) {
|
|
|
- [truncationString deleteCharactersInRange:NSMakeRange((NSUInteger)(lastLineRange.length - 1), 1)];
|
|
|
- }
|
|
|
- }
|
|
|
- [truncationString appendAttributedString:attributedTruncationString];
|
|
|
- CTLineRef truncationLine = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)truncationString);
|
|
|
-
|
|
|
- // Truncate the line in case it is too long.
|
|
|
- CTLineRef truncatedLine = CTLineCreateTruncatedLine(truncationLine, rect.size.width, truncationType, truncationToken);
|
|
|
- if (!truncatedLine) {
|
|
|
- // If the line is not as wide as the truncationToken, truncatedLine is NULL
|
|
|
- truncatedLine = CFRetain(truncationToken);
|
|
|
- }
|
|
|
-
|
|
|
- CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(truncatedLine, flushFactor, rect.size.width);
|
|
|
- CGContextSetTextPosition(c, penOffset, lineOrigin.y - descent - self.font.descender);
|
|
|
-
|
|
|
- CTLineDraw(truncatedLine, c);
|
|
|
-
|
|
|
- NSRange linkRange;
|
|
|
- if ([attributedTruncationString attribute:NSLinkAttributeName atIndex:0 effectiveRange:&linkRange]) {
|
|
|
- NSRange tokenRange = [truncationString.string rangeOfString:attributedTruncationString.string];
|
|
|
- NSRange tokenLinkRange =
|
|
|
- NSMakeRange((NSUInteger)(lastLineRange.location + lastLineRange.length) - tokenRange.length, (NSUInteger)tokenRange.length);
|
|
|
-
|
|
|
- [self addLinkToURL:[attributedTruncationString attribute:NSLinkAttributeName atIndex:0 effectiveRange:&linkRange] withRange:tokenLinkRange];
|
|
|
- }
|
|
|
-
|
|
|
- CFRelease(truncatedLine);
|
|
|
- CFRelease(truncationLine);
|
|
|
- CFRelease(truncationToken);
|
|
|
- } else {
|
|
|
- CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, rect.size.width);
|
|
|
- CGContextSetTextPosition(c, penOffset, lineOrigin.y - descent - self.font.descender);
|
|
|
- CTLineDraw(line, c);
|
|
|
- }
|
|
|
- } else {
|
|
|
- CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, rect.size.width);
|
|
|
- CGContextSetTextPosition(c, penOffset, lineOrigin.y - descent - self.font.descender);
|
|
|
- CTLineDraw(line, c);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- [self drawStrike:frame inRect:rect context:c];
|
|
|
-
|
|
|
- CFRelease(frame);
|
|
|
- CGPathRelease(path);
|
|
|
-}
|
|
|
-
|
|
|
-- (void)drawBackground:(CTFrameRef)frame inRect:(CGRect)rect context:(CGContextRef)c {
|
|
|
- NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
|
|
|
- CGPoint origins[[lines count]];
|
|
|
- CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
|
|
|
-
|
|
|
- CFIndex lineIndex = 0;
|
|
|
- for (id line in lines) {
|
|
|
- CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
|
|
|
- CGFloat width = (CGFloat)CTLineGetTypographicBounds((__bridge CTLineRef)line, &ascent, &descent, &leading);
|
|
|
-
|
|
|
- for (id glyphRun in (__bridge NSArray *)CTLineGetGlyphRuns((__bridge CTLineRef)line)) {
|
|
|
- NSDictionary *attributes = (__bridge NSDictionary *)CTRunGetAttributes((__bridge CTRunRef)glyphRun);
|
|
|
- CGColorRef strokeColor = formatCGColorRefFromColor([attributes objectForKey:kTUIBackgroundStrokeColorAttributeName]);
|
|
|
- CGColorRef fillColor = formatCGColorRefFromColor([attributes objectForKey:kTUIBackgroundFillColorAttributeName]);
|
|
|
- UIEdgeInsets fillPadding = [[attributes objectForKey:kTUIBackgroundFillPaddingAttributeName] UIEdgeInsetsValue];
|
|
|
- CGFloat cornerRadius = [[attributes objectForKey:kTUIBackgroundCornerRadiusAttributeName] floatValue];
|
|
|
- CGFloat lineWidth = [[attributes objectForKey:kTUIBackgroundLineWidthAttributeName] floatValue];
|
|
|
-
|
|
|
- if (strokeColor || fillColor) {
|
|
|
- CGRect runBounds = CGRectZero;
|
|
|
- CGFloat runAscent = 0.0f;
|
|
|
- CGFloat runDescent = 0.0f;
|
|
|
-
|
|
|
- runBounds.size.width = (CGFloat)CTRunGetTypographicBounds((__bridge CTRunRef)glyphRun, CFRangeMake(0, 0), &runAscent, &runDescent, NULL) +
|
|
|
- fillPadding.left + fillPadding.right;
|
|
|
- runBounds.size.height = runAscent + runDescent + fillPadding.top + fillPadding.bottom;
|
|
|
-
|
|
|
- CGFloat xOffset = 0.0f;
|
|
|
- CFRange glyphRange = CTRunGetStringRange((__bridge CTRunRef)glyphRun);
|
|
|
- switch (CTRunGetStatus((__bridge CTRunRef)glyphRun)) {
|
|
|
- case kCTRunStatusRightToLeft:
|
|
|
- xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location + glyphRange.length, NULL);
|
|
|
- break;
|
|
|
- default:
|
|
|
- xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location, NULL);
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- runBounds.origin.x = origins[lineIndex].x + rect.origin.x + xOffset - fillPadding.left - rect.origin.x;
|
|
|
- runBounds.origin.y = origins[lineIndex].y + rect.origin.y - fillPadding.bottom - rect.origin.y;
|
|
|
- runBounds.origin.y -= runDescent;
|
|
|
-
|
|
|
- // Don't draw higlightedLinkBackground too far to the right
|
|
|
- if (CGRectGetWidth(runBounds) > width) {
|
|
|
- runBounds.size.width = width;
|
|
|
- }
|
|
|
-
|
|
|
- CGPathRef path =
|
|
|
- [[UIBezierPath bezierPathWithRoundedRect:CGRectInset(UIEdgeInsetsInsetRect(runBounds, self.linkBackgroundEdgeInset), lineWidth, lineWidth)
|
|
|
- cornerRadius:cornerRadius] CGPath];
|
|
|
-
|
|
|
- CGContextSetLineJoin(c, kCGLineJoinRound);
|
|
|
-
|
|
|
- if (fillColor) {
|
|
|
- CGContextSetFillColorWithColor(c, fillColor);
|
|
|
- CGContextAddPath(c, path);
|
|
|
- CGContextFillPath(c);
|
|
|
- }
|
|
|
-
|
|
|
- if (strokeColor) {
|
|
|
- CGContextSetStrokeColorWithColor(c, strokeColor);
|
|
|
- CGContextAddPath(c, path);
|
|
|
- CGContextStrokePath(c);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- lineIndex++;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-- (void)drawStrike:(CTFrameRef)frame inRect:(__unused CGRect)rect context:(CGContextRef)c {
|
|
|
- NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
|
|
|
- CGPoint origins[[lines count]];
|
|
|
- CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
|
|
|
-
|
|
|
- CFIndex lineIndex = 0;
|
|
|
- for (id line in lines) {
|
|
|
- CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
|
|
|
- CGFloat width = (CGFloat)CTLineGetTypographicBounds((__bridge CTLineRef)line, &ascent, &descent, &leading);
|
|
|
-
|
|
|
- for (id glyphRun in (__bridge NSArray *)CTLineGetGlyphRuns((__bridge CTLineRef)line)) {
|
|
|
- NSDictionary *attributes = (__bridge NSDictionary *)CTRunGetAttributes((__bridge CTRunRef)glyphRun);
|
|
|
- BOOL strikeOut = [[attributes objectForKey:kTUIStrikeOutAttributeName] boolValue];
|
|
|
- NSInteger superscriptStyle = [[attributes objectForKey:(id)kCTSuperscriptAttributeName] integerValue];
|
|
|
-
|
|
|
- if (strikeOut) {
|
|
|
- CGRect runBounds = CGRectZero;
|
|
|
- CGFloat runAscent = 0.0f;
|
|
|
- CGFloat runDescent = 0.0f;
|
|
|
-
|
|
|
- runBounds.size.width = (CGFloat)CTRunGetTypographicBounds((__bridge CTRunRef)glyphRun, CFRangeMake(0, 0), &runAscent, &runDescent, NULL);
|
|
|
- runBounds.size.height = runAscent + runDescent;
|
|
|
-
|
|
|
- CGFloat xOffset = 0.0f;
|
|
|
- CFRange glyphRange = CTRunGetStringRange((__bridge CTRunRef)glyphRun);
|
|
|
- switch (CTRunGetStatus((__bridge CTRunRef)glyphRun)) {
|
|
|
- case kCTRunStatusRightToLeft:
|
|
|
- xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location + glyphRange.length, NULL);
|
|
|
- break;
|
|
|
- default:
|
|
|
- xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location, NULL);
|
|
|
- break;
|
|
|
- }
|
|
|
- runBounds.origin.x = origins[lineIndex].x + xOffset;
|
|
|
- runBounds.origin.y = origins[lineIndex].y;
|
|
|
- runBounds.origin.y -= runDescent;
|
|
|
-
|
|
|
- // Don't draw strikeout too far to the right
|
|
|
- if (CGRectGetWidth(runBounds) > width) {
|
|
|
- runBounds.size.width = width;
|
|
|
- }
|
|
|
-
|
|
|
- switch (superscriptStyle) {
|
|
|
- case 1:
|
|
|
- runBounds.origin.y -= runAscent * 0.47f;
|
|
|
- break;
|
|
|
- case -1:
|
|
|
- runBounds.origin.y += runAscent * 0.25f;
|
|
|
- break;
|
|
|
- default:
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- // Use text color, or default to black
|
|
|
- id color = [attributes objectForKey:(id)kCTForegroundColorAttributeName];
|
|
|
- if (color) {
|
|
|
- CGContextSetStrokeColorWithColor(c, formatCGColorRefFromColor(color));
|
|
|
- } else {
|
|
|
- CGContextSetGrayStrokeColor(c, 0.0f, 1.0);
|
|
|
- }
|
|
|
-
|
|
|
- CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)self.font.fontName, self.font.pointSize, NULL);
|
|
|
- CGContextSetLineWidth(c, CTFontGetUnderlineThickness(font));
|
|
|
- CFRelease(font);
|
|
|
-
|
|
|
- CGFloat y = formatCGFloatRound(runBounds.origin.y + runBounds.size.height / 2.0f);
|
|
|
- CGContextMoveToPoint(c, runBounds.origin.x, y);
|
|
|
- CGContextAddLineToPoint(c, runBounds.origin.x + runBounds.size.width, y);
|
|
|
-
|
|
|
- CGContextStrokePath(c);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- lineIndex++;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-#pragma mark - TUIAttributedLabel
|
|
|
-
|
|
|
-- (void)setText:(id)text {
|
|
|
- NSParameterAssert(!text || [text isKindOfClass:[NSAttributedString class]] || [text isKindOfClass:[NSString class]]);
|
|
|
-
|
|
|
- if ([text isKindOfClass:[NSString class]]) {
|
|
|
- [self setText:text afterInheritingLabelAttributesAndConfiguringWithBlock:nil];
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- self.attributedText = text;
|
|
|
- self.activeLink = nil;
|
|
|
-
|
|
|
- self.linkModels = [NSArray array];
|
|
|
- if (text && self.attributedText && self.enabledTextCheckingTypes) {
|
|
|
- __weak __typeof(self) weakSelf = self;
|
|
|
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
|
- __strong __typeof(weakSelf) strongSelf = weakSelf;
|
|
|
-
|
|
|
- NSDataDetector *dataDetector = strongSelf.dataDetector;
|
|
|
- if (dataDetector && [dataDetector respondsToSelector:@selector(matchesInString:options:range:)]) {
|
|
|
- NSArray *results = [dataDetector matchesInString:[(NSAttributedString *)text string]
|
|
|
- options:0
|
|
|
- range:NSMakeRange(0, [(NSAttributedString *)text length])];
|
|
|
- if ([results count] > 0) {
|
|
|
- dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
- if ([[strongSelf.attributedText string] isEqualToString:[(NSAttributedString *)text string]]) {
|
|
|
- [strongSelf addLinksWithTextCheckingResults:results attributes:strongSelf.linkAttributes];
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- [self.attributedText enumerateAttribute:NSLinkAttributeName
|
|
|
- inRange:NSMakeRange(0, self.attributedText.length)
|
|
|
- options:0
|
|
|
- usingBlock:^(id value, __unused NSRange range, __unused BOOL *stop) {
|
|
|
- if (value) {
|
|
|
- NSURL *URL = [value isKindOfClass:[NSString class]] ? [NSURL URLWithString:value] : value;
|
|
|
- [self addLinkToURL:URL withRange:range];
|
|
|
- }
|
|
|
- }];
|
|
|
-}
|
|
|
-
|
|
|
-- (void)setText:(id)text
|
|
|
- afterInheritingLabelAttributesAndConfiguringWithBlock:(NSMutableAttributedString * (^)(NSMutableAttributedString *mutableAttributedString))block {
|
|
|
- NSMutableAttributedString *mutableAttributedString = nil;
|
|
|
- if ([text isKindOfClass:[NSString class]]) {
|
|
|
- mutableAttributedString = [[NSMutableAttributedString alloc] initWithString:text attributes:formatNSAttributedStringAttributesFromLabel(self)];
|
|
|
- } else {
|
|
|
- mutableAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:text];
|
|
|
- [mutableAttributedString addAttributes:formatNSAttributedStringAttributesFromLabel(self) range:NSMakeRange(0, [mutableAttributedString length])];
|
|
|
- }
|
|
|
-
|
|
|
- if (block) {
|
|
|
- mutableAttributedString = block(mutableAttributedString);
|
|
|
- }
|
|
|
-
|
|
|
- [self setText:mutableAttributedString];
|
|
|
-}
|
|
|
-
|
|
|
-- (void)setActiveLink:(TUIAttributedLabelLink *)activeLink {
|
|
|
- _activeLink = activeLink;
|
|
|
-
|
|
|
- NSDictionary *activeAttributes = activeLink.activeAttributes ?: self.activeLinkAttributes;
|
|
|
-
|
|
|
- if (_activeLink && activeAttributes.count > 0) {
|
|
|
- if (!self.inactiveAttributedText) {
|
|
|
- self.inactiveAttributedText = [self.attributedText copy];
|
|
|
- }
|
|
|
-
|
|
|
- NSMutableAttributedString *mutableAttributedString = [self.inactiveAttributedText mutableCopy];
|
|
|
- if (self.activeLink.result.range.length > 0 &&
|
|
|
- NSLocationInRange(NSMaxRange(self.activeLink.result.range) - 1, NSMakeRange(0, [self.inactiveAttributedText length]))) {
|
|
|
- [mutableAttributedString addAttributes:activeAttributes range:self.activeLink.result.range];
|
|
|
- }
|
|
|
-
|
|
|
- self.attributedText = mutableAttributedString;
|
|
|
- [self setNeedsDisplay];
|
|
|
-
|
|
|
- [CATransaction flush];
|
|
|
- } else if (self.inactiveAttributedText) {
|
|
|
- self.attributedText = self.inactiveAttributedText;
|
|
|
- self.inactiveAttributedText = nil;
|
|
|
-
|
|
|
- [self setNeedsDisplay];
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-- (void)setLinkAttributes:(NSDictionary *)linkAttributes {
|
|
|
- _linkAttributes = convertNSAttributedStringAttributesToCTAttributes(linkAttributes);
|
|
|
-}
|
|
|
-
|
|
|
-- (void)setActiveLinkAttributes:(NSDictionary *)activeLinkAttributes {
|
|
|
- _activeLinkAttributes = convertNSAttributedStringAttributesToCTAttributes(activeLinkAttributes);
|
|
|
-}
|
|
|
-
|
|
|
-- (void)setInactiveLinkAttributes:(NSDictionary *)inactiveLinkAttributes {
|
|
|
- _inactiveLinkAttributes = convertNSAttributedStringAttributesToCTAttributes(inactiveLinkAttributes);
|
|
|
-}
|
|
|
-
|
|
|
-#pragma mark - UILabel
|
|
|
-
|
|
|
-- (void)setHighlighted:(BOOL)highlighted {
|
|
|
- [super setHighlighted:highlighted];
|
|
|
- [self setNeedsDisplay];
|
|
|
-}
|
|
|
-
|
|
|
-// Fixes crash when loading from a UIStoryboard
|
|
|
-- (UIColor *)textColor {
|
|
|
- UIColor *color = [super textColor];
|
|
|
- if (!color) {
|
|
|
- color = [UIColor blackColor];
|
|
|
- }
|
|
|
-
|
|
|
- return color;
|
|
|
-}
|
|
|
-
|
|
|
-- (void)setTextColor:(UIColor *)textColor {
|
|
|
- UIColor *oldTextColor = self.textColor;
|
|
|
- [super setTextColor:textColor];
|
|
|
-
|
|
|
- // Redraw to allow any ColorFromContext attributes a chance to update
|
|
|
- if (textColor != oldTextColor) {
|
|
|
- [self setNeedsFramesetter];
|
|
|
- [self setNeedsDisplay];
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-- (CGRect)textRectForBounds:(CGRect)bounds limitedToNumberOfLines:(NSInteger)numberOfLines {
|
|
|
- bounds = UIEdgeInsetsInsetRect(bounds, self.textInsets);
|
|
|
- if (!self.attributedText) {
|
|
|
- return [super textRectForBounds:bounds limitedToNumberOfLines:numberOfLines];
|
|
|
- }
|
|
|
-
|
|
|
- CGRect textRect = bounds;
|
|
|
-
|
|
|
- // Calculate height with a minimum of double the font pointSize, to ensure that CTFramesetterSuggestFrameSizeWithConstraints doesn't return CGSizeZero, as
|
|
|
- // it would if textRect height is insufficient.
|
|
|
- textRect.size.height = MAX(self.font.lineHeight * MAX(2, numberOfLines), bounds.size.height);
|
|
|
-
|
|
|
- // Adjust the text to be in the center vertically, if the text size is smaller than bounds
|
|
|
- CGSize textSize =
|
|
|
- CTFramesetterSuggestFrameSizeWithConstraints([self framesetter], CFRangeMake(0, (CFIndex)[self.attributedText length]), NULL, textRect.size, NULL);
|
|
|
- textSize =
|
|
|
- CGSizeMake(formatCGFloatCeil(textSize.width),
|
|
|
- formatCGFloatCeil(textSize.height));
|
|
|
- // Fix for iOS 4, CTFramesetterSuggestFrameSizeWithConstraints sometimes returns fractional sizes
|
|
|
-
|
|
|
- if (textSize.height < bounds.size.height) {
|
|
|
- CGFloat yOffset = 0.0f;
|
|
|
- switch (self.verticalAlignment) {
|
|
|
- case TUIAttributedLabelVerticalAlignmentCenter:
|
|
|
- yOffset = formatCGFloatFloor((bounds.size.height - textSize.height) / 2.0f);
|
|
|
- break;
|
|
|
- case TUIAttributedLabelVerticalAlignmentBottom:
|
|
|
- yOffset = bounds.size.height - textSize.height;
|
|
|
- break;
|
|
|
- case TUIAttributedLabelVerticalAlignmentTop:
|
|
|
- default:
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- textRect.origin.y += yOffset;
|
|
|
- }
|
|
|
-
|
|
|
- return textRect;
|
|
|
-}
|
|
|
-
|
|
|
-- (void)drawTextInRect:(CGRect)rect {
|
|
|
- CGRect insetRect = UIEdgeInsetsInsetRect(rect, self.textInsets);
|
|
|
- if (!self.attributedText) {
|
|
|
- [super drawTextInRect:insetRect];
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- NSAttributedString *originalAttributedText = nil;
|
|
|
-
|
|
|
- // Adjust the font size to fit width, if necessarry
|
|
|
- if (self.adjustsFontSizeToFitWidth && self.numberOfLines > 0) {
|
|
|
- // Framesetter could still be working with a resized version of the text;
|
|
|
- // need to reset so we start from the original font size.
|
|
|
- // See #393.
|
|
|
- [self setNeedsFramesetter];
|
|
|
- [self setNeedsDisplay];
|
|
|
-
|
|
|
- if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) {
|
|
|
- [self invalidateIntrinsicContentSize];
|
|
|
- }
|
|
|
-
|
|
|
- // Use infinite width to find the max width, which will be compared to availableWidth if needed.
|
|
|
- CGSize maxSize = (self.numberOfLines > 1) ? CGSizeMake(TUIFLOAT_MAX, TUIFLOAT_MAX) : CGSizeZero;
|
|
|
-
|
|
|
- CGFloat textWidth = [self sizeThatFits:maxSize].width;
|
|
|
- CGFloat availableWidth = self.frame.size.width * self.numberOfLines;
|
|
|
- if (self.numberOfLines > 1 && self.lineBreakMode == TUILineBreakByWordWrapping) {
|
|
|
- textWidth *= kTUILineBreakWordWrapTextWidthScalingFactor;
|
|
|
- }
|
|
|
-
|
|
|
- if (textWidth > availableWidth && textWidth > 0.0f) {
|
|
|
- originalAttributedText = [self.attributedText copy];
|
|
|
-
|
|
|
- CGFloat scaleFactor = availableWidth / textWidth;
|
|
|
- if ([self respondsToSelector:@selector(minimumScaleFactor)] && self.minimumScaleFactor > scaleFactor) {
|
|
|
- scaleFactor = self.minimumScaleFactor;
|
|
|
- }
|
|
|
-
|
|
|
- self.attributedText = formatNSAttributedStringByScalingFontSize(self.attributedText, scaleFactor);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- CGContextRef c = UIGraphicsGetCurrentContext();
|
|
|
- CGContextSaveGState(c);
|
|
|
- {
|
|
|
- CGContextSetTextMatrix(c, CGAffineTransformIdentity);
|
|
|
-
|
|
|
- // Inverts the CTM to match iOS coordinates (otherwise text draws upside-down; Mac OS's system is different)
|
|
|
- CGContextTranslateCTM(c, 0.0f, insetRect.size.height);
|
|
|
- CGContextScaleCTM(c, 1.0f, -1.0f);
|
|
|
-
|
|
|
- CFRange textRange = CFRangeMake(0, (CFIndex)[self.attributedText length]);
|
|
|
-
|
|
|
- // First, get the text rect (which takes vertical centering into account)
|
|
|
- CGRect textRect = [self textRectForBounds:rect limitedToNumberOfLines:self.numberOfLines];
|
|
|
-
|
|
|
- // CoreText draws its text aligned to the bottom, so we move the CTM here to take our vertical offsets into account
|
|
|
- CGContextTranslateCTM(c, insetRect.origin.x, insetRect.size.height - textRect.origin.y - textRect.size.height);
|
|
|
-
|
|
|
- // Second, trace the shadow before the actual text, if we have one
|
|
|
- if (self.shadowColor && !self.highlighted) {
|
|
|
- CGContextSetShadowWithColor(c, self.shadowOffset, self.shadowRadius, [self.shadowColor CGColor]);
|
|
|
- } else if (self.highlightedShadowColor) {
|
|
|
- CGContextSetShadowWithColor(c, self.highlightedShadowOffset, self.highlightedShadowRadius, [self.highlightedShadowColor CGColor]);
|
|
|
- }
|
|
|
-
|
|
|
- // Finally, draw the text or highlighted text itself (on top of the shadow, if there is one)
|
|
|
- if (self.highlightedTextColor && self.highlighted) {
|
|
|
- NSMutableAttributedString *highlightAttributedString = [self.renderedAttributedText mutableCopy];
|
|
|
- [highlightAttributedString addAttribute:(__bridge NSString *)kCTForegroundColorAttributeName
|
|
|
- value:(id)[self.highlightedTextColor CGColor]
|
|
|
- range:NSMakeRange(0, highlightAttributedString.length)];
|
|
|
-
|
|
|
- if (![self highlightFramesetter]) {
|
|
|
- CTFramesetterRef highlightFramesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)highlightAttributedString);
|
|
|
- [self setHighlightFramesetter:highlightFramesetter];
|
|
|
- CFRelease(highlightFramesetter);
|
|
|
- }
|
|
|
-
|
|
|
- [self drawFramesetter:[self highlightFramesetter] attributedString:highlightAttributedString textRange:textRange inRect:textRect context:c];
|
|
|
- } else {
|
|
|
- [self drawFramesetter:[self framesetter] attributedString:self.renderedAttributedText textRange:textRange inRect:textRect context:c];
|
|
|
- }
|
|
|
-
|
|
|
- // If we adjusted the font size, set it back to its original size
|
|
|
- if (originalAttributedText) {
|
|
|
- // Use ivar directly to avoid clearing out framesetter and renderedAttributedText
|
|
|
- _attributedText = originalAttributedText;
|
|
|
- }
|
|
|
- }
|
|
|
- CGContextRestoreGState(c);
|
|
|
-}
|
|
|
-
|
|
|
-#pragma mark - UIAccessibilityElement
|
|
|
-
|
|
|
-- (BOOL)isAccessibilityElement {
|
|
|
- return NO;
|
|
|
-}
|
|
|
-
|
|
|
-- (NSInteger)accessibilityElementCount {
|
|
|
- return (NSInteger)[[self accessibilityElements] count];
|
|
|
-}
|
|
|
-
|
|
|
-- (id)accessibilityElementAtIndex:(NSInteger)index {
|
|
|
- return [[self accessibilityElements] objectAtIndex:(NSUInteger)index];
|
|
|
-}
|
|
|
-
|
|
|
-- (NSInteger)indexOfAccessibilityElement:(id)element {
|
|
|
- return (NSInteger)[[self accessibilityElements] indexOfObject:element];
|
|
|
-}
|
|
|
-
|
|
|
-- (NSArray *)accessibilityElements {
|
|
|
- if (!_accessibilityElements) {
|
|
|
- @synchronized(self) {
|
|
|
- NSMutableArray *mutableAccessibilityItems = [NSMutableArray array];
|
|
|
-
|
|
|
- for (TUIAttributedLabelLink *link in self.linkModels) {
|
|
|
- if (link.result.range.location == NSNotFound) {
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- NSString *sourceText = [self.text isKindOfClass:[NSString class]] ? self.text : [(NSAttributedString *)self.text string];
|
|
|
-
|
|
|
- NSString *accessibilityLabel = [sourceText substringWithRange:link.result.range];
|
|
|
- NSString *accessibilityValue = link.accessibilityValue;
|
|
|
-
|
|
|
- if (accessibilityLabel) {
|
|
|
- TUIAccessibilityElement *linkElement = [[TUIAccessibilityElement alloc] initWithAccessibilityContainer:self];
|
|
|
- linkElement.accessibilityTraits = UIAccessibilityTraitLink;
|
|
|
- linkElement.boundingRect = [self boundingRectForCharacterRange:link.result.range];
|
|
|
- linkElement.superview = self;
|
|
|
- linkElement.accessibilityLabel = accessibilityLabel;
|
|
|
-
|
|
|
- if (![accessibilityLabel isEqualToString:accessibilityValue]) {
|
|
|
- linkElement.accessibilityValue = accessibilityValue;
|
|
|
- }
|
|
|
-
|
|
|
- [mutableAccessibilityItems addObject:linkElement];
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- TUIAccessibilityElement *baseElement = [[TUIAccessibilityElement alloc] initWithAccessibilityContainer:self];
|
|
|
- baseElement.accessibilityLabel = [super accessibilityLabel];
|
|
|
- baseElement.accessibilityHint = [super accessibilityHint];
|
|
|
- baseElement.accessibilityValue = [super accessibilityValue];
|
|
|
- baseElement.boundingRect = self.bounds;
|
|
|
- baseElement.superview = self;
|
|
|
- baseElement.accessibilityTraits = [super accessibilityTraits];
|
|
|
-
|
|
|
- [mutableAccessibilityItems addObject:baseElement];
|
|
|
-
|
|
|
- self.accessibilityElements = [NSArray arrayWithArray:mutableAccessibilityItems];
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return _accessibilityElements;
|
|
|
-}
|
|
|
-
|
|
|
-#pragma mark - UIView
|
|
|
-
|
|
|
-- (CGSize)sizeThatFits:(CGSize)size {
|
|
|
- if (!self.attributedText) {
|
|
|
- return [super sizeThatFits:size];
|
|
|
- } else {
|
|
|
- NSAttributedString *string = [self renderedAttributedText];
|
|
|
-
|
|
|
- CGSize labelSize =
|
|
|
- formatCTFramesetterSuggestFrameSizeForAttributedStringWithConstraints([self framesetter], string, size, (NSUInteger)self.numberOfLines);
|
|
|
- labelSize.width += self.textInsets.left + self.textInsets.right;
|
|
|
- labelSize.height += self.textInsets.top + self.textInsets.bottom;
|
|
|
-
|
|
|
- return labelSize;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-- (CGSize)intrinsicContentSize {
|
|
|
- // There's an implicit width from the original UILabel implementation
|
|
|
- return [self sizeThatFits:[super intrinsicContentSize]];
|
|
|
-}
|
|
|
-
|
|
|
-- (void)tintColorDidChange {
|
|
|
- if (!self.inactiveLinkAttributes || [self.inactiveLinkAttributes count] == 0) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- BOOL isInactive = (self.tintAdjustmentMode == UIViewTintAdjustmentModeDimmed);
|
|
|
-
|
|
|
- NSMutableAttributedString *mutableAttributedString = [self.attributedText mutableCopy];
|
|
|
- for (TUIAttributedLabelLink *link in self.linkModels) {
|
|
|
- NSDictionary *attributesToRemove = isInactive ? link.attributes : link.inactiveAttributes;
|
|
|
- NSDictionary *attributesToAdd = isInactive ? link.inactiveAttributes : link.attributes;
|
|
|
-
|
|
|
- [attributesToRemove enumerateKeysAndObjectsUsingBlock:^(NSString *name, __unused id value, __unused BOOL *stop) {
|
|
|
- if (NSMaxRange(link.result.range) <= mutableAttributedString.length) {
|
|
|
- [mutableAttributedString removeAttribute:name range:link.result.range];
|
|
|
- }
|
|
|
- }];
|
|
|
-
|
|
|
- if (attributesToAdd) {
|
|
|
- if (NSMaxRange(link.result.range) <= mutableAttributedString.length) {
|
|
|
- [mutableAttributedString addAttributes:attributesToAdd range:link.result.range];
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- self.attributedText = mutableAttributedString;
|
|
|
-
|
|
|
- [self setNeedsDisplay];
|
|
|
-}
|
|
|
-
|
|
|
-- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
|
|
|
- if (![self linkAtPoint:point] || !self.userInteractionEnabled || self.hidden || self.alpha < 0.01) {
|
|
|
- return [super hitTest:point withEvent:event];
|
|
|
- }
|
|
|
-
|
|
|
- return self;
|
|
|
-}
|
|
|
-
|
|
|
-#pragma mark - UIResponder
|
|
|
-
|
|
|
-- (BOOL)canBecomeFirstResponder {
|
|
|
- return YES;
|
|
|
-}
|
|
|
-
|
|
|
-- (BOOL)canPerformAction:(SEL)action withSender:(__unused id)sender {
|
|
|
-#if !TARGET_OS_TV
|
|
|
- return (action == @selector(copy:));
|
|
|
-#else
|
|
|
- return NO;
|
|
|
-#endif
|
|
|
-}
|
|
|
-
|
|
|
-- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
|
- UITouch *touch = [touches anyObject];
|
|
|
-
|
|
|
- self.activeLink = [self linkAtPoint:[touch locationInView:self]];
|
|
|
-
|
|
|
- if (!self.activeLink) {
|
|
|
- [super touchesBegan:touches withEvent:event];
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
|
- if (self.activeLink) {
|
|
|
- UITouch *touch = [touches anyObject];
|
|
|
-
|
|
|
- if (self.activeLink != [self linkAtPoint:[touch locationInView:self]]) {
|
|
|
- self.activeLink = nil;
|
|
|
- }
|
|
|
- } else {
|
|
|
- [super touchesMoved:touches withEvent:event];
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
|
- if (self.activeLink) {
|
|
|
- if (self.activeLink.linkTapBlock) {
|
|
|
- self.activeLink.linkTapBlock(self, self.activeLink);
|
|
|
- self.activeLink = nil;
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- NSTextCheckingResult *result = self.activeLink.result;
|
|
|
- self.activeLink = nil;
|
|
|
-
|
|
|
- switch (result.resultType) {
|
|
|
- case NSTextCheckingTypeLink:
|
|
|
- if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithURL:)]) {
|
|
|
- [self.delegate attributedLabel:self didSelectLinkWithURL:result.URL];
|
|
|
- return;
|
|
|
- }
|
|
|
- break;
|
|
|
- case NSTextCheckingTypeAddress:
|
|
|
- if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithAddress:)]) {
|
|
|
- [self.delegate attributedLabel:self didSelectLinkWithAddress:result.addressComponents];
|
|
|
- return;
|
|
|
- }
|
|
|
- break;
|
|
|
- case NSTextCheckingTypePhoneNumber:
|
|
|
- if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithPhoneNumber:)]) {
|
|
|
- [self.delegate attributedLabel:self didSelectLinkWithPhoneNumber:result.phoneNumber];
|
|
|
- return;
|
|
|
- }
|
|
|
- break;
|
|
|
- case NSTextCheckingTypeDate:
|
|
|
- if (result.timeZone && [self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithDate:timeZone:duration:)]) {
|
|
|
- [self.delegate attributedLabel:self didSelectLinkWithDate:result.date timeZone:result.timeZone duration:result.duration];
|
|
|
- return;
|
|
|
- } else if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithDate:)]) {
|
|
|
- [self.delegate attributedLabel:self didSelectLinkWithDate:result.date];
|
|
|
- return;
|
|
|
- }
|
|
|
- break;
|
|
|
- case NSTextCheckingTypeTransitInformation:
|
|
|
- if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithTransitInformation:)]) {
|
|
|
- [self.delegate attributedLabel:self didSelectLinkWithTransitInformation:result.components];
|
|
|
- return;
|
|
|
- }
|
|
|
- default:
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- // Fallback to `attributedLabel:didSelectLinkWithTextCheckingResult:` if no other delegate method matched.
|
|
|
- if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithTextCheckingResult:)]) {
|
|
|
- [self.delegate attributedLabel:self didSelectLinkWithTextCheckingResult:result];
|
|
|
- }
|
|
|
- } else {
|
|
|
- [super touchesEnded:touches withEvent:event];
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
|
- if (self.activeLink) {
|
|
|
- self.activeLink = nil;
|
|
|
- } else {
|
|
|
- [super touchesCancelled:touches withEvent:event];
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-#pragma mark - UIGestureRecognizerDelegate
|
|
|
-
|
|
|
-- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
|
|
|
- return [self containslinkAtPoint:[touch locationInView:self]];
|
|
|
-}
|
|
|
-
|
|
|
-#pragma mark - UILongPressGestureRecognizer
|
|
|
-
|
|
|
-- (void)longPressGestureDidFire:(UILongPressGestureRecognizer *)sender {
|
|
|
- switch (sender.state) {
|
|
|
- case UIGestureRecognizerStateBegan: {
|
|
|
- CGPoint touchPoint = [sender locationInView:self];
|
|
|
- TUIAttributedLabelLink *link = [self linkAtPoint:touchPoint];
|
|
|
-
|
|
|
- if (link) {
|
|
|
- if (link.linkLongPressBlock) {
|
|
|
- link.linkLongPressBlock(self, link);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- NSTextCheckingResult *result = link.result;
|
|
|
-
|
|
|
- if (!result) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- switch (result.resultType) {
|
|
|
- case NSTextCheckingTypeLink:
|
|
|
- if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithURL:atPoint:)]) {
|
|
|
- [self.delegate attributedLabel:self didLongPressLinkWithURL:result.URL atPoint:touchPoint];
|
|
|
- return;
|
|
|
- }
|
|
|
- break;
|
|
|
- case NSTextCheckingTypeAddress:
|
|
|
- if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithAddress:atPoint:)]) {
|
|
|
- [self.delegate attributedLabel:self didLongPressLinkWithAddress:result.addressComponents atPoint:touchPoint];
|
|
|
- return;
|
|
|
- }
|
|
|
- break;
|
|
|
- case NSTextCheckingTypePhoneNumber:
|
|
|
- if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithPhoneNumber:atPoint:)]) {
|
|
|
- [self.delegate attributedLabel:self didLongPressLinkWithPhoneNumber:result.phoneNumber atPoint:touchPoint];
|
|
|
- return;
|
|
|
- }
|
|
|
- break;
|
|
|
- case NSTextCheckingTypeDate:
|
|
|
- if (result.timeZone && [self.delegate respondsToSelector:@selector(attributedLabel:
|
|
|
- didLongPressLinkWithDate:timeZone:duration:atPoint:)]) {
|
|
|
- [self.delegate attributedLabel:self
|
|
|
- didLongPressLinkWithDate:result.date
|
|
|
- timeZone:result.timeZone
|
|
|
- duration:result.duration
|
|
|
- atPoint:touchPoint];
|
|
|
- return;
|
|
|
- } else if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithDate:atPoint:)]) {
|
|
|
- [self.delegate attributedLabel:self didLongPressLinkWithDate:result.date atPoint:touchPoint];
|
|
|
- return;
|
|
|
- }
|
|
|
- break;
|
|
|
- case NSTextCheckingTypeTransitInformation:
|
|
|
- if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithTransitInformation:atPoint:)]) {
|
|
|
- [self.delegate attributedLabel:self didLongPressLinkWithTransitInformation:result.components atPoint:touchPoint];
|
|
|
- return;
|
|
|
- }
|
|
|
- default:
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- // Fallback to `attributedLabel:didLongPressLinkWithTextCheckingResult:atPoint:` if no other delegate method matched.
|
|
|
- if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithTextCheckingResult:atPoint:)]) {
|
|
|
- [self.delegate attributedLabel:self didLongPressLinkWithTextCheckingResult:result atPoint:touchPoint];
|
|
|
- }
|
|
|
- }
|
|
|
- break;
|
|
|
- }
|
|
|
- default:
|
|
|
- break;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-#if !TARGET_OS_TV
|
|
|
-#pragma mark - UIResponderStandardEditActions
|
|
|
-
|
|
|
-- (void)copy:(__unused id)sender {
|
|
|
- [[UIPasteboard generalPasteboard] setString:self.text];
|
|
|
-}
|
|
|
-#endif
|
|
|
-
|
|
|
-#pragma mark - NSCoding
|
|
|
-
|
|
|
-- (void)encodeWithCoder:(NSCoder *)coder {
|
|
|
- [super encodeWithCoder:coder];
|
|
|
-
|
|
|
- [coder encodeObject:@(self.enabledTextCheckingTypes) forKey:NSStringFromSelector(@selector(enabledTextCheckingTypes))];
|
|
|
-
|
|
|
- [coder encodeObject:self.linkModels forKey:NSStringFromSelector(@selector(linkModels))];
|
|
|
- if ([NSMutableParagraphStyle class]) {
|
|
|
- [coder encodeObject:self.linkAttributes forKey:NSStringFromSelector(@selector(linkAttributes))];
|
|
|
- [coder encodeObject:self.activeLinkAttributes forKey:NSStringFromSelector(@selector(activeLinkAttributes))];
|
|
|
- [coder encodeObject:self.inactiveLinkAttributes forKey:NSStringFromSelector(@selector(inactiveLinkAttributes))];
|
|
|
- }
|
|
|
- [coder encodeObject:@(self.shadowRadius) forKey:NSStringFromSelector(@selector(shadowRadius))];
|
|
|
- [coder encodeObject:@(self.highlightedShadowRadius) forKey:NSStringFromSelector(@selector(highlightedShadowRadius))];
|
|
|
- [coder encodeCGSize:self.highlightedShadowOffset forKey:NSStringFromSelector(@selector(highlightedShadowOffset))];
|
|
|
- [coder encodeObject:self.highlightedShadowColor forKey:NSStringFromSelector(@selector(highlightedShadowColor))];
|
|
|
- [coder encodeObject:@(self.kern) forKey:NSStringFromSelector(@selector(kern))];
|
|
|
- [coder encodeObject:@(self.firstLineIndent) forKey:NSStringFromSelector(@selector(firstLineIndent))];
|
|
|
- [coder encodeObject:@(self.lineSpacing) forKey:NSStringFromSelector(@selector(lineSpacing))];
|
|
|
- [coder encodeObject:@(self.lineHeightMultiple) forKey:NSStringFromSelector(@selector(lineHeightMultiple))];
|
|
|
- [coder encodeUIEdgeInsets:self.textInsets forKey:NSStringFromSelector(@selector(textInsets))];
|
|
|
- [coder encodeInteger:self.verticalAlignment forKey:NSStringFromSelector(@selector(verticalAlignment))];
|
|
|
-
|
|
|
- [coder encodeObject:self.attributedTruncationToken forKey:NSStringFromSelector(@selector(attributedTruncationToken))];
|
|
|
-
|
|
|
- [coder encodeObject:NSStringFromUIEdgeInsets(self.linkBackgroundEdgeInset) forKey:NSStringFromSelector(@selector(linkBackgroundEdgeInset))];
|
|
|
- [coder encodeObject:self.attributedText forKey:NSStringFromSelector(@selector(attributedText))];
|
|
|
- [coder encodeObject:self.text forKey:NSStringFromSelector(@selector(text))];
|
|
|
-}
|
|
|
-
|
|
|
-- (id)initWithCoder:(NSCoder *)coder {
|
|
|
- self = [super initWithCoder:coder];
|
|
|
- if (!self) {
|
|
|
- return nil;
|
|
|
- }
|
|
|
-
|
|
|
- [self commonInit];
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(enabledTextCheckingTypes))]) {
|
|
|
- self.enabledTextCheckingTypes = [[coder decodeObjectForKey:NSStringFromSelector(@selector(enabledTextCheckingTypes))] unsignedLongLongValue];
|
|
|
- }
|
|
|
-
|
|
|
- if ([NSMutableParagraphStyle class]) {
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(linkAttributes))]) {
|
|
|
- self.linkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(linkAttributes))];
|
|
|
- }
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(activeLinkAttributes))]) {
|
|
|
- self.activeLinkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(activeLinkAttributes))];
|
|
|
- }
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(inactiveLinkAttributes))]) {
|
|
|
- self.inactiveLinkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(inactiveLinkAttributes))];
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(links))]) {
|
|
|
- NSArray *oldLinks = [coder decodeObjectForKey:NSStringFromSelector(@selector(links))];
|
|
|
- [self addLinksWithTextCheckingResults:oldLinks attributes:nil];
|
|
|
- }
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(linkModels))]) {
|
|
|
- self.linkModels = [coder decodeObjectForKey:NSStringFromSelector(@selector(linkModels))];
|
|
|
- }
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(shadowRadius))]) {
|
|
|
- self.shadowRadius = [[coder decodeObjectForKey:NSStringFromSelector(@selector(shadowRadius))] floatValue];
|
|
|
- }
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowRadius))]) {
|
|
|
- self.highlightedShadowRadius = [[coder decodeObjectForKey:NSStringFromSelector(@selector(highlightedShadowRadius))] floatValue];
|
|
|
- }
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowOffset))]) {
|
|
|
- self.highlightedShadowOffset = [coder decodeCGSizeForKey:NSStringFromSelector(@selector(highlightedShadowOffset))];
|
|
|
- }
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowColor))]) {
|
|
|
- self.highlightedShadowColor = [coder decodeObjectForKey:NSStringFromSelector(@selector(highlightedShadowColor))];
|
|
|
- }
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(kern))]) {
|
|
|
- self.kern = [[coder decodeObjectForKey:NSStringFromSelector(@selector(kern))] floatValue];
|
|
|
- }
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(firstLineIndent))]) {
|
|
|
- self.firstLineIndent = [[coder decodeObjectForKey:NSStringFromSelector(@selector(firstLineIndent))] floatValue];
|
|
|
- }
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(lineSpacing))]) {
|
|
|
- self.lineSpacing = [[coder decodeObjectForKey:NSStringFromSelector(@selector(lineSpacing))] floatValue];
|
|
|
- }
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(minimumLineHeight))]) {
|
|
|
- self.minimumLineHeight = [[coder decodeObjectForKey:NSStringFromSelector(@selector(minimumLineHeight))] floatValue];
|
|
|
- }
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(maximumLineHeight))]) {
|
|
|
- self.maximumLineHeight = [[coder decodeObjectForKey:NSStringFromSelector(@selector(maximumLineHeight))] floatValue];
|
|
|
- }
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(lineHeightMultiple))]) {
|
|
|
- self.lineHeightMultiple = [[coder decodeObjectForKey:NSStringFromSelector(@selector(lineHeightMultiple))] floatValue];
|
|
|
- }
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(textInsets))]) {
|
|
|
- self.textInsets = [coder decodeUIEdgeInsetsForKey:NSStringFromSelector(@selector(textInsets))];
|
|
|
- }
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(verticalAlignment))]) {
|
|
|
- self.verticalAlignment = [coder decodeIntegerForKey:NSStringFromSelector(@selector(verticalAlignment))];
|
|
|
- }
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(attributedTruncationToken))]) {
|
|
|
- self.attributedTruncationToken = [coder decodeObjectForKey:NSStringFromSelector(@selector(attributedTruncationToken))];
|
|
|
- }
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(linkBackgroundEdgeInset))]) {
|
|
|
- self.linkBackgroundEdgeInset = UIEdgeInsetsFromString([coder decodeObjectForKey:NSStringFromSelector(@selector(linkBackgroundEdgeInset))]);
|
|
|
- }
|
|
|
-
|
|
|
- if ([coder containsValueForKey:NSStringFromSelector(@selector(attributedText))]) {
|
|
|
- self.attributedText = [coder decodeObjectForKey:NSStringFromSelector(@selector(attributedText))];
|
|
|
- } else {
|
|
|
- self.text = super.text;
|
|
|
- }
|
|
|
-
|
|
|
- return self;
|
|
|
-}
|
|
|
-
|
|
|
-@end
|
|
|
-
|
|
|
-#pragma mark - TUIAttributedLabelLink
|
|
|
-
|
|
|
-@implementation TUIAttributedLabelLink
|
|
|
-
|
|
|
-- (instancetype)initWithAttributes:(NSDictionary *)attributes
|
|
|
- activeAttributes:(NSDictionary *)activeAttributes
|
|
|
- inactiveAttributes:(NSDictionary *)inactiveAttributes
|
|
|
- textCheckingResult:(NSTextCheckingResult *)result {
|
|
|
- if ((self = [super init])) {
|
|
|
- _result = result;
|
|
|
- _attributes = [attributes copy];
|
|
|
- _activeAttributes = [activeAttributes copy];
|
|
|
- _inactiveAttributes = [inactiveAttributes copy];
|
|
|
- }
|
|
|
-
|
|
|
- return self;
|
|
|
-}
|
|
|
-
|
|
|
-- (instancetype)initWithAttributesFromLabel:(TUIAttributedLabel *)label textCheckingResult:(NSTextCheckingResult *)result {
|
|
|
- return [self initWithAttributes:label.linkAttributes
|
|
|
- activeAttributes:label.activeLinkAttributes
|
|
|
- inactiveAttributes:label.inactiveLinkAttributes
|
|
|
- textCheckingResult:result];
|
|
|
-}
|
|
|
-
|
|
|
-#pragma mark - Accessibility
|
|
|
-
|
|
|
-- (NSString *)accessibilityValue {
|
|
|
- if ([_accessibilityValue length] == 0) {
|
|
|
- switch (self.result.resultType) {
|
|
|
- case NSTextCheckingTypeLink:
|
|
|
- _accessibilityValue = self.result.URL.absoluteString;
|
|
|
- break;
|
|
|
- case NSTextCheckingTypePhoneNumber:
|
|
|
- _accessibilityValue = self.result.phoneNumber;
|
|
|
- break;
|
|
|
- case NSTextCheckingTypeDate:
|
|
|
- _accessibilityValue = [NSDateFormatter localizedStringFromDate:self.result.date
|
|
|
- dateStyle:NSDateFormatterLongStyle
|
|
|
- timeStyle:NSDateFormatterLongStyle];
|
|
|
- break;
|
|
|
- default:
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return _accessibilityValue;
|
|
|
-}
|
|
|
-
|
|
|
-#pragma mark - NSCoding
|
|
|
-
|
|
|
-- (void)encodeWithCoder:(NSCoder *)aCoder {
|
|
|
- [aCoder encodeObject:self.result forKey:NSStringFromSelector(@selector(result))];
|
|
|
- [aCoder encodeObject:self.attributes forKey:NSStringFromSelector(@selector(attributes))];
|
|
|
- [aCoder encodeObject:self.activeAttributes forKey:NSStringFromSelector(@selector(activeAttributes))];
|
|
|
- [aCoder encodeObject:self.inactiveAttributes forKey:NSStringFromSelector(@selector(inactiveAttributes))];
|
|
|
- [aCoder encodeObject:self.accessibilityValue forKey:NSStringFromSelector(@selector(accessibilityValue))];
|
|
|
-}
|
|
|
-
|
|
|
-- (id)initWithCoder:(NSCoder *)aDecoder {
|
|
|
- if ((self = [super init])) {
|
|
|
- _result = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(result))];
|
|
|
- _attributes = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(attributes))];
|
|
|
- _activeAttributes = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(activeAttributes))];
|
|
|
- _inactiveAttributes = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(inactiveAttributes))];
|
|
|
- self.accessibilityValue = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(accessibilityValue))];
|
|
|
- }
|
|
|
-
|
|
|
- return self;
|
|
|
-}
|
|
|
-
|
|
|
-@end
|
|
|
-
|
|
|
-#pragma mark -
|
|
|
-
|
|
|
-static inline CGColorRef formatCGColorRefFromColor(id color) { return [color isKindOfClass:[UIColor class]] ? [color CGColor] : (__bridge CGColorRef)color; }
|
|
|
-
|
|
|
-static inline CTFontRef formatCTFontRefFromUIFont(UIFont *font) {
|
|
|
- CTFontRef ctfont = CTFontCreateWithName((__bridge CFStringRef)font.fontName, font.pointSize, NULL);
|
|
|
- return CFAutorelease(ctfont);
|
|
|
-}
|
|
|
-
|
|
|
-static inline NSDictionary *convertNSAttributedStringAttributesToCTAttributes(NSDictionary *attributes) {
|
|
|
- if (!attributes) return nil;
|
|
|
-
|
|
|
- NSMutableDictionary *mutableAttributes = [NSMutableDictionary dictionary];
|
|
|
-
|
|
|
- NSDictionary *convertMap = @{
|
|
|
- NSFontAttributeName : (NSString *)kCTFontAttributeName,
|
|
|
- NSBackgroundColorAttributeName : (NSString *)kTUIBackgroundFillColorAttributeName,
|
|
|
- NSForegroundColorAttributeName : (NSString *)kCTForegroundColorAttributeName,
|
|
|
- NSUnderlineColorAttributeName : (NSString *)kCTUnderlineColorAttributeName,
|
|
|
- NSUnderlineStyleAttributeName : (NSString *)kCTUnderlineStyleAttributeName,
|
|
|
- NSStrokeWidthAttributeName : (NSString *)kCTStrokeWidthAttributeName,
|
|
|
- NSStrokeColorAttributeName : (NSString *)kCTStrokeWidthAttributeName,
|
|
|
- NSKernAttributeName : (NSString *)kCTKernAttributeName,
|
|
|
- NSLigatureAttributeName : (NSString *)kCTLigatureAttributeName
|
|
|
- };
|
|
|
-
|
|
|
- [attributes enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
|
|
|
- key = [convertMap objectForKey:key] ?: key;
|
|
|
-
|
|
|
- if (![NSMutableParagraphStyle class]) {
|
|
|
- if ([value isKindOfClass:[UIFont class]]) {
|
|
|
- value = (__bridge id)formatCTFontRefFromUIFont(value);
|
|
|
- } else if ([value isKindOfClass:[UIColor class]]) {
|
|
|
- value = (__bridge id)((UIColor *)value).CGColor;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- [mutableAttributes setObject:value forKey:key];
|
|
|
- }];
|
|
|
-
|
|
|
- return [NSDictionary dictionaryWithDictionary:mutableAttributes];
|
|
|
-}
|