Menus with unread count: mark, unmark, open all unread
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
1968E0AE14B8E8A90E194980 /* PyHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 1968EF7567E06D2A5BB3481A /* PyHandler.m */; };
|
1968E0AE14B8E8A90E194980 /* PyHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 1968EF7567E06D2A5BB3481A /* PyHandler.m */; };
|
||||||
|
541A90F221257D77002680A6 /* MenuItemInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 541A90F121257D77002680A6 /* MenuItemInfo.m */; };
|
||||||
54209E942117325100F3B5EF /* DrawImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 54209E932117325100F3B5EF /* DrawImage.m */; };
|
54209E942117325100F3B5EF /* DrawImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 54209E932117325100F3B5EF /* DrawImage.m */; };
|
||||||
544B011A2114B41200386E5C /* ModalSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B01192114B41200386E5C /* ModalSheet.m */; };
|
544B011A2114B41200386E5C /* ModalSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B01192114B41200386E5C /* ModalSheet.m */; };
|
||||||
544B011D2114EE9100386E5C /* AppHook.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B011C2114EE9100386E5C /* AppHook.m */; };
|
544B011D2114EE9100386E5C /* AppHook.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B011C2114EE9100386E5C /* AppHook.m */; };
|
||||||
@@ -34,6 +35,8 @@
|
|||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
1968E7919BAA36F042FCB717 /* PyHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PyHandler.h; sourceTree = "<group>"; };
|
1968E7919BAA36F042FCB717 /* PyHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PyHandler.h; sourceTree = "<group>"; };
|
||||||
1968EF7567E06D2A5BB3481A /* PyHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PyHandler.m; sourceTree = "<group>"; };
|
1968EF7567E06D2A5BB3481A /* PyHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PyHandler.m; sourceTree = "<group>"; };
|
||||||
|
541A90F021257D77002680A6 /* MenuItemInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MenuItemInfo.h; sourceTree = "<group>"; };
|
||||||
|
541A90F121257D77002680A6 /* MenuItemInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MenuItemInfo.m; sourceTree = "<group>"; };
|
||||||
54209E922117325100F3B5EF /* DrawImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DrawImage.h; sourceTree = "<group>"; };
|
54209E922117325100F3B5EF /* DrawImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DrawImage.h; sourceTree = "<group>"; };
|
||||||
54209E932117325100F3B5EF /* DrawImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DrawImage.m; sourceTree = "<group>"; };
|
54209E932117325100F3B5EF /* DrawImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DrawImage.m; sourceTree = "<group>"; };
|
||||||
544B01182114B41200386E5C /* ModalSheet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ModalSheet.h; sourceTree = "<group>"; };
|
544B01182114B41200386E5C /* ModalSheet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ModalSheet.h; sourceTree = "<group>"; };
|
||||||
@@ -82,6 +85,17 @@
|
|||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
541A90EF21257D4F002680A6 /* Status Bar Menu */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
541A90F021257D77002680A6 /* MenuItemInfo.h */,
|
||||||
|
541A90F121257D77002680A6 /* MenuItemInfo.m */,
|
||||||
|
54FE73D1212316CD003EAC65 /* BarMenu.h */,
|
||||||
|
54FE73D2212316CD003EAC65 /* BarMenu.m */,
|
||||||
|
);
|
||||||
|
path = "Status Bar Menu";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
544FBD4321064AEB008A260C /* Frameworks */ = {
|
544FBD4321064AEB008A260C /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -150,8 +164,7 @@
|
|||||||
549369F421091E6D001AF895 /* python */,
|
549369F421091E6D001AF895 /* python */,
|
||||||
544B011B2114EE9100386E5C /* AppHook.h */,
|
544B011B2114EE9100386E5C /* AppHook.h */,
|
||||||
544B011C2114EE9100386E5C /* AppHook.m */,
|
544B011C2114EE9100386E5C /* AppHook.m */,
|
||||||
54FE73D1212316CD003EAC65 /* BarMenu.h */,
|
541A90EF21257D4F002680A6 /* Status Bar Menu */,
|
||||||
54FE73D2212316CD003EAC65 /* BarMenu.m */,
|
|
||||||
54FE73CE21220DEC003EAC65 /* StoreCoordinator.h */,
|
54FE73CE21220DEC003EAC65 /* StoreCoordinator.h */,
|
||||||
54FE73CF21220DEC003EAC65 /* StoreCoordinator.m */,
|
54FE73CF21220DEC003EAC65 /* StoreCoordinator.m */,
|
||||||
54ACC29321061E270020715F /* FeedDownload.h */,
|
54ACC29321061E270020715F /* FeedDownload.h */,
|
||||||
@@ -277,6 +290,7 @@
|
|||||||
1968E0AE14B8E8A90E194980 /* PyHandler.m in Sources */,
|
1968E0AE14B8E8A90E194980 /* PyHandler.m in Sources */,
|
||||||
54209E942117325100F3B5EF /* DrawImage.m in Sources */,
|
54209E942117325100F3B5EF /* DrawImage.m in Sources */,
|
||||||
54FE73D021220DEC003EAC65 /* StoreCoordinator.m in Sources */,
|
54FE73D021220DEC003EAC65 /* StoreCoordinator.m in Sources */,
|
||||||
|
541A90F221257D77002680A6 /* MenuItemInfo.m in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
196
baRSS/BarMenu.m
196
baRSS/BarMenu.m
@@ -1,196 +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 "BarMenu.h"
|
|
||||||
#import "StoreCoordinator.h"
|
|
||||||
#import "DrawImage.h"
|
|
||||||
#import "Preferences.h"
|
|
||||||
|
|
||||||
@interface BarMenu()
|
|
||||||
@property (strong) NSStatusItem *barItem;
|
|
||||||
@property (strong) Preferences *prefWindow;
|
|
||||||
@property (weak) NSMenu *mm;
|
|
||||||
@end
|
|
||||||
|
|
||||||
|
|
||||||
@implementation BarMenu
|
|
||||||
|
|
||||||
- (instancetype)init {
|
|
||||||
self = [super init];
|
|
||||||
self.barItem = [self statusItem];
|
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(preferencesClosed) name:@"baRSSPreferencesClosed" object:nil];
|
|
||||||
// [self donothing];
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)donothing {
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
[self.mm itemAtIndex:4].title = [NSString stringWithFormat:@"%@", [NSDate date]];
|
|
||||||
});
|
|
||||||
sleep(1);
|
|
||||||
[self performSelectorInBackground:@selector(donothing) withObject:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
-(void)dealloc {
|
|
||||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSStatusItem*)statusItem {
|
|
||||||
NSStatusItem *item = [NSStatusBar.systemStatusBar statusItemWithLength:NSVariableStatusItemLength];
|
|
||||||
item.title = @"me";
|
|
||||||
item.menu = self.mainMenu;
|
|
||||||
item.highlightMode = YES;
|
|
||||||
item.image = [[RSSIcon templateIcon:16 tint:nil] image];
|
|
||||||
item.image.template = YES;
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)openPreferences {
|
|
||||||
if (!self.prefWindow)
|
|
||||||
self.prefWindow = [[Preferences alloc] initWithWindowNibName:@"Preferences"];
|
|
||||||
[NSApp activateIgnoringOtherApps:YES];
|
|
||||||
[self.prefWindow showWindow:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)preferencesClosed {
|
|
||||||
self.prefWindow = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Main Menu Item Actions
|
|
||||||
|
|
||||||
- (void)pauseUpdates {
|
|
||||||
NSLog(@"1pause");
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)updateAllFeeds {
|
|
||||||
NSLog(@"1update all");
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)openAllUnread {
|
|
||||||
NSLog(@"1all unread");
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)openFeedURL:(NSMenuItem*)sender {
|
|
||||||
id obj = [StoreCoordinator objectWithID:sender.representedObject];
|
|
||||||
NSString *url = nil;
|
|
||||||
if ([obj isKindOfClass:[FeedItem class]]) {
|
|
||||||
url = [(FeedItem*)obj link];
|
|
||||||
} else if ([obj isKindOfClass:[FeedConfig class]]) {
|
|
||||||
url = [[(FeedConfig*)obj feed] link];
|
|
||||||
}
|
|
||||||
if (!url || url.length == 0) return;
|
|
||||||
[[NSWorkspace sharedWorkspace] openURLs:@[[NSURL URLWithString:url]] withAppBundleIdentifier:@"com.apple.Safari" options:NSWorkspaceLaunchDefault additionalEventParamDescriptor:nil launchIdentifiers:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Menu Generator
|
|
||||||
|
|
||||||
- (NSMenu*)mainMenu {
|
|
||||||
NSMenu *menu = [NSMenu new];
|
|
||||||
menu.autoenablesItems = NO;
|
|
||||||
// self.mm = menu;
|
|
||||||
[self addTitle:@"Pause Updates" selector:@selector(pauseUpdates) key:@"" toMenu:menu];
|
|
||||||
[self addTitle:@"Update all feeds" selector:@selector(updateAllFeeds) key:@"" toMenu:menu];
|
|
||||||
[self addTitle:@"Open all unread" selector:@selector(openAllUnread) key:@"" toMenu:menu];
|
|
||||||
[menu addItem:[NSMenuItem separatorItem]];
|
|
||||||
|
|
||||||
NSArray<FeedConfig*> *items = [StoreCoordinator sortedFeedConfigItems];
|
|
||||||
for (FeedConfig *fc in items) {
|
|
||||||
[menu addItem:[self menuItemForFeedConfig:fc]];
|
|
||||||
}
|
|
||||||
|
|
||||||
[menu addItem:[NSMenuItem separatorItem]];
|
|
||||||
[self addTitle:@"Preferences" selector:@selector(openPreferences) key:@"," toMenu:menu];
|
|
||||||
[menu addItemWithTitle:@"Quit" action:@selector(terminate:) keyEquivalent:@"q"];
|
|
||||||
return menu;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)addTitle:(NSString*)title selector:(SEL)selector key:(NSString*)key toMenu:(NSMenu*)menu {
|
|
||||||
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:key];
|
|
||||||
item.target = self;
|
|
||||||
[menu addItem:item];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSMenuItem*)menuItemForFeedConfig:(FeedConfig*)fc {
|
|
||||||
NSMenuItem *item;
|
|
||||||
if (fc.typ == SEPARATOR) {
|
|
||||||
item = [NSMenuItem separatorItem];
|
|
||||||
} else {
|
|
||||||
item = [[NSMenuItem alloc] initWithTitle:fc.name action:nil keyEquivalent:@""];
|
|
||||||
if (fc.typ == FEED) {
|
|
||||||
item.submenu = [self menuForFeed:fc.feed];
|
|
||||||
item.action = @selector(openFeedURL:);
|
|
||||||
item.target = self;
|
|
||||||
item.toolTip = fc.feed.subtitle;
|
|
||||||
item.enabled = (fc.feed.link.length > 0);
|
|
||||||
static NSImage *defaultRSSIcon;
|
|
||||||
if (!defaultRSSIcon)
|
|
||||||
defaultRSSIcon = [[[RSSIcon iconWithSize:NSMakeSize(16, 16)] autoGradient] image];
|
|
||||||
item.image = defaultRSSIcon;
|
|
||||||
} else {
|
|
||||||
item.submenu = [self menuForFeedConfig:fc];
|
|
||||||
item.image = [NSImage imageNamed:NSImageNameFolder];
|
|
||||||
item.image.size = NSMakeSize(16, 16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
item.representedObject = fc.objectID;
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSMenu*)menuForFeedConfig:(FeedConfig*)parent {
|
|
||||||
NSMenu *menu = [NSMenu new];
|
|
||||||
menu.autoenablesItems = NO;
|
|
||||||
// TODO: open unread for groups ...
|
|
||||||
for (FeedConfig *fc in parent.sortedChildren) {
|
|
||||||
[menu addItem:[self menuItemForFeedConfig:fc]];
|
|
||||||
}
|
|
||||||
return menu;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSMenu*)menuForFeed:(Feed*)feed {
|
|
||||||
NSMenu *menu = [NSMenu new];
|
|
||||||
menu.autoenablesItems = NO;
|
|
||||||
// TODO: open unread for feed only ...
|
|
||||||
for (FeedItem *entry in feed.items) {
|
|
||||||
[menu addItem:[self menuItemForFeedItem:entry]];
|
|
||||||
}
|
|
||||||
return menu;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSMenuItem*)menuItemForFeedItem:(FeedItem*)item {
|
|
||||||
NSMenuItem *mi = [[NSMenuItem alloc] initWithTitle:item.title action:@selector(openFeedURL:) keyEquivalent:@""];
|
|
||||||
mi.target = self;
|
|
||||||
mi.representedObject = item.objectID;
|
|
||||||
mi.toolTip = item.subtitle;
|
|
||||||
mi.enabled = (item.link.length > 0);
|
|
||||||
return mi;
|
|
||||||
}
|
|
||||||
|
|
||||||
//- (NSIndexPath*)indexPathForMenu:(NSMenu*)menu {
|
|
||||||
// NSMenu *parent = menu.supermenu;
|
|
||||||
// if (parent == nil) {
|
|
||||||
// return [NSIndexPath new];
|
|
||||||
// } else {
|
|
||||||
// return [[self indexPathForMenu:parent] indexPathByAddingIndex:(NSUInteger)[parent indexOfItemWithSubmenu:menu]];
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
<attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
<attribute name="summary" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="summary" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="unread" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="items" inverseEntity="Feed" syncable="YES"/>
|
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="items" inverseEntity="Feed" syncable="YES"/>
|
||||||
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="FeedTag" inverseName="feedItem" inverseEntity="FeedTag" syncable="YES"/>
|
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="FeedTag" inverseName="feedItem" inverseEntity="FeedTag" syncable="YES"/>
|
||||||
</entity>
|
</entity>
|
||||||
@@ -41,7 +42,7 @@
|
|||||||
<elements>
|
<elements>
|
||||||
<element name="Feed" positionX="-209" positionY="-3" width="128" height="210"/>
|
<element name="Feed" positionX="-209" positionY="-3" width="128" height="210"/>
|
||||||
<element name="FeedConfig" positionX="-20" positionY="-126" width="128" height="180"/>
|
<element name="FeedConfig" positionX="-20" positionY="-126" width="128" height="180"/>
|
||||||
<element name="FeedItem" positionX="-20" positionY="81" width="128" height="165"/>
|
<element name="FeedItem" positionX="-20" positionY="81" width="128" height="180"/>
|
||||||
<element name="FeedTag" positionX="187" positionY="171" width="128" height="75"/>
|
<element name="FeedTag" positionX="187" positionY="171" width="128" height="75"/>
|
||||||
</elements>
|
</elements>
|
||||||
</model>
|
</model>
|
||||||
@@ -22,15 +22,28 @@
|
|||||||
|
|
||||||
#import "FeedConfig+CoreDataClass.h"
|
#import "FeedConfig+CoreDataClass.h"
|
||||||
|
|
||||||
|
@class FeedItem;
|
||||||
|
|
||||||
@interface FeedConfig (Ext)
|
@interface FeedConfig (Ext)
|
||||||
|
/// Enum type to distinguish different @c FeedConfig types
|
||||||
typedef enum int16_t {
|
typedef enum int16_t {
|
||||||
GROUP = 0,
|
GROUP = 0,
|
||||||
FEED = 1,
|
FEED = 1,
|
||||||
SEPARATOR = 2
|
SEPARATOR = 2
|
||||||
} FeedConfigType;
|
} FeedConfigType;
|
||||||
|
/**
|
||||||
|
Iteration block for descendants of @c FeedItem.
|
||||||
|
|
||||||
|
@param parent The parent @c FeedConfig where this @c FeedItem belongs to.
|
||||||
|
@param item Currently processed @c FeedItem.
|
||||||
|
@return Return @c YES to continue processing. Return @c NO to stop processing and exit early.
|
||||||
|
*/
|
||||||
|
typedef BOOL (^FeedConfigRecursiveItemsBlock) (FeedConfig *parent, FeedItem *item);
|
||||||
|
|
||||||
@property (getter=typ, setter=setTyp:) FeedConfigType typ;
|
@property (getter=typ, setter=setTyp:) FeedConfigType typ;
|
||||||
@property (readonly) NSArray<FeedConfig*> *sortedChildren;
|
@property (readonly) NSArray<FeedConfig*> *sortedChildren;
|
||||||
|
|
||||||
|
- (BOOL)descendantFeedItems:(FeedConfigRecursiveItemsBlock)block;
|
||||||
- (NSString*)readableRefreshString;
|
- (NSString*)readableRefreshString;
|
||||||
- (NSString*)readableDescription;
|
- (NSString*)readableDescription;
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -21,27 +21,52 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
#import "FeedConfig+Ext.h"
|
#import "FeedConfig+Ext.h"
|
||||||
|
#import "Feed+CoreDataClass.h"
|
||||||
|
|
||||||
@implementation FeedConfig (Ext)
|
@implementation FeedConfig (Ext)
|
||||||
|
/// Enum tpye getter see @c FeedConfigType
|
||||||
|
- (FeedConfigType)typ { return (FeedConfigType)self.type; }
|
||||||
|
/// Enum type setter see @c FeedConfigType
|
||||||
|
- (void)setTyp:(FeedConfigType)typ { self.type = typ; }
|
||||||
|
|
||||||
- (FeedConfigType)typ {
|
/**
|
||||||
return (FeedConfigType)self.type;
|
Sorted children array based on sort order provided in feed settings.
|
||||||
}
|
|
||||||
|
|
||||||
- (void)setTyp:(FeedConfigType)typ {
|
|
||||||
self.type = typ;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@return Sorted array of @c FeedConfig items.
|
||||||
|
*/
|
||||||
- (NSArray<FeedConfig *> *)sortedChildren {
|
- (NSArray<FeedConfig *> *)sortedChildren {
|
||||||
if (self.children.count == 0)
|
if (self.children.count == 0)
|
||||||
return nil;
|
return nil;
|
||||||
return [self.children sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:YES]]];
|
return [self.children sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:YES]]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Iterate over all descendant @c FeedItems in sub groups
|
||||||
|
|
||||||
|
@param block Will yield the current parent config and feed item. Return @c NO to cancel iteration.
|
||||||
|
@return Returns @c NO if the iteration was canceled early. Otherwise @c YES.
|
||||||
|
*/
|
||||||
|
- (BOOL)descendantFeedItems:(FeedConfigRecursiveItemsBlock)block {
|
||||||
|
if (self.children.count > 0) {
|
||||||
|
for (FeedConfig *config in self.children) {
|
||||||
|
if ([config descendantFeedItems:block] == NO)
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
} else if (self.feed.items.count > 0) {
|
||||||
|
for (FeedItem* item in self.feed.items) {
|
||||||
|
if (block(self, item) == NO)
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @return Formatted string for update interval ( e.g., @c 30m or @c 12h )
|
||||||
- (NSString*)readableRefreshString {
|
- (NSString*)readableRefreshString {
|
||||||
return [NSString stringWithFormat:@"%d%c", self.refreshNum, [@"smhdw" characterAtIndex:self.refreshUnit % 5]];
|
return [NSString stringWithFormat:@"%d%c", self.refreshNum, [@"smhdw" characterAtIndex:self.refreshUnit % 5]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @return Simplified description of the feed object.
|
||||||
- (NSString*)readableDescription {
|
- (NSString*)readableDescription {
|
||||||
switch (self.typ) {
|
switch (self.typ) {
|
||||||
case SEPARATOR: return @"-------------";
|
case SEPARATOR: return @"-------------";
|
||||||
|
|||||||
@@ -191,8 +191,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
- (void)loadView {
|
- (void)loadView {
|
||||||
NSTextField *tf = [NSTextField textFieldWithString:@"New Group"];
|
NSTextField *tf = [NSTextField textFieldWithString:NSLocalizedString(@"New Group", nil)];
|
||||||
tf.placeholderString = @"New Group";
|
tf.placeholderString = NSLocalizedString(@"New Group", nil);
|
||||||
tf.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
|
tf.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
|
||||||
self.view = tf;
|
self.view = tf;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,10 +56,6 @@
|
|||||||
[[NSUserDefaults standardUserDefaults] setInteger:selectedIndex forKey:@"preferencesTab"];
|
[[NSUserDefaults standardUserDefaults] setInteger:selectedIndex forKey:@"preferencesTab"];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)windowWillClose:(NSNotification *)notification {
|
|
||||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"baRSSPreferencesClosed" object:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</connections>
|
</connections>
|
||||||
</customObject>
|
</customObject>
|
||||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||||
<window title="baRSS Preferences" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="prefWindow" animationBehavior="default" tabbingMode="disallowed" id="XQ4-ia-CCO" userLabel="Window" customClass="NonRespondingWindow">
|
<window title="Preferences" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="prefWindow" animationBehavior="default" tabbingMode="disallowed" id="XQ4-ia-CCO" userLabel="Window" customClass="NonRespondingWindow">
|
||||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" resizable="YES"/>
|
<windowStyleMask key="styleMask" titled="YES" closable="YES" resizable="YES"/>
|
||||||
<rect key="contentRect" x="948" y="431" width="320" height="327"/>
|
<rect key="contentRect" x="948" y="431" width="320" height="327"/>
|
||||||
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="878"/>
|
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="878"/>
|
||||||
|
|||||||
369
baRSS/Status Bar Menu/BarMenu.m
Normal file
369
baRSS/Status Bar Menu/BarMenu.m
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
//
|
||||||
|
// 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 "BarMenu.h"
|
||||||
|
#import "StoreCoordinator.h"
|
||||||
|
#import "DrawImage.h"
|
||||||
|
#import "Preferences.h"
|
||||||
|
#import "MenuItemInfo.h"
|
||||||
|
|
||||||
|
|
||||||
|
@interface BarMenu()
|
||||||
|
typedef NS_OPTIONS(NSInteger, MenuItemTag) {
|
||||||
|
ScopeGlobal = 1,
|
||||||
|
ScopeGroup = (1<<1),
|
||||||
|
ScopeLocal = (1<<2),
|
||||||
|
PauseUpdates = (1<<3),
|
||||||
|
UpdateFeed = (1<<4),
|
||||||
|
MarkAllRead = (1<<5),
|
||||||
|
MarkAllUnread = (1<<6),
|
||||||
|
OpenAllUnread = (1<<7),
|
||||||
|
};
|
||||||
|
|
||||||
|
@property (strong) NSStatusItem *barItem;
|
||||||
|
@property (strong) Preferences *prefWindow;
|
||||||
|
@property (weak) NSMenu *mm;
|
||||||
|
@property (assign) int unreadCountTotal;
|
||||||
|
@end
|
||||||
|
|
||||||
|
|
||||||
|
@implementation BarMenu
|
||||||
|
|
||||||
|
- (instancetype)init {
|
||||||
|
self = [super init];
|
||||||
|
self.barItem = [NSStatusBar.systemStatusBar statusItemWithLength:NSVariableStatusItemLength];
|
||||||
|
self.barItem.menu = self.mainMenu;
|
||||||
|
self.barItem.highlightMode = YES;
|
||||||
|
[self updateBarIcon];
|
||||||
|
// [self donothing];
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)donothing {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[self.mm itemAtIndex:4].title = [NSString stringWithFormat:@"%@", [NSDate date]];
|
||||||
|
});
|
||||||
|
sleep(1);
|
||||||
|
[self performSelectorInBackground:@selector(donothing) withObject:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (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:[RSSIcon rssOrange]] image];
|
||||||
|
} else {
|
||||||
|
self.barItem.title = @"";
|
||||||
|
self.barItem.image = [[RSSIcon templateIcon:16 tint:nil] image];
|
||||||
|
self.barItem.image.template = YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#pragma mark - Menu Generator
|
||||||
|
|
||||||
|
|
||||||
|
- (NSMenu*)mainMenu {
|
||||||
|
NSMenu *menu = [NSMenu new];
|
||||||
|
menu.autoenablesItems = NO;
|
||||||
|
[self addTitle:NSLocalizedString(@"Pause Updates", nil) selector:@selector(pauseUpdates:) key:@"" toMenu:menu tag:PauseUpdates];
|
||||||
|
[self addTitle:NSLocalizedString(@"Update all feeds", nil) selector:@selector(updateAllFeeds:) key:@"" toMenu:menu tag:UpdateFeed];
|
||||||
|
[menu addItem:[NSMenuItem separatorItem]];
|
||||||
|
[self defaultHeaderForMenu:menu scope:ScopeGlobal];
|
||||||
|
|
||||||
|
for (FeedConfig *fc in [StoreCoordinator sortedFeedConfigItems]) {
|
||||||
|
[menu addItem:[self menuItemForFeedConfig:fc unread:&_unreadCountTotal]];
|
||||||
|
}
|
||||||
|
|
||||||
|
[menu addItem:[NSMenuItem separatorItem]];
|
||||||
|
[self addTitle:NSLocalizedString(@"Preferences", nil) selector:@selector(openPreferences) key:@"," toMenu:menu tag:0];
|
||||||
|
[menu addItemWithTitle:NSLocalizedString(@"Quit", nil) action:@selector(terminate:) keyEquivalent:@"q"];
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSMenuItem*)menuItemForFeedConfig:(FeedConfig*)fc unread:(int*)unread {
|
||||||
|
NSMenuItem *item;
|
||||||
|
if (fc.typ == SEPARATOR) {
|
||||||
|
item = [NSMenuItem separatorItem];
|
||||||
|
item.representedObject = [MenuItemInfo withID:fc.objectID];
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
int count = 0;
|
||||||
|
if (fc.typ == FEED) {
|
||||||
|
item = [self feedItem:fc unread:&count];
|
||||||
|
} else if (fc.typ == GROUP) {
|
||||||
|
item = [self groupItem:fc unread:&count];
|
||||||
|
}
|
||||||
|
*unread += count;
|
||||||
|
item.representedObject = [MenuItemInfo withID:fc.objectID];
|
||||||
|
[item markReadAndUpdateTitle:-count];
|
||||||
|
[self updateMenuHeader:item.submenu hasUnread:(count > 0)];
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSMenuItem*)feedItem:(FeedConfig*)fc unread:(int*)unread {
|
||||||
|
static NSImage *defaultRSSIcon;
|
||||||
|
if (!defaultRSSIcon)
|
||||||
|
defaultRSSIcon = [[[RSSIcon iconWithSize:NSMakeSize(16, 16)] autoGradient] image];
|
||||||
|
|
||||||
|
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:fc.name action:@selector(openFeedURL:) keyEquivalent:@""];
|
||||||
|
item.target = self;
|
||||||
|
item.submenu = [self defaultHeaderForMenu:nil scope:ScopeLocal];
|
||||||
|
for (FeedItem *obj in fc.feed.items) {
|
||||||
|
if (obj.unread) ++(*unread);
|
||||||
|
[item.submenu addItem:[self feedEntryItem:obj]];
|
||||||
|
}
|
||||||
|
item.toolTip = fc.feed.subtitle;
|
||||||
|
item.enabled = (fc.feed.items.count > 0);
|
||||||
|
item.image = defaultRSSIcon;
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSMenuItem*)groupItem:(FeedConfig*)fc unread:(int*)unread {
|
||||||
|
static NSImage *groupIcon;
|
||||||
|
if (!groupIcon) {
|
||||||
|
groupIcon = [NSImage imageNamed:NSImageNameFolder];
|
||||||
|
groupIcon.size = NSMakeSize(16, 16);
|
||||||
|
}
|
||||||
|
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:fc.name action:nil keyEquivalent:@""];
|
||||||
|
item.image = groupIcon;
|
||||||
|
item.submenu = [self defaultHeaderForMenu:nil scope:ScopeGroup];
|
||||||
|
for (FeedConfig *obj in fc.sortedChildren) {
|
||||||
|
NSMenuItem *subItem = [self menuItemForFeedConfig:obj unread:unread];
|
||||||
|
// *unread += [(MenuItemInfo*)subItem.representedObject unreadCount];
|
||||||
|
[item.submenu addItem:subItem];
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (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.toolTip = item.subtitle;
|
||||||
|
mi.enabled = (item.link.length > 0);
|
||||||
|
mi.state = (item.unread ? NSControlStateValueOn : NSControlStateValueOff);
|
||||||
|
mi.tag = ScopeLocal;
|
||||||
|
return mi;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
Append header items to menu accoring to user preferences.
|
||||||
|
|
||||||
|
@note If @c menu is @c nil a new menu is created and returned.
|
||||||
|
@param menu The menu where the items should be appended.
|
||||||
|
@param scope Tag will be concatenated with that scope (Global, Group or Local).
|
||||||
|
@return Will return the menu item provided or create a new one if menu was @c nil.
|
||||||
|
*/
|
||||||
|
- (NSMenu*)defaultHeaderForMenu:(NSMenu*)menu scope:(MenuItemTag)scope {
|
||||||
|
if (!menu) {
|
||||||
|
menu = [NSMenu new];
|
||||||
|
menu.autoenablesItems = NO;
|
||||||
|
}
|
||||||
|
// 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 unread", nil) selector:@selector(markAllUnread:) key:@"" toMenu:menu tag:MarkAllUnread | scope];
|
||||||
|
[self addTitle:NSLocalizedString(@"Open all unread", nil) selector:@selector(openAllUnread:) key:@"" toMenu:menu tag:OpenAllUnread | scope];
|
||||||
|
[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)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];
|
||||||
|
item.target = self;
|
||||||
|
item.tag = tag;
|
||||||
|
[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
|
||||||
|
|
||||||
|
|
||||||
|
- (void)openPreferences {
|
||||||
|
if (!self.prefWindow) {
|
||||||
|
self.prefWindow = [[Preferences alloc] initWithWindowNibName:@"Preferences"];
|
||||||
|
self.prefWindow.window.title = [NSString stringWithFormat:@"%@ %@", NSProcessInfo.processInfo.processName,
|
||||||
|
NSLocalizedString(@"Preferences", nil)];
|
||||||
|
// one time token to set reference to nil, which will release window
|
||||||
|
NSNotificationCenter * __weak center = [NSNotificationCenter defaultCenter];
|
||||||
|
id __block token = [center addObserverForName:NSWindowWillCloseNotification object:self.prefWindow.window queue:nil usingBlock:^(NSNotification *note) {
|
||||||
|
self.prefWindow = nil;
|
||||||
|
[center removeObserver:token];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
[NSApp activateIgnoringOtherApps:YES];
|
||||||
|
[self.prefWindow showWindow:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
- (void)pauseUpdates:(NSMenuItem*)sender {
|
||||||
|
NSLog(@"1pause");
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)updateAllFeeds:(NSMenuItem*)sender {
|
||||||
|
NSLog(@"1update all");
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)openAllUnread:(NSMenuItem*)sender {
|
||||||
|
__block int maxItemCount = INT_MAX;
|
||||||
|
NSMutableArray<NSURL*> *urls = [NSMutableArray<NSURL*> array];
|
||||||
|
[self siblingsDescendantFeedConfigs:sender block:^BOOL(FeedConfig *parent, FeedItem *item) {
|
||||||
|
if (maxItemCount <= 0)
|
||||||
|
return NO; // stop further processing
|
||||||
|
if (item.unread && item.link.length > 0) {
|
||||||
|
[urls addObject:[NSURL URLWithString:item.link]];
|
||||||
|
item.unread = NO;
|
||||||
|
--maxItemCount;
|
||||||
|
}
|
||||||
|
return YES;
|
||||||
|
}];
|
||||||
|
maxItemCount = INT_MAX;
|
||||||
|
int total = [sender siblingsDescendantItemInfo:^int(NSMenuItem *item, MenuItemInfo *info, int count) {
|
||||||
|
if (maxItemCount <= 0)
|
||||||
|
return -1; // stop further processing
|
||||||
|
if (info.hasUnread) {
|
||||||
|
[item markReadAndUpdateTitle:count];
|
||||||
|
--maxItemCount;
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} unreadEntriesOnly:YES];
|
||||||
|
[self updateAcestors:sender markRead:total];
|
||||||
|
[self openURLsWithPreferredBrowser:urls];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)markAllRead:(NSMenuItem*)sender {
|
||||||
|
[self siblingsDescendantFeedConfigs:sender block:^BOOL(FeedConfig *parent, FeedItem *item) {
|
||||||
|
if (item.unread)
|
||||||
|
item.unread = NO;
|
||||||
|
return YES;
|
||||||
|
}];
|
||||||
|
int total = [sender siblingsDescendantItemInfo:^int(NSMenuItem *item, MenuItemInfo *info, int count) {
|
||||||
|
if (info.hasUnread) {
|
||||||
|
[item markReadAndUpdateTitle:count];
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} unreadEntriesOnly:YES];
|
||||||
|
[self updateAcestors:sender markRead:total];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)markAllUnread:(NSMenuItem*)sender {
|
||||||
|
[self siblingsDescendantFeedConfigs:sender block:^BOOL(FeedConfig *parent, FeedItem *item) {
|
||||||
|
if (item.unread == NO)
|
||||||
|
item.unread = YES;
|
||||||
|
return YES;
|
||||||
|
}];
|
||||||
|
int total = [sender siblingsDescendantItemInfo:^int(NSMenuItem *item, MenuItemInfo *info, int count) {
|
||||||
|
if (count > info.unreadCount)
|
||||||
|
[item markReadAndUpdateTitle:(info.unreadCount - count)];
|
||||||
|
return count;
|
||||||
|
} unreadEntriesOnly:NO];
|
||||||
|
[self updateAcestors:sender markRead:([self getAncestorUnreadCount:sender] - total)];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)openFeedURL:(NSMenuItem*)sender {
|
||||||
|
MenuItemInfo *info = sender.representedObject;
|
||||||
|
if (![info isKindOfClass:[MenuItemInfo class]]) return;
|
||||||
|
|
||||||
|
id obj = [StoreCoordinator objectWithID:info.objID];
|
||||||
|
NSString *url = nil;
|
||||||
|
if ([obj isKindOfClass:[FeedConfig class]]) {
|
||||||
|
url = [[(FeedConfig*)obj feed] link];
|
||||||
|
} else if ([obj isKindOfClass:[FeedItem class]]) {
|
||||||
|
FeedItem *feed = obj;
|
||||||
|
url = [feed link];
|
||||||
|
if (info.hasUnread) {
|
||||||
|
feed.unread = NO;
|
||||||
|
[sender markReadAndUpdateTitle:1];
|
||||||
|
[self updateAcestors:sender markRead:1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!url || url.length == 0) return;
|
||||||
|
[self openURLsWithPreferredBrowser:@[[NSURL URLWithString:url]]];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)openURLsWithPreferredBrowser:(NSArray<NSURL*>*)urls {
|
||||||
|
if (urls.count == 0) return;
|
||||||
|
// [[NSWorkspace sharedWorkspace] openURLs:urls withAppBundleIdentifier:@"com.apple.Safari" options:NSWorkspaceLaunchDefault additionalEventParamDescriptor:nil launchIdentifiers:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#pragma mark - Iterating over items and propagating unread count
|
||||||
|
|
||||||
|
|
||||||
|
- (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;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)siblingsDescendantFeedConfigs:(NSMenuItem*)sender block:(FeedConfigRecursiveItemsBlock)block {
|
||||||
|
if (sender.parentItem) {
|
||||||
|
[[self requestFeedConfigForMenuItem:sender.parentItem] 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)
|
||||||
|
for (FeedConfig *config in [StoreCoordinator sortedFeedConfigItems]) {
|
||||||
|
if ([config descendantFeedItems:block] == NO)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)updateAcestors:(NSMenuItem*)sender markRead:(int)count {
|
||||||
|
[sender markAncestorsRead:count];
|
||||||
|
self.unreadCountTotal -= count;
|
||||||
|
if (self.unreadCountTotal < 0) {
|
||||||
|
NSLog(@"Should never happen. Global unread count < 0");
|
||||||
|
self.unreadCountTotal = 0;
|
||||||
|
}
|
||||||
|
[self updateBarIcon];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (int)getAncestorUnreadCount:(NSMenuItem*)sender {
|
||||||
|
MenuItemInfo *info = sender.parentItem.representedObject;
|
||||||
|
if ([info isKindOfClass:[MenuItemInfo class]])
|
||||||
|
return info.unreadCount;
|
||||||
|
return self.unreadCountTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
60
baRSS/Status Bar Menu/MenuItemInfo.h
Normal file
60
baRSS/Status Bar Menu/MenuItemInfo.h
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
//
|
||||||
|
// 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>
|
||||||
|
|
||||||
|
/**
|
||||||
|
Object storing the corresponding Core Data object id and an unread counter.
|
||||||
|
*/
|
||||||
|
@interface MenuItemInfo : NSObject
|
||||||
|
/// Core Data store ID
|
||||||
|
@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 unread:(int)count;
|
||||||
|
|
||||||
|
- (BOOL)hasUnread;
|
||||||
|
- (void)markRead:(int)count;
|
||||||
|
@end
|
||||||
|
|
||||||
|
|
||||||
|
@interface NSMenuItem (MenuItemInfo)
|
||||||
|
/**
|
||||||
|
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.
|
||||||
|
@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.
|
||||||
|
*/
|
||||||
|
typedef int (^MenuItemInfoRecursiveBlock) (NSMenuItem *item, MenuItemInfo *info, int count);
|
||||||
|
|
||||||
|
- (int)unreadCount;
|
||||||
|
- (int)siblingsDescendantItemInfo:(MenuItemInfoRecursiveBlock)block unreadEntriesOnly:(BOOL)flag;
|
||||||
|
- (void)markAncestorsRead:(int)count;
|
||||||
|
- (void)markReadAndUpdateTitle:(int)count;
|
||||||
|
@end
|
||||||
|
|
||||||
158
baRSS/Status Bar Menu/MenuItemInfo.m
Normal file
158
baRSS/Status Bar Menu/MenuItemInfo.m
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
//
|
||||||
|
// 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"
|
||||||
|
|
||||||
|
|
||||||
|
@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 retrieve the unread count from info.
|
||||||
|
|
||||||
|
@return Unread count stored in menu info.
|
||||||
|
*/
|
||||||
|
- (int)unreadCount {
|
||||||
|
MenuItemInfo *info = self.representedObject;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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 {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
Reference in New Issue
Block a user