/* Project: RSSReader Copyright (C) 2006 Yen-Ju Chen Copyright (C) 2005, 2006 Guenther Noack Author: Yen-Ju Chen Author: Guenther Noack,,, Created: 2005-03-25 19:42:31 +0100 by guenther Application Controller This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This application is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111 USA. */ #import "Controller.h" #import "MainWindow.h" #import "FeedList.h" #import "RSSReaderFeed.h" #import "SubscriptionPanelController.h" #import "FetchingProgressManager.h" #import "RSSReaderService.h" #import "Global.h" #import "GNUstep.h" #import "ContentTextView.h" #import "PreferencePane.h" #import "FontPreferencePane.h" static Controller *sharedInstance; @interface Controller (Private) /* Listen from notification */ - (void) feedFetchFailed: (NSNotification *) not; - (void) fontChanged: (NSNotification *) not; - (void) feedFetched: (NSNotification *) not; /* Change fonts on feed list, article list and article content * based on user defatuls or system font. */ - (void) changeFont; @end @implementation Controller + (Controller *) mainController { if (sharedInstance == nil) { sharedInstance = [[Controller alloc] init]; } return sharedInstance; } - (id) init { self = [super init]; return self; } - (void) dealloc { DESTROY(feedList); [super dealloc]; } - (void)awakeFromNib { NSLog(@"awakeFromNib"); } - (void)applicationWillFinishLaunching: (NSNotification *) not { /* Create pane as early as possible for logging */ paneController = [[PaneController alloc] initWithRegistry: AUTORELEASE([[PKPaneRegistry alloc] init]) presentationMode: PKMatrixPresentationMode owner: nil]; ASSIGN(feedList, [FeedList feedList]); /* Make sure defaults are properly set */ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; if ([defaults objectForKey: RSSReaderWebBrowserDefaults] == nil) { [defaults setObject: @"/usr/bin/firefox" forKey: RSSReaderWebBrowserDefaults]; } if ([defaults objectForKey: RSSReaderUseSystemFontDefaults] == nil) { [defaults setBool: YES forKey: RSSReaderUseSystemFontDefaults]; } if ([defaults objectForKey: RSSReaderUseSystemSizeDefaults] == nil) { [defaults setBool: YES forKey: RSSReaderUseSystemSizeDefaults]; } else { /* This builds fonts based on user defatuls */ [self fontChanged: nil]; } int number = [defaults integerForKey: RSSReaderRemoveArticlesAfterDefaults]; if (number == 0) { /* Not set, use Never as default */ [defaults setInteger: -1 forKey: RSSReaderRemoveArticlesAfterDefaults]; } if ([defaults objectForKey: RSSReaderAutomaticRefreshIntervalDefaults] == nil) { /* Not set. use Never as default */ [defaults setFloat: -1 forKey: RSSReaderAutomaticRefreshIntervalDefaults]; } } - (void)applicationDidFinishLaunching:(NSNotification *)aNotif { /* We initialize FetchingManager here so that it can start * fetching automatically based on user defaults */ [FetchingProgressManager defaultManager]; /* Toolbar */ NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier: RSSReaderToolbarIdentifier]; [toolbar setDelegate: mainWindow]; [mainWindow setToolbar: toolbar]; RELEASE(toolbar); /* Progress Bar */ [progressBar setHidden: YES]; [progressBar setIndeterminate: NO]; [mainWindow setFrameAutosaveName: RSSReaderMainWindowFrameName]; searchField = [mainWindow searchField]; [mainWindow makeFirstResponder: [feedBookmarkView outlineView]]; /* Start build article view */ [articleCollectionView beginEditing]; [feedBookmarkView setDisplayProperties: [NSArray arrayWithObject: kBKBookmarkTitleProperty]]; [[feedBookmarkView outlineView] setDelegate: self]; [[feedBookmarkView outlineView] setNextKeyView: [articleCollectionView tableView]]; [feedBookmarkView setBookmarkStore: [feedList feedStore]]; [[articleCollectionView tableView] setDelegate: self]; [[articleCollectionView tableView] setNextKeyView: [feedBookmarkView outlineView]]; [articleCollectionView setDisplayProperties: [NSArray arrayWithObject: kArticleHeadlineProperty]]; [articleCollectionView setSortingProperty: kArticleDateProperty reverse: YES]; [articleCollectionView setCollection: [feedList articleCollection]]; /* finish build article view */ [articleCollectionView endEditing]; /* Register service... */ [NSApp setServicesProvider: [[RSSReaderService alloc] init]]; [feedBookmarkView reloadData]; [articleCollectionView reloadData]; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; /* Resize bookmark view from saved size */ NSString *s = [defaults stringForKey: RSSReaderBookmarkViewFrameDefaults]; if (s) { NSRect rect = NSRectFromString(s); [feedBookmarkView setFrameSize: rect.size]; NSView *view = [[[feedBookmarkView superview] subviews] objectAtIndex: 1]; rect.size.width = [[feedBookmarkView superview] bounds].size.width - rect.size.width; [view setFrameSize: rect.size]; } /* Listen to RSSFeedFetchedNotification for loading in backgroun */ [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(feedFetched:) name: RSSFeedFetchedNotification object: nil]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(feedFetchFailed:) name: RSSFeedFetchFailedNotification object: nil]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(fontChanged:) name: RSSReaderFontChangeNotification object: nil]; [mainWindow makeKeyAndOrderFront: self]; /* Start fetch if user set the defaults */ if ([defaults boolForKey: RSSReaderFetchAtStartupDefaults]) { [self reloadAll: self]; } } /* Try to handle the case when user click on the dock. * On Cocoa, it calls -applicationShouldHandleReopen:hasVisibleWindows:. * GNUstep misses the implementation. So we compensate with * -applicationDidBecomeActive:, which only works when application * is not on focus before dock is clicked. */ #ifdef GNUSTEP - (void) applicationDidBecomeActive: (NSNotification *) not { if ([mainWindow isVisible] == NO) { [mainWindow makeKeyAndOrderFront: self]; } } #else - (BOOL) applicationShouldHandleReopen: (NSApplication *) app hasVisibleWindows: (BOOL) flag { if ([mainWindow isVisible] == NO) { [mainWindow makeKeyAndOrderFront: self]; } return YES; } #endif - (BOOL)applicationShouldTerminate:(id)sender { [feedList save]; return YES; } - (void)applicationWillTerminate:(NSNotification *)aNotif { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; /* Save bookmar view size */ NSRect rect = [feedBookmarkView frame]; [defaults setObject: NSStringFromRect(rect) forKey: RSSReaderBookmarkViewFrameDefaults]; [defaults synchronize]; } - (BOOL)application:(NSApplication *)application openFile:(NSString *)fileName { [feedList addFeedWithURL: [NSURL fileURLWithPath: fileName]]; return YES; } - (void) showPreferences: (id) sender { if (preferencesController == nil) { /* Initialize here */ /* Automatically register into PKPreferencePaneRegistry */ AUTORELEASE([[PreferencePane alloc] init]); AUTORELEASE([[FontPreferencePane alloc] init]); ASSIGN(preferencesController, [PKPreferencesController sharedPreferencesController]); [preferencesController setPresentationMode: PKMatrixPresentationMode]; } [(NSWindow *)[preferencesController owner] setTitle: @"Preferences"]; // [(NSWindow *)[preferencesController owner] makeKeyAndOrderFront: self]; [NSApp runModalForWindow: (NSPanel *)[preferencesController owner]]; } - (void) subscribe: (id) sender { SubscriptionPanelController *controller = [SubscriptionPanelController subscriptionPanelController]; /* See whether there is anything in pasteboard */ NSPasteboard *pboard = [NSPasteboard generalPasteboard]; NSString *type = [pboard availableTypeFromArray: [NSArray arrayWithObjects: NSURLPboardType, NSStringPboardType, nil]]; if (type) { NSString *s = [pboard stringForType: type]; if ([s hasPrefix: @"http"] || [s hasPrefix: @"https"] || [s hasPrefix: @"rss"]) { [controller setURL: [NSURL URLWithString: s]]; } } else { [controller setURL: nil]; } int result = [controller runPanelInModal]; if (result == NSOKButton) { NSURL *url = [controller url]; // create the feed RSSFeed *feed = AUTORELEASE([[RSSReaderFeed alloc] initWithURL: url]); [feed setAutoClear: YES]; // actually add the feed [feedList addFeed: feed]; [feedBookmarkView reloadData]; [articleCollectionView reloadData]; [controller setURL: nil]; } } /** Private **/ - (void) removeGroupRecursive: (BKGroup *) g { NSEnumerator *e = nil; BKBookmark *bk = nil; BKGroup *subg = nil; e = [[g subgroups] objectEnumerator]; while ((subg = [e nextObject])) { [self removeGroupRecursive: subg]; } e = [[g items] objectEnumerator]; while((bk = [e nextObject])) { RSSFeed *feed = [feedList feedForURL: [bk URL]]; [feedList removeFeed: feed]; [[feedList feedStore] removeBookmark: bk]; } if ([[feedList feedStore] removeRecord: g] == NO) { NSLog(@"Fail to remove %@(%@)", g, [g valueForProperty: kBKGroupNameProperty]); } } /** End of Private **/ - (void) delete: (id) sender { int index = [[feedBookmarkView outlineView] selectedRow]; if (index > -1) { id item = [[feedBookmarkView outlineView] itemAtRow: index]; if ([item isKindOfClass: [BKBookmark class]]) { BKBookmark *bk = (BKBookmark *) item; RSSFeed *feed = [feedList feedForURL: [bk URL]]; [feedList removeFeed: feed]; } else if ([item isKindOfClass: [BKGroup class]]) { int result = NSRunAlertPanel(_(@"Remove feeds"), _(@"All needs under this group will be removed. Are you sure ?"), _(@"Cancel"), _(@"Remove"), nil, nil); if (result == NSAlertAlternateReturn) { [self removeGroupRecursive: (BKGroup *) item]; } } [feedBookmarkView reloadData]; [[feedBookmarkView outlineView] deselectAll: self]; [articleCollectionView reloadData]; } } - (void) reload: (id) sender { int index = [[feedBookmarkView outlineView] selectedRow]; if (index > -1) { /* Setup progress bar */ [progressBar setHidden: NO]; [progressBar setMinValue: 0]; [progressBar setMaxValue: 1]; [progressBar setDoubleValue: 0.5]; [progressBar startAnimation: self]; NSMutableArray *allItems = nil; RSSFeed *feed; BKBookmark *bk; id object = [[feedBookmarkView outlineView] itemAtRow: index]; if ([object isKindOfClass: [BKBookmark class]]) { bk = (BKBookmark *) object; feed = [feedList feedForURL: [bk URL]]; allItems = [NSArray arrayWithObject: feed]; } else if ([object isKindOfClass: [BKGroup class]]) { BKGroup *group = (BKGroup *) object; NSEnumerator *e = [[[feedList feedStore] itemsUnderGroup: group] objectEnumerator]; allItems = AUTORELEASE([[NSMutableArray alloc] init]); while ((bk = [e nextObject])) { feed = [feedList feedForURL: [bk URL]]; [allItems addObject: feed]; } } else { NSLog(@"Internal Error: unknown object in FeedBookmarkView"); return; } if (allItems) { [[FetchingProgressManager defaultManager] fetchFeeds: allItems]; } } } - (void) reloadAll: (id) sender { [progressBar setHidden: NO]; [progressBar setMinValue: 0]; [progressBar setMaxValue: [[feedList feeds] count]]; [progressBar setDoubleValue: 0]; [progressBar startAnimation: self]; [[FetchingProgressManager defaultManager] fetchFeeds: [feedList feeds]]; } - (void) addGroup: (id) sender { BKGroup *group = [[BKGroup alloc] init]; [group setValue: @"New Group" forProperty: kBKGroupNameProperty]; [[feedList feedStore] addRecord: group]; DESTROY(group); [feedBookmarkView reloadData]; } - (void) markAllRead: (id) sender { [articleCollectionView beginEditing]; int i; for (i = 0; i < [articleCollectionView numberOfItems]; i++) { id item = [articleCollectionView itemAtIndex: i]; if ([item isKindOfClass: [CKItem class]]) { [(CKItem *)item setValue: [NSNumber numberWithInt: 1] forProperty: kArticleReadProperty]; } } [articleCollectionView endEditing]; /* This change unread number. Therefore, reload bookmark view */ [feedBookmarkView reloadData]; [articleCollectionView reloadData]; } - (void) markAllUnread: (id) sender { [articleCollectionView beginEditing]; int i; for (i = 0; i < [articleCollectionView numberOfItems]; i++) { id item = [articleCollectionView itemAtIndex: i]; if ([item isKindOfClass: [CKItem class]]) { [(CKItem *)item setValue: [NSNumber numberWithInt: 0] forProperty: kArticleReadProperty]; } } [articleCollectionView endEditing]; [feedBookmarkView reloadData]; [articleCollectionView reloadData]; } - (void) markRead: (id) sender { int index = [[articleCollectionView tableView] selectedRow]; if (index > -1) { id item = [articleCollectionView itemAtIndex: index]; if ([item isKindOfClass: [CKItem class]]) { [(CKItem *)item setValue: [NSNumber numberWithInt: 1] forProperty: kArticleReadProperty]; [feedBookmarkView reloadData]; [articleCollectionView reloadData]; } } } - (void) markUnread: (id) sender { int index = [[articleCollectionView tableView] selectedRow]; if (index > -1) { id item = [articleCollectionView itemAtIndex: index]; if ([item isKindOfClass: [CKItem class]]) { [(CKItem *)item setValue: [NSNumber numberWithInt: 0] forProperty: kArticleReadProperty]; [feedBookmarkView reloadData]; [articleCollectionView reloadData]; } } } - (void) showMainWindow: (id) sender { [mainWindow makeKeyAndOrderFront: sender]; } - (void) showLog: (id) sender { [[paneController owner] makeKeyAndOrderFront: sender]; } - (void) search: (id) sender { NSString *value = [searchField stringValue]; if ((value == nil) || ([value length] == 0)) { /* Remove search element */ [articleCollectionView setSearchElement: nil]; } else { /* Currently, search on headline and description */ CKSearchElement *e1, *e2, *element; e1 = [CKItem searchElementForProperty: kArticleHeadlineProperty label: nil key: nil value: value comparison: CKContainsSubStringCaseInsensitive]; e2 = [CKItem searchElementForProperty: kArticleDescriptionProperty label: nil key: nil value: value comparison: CKContainsSubStringCaseInsensitive]; element = [CKSearchElement searchElementForConjunction: CKSearchOr children: [NSArray arrayWithObjects: e1, e2, nil]]; [articleCollectionView setSearchElement: element]; } [articleCollectionView reloadData]; } /** BKBookmarkView Delegate */ - (BOOL) outlineView: (NSOutlineView *) outlineView shouldSelectItem: (id) item { if ([item isKindOfClass: [BKBookmark class]]) { RSSFeed *feed = [feedList feedForURL: [(BKBookmark *)item URL]]; CKGroup *group = [feedList articleGroupForURL: [feed feedURL]]; [articleCollectionView setDisplayProperties: [NSArray arrayWithObject: kArticleHeadlineProperty]]; [articleCollectionView setRoot: group]; [articleCollectionView reloadData]; } else if ([item isKindOfClass: [BKGroup class]]) { NSEnumerator *e = [[[feedList feedStore] itemsUnderGroup: (BKGroup *)item] objectEnumerator]; BKBookmark *bk = nil; NSURL *url = nil; NSMutableArray *array = AUTORELEASE([[NSMutableArray alloc] init]); while ((bk = [e nextObject])) { CKGroup *group = [feedList articleGroupForURL: [bk URL]]; if (group) { [array addObject: group]; } else { NSLog(@"Weird. Has no group for feed, %@", bk); } } [articleCollectionView setDisplayProperties: [NSArray arrayWithObjects: kArticleHeadlineProperty, kArticleGroupProperty, nil]]; [articleCollectionView setRoot: array]; [articleCollectionView reloadData]; } /* When feed view is selected, unselect in article view. * Otherwise, the selection in article view will not match * the content of content view. * And when user select the same row in article view, * because of the row doesn't change, it will not trigger a refresh * of content view. */ [[articleCollectionView tableView] deselectAll: self]; return YES; } - (BOOL) outlineView: (NSOutlineView *) outlineView shouldEditTableColumn: (NSTableColumn *) tableColumn item: (id) item { if ([item isKindOfClass: [BKGroup class]]) { return YES; } return NO; } /** Article Table View Delegate */ // is executed when something is clicked -(BOOL) tableView: (NSTableView*) tableView shouldSelectRow: (int) rowIndex { id item = [articleCollectionView itemAtIndex: rowIndex]; if ([item isKindOfClass: [CKItem class]]) { [contentTextView setItem: item]; if ([[item valueForProperty: kArticleReadProperty] intValue] == 0) { [item setValue: [NSNumber numberWithInt: 1] forProperty: kArticleReadProperty]; [feedBookmarkView reloadData]; } return YES; } return NO; } // changes boldness and color - (void) tableView: (NSTableView*) tableView willDisplayCell: (id) cell forTableColumn: (NSTableColumn*) aTableColumn row: (int) rowIndex { id item = [articleCollectionView itemAtIndex: rowIndex]; if ([item isKindOfClass: [CKItem class]]) { if ([[item valueForProperty: kArticleReadProperty] intValue] == 0) { [cell setFont: articleListBoldFont]; } else { [cell setFont: articleListFont]; } } } - (void) enterKeyDownInTableView: (NSTableView *) tableView { int index = [tableView selectedRow]; if (index > -1) { id item = [articleCollectionView itemAtIndex: index]; if ([item isKindOfClass: [CKItem class]]) { NSString *urlString = [item valueForProperty: kArticleURLProperty]; if (urlString) { NSURL *url = [NSURL URLWithString: urlString]; [[NSWorkspace sharedWorkspace] openURL: url]; } } } } @end @implementation Controller (Private) - (void) updateProgressBar { /* Increase one and stop if full */ [progressBar incrementBy: 1]; if ([progressBar doubleValue] == [progressBar maxValue]) { [progressBar setHidden: YES]; [progressBar stopAnimation: self]; } } - (void) feedFetched: (NSNotification *) not { NSLog(@"feedFetched"); CREATE_AUTORELEASE_POOL(x); /* This update article collection for each feed. */ [articleCollectionView beginEditing]; [feedList updateFeed: [not object]]; [articleCollectionView endEditing]; [self updateProgressBar]; [feedBookmarkView reloadData]; [articleCollectionView reloadData]; DESTROY(x); NSLog(@"feedFetched done"); } - (void) feedFetchFailed: (NSNotification *) not { [self updateProgressBar]; } - (void) fontChanged: (NSNotification *) not { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; int feedListSize, articleListSize, articleContentSize; BOOL b = [defaults boolForKey: RSSReaderUseSystemSizeDefaults]; if (b == YES) { feedListSize = articleListSize = articleContentSize = [NSFont systemFontSize]; } else { feedListSize = [defaults integerForKey: RSSReaderFeedListSizeDefaults]; articleListSize = [defaults integerForKey: RSSReaderArticleListSizeDefaults]; articleContentSize = [defaults integerForKey: RSSReaderArticleContentSizeDefaults]; } b = [defaults boolForKey: RSSReaderUseSystemFontDefaults]; if (b == YES) { ASSIGN(feedListFont, [NSFont systemFontOfSize: feedListSize]); ASSIGN(articleListFont, [NSFont systemFontOfSize: articleListSize]); ASSIGN(articleContentFont, [NSFont systemFontOfSize: articleContentSize]); } else { ASSIGN(feedListFont, [NSFont fontWithName: [defaults stringForKey: RSSReaderFeedListFontDefaults] size: feedListSize]); ASSIGN(articleListFont, [NSFont fontWithName: [defaults stringForKey: RSSReaderArticleListFontDefaults] size: articleListSize]); ASSIGN(articleContentFont, [NSFont fontWithName: [defaults stringForKey: RSSReaderArticleContentFontDefaults] size: articleContentSize]); } [self changeFont]; } - (void) changeFont { /* Safe guard */ int defaultSize = [NSFont systemFontSize]; if (feedListFont == nil) { ASSIGN(feedListFont, [NSFont systemFontOfSize: defaultSize]); } if (articleListFont == nil) { ASSIGN(articleListFont, [NSFont systemFontOfSize: defaultSize]); } if (articleContentFont == nil) { ASSIGN(articleContentFont, [NSFont systemFontOfSize: defaultSize]); } NSFontManager *fm = [NSFontManager sharedFontManager]; ASSIGN(articleListBoldFont, [fm convertFont: articleListFont toHaveTrait: NSBoldFontMask]); /* Feed List */ NSEnumerator *e = [[[feedBookmarkView outlineView] tableColumns] objectEnumerator]; NSTableColumn *column; while ((column = [e nextObject])) { [[column dataCell] setFont: feedListFont]; } /* Article list */ e = [[[articleCollectionView tableView] tableColumns] objectEnumerator]; while ((column = [e nextObject])) { [[column dataCell] setFont: articleListFont]; } [contentTextView setBaseFont: articleContentFont]; #ifdef GNUSTEP /* GNUstep need a reload to use new font */ [feedBookmarkView reloadData]; [articleCollectionView reloadData]; #endif } @end