Refactoring Status Menu
This commit is contained in:
@@ -21,47 +21,31 @@
|
||||
// SOFTWARE.
|
||||
|
||||
#import "BarMenu.h"
|
||||
#import "StoreCoordinator.h"
|
||||
#import "FeedDownload.h"
|
||||
#import "DrawImage.h"
|
||||
#import "Preferences.h"
|
||||
#import "UserPrefs.h"
|
||||
#import "NSMenu+Ext.h"
|
||||
#import "Constants.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 (strong) NSStatusItem *barItem;
|
||||
@property (strong) Preferences *prefWindow;
|
||||
@property (assign, atomic) NSInteger unreadCountTotal;
|
||||
@property (assign) BOOL coreDataEmpty;
|
||||
@property (weak) NSMenu *currentOpenMenu;
|
||||
@property (strong) NSArray<NSManagedObjectID*> *objectIDsForMenu;
|
||||
@property (strong) NSManagedObjectContext *readContext;
|
||||
@property (weak) BarStatusItem *statusItem;
|
||||
@property (strong) MapUnreadTotal *unreadMap;
|
||||
@end
|
||||
|
||||
|
||||
@implementation BarMenu
|
||||
|
||||
- (instancetype)init {
|
||||
- (instancetype)initWithStatusItem:(BarStatusItem*)statusItem {
|
||||
self = [super init];
|
||||
self.barItem = [NSStatusBar.systemStatusBar statusItemWithLength:NSVariableStatusItemLength];
|
||||
self.barItem.highlightMode = YES;
|
||||
self.barItem.menu = [NSMenu menuWithDelegate:self];
|
||||
|
||||
// Unread counter
|
||||
self.unreadCountTotal = 0;
|
||||
[self updateBarIcon];
|
||||
[self asyncReloadUnreadCountAndUpdateBarIcon:nil];
|
||||
|
||||
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
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(feedUpdated:) name:kNotificationFeedUpdated object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(feedIconUpdated:) name:kNotificationFeedIconUpdated object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkChanged:) name:kNotificationNetworkStatusChanged object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(unreadCountChanged:) name:kNotificationTotalUnreadCountChanged object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(asyncReloadUnreadCountAndUpdateBarIcon:) name:kNotificationTotalUnreadCountReset object:nil];
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -69,167 +53,8 @@
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
#pragma mark - Update Menu Bar Icon
|
||||
|
||||
/**
|
||||
If notification has @c object use this object to set unread count directly.
|
||||
If @c object is @c nil perform core data fetch on total unread count and update icon.
|
||||
*/
|
||||
- (void)asyncReloadUnreadCountAndUpdateBarIcon:(NSNotification*)notify {
|
||||
if (notify.object) { // set unread count directly
|
||||
self.unreadCountTotal = [[notify object] integerValue];
|
||||
[self updateBarIcon];
|
||||
} else {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.unreadCountTotal = [StoreCoordinator unreadCountForIndexPathString:nil];
|
||||
[self updateBarIcon];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Update menu bar icon and text according to unread count and user preferences.
|
||||
- (void)updateBarIcon {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self.unreadCountTotal > 0 && [UserPrefs defaultYES:@"globalUnreadCount"]) {
|
||||
self.barItem.title = [NSString stringWithFormat:@"%ld", self.unreadCountTotal];
|
||||
} else {
|
||||
self.barItem.title = @"";
|
||||
}
|
||||
BOOL hasNet = [FeedDownload allowNetworkConnection];
|
||||
if (self.unreadCountTotal > 0 && hasNet && [UserPrefs defaultYES:@"tintMenuBarIcon"]) {
|
||||
self.barItem.image = [RSSIcon systemBarIcon:16 tint:[NSColor rssOrange] noConnection:!hasNet];
|
||||
} else {
|
||||
self.barItem.image = [RSSIcon systemBarIcon:16 tint:nil noConnection:!hasNet];
|
||||
self.barItem.image.template = YES;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Notification callback methods
|
||||
|
||||
|
||||
/**
|
||||
Callback method fired when network conditions change.
|
||||
|
||||
@param notify Notification object contains a @c BOOL value indicating the current status.
|
||||
*/
|
||||
- (void)networkChanged:(NSNotification*)notify {
|
||||
BOOL available = [[notify object] boolValue];
|
||||
[self.barItem.menu itemWithTag:TagUpdateFeed].enabled = available;
|
||||
[self updateBarIcon];
|
||||
}
|
||||
|
||||
/**
|
||||
Callback method fired when feeds have been updated and the total unread count needs update.
|
||||
|
||||
@param notify Notification object contains the unread count difference to the current count. May be negative.
|
||||
*/
|
||||
- (void)unreadCountChanged:(NSNotification*)notify {
|
||||
self.unreadCountTotal += [[notify object] integerValue];
|
||||
[self updateBarIcon];
|
||||
}
|
||||
|
||||
/// Callback method fired when feeds have been updated in the background.
|
||||
- (void)feedUpdated:(NSNotification*)notify {
|
||||
[self updateFeed:notify.object updateIconOnly:NO];
|
||||
}
|
||||
|
||||
- (void)feedIconUpdated:(NSNotification*)notify {
|
||||
[self updateFeed:notify.object updateIconOnly:YES];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Rebuild menu after background feed update
|
||||
|
||||
|
||||
/**
|
||||
Use this method to update a single menu item and all ancestors unread count.
|
||||
If the menu isn't currently open, nothing will happen.
|
||||
|
||||
@param oid @c NSManagedObjectID must be a @c Feed instance object id.
|
||||
*/
|
||||
- (void)updateFeed:(NSManagedObjectID*)oid updateIconOnly:(BOOL)flag {
|
||||
if (self.barItem.menu.numberOfItems > 0) {
|
||||
// update items only if menu is already open (e.g., during background update)
|
||||
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
|
||||
Feed *feed = [moc objectWithID:oid];
|
||||
if ([feed isKindOfClass:[Feed class]]) {
|
||||
NSMenu *menu = [self fixUnreadCountForSubmenus:feed];
|
||||
if (!flag) [self rebuiltFeedArticle:feed inMenu:menu]; // deepest menu level, feed items
|
||||
}
|
||||
[moc reset];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Go through all parent menus and reset the menu title and unread count
|
||||
|
||||
@return @c NSMenu containing @c FeedArticle. Will be @c nil if user hasn't open the menu yet.
|
||||
*/
|
||||
- (nullable NSMenu*)fixUnreadCountForSubmenus:(Feed*)feed {
|
||||
NSMenu *menu = self.barItem.menu;
|
||||
[menu autoEnableMenuHeader:(self.unreadCountTotal > 0)];
|
||||
for (FeedGroup *parent in [feed.group allParents]) {
|
||||
NSInteger itemIndex = [menu feedDataOffset] + parent.sortIndex;
|
||||
NSMenuItem *item = [menu itemAtIndex:itemIndex];
|
||||
NSInteger unread = [item setTitleAndUnreadCount:parent];
|
||||
menu = item.submenu;
|
||||
|
||||
if (parent == feed.group) {
|
||||
// Always set icon. Will flip warning icon to default icon if article count changes.
|
||||
item.image = [feed iconImage16];
|
||||
item.enabled = (feed.articles.count > 0);
|
||||
return menu;
|
||||
}
|
||||
|
||||
if (!menu || menu.numberOfItems == 0)
|
||||
return nil;
|
||||
if (unread == 0) // if != 0 then 'setTitleAndUnreadCount' was successful (UserPrefs visible)
|
||||
unread = [menu coreDataUnreadCount];
|
||||
[menu autoEnableMenuHeader:(unread > 0)]; // of submenu but not articles menu (will be rebuild anyway)
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
/**
|
||||
Remove all @c NSMenuItem in menu and generate new ones. items from @c feed.items.
|
||||
|
||||
@param feed Corresponding @c Feed to @c NSMenu.
|
||||
@param menu Deepest menu level which contains only feed items.
|
||||
*/
|
||||
- (void)rebuiltFeedArticle:(Feed*)feed inMenu:(NSMenu*)menu {
|
||||
if (!menu || menu.numberOfItems == 0) // not opened yet
|
||||
return;
|
||||
if (self.currentOpenMenu != menu) {
|
||||
// if the menu isn't open, re-create it dynamically instead
|
||||
menu.itemArray.firstObject.parentItem.submenu = [menu cleanInstanceCopy];
|
||||
} else {
|
||||
[menu removeAllItems];
|
||||
[self insertDefaultHeaderForAllMenus:menu hasUnread:(feed.unreadCount > 0)];
|
||||
for (FeedArticle *fa in [feed sortedArticles]) {
|
||||
NSMenuItem *mi = [menu addItemWithTitle:@"" action:@selector(openFeedURL:) keyEquivalent:@""];
|
||||
mi.target = self;
|
||||
[mi setFeedArticle:fa];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Menu Delegate & Menu Generation
|
||||
|
||||
|
||||
/// @c currentOpenMenu is needed when a background update occurs. In case a feed items menu is open.
|
||||
- (void)menuWillOpen:(NSMenu *)menu {
|
||||
self.currentOpenMenu = menu;
|
||||
}
|
||||
|
||||
/// Get rid of everything that is not needed when the system bar menu is closed.
|
||||
- (void)menuDidClose:(NSMenu*)menu {
|
||||
self.currentOpenMenu = nil;
|
||||
if ([menu isMainMenu])
|
||||
self.barItem.menu = [NSMenu menuWithDelegate:self];
|
||||
}
|
||||
#pragma mark - Generate Menu Items
|
||||
|
||||
/**
|
||||
@note Delegate method not used. Here to prevent weird @c NSMenu behavior.
|
||||
@@ -240,246 +65,110 @@
|
||||
return NO;
|
||||
}
|
||||
|
||||
/// Perform a core data fatch request, store sorted object ids array and return object count.
|
||||
- (NSInteger)numberOfItemsInMenu:(NSMenu*)menu {
|
||||
[self prepareContextAndTemporaryObjectIDs:menu];
|
||||
if (_coreDataEmpty) return 1; // only if main menu empty
|
||||
return (NSInteger)self.objectIDsForMenu.count;
|
||||
}
|
||||
|
||||
/// Lazy populate system bar menus when needed.
|
||||
- (BOOL)menu:(NSMenu*)menu updateItem:(NSMenuItem*)item atIndex:(NSInteger)index shouldCancel:(BOOL)shouldCancel {
|
||||
if (_coreDataEmpty) {
|
||||
item.title = NSLocalizedString(@"~~~ list empty ~~~", nil);
|
||||
item.enabled = NO;
|
||||
[self finalizeMenu:menu object:nil];
|
||||
return YES;
|
||||
}
|
||||
id obj = [self.readContext objectWithID:[self.objectIDsForMenu objectAtIndex:(NSUInteger)index]];
|
||||
if ([obj isKindOfClass:[FeedGroup class]]) {
|
||||
[item setFeedGroup:obj];
|
||||
if ([(FeedGroup*)obj type] == FEED)
|
||||
[item setTarget:self action:@selector(openFeedURL:)];
|
||||
} else if ([obj isKindOfClass:[FeedArticle class]]) {
|
||||
[item setFeedArticle:obj];
|
||||
[item setTarget:self action:@selector(openFeedURL:)];
|
||||
}
|
||||
|
||||
if (index + 1 == menu.numberOfItems) { // last item of the menu
|
||||
[self finalizeMenu:menu object:obj];
|
||||
[self resetContextAndTemporaryObjectIDs];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Helper
|
||||
|
||||
|
||||
- (void)prepareContextAndTemporaryObjectIDs:(NSMenu*)menu {
|
||||
NSMenuItem *parent = [menu.supermenu itemAtIndex:[menu.supermenu indexOfItemWithSubmenu:menu]];
|
||||
self.readContext = [StoreCoordinator createChildContext]; // will be deleted after menu:updateItem:
|
||||
self.objectIDsForMenu = [StoreCoordinator sortedObjectIDsForParent:parent.representedObject isFeed:[menu isFeedMenu] inContext:self.readContext];
|
||||
_coreDataEmpty = ([menu isMainMenu] && self.objectIDsForMenu.count == 0); // initial state or no feeds in date store
|
||||
}
|
||||
|
||||
- (void)resetContextAndTemporaryObjectIDs {
|
||||
self.objectIDsForMenu = nil;
|
||||
[self.readContext reset];
|
||||
self.readContext = nil;
|
||||
}
|
||||
|
||||
/**
|
||||
Add default menu items that are present in each menu as header and disable menu items if necessary
|
||||
*/
|
||||
- (void)finalizeMenu:(NSMenu*)menu object:(id)obj {
|
||||
NSInteger unreadCount = self.unreadCountTotal; // if parent == nil
|
||||
if ([menu isFeedMenu]) {
|
||||
unreadCount = ((FeedArticle*)obj).feed.unreadCount;
|
||||
} else if (![menu isMainMenu]) {
|
||||
unreadCount = [menu coreDataUnreadCount];
|
||||
}
|
||||
[menu replaceSeparatorStringsWithActualSeparator];
|
||||
[self insertDefaultHeaderForAllMenus:menu hasUnread:(unreadCount > 0)];
|
||||
if ([menu isMainMenu])
|
||||
[self insertMainMenuHeader:menu];
|
||||
}
|
||||
|
||||
/**
|
||||
Insert items 'Open all unread', 'Mark all read' and 'Mark all unread' at index 0.
|
||||
|
||||
@param flag If @c NO, 'Open all unread' and 'Mark all read' will be disabled.
|
||||
*/
|
||||
- (void)insertDefaultHeaderForAllMenus:(NSMenu*)menu hasUnread:(BOOL)flag {
|
||||
MenuItemTag scope = [menu scope];
|
||||
NSMenuItem *item1 = [NSMenuItem itemWithTitle:NSLocalizedString(@"Open all unread", nil)
|
||||
action:@selector(openAllUnread:) target:self tag:TagOpenAllUnread | scope];
|
||||
NSMenuItem *item2 = [item1 alternateWithTitle:[NSString stringWithFormat:@"%@ (%lu)",
|
||||
NSLocalizedString(@"Open a few unread", nil), [UserPrefs openFewLinksLimit]]];
|
||||
NSMenuItem *item3 = [NSMenuItem itemWithTitle:NSLocalizedString(@"Mark all read", nil)
|
||||
action:@selector(markAllReadOrUnread:) target:self tag:TagMarkAllRead | scope];
|
||||
NSMenuItem *item4 = [NSMenuItem itemWithTitle:NSLocalizedString(@"Mark all unread", nil)
|
||||
action:@selector(markAllReadOrUnread:) target:self tag:TagMarkAllUnread | scope];
|
||||
item1.enabled = flag;
|
||||
item2.enabled = flag;
|
||||
item3.enabled = flag;
|
||||
// TODO: disable item3 if all items are unread?
|
||||
[menu insertItem:item1 atIndex:0];
|
||||
[menu insertItem:item2 atIndex:1];
|
||||
[menu insertItem:item3 atIndex:2];
|
||||
[menu insertItem:item4 atIndex:3];
|
||||
[menu insertItem:[NSMenuItem separatorItem] atIndex:4];
|
||||
}
|
||||
|
||||
/**
|
||||
Insert default menu items for the main menu only. Like 'Pause Updates', 'Update all feeds', 'Preferences' and 'Quit'.
|
||||
*/
|
||||
- (void)insertMainMenuHeader:(NSMenu*)menu {
|
||||
NSMenuItem *item1 = [NSMenuItem itemWithTitle:@"" action:@selector(pauseUpdates:) target:self tag:TagPauseUpdates];
|
||||
NSMenuItem *item2 = [NSMenuItem itemWithTitle:NSLocalizedString(@"Update all feeds", nil)
|
||||
action:@selector(updateAllFeeds:) target:self tag:TagUpdateFeed];
|
||||
item1.title = ([FeedDownload isPaused] ?
|
||||
NSLocalizedString(@"Resume Updates", nil) : NSLocalizedString(@"Pause Updates", nil));
|
||||
if ([UserPrefs defaultYES:@"globalUpdateAll"] == NO)
|
||||
item2.hidden = YES;
|
||||
if (![FeedDownload allowNetworkConnection])
|
||||
item2.enabled = NO;
|
||||
[menu insertItem:item1 atIndex:0];
|
||||
[menu insertItem:item2 atIndex:1];
|
||||
[menu insertItem:[NSMenuItem separatorItem] atIndex:2];
|
||||
// < feed content >
|
||||
[menu addItem:[NSMenuItem separatorItem]];
|
||||
NSMenuItem *prefs = [NSMenuItem itemWithTitle:NSLocalizedString(@"Preferences", nil)
|
||||
action:@selector(openPreferences) target:self tag:TagPreferences];
|
||||
prefs.keyEquivalent = @",";
|
||||
[menu addItem:prefs];
|
||||
[menu addItemWithTitle:NSLocalizedString(@"Quit", nil) action:@selector(terminate:) keyEquivalent:@"q"];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Menu Actions
|
||||
|
||||
|
||||
/**
|
||||
Called whenever the user activates the preferences (either through menu click or hotkey)
|
||||
*/
|
||||
- (void)openPreferences {
|
||||
if (!self.prefWindow) {
|
||||
self.prefWindow = [[Preferences alloc] initWithWindowNibName:@"Preferences"];
|
||||
self.prefWindow.window.title = [NSString stringWithFormat:@"%@ %@", NSProcessInfo.processInfo.processName,
|
||||
NSLocalizedString(@"Preferences", nil)];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(preferencesClosed:) name:NSWindowWillCloseNotification object:self.prefWindow.window];
|
||||
}
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
[self.prefWindow showWindow:nil];
|
||||
}
|
||||
|
||||
/**
|
||||
Callback method after user closes the preferences window.
|
||||
*/
|
||||
- (void)preferencesClosed:(id)sender {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSWindowWillCloseNotification object:self.prefWindow.window];
|
||||
self.prefWindow = nil;
|
||||
[FeedDownload scheduleUpdateForUpcomingFeeds];
|
||||
}
|
||||
|
||||
/**
|
||||
Called when user clicks on 'Pause Updates' in the main menu (only).
|
||||
*/
|
||||
- (void)pauseUpdates:(NSMenuItem*)sender {
|
||||
[FeedDownload setPaused:![FeedDownload isPaused]];
|
||||
[self updateBarIcon];
|
||||
}
|
||||
|
||||
/**
|
||||
Called when user clicks on 'Update all feeds' in the main menu (only).
|
||||
*/
|
||||
- (void)updateAllFeeds:(NSMenuItem*)sender {
|
||||
[self asyncReloadUnreadCountAndUpdateBarIcon:nil];
|
||||
[FeedDownload forceUpdateAllFeeds];
|
||||
}
|
||||
|
||||
/**
|
||||
Called when user clicks on 'Open all unread' or 'Open a few unread ...' on any scope level.
|
||||
*/
|
||||
- (void)openAllUnread:(NSMenuItem*)sender {
|
||||
NSMutableArray<NSURL*> *urls = [NSMutableArray<NSURL*> array];
|
||||
__block int maxItemCount = INT_MAX;
|
||||
if (sender.isAlternate)
|
||||
maxItemCount = (int)[UserPrefs openFewLinksLimit];
|
||||
|
||||
/// Populate menu with items.
|
||||
- (void)menuNeedsUpdate:(NSMenu*)menu {
|
||||
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
|
||||
[sender iterateSorted:YES inContext:moc overDescendentFeeds:^(Feed *feed, BOOL *cancel) {
|
||||
for (FeedArticle *fa in [feed sortedArticles]) { // TODO: open oldest articles first?
|
||||
if (maxItemCount <= 0) break;
|
||||
if (fa.unread && fa.link.length > 0) {
|
||||
[urls addObject:[NSURL URLWithString:fa.link]];
|
||||
fa.unread = NO;
|
||||
feed.unreadCount -= 1;
|
||||
self.unreadCountTotal -= 1;
|
||||
maxItemCount -= 1;
|
||||
if (menu.isFeedMenu) {
|
||||
Feed *feed = [StoreCoordinator feedWithIndexPath:menu.titleIndexPath inContext:moc];
|
||||
[self setArticles:[feed sortedArticles] forMenu:menu];
|
||||
} else {
|
||||
NSArray<FeedGroup*> *groups = [StoreCoordinator sortedFeedGroupsWithParent:menu.parentItem.representedObject inContext:moc];
|
||||
if (groups.count == 0) {
|
||||
[menu addItemWithTitle:NSLocalizedString(@"~~~ no entries ~~~", nil) action:nil keyEquivalent:@""].enabled = NO;
|
||||
} else {
|
||||
[self setFeedGroups:groups forMenu:menu];
|
||||
}
|
||||
}
|
||||
[moc reset];
|
||||
}
|
||||
|
||||
/// 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];
|
||||
for (FeedArticle *fa in sortedList) {
|
||||
[menu addItem:[fa newMenuItem]];
|
||||
}
|
||||
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 {
|
||||
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
|
||||
Feed *feed = [moc objectWithID:oid];
|
||||
if (![feed isKindOfClass:[Feed class]]) {
|
||||
[moc reset];
|
||||
return;
|
||||
}
|
||||
NSMenuItem *item = [self.statusItem.mainMenu deepestItemWithPath:feed.indexPath];
|
||||
if (!item) {
|
||||
[moc reset];
|
||||
return;
|
||||
}
|
||||
block(feed, item);
|
||||
[moc reset];
|
||||
}
|
||||
|
||||
/// Callback method fired when feed has been updated in the background.
|
||||
- (void)feedUpdated:(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.enabled = (feed.articles.count > 0);
|
||||
if (item.submenu.numberOfItems > 0) { // replace articles menu
|
||||
[item.submenu removeAllItems];
|
||||
[self setArticles:[feed sortedArticles] forMenu:item.submenu];
|
||||
}
|
||||
}
|
||||
*cancel = (maxItemCount <= 0);
|
||||
}];
|
||||
[self updateBarIcon];
|
||||
[self openURLsWithPreferredBrowser:urls];
|
||||
[StoreCoordinator saveContext:moc andParent:YES];
|
||||
[moc reset];
|
||||
}
|
||||
|
||||
/**
|
||||
Called when user clicks on 'Mark all read' @b or 'Mark all unread' on any scope level.
|
||||
*/
|
||||
- (void)markAllReadOrUnread:(NSMenuItem*)sender {
|
||||
BOOL markRead = ((sender.tag & TagMaskType) == TagMarkAllRead);
|
||||
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
|
||||
[sender iterateSorted:NO inContext:moc overDescendentFeeds:^(Feed *feed, BOOL *cancel) {
|
||||
self.unreadCountTotal += (markRead ? [feed markAllItemsRead] : [feed markAllItemsUnread]);
|
||||
}];
|
||||
[self updateBarIcon];
|
||||
[StoreCoordinator saveContext:moc andParent:YES];
|
||||
[moc reset];
|
||||
}
|
||||
|
||||
/**
|
||||
Called when user clicks on a single feed item or the feed group.
|
||||
|
||||
@param sender A menu item containing either a @c FeedArticle or a @c FeedGroup objectID.
|
||||
*/
|
||||
- (void)openFeedURL:(NSMenuItem*)sender {
|
||||
NSManagedObjectID *oid = sender.representedObject;
|
||||
if (!oid)
|
||||
return;
|
||||
NSString *url = nil;
|
||||
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
|
||||
id obj = [moc objectWithID:oid];
|
||||
if ([obj isKindOfClass:[FeedGroup class]]) {
|
||||
url = ((FeedGroup*)obj).feed.link;
|
||||
} else if ([obj isKindOfClass:[FeedArticle class]]) {
|
||||
FeedArticle *fa = obj;
|
||||
url = fa.link;
|
||||
if (fa.unread) {
|
||||
fa.unread = NO;
|
||||
fa.feed.unreadCount -= 1;
|
||||
self.unreadCountTotal -= 1;
|
||||
[self updateBarIcon];
|
||||
[StoreCoordinator saveContext:moc andParent:YES];
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
[moc reset];
|
||||
if (!url || url.length == 0) return;
|
||||
[self openURLsWithPreferredBrowser:@[[NSURL URLWithString:url]]];
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
Open web links in default browser or a browser the user selected in the preferences.
|
||||
|
||||
@param urls A list of @c NSURL objects that will be opened immediatelly in bulk.
|
||||
*/
|
||||
- (void)openURLsWithPreferredBrowser:(NSArray<NSURL*>*)urls {
|
||||
if (urls.count == 0) return;
|
||||
[[NSWorkspace sharedWorkspace] openURLs:urls withAppBundleIdentifier:[UserPrefs getHttpApplication] options:NSWorkspaceLaunchDefault additionalEventParamDescriptor:nil launchIdentifiers:nil];
|
||||
/// 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
|
||||
|
||||
Reference in New Issue
Block a user