diff --git a/samples/TTCatalog/Classes/RootViewController.m b/samples/TTCatalog/Classes/RootViewController.m index 1f99733eba..4545b74ac1 100644 --- a/samples/TTCatalog/Classes/RootViewController.m +++ b/samples/TTCatalog/Classes/RootViewController.m @@ -111,4 +111,14 @@ - (void)willNavigateToObject:(id)object inView:(NSString*)viewType viewController.title = field.text; } +- (BOOL)shouldLoadExternalURL:(NSURL*)url { + NSString* message = [NSString stringWithFormat:@"You touched a link to %@", url]; + UIAlertView* alertView = [[[UIAlertView alloc] initWithTitle:@"Link" + message:message delegate:nil cancelButtonTitle:NSLocalizedString(@"Ok", @"") + otherButtonTitles:nil] autorelease]; + [alertView show]; + + return NO; +} + @end diff --git a/samples/TTCatalog/Classes/StyledTextTableTestController.m b/samples/TTCatalog/Classes/StyledTextTableTestController.m index e8b26a923b..a2865f10f6 100644 --- a/samples/TTCatalog/Classes/StyledTextTableTestController.m +++ b/samples/TTCatalog/Classes/StyledTextTableTestController.m @@ -26,6 +26,7 @@ - (void)loadView { @"This is a whole bunch of text made from characters and followed by this url http://bit.ly/1234", @"Here we have a url http://www.h0tlinkz.com followed by another http://www.internets.com", @"http://www.cnn.com is a url and the words you are now reading are the text that follows", + @"Here is text that has absolutely no styles. Move along now. Nothing to see here. Goodbye now.", // @"Let's test out some line breaks.\n\nOh yeah.", // @"This is a message with a long url in it http://www.foo.com/abra/cadabra/abrabra/dabarababa", nil]; @@ -34,7 +35,7 @@ - (void)loadView { TTListDataSource* dataSource = [[[TTListDataSource alloc] init] autorelease]; for (int i = 0; i < 50; ++i) { NSString* string = [strings objectAtIndex:i % strings.count]; - TTStyledText* text = [TTStyledText textFromURLString:string]; + TTStyledText* text = [TTStyledText textWithURLs:string]; // Add a bold prefix to the text NSString* title = [NSString stringWithFormat:@"Row %d: ", i+1]; diff --git a/samples/TTCatalog/Classes/StyledTextTestController.m b/samples/TTCatalog/Classes/StyledTextTestController.m index b6368770e5..dddaccfdee 100644 --- a/samples/TTCatalog/Classes/StyledTextTestController.m +++ b/samples/TTCatalog/Classes/StyledTextTestController.m @@ -8,16 +8,28 @@ @implementation StyledTextTestController - (void)loadView { [super loadView]; - NSString* kSampleText = @"This is a test of http://foo.com styled text. This test will \ -be more interesting when I implement the HTML parser. See the 'Styled Labels in Table' test \ -for another example of styled text. Gratuitous URL alert: http://www.foo.com"; + NSString* kText = @"This is a test of styled labels. Styled labels support \ +bold text and italic text. They also support \ +hyperlinks and inline images \ +. You can also embed a URL inline and it will be turned into \ +a link, like the following URL: http://www.foo.com"; - TTStyledLabel* label = [[[TTStyledLabel alloc] initWithFrame: + TTStyledLabel* label1 = [[[TTStyledLabel alloc] initWithFrame: CGRectInset(self.view.bounds, 10, 10)] autorelease]; - label.font = [UIFont systemFontOfSize:18]; - label.text = [TTStyledText textFromURLString:kSampleText]; - - [self.view addSubview:label]; + label1.font = [UIFont systemFontOfSize:17]; + label1.text = [TTStyledText textFromXHTML:kText]; + [label1 sizeToFit]; + [self.view addSubview:label1]; + + TTStyledLabel* label2 = [[[TTStyledLabel alloc] initWithFrame: + CGRectInset(self.view.bounds, 10, 10)] autorelease]; + label2.font = [UIFont systemFontOfSize:12]; + label2.text = [TTStyledText textFromXHTML:kText]; + label2.textColor = [UIColor grayColor]; + [label2 sizeToFit]; + label2.top = label1.bottom + 20; + [self.view addSubview:label2]; } @end + \ No newline at end of file diff --git a/samples/TTCatalog/TTCatalog.xcodeproj/project.pbxproj b/samples/TTCatalog/TTCatalog.xcodeproj/project.pbxproj index 581da40ad2..0beac15ed7 100755 --- a/samples/TTCatalog/TTCatalog.xcodeproj/project.pbxproj +++ b/samples/TTCatalog/TTCatalog.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 2899E5600DE3E45000AC0155 /* RootViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2899E55F0DE3E45000AC0155 /* RootViewController.xib */; }; 28AD73600D9D9599002E5188 /* MainWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 28AD735F0D9D9599002E5188 /* MainWindow.xib */; }; 28C286E10D94DF7D0034E888 /* RootViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 28C286E00D94DF7D0034E888 /* RootViewController.m */; }; + BE3188A10F822E2C00E3067D /* smiley.png in Resources */ = {isa = PBXBuildFile; fileRef = BE3188A00F822E2C00E3067D /* smiley.png */; }; BE5F25920EBA5F0400FD59A6 /* PhotoTest2Controller.m in Sources */ = {isa = PBXBuildFile; fileRef = BE5F25910EBA5F0400FD59A6 /* PhotoTest2Controller.m */; }; BE69B7590F62874900C02928 /* TableTestController.m in Sources */ = {isa = PBXBuildFile; fileRef = BE69B7580F62874900C02928 /* TableTestController.m */; }; BE6E46EC0F4578BA001CE9B4 /* TabBarTestController.m in Sources */ = {isa = PBXBuildFile; fileRef = BE6E46EB0F4578BA001CE9B4 /* TabBarTestController.m */; }; @@ -70,6 +71,7 @@ 28C286E00D94DF7D0034E888 /* RootViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RootViewController.m; sourceTree = ""; }; 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BE3188A00F822E2C00E3067D /* smiley.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = smiley.png; sourceTree = ""; }; BE5F25900EBA5F0400FD59A6 /* PhotoTest2Controller.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PhotoTest2Controller.h; sourceTree = ""; }; BE5F25910EBA5F0400FD59A6 /* PhotoTest2Controller.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PhotoTest2Controller.m; sourceTree = ""; }; BE69B7570F62874900C02928 /* TableTestController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TableTestController.h; sourceTree = ""; }; @@ -207,6 +209,7 @@ BE80E39C0EAF103200743358 /* DefaultAlbum.png */, BE6E4DBF0F46A352001CE9B4 /* tableIcon.png */, BECB1CC10F46AE9600AE5B52 /* person.jpg */, + BE3188A00F822E2C00E3067D /* smiley.png */, 8D1107310486CEB800E47090 /* Info.plist */, ); name = Resources; @@ -304,6 +307,7 @@ BE6E4DC00F46A352001CE9B4 /* tableIcon.png in Resources */, BECB1CC20F46AE9600AE5B52 /* person.jpg in Resources */, BEDCFBB40F4FFF820060B7D1 /* Three20.bundle in Resources */, + BE3188A10F822E2C00E3067D /* smiley.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/samples/TTCatalog/smiley.png b/samples/TTCatalog/smiley.png new file mode 100644 index 0000000000..86d17949c2 Binary files /dev/null and b/samples/TTCatalog/smiley.png differ diff --git a/src/TTNavigationCenter.m b/src/TTNavigationCenter.m index b6eb37d20d..94a271b47e 100644 --- a/src/TTNavigationCenter.m +++ b/src/TTNavigationCenter.m @@ -440,7 +440,10 @@ - (TTViewController*)displayURL:(NSString*)url withState:(NSDictionary*)state animated:(BOOL)animated { NSURL* u = [NSURL URLWithString:url]; if ([_urlSchemes indexOfObject:u.scheme] == NSNotFound) { - [[UIApplication sharedApplication] openURL:u]; + if (![_delegate respondsToSelector:@selector(shouldLoadExternalURL:)] + || [_delegate shouldLoadExternalURL:u]) { + [[UIApplication sharedApplication] openURL:u]; + } } else if (_viewLoaders) { id object = [self locateObject:u]; NSString* viewType = object && u.query ? u.query : u.host; diff --git a/src/TTStyledLabel.m b/src/TTStyledLabel.m index b29931b55d..88168c74b5 100644 --- a/src/TTStyledLabel.m +++ b/src/TTStyledLabel.m @@ -77,9 +77,13 @@ - (void)layoutSubviews { } - (CGSize)sizeThatFits:(CGSize)size { + _text.width = size.width; return CGSizeMake(_text.width, _text.height); } +/////////////////////////////////////////////////////////////////////////////////////////////////// +// UIResponder + - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { UITouch* touch = [touches anyObject]; CGPoint point = [touch locationInView:self]; @@ -97,8 +101,7 @@ - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { if (_highlightedNode) { - // XXXjoe Still deciding whether to do this, or use a delegate - // [[TTNavigationCenter defaultCenter] displayURL:_highlightedNode.url]; + [[TTNavigationCenter defaultCenter] displayURL:_highlightedNode.url]; self.highlightedNode = nil; @@ -164,6 +167,7 @@ - (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)style { if (self = [super initWithFrame:frame style:style]) { _highlightedLabel = nil; _highlightStartPoint = CGPointZero; + _highlightTimer = nil; self.delaysContentTouches = NO; } return self; @@ -171,15 +175,28 @@ - (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)style { - (void)dealloc { [_highlightedLabel release]; + [_highlightTimer invalidate]; [super dealloc]; } +- (void)delayedTouchesEnded:(NSTimer*)timer { + _highlightTimer = nil; + + self.highlightedLabel = nil; + + NSString* url = timer.userInfo; + [[TTNavigationCenter defaultCenter] displayURL:url]; +} + /////////////////////////////////////////////////////////////////////////////////////////////////// -// UIView +// UIResponder - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { [super touchesBegan:touches withEvent:event]; + [_highlightTimer invalidate]; + _highlightTimer = nil; + if (_highlightedLabel) { UITouch* touch = [touches anyObject]; _highlightStartPoint = [touch locationInView:self]; @@ -204,8 +221,11 @@ - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event { - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { if (_highlightedLabel) { + NSString* url = _highlightedLabel.highlightedNode.url; _highlightedLabel.highlightedNode = nil; - self.highlightedLabel = nil; + + _highlightTimer = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self + selector:@selector(delayedTouchesEnded:) userInfo:url repeats:NO]; } else { [super touchesEnded:touches withEvent:event]; } diff --git a/src/TTStyledText.m b/src/TTStyledText.m index 4164ba8cb3..a3b7680e37 100644 --- a/src/TTStyledText.m +++ b/src/TTStyledText.m @@ -1,88 +1,35 @@ #import "Three20/TTStyledText.h" #import "Three20/TTStyledTextNode.h" +#import "Three20/TTStyledTextParser.h" #import "Three20/TTAppearance.h" ////////////////////////////////////////////////////////////////////////////////////////////////// @implementation TTStyledText -@synthesize rootNode = _rootNode, font = _font, width = _width, height = _height, - lastLineWidth = _lastLineWidth; +@synthesize rootNode = _rootNode, font = _font, width = _width, height = _height; ////////////////////////////////////////////////////////////////////////////////////////////////// // class public -+ (TTStyledText*)textFromHTMLString:(NSString*)string { - // XXXjoe XHTML parser yet to be implemented - return nil; ++ (TTStyledText*)textFromXHTML:(NSString*)source { + TTStyledTextParser* parser = [[[TTStyledTextParser alloc] init] autorelease]; + [parser parseXHTML:source]; + if (parser.rootNode) { + return [[[TTStyledText alloc] initWithNode:parser.rootNode] autorelease]; + } else { + return nil; + } } -+ (TTStyledText*)textFromURLString:(NSString*)string { - TTStyledTextNode* rootNode = nil; - TTStyledTextNode* lastNode = nil; - - NSInteger index = 0; - while (index < string.length) { - NSRange searchRange = NSMakeRange(index, string.length - index); - NSRange startRange = [string rangeOfString:@"http://" options:NSCaseInsensitiveSearch - range:searchRange]; - if (startRange.location == NSNotFound) { - NSString* text = [string substringWithRange:searchRange]; - TTStyledTextNode* node = [[[TTStyledTextNode alloc] initWithText:text] autorelease]; - if (lastNode) { - lastNode.nextNode = node; - } else { - rootNode = node; - } - lastNode = node; - break; - } else { - NSRange beforeRange = NSMakeRange(searchRange.location, - startRange.location - searchRange.location); - if (beforeRange.length) { - NSString* text = [string substringWithRange:beforeRange]; - - TTStyledTextNode* node = [[[TTStyledTextNode alloc] initWithText:text] autorelease]; - if (lastNode) { - lastNode.nextNode = node; - } else { - rootNode = node; - } - lastNode = node; - } - - NSRange searchRange = NSMakeRange(startRange.location, string.length - startRange.location); - NSRange endRange = [string rangeOfString:@" " options:NSCaseInsensitiveSearch - range:searchRange]; - if (endRange.location == NSNotFound) { - NSString* url = [string substringWithRange:searchRange]; - TTStyledLinkNode* node = [[[TTStyledLinkNode alloc] initWithText:url] autorelease]; - node.url = url; - if (lastNode) { - lastNode.nextNode = node; - } else { - rootNode = node; - } - lastNode = node; - break; - } else { - NSRange urlRange = NSMakeRange(startRange.location, - endRange.location - startRange.location); - NSString* url = [string substringWithRange:urlRange]; - TTStyledLinkNode* node = [[[TTStyledLinkNode alloc] initWithText:url] autorelease]; - node.url = url; - if (lastNode) { - lastNode.nextNode = node; - } else { - rootNode = node; - } - lastNode = node; - index = endRange.location; - } - } ++ (TTStyledText*)textWithURLs:(NSString*)source { + TTStyledTextParser* parser = [[[TTStyledTextParser alloc] init] autorelease]; + [parser parseURLs:source]; + if (parser.rootNode) { + return [[[TTStyledText alloc] initWithNode:parser.rootNode] autorelease]; + } else { + return nil; } - - return [[[TTStyledText alloc] initWithNode:rootNode] autorelease]; } /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -93,91 +40,163 @@ - (UIFont*)defaultFont { } - (UIFont*)boldVersionOfFont:(UIFont*)font { - // XXXjoe Construct the family name + bold and use that instead of this return [UIFont boldSystemFontOfSize:font.pointSize]; } +- (UIFont*)italicVersionOfFont:(UIFont*)font { + return [UIFont italicSystemFontOfSize:font.pointSize]; +} + - (TTStyledTextFrame*)addFrameForText:(NSString*)text node:(TTStyledTextNode*)node after:(TTStyledTextFrame*)lastFrame { - TTStyledTextFrame* frame = [[TTStyledTextFrame alloc] initWithText:text node:node]; + TTStyledTextFrame* frame = [[[TTStyledTextFrame alloc] initWithText:text node:node] autorelease]; if (lastFrame) { lastFrame.nextFrame = frame; } else { _rootFrame = [frame retain]; } - [frame release]; return frame; } - (void)layoutFrames { UIFont* baseFont = _font ? _font : [self defaultFont]; - UIFont* boldFont = [self boldVersionOfFont:baseFont]; - CGSize spaceSize = [@" " sizeWithFont:baseFont]; - NSCharacterSet* whitespace = [NSCharacterSet whitespaceCharacterSet]; + UIFont* boldFont = nil; + UIFont* italicFont = nil; + NSCharacterSet* whitespace = [NSCharacterSet whitespaceAndNewlineCharacterSet]; - _lineHeight = spaceSize.height; - _height = _lineHeight; - _lastLineWidth = 0; - TTStyledTextFrame* lastFrame = nil; + CGFloat lineWidth = 0; + CGFloat height = 0; TTStyledTextNode* node = _rootNode; while (node) { - if ([node isKindOfClass:[TTStyledTextNode class]]) { - TTStyledTextNode* textNode = (TTStyledTextNode*)node; - NSString* text = textNode.text; + if ([node isKindOfClass:[TTStyledImageNode class]]) { + UIImage* image = [(TTStyledImageNode*)node image]; + + if (lineWidth + image.size.width > _width) { + // The image will be placed on the next line, so create a new frame for + // the current line and mark it with a line break + lastFrame.lineBreak = YES; + lineWidth = 0; + } + + lastFrame = [self addFrameForText:nil node:node after:lastFrame]; + lastFrame.width = image.size.width; + lastFrame.height = baseFont.ascender; + + if (!lineWidth) { + height += lastFrame.height; + } + lineWidth += image.size.width; + } else { + NSString* text = node.text; + NSUInteger length = text.length; - UIFont* font = [node isKindOfClass:[TTStyledLinkNode class]] - || [node isKindOfClass:[TTStyledBoldNode class]] ? boldFont : baseFont; - + // Figure out which font to use for the node + UIFont* font = baseFont; + if ([node isKindOfClass:[TTStyledLinkNode class]] + || [node isKindOfClass:[TTStyledBoldNode class]]) { + if (!boldFont) { + boldFont = [self boldVersionOfFont:baseFont]; + } + font = boldFont; + } else if ([node isKindOfClass:[TTStyledItalicNode class]]) { + if (!italicFont) { + italicFont = [self italicVersionOfFont:baseFont]; + } + font = italicFont; + } + + if (!node.nextNode && node == _rootNode) { + // This is this is the only node, so measure it all at once and move on + CGSize textSize = [text sizeWithFont:font + constrainedToSize:CGSizeMake(_width, CGFLOAT_MAX) + lineBreakMode:UILineBreakModeWordWrap]; + lastFrame = [self addFrameForText:text node:node after:lastFrame]; + lastFrame.width = textSize.width; + lastFrame.height = textSize.height; + lastFrame.font = font; + height += textSize.height; + break; + } + NSInteger index = 0; NSInteger lineStartIndex = 0; CGFloat frameWidth = 0; - while (index < text.length) { - NSRange searchRange = NSMakeRange(index, text.length - index); + + while (index < length) { + // Search for the next whitespace character + NSRange searchRange = NSMakeRange(index, length - index); NSRange spaceRange = [text rangeOfCharacterFromSet:whitespace options:0 range:searchRange]; - - NSRange wordRange; - if (spaceRange.location != NSNotFound) { - wordRange = NSMakeRange(searchRange.location, - (spaceRange.location+1) - searchRange.location); - } else { - wordRange = NSMakeRange(searchRange.location, text.length - searchRange.location); - } - + + // Get the word prior to the whitespace + NSRange wordRange = spaceRange.location != NSNotFound + ? NSMakeRange(searchRange.location, (spaceRange.location+1) - searchRange.location) + : NSMakeRange(searchRange.location, length - searchRange.location); NSString* word = [text substringWithRange:wordRange]; - CGSize wordSize = [word sizeWithFont:font]; - - if (_lastLineWidth + wordSize.width > _width) { - if (wordSize.width > _width) { - // XXXjoe Split word into multiple frames here - } - NSRange lineRange = NSMakeRange(lineStartIndex, index - lineStartIndex); - if (lineRange.length) { - NSString* line = [text substringWithRange:lineRange]; - lastFrame = [self addFrameForText:line node:node after:lastFrame]; - lastFrame.width = frameWidth; + // Measure the word and check to see if it fits on the current line + CGSize wordSize = [word sizeWithFont:font]; + if (lineWidth + wordSize.width > _width) { + if (0 && wordSize.width > _width) { + // The word is larger than an entire line, so we need to split it across lines + // XXXjoe TODO + } else { + // The word will be placed on the next line, so create a new frame for + // the current line and mark it with a line break + NSRange lineRange = NSMakeRange(lineStartIndex, index - lineStartIndex); + if (lineRange.length) { + NSString* line = [text substringWithRange:lineRange]; + lastFrame = [self addFrameForText:line node:node after:lastFrame]; + lastFrame.width = frameWidth; + lastFrame.height = wordSize.height; + lastFrame.font = font; + } + + lastFrame.lineBreak = YES; + lineStartIndex = lineRange.location + lineRange.length; frameWidth = 0; + lineWidth = 0; } - - lastFrame.lineBreak = YES; - lineStartIndex = lineRange.location + lineRange.length; - - _lastLineWidth = 0; - _height += _lineHeight; } - _lastLineWidth += wordSize.width; + if (!lineWidth) { + // We are at the start of a new line + if (node.nextNode) { + // Count the height of the new line + height += wordSize.height; + } else { + // This is the last node, so we don't need to keep measuring every word. We + // can just measure all remaining text and create a new frame for all of it. + NSString* lines = [text substringWithRange:searchRange]; + CGSize linesSize = [lines sizeWithFont:font + constrainedToSize:CGSizeMake(_width, CGFLOAT_MAX) + lineBreakMode:UILineBreakModeWordWrap]; + + lastFrame = [self addFrameForText:lines node:node after:lastFrame]; + lastFrame.width = linesSize.width; + lastFrame.height = linesSize.height; + lastFrame.font = font; + height += linesSize.height; + break; + } + } + frameWidth += wordSize.width; + lineWidth += wordSize.width; index = wordRange.location + wordRange.length; - if (index >= text.length) { - NSRange lineRange = NSMakeRange(lineStartIndex, - (wordRange.location + wordRange.length) - lineStartIndex); - NSString* line = [text substringWithRange:lineRange]; + if (index >= length) { + // The current word was at the very end of the string + NSRange lineRange = NSMakeRange(lineStartIndex, (wordRange.location + wordRange.length) + - lineStartIndex); + NSString* line = !lineWidth ? word : [text substringWithRange:lineRange]; + TTLOG(@"LINE '%@'", line); + lastFrame = [self addFrameForText:line node:node after:lastFrame]; lastFrame.width = frameWidth; + lastFrame.height = wordSize.height; + lastFrame.font = font; frameWidth = 0; } } @@ -186,8 +205,7 @@ - (void)layoutFrames { node = node.nextNode; } - _height = ceil(_height); - _lastLineWidth = ceil(_lastLineWidth); + _height = ceil(height); } /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -200,7 +218,6 @@ - (id)initWithNode:(TTStyledTextNode*)rootNode { _font = nil; _width = 0; _height = 0; - _lastLineWidth = 0; } return self; } @@ -259,17 +276,14 @@ - (void)drawAtPoint:(CGPoint)point { - (void)drawAtPoint:(CGPoint)point highlighted:(BOOL)highlighted { CGContextRef context = UIGraphicsGetCurrentContext(); - UIFont* baseFont = _font ? _font : [self defaultFont]; - UIFont* boldFont = [self boldVersionOfFont:baseFont]; - CGPoint origin = point; TTStyledTextFrame* frame = self.rootFrame; while (frame) { + CGRect frameRect = CGRectMake(origin.x, origin.y, frame.width, frame.height); if ([frame.node isKindOfClass:[TTStyledLinkNode class]]) { TTStyledLinkNode* linkNode = (TTStyledLinkNode*)frame.node; if (linkNode.highlighted) { - CGRect frameRect = CGRectMake(origin.x, origin.y, frame.width, _lineHeight); UIColor* fill[] = {[UIColor colorWithWhite:0 alpha:0.3]}; [[TTAppearance appearance] draw:TTStyleFill rect:frameRect fill:fill fillCount:1 stroke:nil radius:3]; @@ -280,21 +294,29 @@ - (void)drawAtPoint:(CGPoint)point highlighted:(BOOL)highlighted { [[TTAppearance appearance].linkTextColor setFill]; } - [frame.text drawAtPoint:origin withFont:boldFont]; + [frame.text drawInRect:frameRect withFont:frame.font]; if (!highlighted) { CGContextRestoreGState(context); } } else if ([frame.node isKindOfClass:[TTStyledBoldNode class]]) { - [frame.text drawAtPoint:origin withFont:boldFont]; + [frame.text drawInRect:frameRect withFont:frame.font]; + } else if ([frame.node isKindOfClass:[TTStyledItalicNode class]]) { + [frame.text drawInRect:frameRect withFont:frame.font]; + } else if ([frame.node isKindOfClass:[TTStyledImageNode class]]) { + TTStyledImageNode* imageNode = (TTStyledImageNode*)frame.node; + UIImage* image = imageNode.image; + CGRect imageRect = CGRectMake(origin.x, (origin.y+frame.height)-image.size.height, + image.size.width, image.size.height); + [imageNode.image drawInRect:imageRect]; } else { - [frame.text drawAtPoint:origin withFont:baseFont]; + [frame.text drawInRect:frameRect withFont:frame.font]; } origin.x += frame.width; if (frame.lineBreak) { origin.x = 0; - origin.y += _lineHeight; + origin.y += frame.height; } frame = frame.nextFrame; @@ -305,7 +327,7 @@ - (TTStyledTextFrame*)hitTest:(CGPoint)point { CGPoint origin = CGPointMake(0, 0); TTStyledTextFrame* frame = self.rootFrame; while (frame) { - CGRect rect = CGRectMake(origin.x, origin.y, frame.width, _lineHeight); + CGRect rect = CGRectMake(origin.x, origin.y, frame.width, frame.height); if (CGRectContainsPoint(rect, point)) { return frame; } @@ -313,7 +335,7 @@ - (TTStyledTextFrame*)hitTest:(CGPoint)point { origin.x += frame.width; if (frame.lineBreak) { origin.x = 0; - origin.y += _lineHeight; + origin.y += frame.height; } frame = frame.nextFrame; @@ -327,8 +349,8 @@ - (TTStyledTextFrame*)hitTest:(CGPoint)point { @implementation TTStyledTextFrame -@synthesize node = _node, text = _text, nextFrame = _nextFrame, width = _width, - lineBreak = _lineBreak; +@synthesize node = _node, nextFrame = _nextFrame, text = _text, font = _font, + width = _width, height = _height, lineBreak = _lineBreak; /////////////////////////////////////////////////////////////////////////////////////////////////// // NSObject @@ -336,17 +358,21 @@ @implementation TTStyledTextFrame - (id)initWithText:(NSString*)text node:(TTStyledTextNode*)node { if (self = [super init]) { _text = [text retain]; - _node = [node retain]; _nextFrame = nil; + _node = [node retain]; + _font = nil; + _width = 0; + _height = 0; _lineBreak = NO; } return self; } - (void)dealloc { - [_text release]; [_node release]; [_nextFrame release]; + [_text release]; + [_font release]; [super dealloc]; } diff --git a/src/TTStyledTextNode.m b/src/TTStyledTextNode.m index 9a5451c817..5db35a32bc 100644 --- a/src/TTStyledTextNode.m +++ b/src/TTStyledTextNode.m @@ -1,4 +1,6 @@ #import "Three20/TTStyledTextNode.h" +#import "Three20/TTURLRequest.h" +#import "Three20/TTURLResponse.h" ////////////////////////////////////////////////////////////////////////////////////////////////// @@ -57,6 +59,20 @@ - (NSString*)description { @end + +////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation TTStyledItalicNode + +////////////////////////////////////////////////////////////////////////////////////////////////// +// NSObject + +- (NSString*)description { + return [NSString stringWithFormat:@"/%@/", _text]; +} + +@end + ////////////////////////////////////////////////////////////////////////////////////////////////// @implementation TTStyledLinkNode @@ -84,3 +100,47 @@ - (NSString*)description { } @end + +////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation TTStyledImageNode + +@synthesize url = _url, image = _image; + +////////////////////////////////////////////////////////////////////////////////////////////////// +// NSObject + +- (id)init { + if (self = [super init]) { + _url = nil; + _image = nil; + } + return self; +} + +- (void)dealloc { + [_url release]; + [_image release]; + [super dealloc]; +} + +- (NSString*)description { + return [NSString stringWithFormat:@"(%@)", _url]; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +// public + +- (UIImage*)image { + if (!_image && _url) { + TTURLRequest* request = [TTURLRequest requestWithURL:_url delegate:nil]; + TTURLImageResponse* response = [[[TTURLImageResponse alloc] init] autorelease]; + request.response = response; + if ([request send]) { + _image = [response.image retain]; + } + } + return _image; +} + +@end diff --git a/src/TTStyledTextParser.m b/src/TTStyledTextParser.m new file mode 100644 index 0000000000..5b4a53541a --- /dev/null +++ b/src/TTStyledTextParser.m @@ -0,0 +1,168 @@ +#import "Three20/TTStyledTextParser.h" +#import "Three20/TTStyledTextNode.h" + +////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation TTStyledTextParser + +@synthesize rootNode = _rootNode; + +////////////////////////////////////////////////////////////////////////////////////////////////// +// private + +- (void)addNode:(TTStyledTextNode*)node { + if (!_rootNode) { + _rootNode = [node retain]; + } else { + _lastNode.nextNode = node; + } + _lastNode = node; +} + +- (void)flushCharacters { + if (_chars.length) { + if (_openNode) { + _openNode.text = _chars; + _openNode = nil; + } else if (1) { + [self parseURLs:_chars]; + } else { + TTStyledTextNode* node = [[[TTStyledTextNode alloc] initWithText:_chars] autorelease]; + [self addNode:node]; + } + } + + [_chars release]; + _chars = nil; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +// NSObject + +- (id)init { + if (self = [super init]) { + _rootNode = nil; + _openNode = nil; + _lastNode = nil; + _chars = nil; + } + return self; +} + +- (void)dealloc { + [_rootNode release]; + [_chars release]; + [super dealloc]; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// NSXMLParserDelegate + +- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName + namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName + attributes:(NSDictionary *)attributeDict { + [self flushCharacters]; + + NSString* tag = [elementName lowercaseString]; + if ([tag isEqualToString:@"b"]) { + TTStyledBoldNode* node = [[[TTStyledBoldNode alloc] init] autorelease]; + [self addNode:node]; + if (_openNode) { + // XXXjoe Merge styles + } else { + _openNode = node; + } + } else if ([tag isEqualToString:@"i"]) { + TTStyledItalicNode* node = [[[TTStyledItalicNode alloc] init] autorelease]; + [self addNode:node]; + if (_openNode) { + // XXXjoe Merge styles + } else { + _openNode = node; + } + } else if ([tag isEqualToString:@"a"]) { + TTStyledLinkNode* node = [[[TTStyledLinkNode alloc] init] autorelease]; + node.url = [attributeDict objectForKey:@"href"]; + + [self addNode:node]; + if (_openNode) { + // XXXjoe Merge styles + } else { + _openNode = node; + } + } else if ([tag isEqualToString:@"img"]) { + TTStyledImageNode* node = [[[TTStyledImageNode alloc] init] autorelease]; + node.url = [attributeDict objectForKey:@"src"]; + [self addNode:node]; + } +} + +- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string { + if (!_chars) { + _chars = [string mutableCopy]; + } else { + [_chars appendString:string]; + } +} + +- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName + namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { + [self flushCharacters]; + _openNode = nil; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// public + +- (void)parseXHTML:(NSString*)html { + NSString* document = [NSString stringWithFormat:@"%@", html]; + NSData* data = [document dataUsingEncoding:html.fastestEncoding]; + NSXMLParser* parser = [[[NSXMLParser alloc] initWithData:data] autorelease]; + parser.delegate = self; + [parser parse]; +} + +- (void)parseURLs:(NSString*)string { + NSInteger index = 0; + while (index < string.length) { + NSRange searchRange = NSMakeRange(index, string.length - index); + NSRange startRange = [string rangeOfString:@"http://" options:NSCaseInsensitiveSearch + range:searchRange]; + if (startRange.location == NSNotFound) { + NSString* text = [string substringWithRange:searchRange]; + TTStyledTextNode* node = [[[TTStyledTextNode alloc] initWithText:text] autorelease]; + [self addNode:node]; + break; + } else { + NSRange beforeRange = NSMakeRange(searchRange.location, + startRange.location - searchRange.location); + if (beforeRange.length) { + NSString* text = [string substringWithRange:beforeRange]; + + TTStyledTextNode* node = [[[TTStyledTextNode alloc] initWithText:text] autorelease]; + [self addNode:node]; + } + + NSRange searchRange = NSMakeRange(startRange.location, string.length - startRange.location); + NSRange endRange = [string rangeOfString:@" " options:NSCaseInsensitiveSearch + range:searchRange]; + if (endRange.location == NSNotFound) { + NSString* url = [string substringWithRange:searchRange]; + TTStyledLinkNode* node = [[[TTStyledLinkNode alloc] initWithText:url] autorelease]; + node.url = url; + [self addNode:node]; + break; + } else { + NSRange urlRange = NSMakeRange(startRange.location, + endRange.location - startRange.location); + NSString* url = [string substringWithRange:urlRange]; + TTStyledLinkNode* node = [[[TTStyledLinkNode alloc] initWithText:url] autorelease]; + node.url = url; + [self addNode:node]; + index = endRange.location; + } + } + } +} + +@end diff --git a/src/TTTableFieldCell.m b/src/TTTableFieldCell.m index 811660b8b3..b4a01aa6e3 100644 --- a/src/TTTableFieldCell.m +++ b/src/TTTableFieldCell.m @@ -228,18 +228,6 @@ - (void)layoutSubviews { _label.frame = CGRectInset(self.contentView.bounds, kHPadding, kVPadding); } -/////////////////////////////////////////////////////////////////////////////////////////////////// -// UITableViewCell - -//- (void)setSelected:(BOOL)selected animated:(BOOL)animated { -// if (_label.highlightedNode) { -// self.selectionStyle = UITableViewCellSelectionStyleNone; -// } else { -// self.selectionStyle = UITableViewCellSelectionStyleBlue; -// [super setSelected:selected animated:animated]; -// } -//} - /////////////////////////////////////////////////////////////////////////////////////////////////// // TTTableViewCell diff --git a/src/TTTableViewController.m b/src/TTTableViewController.m index 976d48f09a..f227eae954 100644 --- a/src/TTTableViewController.m +++ b/src/TTTableViewController.m @@ -213,7 +213,9 @@ - (void)updateTableDelegate { - (void)reloadTableData { [self updateTableDelegate]; + //NSDate* date = [NSDate date]; [_tableView reloadData]; + //NSLog(@"TABLE LAYOUT %fs", [date timeIntervalSinceNow]); } - (void)refreshingHideAnimationStopped { diff --git a/src/TTTableViewDataSource.m b/src/TTTableViewDataSource.m index 6c6d2d1f79..14c6d24871 100644 --- a/src/TTTableViewDataSource.m +++ b/src/TTTableViewDataSource.m @@ -2,6 +2,7 @@ #import "Three20/TTTableField.h" #import "Three20/TTTableFieldCell.h" #import "Three20/TTURLCache.h" +#import /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -34,13 +35,18 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger - (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { id object = [self tableView:tableView objectForRowAtIndexPath:indexPath]; + Class cellClass = [self tableView:tableView cellClassForObject:object]; + const char* className = class_getName(cellClass); + NSString* identifier = [[NSString alloc] initWithBytesNoCopy:(char*)className + length:strlen(className) + encoding:NSASCIIStringEncoding freeWhenDone:NO]; - NSString* className = NSStringFromClass(cellClass); - UITableViewCell* cell = (UITableViewCell*)[tableView dequeueReusableCellWithIdentifier:className]; + UITableViewCell* cell = (UITableViewCell*)[tableView dequeueReusableCellWithIdentifier:identifier]; if (cell == nil) { - cell = [[[cellClass alloc] initWithFrame:CGRectZero reuseIdentifier:className] autorelease]; + cell = [[[cellClass alloc] initWithFrame:CGRectZero reuseIdentifier:identifier] autorelease]; } + [identifier release]; if ([cell isKindOfClass:[TTTableViewCell class]]) { [(TTTableViewCell*)cell setObject:object]; diff --git a/src/TTURLRequest.m b/src/TTURLRequest.m index db7ce1a270..e8637a978a 100644 --- a/src/TTURLRequest.m +++ b/src/TTURLRequest.m @@ -28,7 +28,9 @@ + (TTURLRequest*)requestWithURL:(NSString*)url delegate:(id)delegate { if (self = [self init]) { _url = [url retain]; - [_delegates addObject:delegate]; + if (delegate) { + [_delegates addObject:delegate]; + } } return self; } diff --git a/src/TTURLRequestQueue.m b/src/TTURLRequestQueue.m index 447c398c40..2b32f20158 100644 --- a/src/TTURLRequestQueue.m +++ b/src/TTURLRequestQueue.m @@ -43,7 +43,7 @@ - (id)initForRequest:(TTURLRequest*)request queue:(TTURLRequestQueue*)queue; - (void)addRequest:(TTURLRequest*)request; - (void)removeRequest:(TTURLRequest*)request; -- (void)load; +- (void)load:(NSURL*)url; - (BOOL)cancel:(TTURLRequest*)request; @end @@ -158,28 +158,6 @@ - (void)dispatchError:(NSError*)error { } } -- (void)loadFromBundle:(NSURL*)url { - NSString* path = nil; - if (url.path.length) { - NSString* fileName = [url.path substringFromIndex:1]; - path = [[NSBundle mainBundle] pathForResource:fileName ofType:nil inDirectory:url.host]; - } else { - path = [[NSBundle mainBundle] pathForResource:url.host ofType:nil]; - } - - NSFileManager* fm = [NSFileManager defaultManager]; - if (path && [fm fileExistsAtPath:path]) { - NSData* data = [NSData dataWithContentsOfFile:path]; - [_queue performSelector:@selector(loader:didLoadResponse:data:) withObject:self - withObject:nil withObject:data]; - } else { - NSError* error = [NSError errorWithDomain:NSCocoaErrorDomain - code:NSFileReadNoSuchFileError userInfo:nil]; - [_queue performSelector:@selector(loader:didFailLoadWithError:) withObject:self - withObject:error]; - } -} - ////////////////////////////////////////////////////////////////////////////////////////////////// // NSURLConnectionDelegate @@ -238,7 +216,7 @@ - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)err // If there is a network error then we will wait and retry a few times just in case // it was just a temporary blip in connectivity --_retriesLeft; - [self load]; + [self load:[NSURL URLWithString:_url]]; } else { [_queue performSelector:@selector(loader:didFailLoadWithError:) withObject:self withObject:error]; @@ -259,14 +237,9 @@ - (void)removeRequest:(TTURLRequest*)request { [_requests removeObject:request]; } -- (void)load { +- (void)load:(NSURL*)url { if (!_connection) { - NSURL* url = [NSURL URLWithString:_url]; - if ([url.scheme isEqualToString:@"bundle"]) { - [self loadFromBundle:url]; - } else { - [self connectToURL:url]; - } + [self connectToURL:url]; } } @@ -416,7 +389,7 @@ - (void)executeLoader:(TTRequestLoader*)loader { [_loaders removeObjectForKey:loader.cacheKey]; } else { ++_totalLoading; - [loader load]; + [loader load:[NSURL URLWithString:loader.url]]; } } @@ -479,6 +452,45 @@ - (void)loaderDidCancel:(TTRequestLoader*)loader wasLoading:(BOOL)wasLoading { } } +- (void)loadFromBundle:(NSURL*)url request:(TTURLRequest*)request { + request.respondedFromCache = YES; + + NSString* path = nil; + if (url.path.length) { + NSString* fileName = [url.path substringFromIndex:1]; + path = [[NSBundle mainBundle] pathForResource:fileName ofType:nil inDirectory:url.host]; + } else { + path = [[NSBundle mainBundle] pathForResource:url.host ofType:nil]; + } + + NSFileManager* fm = [NSFileManager defaultManager]; + if (path && [fm fileExistsAtPath:path]) { + NSData* data = [NSData dataWithContentsOfFile:path]; + NSError* error = [request.response request:request processResponse:nil data:data]; + if (error) { + for (id delegate in request.delegates) { + if ([delegate respondsToSelector:@selector(request:didFailLoadWithError:)]) { + [delegate request:request didFailLoadWithError:error]; + } + } + } else { + for (id delegate in request.delegates) { + if ([delegate respondsToSelector:@selector(requestDidFinishLoad:)]) { + [delegate requestDidFinishLoad:request]; + } + } + } + } else { + NSError* error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSFileReadNoSuchFileError userInfo:nil]; + for (id delegate in request.delegates) { + if ([delegate respondsToSelector:@selector(request:didFailLoadWithError:)]) { + [delegate request:request didFailLoadWithError:error]; + } + } + } +} + ////////////////////////////////////////////////////////////////////////////////////////////////// - (void)setSuspended:(BOOL)isSuspended { @@ -494,6 +506,12 @@ - (void)setSuspended:(BOOL)isSuspended { } - (BOOL)sendRequest:(TTURLRequest*)request { + NSURL* url = [NSURL URLWithString:request.url]; + if ([url.scheme isEqualToString:@"bundle"]) { + [self loadFromBundle:url request:request]; + return YES; + } + if (!request.cacheKey) { request.cacheKey = [[TTURLCache sharedCache] keyForURL:request.url]; } @@ -529,7 +547,7 @@ - (BOOL)sendRequest:(TTURLRequest*)request { return NO; } } - + // Finally, create a new loader and hit the network (unless we are suspended) loader = [[TTRequestLoader alloc] initForRequest:request queue:self]; [_loaders setObject:loader forKey:request.cacheKey]; @@ -537,10 +555,10 @@ - (BOOL)sendRequest:(TTURLRequest*)request { [_loaderQueue addObject:loader]; } else { ++_totalLoading; - [loader load]; + [loader load:url]; } [loader release]; - + return NO; } diff --git a/src/Three20.xcodeproj/project.pbxproj b/src/Three20.xcodeproj/project.pbxproj index 39ea285888..5ff9c4959d 100755 --- a/src/Three20.xcodeproj/project.pbxproj +++ b/src/Three20.xcodeproj/project.pbxproj @@ -63,6 +63,8 @@ BEE34F270F496620008C826E /* TTTextEditor.h in Headers */ = {isa = PBXBuildFile; fileRef = BEE34F260F496620008C826E /* TTTextEditor.h */; }; BEF198E00F818A1C0010D36E /* TTStyledTextNode.m in Sources */ = {isa = PBXBuildFile; fileRef = BEF198DF0F818A1C0010D36E /* TTStyledTextNode.m */; }; BEF198E20F818A240010D36E /* TTStyledTextNode.h in Headers */ = {isa = PBXBuildFile; fileRef = BEF198E10F818A240010D36E /* TTStyledTextNode.h */; }; + BEF19D6B0F81B8440010D36E /* TTStyledTextParser.h in Headers */ = {isa = PBXBuildFile; fileRef = BEF19D6A0F81B8440010D36E /* TTStyledTextParser.h */; }; + BEF19D6D0F81B8530010D36E /* TTStyledTextParser.m in Sources */ = {isa = PBXBuildFile; fileRef = BEF19D6C0F81B8530010D36E /* TTStyledTextParser.m */; }; BEF31F700F352E64000DE5D2 /* TTActivityLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = BEF31F460F352E64000DE5D2 /* TTActivityLabel.m */; }; BEF31F710F352E64000DE5D2 /* TTErrorView.m in Sources */ = {isa = PBXBuildFile; fileRef = BEF31F470F352E64000DE5D2 /* TTErrorView.m */; }; BEF31F720F352E64000DE5D2 /* TTGlobal.m in Sources */ = {isa = PBXBuildFile; fileRef = BEF31F480F352E64000DE5D2 /* TTGlobal.m */; }; @@ -170,6 +172,8 @@ BEE34F260F496620008C826E /* TTTextEditor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TTTextEditor.h; path = Three20/TTTextEditor.h; sourceTree = ""; }; BEF198DF0F818A1C0010D36E /* TTStyledTextNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TTStyledTextNode.m; sourceTree = ""; }; BEF198E10F818A240010D36E /* TTStyledTextNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TTStyledTextNode.h; path = Three20/TTStyledTextNode.h; sourceTree = ""; }; + BEF19D6A0F81B8440010D36E /* TTStyledTextParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TTStyledTextParser.h; path = Three20/TTStyledTextParser.h; sourceTree = ""; }; + BEF19D6C0F81B8530010D36E /* TTStyledTextParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TTStyledTextParser.m; sourceTree = ""; }; BEF31F3A0F352DF5000DE5D2 /* libThree20.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libThree20.a; sourceTree = BUILT_PRODUCTS_DIR; }; BEF31F460F352E64000DE5D2 /* TTActivityLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TTActivityLabel.m; sourceTree = ""; }; BEF31F470F352E64000DE5D2 /* TTErrorView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TTErrorView.m; sourceTree = ""; }; @@ -274,6 +278,8 @@ BE3EDE420F80C79600A732BE /* TTStyledText.m */, BEF198E10F818A240010D36E /* TTStyledTextNode.h */, BEF198DF0F818A1C0010D36E /* TTStyledTextNode.m */, + BEF19D6A0F81B8440010D36E /* TTStyledTextParser.h */, + BEF19D6C0F81B8530010D36E /* TTStyledTextParser.m */, ); name = "Styled Text"; sourceTree = ""; @@ -484,6 +490,7 @@ BEF341970F8042520027E93C /* TTStyledLabel.h in Headers */, BE3EDE410F80C78500A732BE /* TTStyledText.h in Headers */, BEF198E20F818A240010D36E /* TTStyledTextNode.h in Headers */, + BEF19D6B0F81B8440010D36E /* TTStyledTextParser.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -577,6 +584,7 @@ BEF341950F8042440027E93C /* TTStyledLabel.m in Sources */, BE3EDE430F80C79600A732BE /* TTStyledText.m in Sources */, BEF198E00F818A1C0010D36E /* TTStyledTextNode.m in Sources */, + BEF19D6D0F81B8530010D36E /* TTStyledTextParser.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/src/Three20/TTNavigationCenter.h b/src/Three20/TTNavigationCenter.h index edf921afe4..e961c322ae 100644 --- a/src/Three20/TTNavigationCenter.h +++ b/src/Three20/TTNavigationCenter.h @@ -98,5 +98,7 @@ typedef enum { - (void)didNavigateToObject:(id)object inView:(NSString*)viewType withController:(UIViewController*)viewController; + +- (BOOL)shouldLoadExternalURL:(NSURL*)url; @end diff --git a/src/Three20/TTStyledLabel.h b/src/Three20/TTStyledLabel.h index ccc42cd257..cea0e76d96 100644 --- a/src/Three20/TTStyledLabel.h +++ b/src/Three20/TTStyledLabel.h @@ -60,6 +60,7 @@ @interface TTStyledTextTableView : UITableView { TTStyledLabel* _highlightedLabel; CGPoint _highlightStartPoint; + NSTimer* _highlightTimer; } @property(nonatomic,retain) TTStyledLabel* highlightedLabel; diff --git a/src/Three20/TTStyledText.h b/src/Three20/TTStyledText.h index 56389f83f8..e6891046ff 100644 --- a/src/Three20/TTStyledText.h +++ b/src/Three20/TTStyledText.h @@ -8,8 +8,6 @@ UIFont* _font; CGFloat _width; CGFloat _height; - CGFloat _lineHeight; - CGFloat _lastLineWidth; } @property(nonatomic, retain) TTStyledTextNode* rootNode; @@ -17,21 +15,18 @@ @property(nonatomic, retain) UIFont* font; @property(nonatomic) CGFloat width; @property(nonatomic, readonly) CGFloat height; -@property(nonatomic, readonly) CGFloat lastLineWidth; /** - * Constructs a tree of HTML nodes from a well-formatted XHTML string. - * - * NOT YET IMPLEMENTED. + * Constructs a tree of HTML nodes from a well-formed XHTML fragment. */ -+ (TTStyledText*)textFromHTMLString:(NSString*)string; ++ (TTStyledText*)textFromXHTML:(NSString*)source; /** * Constructs a tree of HTML nodes from a string containing URLs. * * Only URLs are parsed, not HTML markup. URLs are turned into links. */ -+ (TTStyledText*)textFromURLString:(NSString*)string; ++ (TTStyledText*)textWithURLs:(NSString*)source; - (id)initWithNode:(TTStyledTextNode*)rootNode; @@ -48,16 +43,20 @@ @interface TTStyledTextFrame : NSObject { TTStyledTextNode* _node; - NSString* _text; TTStyledTextFrame* _nextFrame; + NSString* _text; + UIFont* _font; CGFloat _width; + CGFloat _height; BOOL _lineBreak; } @property(nonatomic, readonly) TTStyledTextNode* node; -@property(nonatomic, readonly) NSString* text; @property(nonatomic, retain) TTStyledTextFrame* nextFrame; +@property(nonatomic, readonly) NSString* text; +@property(nonatomic, retain) UIFont* font; @property(nonatomic) CGFloat width; +@property(nonatomic) CGFloat height; @property(nonatomic) BOOL lineBreak; - (id)initWithText:(NSString*)text node:(TTStyledTextNode*)node; diff --git a/src/Three20/TTStyledTextNode.h b/src/Three20/TTStyledTextNode.h index 4737cb41e6..67e6f0b9cd 100644 --- a/src/Three20/TTStyledTextNode.h +++ b/src/Three20/TTStyledTextNode.h @@ -26,6 +26,11 @@ /////////////////////////////////////////////////////////////////////////////////////////////////// +@interface TTStyledItalicNode : TTStyledTextNode +@end + +/////////////////////////////////////////////////////////////////////////////////////////////////// + @interface TTStyledLinkNode : TTStyledTextNode { NSString* _url; BOOL _highlighted; @@ -35,3 +40,15 @@ @property(nonatomic,retain) NSString* url; @end + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface TTStyledImageNode : TTStyledTextNode { + NSString* _url; + UIImage* _image; +} + +@property(nonatomic,retain) NSString* url; +@property(nonatomic,retain) UIImage* image; + +@end diff --git a/src/Three20/TTStyledTextParser.h b/src/Three20/TTStyledTextParser.h new file mode 100644 index 0000000000..5290df2577 --- /dev/null +++ b/src/Three20/TTStyledTextParser.h @@ -0,0 +1,19 @@ +#import "Three20/TTGlobal.h" + +@class TTStyledTextNode; + +@interface TTStyledTextParser : NSObject { + TTStyledTextNode* _rootNode; + TTStyledTextNode* _lastNode; + TTStyledTextNode* _openNode; + NSError* _parserError; + NSMutableString* _chars; +} + +@property(nonatomic, retain) TTStyledTextNode* rootNode; + +- (void)parseXHTML:(NSString*)html; + +- (void)parseURLs:(NSString*)string; + +@end