Refactoring Part 1: Dynamic menus (stable)

This commit is contained in:
relikd
2018-11-24 14:18:06 +01:00
parent 080991ebc4
commit 6223d1a169
22 changed files with 1026 additions and 973 deletions

View File

@@ -25,16 +25,20 @@
#import "FeedDownload.h"
#import "DrawImage.h"
#import "Preferences.h"
#import "NSMenuItem+Info.h"
#import "NSMenuItem+Generate.h"
#import "UserPrefs.h"
#import "NSMenu+Ext.h"
#import "NSMenuItem+Ext.h"
#import "Feed+Ext.h"
#import "Constants.h"
@interface BarMenu()
@property (strong) NSStatusItem *barItem;
@property (strong) Preferences *prefWindow;
@property (weak) NSMenu *mm;
@property (assign) int unreadCountTotal;
@property (strong) NSArray<FeedConfig*> *allFeeds;
@property (strong) NSArray<NSManagedObjectID*> *currentOpenMenu;
@property (strong) NSManagedObjectContext *readContext;
@end
@@ -44,9 +48,20 @@
self = [super init];
self.barItem = [NSStatusBar.systemStatusBar statusItemWithLength:NSVariableStatusItemLength];
self.barItem.highlightMode = YES;
[self rebuildMenu];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkChange:) name:@"baRSS-notification-network-status-change" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(feedUpdated:) name:@"baRSS-notification-feed-updated" object:nil];
self.barItem.menu = [NSMenu menuWithDelegate:self];
// Unread counter
self.unreadCountTotal = 0;
[self updateBarIcon];
dispatch_async(dispatch_get_main_queue(), ^{
self.unreadCountTotal = [StoreCoordinator totalNumberOfUnreadFeeds];
[self updateBarIcon];
});
// Register for notifications
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(feedUpdated:) name:kNotificationFeedUpdated object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkChanged:) name:kNotificationNetworkStatusChanged object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(unreadCountChanged:) name:kNotificationTotalUnreadCountChanged object:nil];
[FeedDownload registerNetworkChangeNotification];
[FeedDownload performSelectorInBackground:@selector(scheduleNextUpdate:) withObject:[NSNumber numberWithBool:NO]];
return self;
@@ -57,46 +72,6 @@
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)networkChange:(NSNotification*)notify {
BOOL available = [[notify object] boolValue];
[self.barItem.menu itemWithTag:TagUpdateFeed].enabled = available;
[self updateBarIcon];
// TODO: Disable 'update all' menu item?
}
- (void)feedUpdated:(NSNotification*)notify {
FeedConfig *config = notify.object;
NSLog(@"%@", config.indexPath);
[self rebuildMenu];
}
- (void)rebuildMenu {
self.barItem.menu = [self generateMainMenu];
[self updateBarIcon];
}
- (void)donothing {
dispatch_async(dispatch_get_main_queue(), ^{
[self.mm itemAtIndex:4].title = [NSString stringWithFormat:@"%@", [NSDate date]];
});
sleep(1);
[self performSelectorInBackground:@selector(donothing) withObject:nil];
}
// TODO: remove debugging stuff
- (void)printUnreadRecurisve:(NSMenu*)menu str:(NSString*)prefix {
for (NSMenuItem *item in menu.itemArray) {
if (![item hasReaderInfo]) continue;
id obj = [item requestCoreDataObject];
if ([obj isKindOfClass:[FeedItem class]] && ([obj unread] > 0 || item.unreadCount > 0))
NSLog(@"%@ %@ (%d == %d)", prefix, item.title, item.unreadCount, [obj unread]);
else if ([item hasUnread])
NSLog(@"%@ %@ (%d)", prefix, item.title, item.unreadCount);
if (item.hasSubmenu) {
[self printUnreadRecurisve:item.submenu str:[NSString stringWithFormat:@" %@", prefix]];
}
}
}
/**
Update menu bar icon and text according to unread count and user preferences.
*/
@@ -117,170 +92,229 @@
self.barItem.image.template = YES;
}
});
// NSLog(@"==> %d", self.unreadCountTotal);
// [self printUnreadRecurisve:self.barItem.menu str:@""];
}
#pragma mark - Menu Generator
#pragma mark - Notification callback methods -
/**
Builds main menu with items on the very first menu level. Including Preferences, Quit, etc.
Callback method fired when network conditions change.
@param notify Notification object contains a @c BOOL value indicating the current status.
*/
- (NSMenu*)generateMainMenu {
NSMenu *menu = [NSMenu new];
menu.autoenablesItems = NO;
[self addTitle:NSLocalizedString(@"Pause Updates", nil) selector:@selector(pauseUpdates:) toMenu:menu tag:TagPauseUpdates];
NSMenuItem *updateAll = [self addTitle:NSLocalizedString(@"Update all feeds", nil) selector:@selector(updateAllFeeds:) toMenu:menu tag:TagUpdateFeed];
if ([UserPrefs defaultYES:@"globalUpdateAll"] == NO)
updateAll.hidden = YES;
[menu addItem:[NSMenuItem separatorItem]];
[self defaultHeaderForMenu:menu scope:ScopeGlobal];
self.unreadCountTotal = 0;
@autoreleasepool {
for (FeedConfig *fc in [StoreCoordinator sortedFeedConfigItems]) {
[menu addItem:[self generateMenuItem:fc unread:&_unreadCountTotal]];
- (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] intValue];
[self updateBarIcon];
}
/**
Callback method fired when feeds have been updated in the background.
*/
- (void)feedUpdated:(NSNotification*)notify {
if (self.barItem.menu.numberOfItems > 0) {
// update items only if menu is already open (e.g., during background update)
[self.readContext refreshAllObjects]; // because self.allFeeds is the same context
[self recursiveUpdateMenu:self.barItem.menu withFeed:nil];
}
}
/**
Called recursively for all @c FeedConfig children.
If the projected submenu in @c menu does not exist, all subsequent children are skipped in @c FeedConfig.
The title and unread count is updated for all menu items. @c FeedItem menus are completely re-generated.
@param config If @c nil the root object (@c self.allFeeds) is used.
*/
- (void)recursiveUpdateMenu:(NSMenu*)menu withFeed:(FeedConfig*)config {
if (config.feed.items.count > 0) { // deepest menu level, feed items
[menu removeAllItems];
[self insertDefaultHeaderForAllMenus:menu scope:ScopeFeed hasUnread:(config.unreadCount > 0)];
for (FeedItem *fi in config.feed.items) {
NSMenuItem *mi = [menu addItemWithTitle:@"" action:@selector(openFeedURL:) keyEquivalent:@""];
mi.target = self;
[mi setFeedItem:fi];
}
} else {
BOOL hasUnread = (config ? config.unreadCount > 0 : self.unreadCountTotal > 0);
NSInteger offset = [menu getFeedConfigOffsetAndUpdateUnread:hasUnread];
for (FeedConfig *child in (config ? config.children : self.allFeeds)) {
NSMenuItem *item = [menu itemAtIndex:offset + child.sortIndex];
[item setTitleAndUnreadCount:child];
if (item.submenu.numberOfItems > 0)
[self recursiveUpdateMenu:[item submenu] withFeed:child];
}
}
[self updateMenuHeaderEnabled:menu hasUnread:(self.unreadCountTotal > 0)];
}
#pragma mark - Menu Delegate & Menu Generation -
// Get rid of everything that is not needed when the system bar menu isnt open.
- (void)menuDidClose:(NSMenu*)menu {
if ([menu isMainMenu]) {
self.allFeeds = nil;
[self.readContext reset];
self.readContext = nil;
self.barItem.menu = [NSMenu menuWithDelegate:self];
}
}
// If main menu load inital set of items, then find item based on index path.
- (NSInteger)numberOfItemsInMenu:(NSMenu*)menu {
if ([menu isMainMenu]) {
[self.readContext reset]; // will be ignored if nil
self.readContext = [StoreCoordinator createChildContext];
self.allFeeds = [StoreCoordinator sortedFeedConfigItemsInContext:self.readContext];
self.currentOpenMenu = [self.allFeeds valueForKeyPath:@"objectID"];
} else {
FeedConfig *conf = [self configAtIndexPathStr:menu.title];
[self.readContext refreshObject:conf mergeChanges:YES];
self.currentOpenMenu = [(conf.typ == FEED ? conf.feed.items : [conf sortedChildren]) valueForKeyPath:@"objectID"];
}
return (NSInteger)[self.currentOpenMenu count];
}
/**
Find @c FeedConfig item in array @c self.allFeeds that is already loaded.
@param indexString Path as string that is stored in @c NSMenu title
*/
- (FeedConfig*)configAtIndexPathStr:(NSString*)indexString {
NSArray<NSString*> *parts = [indexString componentsSeparatedByString:@"."];
NSInteger firstIndex = [[parts objectAtIndex:1] integerValue];
FeedConfig *changing = [self.allFeeds objectAtIndex:(NSUInteger)firstIndex];
for (NSUInteger i = 2; i < parts.count; i++) {
NSInteger childIndex = [[parts objectAtIndex:i] integerValue];
BOOL err = YES;
for (FeedConfig *c in changing.children) {
if (c.sortIndex == childIndex) {
err = NO;
changing = c;
break; // Exit early. Should be faster than sorted children method.
}
}
NSAssert(!err, @"ERROR configAtIndex: Shouldn't happen. Something wrong with indexing.");
}
return changing;
}
// Lazy populate the system bar menus when needed.
- (BOOL)menu:(NSMenu*)menu updateItem:(NSMenuItem*)item atIndex:(NSInteger)index shouldCancel:(BOOL)shouldCancel {
NSManagedObjectID *moid = [self.currentOpenMenu objectAtIndex:(NSUInteger)index];
id obj = [self.readContext objectWithID:moid];
[self.readContext refreshObject:obj mergeChanges:YES];
if ([obj isKindOfClass:[FeedConfig class]]) {
[item setFeedConfig:obj];
if ([(FeedConfig*)obj typ] == FEED) {
item.target = self;
item.action = @selector(openFeedURL:);
}
} else if ([obj isKindOfClass:[FeedItem class]]) {
[item setFeedItem:obj];
item.target = self;
item.action = @selector(openFeedURL:);
}
if (menu.numberOfItems == index + 1) {
int unreadCount = self.unreadCountTotal; // if parent == nil
if ([obj isKindOfClass:[FeedItem class]]) {
unreadCount = [[[(FeedItem*)obj feed] config] unreadCount];
} else if ([(FeedConfig*)obj parent]) {
unreadCount = [[(FeedConfig*)obj parent] unreadCount];
}
[self finalizeMenu:menu hasUnread:(unreadCount > 0)];
self.currentOpenMenu = nil;
}
return YES;
}
/**
Add default menu items that are present in each menu as header.
@param flag If @c NO, 'Open all unread' and 'Mark all read' will be disabled.
*/
- (void)finalizeMenu:(NSMenu*)menu hasUnread:(BOOL)flag {
BOOL isMainMenu = [menu isMainMenu];
MenuItemTag scope;
if (isMainMenu) scope = ScopeGlobal;
else if ([menu isFeedMenu]) scope = ScopeFeed;
else scope = ScopeGroup;
[menu replaceSeparatorStringsWithActualSeparator];
[self insertDefaultHeaderForAllMenus:menu scope:scope hasUnread:flag];
if (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 scope:(MenuItemTag)scope hasUnread:(BOOL)flag {
NSMenuItem *item1 = [self itemTitle:NSLocalizedString(@"Open all unread", nil) selector:@selector(openAllUnread:) tag:TagOpenAllUnread | scope];
NSMenuItem *item2 = [item1 alternateWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Open a few unread (%d)", nil), 3]];
NSMenuItem *item3 = [self itemTitle:NSLocalizedString(@"Mark all read", nil) selector:@selector(markAllReadOrUnread:) tag:TagMarkAllRead | scope];
NSMenuItem *item4 = [self itemTitle:NSLocalizedString(@"Mark all unread", nil) selector:@selector(markAllReadOrUnread:) 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 = [self itemTitle:NSLocalizedString(@"Pause Updates", nil) selector:@selector(pauseUpdates:) tag:TagPauseUpdates];
NSMenuItem *item2 = [self itemTitle:NSLocalizedString(@"Update all feeds", nil) selector:@selector(updateAllFeeds:) tag:TagUpdateFeed];
if ([UserPrefs defaultYES:@"globalUpdateAll"] == NO)
item2.hidden = YES;
if (![FeedDownload isNetworkReachable])
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 = [self addTitle:NSLocalizedString(@"Preferences", nil) selector:@selector(openPreferences) toMenu:menu tag:TagPreferences];
NSMenuItem *prefs = [self itemTitle:NSLocalizedString(@"Preferences", nil) selector:@selector(openPreferences) tag:TagPreferences];
prefs.keyEquivalent = @",";
[menu addItem:prefs];
[menu addItemWithTitle:NSLocalizedString(@"Quit", nil) action:@selector(terminate:) keyEquivalent:@"q"];
return menu;
}
/**
Generate menu item with all its sub-menus. @c FeedConfig type is evaluated automatically.
@param unread Pointer to an unread count. Will be incremented while traversing through sub-menus.
Helper method to generate a new @c NSMenuItem.
*/
- (NSMenuItem*)generateMenuItem:(FeedConfig*)config unread:(int*)unread {
NSMenuItem *item = [NSMenuItem feedConfig:config];
int count = 0;
if (item.tag == ScopeFeed) {
count += [self setSubmenuForFeedScope:item config:config];
} else if (item.tag == ScopeGroup) {
[self setSubmenuForGroupScope:item config:config unread:&count];
} else { // Separator item
return item;
}
*unread += count;
[item markReadAndUpdateTitle:-count];
[self updateMenuHeaderEnabled:item.submenu hasUnread:(count > 0)];
return item;
}
/**
Set subitems for a @c FeedConfig group item. Namely various @c FeedConfig and @c FeedItem items.
@param item The item where the menu will be appended.
@param config A @c FeedConfig group item.
@param unread Pointer to an unread count. Will be incremented while traversing through sub-menus.
*/
- (void)setSubmenuForGroupScope:(NSMenuItem*)item config:(FeedConfig*)config unread:(int*)unread {
item.submenu = [self defaultHeaderForMenu:nil scope:ScopeGroup];
for (FeedConfig *obj in config.sortedChildren) {
[item.submenu addItem: [self generateMenuItem:obj unread:unread]];
}
}
/**
Set subitems for a @c FeedConfig feed item. Namely its @c FeedItem items.
@param item The item where the menu will be appended.
@param config For which item the menu should be generated. Attribute @c feed should be populated.
@return Unread count for feed.
*/
- (int)setSubmenuForFeedScope:(NSMenuItem*)item config:(FeedConfig*)config {
item.submenu = [self defaultHeaderForMenu:nil scope:ScopeFeed];
int count = 0;
for (FeedItem *obj in config.feed.items) {
if (obj.unread) ++count;
[item.submenu addItem:[[NSMenuItem feedItem:obj] setAction:@selector(openFeedURL:) target:self]];
}
[item setAction:@selector(openFeedURL:) target:self];
return count;
}
/**
Helper function to insert a menu item with @c target @c = @c self
*/
- (NSMenuItem*)addTitle:(NSString*)title selector:(SEL)selector toMenu:(NSMenu*)menu tag:(MenuItemTag)tag {
- (NSMenuItem*)itemTitle:(NSString*)title selector:(SEL)selector tag:(MenuItemTag)tag {
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:@""];
item.target = self;
item.tag = tag;
[item applyUserSettingsDisplay];
[menu addItem:item];
return item;
}
#pragma mark - Default Menu Header Items
/**
Append header items to menu accoring to user preferences.
@note If @c menu is @c nil a new menu is created and returned.
@param menu The menu where the items should be appended.
@param scope Tag will be concatenated with that scope (Global, Group or Local).
@return Will return the menu item provided or create a new one if menu was @c nil.
*/
- (NSMenu*)defaultHeaderForMenu:(NSMenu*)menu scope:(MenuItemTag)scope {
if (!menu) {
menu = [NSMenu new];
menu.autoenablesItems = NO;
}
NSMenuItem *item = [self addTitle:NSLocalizedString(@"Open all unread", nil) selector:@selector(openAllUnread:) toMenu:menu tag:TagOpenAllUnread | scope];
[menu addItem:[item alternateWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Open a few unread (%d)", nil), 3]]];
[self addTitle:NSLocalizedString(@"Mark all read", nil) selector:@selector(markAllRead:) toMenu:menu tag:TagMarkAllRead | scope];
[self addTitle:NSLocalizedString(@"Mark all unread", nil) selector:@selector(markAllUnread:) toMenu:menu tag:TagMarkAllUnread | scope];
[menu addItem:[NSMenuItem separatorItem]];
return menu;
}
- (void)setItemUpdateAllHidden:(BOOL)hidden {
[self.barItem.menu itemWithTag:TagUpdateFeed].hidden = hidden;
}
- (void)updateMenuHeaders:(BOOL)recursive {
[self updateMenuHeaderHidden:self.barItem.menu recursive:recursive];
}
- (void)updateMenuHeaderHidden:(NSMenu*)menu recursive:(BOOL)flag {
for (NSMenuItem *item in menu.itemArray) {
[item applyUserSettingsDisplay];
if (flag && item.hasSubmenu) {
[self updateMenuHeaderHidden:item.submenu recursive:YES];
}
}
}
- (void)updateMenuHeaderEnabled:(NSMenu*)menu hasUnread:(BOOL)flag {
int stopAfter = 4; // 3 (+1 alternate)
for (NSMenuItem *item in menu.itemArray) {
switch (item.tag & TagMaskType) {
case TagMarkAllRead: item.enabled = flag; break;
case TagMarkAllUnread: item.enabled = !flag; break;
case TagOpenAllUnread: item.enabled = flag; break;
default: continue; // wrong tag, ignore
}
--stopAfter;
if (stopAfter < 0)
break; // break early after all header items have been processed
}
}
#pragma mark - Menu Actions
#pragma mark - Menu Actions -
/**
@@ -297,117 +331,100 @@
[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;
}
/**
Called when user clicks on 'Pause Updates' in the main menu (only).
*/
- (void)pauseUpdates:(NSMenuItem*)sender {
NSLog(@"1pause");
}
/**
Called when user clicks on 'Update all feeds' in the main menu (only).
*/
- (void)updateAllFeeds:(NSMenuItem*)sender {
// TODO: Disable 'update all' menu item during update?
[FeedDownload scheduleNextUpdate:YES];
}
/**
Combined selector for menu action.
@note @c sender.tag includes @c ScopeLocal, @c ScopeGroup @b or @c ScopeGlobal.
@param sender @c NSMenuItem that was clicked during the action (e.g., "open all unread")
Called when user clicks on 'Open all unread' or 'Open a few unread ...' on any scope level.
*/
- (void)openAllUnread:(NSMenuItem*)sender {
int maxItemCount = INT_MAX;
NSMutableArray<NSURL*> *urls = [NSMutableArray<NSURL*> array];
__block int maxItemCount = INT_MAX;
if (sender.isAlternate)
maxItemCount = 3; // TODO: read from preferences
__block int stopAfter = maxItemCount;
NSMutableArray<NSURL*> *urls = [NSMutableArray<NSURL*> array];
[self siblingsDescendantFeedConfigs:sender block:^BOOL(FeedConfig *parent, FeedItem *item) {
if (stopAfter <= 0)
return NO; // stop further processing
if (item.unread && item.link.length > 0) {
[urls addObject:[NSURL URLWithString:item.link]];
item.unread = NO;
--stopAfter;
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
[sender iterateSorted:YES inContext:moc overDescendentFeeds:^(Feed *feed, BOOL *cancel) {
int itemSum = 0;
for (FeedItem *i in feed.items) {
if (itemSum >= maxItemCount) {
break;
}
if (i.unread && i.link.length > 0) {
[urls addObject:[NSURL URLWithString:i.link]];
i.unread = NO;
++itemSum;
}
}
return YES;
if (itemSum > 0) {
[feed.config markUnread:-itemSum ancestorsOnly:NO];
maxItemCount -= itemSum;
}
*cancel = (maxItemCount <= 0);
}];
stopAfter = maxItemCount;
int total = [sender siblingsDescendantItemInfo:^int(NSMenuItem *item, int count) {
if (item.tag & ScopeFeed) {
if (stopAfter <= 0) return -1;
--stopAfter;
}
[item markReadAndUpdateTitle:count];
return count;
} unreadEntriesOnly:YES];
[self updateAcestors:sender markRead:total];
[self openURLsWithPreferredBrowser:urls];
[StoreCoordinator saveContext:moc andParent:YES];
[moc reset];
}
/**
Combined selector for menu action.
@note @c sender.tag includes @c ScopeLocal, @c ScopeGroup @b or @c ScopeGlobal.
@param sender @c NSMenuItem that was clicked during the action (e.g., "mark all read")
Called when user clicks on 'Mark all read' @b or 'Mark all unread' on any scope level.
*/
- (void)markAllRead:(NSMenuItem*)sender {
[self siblingsDescendantFeedConfigs:sender block:^BOOL(FeedConfig *parent, FeedItem *item) {
if (item.unread)
item.unread = NO;
return YES;
- (void)markAllReadOrUnread:(NSMenuItem*)sender {
BOOL markRead = ((sender.tag & TagMaskType) == TagMarkAllRead);
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
[sender iterateSorted:NO inContext:moc overDescendentFeeds:^(Feed *feed, BOOL *cancel) {
if (markRead) [feed markAllItemsRead];
else [feed markAllItemsUnread];
}];
int total = [sender siblingsDescendantItemInfo:^int(NSMenuItem *item, int count) {
[item markReadAndUpdateTitle:count];
return count;
} unreadEntriesOnly:YES];
[self updateAcestors:sender markRead:total];
[StoreCoordinator saveContext:moc andParent:YES];
[moc reset];
}
/**
Combined selector for menu action.
@note @c sender.tag includes @c ScopeLocal, @c ScopeGroup @b or @c ScopeGlobal.
@param sender @c NSMenuItem that was clicked during the action (e.g., "mark all unread")
*/
- (void)markAllUnread:(NSMenuItem*)sender {
[self siblingsDescendantFeedConfigs:sender block:^BOOL(FeedConfig *parent, FeedItem *item) {
if (item.unread == NO)
item.unread = YES;
return YES;
}];
int total = [sender siblingsDescendantItemInfo:^int(NSMenuItem *item, int count) {
if (count > item.unreadCount)
[item markReadAndUpdateTitle:(item.unreadCount - count)];
return count;
} unreadEntriesOnly:NO];
[self updateAcestors:sender markRead:([self getAncestorUnreadCount:sender] - total)];
}
Called when user clicks on a single feed item or the feed group.
/**
Called when user clicks on a single feed item or the superior feed.
@param sender A menu item containing either a @c FeedItem or a @c FeedConfig.
@param sender A menu item containing either a @c FeedItem or a @c FeedConfig objectID.
*/
- (void)openFeedURL:(NSMenuItem*)sender {
if (!sender.hasReaderInfo)
NSManagedObjectID *oid = sender.representedObject;
if (!oid)
return;
NSString *url = nil;
id obj = [sender requestCoreDataObject];
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
id obj = [moc objectWithID:oid];
if ([obj isKindOfClass:[FeedConfig class]]) {
url = [[(FeedConfig*)obj feed] link];
} else if ([obj isKindOfClass:[FeedItem class]]) {
FeedItem *feed = obj;
url = [feed link];
if ([sender hasUnread]) {
if (feed.unread) {
feed.unread = NO;
[sender markReadAndUpdateTitle:1];
[self updateAcestors:sender markRead:1];
[feed.feed.config markUnread:-1 ancestorsOnly:NO];
[StoreCoordinator saveContext:moc andParent:YES];
}
}
[moc reset];
if (!url || url.length == 0) return;
[self openURLsWithPreferredBrowser:@[[NSURL URLWithString:url]]];
}
@@ -422,58 +439,4 @@
[[NSWorkspace sharedWorkspace] openURLs:urls withAppBundleIdentifier:[UserPrefs getHttpApplication] options:NSWorkspaceLaunchDefault additionalEventParamDescriptor:nil launchIdentifiers:nil];
}
#pragma mark - Iterating over items and propagating unread count
/**
Iterate over all feed items from siblings and contained children.
@param sender @c NSMenuItem that was clicked during the action (e.g., "open all unread")
@param block Iterate over all FeedItems on the deepest layer.
*/
- (void)siblingsDescendantFeedConfigs:(NSMenuItem*)sender block:(FeedConfigRecursiveItemsBlock)block {
if (sender.parentItem) {
FeedConfig *obj = [sender.parentItem requestCoreDataObject];
if ([obj isKindOfClass:[FeedConfig class]]) // important: this could be a FeedItem
[obj descendantFeedItems:block];
} else {
// Sadly we can't just fetch the list of FeedItems since it is not ordered (in case open 10 at a time)
@autoreleasepool {
for (FeedConfig *config in [StoreCoordinator sortedFeedConfigItems]) {
if ([config descendantFeedItems:block] == NO)
break;
}
}
}
}
/**
Recursively update all parent's unread count and total unread count.
@param sender Current menu item, parent will be called recursively on this element.
@param count The amount by which the unread count is adjusted. If negative, items will be marked as unread.
*/
- (void)updateAcestors:(NSMenuItem*)sender markRead:(int)count {
[sender markAncestorsRead:count];
self.unreadCountTotal -= count;
if (self.unreadCountTotal < 0) {
NSLog(@"Should never happen. Global unread count < 0");
self.unreadCountTotal = 0;
}
[self updateBarIcon];
}
/**
Get unread count from the parent menu item. If there is none, get the total unread count
@param sender Current menu item, parent will be called on this element.
@return Unread count for parent element (total count if parent is @c nil)
*/
- (int)getAncestorUnreadCount:(NSMenuItem*)sender {
if ([sender.parentItem hasReaderInfo])
return [sender.parentItem unreadCount];
return self.unreadCountTotal;
}
@end