Menu generation refactored

This commit is contained in:
relikd
2018-09-17 04:09:12 +02:00
parent 23c7645d36
commit 7d68a25de2
4 changed files with 199 additions and 93 deletions

View File

@@ -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 = "<group>"; };
543695D3214EFD9800DA979D /* NSMenuItem+Info.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSMenuItem+Info.h"; sourceTree = "<group>"; };
543695D4214EFD9800DA979D /* NSMenuItem+Info.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSMenuItem+Info.m"; sourceTree = "<group>"; };
543695D6214F1F2700DA979D /* NSMenuItem+Generate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSMenuItem+Generate.h"; sourceTree = "<group>"; };
543695D7214F1F2700DA979D /* NSMenuItem+Generate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSMenuItem+Generate.m"; sourceTree = "<group>"; };
544B01182114B41200386E5C /* ModalSheet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ModalSheet.h; sourceTree = "<group>"; };
544B01192114B41200386E5C /* ModalSheet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ModalSheet.m; sourceTree = "<group>"; };
544B011B2114EE9100386E5C /* AppHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppHook.h; sourceTree = "<group>"; };
@@ -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 */,

View File

@@ -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 {

View File

@@ -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 <Cocoa/Cocoa.h>
@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

View File

@@ -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