Files
baRSS/baRSS/Status Bar Menu/BarMenu.m
2019-09-23 17:38:31 +02:00

175 lines
6.3 KiB
Objective-C

//
// The MIT License (MIT)
// Copyright (c) 2018 Oleg Geier
//
// 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.
#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];
}
}
}
/// Get rid of everything that is not needed.
- (void)menuDidClose:(NSMenu*)menu {
[menu cleanup];
}
/// Generate items for @c FeedGroup menu.
- (void)setFeedGroups:(NSArray<FeedGroup*>*)sortedList forMenu:(NSMenu*)menu {
[menu insertDefaultHeader];
for (FeedGroup *fg in sortedList) {
[menu insertFeedGroupItem:fg].submenu.delegate = self;
}
UnreadTotal *uct = self.unreadMap[menu.titleIndexPath];
[menu setHeaderHasUnread:(uct.unread > 0) hasRead:(uct.unread < uct.total)];
// set unread counts
for (NSMenuItem *item in menu.itemArray) {
if (item.hasSubmenu)
[item setTitleCount:self.unreadMap[item.submenu.titleIndexPath].unread];
}
}
/// Generate items for @c FeedArticles menu.
- (void)setArticles:(NSArray<FeedArticle*>*)sortedList forMenu:(NSMenu*)menu {
[menu insertDefaultHeader];
NSUInteger mc = 0;
if ([UserPrefs defaultNO:@"feedLimitArticles"]) {
mc = [UserPrefs articlesInMenuLimit];
}
for (FeedArticle *fa in sortedList) {
[menu addItem:[fa newMenuItem]];
if (--mc == 0) // if mc==0 then unsigned int will underflow and turn into INT_MAX
break;
}
UnreadTotal *uct = self.unreadMap[menu.titleIndexPath];
[menu setHeaderHasUnread:(uct.unread > 0) hasRead:(uct.unread < uct.total)];
}
#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.
*/
- (void)updateFeedMenuItem:(NSManagedObjectID*)oid withBlock:(void(^)(Feed *feed, NSMenuItem *item))block {
Feed *feed = [[StoreCoordinator getMainContext] objectWithID:oid];
if ([feed isKindOfClass:[Feed class]]) {
NSMenuItem *item = [self.statusItem.mainMenu deepestItemWithPath:feed.indexPath];
if (item) block(feed, item);
}
}
/// Callback method fired when feed has been updated in the background.
- (void)articlesUpdated:(NSNotification*)notify {
[self updateFeedMenuItem:notify.object withBlock:^(Feed *feed, NSMenuItem *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) {
[item.submenu setHeaderHasUnread:(uct.unread > 0) hasRead:(uct.unread < uct.total)];
[item setTitleCount:uct.unread];
item = item.parentItem;
}
}];
}
/// Callback method fired when feed icon has changed.
- (void)feedIconUpdated:(NSNotification*)notify {
[self updateFeedMenuItem:notify.object withBlock:^(Feed *feed, NSMenuItem *item) {
if (item.submenu.isFeedMenu)
item.image = [feed iconImage16];
}];
}
@end