From 7d68a25de2903c33542cfd7a301c8f71cbf5aa66 Mon Sep 17 00:00:00 2001 From: relikd Date: Mon, 17 Sep 2018 04:09:12 +0200 Subject: [PATCH] Menu generation refactored --- baRSS.xcodeproj/project.pbxproj | 6 + baRSS/Status Bar Menu/BarMenu.m | 126 +++++-------------- baRSS/Status Bar Menu/NSMenuItem+Generate.h | 33 +++++ baRSS/Status Bar Menu/NSMenuItem+Generate.m | 127 ++++++++++++++++++++ 4 files changed, 199 insertions(+), 93 deletions(-) create mode 100644 baRSS/Status Bar Menu/NSMenuItem+Generate.h create mode 100644 baRSS/Status Bar Menu/NSMenuItem+Generate.m diff --git a/baRSS.xcodeproj/project.pbxproj b/baRSS.xcodeproj/project.pbxproj index 1346916..2cdea8f 100644 --- a/baRSS.xcodeproj/project.pbxproj +++ b/baRSS.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 54209E942117325100F3B5EF /* DrawImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 54209E932117325100F3B5EF /* DrawImage.m */; }; 543695D5214EFD9800DA979D /* NSMenuItem+Info.m in Sources */ = {isa = PBXBuildFile; fileRef = 543695D4214EFD9800DA979D /* NSMenuItem+Info.m */; }; + 543695D8214F1F2700DA979D /* NSMenuItem+Generate.m in Sources */ = {isa = PBXBuildFile; fileRef = 543695D7214F1F2700DA979D /* NSMenuItem+Generate.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 */; }; @@ -61,6 +62,8 @@ 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 = ""; }; + 543695D6214F1F2700DA979D /* NSMenuItem+Generate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSMenuItem+Generate.h"; sourceTree = ""; }; + 543695D7214F1F2700DA979D /* NSMenuItem+Generate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSMenuItem+Generate.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 = ""; }; @@ -113,6 +116,8 @@ children = ( 543695D3214EFD9800DA979D /* NSMenuItem+Info.h */, 543695D4214EFD9800DA979D /* NSMenuItem+Info.m */, + 543695D6214F1F2700DA979D /* NSMenuItem+Generate.h */, + 543695D7214F1F2700DA979D /* NSMenuItem+Generate.m */, 54FE73D1212316CD003EAC65 /* BarMenu.h */, 54FE73D2212316CD003EAC65 /* BarMenu.m */, ); @@ -290,6 +295,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 543695D8214F1F2700DA979D /* NSMenuItem+Generate.m in Sources */, 54F39C2E210BE1F700AEE730 /* DBv1.xcdatamodeld in Sources */, 544B011D2114EE9100386E5C /* AppHook.m in Sources */, 546FC44321189975007CC3A3 /* SettingsGeneral.m in Sources */, diff --git a/baRSS/Status Bar Menu/BarMenu.m b/baRSS/Status Bar Menu/BarMenu.m index 0eed31f..22f3426 100644 --- a/baRSS/Status Bar Menu/BarMenu.m +++ b/baRSS/Status Bar Menu/BarMenu.m @@ -25,6 +25,7 @@ #import "DrawImage.h" #import "Preferences.h" #import "NSMenuItem+Info.h" +#import "NSMenuItem+Generate.h" #import "UserPrefs.h" @@ -117,7 +118,7 @@ self.unreadCountTotal = 0; @autoreleasepool { for (FeedConfig *fc in [StoreCoordinator sortedFeedConfigItems]) { - [menu addItem:[self menuItemForFeedConfig:fc unread:&_unreadCountTotal]]; + [menu addItem:[self generateMenuItem:fc unread:&_unreadCountTotal]]; } } [self updateMenuHeaderEnabled:menu hasUnread:(self.unreadCountTotal > 0)]; @@ -132,104 +133,56 @@ } /** - Create and return a new @c NSMenuItem from the objects attributes. + Generate menu item with all its sub-menus. @c FeedConfig type is evaluated automatically. - @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) + @param unread Pointer to an unread count. Will be incremented while traversing through sub-menus. */ -- (NSMenuItem*)menuItemForFeedConfig:(FeedConfig*)config unread:(int*)unread { - NSMenuItem *item; - if (config.typ == SEPARATOR) { - item = [NSMenuItem separatorItem]; - [item setReaderInfo:config.objectID unread:0]; +- (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; } - int count = 0; - if (config.typ == FEED) { - item = [self feedItem:config unread:&count]; - } else if (config.typ == GROUP) { - item = [self groupItem:config unread:&count]; - } *unread += count; - [item setReaderInfo:config.objectID unread:0]; - // !!!: fix that double count [item markReadAndUpdateTitle:-count]; [self updateMenuHeaderEnabled:item.submenu hasUnread:(count > 0)]; return item; } /** - 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 { - NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:config.name action:@selector(openFeedURL:) keyEquivalent:@""]; - item.target = self; - 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); - - // 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; -} + Set subitems for a @c FeedConfig group item. Namely various @c FeedConfig and @c FeedItem items. -/** - 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. + @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. */ -- (NSMenuItem*)groupItem:(FeedConfig*)config unread:(int*)unread { - NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:config.name action:nil keyEquivalent:@""]; +- (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 menuItemForFeedConfig:obj unread:unread]]; + [item.submenu addItem: [self generateMenuItem: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; } /** - Create and return a new @c NSMenuItem from @c FeedItem attributes. + 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. */ -- (NSMenuItem*)feedEntryItem:(FeedItem*)item { - NSMenuItem *mi = [[NSMenuItem alloc] initWithTitle:item.title action:@selector(openFeedURL:) keyEquivalent:@""]; - mi.target = self; - [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) { - NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"<[^>]*>" options:kNilOptions error:nil]; - mi.toolTip = [regex stringByReplacingMatchesInString:item.abstract options:kNilOptions range:NSMakeRange(0, item.abstract.length) withTemplate:@""]; +- (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]]; } - mi.enabled = (item.link.length > 0); - mi.state = (item.unread ? NSControlStateValueOn : NSControlStateValueOff); - mi.tag = ScopeFeed; - return mi; + [item setAction:@selector(openFeedURL:) target:self]; + return count; } /** @@ -244,19 +197,6 @@ 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 @@ -276,7 +216,7 @@ } 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]; + [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]; @@ -471,7 +411,7 @@ */ - (void)siblingsDescendantFeedConfigs:(NSMenuItem*)sender block:(FeedConfigRecursiveItemsBlock)block { if (sender.parentItem) { - FeedConfig *obj = [sender requestCoreDataObject]; + FeedConfig *obj = [sender.parentItem requestCoreDataObject]; if ([obj isKindOfClass:[FeedConfig class]]) // important: this could be a FeedItem [obj descendantFeedItems:block]; } else { diff --git a/baRSS/Status Bar Menu/NSMenuItem+Generate.h b/baRSS/Status Bar Menu/NSMenuItem+Generate.h new file mode 100644 index 0000000..fcac4d6 --- /dev/null +++ b/baRSS/Status Bar Menu/NSMenuItem+Generate.h @@ -0,0 +1,33 @@ +// +// 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 + +@class FeedConfig, FeedItem; + +@interface NSMenuItem (Generate) ++ (NSMenuItem*)feedConfig:(FeedConfig*)config; ++ (NSMenuItem*)feedItem:(FeedItem*)item; +- (NSMenuItem*)alternateWithTitle:(NSString*)title; + +- (NSMenuItem*)setAction:(nullable SEL)action target:(nullable id)target; +@end diff --git a/baRSS/Status Bar Menu/NSMenuItem+Generate.m b/baRSS/Status Bar Menu/NSMenuItem+Generate.m new file mode 100644 index 0000000..15ccb5c --- /dev/null +++ b/baRSS/Status Bar Menu/NSMenuItem+Generate.m @@ -0,0 +1,127 @@ +// +// 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+Generate.h" +#import "NSMenuItem+Info.h" +#import "StoreCoordinator.h" +#import "DrawImage.h" + +@implementation NSMenuItem (Feed) +/** + Generate a new @c NSMenuItem based on the type stored in @c FeedConfig. + + @param config @c FeedConfig object that represents a superior feed element. + @return Return a fully configured Separator item OR group item OR feed item. (but not @c FeedItem item) + */ ++ (NSMenuItem*)feedConfig:(FeedConfig*)config { + NSMenuItem *item; + switch (config.typ) { + case SEPARATOR: item = [NSMenuItem separatorItem]; break; + case GROUP: item = [self feedConfigItemGroup:config]; break; + case FEED: item = [self feedConfigItemFeed:config]; break; + } + [item setReaderInfo:config.objectID unread:0]; + return item; +} + +/** + Generate a new @c NSMenuItem from a @c FeedConfig feed item. + + @param config @c FeedConfig object that represents a superior feed element. + */ ++ (NSMenuItem*)feedConfigItemFeed:(FeedConfig*)config { + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:config.name action:nil keyEquivalent:@""]; + item.toolTip = config.feed.subtitle; + item.enabled = (config.feed.items.count > 0); + item.tag = ScopeFeed; + // set icon + dispatch_async(dispatch_get_main_queue(), ^{ + static NSImage *defaultRSSIcon; + if (!defaultRSSIcon) + defaultRSSIcon = [RSSIcon iconWithSize:16]; + item.image = defaultRSSIcon; + }); + return item; +} + +/** + Generate a new @c NSMenuItem from a @c FeedConfig group item + + @param config @c FeedConfig object that represents a group item. + */ ++ (NSMenuItem*)feedConfigItemGroup:(FeedConfig*)config { + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:config.name action:nil keyEquivalent:@""]; + item.tag = ScopeGroup; + // 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; + }); + return item; +} + +/** + Generate new @c NSMenuItem based on the attributes of a @c FeedItem. + */ ++ (NSMenuItem*)feedItem:(FeedItem*)item { + NSMenuItem *mi = [[NSMenuItem alloc] initWithTitle:item.title action:nil keyEquivalent:@""]; + [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) { + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"<[^>]*>" options:kNilOptions error:nil]; + mi.toolTip = [regex stringByReplacingMatchesInString:item.abstract options:kNilOptions range:NSMakeRange(0, item.abstract.length) withTemplate:@""]; + } + mi.enabled = (item.link.length > 0); + mi.state = (item.unread ? NSControlStateValueOn : NSControlStateValueOff); + mi.tag = ScopeFeed; + return mi; +} + +/** + Create a copy of an existing menu item and set it's option key modifier. + */ +- (NSMenuItem*)alternateWithTitle:(NSString*)title { + NSMenuItem *alt = [self copy]; + alt.title = title; + alt.keyEquivalentModifierMask = NSEventModifierFlagOption; + if (!alt.hidden) // hidden will be ignored if alternate is YES + alt.alternate = YES; + return alt; +} + +/** + Set @c action and @c target attributes. + + @return Return @c self instance. Intended for method chains. + */ +- (NSMenuItem*)setAction:(SEL)action target:(id)target { + self.action = action; + self.target = target; + return self; +} + +@end