Files
baRSS/baRSS/Status Bar Menu/BarMenu.m

155 lines
5.2 KiB
Objective-C

#import "BarMenu.h"
#import "Constants.h"
#import "UserPrefs.h"
#import "NSMenu+Ext.h"
#import "BarStatusItem.h"
#import "MapUnreadTotal.h"
#import "StoreCoordinator.h"
#import "Feed+Ext.h"
#import "FeedGroup+Ext.h"
#import "FeedArticle+Ext.h"
@interface BarMenu()
@property (weak) BarStatusItem *statusItem;
@property (strong) MapUnreadTotal *unreadMap;
@end
@implementation BarMenu
- (instancetype)initWithStatusItem:(BarStatusItem*)statusItem {
self = [super init];
self.statusItem = statusItem;
// TODO: move unread counts to status item and keep in sync when changing feeds in preferences
self.unreadMap = [[MapUnreadTotal alloc] initWithCoreData: [StoreCoordinator countAggregatedUnread]];
// Register for notifications
RegisterNotification(kNotificationArticlesUpdated, @selector(articlesUpdated:), self);
RegisterNotification(kNotificationFeedIconUpdated, @selector(feedIconUpdated:), self);
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Generate Menu Items
/**
@note Delegate method not used. Here to prevent weird @c NSMenu behavior.
Otherwise, Cmd-Q (Quit) and Cmd-, (Preferences) will traverse all submenus.
Try yourself with @c NSLog() in @c numberOfItemsInMenu: and @c menuDidClose:
*/
- (BOOL)menuHasKeyEquivalent:(NSMenu *)menu forEvent:(NSEvent *)event target:(id _Nullable __autoreleasing *)target action:(SEL _Nullable *)action {
return NO;
}
/// Populate menu with items.
- (void)menuNeedsUpdate:(NSMenu*)menu {
if (menu.isFeedMenu) {
Feed *feed = [StoreCoordinator feedWithIndexPath:menu.titleIndexPath inContext:nil];
[self setArticles:[feed sortedArticles] forMenu:menu];
} else {
NSArray<FeedGroup*> *groups = [StoreCoordinator sortedFeedGroupsWithParent:menu.parentItem.representedObject inContext:nil];
if (groups.count == 0) {
[menu addItemWithTitle:NSLocalizedString(@"~~~ no entries ~~~", nil) action:nil keyEquivalent:@""].enabled = NO;
} else {
[self setFeedGroups:groups forMenu:menu];
}
}
}
/// Generate items for @c FeedGroup menu.
- (void)setFeedGroups:(NSArray<FeedGroup*>*)sortedList forMenu:(NSMenu*)menu {
[menu insertDefaultHeader];
for (FeedGroup *fg in sortedList) {
[menu insertFeedGroupItem:fg withUnread:self.unreadMap].submenu.delegate = self;
}
[menu setHeaderHasUnread:self.unreadMap[menu.titleIndexPath]];
}
/// Generate items for @c FeedArticles menu.
- (void)setArticles:(NSArray<FeedArticle*>*)sortedList forMenu:(NSMenu*)menu {
[menu insertDefaultHeader];
NSInteger mc = NSIntegerMax;
if (UserPrefsBool(Pref_feedLimitArticles))
mc = UserPrefsInt(Pref_articlesInMenuLimit);
BOOL onlyUnread = UserPrefsBool(Pref_feedUnreadOnly);
for (FeedArticle *fa in sortedList) {
if (onlyUnread && !fa.unread)
continue;
if (--mc < 0) // mc == 0 will first decrement to -1, then evaluate
break;
[menu addItem:[fa newMenuItem]];
}
[menu setHeaderHasUnread:self.unreadMap[menu.titleIndexPath]];
}
#pragma mark - Background Update / Rebuild Menu
/**
Fetch @c Feed from core data and find deepest visible @c NSMenuItem.
@warning @c item and @c feed will often mismatch.
*/
- (BOOL)findDeepest:(NSManagedObjectID*)oid feed:(Feed*__autoreleasing*)feed menuItem:(NSMenuItem*__autoreleasing*)item {
Feed *f = [[StoreCoordinator getMainContext] objectWithID:oid];
if (![f isKindOfClass:[Feed class]]) return NO;
NSMenuItem *mi = [self.statusItem.mainMenu deepestItemWithPath:f.indexPath];
if (!mi) return NO;
*feed = f;
*item = mi;
return YES;
}
/// Callback method fired when feed has been updated in the background.
- (void)articlesUpdated:(NSNotification*)notify {
Feed *feed;
NSMenuItem *item;
if ([self findDeepest:notify.object feed:&feed menuItem:&item]) {
// 1. update in-memory unread count
UnreadTotal *updated = [UnreadTotal new];
updated.total = feed.articles.count;
for (FeedArticle *fa in feed.articles) {
if (fa.unread) updated.unread += 1;
}
[self.unreadMap updateAllCounts:updated forPath:feed.indexPath];
// 2. rebuild articles menu if it is open
if (item.submenu.isFeedMenu) { // menu item is visible
item.title = feed.group.anyName; // will replace (no title)
item.image = [feed iconImage16];
item.enabled = (feed.articles.count > 0);
if (item.submenu.numberOfItems > 0) { // replace articles menu
[item.submenu removeAllItems];
[self setArticles:[feed sortedArticles] forMenu:item.submenu];
}
}
// 3. set unread count & enabled header for all parents
NSArray<UnreadTotal*> *itms = [self.unreadMap itemsForPath:item.submenu.titleIndexPath create:NO];
for (UnreadTotal *uct in itms.reverseObjectEnumerator) {
if (item) { // nil on last loop (aka main menu, see below)
[item.submenu setHeaderHasUnread:uct];
[item setTitleCount:uct.unread];
item = item.parentItem;
}
}
// call on main menu
[self.statusItem.mainMenu setHeaderHasUnread:itms.firstObject];
// TODO: need to re-create groups if user chose to hide already read articles
}
}
/// Callback method fired when feed icon has changed.
- (void)feedIconUpdated:(NSNotification*)notify {
Feed *feed;
NSMenuItem *item;
if ([self findDeepest:notify.object feed:&feed menuItem:&item]) {
if (item.submenu.isFeedMenu)
item.image = [feed iconImage16];
}
}
@end