/*********************************************************************************** * * Copyright (c) 2010 Olivier Halligon * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * *********************************************************************************** * * Created by Olivier Halligon (AliSoftware) on 20 Jul. 2010. * * Any comment or suggestion welcome. Please contact me before using this class in * your projects. Referencing this project in your AboutBox/Credits is appreciated. * ***********************************************************************************/ #import "OHAttributedLabel.h" #import "NSAttributedString+Attributes.h" #define OHAttributedLabel_WarnAboutKnownIssues 1 ///////////////////////////////////////////////////////////////////////////// // MARK: Private Utility methods CGPoint CGPointFlipped(CGPoint point, CGRect bounds); CGRect CGRectFlipped(CGRect rect, CGRect bounds); NSRange NSRangeFromCFRange(CFRange range); CGRect CTLineGetTypographicBoundsAsRect(CTLineRef line, CGPoint lineOrigin); CGRect CTRunGetTypographicBoundsAsRect(CTRunRef run, CTLineRef line, CGPoint lineOrigin); BOOL CTLineContainsCharactersFromStringRange(CTLineRef line, NSRange range); BOOL CTRunContainsCharactersFromStringRange(CTRunRef run, NSRange range); ///////////////////////////////////////////////////////////////////////////// // MARK: - ///////////////////////////////////////////////////////////////////////////// CTTextAlignment CTTextAlignmentFromUITextAlignment(UITextAlignment alignment) { switch (alignment) { case UITextAlignmentLeft: return kCTLeftTextAlignment; case UITextAlignmentCenter: return kCTCenterTextAlignment; case UITextAlignmentRight: return kCTRightTextAlignment; case UITextAlignmentJustify: return kCTJustifiedTextAlignment; /* special OOB value if we decide to use it even if it's not really standard... */ default: return kCTNaturalTextAlignment; } } CTLineBreakMode CTLineBreakModeFromUILineBreakMode(UILineBreakMode lineBreakMode) { switch (lineBreakMode) { case UILineBreakModeWordWrap: return kCTLineBreakByWordWrapping; case UILineBreakModeCharacterWrap: return kCTLineBreakByCharWrapping; case UILineBreakModeClip: return kCTLineBreakByClipping; case UILineBreakModeHeadTruncation: return kCTLineBreakByTruncatingHead; case UILineBreakModeTailTruncation: return kCTLineBreakByTruncatingTail; case UILineBreakModeMiddleTruncation: return kCTLineBreakByTruncatingMiddle; default: return 0; } } // Don't use this method for origins. Origins always depend on the height of the rect. CGPoint CGPointFlipped(CGPoint point, CGRect bounds) { return CGPointMake(point.x, CGRectGetMaxY(bounds)-point.y); } CGRect CGRectFlipped(CGRect rect, CGRect bounds) { return CGRectMake(CGRectGetMinX(rect), CGRectGetMaxY(bounds)-CGRectGetMaxY(rect), CGRectGetWidth(rect), CGRectGetHeight(rect)); } NSRange NSRangeFromCFRange(CFRange range) { return NSMakeRange(range.location, range.length); } // Font Metrics: http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/FontHandling/Tasks/GettingFontMetrics.html CGRect CTLineGetTypographicBoundsAsRect(CTLineRef line, CGPoint lineOrigin) { CGFloat ascent = 0; CGFloat descent = 0; CGFloat leading = 0; CGFloat width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading); CGFloat height = ascent + descent /* + leading */; return CGRectMake(lineOrigin.x, lineOrigin.y - descent, width, height); } CGRect CTRunGetTypographicBoundsAsRect(CTRunRef run, CTLineRef line, CGPoint lineOrigin) { CGFloat ascent = 0; CGFloat descent = 0; CGFloat leading = 0; CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, &leading); CGFloat height = ascent + descent /* + leading */; CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL); return CGRectMake(lineOrigin.x + xOffset, lineOrigin.y - descent, width, height); } BOOL CTLineContainsCharactersFromStringRange(CTLineRef line, NSRange range) { NSRange lineRange = NSRangeFromCFRange(CTLineGetStringRange(line)); NSRange intersectedRange = NSIntersectionRange(lineRange, range); return (intersectedRange.length > 0); } BOOL CTRunContainsCharactersFromStringRange(CTRunRef run, NSRange range) { NSRange runRange = NSRangeFromCFRange(CTRunGetStringRange(run)); NSRange intersectedRange = NSIntersectionRange(runRange, range); return (intersectedRange.length > 0); } ///////////////////////////////////////////////////////////////////////////// // MARK: - // MARK: Private interface ///////////////////////////////////////////////////////////////////////////// @interface OHAttributedLabel(/* Private */) -(NSTextCheckingResult*)linkAtCharacterIndex:(CFIndex)idx; -(NSTextCheckingResult*)linkAtPoint:(CGPoint)pt; -(NSMutableAttributedString*)attributedTextWithLinks; -(void)resetTextFrame; -(void)drawActiveLinkHighlightForRect:(CGRect)rect; #if OHAttributedLabel_WarnAboutKnownIssues -(void)warnAboutKnownIssues_CheckLineBreakMode; -(void)warnAboutKnownIssues_CheckAdjustsFontSizeToFitWidth; #endif @end ///////////////////////////////////////////////////////////////////////////// // MARK: - // MARK: Implementation ///////////////////////////////////////////////////////////////////////////// @implementation OHAttributedLabel ///////////////////////////////////////////////////////////////////////////// // MARK: - // MARK: Init/Dealloc ///////////////////////////////////////////////////////////////////////////// - (void)commonInit { customLinks = [[NSMutableArray alloc] init]; self.linkColor = [UIColor blueColor]; self.highlightedLinkColor = [UIColor colorWithWhite:0.4 alpha:0.3]; self.underlineLinks = YES; self.automaticallyAddLinksForType = NSTextCheckingTypeLink; if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"tel:0"]]) { self.automaticallyAddLinksForType |= NSTextCheckingTypePhoneNumber; } self.onlyCatchTouchesOnLinks = YES; self.userInteractionEnabled = YES; self.contentMode = UIViewContentModeRedraw; [self resetAttributedText]; } - (id) initWithFrame:(CGRect)aFrame { self = [super initWithFrame:aFrame]; if (self != nil) { [self commonInit]; } return self; } - (id)initWithCoder:(NSCoder *)decoder { self = [super initWithCoder:decoder]; if (self != nil) { [self commonInit]; #if OHAttributedLabel_WarnAboutKnownIssues [self warnAboutKnownIssues_CheckLineBreakMode]; [self warnAboutKnownIssues_CheckAdjustsFontSizeToFitWidth]; #endif } return self; } -(void)dealloc { [_attributedText release]; [self resetTextFrame]; [customLinks release]; self.linkColor = nil; self.highlightedLinkColor = nil; [activeLink release]; [super dealloc]; } ///////////////////////////////////////////////////////////////////////////// // MARK: - // MARK: Links Mgmt ///////////////////////////////////////////////////////////////////////////// -(void)addCustomLink:(NSURL*)linkUrl inRange:(NSRange)range { NSTextCheckingResult* link = [NSTextCheckingResult linkCheckingResultWithRange:range URL:linkUrl]; [customLinks addObject:link]; [self setNeedsDisplay]; } -(void)removeAllCustomLinks { [customLinks removeAllObjects]; [self setNeedsDisplay]; } -(NSMutableAttributedString*)attributedTextWithLinks { NSMutableAttributedString* str = [self.attributedText mutableCopy]; if (!str) return nil; NSString* plainText = [str string]; if (plainText && (self.automaticallyAddLinksForType > 0)) { NSError* error = nil; NSDataDetector* linkDetector = [NSDataDetector dataDetectorWithTypes:self.automaticallyAddLinksForType error:&error]; [linkDetector enumerateMatchesInString:plainText options:0 range:NSMakeRange(0,[plainText length]) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { int32_t uStyle = self.underlineLinks ? kCTUnderlineStyleSingle : kCTUnderlineStyleNone; UIColor* thisLinkColor = (self.delegate && [self.delegate respondsToSelector:@selector(colorForLink:underlineStyle:)]) ? [self.delegate colorForLink:result underlineStyle:&uStyle] : self.linkColor; if (thisLinkColor) [str setTextColor:thisLinkColor range:[result range]]; if (uStyle>0) [str setTextUnderlineStyle:uStyle range:[result range]]; }]; } [customLinks enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { NSTextCheckingResult* result = (NSTextCheckingResult*)obj; int32_t uStyle = self.underlineLinks ? kCTUnderlineStyleSingle : kCTUnderlineStyleNone; UIColor* thisLinkColor = (self.delegate && [self.delegate respondsToSelector:@selector(colorForLink:underlineStyle:)]) ? [self.delegate colorForLink:result underlineStyle:&uStyle] : self.linkColor; @try { if (thisLinkColor) [str setTextColor:thisLinkColor range:[result range]]; if (uStyle>0) [str setTextUnderlineStyle:uStyle range:[result range]]; } @catch (NSException * e) { // Protection against NSRangeException if ([[e name] isEqualToString:NSRangeException]) { NSLog(@"[OHAttributedLabel] exception: %@",e); } else { @throw; } } }]; return [str autorelease]; } -(NSTextCheckingResult*)linkAtCharacterIndex:(CFIndex)idx { __block NSTextCheckingResult* foundResult = nil; NSString* plainText = [_attributedText string]; if (plainText && (self.automaticallyAddLinksForType > 0)) { NSError* error = nil; NSDataDetector* linkDetector = [NSDataDetector dataDetectorWithTypes:self.automaticallyAddLinksForType error:&error]; [linkDetector enumerateMatchesInString:plainText options:0 range:NSMakeRange(0,[plainText length]) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { NSRange r = [result range]; if (NSLocationInRange(idx, r)) { foundResult = [[result retain] autorelease]; *stop = YES; } }]; if (foundResult) return foundResult; } [customLinks enumerateObjectsUsingBlock:^(id obj, NSUInteger aidx, BOOL *stop) { NSRange r = [(NSTextCheckingResult*)obj range]; if (NSLocationInRange(idx, r)) { foundResult = [[obj retain] autorelease]; *stop = YES; } }]; return foundResult; } -(NSTextCheckingResult*)linkAtPoint:(CGPoint)point { static const CGFloat kVMargin = 5.f; if (!CGRectContainsPoint(CGRectInset(drawingRect, 0, -kVMargin), point)) return nil; CFArrayRef lines = CTFrameGetLines(textFrame); if (!lines) return nil; CFIndex nbLines = CFArrayGetCount(lines); NSTextCheckingResult* link = nil; CGPoint origins[nbLines]; CTFrameGetLineOrigins(textFrame, CFRangeMake(0,0), origins); for (int lineIndex=0 ; lineIndex