Open some feeds functionality
This commit is contained in:
@@ -28,15 +28,23 @@
|
|||||||
|
|
||||||
|
|
||||||
@interface BarMenu()
|
@interface BarMenu()
|
||||||
|
/// @c NSMenuItem options that are assigned to the @c tag attribute.
|
||||||
typedef NS_OPTIONS(NSInteger, MenuItemTag) {
|
typedef NS_OPTIONS(NSInteger, MenuItemTag) {
|
||||||
|
/// Item visible at the very first menu level
|
||||||
ScopeGlobal = 1,
|
ScopeGlobal = 1,
|
||||||
ScopeGroup = (1<<1),
|
/// Item visible at each grouping, e.g., multiple feeds in one group
|
||||||
ScopeLocal = (1<<2),
|
ScopeGroup = 2,
|
||||||
PauseUpdates = (1<<3),
|
/// Item visible at the deepest menu level (@c FeedItem elements and header)
|
||||||
UpdateFeed = (1<<4),
|
ScopeLocal = 4,
|
||||||
MarkAllRead = (1<<5),
|
/// @c NSMenuItem is an alternative
|
||||||
MarkAllUnread = (1<<6),
|
ScopeAlternative = 8,
|
||||||
OpenAllUnread = (1<<7),
|
///
|
||||||
|
TagPreferences = (1 << 4),
|
||||||
|
TagPauseUpdates = (2 << 4),
|
||||||
|
TagUpdateFeed = (3 << 4),
|
||||||
|
TagMarkAllRead = (4 << 4),
|
||||||
|
TagMarkAllUnread = (5 << 4),
|
||||||
|
TagOpenAllUnread = (6 << 4),
|
||||||
};
|
};
|
||||||
|
|
||||||
@property (strong) NSStatusItem *barItem;
|
@property (strong) NSStatusItem *barItem;
|
||||||
@@ -66,6 +74,9 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
|
|||||||
[self performSelectorInBackground:@selector(donothing) withObject:nil];
|
[self performSelectorInBackground:@selector(donothing) withObject:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Update menu bar icon and text according to unread count and user preferences.
|
||||||
|
*/
|
||||||
- (void)updateBarIcon {
|
- (void)updateBarIcon {
|
||||||
// TODO: Option: unread count in menubar, Option: highlight color, Option: icon choice
|
// TODO: Option: unread count in menubar, Option: highlight color, Option: icon choice
|
||||||
if (self.unreadCountTotal > 0) {
|
if (self.unreadCountTotal > 0) {
|
||||||
@@ -82,11 +93,14 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
|
|||||||
#pragma mark - Menu Generator
|
#pragma mark - Menu Generator
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
Builds main menu with items on the very first menu level. Including Preferences, Quit, etc.
|
||||||
|
*/
|
||||||
- (NSMenu*)mainMenu {
|
- (NSMenu*)mainMenu {
|
||||||
NSMenu *menu = [NSMenu new];
|
NSMenu *menu = [NSMenu new];
|
||||||
menu.autoenablesItems = NO;
|
menu.autoenablesItems = NO;
|
||||||
[self addTitle:NSLocalizedString(@"Pause Updates", nil) selector:@selector(pauseUpdates:) key:@"" toMenu:menu tag:PauseUpdates];
|
[self addTitle:NSLocalizedString(@"Pause Updates", nil) selector:@selector(pauseUpdates:) toMenu:menu tag:TagPauseUpdates];
|
||||||
[self addTitle:NSLocalizedString(@"Update all feeds", nil) selector:@selector(updateAllFeeds:) key:@"" toMenu:menu tag:UpdateFeed];
|
[self addTitle:NSLocalizedString(@"Update all feeds", nil) selector:@selector(updateAllFeeds:) toMenu:menu tag:TagUpdateFeed];
|
||||||
[menu addItem:[NSMenuItem separatorItem]];
|
[menu addItem:[NSMenuItem separatorItem]];
|
||||||
[self defaultHeaderForMenu:menu scope:ScopeGlobal];
|
[self defaultHeaderForMenu:menu scope:ScopeGlobal];
|
||||||
|
|
||||||
@@ -95,66 +109,87 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[menu addItem:[NSMenuItem separatorItem]];
|
[menu addItem:[NSMenuItem separatorItem]];
|
||||||
[self addTitle:NSLocalizedString(@"Preferences", nil) selector:@selector(openPreferences) key:@"," toMenu:menu tag:0];
|
[self addTitle:NSLocalizedString(@"Preferences", nil) selector:@selector(openPreferences) toMenu:menu tag:TagPreferences];
|
||||||
|
menu.itemArray.lastObject.keyEquivalent = @",";
|
||||||
[menu addItemWithTitle:NSLocalizedString(@"Quit", nil) action:@selector(terminate:) keyEquivalent:@"q"];
|
[menu addItemWithTitle:NSLocalizedString(@"Quit", nil) action:@selector(terminate:) keyEquivalent:@"q"];
|
||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSMenuItem*)menuItemForFeedConfig:(FeedConfig*)fc unread:(int*)unread {
|
/**
|
||||||
|
Create and return a new @c NSMenuItem from the objects attributes.
|
||||||
|
|
||||||
|
@param config @c FeedConfig object that represents a superior feed element.
|
||||||
|
@param unread Pointer to an int that will be incremented for each unread item.
|
||||||
|
@return Return a fully configured Separator item OR group item OR feed item. (but not @c FeedItem item)
|
||||||
|
*/
|
||||||
|
- (NSMenuItem*)menuItemForFeedConfig:(FeedConfig*)config unread:(int*)unread {
|
||||||
NSMenuItem *item;
|
NSMenuItem *item;
|
||||||
if (fc.typ == SEPARATOR) {
|
if (config.typ == SEPARATOR) {
|
||||||
item = [NSMenuItem separatorItem];
|
item = [NSMenuItem separatorItem];
|
||||||
item.representedObject = [MenuItemInfo withID:fc.objectID];
|
item.representedObject = [MenuItemInfo withID:config.objectID];
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
int count = 0;
|
int count = 0;
|
||||||
if (fc.typ == FEED) {
|
if (config.typ == FEED) {
|
||||||
item = [self feedItem:fc unread:&count];
|
item = [self feedItem:config unread:&count];
|
||||||
} else if (fc.typ == GROUP) {
|
} else if (config.typ == GROUP) {
|
||||||
item = [self groupItem:fc unread:&count];
|
item = [self groupItem:config unread:&count];
|
||||||
}
|
}
|
||||||
*unread += count;
|
*unread += count;
|
||||||
item.representedObject = [MenuItemInfo withID:fc.objectID];
|
item.representedObject = [MenuItemInfo withID:config.objectID];
|
||||||
[item markReadAndUpdateTitle:-count];
|
[item markReadAndUpdateTitle:-count];
|
||||||
[self updateMenuHeader:item.submenu hasUnread:(count > 0)];
|
[self updateMenuHeader:item.submenu hasUnread:(count > 0)];
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSMenuItem*)feedItem:(FeedConfig*)fc unread:(int*)unread {
|
/**
|
||||||
|
Create and return a new @c NSMenuItem from the objects attributes.
|
||||||
|
|
||||||
|
@param config @c FeedConfig object that represents a superior feed element.
|
||||||
|
@param unread Pointer to an int that will be incremented for each unread item.
|
||||||
|
*/
|
||||||
|
- (NSMenuItem*)feedItem:(FeedConfig*)config unread:(int*)unread {
|
||||||
static NSImage *defaultRSSIcon;
|
static NSImage *defaultRSSIcon;
|
||||||
if (!defaultRSSIcon)
|
if (!defaultRSSIcon)
|
||||||
defaultRSSIcon = [[[RSSIcon iconWithSize:NSMakeSize(16, 16)] autoGradient] image];
|
defaultRSSIcon = [[[RSSIcon iconWithSize:NSMakeSize(16, 16)] autoGradient] image];
|
||||||
|
|
||||||
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:fc.name action:@selector(openFeedURL:) keyEquivalent:@""];
|
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:config.name action:@selector(openFeedURL:) keyEquivalent:@""];
|
||||||
item.target = self;
|
item.target = self;
|
||||||
item.submenu = [self defaultHeaderForMenu:nil scope:ScopeLocal];
|
item.submenu = [self defaultHeaderForMenu:nil scope:ScopeLocal];
|
||||||
for (FeedItem *obj in fc.feed.items) {
|
for (FeedItem *obj in config.feed.items) {
|
||||||
if (obj.unread) ++(*unread);
|
if (obj.unread) ++(*unread);
|
||||||
[item.submenu addItem:[self feedEntryItem:obj]];
|
[item.submenu addItem:[self feedEntryItem:obj]];
|
||||||
}
|
}
|
||||||
item.toolTip = fc.feed.subtitle;
|
item.toolTip = config.feed.subtitle;
|
||||||
item.enabled = (fc.feed.items.count > 0);
|
item.enabled = (config.feed.items.count > 0);
|
||||||
item.image = defaultRSSIcon;
|
item.image = defaultRSSIcon;
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSMenuItem*)groupItem:(FeedConfig*)fc unread:(int*)unread {
|
/**
|
||||||
|
Create and return a new @c NSMenuItem from the objects attributes.
|
||||||
|
|
||||||
|
@param config @c FeedConfig object that represents a group item.
|
||||||
|
@param unread Pointer to an int that will be incremented for each unread item.
|
||||||
|
*/
|
||||||
|
- (NSMenuItem*)groupItem:(FeedConfig*)config unread:(int*)unread {
|
||||||
static NSImage *groupIcon;
|
static NSImage *groupIcon;
|
||||||
if (!groupIcon) {
|
if (!groupIcon) {
|
||||||
groupIcon = [NSImage imageNamed:NSImageNameFolder];
|
groupIcon = [NSImage imageNamed:NSImageNameFolder];
|
||||||
groupIcon.size = NSMakeSize(16, 16);
|
groupIcon.size = NSMakeSize(16, 16);
|
||||||
}
|
}
|
||||||
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:fc.name action:nil keyEquivalent:@""];
|
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:config.name action:nil keyEquivalent:@""];
|
||||||
item.image = groupIcon;
|
item.image = groupIcon;
|
||||||
item.submenu = [self defaultHeaderForMenu:nil scope:ScopeGroup];
|
item.submenu = [self defaultHeaderForMenu:nil scope:ScopeGroup];
|
||||||
for (FeedConfig *obj in fc.sortedChildren) {
|
for (FeedConfig *obj in config.sortedChildren) {
|
||||||
NSMenuItem *subItem = [self menuItemForFeedConfig:obj unread:unread];
|
[item.submenu addItem: [self menuItemForFeedConfig:obj unread:unread]];
|
||||||
// *unread += [(MenuItemInfo*)subItem.representedObject unreadCount];
|
|
||||||
[item.submenu addItem:subItem];
|
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Create and return a new @c NSMenuItem from @c FeedItem attributes.
|
||||||
|
*/
|
||||||
- (NSMenuItem*)feedEntryItem:(FeedItem*)item {
|
- (NSMenuItem*)feedEntryItem:(FeedItem*)item {
|
||||||
NSMenuItem *mi = [[NSMenuItem alloc] initWithTitle:item.title action:@selector(openFeedURL:) keyEquivalent:@""];
|
NSMenuItem *mi = [[NSMenuItem alloc] initWithTitle:item.title action:@selector(openFeedURL:) keyEquivalent:@""];
|
||||||
mi.target = self;
|
mi.target = self;
|
||||||
@@ -181,9 +216,15 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
|
|||||||
menu.autoenablesItems = NO;
|
menu.autoenablesItems = NO;
|
||||||
}
|
}
|
||||||
// TODO: hide items according to preferences
|
// TODO: hide items according to preferences
|
||||||
[self addTitle:NSLocalizedString(@"Mark all read", nil) selector:@selector(markAllRead:) key:@"" toMenu:menu tag:MarkAllRead | scope];
|
[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:) key:@"" toMenu:menu tag:MarkAllUnread | 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:) key:@"" toMenu:menu tag:OpenAllUnread | scope];
|
[self addTitle:NSLocalizedString(@"Open all unread", nil) selector:@selector(openAllUnread:) toMenu:menu tag:TagOpenAllUnread | scope];
|
||||||
|
|
||||||
|
NSString *alternateTitle = [NSString stringWithFormat:NSLocalizedString(@"Open a few unread (%d)", nil), 3];
|
||||||
|
[self addTitle:alternateTitle selector:@selector(openAllUnread:) toMenu:menu tag:TagOpenAllUnread | scope | ScopeAlternative];
|
||||||
|
menu.itemArray.lastObject.alternate = YES;
|
||||||
|
menu.itemArray.lastObject.keyEquivalentModifierMask = NSEventModifierFlagOption;
|
||||||
|
|
||||||
[menu addItem:[NSMenuItem separatorItem]];
|
[menu addItem:[NSMenuItem separatorItem]];
|
||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
@@ -194,26 +235,23 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
|
|||||||
// [menu itemWithTag:MenuItemTag_FeedOpenAllUnread].enabled = flag;
|
// [menu itemWithTag:MenuItemTag_FeedOpenAllUnread].enabled = flag;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)addTitle:(NSString*)title selector:(SEL)selector key:(NSString*)key toMenu:(NSMenu*)menu tag:(MenuItemTag)tag {
|
/**
|
||||||
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:key];
|
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.target = self;
|
||||||
item.tag = tag;
|
item.tag = tag;
|
||||||
[menu addItem:item];
|
[menu addItem:item];
|
||||||
}
|
}
|
||||||
|
|
||||||
//- (NSIndexPath*)indexPathForMenu:(NSMenu*)menu {
|
|
||||||
// NSMenu *parent = menu.supermenu;
|
|
||||||
// if (parent == nil) {
|
|
||||||
// return [NSIndexPath new];
|
|
||||||
// } else {
|
|
||||||
// return [[self indexPathForMenu:parent] indexPathByAddingIndex:(NSUInteger)[parent indexOfItemWithSubmenu:menu]];
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - Menu Actions
|
#pragma mark - Menu Actions
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
Called whenever the user activates the preferences (either through menu click or hotkey)
|
||||||
|
*/
|
||||||
- (void)openPreferences {
|
- (void)openPreferences {
|
||||||
if (!self.prefWindow) {
|
if (!self.prefWindow) {
|
||||||
self.prefWindow = [[Preferences alloc] initWithWindowNibName:@"Preferences"];
|
self.prefWindow = [[Preferences alloc] initWithWindowNibName:@"Preferences"];
|
||||||
@@ -239,64 +277,86 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
|
|||||||
NSLog(@"1update all");
|
NSLog(@"1update all");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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")
|
||||||
|
*/
|
||||||
- (void)openAllUnread:(NSMenuItem*)sender {
|
- (void)openAllUnread:(NSMenuItem*)sender {
|
||||||
__block int maxItemCount = INT_MAX;
|
int maxItemCount = INT_MAX;
|
||||||
|
if (sender.isAlternate)
|
||||||
|
maxItemCount = 3; // TODO: read from preferences
|
||||||
|
|
||||||
|
__block int stopAfter = maxItemCount;
|
||||||
NSMutableArray<NSURL*> *urls = [NSMutableArray<NSURL*> array];
|
NSMutableArray<NSURL*> *urls = [NSMutableArray<NSURL*> array];
|
||||||
[self siblingsDescendantFeedConfigs:sender block:^BOOL(FeedConfig *parent, FeedItem *item) {
|
[self siblingsDescendantFeedConfigs:sender block:^BOOL(FeedConfig *parent, FeedItem *item) {
|
||||||
if (maxItemCount <= 0)
|
if (stopAfter <= 0)
|
||||||
return NO; // stop further processing
|
return NO; // stop further processing
|
||||||
if (item.unread && item.link.length > 0) {
|
if (item.unread && item.link.length > 0) {
|
||||||
[urls addObject:[NSURL URLWithString:item.link]];
|
[urls addObject:[NSURL URLWithString:item.link]];
|
||||||
item.unread = NO;
|
item.unread = NO;
|
||||||
--maxItemCount;
|
--stopAfter;
|
||||||
}
|
}
|
||||||
return YES;
|
return YES;
|
||||||
}];
|
}];
|
||||||
maxItemCount = INT_MAX;
|
stopAfter = maxItemCount;
|
||||||
int total = [sender siblingsDescendantItemInfo:^int(NSMenuItem *item, MenuItemInfo *info, int count) {
|
int total = [sender siblingsDescendantItemInfo:^int(NSMenuItem *item, int count) {
|
||||||
if (maxItemCount <= 0)
|
if (item.tag & ScopeLocal) {
|
||||||
return -1; // stop further processing
|
if (stopAfter <= 0) return -1;
|
||||||
if (info.hasUnread) {
|
--stopAfter;
|
||||||
[item markReadAndUpdateTitle:count];
|
|
||||||
--maxItemCount;
|
|
||||||
return count;
|
|
||||||
}
|
}
|
||||||
return 0;
|
[item markReadAndUpdateTitle:count];
|
||||||
|
return count;
|
||||||
} unreadEntriesOnly:YES];
|
} unreadEntriesOnly:YES];
|
||||||
[self updateAcestors:sender markRead:total];
|
[self updateAcestors:sender markRead:total];
|
||||||
[self openURLsWithPreferredBrowser:urls];
|
[self openURLsWithPreferredBrowser:urls];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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")
|
||||||
|
*/
|
||||||
- (void)markAllRead:(NSMenuItem*)sender {
|
- (void)markAllRead:(NSMenuItem*)sender {
|
||||||
[self siblingsDescendantFeedConfigs:sender block:^BOOL(FeedConfig *parent, FeedItem *item) {
|
[self siblingsDescendantFeedConfigs:sender block:^BOOL(FeedConfig *parent, FeedItem *item) {
|
||||||
if (item.unread)
|
if (item.unread)
|
||||||
item.unread = NO;
|
item.unread = NO;
|
||||||
return YES;
|
return YES;
|
||||||
}];
|
}];
|
||||||
int total = [sender siblingsDescendantItemInfo:^int(NSMenuItem *item, MenuItemInfo *info, int count) {
|
int total = [sender siblingsDescendantItemInfo:^int(NSMenuItem *item, int count) {
|
||||||
if (info.hasUnread) {
|
[item markReadAndUpdateTitle:count];
|
||||||
[item markReadAndUpdateTitle:count];
|
return count;
|
||||||
return count;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
} unreadEntriesOnly:YES];
|
} unreadEntriesOnly:YES];
|
||||||
[self updateAcestors:sender markRead:total];
|
[self updateAcestors:sender markRead:total];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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 {
|
- (void)markAllUnread:(NSMenuItem*)sender {
|
||||||
[self siblingsDescendantFeedConfigs:sender block:^BOOL(FeedConfig *parent, FeedItem *item) {
|
[self siblingsDescendantFeedConfigs:sender block:^BOOL(FeedConfig *parent, FeedItem *item) {
|
||||||
if (item.unread == NO)
|
if (item.unread == NO)
|
||||||
item.unread = YES;
|
item.unread = YES;
|
||||||
return YES;
|
return YES;
|
||||||
}];
|
}];
|
||||||
int total = [sender siblingsDescendantItemInfo:^int(NSMenuItem *item, MenuItemInfo *info, int count) {
|
int total = [sender siblingsDescendantItemInfo:^int(NSMenuItem *item, int count) {
|
||||||
if (count > info.unreadCount)
|
if (count > item.unreadCount)
|
||||||
[item markReadAndUpdateTitle:(info.unreadCount - count)];
|
[item markReadAndUpdateTitle:(item.unreadCount - count)];
|
||||||
return count;
|
return count;
|
||||||
} unreadEntriesOnly:NO];
|
} unreadEntriesOnly:NO];
|
||||||
[self updateAcestors:sender markRead:([self getAncestorUnreadCount:sender] - total)];
|
[self updateAcestors:sender markRead:([self getAncestorUnreadCount:sender] - total)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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.
|
||||||
|
*/
|
||||||
- (void)openFeedURL:(NSMenuItem*)sender {
|
- (void)openFeedURL:(NSMenuItem*)sender {
|
||||||
MenuItemInfo *info = sender.representedObject;
|
MenuItemInfo *info = sender.representedObject;
|
||||||
if (![info isKindOfClass:[MenuItemInfo class]]) return;
|
if (![info isKindOfClass:[MenuItemInfo class]]) return;
|
||||||
@@ -308,7 +368,7 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
|
|||||||
} else if ([obj isKindOfClass:[FeedItem class]]) {
|
} else if ([obj isKindOfClass:[FeedItem class]]) {
|
||||||
FeedItem *feed = obj;
|
FeedItem *feed = obj;
|
||||||
url = [feed link];
|
url = [feed link];
|
||||||
if (info.hasUnread) {
|
if (sender.hasUnread) {
|
||||||
feed.unread = NO;
|
feed.unread = NO;
|
||||||
[sender markReadAndUpdateTitle:1];
|
[sender markReadAndUpdateTitle:1];
|
||||||
[self updateAcestors:sender markRead:1];
|
[self updateAcestors:sender markRead:1];
|
||||||
@@ -318,7 +378,13 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
|
|||||||
[self openURLsWithPreferredBrowser:@[[NSURL URLWithString:url]]];
|
[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 {
|
- (void)openURLsWithPreferredBrowser:(NSArray<NSURL*>*)urls {
|
||||||
|
// TODO: lookup preferred browser in user preferences
|
||||||
if (urls.count == 0) return;
|
if (urls.count == 0) return;
|
||||||
// [[NSWorkspace sharedWorkspace] openURLs:urls withAppBundleIdentifier:@"com.apple.Safari" options:NSWorkspaceLaunchDefault additionalEventParamDescriptor:nil launchIdentifiers:nil];
|
// [[NSWorkspace sharedWorkspace] openURLs:urls withAppBundleIdentifier:@"com.apple.Safari" options:NSWorkspaceLaunchDefault additionalEventParamDescriptor:nil launchIdentifiers:nil];
|
||||||
}
|
}
|
||||||
@@ -327,6 +393,12 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
|
|||||||
#pragma mark - Iterating over items and propagating unread count
|
#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 {
|
- (FeedConfig*)requestFeedConfigForMenuItem:(NSMenuItem*)sender {
|
||||||
MenuItemInfo *info = sender.representedObject;
|
MenuItemInfo *info = sender.representedObject;
|
||||||
if (![info isKindOfClass:[MenuItemInfo class]])
|
if (![info isKindOfClass:[MenuItemInfo class]])
|
||||||
@@ -337,6 +409,12 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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 {
|
- (void)siblingsDescendantFeedConfigs:(NSMenuItem*)sender block:(FeedConfigRecursiveItemsBlock)block {
|
||||||
if (sender.parentItem) {
|
if (sender.parentItem) {
|
||||||
[[self requestFeedConfigForMenuItem:sender.parentItem] descendantFeedItems:block];
|
[[self requestFeedConfigForMenuItem:sender.parentItem] descendantFeedItems:block];
|
||||||
@@ -349,6 +427,12 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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 {
|
- (void)updateAcestors:(NSMenuItem*)sender markRead:(int)count {
|
||||||
[sender markAncestorsRead:count];
|
[sender markAncestorsRead:count];
|
||||||
self.unreadCountTotal -= count;
|
self.unreadCountTotal -= count;
|
||||||
@@ -359,10 +443,15 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
|
|||||||
[self updateBarIcon];
|
[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 {
|
- (int)getAncestorUnreadCount:(NSMenuItem*)sender {
|
||||||
MenuItemInfo *info = sender.parentItem.representedObject;
|
if ([sender.parentItem.representedObject isKindOfClass:[MenuItemInfo class]])
|
||||||
if ([info isKindOfClass:[MenuItemInfo class]])
|
return sender.parentItem.unreadCount;
|
||||||
return info.unreadCount;
|
|
||||||
return self.unreadCountTotal;
|
return self.unreadCountTotal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,22 +22,10 @@
|
|||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
/**
|
|
||||||
Object storing the corresponding Core Data object id and an unread counter.
|
|
||||||
*/
|
|
||||||
@interface MenuItemInfo : NSObject
|
@interface MenuItemInfo : NSObject
|
||||||
/// Core Data store ID
|
|
||||||
@property (strong) NSManagedObjectID *objID;
|
@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;
|
|
||||||
|
|
||||||
+ (instancetype)withID:(NSManagedObjectID*)oid;
|
+ (instancetype)withID:(NSManagedObjectID*)oid;
|
||||||
+ (instancetype)withID:(NSManagedObjectID*)oid unread:(int)count;
|
+ (instancetype)withID:(NSManagedObjectID*)oid unread:(int)count;
|
||||||
|
|
||||||
- (BOOL)hasUnread;
|
|
||||||
- (void)markRead:(int)count;
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|
||||||
@@ -50,11 +38,12 @@
|
|||||||
@return Return how many elements are updated in this block execution. If none were changed return @c 0.
|
@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, MenuItemInfo *info, int count);
|
typedef int (^MenuItemInfoRecursiveBlock) (NSMenuItem *item, int count);
|
||||||
|
|
||||||
|
- (BOOL)hasUnread;
|
||||||
- (int)unreadCount;
|
- (int)unreadCount;
|
||||||
- (int)siblingsDescendantItemInfo:(MenuItemInfoRecursiveBlock)block unreadEntriesOnly:(BOOL)flag;
|
|
||||||
- (void)markAncestorsRead:(int)count;
|
|
||||||
- (void)markReadAndUpdateTitle:(int)count;
|
- (void)markReadAndUpdateTitle:(int)count;
|
||||||
|
- (void)markAncestorsRead:(int)count;
|
||||||
|
- (int)siblingsDescendantItemInfo:(MenuItemInfoRecursiveBlock)block unreadEntriesOnly:(BOOL)flag;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,12 @@
|
|||||||
|
|
||||||
#import "MenuItemInfo.h"
|
#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
|
@implementation MenuItemInfo
|
||||||
/// @return Info with unreadCount = 0
|
/// @return Info with unreadCount = 0
|
||||||
@@ -57,72 +63,14 @@
|
|||||||
|
|
||||||
@implementation NSMenuItem (MenuItemInfo)
|
@implementation NSMenuItem (MenuItemInfo)
|
||||||
|
|
||||||
/**
|
/** Call represented object and check whether unread count > 0. */
|
||||||
Call represented object and retrieve the unread count from info.
|
- (BOOL)hasUnread {
|
||||||
|
return [self.representedObject unreadCount] > 0;
|
||||||
|
}
|
||||||
|
|
||||||
@return Unread count stored in menu info.
|
/** Call represented object and retrieve the unread count from info. */
|
||||||
*/
|
|
||||||
- (int)unreadCount {
|
- (int)unreadCount {
|
||||||
MenuItemInfo *info = self.representedObject;
|
return [self.representedObject unreadCount];
|
||||||
if (![info isKindOfClass:[MenuItemInfo class]]) return 0;
|
|
||||||
return info.unreadCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Recursively iterate over submenues and children. Count aggregated element edits.
|
|
||||||
|
|
||||||
@param block Will be called for each @c NSMenuItem sub-element where @c representedObject is set to a @c MenuItemInfo.
|
|
||||||
@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, info, countItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Recursively iterate over siblings and all contained children. Count aggregated element edits.
|
|
||||||
|
|
||||||
@param block Will be called for each @c NSMenuItem sub-element where @c representedObject is set to a @c MenuItemInfo.
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -131,9 +79,10 @@
|
|||||||
@note Count may be negative to mark items as unread.
|
@note Count may be negative to mark items as unread.
|
||||||
@warning Does not check if @c representedObject is set accordingly
|
@warning Does not check if @c representedObject is set accordingly
|
||||||
@param count The amount by which the counter is adjusted.
|
@param count The amount by which the counter is adjusted.
|
||||||
If negative the items will be marked as unread.
|
If negative the items will be marked as unread.
|
||||||
*/
|
*/
|
||||||
- (void)markReadAndUpdateTitle:(int)count {
|
- (void)markReadAndUpdateTitle:(int)count {
|
||||||
|
if (count == 0) return; // 0 won't change anything
|
||||||
MenuItemInfo *info = self.representedObject;
|
MenuItemInfo *info = self.representedObject;
|
||||||
if (!self.hasSubmenu) {
|
if (!self.hasSubmenu) {
|
||||||
[info markRead:count];
|
[info markRead:count];
|
||||||
@@ -155,4 +104,65 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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
|
@end
|
||||||
|
|||||||
Reference in New Issue
Block a user