diff --git a/baRSS.xcodeproj/project.pbxproj b/baRSS.xcodeproj/project.pbxproj index 47d598c..1346916 100644 --- a/baRSS.xcodeproj/project.pbxproj +++ b/baRSS.xcodeproj/project.pbxproj @@ -7,8 +7,8 @@ objects = { /* Begin PBXBuildFile section */ - 541A90F221257D77002680A6 /* MenuItemInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 541A90F121257D77002680A6 /* MenuItemInfo.m */; }; 54209E942117325100F3B5EF /* DrawImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 54209E932117325100F3B5EF /* DrawImage.m */; }; + 543695D5214EFD9800DA979D /* NSMenuItem+Info.m in Sources */ = {isa = PBXBuildFile; fileRef = 543695D4214EFD9800DA979D /* NSMenuItem+Info.m */; }; 544B011A2114B41200386E5C /* ModalSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B01192114B41200386E5C /* ModalSheet.m */; }; 544B011D2114EE9100386E5C /* AppHook.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B011C2114EE9100386E5C /* AppHook.m */; }; 544DCCB9212A2B4D002DBC46 /* RSXML.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 544DCCB8212A2B4D002DBC46 /* RSXML.framework */; }; @@ -20,6 +20,7 @@ 546FC44421189975007CC3A3 /* SettingsGeneral.xib in Resources */ = {isa = PBXBuildFile; fileRef = 546FC44221189975007CC3A3 /* SettingsGeneral.xib */; }; 546FC4472118A8E6007CC3A3 /* Preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = 546FC4462118A8E6007CC3A3 /* Preferences.xib */; }; 5477D34E21233C62002BA27F /* FeedConfig+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 5477D34D21233C62002BA27F /* FeedConfig+Ext.m */; }; + 5496B511214D6275003ED4ED /* UserPrefs.m in Sources */ = {isa = PBXBuildFile; fileRef = 5496B510214D6275003ED4ED /* UserPrefs.m */; }; 54ACC28621061B3C0020715F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 54ACC28521061B3C0020715F /* Assets.xcassets */; }; 54ACC28C21061B3C0020715F /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC28B21061B3C0020715F /* main.m */; }; 54ACC29521061E270020715F /* FeedDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC29421061E270020715F /* FeedDownload.m */; }; @@ -56,10 +57,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 541A90F021257D77002680A6 /* MenuItemInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MenuItemInfo.h; sourceTree = ""; }; - 541A90F121257D77002680A6 /* MenuItemInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MenuItemInfo.m; sourceTree = ""; }; 54209E922117325100F3B5EF /* DrawImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DrawImage.h; sourceTree = ""; }; 54209E932117325100F3B5EF /* DrawImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DrawImage.m; sourceTree = ""; }; + 543695D3214EFD9800DA979D /* NSMenuItem+Info.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSMenuItem+Info.h"; sourceTree = ""; }; + 543695D4214EFD9800DA979D /* NSMenuItem+Info.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSMenuItem+Info.m"; sourceTree = ""; }; 544B01182114B41200386E5C /* ModalSheet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ModalSheet.h; sourceTree = ""; }; 544B01192114B41200386E5C /* ModalSheet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ModalSheet.m; sourceTree = ""; }; 544B011B2114EE9100386E5C /* AppHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppHook.h; sourceTree = ""; }; @@ -75,6 +76,8 @@ 546FC4462118A8E6007CC3A3 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Preferences.xib; sourceTree = ""; }; 5477D34C21233C62002BA27F /* FeedConfig+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FeedConfig+Ext.h"; sourceTree = ""; }; 5477D34D21233C62002BA27F /* FeedConfig+Ext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FeedConfig+Ext.m"; sourceTree = ""; }; + 5496B50F214D6275003ED4ED /* UserPrefs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UserPrefs.h; sourceTree = ""; }; + 5496B510214D6275003ED4ED /* UserPrefs.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UserPrefs.m; sourceTree = ""; }; 54ACC27C21061B3B0020715F /* baRSS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = baRSS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 54ACC28321061B3B0020715F /* DBv1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = DBv1.xcdatamodel; sourceTree = ""; }; 54ACC28521061B3C0020715F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -108,8 +111,8 @@ 541A90EF21257D4F002680A6 /* Status Bar Menu */ = { isa = PBXGroup; children = ( - 541A90F021257D77002680A6 /* MenuItemInfo.h */, - 541A90F121257D77002680A6 /* MenuItemInfo.m */, + 543695D3214EFD9800DA979D /* NSMenuItem+Info.h */, + 543695D4214EFD9800DA979D /* NSMenuItem+Info.m */, 54FE73D1212316CD003EAC65 /* BarMenu.h */, 54FE73D2212316CD003EAC65 /* BarMenu.m */, ); @@ -128,6 +131,8 @@ 546FC44521189ADC007CC3A3 /* General Tab */ = { isa = PBXGroup; children = ( + 5496B50F214D6275003ED4ED /* UserPrefs.h */, + 5496B510214D6275003ED4ED /* UserPrefs.m */, 546FC44021189975007CC3A3 /* SettingsGeneral.h */, 546FC44121189975007CC3A3 /* SettingsGeneral.m */, 546FC44221189975007CC3A3 /* SettingsGeneral.xib */, @@ -293,12 +298,13 @@ 54ACC28C21061B3C0020715F /* main.m in Sources */, 54FE73D3212316CD003EAC65 /* BarMenu.m in Sources */, 544B011A2114B41200386E5C /* ModalSheet.m in Sources */, + 543695D5214EFD9800DA979D /* NSMenuItem+Info.m in Sources */, 54ACC29821061FBA0020715F /* Preferences.m in Sources */, + 5496B511214D6275003ED4ED /* UserPrefs.m in Sources */, 546FC43D21188AD5007CC3A3 /* SettingsFeeds.m in Sources */, 54E88320211B509D00064188 /* ModalFeedEdit.m in Sources */, 54209E942117325100F3B5EF /* DrawImage.m in Sources */, 54FE73D021220DEC003EAC65 /* StoreCoordinator.m in Sources */, - 541A90F221257D77002680A6 /* MenuItemInfo.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/baRSS/Info.plist b/baRSS/Info.plist index 1cf9527..4d0013d 100644 --- a/baRSS/Info.plist +++ b/baRSS/Info.plist @@ -2,8 +2,6 @@ - LSMultipleInstancesProhibited - CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleName diff --git a/baRSS/Preferences/General Tab/SettingsGeneral.m b/baRSS/Preferences/General Tab/SettingsGeneral.m index 50b7499..98e7c47 100644 --- a/baRSS/Preferences/General Tab/SettingsGeneral.m +++ b/baRSS/Preferences/General Tab/SettingsGeneral.m @@ -21,6 +21,8 @@ // SOFTWARE. #import "SettingsGeneral.h" +#import "AppHook.h" +#import "BarMenu.h" @implementation SettingsGeneral @@ -28,6 +30,29 @@ [super viewDidLoad]; } +- (IBAction)checkmarkClicked:(NSButton*)sender { + // TODO: Could be optimized by updating only the relevant parts + [[(AppHook*)NSApp barMenu] rebuildMenu]; +} + +- (IBAction)changeMenuBarIconSetting:(NSButton*)sender { + [[(AppHook*)NSApp barMenu] updateBarIcon]; +} + +- (IBAction)changeMenuHeaderSetting:(NSButton*)sender { + BOOL recursive = YES; + NSString *bindingKey = [[sender infoForBinding:@"value"] valueForKey:NSObservedKeyPathKey]; + if ([bindingKey containsString:@"values.global"]) { + recursive = NO; // item is in menu bar menu, no need to go recursive + } + [[(AppHook*)NSApp barMenu] updateMenuHeaders:recursive]; +} + +- (IBAction)changeMenuItemUpdateAllHidden:(NSButton*)sender { + BOOL checked = (sender.state == NSControlStateValueOn); + [[(AppHook*)NSApp barMenu] setItemUpdateAllHidden:!checked]; +} + // TODO: show list of installed browsers and make menu choosable @end diff --git a/baRSS/Preferences/General Tab/SettingsGeneral.xib b/baRSS/Preferences/General Tab/SettingsGeneral.xib index 32b617c..712bcd5 100644 --- a/baRSS/Preferences/General Tab/SettingsGeneral.xib +++ b/baRSS/Preferences/General Tab/SettingsGeneral.xib @@ -34,101 +34,16 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + @@ -332,6 +308,15 @@ + + + + + + + + + @@ -341,6 +326,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -377,15 +400,23 @@ - - - - + diff --git a/baRSS/Preferences/General Tab/UserPrefs.h b/baRSS/Preferences/General Tab/UserPrefs.h new file mode 100644 index 0000000..f426f25 --- /dev/null +++ b/baRSS/Preferences/General Tab/UserPrefs.h @@ -0,0 +1,28 @@ +// +// 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 + +@interface UserPrefs : NSObject ++ (BOOL)defaultYES:(NSString*)key; ++ (BOOL)defaultNO:(NSString*)key; +@end diff --git a/baRSS/Preferences/General Tab/UserPrefs.m b/baRSS/Preferences/General Tab/UserPrefs.m new file mode 100644 index 0000000..37f706f --- /dev/null +++ b/baRSS/Preferences/General Tab/UserPrefs.m @@ -0,0 +1,38 @@ +// +// 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 "UserPrefs.h" + +@implementation UserPrefs + ++ (BOOL)defaultYES:(NSString*)key { + if ([[NSUserDefaults standardUserDefaults] objectForKey:key] == NULL) { + return YES; + } + return [[NSUserDefaults standardUserDefaults] boolForKey:key]; +} + ++ (BOOL)defaultNO:(NSString*)key { + return [[NSUserDefaults standardUserDefaults] boolForKey:key]; +} + +@end diff --git a/baRSS/Status Bar Menu/BarMenu.h b/baRSS/Status Bar Menu/BarMenu.h index 5ecb3d1..ad1ff52 100644 --- a/baRSS/Status Bar Menu/BarMenu.h +++ b/baRSS/Status Bar Menu/BarMenu.h @@ -24,4 +24,7 @@ @interface BarMenu : NSObject - (void)rebuildMenu; +- (void)updateBarIcon; +- (void)updateMenuHeaders:(BOOL)recursive; +- (void)setItemUpdateAllHidden:(BOOL)hidden; @end diff --git a/baRSS/Status Bar Menu/BarMenu.m b/baRSS/Status Bar Menu/BarMenu.m index 2550037..0eed31f 100644 --- a/baRSS/Status Bar Menu/BarMenu.m +++ b/baRSS/Status Bar Menu/BarMenu.m @@ -24,27 +24,11 @@ #import "StoreCoordinator.h" #import "DrawImage.h" #import "Preferences.h" -#import "MenuItemInfo.h" +#import "NSMenuItem+Info.h" +#import "UserPrefs.h" @interface BarMenu() -/// @c NSMenuItem options that are assigned to the @c tag attribute. -typedef NS_OPTIONS(NSInteger, MenuItemTag) { - /// Item visible at the very first menu level - ScopeGlobal = 2, - /// Item visible at each grouping, e.g., multiple feeds in one group - ScopeGroup = 4, - /// Item visible at the deepest menu level (@c FeedItem elements and header) - ScopeLocal = 8, - /// - TagPreferences = (1 << 4), - TagPauseUpdates = (2 << 4), - TagUpdateFeed = (3 << 4), - TagMarkAllRead = (4 << 4), - TagMarkAllUnread = (5 << 4), - TagOpenAllUnread = (6 << 4), -}; - @property (strong) NSStatusItem *barItem; @property (strong) Preferences *prefWindow; @property (weak) NSMenu *mm; @@ -58,7 +42,7 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) { self = [super init]; self.barItem = [NSStatusBar.systemStatusBar statusItemWithLength:NSVariableStatusItemLength]; self.barItem.highlightMode = YES; - self.barItem.menu = [self generateMainMenu]; + [self rebuildMenu]; // [self donothing]; return self; } @@ -77,12 +61,11 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) { // TODO: remove debugging stuff - (void)printUnreadRecurisve:(NSMenu*)menu str:(NSString*)prefix { for (NSMenuItem *item in menu.itemArray) { - MenuItemInfo *info = item.representedObject; - if (!info) continue; - id obj = [StoreCoordinator objectWithID:info.objID]; + 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) + else if ([item hasUnread]) NSLog(@"%@ %@ (%d)", prefix, item.title, item.unreadCount); if (item.hasSubmenu) { [self printUnreadRecurisve:item.submenu str:[NSString stringWithFormat:@" %@", prefix]]; @@ -95,14 +78,20 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) { */ - (void)updateBarIcon { // TODO: Option: unread count in menubar, Option: highlight color, Option: icon choice - if (self.unreadCountTotal > 0) { - self.barItem.title = [NSString stringWithFormat:@"%d", self.unreadCountTotal]; - self.barItem.image = [RSSIcon templateIcon:16 tint:[NSColor rssOrange]]; - } else { - self.barItem.title = @""; - self.barItem.image = [RSSIcon templateIcon:16 tint:nil]; - self.barItem.image.template = YES; - } + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.unreadCountTotal > 0 && [UserPrefs defaultYES:@"globalUnreadCount"]) { + self.barItem.title = [NSString stringWithFormat:@"%d", self.unreadCountTotal]; + } else { + self.barItem.title = @""; + } + + if (self.unreadCountTotal > 0 && [UserPrefs defaultYES:@"tintMenuBarIcon"]) { + self.barItem.image = [RSSIcon templateIcon:16 tint:[NSColor rssOrange]]; + } else { + self.barItem.image = [RSSIcon templateIcon:16 tint:nil]; + self.barItem.image.template = YES; + } + }); // NSLog(@"==> %d", self.unreadCountTotal); // [self printUnreadRecurisve:self.barItem.menu str:@""]; } @@ -118,7 +107,10 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) { NSMenu *menu = [NSMenu new]; menu.autoenablesItems = NO; [self addTitle:NSLocalizedString(@"Pause Updates", nil) selector:@selector(pauseUpdates:) toMenu:menu tag:TagPauseUpdates]; - [self addTitle:NSLocalizedString(@"Update all feeds", nil) selector:@selector(updateAllFeeds:) toMenu:menu tag:TagUpdateFeed]; + 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]; @@ -128,11 +120,13 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) { [menu addItem:[self menuItemForFeedConfig:fc unread:&_unreadCountTotal]]; } } + [self updateMenuHeaderEnabled:menu hasUnread:(self.unreadCountTotal > 0)]; [self updateBarIcon]; [menu addItem:[NSMenuItem separatorItem]]; - [self addTitle:NSLocalizedString(@"Preferences", nil) selector:@selector(openPreferences) toMenu:menu tag:TagPreferences]; - menu.itemArray.lastObject.keyEquivalent = @","; + + NSMenuItem *prefs = [self addTitle:NSLocalizedString(@"Preferences", nil) selector:@selector(openPreferences) toMenu:menu tag:TagPreferences]; + prefs.keyEquivalent = @","; [menu addItemWithTitle:NSLocalizedString(@"Quit", nil) action:@selector(terminate:) keyEquivalent:@"q"]; return menu; } @@ -148,7 +142,7 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) { NSMenuItem *item; if (config.typ == SEPARATOR) { item = [NSMenuItem separatorItem]; - item.representedObject = [MenuItemInfo withID:config.objectID]; + [item setReaderInfo:config.objectID unread:0]; return item; } int count = 0; @@ -158,9 +152,10 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) { item = [self groupItem:config unread:&count]; } *unread += count; - item.representedObject = [MenuItemInfo withID:config.objectID]; + [item setReaderInfo:config.objectID unread:0]; + // !!!: fix that double count [item markReadAndUpdateTitle:-count]; - [self updateMenuHeader:item.submenu hasUnread:(count > 0)]; + [self updateMenuHeaderEnabled:item.submenu hasUnread:(count > 0)]; return item; } @@ -171,20 +166,25 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) { @param unread Pointer to an int that will be incremented for each unread item. */ - (NSMenuItem*)feedItem:(FeedConfig*)config unread:(int*)unread { - static NSImage *defaultRSSIcon; - if (!defaultRSSIcon) - defaultRSSIcon = [RSSIcon iconWithSize:16]; - NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:config.name action:@selector(openFeedURL:) keyEquivalent:@""]; item.target = self; - item.submenu = [self defaultHeaderForMenu:nil scope:ScopeLocal]; + item.submenu = [self defaultHeaderForMenu:nil scope:ScopeFeed]; for (FeedItem *obj in config.feed.items) { if (obj.unread) ++(*unread); [item.submenu addItem:[self feedEntryItem:obj]]; } item.toolTip = config.feed.subtitle; item.enabled = (config.feed.items.count > 0); - item.image = defaultRSSIcon; + + // set icon + dispatch_async(dispatch_get_main_queue(), ^{ + static NSImage *defaultRSSIcon; + if (!defaultRSSIcon) + defaultRSSIcon = [RSSIcon iconWithSize:16]; + item.image = defaultRSSIcon; + }); + + item.tag = ScopeFeed; return item; } @@ -195,17 +195,21 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) { @param unread Pointer to an int that will be incremented for each unread item. */ - (NSMenuItem*)groupItem:(FeedConfig*)config unread:(int*)unread { - static NSImage *groupIcon; - if (!groupIcon) { - groupIcon = [NSImage imageNamed:NSImageNameFolder]; - groupIcon.size = NSMakeSize(16, 16); - } NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:config.name action:nil keyEquivalent:@""]; - item.image = groupIcon; item.submenu = [self defaultHeaderForMenu:nil scope:ScopeGroup]; for (FeedConfig *obj in config.sortedChildren) { [item.submenu addItem: [self menuItemForFeedConfig:obj unread:unread]]; } + // set icon + dispatch_async(dispatch_get_main_queue(), ^{ + static NSImage *groupIcon; + if (!groupIcon) { + groupIcon = [NSImage imageNamed:NSImageNameFolder]; + groupIcon.size = NSMakeSize(16, 16); + } + item.image = groupIcon; + }); + item.tag = ScopeGroup; return item; } @@ -215,7 +219,7 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) { - (NSMenuItem*)feedEntryItem:(FeedItem*)item { NSMenuItem *mi = [[NSMenuItem alloc] initWithTitle:item.title action:@selector(openFeedURL:) keyEquivalent:@""]; mi.target = self; - mi.representedObject = [MenuItemInfo withID:item.objectID unread:(item.unread ? 1 : 0)]; + [mi setReaderInfo:item.objectID unread:(item.unread ? 1 : 0)]; //mi.toolTip = item.abstract; // TODO: Do regex during save, not during display. Its here for testing purposes ... if (item.abstract.length > 0) { @@ -224,10 +228,38 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) { } mi.enabled = (item.link.length > 0); mi.state = (item.unread ? NSControlStateValueOn : NSControlStateValueOff); - mi.tag = ScopeLocal; + mi.tag = ScopeFeed; return mi; } +/** + 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 *item = [[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:@""]; + item.target = self; + item.tag = tag; + [item applyUserSettingsDisplay]; + [menu addItem:item]; + return item; +} + +/** + Helper function to copy an existing menu item and set the option key modifier + */ +- (NSMenuItem*)addAlternateItem:(NSMenuItem*)alternateParent withTitle:(NSString*)title toMenu:(NSMenu*)menu { + NSMenuItem *alt = [alternateParent copy]; + alt.title = title; + alt.keyEquivalentModifierMask = NSEventModifierFlagOption; + if (!alt.hidden) // hidden will be ignored if alternate is YES + alt.alternate = YES; + [menu addItem:alt]; + return alt; +} + + +#pragma mark - Default Menu Header Items + /** Append header items to menu accoring to user preferences. @@ -242,35 +274,46 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) { menu = [NSMenu new]; menu.autoenablesItems = NO; } - // TODO: hide items according to preferences + + NSMenuItem *item = [self addTitle:NSLocalizedString(@"Open all unread", nil) selector:@selector(openAllUnread:) toMenu:menu tag:TagOpenAllUnread | scope]; + [self addAlternateItem:item withTitle:[NSString stringWithFormat:NSLocalizedString(@"Open a few unread (%d)", nil), 3] toMenu:menu]; [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]; - [self addTitle:NSLocalizedString(@"Open all unread", nil) selector:@selector(openAllUnread:) toMenu:menu tag:TagOpenAllUnread | scope]; - - NSMenuItem *openSomeUrls = [menu.itemArray.lastObject copy]; - openSomeUrls.title = [NSString stringWithFormat:NSLocalizedString(@"Open a few unread (%d)", nil), 3]; - openSomeUrls.alternate = YES; - openSomeUrls.keyEquivalentModifierMask = NSEventModifierFlagOption; - [menu addItem:openSomeUrls]; [menu addItem:[NSMenuItem separatorItem]]; return menu; } -- (void)updateMenuHeader:(NSMenu*)menu hasUnread:(BOOL)flag { -// [menu itemWithTag:MenuItemTag_FeedMarkAllRead].enabled = flag; -// [menu itemWithTag:MenuItemTag_FeedMarkAllUnread].enabled = !flag; -// [menu itemWithTag:MenuItemTag_FeedOpenAllUnread].enabled = flag; +- (void)setItemUpdateAllHidden:(BOOL)hidden { + [self.barItem.menu itemWithTag:TagUpdateFeed].hidden = hidden; } -/** - Helper function to insert a menu item with @c target @c = @c self - */ -- (void)addTitle:(NSString*)title selector:(SEL)selector toMenu:(NSMenu*)menu tag:(MenuItemTag)tag { - NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:@""]; - item.target = self; - item.tag = tag; - [menu addItem:item]; +- (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 + } } @@ -330,7 +373,7 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) { }]; stopAfter = maxItemCount; int total = [sender siblingsDescendantItemInfo:^int(NSMenuItem *item, int count) { - if (item.tag & ScopeLocal) { + if (item.tag & ScopeFeed) { if (stopAfter <= 0) return -1; --stopAfter; } @@ -386,17 +429,16 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) { @param sender A menu item containing either a @c FeedItem or a @c FeedConfig. */ - (void)openFeedURL:(NSMenuItem*)sender { - MenuItemInfo *info = sender.representedObject; - if (![info isKindOfClass:[MenuItemInfo class]]) return; - - id obj = [StoreCoordinator objectWithID:info.objID]; + if (!sender.hasReaderInfo) + return; NSString *url = nil; + id obj = [sender requestCoreDataObject]; 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 ([sender hasUnread]) { feed.unread = NO; [sender markReadAndUpdateTitle:1]; [self updateAcestors:sender markRead:1]; @@ -421,22 +463,6 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) { #pragma mark - Iterating over items and propagating unread count -/** - Perform a fetch request to the Core Data storage to retrieve the feed item associated with the @c representedObject. - - @param sender The @c NSMenuItem that contains the Core Data reference. - @return Returns @c nil if the menu item has no @c representedObject or the contained class doesn't match. - */ -- (FeedConfig*)requestFeedConfigForMenuItem:(NSMenuItem*)sender { - MenuItemInfo *info = sender.representedObject; - if (![info isKindOfClass:[MenuItemInfo class]]) - return nil; - id obj = [StoreCoordinator objectWithID:info.objID]; - if (![obj isKindOfClass:[FeedConfig class]]) - return nil; - return obj; -} - /** Iterate over all feed items from siblings and contained children. @@ -445,7 +471,9 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) { */ - (void)siblingsDescendantFeedConfigs:(NSMenuItem*)sender block:(FeedConfigRecursiveItemsBlock)block { if (sender.parentItem) { - [[self requestFeedConfigForMenuItem:sender.parentItem] descendantFeedItems:block]; + FeedConfig *obj = [sender 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 { @@ -480,8 +508,8 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) { @return Unread count for parent element (total count if parent is @c nil) */ - (int)getAncestorUnreadCount:(NSMenuItem*)sender { - if ([sender.parentItem.representedObject isKindOfClass:[MenuItemInfo class]]) - return sender.parentItem.unreadCount; + if ([sender.parentItem hasReaderInfo]) + return [sender.parentItem unreadCount]; return self.unreadCountTotal; } diff --git a/baRSS/Status Bar Menu/MenuItemInfo.m b/baRSS/Status Bar Menu/MenuItemInfo.m deleted file mode 100644 index 43910d3..0000000 --- a/baRSS/Status Bar Menu/MenuItemInfo.m +++ /dev/null @@ -1,168 +0,0 @@ -// -// 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 "MenuItemInfo.h" - -@interface MenuItemInfo() -/// internal counter used to sum the unread count of all sub items -@property (assign) int unreadCount; -/// internal flag whether unread count is displayed in parenthesis -@property (assign) BOOL countInTitle; -@end - -@implementation MenuItemInfo -/// @return Info with unreadCount = 0 -+ (instancetype)withID:(NSManagedObjectID*)oid { - return [MenuItemInfo withID:oid unread:0]; -} - -+ (instancetype)withID:(NSManagedObjectID*)oid unread:(int)count { - MenuItemInfo *info = [MenuItemInfo new]; - info.objID = oid; - info.unreadCount = count; - return info; -} - -/// @return @c YES if (unreadCount > 0) -- (BOOL)hasUnread { - return self.unreadCount > 0; -} - -/// set: unreadCount -= count -- (void)markRead:(int)count { - if (count > self.unreadCount) { - NSLog(@"should never happen, trying to set an unread count below zero"); - self.unreadCount = 0; - } else { - self.unreadCount -= count; - } -} - -@end - - - -@implementation NSMenuItem (MenuItemInfo) - -/** Call represented object and check whether unread count > 0. */ -- (BOOL)hasUnread { - return [self.representedObject unreadCount] > 0; -} - -/** Call represented object and retrieve the unread count from info. */ -- (int)unreadCount { - return [self.representedObject unreadCount]; -} - -/** - Update internal unread counter and append unread count to title. - - @note Count may be negative to mark items as unread. - @warning Does not check if @c representedObject is set accordingly - @param count The amount by which the counter is adjusted. - If negative the items will be marked as unread. - */ -- (void)markReadAndUpdateTitle:(int)count { - if (count == 0) return; // 0 won't change anything - MenuItemInfo *info = self.representedObject; - if (!self.hasSubmenu) { - [info markRead:count]; - self.state = (info.hasUnread ? NSControlStateValueOn : NSControlStateValueOff); - } else { - int countBefore = info.unreadCount; - [info markRead:count]; - if (info.countInTitle) { - int digitsBefore = (int)log10f(countBefore) + 1; - NSInteger index = (NSInteger)self.title.length - digitsBefore - 3; // " (%d)" - if (index < 0) index = 0; - self.title = [self.title substringToIndex:(NSUInteger)index]; // remove old count - info.countInTitle = NO; - } - if (info.unreadCount > 0) { - self.title = [self.title stringByAppendingFormat:@" (%d)", info.unreadCount]; - info.countInTitle = YES; - } - } -} - -/** - Recursively propagate unread count to ancestor menu items. - - @note Does not update the current item, only the ancestors. - @param count The amount by which the counter is adjusted. - If negative the items will be marked as unread. - */ -- (void)markAncestorsRead:(int)count { - NSMenuItem *parent = self.parentItem; - while (parent.representedObject) { - [parent markReadAndUpdateTitle:count]; - parent = parent.parentItem; - } -} - -/** - Recursively iterate over submenues and children. Count aggregated element edits. - - @warning Block will be called for parent items, too. Consider this when using counters. - @param block Will be called for each @c NSMenuItem sub-element where @c representedObject is set to a @c MenuItemInfo. - Return -1 to stop processing early. - @param flag If set to @c YES, recursive calls will be skipped for submenus that contain soleily read elements. - @return The number of changed elements in total. - */ -- (int)descendantItemInfo:(MenuItemInfoRecursiveBlock)block unreadEntriesOnly:(BOOL)flag { - MenuItemInfo *info = self.representedObject; - if (![info isKindOfClass:[MenuItemInfo class]]) return 0; - if (flag && !info.hasUnread) return 0; - if (self.isSeparatorItem) return 0; - - int countItems = 1; // deepest entry, FeedItem - if (self.hasSubmenu) { - countItems = 0; - for (NSMenuItem *child in self.submenu.itemArray) { - int c = [child descendantItemInfo:block unreadEntriesOnly:flag]; - if (c < 0) break; - countItems += c; - } - } - return block(self, countItems); -} - -/** - Recursively iterate over siblings and all contained children. Count aggregated element edits. - - @warning Block will be called for parent items, too. Consider this when using counters. - @param block Will be called for each @c NSMenuItem sub-element where @c representedObject is set to a @c MenuItemInfo. - Return -1 to stop processing early. - @param flag If set to @c YES, recursive calls will be skipped for submenus that contain soleily read elements. - @return The number of changed elements in total. - */ -- (int)siblingsDescendantItemInfo:(MenuItemInfoRecursiveBlock)block unreadEntriesOnly:(BOOL)flag { - int markedTotal = 0; - for (NSMenuItem *sibling in self.menu.itemArray) { - int marked = [sibling descendantItemInfo:block unreadEntriesOnly:flag]; - if (marked < 0) break; - markedTotal += marked; - } - return markedTotal; -} - -@end diff --git a/baRSS/Status Bar Menu/MenuItemInfo.h b/baRSS/Status Bar Menu/NSMenuItem+Info.h similarity index 59% rename from baRSS/Status Bar Menu/MenuItemInfo.h rename to baRSS/Status Bar Menu/NSMenuItem+Info.h index a36b0b2..f2570ad 100644 --- a/baRSS/Status Bar Menu/MenuItemInfo.h +++ b/baRSS/Status Bar Menu/NSMenuItem+Info.h @@ -22,28 +22,46 @@ #import -@interface MenuItemInfo : NSObject -@property (strong) NSManagedObjectID *objID; -+ (instancetype)withID:(NSManagedObjectID*)oid; -+ (instancetype)withID:(NSManagedObjectID*)oid unread:(int)count; -@end +/// @c NSMenuItem options that are assigned to the @c tag attribute. +typedef NS_OPTIONS(NSInteger, MenuItemTag) { + /// Item visible at the very first menu level + ScopeGlobal = 2, + /// Item visible at each grouping, e.g., multiple feeds in one group + ScopeGroup = 4, + /// Item visible at the deepest menu level (@c FeedItem elements and header) + ScopeFeed = 8, + /// + TagPreferences = (1 << 4), + TagPauseUpdates = (2 << 4), + TagUpdateFeed = (3 << 4), + TagMarkAllRead = (4 << 4), + TagMarkAllUnread = (5 << 4), + TagOpenAllUnread = (6 << 4), + + TagMaskScope = 0xF, + TagMaskType = 0xFFF0, +}; -@interface NSMenuItem (MenuItemInfo) +@interface NSMenuItem (Info) /** Iteration block for descendants of @c NSMenuItem. - + @param count The number of sub-elements contained in that @c NSMenuItem. 1 for @c FeedItems at the deepest layer. - Otherwise the number of (updated) descendants. + Otherwise the number of (updated) descendants. @return Return how many elements are updated in this block execution. If none were changed return @c 0. - If execution should be stopped early, return @c -1. + If execution should be stopped early, return @c -1. */ -typedef int (^MenuItemInfoRecursiveBlock) (NSMenuItem *item, int count); +typedef int (^ReaderInfoRecursiveBlock) (NSMenuItem *item, int count); - (BOOL)hasUnread; - (int)unreadCount; +- (BOOL)hasReaderInfo; +- (void)setReaderInfo:(NSManagedObjectID*)oid unread:(int)count; +- (id)requestCoreDataObject; +- (void)applyUserSettingsDisplay; - (void)markReadAndUpdateTitle:(int)count; +- (void)countInTitle:(BOOL)show; - (void)markAncestorsRead:(int)count; -- (int)siblingsDescendantItemInfo:(MenuItemInfoRecursiveBlock)block unreadEntriesOnly:(BOOL)flag; +- (int)siblingsDescendantItemInfo:(ReaderInfoRecursiveBlock)block unreadEntriesOnly:(BOOL)flag; @end - diff --git a/baRSS/Status Bar Menu/NSMenuItem+Info.m b/baRSS/Status Bar Menu/NSMenuItem+Info.m new file mode 100644 index 0000000..5cb01a8 --- /dev/null +++ b/baRSS/Status Bar Menu/NSMenuItem+Info.m @@ -0,0 +1,275 @@ +// +// 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 "NSMenuItem+Info.h" +#import "UserPrefs.h" +#import "StoreCoordinator.h" + +/// User preferences for displaying menu items +typedef NS_ENUM(char, DisplaySetting) { + /// User preference not available. @c NSMenuItem is not configurable (not a header item) + INVALID, + /// User preference to display this item + ALLOW, + /// User preference to hide this item + PROHIBIT +}; + +@interface ReaderInfo : NSObject +@property (strong) NSManagedObjectID *objID; +/// internal counter used to sum the unread count of all sub items +@property (assign) int unreadCount; +/// internal flag whether unread count is displayed in parenthesis +@property (assign) BOOL countInTitle; +@end + +@implementation ReaderInfo +/// set: unreadCount -= count +- (void)markRead:(int)count { + if (count > self.unreadCount) { + NSLog(@"should never happen, trying to set an unread count below zero"); + self.unreadCount = 0; + } else { + self.unreadCount -= count; + } +} +@end + + +// ################################################################ +// # +// # NSMenuItem ReaderInfo Extension +// # +// ################################################################ + +@implementation NSMenuItem (Info) +/** Call represented object and check whether unread count > 0. */ +- (BOOL)hasUnread { + return [(ReaderInfo*)self.representedObject unreadCount] > 0; +} + +/** Call represented object and retrieve the unread count from info. */ +- (int)unreadCount { + return [(ReaderInfo*)self.representedObject unreadCount]; +} + +/** Return @c YES if @c ReaderInfo is stored in @c representedObject. */ +- (BOOL)hasReaderInfo { + return [self.representedObject isKindOfClass:[ReaderInfo class]]; +} + +/** + Save represented core data object in @c ReaderInfo. + + @param oid Represented core data object id. + @param count Unread count for item. + */ +- (void)setReaderInfo:(NSManagedObjectID*)oid unread:(int)count { + ReaderInfo *info = [ReaderInfo new]; + info.objID = oid; + info.unreadCount = count; + self.representedObject = info; +} + +/** + Return represented core data object. Return @c nil if @c ReaderInfo is missing. + */ +- (id)requestCoreDataObject { + if (![self hasReaderInfo]) + return nil; + return [StoreCoordinator objectWithID: [(ReaderInfo*)self.representedObject objID]]; +} + +/** + Check user preferences for preferred display style. + + @return As per user settings return @c ALLOW or @c PROHIBIT. Will return @c INVALID for items that aren't configurable. + */ +- (DisplaySetting)allowsDisplay { + NSString *prefix; + switch (self.tag & TagMaskScope) { + case ScopeFeed: prefix = @"feed"; break; + case ScopeGroup: prefix = @"group"; break; + case ScopeGlobal: prefix = @"global"; break; + default: return INVALID; // no scope, not recognized menu item + } + NSString *postfix; + switch (self.tag & TagMaskType) { + case TagOpenAllUnread: postfix = @"OpenUnread"; break; + case TagMarkAllRead: postfix = @"MarkRead"; break; + case TagMarkAllUnread: postfix = @"MarkUnread"; break; + default: return INVALID; // wrong tag, ignore + } + + if ([UserPrefs defaultYES:[prefix stringByAppendingString:postfix]]) + return ALLOW; + return PROHIBIT; +} + +/** + Set item @c hidden based on user preferences. Does nothing for items that aren't configurable in settings. + */ +- (void)applyUserSettingsDisplay { + switch ([self allowsDisplay]) { + case ALLOW: + self.hidden = NO; + if (self.keyEquivalentModifierMask == NSEventModifierFlagOption) + self.alternate = YES; // restore alternate flag + break; + case PROHIBIT: + if (self.isAlternate) + self.alternate = NO; // to allow hidden = YES, alternate flag needs to be NO + self.hidden = YES; + break; + case INVALID: break; + } +} + +/** + Update internal unread counter and append unread count to title. + + @note Count may be negative to mark items as unread. + @warning Does not check if @c representedObject is set accordingly + @param count The amount by which the counter is adjusted. + If negative the items will be marked as unread. + */ +- (void)markReadAndUpdateTitle:(int)count { + if (count == 0) return; // 0 won't change anything + ReaderInfo *info = self.representedObject; + if (!self.hasSubmenu) { + [info markRead:count]; + self.state = ([self hasUnread] ? NSControlStateValueOn : NSControlStateValueOff); + } else { + int countBefore = info.unreadCount; + [info markRead:count]; + if (info.countInTitle) { + [self removeUnreadCountFromTitle:countBefore]; + info.countInTitle = NO; + } + [self addUnreadCountToTitle]; + } +} + +/** + Update title without changing internal unread count. Save to call multiple times. + + @param show Whether to show or hide count + */ +- (void)countInTitle:(BOOL)show { + ReaderInfo *info = self.representedObject; + NSLog(@"%@", info); + return; + if (!show && info.countInTitle) { + [self removeUnreadCountFromTitle: info.unreadCount]; + info.countInTitle = NO; + } else if (show && !info.countInTitle) { + [self addUnreadCountToTitle]; + } +} + +/** + Update title after unread count has changed + + @param countBefore The count before the update + */ +- (void)removeUnreadCountFromTitle:(int)countBefore { + int digitsBefore = (int)log10f(countBefore) + 1; + NSInteger index = (NSInteger)self.title.length - digitsBefore - 3; // " (%d)" + if (index < 0) index = 0; + self.title = [self.title substringToIndex:(NSUInteger)index]; // remove old count +} + +/** + Append count in parenthesis if thats allowed for the current scope (user settings) + */ +- (void)addUnreadCountToTitle { + ReaderInfo *info = self.representedObject; + if (info.unreadCount > 0 && + (((self.tag & ScopeGroup) && [UserPrefs defaultYES:@"groupUnreadCount"]) || + ((self.tag & ScopeFeed) && [UserPrefs defaultYES:@"feedUnreadCount"]))) + { + self.title = [self.title stringByAppendingFormat:@" (%d)", info.unreadCount]; + info.countInTitle = YES; + } +} + +/** + Recursively propagate unread count to ancestor menu items. + + @note Does not update the current item, only the ancestors. + @param count The amount by which the counter is adjusted. + If negative the items will be marked as unread. + */ +- (void)markAncestorsRead:(int)count { + NSMenuItem *parent = self.parentItem; + while (parent.representedObject) { + [parent markReadAndUpdateTitle:count]; + parent = parent.parentItem; + } +} + +/** + Recursively iterate over submenues and children. Count aggregated element edits. + + @warning Block will be called for parent items, too. Consider this when using counters. + @param block Will be called for each @c NSMenuItem sub-element where @c representedObject is set to a @c ReaderInfo. + Return -1 to stop processing early. + @param flag If set to @c YES, recursive calls will be skipped for submenus that contain soleily read elements. + @return The number of changed elements in total. + */ +- (int)descendantItemInfo:(ReaderInfoRecursiveBlock)block unreadEntriesOnly:(BOOL)flag { + if (self.isSeparatorItem) return 0; + if (![self hasReaderInfo]) return 0; + if (flag && ![self hasUnread]) return 0; + + int countItems = 1; // deepest entry, FeedItem + if (self.hasSubmenu) { + countItems = 0; + for (NSMenuItem *child in self.submenu.itemArray) { + int c = [child descendantItemInfo:block unreadEntriesOnly:flag]; + if (c < 0) break; + countItems += c; + } + } + return block(self, countItems); +} + +/** + Recursively iterate over siblings and all contained children. Count aggregated element edits. + + @warning Block will be called for parent items, too. Consider this when using counters. + @param block Will be called for each @c NSMenuItem sub-element where @c representedObject is set to a @c ReaderInfo. + Return -1 to stop processing early. + @param flag If set to @c YES, recursive calls will be skipped for submenus that contain soleily read elements. + @return The number of changed elements in total. + */ +- (int)siblingsDescendantItemInfo:(ReaderInfoRecursiveBlock)block unreadEntriesOnly:(BOOL)flag { + int markedTotal = 0; + for (NSMenuItem *sibling in self.menu.itemArray) { + int marked = [sibling descendantItemInfo:block unreadEntriesOnly:flag]; + if (marked < 0) break; + markedTotal += marked; + } + return markedTotal; +} + +@end