#import #import "Controller.h" #import "TickerWindow.h" #import "ColorAsRGBADictionary.h" #import "RSSHandler.h" #import "StringAdditions.h" #import "TempFilename.h" #import "ArrayAdditions.h" #import "HeadlineView.h" #import "DictionaryAdditions.h" #import "TextWindow.h" #import "FeedWindow.h" @implementation TickerWindow - (void)dealloc { if(loadInProgress==YES){ [urlHandle removeClient:self]; } [attr release]; TEST_RELEASE(url); TEST_RELEASE(urlHandle); TEST_RELEASE(feedData); TEST_RELEASE(articles); TEST_RELEASE(cache); [super dealloc]; } static int _count = 0; #define MAX_OFFSET 10 #define WINDOW_SHIFT 40 #define WINDOW_BASE_X 200 #define WINDOW_BASE_Y 400 - initWithAttributes:(NSDictionary *)attributes { attr = [[NSMutableDictionary alloc] initWithDictionary:attributes]; NSFont *font = [NSFont fontWithName:[attr objectForKey:ATTR_FONTNAME] size:[attr floatForKey:ATTR_FONTSIZE]]; NSRect frame, content; NSString *contentstr = [attr objectForKey:ATTR_CONTENT]; unsigned int tickerstyle = (NSResizableWindowMask | NSClosableWindowMask | NSTitledWindowMask); if(contentstr!=nil){ content = NSRectFromString(contentstr); frame = [NSWindow frameRectForContentRect:content styleMask:tickerstyle]; } else{ content.origin.x = 0; content.origin.y = 0; content.size.width = TICKER_MINWIDTH; content.size.height = SCALE_FOR_HEIGHT*[font boundingRectForFont].size.height; frame = [NSWindow frameRectForContentRect:content styleMask:tickerstyle]; frame.origin.x = WINDOW_BASE_X + (_count % MAX_OFFSET)*WINDOW_SHIFT; frame.origin.y = WINDOW_BASE_Y + ((_count / MAX_OFFSET) % MAX_OFFSET)*WINDOW_SHIFT; _count++; } [super initWithContentRect:frame styleMask:tickerstyle backing:NSBackingStoreRetained defer:NO]; refreshsecs = (long)60*(long)[[attr objectForKey:ATTR_REFRESH] intValue]; ticksecs = (long)[[attr objectForKey:ATTR_TICK] intValue]; [self setMinSize:NSMakeSize(TICKER_MINWIDTH, frame.size.height)]; [self setMaxSize:NSMakeSize(TICKER_MAXWIDTH, frame.size.height)]; // [self setContentMinSize:NSMakeSize(TICKER_MINWIDTH, frame.size.height)]; // [self setContentMaxSize:NSMakeSize(TICKER_MAXWIDTH, frame.size.height)]; text = [[HeadlineView alloc] initWithFrame:content]; AUTORELEASE(text); [text setEditable:NO]; [text setSelectable:NO]; [text setTextColor: [NSColor colorFromRGBADictionary:[attr objectForKey:ATTR_FG]]]; [text setBackgroundColor: [NSColor colorFromRGBADictionary:[attr objectForKey:ATTR_BG]]]; [text setFont:font]; [[text textContainer] setWidthTracksTextView:YES]; [text setString:_(@"(loading)")]; [self setContentView:text]; [[NSFontManager sharedFontManager] setSelectedFont:font isMultiple:NO]; [self setReleasedWhenClosed:YES]; articles = nil; cache = nil; externalChoice = -1; feedData = nil; url = nil; urlHandle = nil; loadInProgress = NO; signalChange = NO; [self refresh]; [self display]; return self; } - (NSDictionary *)attributes { // NSRect frame = [self frame]; // [attr setObject:NSStringFromRect(frame) forKey:ATTR_FRAME]; NSRect frame = [self frame], content = [NSWindow contentRectForFrameRect:frame styleMask:[self styleMask]]; [attr setObject:NSStringFromRect(content) forKey:ATTR_CONTENT]; return attr; } - swapColors { NSColor *a = [text textColor], *b = [text backgroundColor]; [text setTextColor:b]; [text setBackgroundColor:a]; return self; } - (NSArray *)prepareCommand:(NSString *)what error:(NSString **)errPtr { NSString *cmdstr = [attr objectForKey:what]; if(cmdstr==nil){ *errPtr = [NSString stringWithFormat:_(@"(no command for %@)"), what]; return nil; } cmdstr = [cmdstr stringByTrimmingWhitespaceAndNewlines]; if(![cmdstr length]){ *errPtr = [NSString stringWithFormat:_(@"(command for %@ empty: <%@>)"), what, [attr objectForKey:what]]; return nil; } NSArray *allargs = [cmdstr stringToComponents]; if(![allargs count]){ *errPtr = [NSString stringWithFormat: _(@"(no components in command for %@)"), what]; return nil; } return allargs; } - (NSString *)untag:(NSString *)source title:(NSString *)aTitle { if(cache!=nil){ NSString *cached = [cache objectForKey:source]; if(cached!=nil){ // NSLog(@"cache hit %@", [self title]); return cached; } } NSString *error; NSArray *allargs = [self prepareCommand:ATTR_UNTAG error:&error]; if(allargs==nil){ return error; } NSTask *cmdTask = [[NSTask alloc] init]; // AUTORELEASE(cmdTask); (released after NSTaskDidTerminate notification) [cmdTask setLaunchPath:[allargs objectAtIndex:0]]; NSMutableArray *args = [NSMutableArray arrayWithArray: [allargs subarrayWithRange:NSMakeRange(1, [allargs count]-1)]]; NSString *htmlfile = [[NSFileManager defaultManager] tempFilenameExt:@"html"]; NSString *htmlstr = [NSString stringWithFormat:@"%@" @"\n%@\n\n", aTitle, source]; if([htmlstr writeToFile:htmlfile atomically:NO]==NO){ return [NSString stringWithFormat:_(@"(couldn't write file %@)"), htmlfile]; } [args addObject:htmlfile]; [cmdTask setArguments:args]; NSMutableDictionary *edict = [NSMutableDictionary dictionaryWithDictionary: [[NSProcessInfo processInfo] environment]]; [edict setObject:htmlfile forKey:TICKER_TEMP_FILE]; [edict setObject:[cmdTask description] forKey:TICKER_TASK_OBJECT]; [cmdTask setEnvironment:edict]; NSPipe *pipe = [NSPipe pipe]; NSFileHandle *reader = [pipe fileHandleForReading]; [cmdTask setStandardOutput:pipe]; NSMutableData *data = [NSMutableData dataWithCapacity:1024]; NS_DURING [cmdTask launch]; while([cmdTask isRunning]==YES){ [data appendData:[reader availableData]]; } [data appendData:[reader readDataToEndOfFile]]; NS_HANDLER NSString *emsg = [NSString stringWithFormat:_(@"(%@ exception: %@, %@)"), ATTR_UNTAG, [localException name], [localException reason]]; NS_VALUERETURN(emsg, id); NS_ENDHANDLER NSString *result = [[NSString alloc] initWithBytes:[data bytes] length:[data length] encoding:NSASCIIStringEncoding]; AUTORELEASE(result); if(cache!=nil && result!=nil && source!=nil){ // NSLog(@"cache miss %@", [self title]); [cache setObject:result forKey:source]; } return result; } - browse:(GenericArticle *)art text:(BOOL)textFlag { NSString *alert = [NSString stringWithString: (textFlag==YES ? _(@"Browse article text") : _(@"Follow article link"))]; NSString *error; NSArray *allargs = [self prepareCommand:ATTR_BROWSE error:&error]; if(allargs==nil){ NSRunAlertPanel(alert, error, _(@"Ok"), nil, nil); return self; } NSTask *cmdTask = [[NSTask alloc] init]; // AUTORELEASE(cmdTask); (released after NSTaskDidTerminate notification) [cmdTask setLaunchPath:[allargs objectAtIndex:0]]; NSMutableArray *args = [NSMutableArray arrayWithArray: [allargs subarrayWithRange:NSMakeRange(1, [allargs count]-1)]]; NSMutableDictionary *edict = [NSMutableDictionary dictionaryWithDictionary: [[NSProcessInfo processInfo] environment]]; NSString *lastarg; if(textFlag==YES){ NSString *htmlfile = [[NSFileManager defaultManager] tempFilenameExt:@"html"]; NSString *htmlstr = [NSString stringWithFormat:@"%@" @"\n%@\n\n", [art title], [art desc]]; if([htmlstr writeToFile:htmlfile atomically:NO]==NO){ NSRunAlertPanel(alert, _(@"(couldn't write file %@)"), _(@"Ok"), nil, nil, htmlfile); return self; } lastarg = htmlfile; [edict setObject:htmlfile forKey:TICKER_TEMP_FILE]; } else{ lastarg = [art link]; } [args addObject:lastarg]; [cmdTask setArguments:args]; [edict setObject:[cmdTask description] forKey:TICKER_TASK_OBJECT]; [cmdTask setEnvironment:edict]; NS_DURING [cmdTask launch]; NS_HANDLER NSRunAlertPanel(alert, _(@"(%@ exception: %@, %@)"), _(@"Ok"), nil, nil, ATTR_BROWSE, [localException name], [localException reason]); NS_ENDHANDLER return self; } - setErrorFormat:(NSString *)fmt arg:(NSString *)arg { [text setString: [NSString stringWithFormat:fmt, arg]]; return self; } - (NSString *)loadtitle { NSString *name = [attr objectForKey:ATTR_NAME]; if(name==nil){ name = [attr objectForKey:ATTR_URL]; } return [NSString stringWithFormat:_(@"Loading %@: %@"), name, [self description]]; } - (BOOL)loadInProgress { return loadInProgress; } - refresh { if(loadInProgress==YES){ return self; } [self setTitle:[self loadtitle]]; NSString *urlstr = [attr objectForKey:ATTR_URL]; url = [[NSURL alloc] initWithString:urlstr]; if(url==nil || [url scheme]==nil || [url host]==nil || [url path]==nil){ [self setErrorFormat:_(@"(error: the url \"%@\" is not valid)") arg:urlstr]; return self; } if(![[NSHost hostWithName:[url host]] names]){ [self setErrorFormat:_(@"(error: host \"%@\" unknown)") arg:[url host]]; return self; } loadInProgress = YES; urlHandle = [[[NSURLHandle URLHandleClassForURL:url] alloc] initWithURL:url cached:NO]; NSString *prxflag = [attr objectForKey:ATTR_PRXUSE]; if(prxflag!=nil && [prxflag isEqualToString:@"On"]==YES){ if([urlHandle writeProperty: [attr objectForKey:ATTR_PRXHOST] forKey:GSHTTPPropertyProxyHostKey]==NO){ NSLog(@"couldn't write host property %@ for %@", [attr objectForKey:ATTR_PRXHOST], [attr objectForKey:ATTR_URL]); } if([urlHandle writeProperty: [attr objectForKey:ATTR_PRXPORT] forKey:GSHTTPPropertyProxyPortKey]==NO){ NSLog(@"couldn't write port property %@ for %@", [attr objectForKey:ATTR_PRXPORT], [attr objectForKey:ATTR_URL]); } } [urlHandle addClient:self]; [urlHandle loadInBackground]; return self; } - (void)URLHandleResourceDidBeginLoading:(NSURLHandle *)sender { feedData = [[NSMutableData alloc] initWithCapacity:URL_RESOURCE_CAPACITY]; } - (void)URLHandle:(NSURLHandle *)sender resourceDataDidBecomeAvailable:(NSData *)newBytes { [feedData appendData:newBytes]; } - (void)URLHandle:(NSURLHandle *)sender resourceDidFailLoadingWithReason:(NSString *)reason { [self setErrorFormat:_(@"(load failed: %@)") arg:reason]; loadInProgress = NO; TEST_AUTORELEASE(feedData); feedData = nil; AUTORELEASE(url); url = nil; AUTORELEASE(urlHandle); urlHandle = nil; } - (void)URLHandleResourceDidCancelLoading:(NSURLHandle *)sender { [text setString:_(@"(load canceled)")]; loadInProgress = NO; TEST_AUTORELEASE(feedData); feedData = nil; AUTORELEASE(url); url = nil; AUTORELEASE(urlHandle); urlHandle = nil; } - (void)URLHandleResourceDidFinishLoading:(NSURLHandle *)sender { RSSHandler *handler = [RSSHandler handler]; GSXMLParser *parser = [GSXMLParser parserWithSAXHandler:handler withData:feedData]; loadInProgress = NO; TEST_AUTORELEASE(feedData); feedData = nil; AUTORELEASE(url); url = nil; AUTORELEASE(urlHandle); urlHandle = nil; [parser keepBlanks:NO]; [parser substituteEntities:NO]; if([parser parse]==NO){ NSMutableArray *msg = [handler messages]; NSString *msgstr; if(![msg count]){ msgstr = _(@"unknown error"); } else{ msgstr = [msg componentsJoinedByString:@" "]; } [self setErrorFormat:_(@"(parse error: %@)") arg:msgstr]; return; } [Controller debugAllocation]; NSMutableArray *newArticles = [NSMutableArray arrayWithCapacity: (articles==nil ? TICKER_DEFAULT_CAP : [articles count])]; RSSNode *root = [handler root], *rssOrAtom = nil; if((rssOrAtom=[root childNamed:@"rss"])!=nil || (rssOrAtom=[root childNamed:@"RDF"])!=nil || (rssOrAtom=[root childNamed:@"rdf"])){ BOOL isRSS = [[rssOrAtom name] isEqualToString:@"rss"]; RSSNode *channel; if((channel=[rssOrAtom childNamed:@"channel"])==nil){ [self setErrorFormat:_(@"(channel expected)") arg:nil]; return; } NSString *newTitle = [self description]; RSSNode *title, *copyright, *rights; if((title=[channel childNamed:@"title"])!=nil){ newTitle = [title value]; if((copyright=[channel childNamed:@"copyright"])!=nil){ newTitle = [NSString stringWithFormat:@"%@ %@", newTitle, [copyright value]]; } if((rights=[channel childNamed:@"rights"])!=nil){ newTitle = [NSString stringWithFormat:@"%@ %@", newTitle, [rights value]]; } [self setTitle:[newTitle stringByUnescapingHTML]]; } NSString *globalAuthor = nil; RSSNode *author; if((author=[channel childNamed:CHAN_FIELD_ATHRMANAG])!=nil){ globalAuthor = [author value]; } else if((author=[channel childNamed:CHAN_FIELD_ATHRWEBMSTR])!=nil){ globalAuthor = [author value]; } RSSNode *item; NSEnumerator *en = [[(isRSS==YES ? channel : rssOrAtom) children] objectEnumerator]; while((item=[en nextObject])!=nil){ if([[item name] isEqualToString:@"item"]==YES){ RSSArticle *art = [RSSArticle newWithNode:item inWindow:self]; if([art author]==nil && globalAuthor!=nil){ [art setAuthor:globalAuthor]; } [newArticles addObject:art]; // GSPrintf(stdout, @"%@", [item description]); } } } else if((rssOrAtom=[root childNamed:@"feed"])!=nil){ NSString *newTitle = [self description]; RSSNode *title, *copyright, *rights; if((title=[rssOrAtom childNamed:@"title"])!=nil){ newTitle = [title value]; if((copyright=[rssOrAtom childNamed:@"copyright"])!=nil){ newTitle = [NSString stringWithFormat:@"%@ %@", newTitle, [copyright value]]; } if((rights=[rssOrAtom childNamed:@"rights"])!=nil){ newTitle = [NSString stringWithFormat:@"%@ %@", newTitle, [rights value]]; } [self setTitle:[newTitle stringByUnescapingHTML]]; } NSString *globalAuthor = nil; RSSNode *author = [rssOrAtom childNamed:@"author"]; if(author!=nil){ globalAuthor = [GenericArticle authorFromAtomAuthor:author]; } RSSNode *entry; NSEnumerator *en = [[rssOrAtom children] objectEnumerator]; while((entry=[en nextObject])!=nil){ if([[entry name] isEqualToString:@"entry"]==YES){ AtomArticle *art = [AtomArticle newWithNode:entry inWindow:self]; if([art author]==nil && globalAuthor!=nil){ [art setAuthor:globalAuthor]; } [newArticles addObject:art]; // GSPrintf(stdout, @"%@", [entry description]); } } } else{ RSSNode *what = root; NSArray *children = [root children]; if(children!=nil && [children count]>0){ what = [children objectAtIndex:0]; } NSMutableString *desc = [NSMutableString stringWithString:[what description]]; [desc replaceOccurrencesOfString:@"\n" withString:@" " options:NSLiteralSearch range:NSMakeRange(0, [desc length])]; #define PREFIX_LENGTH 64 int len = [desc length]; int prefix = (len>=PREFIX_LENGTH ? PREFIX_LENGTH : len); [self setErrorFormat:_(@"(rss or atom feed expected, got \"%@\")") arg:[desc substringToIndex:prefix]]; return; } GenericArticle *genart; NSDate *now = [NSDate date]; NSEnumerator *en = [newArticles objectEnumerator]; int apos = 0; while((genart=[en nextObject])!=nil){ if([genart date]==nil){ NSDate *artDate = [now addTimeInterval: (NSTimeInterval)-ART_NODATE_SECS*apos]; [genart setDate:artDate]; } apos++; } [newArticles sortUsingSelector:@selector(compareByDate:)]; // GSPrintf(stdout, @"%@\n", [newArticles mapSelector:@selector(date)]); BOOL swapped = signalChange; if(articles==nil){ signalChange = NO; } else{ NSArray *guid1 = [[articles mapSelector:@selector(uniqueid)] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)], *guid2 = [[newArticles mapSelector:@selector(uniqueid)] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; signalChange = ([guid1 isEqualToArray:guid2]==YES ? NO : YES); } ASSIGN(articles, newArticles); [[NSNotificationCenter defaultCenter] postNotification: [NSNotification notificationWithName:TickerDidUpdateNotification object:self]]; int artcount = [articles count]; if(cache==nil && artcount>0){ cache = [[Cache alloc] initWithCapacity:artcount]; } if(signalChange==YES){ NSBeep(); } if(swapped!=signalChange){ [self swapColors]; } if(artcount>0){ current = 0; [text setString: [[articles objectAtIndex:current++] title]]; } else{ [text setString:_(@"(no articles found)")]; } [text display]; return; } - tick { if(signalChange==YES){ signalChange = NO; [self swapColors]; } int acount = [articles count]; if(acount){ if(current==acount){ current = 0; } [text setString: [[articles objectAtIndex:current++] title]]; } [text display]; return self; } - (unsigned long)refreshsecs { return refreshsecs; } - (unsigned long)ticksecs { return ticksecs; } - (GenericArticle *)currentArticle { int what = current-1; if(externalChoice!=-1){ what = externalChoice; externalChoice = -1; } if(articles==nil || what>=[articles count]){ return nil; } return [articles objectAtIndex:what]; } - (int)currentArticleIndex { int what = current-1; if(externalChoice!=-1){ what = externalChoice; externalChoice = -1; } return what; } - (NSArray *)articles { return articles; } - setExternalChoice:(int)choice { externalChoice = choice; return self; } - articleText:(id)sender { GenericArticle *art = [self currentArticle]; if(art==nil){ NSBeep(); return self; } TextWindow *win = [[TextWindow alloc] initWithTickerWindow:self article:art number:[articles indexOfObject:art] attributes:attr]; [win orderFrontRegardless]; [win makeMainWindow]; return self; } - articleBrowse:(id)sender { GenericArticle *art = [self currentArticle]; if(art==nil){ NSBeep(); return self; } [self browse:art text:YES]; return self; } - articleLink:(id)sender { GenericArticle *art = [self currentArticle]; if(art==nil || [art link]==nil){ NSBeep(); return self; } [self browse:art text:NO]; return self; } - articleShowAll:(id)sender { if(articles==nil || ![articles count]){ NSBeep(); return self; } FeedWindow *feed = [[FeedWindow alloc] initWithTickerWindow:self]; [feed orderFrontRegardless]; [feed makeMainWindow]; return self; } - articleUpdate:(id)sender { if(loadInProgress==YES){ NSBeep(); return self; } [self refresh]; return self; } - (BOOL)validateMenuItem:(id )menuItem { GenericArticle *art = [self currentArticle]; int tag = [menuItem tag]; if(art==nil || (tag==MENU_LINK && [art link]==nil)){ return NO; } return YES; } @end