46 Commits

Author SHA1 Message Date
relikd
4864208754 chore: update changelog 2025-12-13 00:24:09 +01:00
relikd
76c7263548 doc: remove reference to obsolete QLOPML 2025-12-13 00:02:15 +01:00
relikd
7f40bb259c fix: appearance background color + tabbar transparency 2025-12-13 00:01:48 +01:00
relikd
68aa4ef94b fix: tooltip strings 2025-12-12 14:56:12 +01:00
relikd
217a91b23c feat: option to configure "Open a few unread" 2025-12-12 14:45:52 +01:00
relikd
f0299d8246 fix: spacing + alignment (Appearance settings) 2025-12-12 14:07:43 +01:00
relikd
ae0d5967c7 fix: allow 0 for input field (Appearance settings) 2025-12-12 14:06:44 +01:00
relikd
d45d4864b0 fix: mouseDown on appearance view, not FlippedView 2025-12-12 14:02:49 +01:00
relikd
ef2c588f4c fix: better handling of open-a-few limit 2025-12-12 14:01:53 +01:00
relikd
03aecdfa4a feat: version migration for new option names 2025-12-11 18:34:13 +01:00
relikd
3b65bca88f ref: appearance settings 2025-12-11 18:33:53 +01:00
relikd
bd03059247 ref: rename pref options 2025-12-11 18:33:35 +01:00
relikd
d03840757a feat: Appearance settings v2 2025-12-11 15:51:10 +01:00
relikd
2e77f67102 feat: introduce new Pref_article options 2025-12-11 15:46:40 +01:00
relikd
5d339b8125 ref: rename pref key "articleTooltipLimit" 2025-12-11 15:25:50 +01:00
relikd
65cac6b19a feat: expose more NSView methods 2025-12-11 15:21:14 +01:00
relikd
2ec1743dd9 ref: rename menu item "Show hidden feeds" 2025-12-11 15:20:46 +01:00
relikd
ca2b3cb887 fix: accessibility strings 2025-12-11 15:20:23 +01:00
relikd
b1ca30f914 ref: wrapInScrollView 2025-12-11 15:16:39 +01:00
relikd
5427cb58ee feat: uint formatter with units 2025-12-11 15:10:32 +01:00
relikd
b94dd030b4 ref: cleaner "menu bar icon" 2025-12-11 15:07:56 +01:00
relikd
469d7bcdd4 feat: separator with direction flip 2025-12-09 15:08:20 +01:00
relikd
0806003fc3 fix: draw separator in bounds 2025-12-09 15:07:35 +01:00
relikd
385bcf99f3 ref: uint-formatter on NSView+Ext 2025-12-09 15:06:43 +01:00
relikd
b194a1427d feat: add svg artwork 2025-12-09 00:30:25 +01:00
relikd
ff34781fea ref: simplify regex icon 2025-12-09 00:28:29 +01:00
relikd
4edd4448ae ref: pixel-perfect rss icon alignment 2025-12-09 00:10:54 +01:00
relikd
33f907228b ref: simplify rss icon path 2025-12-08 23:31:09 +01:00
relikd
673e0d3d48 fix: quadratic curve 2025-12-08 23:30:50 +01:00
relikd
b3fdadb9f4 feat: feed group icon 2025-12-08 22:40:11 +01:00
relikd
9fc513254f fix: pixel-perfect group icon 2025-12-08 22:31:49 +01:00
relikd
881b9db02c ref: flip coordinate system 2025-12-08 21:43:24 +01:00
relikd
3a14c90f37 ref: split svgRect and svgRoundedRect 2025-12-08 21:36:49 +01:00
relikd
96884474ac ref: unread dot icon 2025-12-08 21:21:29 +01:00
relikd
82ae18c8a5 ref: pixel-perfect main menu icon (+feed icon) 2025-12-08 21:10:20 +01:00
relikd
6eddb57651 ref: svg rss icon 2025-12-08 21:09:38 +01:00
relikd
67d17599b5 ref: default rss icon 2025-12-08 19:05:27 +01:00
relikd
3507fd8e27 feat: appearance settings article icon 2025-12-08 19:04:53 +01:00
relikd
ca417f35b6 ref: rename drawing methods 2025-12-08 17:36:48 +01:00
relikd
6e5326f913 feat: new menubar icon for Appearance settings 2025-12-08 16:32:39 +01:00
relikd
1589b23aa9 fix: TinySVG rect scaling 2025-12-08 16:31:53 +01:00
relikd
e0cd04b882 feat: new group icon (svg) 2025-12-08 14:59:48 +01:00
relikd
6b4c38ec21 feat: TinySVG support for quadratic curves 2025-12-08 14:49:40 +01:00
relikd
e7208ae2ab fix: variable name 2025-12-08 14:13:09 +01:00
relikd
508377a823 fix: limit tooltip to 2000 characters 2025-12-05 22:24:29 +01:00
relikd
2185eb76fb fix: uniform menu titles 2025-12-05 14:11:48 +01:00
33 changed files with 738 additions and 462 deletions

View File

@@ -5,6 +5,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.6.0] 2025-12-13
### Added
- *UI:* Limit content length for article tooltips. (fixes #25)
- *Settings, Appearance:* Revamped appearance options v2. (thanks @Shnub)
- *Settings, Appearance:* New GUI options for previously CLI-only options. Modify display limits directly in settings.
### Fixed
- *Status Bar Menu:* Uniform capitalization for all menu items.
- *Status Bar Menu:* Setting the "Open a few"-limit to zero, hides the button altogether.
- *Settings, Appearance:* Pixel-perfect alignment of all drawable icons.
- *UI:* Accessibility hints for appearance options generate better VoiceOver output.
### Changed
- *UI:* "Show Hidden Article" renamed to "Show hidden feeds".
## [1.5.5] 2025-12-03
### Added
- *Settings, Appearance:* Improved tooltips on individual options
@@ -245,6 +261,7 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
Initial release
[1.6.0]: https://github.com/relikd/baRSS/compare/v1.5.5...v1.6.0
[1.5.5]: https://github.com/relikd/baRSS/compare/v1.5.4...v1.5.5
[1.5.4]: https://github.com/relikd/baRSS/compare/v1.5.3...v1.5.4
[1.5.3]: https://github.com/relikd/baRSS/compare/v1.5.2...v1.5.3

View File

@@ -6,7 +6,7 @@ CODE_SIGN_IDENTITY = Apple Development
ENABLE_HARDENED_RUNTIME = YES
MACOSX_DEPLOYMENT_TARGET = 10.14
MARKETING_VERSION = 1.5.5
MARKETING_VERSION = 1.6.0
PRODUCT_NAME = baRSS
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.baRSS
CURRENT_PROJECT_VERSION = 16970
CURRENT_PROJECT_VERSION = 17752

View File

@@ -42,18 +42,13 @@ Go to [releases](https://github.com/relikd/baRSS/releases) and downloaded the la
Searching for the App Store release? Read this [notice](#app-store-notice).
### Build from source
You'll need Xcode, [RSXML2] \(required), and [QLOPML] \(optional).
You'll need Xcode and [RSXML2].
```sh
git clone https://github.com/relikd/baRSS
git clone https://github.com/relikd/RSXML2
git clone https://github.com/relikd/QLOPML
```
Alternatively, you can simply delete the `QLOPML` project reference without much harm.
`QLOPML` is a Quick Look plugin for `.opml` files.
It will display the file contents whenever you hit spacebar.
That's it.
Open `baRSS/baRSS.xcodeproj` and build the project.
Note, there are some compiler flags that append 'beta' to the development release.
@@ -95,33 +90,14 @@ Most likely, you will never stumble upon these if not reading this chapter.
**Note:** To reset an option run `defaults delete de.relikd.baRSS {KEY}`, where `{KEY}` is an option from below.
1. When holding down the option key, the menu will show an item to open only a few unread items at a time.
This number can be changed with the following Terminal command (default: 10):
```
defaults write de.relikd.baRSS openFewLinksLimit -int 10
```
2. In preferences you can choose to show 'Short article names'.
This will limit the number of displayed characters to 60 (default).
With this Terminal command you can customize this limit:
```
defaults write de.relikd.baRSS shortArticleNamesLimit -int 50
```
3. Limit the number of displayed articles per feed menu.
**Note:** displayed unread count may be different than the unread items inside. 'Open all unread' will open hidden items too.
```
defaults write de.relikd.baRSS articlesInMenuLimit -int 40
```
4. You can change the appearance of colors throughout the application.
1. You can change the appearance of colors throughout the application.
E.g., The tint color of the menu bar icon and the color of the blue unread articles dot.
```
defaults write de.relikd.baRSS colorStatusIconTint -string "#37F"
defaults write de.relikd.baRSS colorUnreadIndicator -string "#FBA33A"
```
5. To backup your list of subscribed feeds, here is a one-liner:
2. To backup your list of subscribed feeds, here is a one-liner:
```
open barss:backup && cp "$HOME/Library/Containers/de.relikd.baRSS/Data/Library/Application Support/baRSS/backup/feeds_latest.opml" "$HOME/Desktop/baRSS_backup_$(date "+%Y-%m-%d").opml"
```
@@ -188,6 +164,7 @@ Sadly, this was before Swift 5 and ABI stability.
Had I only started the project a year later…
But on the other hand, now it is macOS 10.12 compatible.
### 3rd Party Libraries
This project uses a modified version of Brent Simmons' [RSXML] for feed parsing.
@@ -197,10 +174,9 @@ This project uses a modified version of Brent Simmons' [RSXML] for feed parsing.
##### Trivia
- Start of project: __July 19, 2018__
- Estimated development time: __2053h+__
- Estimated development time: __2121h+__
- First prototype used __feedparser python__ library
[QLOPML]: https://github.com/relikd/QLOPML
[RSXML2]: https://github.com/relikd/RSXML2
[RSXML]: https://github.com/brentsimmons/RSXML

View File

@@ -22,6 +22,7 @@
544F5A752E30EFC700674F81 /* style.css in Resources */ = {isa = PBXBuildFile; fileRef = 544F5A722E30EFC700674F81 /* style.css */; };
544F5A762E30EFC700674F81 /* opml-lib.m in Sources */ = {isa = PBXBuildFile; fileRef = 544F5A702E30EFC700674F81 /* opml-lib.m */; };
54501010230E9C8600F0B165 /* FeedDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 5450100F230E9C8600F0B165 /* FeedDownload.m */; };
545EB5DA2EE8622200FABBE0 /* StrictUIntFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 545EB5D92EE8622200FABBE0 /* StrictUIntFormatter.m */; };
5469E13C2EA90C6C00D46CE7 /* NotifyEndpoint.m in Sources */ = {isa = PBXBuildFile; fileRef = 5469E13B2EA90C6C00D46CE7 /* NotifyEndpoint.m */; };
546A6A2922C583390034E806 /* SettingsGeneralView.m in Sources */ = {isa = PBXBuildFile; fileRef = 54D857D122802309001BA1C8 /* SettingsGeneralView.m */; };
546A6A2C22C584AF0034E806 /* SettingsAppearanceView.m in Sources */ = {isa = PBXBuildFile; fileRef = 546A6A2A22C584AF0034E806 /* SettingsAppearanceView.m */; };
@@ -150,6 +151,8 @@
544F5A722E30EFC700674F81 /* style.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = style.css; sourceTree = "<group>"; };
5450100E230E9C8600F0B165 /* FeedDownload.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FeedDownload.h; sourceTree = "<group>"; };
5450100F230E9C8600F0B165 /* FeedDownload.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FeedDownload.m; sourceTree = "<group>"; };
545EB5D62EE8620300FABBE0 /* StrictUIntFormatter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StrictUIntFormatter.h; sourceTree = "<group>"; };
545EB5D92EE8622200FABBE0 /* StrictUIntFormatter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = StrictUIntFormatter.m; sourceTree = "<group>"; };
5469E13A2EA90C6C00D46CE7 /* NotifyEndpoint.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotifyEndpoint.h; sourceTree = "<group>"; };
5469E13B2EA90C6C00D46CE7 /* NotifyEndpoint.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotifyEndpoint.m; sourceTree = "<group>"; };
546A6A2A22C584AF0034E806 /* SettingsAppearanceView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsAppearanceView.m; sourceTree = "<group>"; };
@@ -515,6 +518,8 @@
54910066233A4D4000858AE2 /* URLScheme.m */,
54229F532E02491A0019ACB0 /* TinySVG.h */,
54229F542E02491A0019ACB0 /* TinySVG.m */,
545EB5D62EE8620300FABBE0 /* StrictUIntFormatter.h */,
545EB5D92EE8622200FABBE0 /* StrictUIntFormatter.m */,
);
path = Helper;
sourceTree = "<group>";
@@ -732,6 +737,7 @@
546FC43D21188AD5007CC3A3 /* SettingsFeeds.m in Sources */,
54253C952C49BFE400742695 /* RegexConverterView.m in Sources */,
548C6D0A230C33DE003A1AAF /* NSURL+Ext.m in Sources */,
545EB5DA2EE8622200FABBE0 /* StrictUIntFormatter.m in Sources */,
54E88320211B509D00064188 /* ModalFeedEdit.m in Sources */,
54195883218A061100581B79 /* Feed+Ext.m in Sources */,
5469E13C2EA90C6C00D46CE7 /* NotifyEndpoint.m in Sources */,

View File

@@ -59,7 +59,44 @@
/// Called during application start. Perform any version migration updates here.
- (void)migrateVersionUpdate {
// Currently unused, but you'll be thankful in the future for a previously saved version number
[StoreCoordinator setOption:@"app-version" value: UserPrefsAppVersion()];
// thank you, past-self! but it would have been nice to have easier "<=" comparison
NSString *prevVersion = [StoreCoordinator optionForKey:@"app-version"];
NSString *curVersion = UserPrefsAppVersion();
// migrate if not run for the first time
if (prevVersion != nil) {
if ([prevVersion isEqualToString:curVersion]) {
return; // migration already performed
}
// else: migrate
NSInteger ver = 0;
for (NSString *part in [prevVersion componentsSeparatedByString:@"."]) {
ver = ver * 100 + [part integerValue];
}
if (ver <= 10505) { // v1.5.5
[self migrate_v1_6_0];
}
}
[StoreCoordinator setOption:@"app-version" value:curVersion];
}
- (void)migrate_v1_6_0 {
NSLog(@"Migrating to v1.6.0");
// rename options
BOOL shouldLimitCount = UserPrefsBool(@"feedLimitArticles"); // default: NO
if (shouldLimitCount) {
NSInteger prev = UserPrefsInt(@"articlesInMenuLimit"); // default: 40
UserPrefsSetInt(Pref_articleCountLimit, prev == 0 ? 40 : prev);
}
BOOL shouldLimitTitle = UserPrefsBool(@"feedTruncateTitle"); // default: NO
if (shouldLimitTitle) {
NSInteger prev = UserPrefsInt(@"shortArticleNamesLimit"); // default: 60
UserPrefsSetInt(Pref_articleTitleLimit, prev == 0 ? 60 : prev);
}
// delete old keys
UserPrefsSet(@"feedLimitArticles", nil);
UserPrefsSet(@"feedTruncateTitle", nil);
UserPrefsSet(@"articlesInMenuLimit", nil);
UserPrefsSet(@"shortArticleNamesLimit", nil);
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<rect y="14" width="16" height="1"/>
<rect y="10" width="16" height="1"/>
<rect x="9" y="6" width="7" height="1"/>
<rect x="9" y="2" width="7" height="1"/>
<rect x="1" y="1" width="7" height="7"/>
</svg>

After

Width:  |  Height:  |  Size: 313 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g fill="none" stroke="#000">
<path d="M3,13.5c-1.5,0-2.5-1-2.5-2.5V3.5c0-1.5.5-2,2-2h1.5c1.5,0,1.5,1,3,1h6c1.5,0,2.5,1,2.5,2.5v6c0,1.5-1,2.5-2.5,2.5H3Z"/>
<path d="M1.5,5h13Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 294 B

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<!-- menu -->
<rect x="0" y="0" width="16" height="3"/>
<rect x="5" y="4" width="9" height="12"/>
<rect x="6" y="3" width="7" height="12" fill="#aaa"/>
<!-- entries -->
<rect x="6" y="12" width="6" height="1"/>
<rect x="6" y="9" width="6" height="1"/>
<rect x="6" y="6" width="6" height="1"/>
</svg>

After

Width:  |  Height:  |  Size: 415 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path d="M18,19c-14,21-13,43,0,62l-7,4C-4,63-4,35,12,14l6,5Z"/>
<circle cx="31" cy="67" r="7"/>
<path d="M65,28l11-4,2,6-11,4,7,9-5,4-7-9-7,9-5-4,7-9-11-4,2-6,11,4v-11h6v11Z"/>
<path d="M82,81c14-21,13-43,0-62l7-5c16,22,15,50,0,71l-7-4Z"/>
</svg>

After

Width:  |  Height:  |  Size: 356 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="13" cy="87" r="13"/>
<path d="M0,35q65,0,65,65h-20q0,-45,-45,-45z"/>
<rect x="60" y="0" width="15" height="50"/>
<rect x="85" y="0" width="15" height="50"/>
</svg>

After

Width:  |  Height:  |  Size: 281 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="13" cy="87" r="13"/>
<path d="M0,35q65,0,65,65h-20q0,-45,-45,-45z"/>
<path d="M0,0q100,0,100,100h-20q0,-80,-80,-80z"/>
</svg>

After

Width:  |  Height:  |  Size: 242 B

View File

@@ -21,21 +21,25 @@ static NSString* const auxiliaryAppURL = @"https://github.com/relikd/URL-Scheme-
/// Default RSS icon (with border, with gradient, orange)
static NSImageName const RSSImageDefaultRSSIcon = @"RSSImageDefaultRSSIcon";
/// Settings, global icon (menu bar, black)
static NSImageName const RSSImageSettingsGlobal = @"RSSImageSettingsGlobal";
static NSImageName const RSSImageDefaultRSSIcon = @"RSSImageDefaultRSSIcon";
/// Settings, global statusbar icon (rss icon with neighbor icons)
static NSImageName const RSSImageSettingsGlobalIcon = @"RSSImageSettingsGlobalIcon";
/// Settings, global menu icon (menu bar, black)
static NSImageName const RSSImageSettingsGlobalMenu = @"RSSImageSettingsGlobalMenu";
/// Settings, group icon (folder, black)
static NSImageName const RSSImageSettingsGroup = @"RSSImageSettingsGroup";
static NSImageName const RSSImageSettingsGroup = @"RSSImageSettingsGroup";
/// Settings, feed icon (RSS, no border, no gradient, black)
static NSImageName const RSSImageSettingsFeed = @"RSSImageSettingsFeed";
static NSImageName const RSSImageSettingsFeed = @"RSSImageSettingsFeed";
/// Settings, article icon (RSS surrounded by text lines)
static NSImageName const RSSImageSettingsArticle = @"RSSImageSettingsArticle";
/// Menu bar, bar icon (RSS, with border, no gradient, orange)
static NSImageName const RSSImageMenuBarIconActive = @"RSSImageMenuBarIconActive";
static NSImageName const RSSImageMenuBarIconActive = @"RSSImageMenuBarIconActive";
/// Menu bar, bar icon (RSS, with border, no gradient, paused, orange)
static NSImageName const RSSImageMenuBarIconPaused = @"RSSImageMenuBarIconPaused";
static NSImageName const RSSImageMenuBarIconPaused = @"RSSImageMenuBarIconPaused";
/// Menu item, unread state icon (blue dot)
static NSImageName const RSSImageMenuItemUnread = @"RSSImageMenuItemUnread";
static NSImageName const RSSImageMenuItemUnread = @"RSSImageMenuItemUnread";
/// Feed edit, regex editor icon @c "(.*)"
static NSImageName const RSSImageRegexIcon = @"RSSImageRegexIcon";
static NSImageName const RSSImageRegexIcon = @"RSSImageRegexIcon";
#pragma mark - NSNotificationName constants

View File

@@ -47,10 +47,9 @@
NSString *title = self.title;
if (!title) return @"";
// TODO: It should be enough to get user prefs once per menu build
if (UserPrefsBool(Pref_feedTruncateTitle)) {
NSUInteger limit = UserPrefsUInt(Pref_shortArticleNamesLimit);
if (title.length > limit)
title = [[title substringToIndex:limit] stringByAppendingString:@"…"];
NSUInteger limit = UserPrefsUInt(Pref_articleTitleLimit); // -1 will become MAX_INT
if (limit > 0 && title.length > limit) {
title = [[title substringToIndex:limit] stringByAppendingString:@"…"];
}
return title;
}
@@ -60,10 +59,17 @@
NSMenuItem *item = [NSMenuItem new];
item.title = [self shortArticleName];
item.enabled = (self.link.length > 0);
item.state = (self.unread && UserPrefsBool(Pref_feedUnreadIndicator) ? NSControlStateValueOn : NSControlStateValueOff);
item.state = (self.unread && UserPrefsBool(Pref_articleUnreadIndicator) ? NSControlStateValueOn : NSControlStateValueOff);
item.onStateImage = [NSImage imageNamed:RSSImageMenuItemUnread];
item.accessibilityLabel = (self.unread ? NSLocalizedString(@"article: unread", @"accessibility label, feed menu item") : NSLocalizedString(@"article: read", @"accessibility label, feed menu item"));
item.toolTip = (self.abstract ? self.abstract : self.body); // fall back to body (html)
// truncate tooltip
NSUInteger limit = UserPrefsUInt(Pref_articleTooltipLimit); // -1 will become MAX_INT
if (limit > 0) {
NSString *tooltip = (self.abstract ? self.abstract : self.body); // fall back to body (html)
if (tooltip.length > limit)
tooltip = [[tooltip substringToIndex:limit] stringByAppendingString:@"…\n[…]"];
item.toolTip = tooltip;
}
item.representedObject = self.objectID;
item.target = [self class];
item.action = @selector(didClickOnMenuItem:);

View File

@@ -3,6 +3,8 @@
/// Draw separator line in @c NSOutlineView
IB_DESIGNABLE
@interface DrawSeparator : NSView
@property (assign) BOOL invert;
+ (instancetype)withSize:(NSSize)size;
@end

View File

@@ -5,10 +5,15 @@
@implementation DrawSeparator
+ (instancetype)withSize:(NSSize)size {
return [[super alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height)];
}
- (void)drawRect:(NSRect)r {
NSColor *color = [NSColor darkGrayColor];
NSGradient *grdnt = [[NSGradient alloc] initWithStartingColor:color endingColor:[color colorWithAlphaComponent:0.0]];
NSRect separatorRect = NSMakeRect(1, NSMidY(self.frame) - 1, NSWidth(self.frame) - 2, 2);
NSColor *transparent = [color colorWithAlphaComponent:0.0];
NSGradient *grdnt = [[NSGradient alloc] initWithStartingColor:self.invert ? transparent : color endingColor:self.invert ? color : transparent];
NSRect separatorRect = NSMakeRect(1, NSMidY(self.bounds) - 1, NSWidth(self.bounds) - 2, 2);
NSBezierPath *rounded = [NSBezierPath bezierPathWithRoundedRect:separatorRect xRadius:1 yRadius:1];
[grdnt drawInBezierPath:rounded angle:0];
}
@@ -23,126 +28,18 @@ static inline const CGFloat ShorterSide(NSSize s) {
return (s.width < s.height ? s.width : s.height);
}
/// Perform @c CGAffineTransform with custom rotation point
// CGAffineTransform RotateAroundPoint(CGAffineTransform at, CGFloat angle, CGFloat x, CGFloat y) {
// at = CGAffineTransformTranslate(at, x, y);
// at = CGAffineTransformRotate(at, angle);
// return CGAffineTransformTranslate(at, -x, -y);
/// Flip coordinate system
//static void FlipCoordinateSystem(CGContextRef c, CGFloat height) {
// CGContextTranslateCTM(c, 0, height);
// CGContextScaleCTM(c, 1, -1);
//}
#pragma mark - CGPath Component Generators
/// Add circle with @c radius
static inline void PathAddCircle(CGMutablePathRef path, CGFloat radius) {
CGPathAddArc(path, NULL, radius, radius, radius, 0, M_PI * 2, YES);
}
/// Add ring with @c radius and @c innerRadius
static inline void PathAddRing(CGMutablePathRef path, CGFloat radius, CGFloat innerRadius) {
CGPathAddArc(path, NULL, radius, radius, radius, 0, M_PI * 2, YES);
CGPathAddArc(path, NULL, radius, radius, innerRadius, 0, M_PI * -2, YES);
}
/// Add a single RSS icon radio wave
static inline void PathAddRSSArc(CGMutablePathRef path, CGFloat radius, CGFloat thickness) {
CGPathMoveToPoint(path, NULL, 0, radius + thickness);
CGPathAddArc(path, NULL, 0, 0, radius + thickness, M_PI_2, 0, YES);
CGPathAddLineToPoint(path, NULL, radius, 0);
CGPathAddArc(path, NULL, 0, 0, radius, 0, M_PI_2, NO);
CGPathCloseSubpath(path);
}
/// Add two vertical bars representing a pause icon
static inline void PathAddPauseIcon(CGMutablePathRef path, CGAffineTransform at, CGFloat size, CGFloat thickness) {
const CGFloat off = (size - 2 * thickness) / 4;
CGPathAddRect(path, &at, CGRectMake(off, 0, thickness, size));
CGPathAddRect(path, &at, CGRectMake(size/2 + off, 0, thickness, size));
}
/// Add X icon by applying a rotational affine transform and drawing a plus sign
// void PathAddXIcon(CGMutablePathRef path, CGAffineTransform at, CGFloat size, CGFloat thickness) {
// at = RotateAroundPoint(at, M_PI_4, size/2, size/2);
// const CGFloat p = size * 0.5 - thickness / 2;
// CGPathAddRect(path, &at, CGRectMake(0, p, size, thickness));
// CGPathAddRect(path, &at, CGRectMake(p, 0, thickness, p));
// CGPathAddRect(path, &at, CGRectMake(p, p + thickness, thickness, p));
//}
#pragma mark - Full Icon Path Generators
/// Create @c CGPath for global icon; a menu bar and an open menu below
static inline void AddGlobalIconPath(CGContextRef c, CGFloat size) {
CGMutablePathRef menu = CGPathCreateMutable();
CGPathAddRect(menu, NULL, CGRectMake(0, 0.8 * size, size, 0.2 * size));
CGPathAddRect(menu, NULL, CGRectMake(0.3 * size, 0, 0.55 * size, 0.75 * size));
CGPathAddRect(menu, NULL, CGRectMake(0.35 * size, 0.05 * size, 0.45 * size, 0.75 * size));
CGFloat entryHeight = 0.1 * size; // 0.075
for (int i = 0; i < 3; i++) { // 4
//CGPathAddRect(menu, NULL, CGRectMake(0.37 * size, (2 * i + 1) * entryHeight, 0.42 * size, entryHeight)); // uncomment path above
CGPathAddRect(menu, NULL, CGRectMake(0.35 * size, (2 * i + 1.5) * entryHeight, 0.4 * size, entryHeight * 0.8));
}
CGContextAddPath(c, menu);
CGPathRelease(menu);
}
/// Create @c CGPath for group icon; a folder symbol
static inline void AddGroupIconPath(CGContextRef c, CGFloat size, BOOL showBackground) {
const CGFloat r1 = size * 0.05; // corners
const CGFloat r2 = size * 0.08; // upper part, name tag
const CGFloat r3 = size * 0.15; // lower part, corners inside
const CGFloat posTop = 0.85 * size;
const CGFloat posMiddle = 0.6 * size - r3;
const CGFloat posBottom = 0.15 * size + r1;
const CGFloat posNameTag = 0.3 * size;
CGMutablePathRef upper = CGPathCreateMutable();
CGPathMoveToPoint(upper, NULL, 0, 0.5 * size);
CGPathAddLineToPoint(upper, NULL, 0, posTop - r1);
CGPathAddArc(upper, NULL, r1, posTop - r1, r1, M_PI, M_PI_2, YES);
CGPathAddArc(upper, NULL, posNameTag, posTop - r2, r2, M_PI_2, M_PI_4, YES);
CGPathAddArc(upper, NULL, posNameTag + 1.85 * r2, posTop, r2, M_PI + M_PI_4, -M_PI_2, NO);
CGPathAddArc(upper, NULL, size - r1, posTop - r1 - r2, r1, M_PI_2, 0, YES);
CGPathAddArc(upper, NULL, size - r1, posBottom, r1, 0, -M_PI_2, YES);
CGPathAddArc(upper, NULL, r1, posBottom, r1, -M_PI_2, M_PI, YES);
CGPathCloseSubpath(upper);
CGMutablePathRef lower = CGPathCreateMutable();
CGPathAddArc(lower, NULL, r3, posMiddle, r3, M_PI, M_PI_2, YES);
CGPathAddArc(lower, NULL, size - r3, posMiddle, r3, M_PI_2, 0, YES);
CGPathAddArc(lower, NULL, size - r1, posBottom, r1, 0, -M_PI_2, YES);
CGPathAddArc(lower, NULL, r1, posBottom, r1, -M_PI_2, M_PI, YES);
CGPathCloseSubpath(lower);
CGContextAddPath(c, upper);
if (showBackground)
CGContextEOFillPath(c);
CGContextAddPath(c, lower);
CGPathRelease(upper);
CGPathRelease(lower);
}
/**
Create @c CGPath for RSS icon; a circle in the lower left bottom and two radio waves going outwards.
@param connection If @c NO, draw only one radio wave and a pause icon in the upper right
*/
static inline void AddRSSIconPath(CGContextRef c, CGFloat size, BOOL connection) {
CGMutablePathRef bars = CGPathCreateMutable(); // the rss bars
PathAddCircle(bars, size * 0.125);
PathAddRSSArc(bars, size * 0.45, size * 0.2);
if (connection) {
PathAddRSSArc(bars, size * 0.8, size * 0.2);
} else {
CGAffineTransform at = CGAffineTransformMake(0.5, 0, 0, 0.5, size/2, size/2);
PathAddPauseIcon(bars, at, size, size * 0.3);
//PathAddXIcon(bars, at, size, size * 0.3);
}
CGContextAddPath(c, bars);
CGPathRelease(bars);
/// Scale and translate context to the center with respect to the new scale. If @c width @c != @c length align top left.
static void SetContentScale(CGContextRef c, CGSize size, CGFloat scale) {
const CGFloat s = ShorterSide(size);
CGFloat offset = s * (1 - scale) / 2;
CGContextTranslateCTM(c, offset, size.height - s + offset); // top left alignment
CGContextScaleCTM(c, scale, scale);
}
@@ -168,130 +65,214 @@ static void DrawGradient(CGContextRef c, CGFloat size, NSColor *color) {
CFArrayRef colors = CFArrayCreate(NULL, cgColors, 3, NULL);
CGGradientRef gradient = CGGradientCreateWithColors(NULL, colors, NULL);
CGContextDrawLinearGradient(c, gradient, CGPointMake(0, size), CGPointMake(size, 0), kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
CGContextDrawLinearGradient(c, gradient, CGPointMake(0, 0), CGPointMake(size, size), kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
CGGradientRelease(gradient);
CFRelease(colors);
}
#pragma mark - CGContext Drawing & Manipulation
#pragma mark - RSS Icon (rounded corners)
/// Flip coordinate system
static void FlipCoordinateSystem(CGContextRef c, CGFloat height) {
CGContextTranslateCTM(c, 0, height);
CGContextScaleCTM(c, 1, -1);
}
/// Scale and translate context to the center with respect to the new scale. If @c width @c != @c length align top left.
static void SetContentScale(CGContextRef c, CGSize size, CGFloat scale) {
const CGFloat s = ShorterSide(size);
CGFloat offset = s * (1 - scale) / 2;
CGContextTranslateCTM(c, offset, size.height - s + offset); // top left alignment
CGContextScaleCTM(c, scale, scale);
}
/// Helper method; set drawing color, add rounded background and prepare content scale
static void DrawRoundedFrame(CGContextRef c, CGRect r, CGColorRef color, BOOL background, CGFloat corner, CGFloat defaultScale, CGFloat scaling) {
CGContextSetFillColorWithColor(c, color);
CGContextSetStrokeColorWithColor(c, color);
CGFloat contentScale = defaultScale;
if (background) {
svgAddRect(c, 1, r, ShorterSide(r.size) * corner/2);
if (scaling != 0.0)
contentScale *= scaling;
/**
Create @c CGPath for RSS icon; a circle in the lower left bottom and two radio waves going outwards.
@param connection If @c NO, draw only one radio wave and a pause icon in the upper right
*/
static inline void AddRSSIconPath(CGContextRef c, CGFloat size, BOOL connection) {
svgCircle(c, size/100, 13, 87, 13, NO);
svgPath(c, size/100, "M0,35q65,0,65,65h-20q0,-45,-45,-45z");
if (connection) {
svgPath(c, size/100, "M0,0q100,0,100,100h-20q0,-80,-80,-80z");
} else {
// pause icon
svgRect(c, size/100, CGRectMake(60, 0, 15, 50));
svgRect(c, size/100, CGRectMake(85, 0, 15, 50));
}
SetContentScale(c, r.size, contentScale);
}
#pragma mark - Easy Icon Drawing Methods
/// Draw global icon (menu bar)
static void DrawGlobalIcon(CGRect r, CGColorRef color, BOOL background) {
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
DrawRoundedFrame(c, r, color, background, 0.4, 1.0, 0.7);
AddGlobalIconPath(c, ShorterSide(r.size));
CGContextEOFillPath(c);
}
/// Draw group icon (folder)
static void DrawGroupIcon(CGRect r, CGColorRef color, BOOL background) {
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
const CGFloat s = ShorterSide(r.size);
const CGFloat l = s * 0.08; // line width
DrawRoundedFrame(c, r, color, background, 0.4, 1.0 - (l / s), 0.85);
CGContextSetLineWidth(c, l * (background ? 0.5 : 1.0));
AddGroupIconPath(c, s, background);
CGContextStrokePath(c);
}
/// Draw RSS icon (flat without gradient)
static void DrawRSSIcon(CGRect r, CGColorRef color, BOOL background, BOOL connection) {
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
DrawRoundedFrame(c, r, color, background, 0.4, 1.0, 0.7);
AddRSSIconPath(c, ShorterSide(r.size), connection);
CGContextEOFillPath(c);
}
/// Draw RSS icon (with orange gradient, corner @c 0.4, white radio waves)
static void DrawRSSGradientIcon(CGRect r, NSColor *color) {
/// Draw monochrome RSS icon with rounded corners
static void RoundedRSS_Monochrome(CGRect r, BOOL connection) {
const CGFloat size = ShorterSide(r.size);
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
DrawRoundedFrame(c, r, NSColor.whiteColor.CGColor, YES, 0.4, 1.0, 0.7);
CGContextSetFillColorWithColor(c, [NSColor menuBarIconColor].CGColor);
// background rounded rect
svgRoundedRect(c, 1, r, size * 0.4/2);
// RSS icon
SetContentScale(c, r.size, 11/16.0);
AddRSSIconPath(c, size, connection);
CGContextEOFillPath(c);
}
/// Draw RSS icon with orange gradient background
static void RoundedRSS_Gradient(CGRect r, NSColor *color) {
const CGFloat size = ShorterSide(r.size);
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
CGContextSetFillColorWithColor(c, NSColor.whiteColor.CGColor);
// background rounded rect
svgRoundedRect(c, 1, r, size * 0.4/2);
// Gradient
CGContextSaveGState(c);
CGContextClip(c);
DrawGradient(c, size, color);
CGContextRestoreGState(c);
// Bars
// RSS icon
SetContentScale(c, r.size, 11/16.0);
AddRSSIconPath(c, size, YES);
CGContextEOFillPath(c);
}
#pragma mark - Appearance Settings
/// Draw icon representing global `status bar icon` (rounded RSS icon with neighbor items)
static void Appearance_MenuBarIcon(CGRect r) {
const CGFloat size = ShorterSide(r.size);
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
// menu bar
CGContextSetFillColorWithColor(c, [NSColor controlTextColor].CGColor);
svgRect(c, 1, CGRectInset(r, 0, size * 2/16));
CGContextFillPath(c);
// neighbors
const CGFloat offset = round(size*.75);
const CGFloat iconInset = round(size*.2);
const CGFloat iconCorner = size*.12;
CGContextSetAlpha(c, .66);
CGContextSetFillColorWithColor(c, [NSColor controlBackgroundColor].CGColor);
// left neighbor
CGContextTranslateCTM(c, -offset, 0);
svgRoundedRect(c, 1, CGRectInset(r, iconInset, iconInset), iconCorner);
CGContextFillPath(c);
// right neighbor
CGContextTranslateCTM(c, +2*offset, 0);
svgRoundedRect(c, 1, CGRectInset(r, iconInset, iconInset), iconCorner);
CGContextFillPath(c);
// main icon
CGContextSetAlpha(c, 1);
CGContextTranslateCTM(c, -offset, 0);
svgRoundedRect(c, 1, CGRectInset(r, iconInset, iconInset), iconCorner);
SetContentScale(c, r.size, 7/16.0);
AddRSSIconPath(c, size, YES);
CGContextEOFillPath(c);
}
/// Draw icon representing `Main Menu` (menu bar)
static void Appearance_MainMenu(CGRect r) {
const CGFloat size = ShorterSide(r.size);
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
CGContextSetFillColorWithColor(c, [NSColor controlTextColor].CGColor);
// menu
svgRect(c, size/16, CGRectMake(0, 0, 16, 3));
svgRect(c, size/16, CGRectMake(5, 4, 9, 12));
svgRect(c, size/16, CGRectMake(6, 3, 7, 12));
// entries
svgRect(c, size/16, CGRectMake(6, 12, 6, 1));
svgRect(c, size/16, CGRectMake(6, 9, 6, 1));
svgRect(c, size/16, CGRectMake(6, 6, 6, 1));
CGContextEOFillPath(c);
}
/// Draw icon representing `FeedGroup` (folder)
static void Appearance_Group(CGRect r, BOOL withLine) {
const CGFloat size = ShorterSide(r.size);
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
// folder path
svgPath(c, size/16, "M3,13.5c-1.5,0-2.5-1-2.5-2.5V3.5c0-1.5.5-2,2-2h1.5c1.5,0,1.5,1,3,1h6c1.5,0,2.5,1,2.5,2.5v6c0,1.5-1,2.5-2.5,2.5H3Z");
// line
if (withLine) {
svgPath(c, size/16, "M1.5,5h13Z");
}
CGContextSetLineWidth(c, size * 1/16);
CGContextSetStrokeColorWithColor(c, [NSColor controlTextColor].CGColor);
CGContextStrokePath(c);
}
/// Draw icon representing `Feed` (group + RSS)
static void Appearance_Feed(CGRect r) {
const CGFloat size = ShorterSide(r.size);
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
CGContextSetFillColorWithColor(c, [NSColor controlTextColor].CGColor);
// folder
Appearance_Group(r, NO);
// rss icon
SetContentScale(c, r.size, 7/16.0);
AddRSSIconPath(c, size, YES);
CGContextFillPath(c);
}
/// Draw icon representing `Article` (RSS inside text document)
static void Appearance_Article(CGRect r) {
const CGFloat size = ShorterSide(r.size);
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
CGContextSetFillColorWithColor(c, [NSColor controlTextColor].CGColor);
// text lines
svgRect(c, size/16, CGRectMake(0, 14, 16, 1));
svgRect(c, size/16, CGRectMake(0, 10, 16, 1));
svgRect(c, size/16, CGRectMake(9, 6, 7, 1));
svgRect(c, size/16, CGRectMake(9, 2, 7, 1));
// picture
//svgRect(c, size/16, CGRectMake(1, 1, 7, 7));
// RSS icon
CGContextTranslateCTM(c, size/16 * 1, size/16 * 1); // same offset as picture
CGContextScaleCTM(c, 7/16.0, 7/16.0); // same size as picture
AddRSSIconPath(c, size, YES);
CGContextEOFillPath(c);
}
#pragma mark - Other Icons
/// Draw unread icon (blue dot for unread menu item)
static void DrawUnreadIcon(CGRect r, NSColor *color) {
CGFloat size = ShorterSide(r.size) / 2.0;
const CGFloat radius = ShorterSide(r.size) / 2.0;
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
CGMutablePathRef path = CGPathCreateMutable();
SetContentScale(c, r.size, 0.7);
CGContextTranslateCTM(c, 0, size * -0.15); // align with baseline of menu item text
CGContextTranslateCTM(c, 0, radius * -0.15); // align with baseline of menu item text
// outer ring (opaque)
CGContextSetFillColorWithColor(c, color.CGColor);
PathAddRing(path, size, size * 0.7);
CGPathAddArc(path, NULL, radius, radius, radius, 0, M_PI * 2, YES);
CGPathAddArc(path, NULL, radius, radius, radius*.7, 0, M_PI * -2, YES);
CGContextAddPath(c, path);
CGContextEOFillPath(c);
// inner circle (translucent)
CGContextSetFillColorWithColor(c, [color colorWithAlphaComponent:0.5].CGColor);
PathAddCircle(path, size);
CGPathAddArc(path, NULL, radius, radius, radius, 0, M_PI * 2, YES);
CGContextAddPath(c, path);
CGContextFillPath(c);
CGPathRelease(path);
}
/// Draw "(.*)" as vector path
/// Draw `(.*)` as vector path
static void DrawRegexIcon(CGRect r) {
const CGFloat size = ShorterSide(r.size);
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
svgAddRect(c, 1, r, .2 * size);
// background
CGContextSetFillColorWithColor(c, NSColor.redColor.CGColor);
svgRoundedRect(c, 1, r, size * 0.4/2);
CGContextFillPath(c);
// SVG files use bottom-left corner coordinate system. Quartz uses top-left.
FlipCoordinateSystem(c, r.size.height);
SetContentScale(c, r.size, 0.8);
// "("
svgAddPath(c, size/1000, "m184 187c-140 205-134 432-1 622l-66 44c-159-221-151-499 0-708z");
// "."
svgAddCircle(c, size/1000, 315, 675, 70, NO);
// "*"
svgAddPath(c, size/1000, "m652 277 107-35 21 63-109 36 68 92-54 39-68-93-66 91-52-41 67-88-109-37 21-63 108 37v-113h66v112z");
// ")"
svgAddPath(c, size/1000, "m816 813c140-205 134-430 1-621l66-45c159 221 151 499 0 708z");
// foreground
CGContextSetFillColorWithColor(c, NSColor.whiteColor.CGColor);
SetContentScale(c, r.size, 25/32.0);
// "("
svgPath(c, size/100, "M18,19c-14,21-13,43,0,62l-7,4C-4,63-4,35,12,14l6,5Z");
// "."
svgCircle(c, size/100, 31, 67, 7, NO);
// "*"
svgPath(c, size/100, "M65,28l11-4,2,6-11,4,7,9-5,4-7-9-7,9-5-4,7-9-11-4,2-6,11,4v-11h6v11Z");
// ")"
svgPath(c, size/100, "M82,81c14-21,13-43,0-62l7-5c16,22,15,50,0,71l-7-4Z");
CGContextFillPath(c);
}
@@ -301,19 +282,25 @@ static void DrawRegexIcon(CGRect r) {
/// Add single image to @c ImageNamed cache and set accessibility description
static void Register(CGFloat size, NSImageName name, NSString *description, BOOL (^draw)(NSRect r)) {
NSImage *img = [NSImage imageWithSize: NSMakeSize(size, size) flipped:NO drawingHandler:draw];
NSImage *img = [NSImage imageWithSize: NSMakeSize(size, size) flipped:YES drawingHandler:draw];
img.accessibilityDescription = description;
img.name = name;
}
/// Register all icons that require custom drawing in @c ImageNamed cache
void RegisterImageViewNames(void) {
Register(16, RSSImageDefaultRSSIcon, NSLocalizedString(@"RSS icon", nil), ^(NSRect r) { DrawRSSGradientIcon(r, [NSColor rssOrange]); return YES; });
Register(16, RSSImageSettingsGlobal, NSLocalizedString(@"Global settings", nil), ^(NSRect r) { DrawGlobalIcon(r, [NSColor controlTextColor].CGColor, NO); return YES; });
Register(16, RSSImageSettingsGroup, NSLocalizedString(@"Group settings", nil), ^(NSRect r) { DrawGroupIcon(r, [NSColor controlTextColor].CGColor, NO); return YES; });
Register(16, RSSImageSettingsFeed, NSLocalizedString(@"Feed settings", nil), ^(NSRect r) { DrawRSSIcon(r, [NSColor controlTextColor].CGColor, NO, YES); return YES; });
Register(16, RSSImageMenuBarIconActive, NSLocalizedString(@"RSS menu bar icon", nil), ^(NSRect r) { DrawRSSIcon(r, [NSColor menuBarIconColor].CGColor, YES, YES); return YES; });
Register(16, RSSImageMenuBarIconPaused, NSLocalizedString(@"RSS menu bar icon, paused", nil), ^(NSRect r) { DrawRSSIcon(r, [NSColor menuBarIconColor].CGColor, YES, NO); return YES; });
Register(14, RSSImageMenuItemUnread, NSLocalizedString(@"Unread icon", nil), ^(NSRect r) { DrawUnreadIcon(r, [NSColor unreadIndicatorColor]); return YES; });
// Default feed icon (fallback icon if no favicon found)
Register(16, RSSImageDefaultRSSIcon, NSLocalizedString(@"Default feed icon", nil), ^(NSRect r) { RoundedRSS_Gradient(r, [NSColor rssOrange]); return YES; });
// Menu bar icon
Register(16, RSSImageMenuBarIconActive, NSLocalizedString(@"Menu bar icon", nil), ^(NSRect r) { RoundedRSS_Monochrome(r, YES); return YES; });
Register(16, RSSImageMenuBarIconPaused, NSLocalizedString(@"Menu bar icon, paused", nil), ^(NSRect r) { RoundedRSS_Monochrome(r, NO); return YES; });
// Appearance settings
Register(16, RSSImageSettingsGlobalIcon, NSLocalizedString(@"Global settings, menu bar icon", nil), ^(NSRect r) { Appearance_MenuBarIcon(r); return YES; });
Register(16, RSSImageSettingsGlobalMenu, NSLocalizedString(@"Global settings, main menu", nil), ^(NSRect r) { Appearance_MainMenu(r); return YES; });
Register(16, RSSImageSettingsGroup, NSLocalizedString(@"Group settings", nil), ^(NSRect r) { Appearance_Group(r, YES); return YES; });
Register(16, RSSImageSettingsFeed, NSLocalizedString(@"Feed settings", nil), ^(NSRect r) { Appearance_Feed(r); return YES; });
Register(16, RSSImageSettingsArticle, NSLocalizedString(@"Article settings", nil), ^(NSRect r) { Appearance_Article(r); return YES; });
// Other settings
Register(14, RSSImageMenuItemUnread, NSLocalizedString(@"Unread indicator", nil), ^(NSRect r) { DrawUnreadIcon(r, [NSColor unreadIndicatorColor]); return YES; });
Register(32, RSSImageRegexIcon, NSLocalizedString(@"Regex icon", nil), ^(NSRect r) { DrawRegexIcon(r); return YES; });
}

View File

@@ -0,0 +1,6 @@
@import Cocoa;
@interface StrictUIntFormatter : NSFormatter
/// Note: must contain `%ld` and is used as formatter string.
@property (nullable, copy) NSString *unit;
@end

View File

@@ -0,0 +1,40 @@
#import "StrictUIntFormatter.h"
@implementation StrictUIntFormatter
/// Display object as integer formatted string.
- (NSString *)stringForObjectValue:(id)obj {
NSString *str = [NSString stringWithFormat:@"%@", obj];
if (str.length == 0)
return @"";
if (self.unit)
return [NSString stringWithFormat:self.unit, [str integerValue]];
return [NSString stringWithFormat:@"%ld", [str integerValue]];
}
- (NSString *)editingStringForObjectValue:(id)obj {
NSString *str = [NSString stringWithFormat:@"%@", obj];
if (str.length == 0)
return @"";
return [NSString stringWithFormat:@"%ld", [str integerValue]];
}
/// Parse any pasted input as integer.
- (BOOL)getObjectValue:(out id _Nullable __autoreleasing *)obj forString:(NSString *)string errorDescription:(out NSString *__autoreleasing _Nullable *)error {
if (string.length == 0) {
*obj = @"";
} else {
*obj = [[NSNumber numberWithInt:[string intValue]] stringValue];
}
return YES;
}
/// Only digits, no other character allowed
- (BOOL)isPartialStringValid:(NSString *__autoreleasing _Nonnull *)partialStringPtr proposedSelectedRange:(NSRangePointer)proposedSelRangePtr originalString:(NSString *)origString originalSelectedRange:(NSRange)origSelRange errorDescription:(NSString *__autoreleasing _Nullable *)error {
for (NSUInteger i = 0; i < [*partialStringPtr length]; i++) {
unichar c = [*partialStringPtr characterAtIndex:i];
if (c < '0' || c > '9')
return NO;
}
return YES;
}
@end

View File

@@ -1,5 +1,6 @@
@import Cocoa;
void svgAddPath(CGContextRef context, CGFloat scale, const char * path);
void svgAddCircle(CGContextRef context, CGFloat scale, CGFloat x, CGFloat y, CGFloat radius, bool clockwise);
void svgAddRect(CGContextRef context, CGFloat scale, CGRect rect, CGFloat cornerRadius);
void svgPath(CGContextRef context, CGFloat scale, const char * path);
void svgCircle(CGContextRef context, CGFloat scale, CGFloat x, CGFloat y, CGFloat radius, bool clockwise);
void svgRoundedRect(CGContextRef context, CGFloat scale, CGRect rect, CGFloat cornerRadius);
void svgRect(CGContextRef context, CGFloat scale, CGRect rect);

View File

@@ -64,10 +64,16 @@ static void finishOp(CGMutablePathRef path, struct SVGState *state) {
state->y = state->num[1];
CGPathAddLineToPoint(path, NULL, state->x * state->scale, state->y * state->scale);
} else if (op == 'Q' && state->iNum == 4) {
state->x = state->num[2];
state->y = state->num[3];
CGPathAddQuadCurveToPoint(path, NULL, state->num[0] * state->scale, state->num[1] * state->scale, state->x * state->scale, state->y * state->scale);
} else if (op == 'C' && state->iNum == 6) {
state->x = state->num[4];
state->y = state->num[5];
CGPathAddCurveToPoint(path, NULL, state->num[0] * state->scale, state->num[1] * state->scale, state->num[2] * state->scale, state->num[3] * state->scale, state->x * state->scale, state->y * state->scale);
} else {
NSLog(@"Unsupported SVG operation %c %d", state->op, state->iNum);
}
@@ -124,6 +130,8 @@ static void tinySVG_parse(const char * code, CGFloat scale, CGMutablePathRef pat
finishOp(path, &state);
} else if (state.iNum == 2 && strchr("MmLl", state.op) != NULL) {
finishOp(path, &state);
} else if (state.iNum == 4 && strchr("Qq", state.op) != NULL) {
finishOp(path, &state);
} else if (state.iNum == 6 && strchr("Cc", state.op) != NULL) {
finishOp(path, &state);
}
@@ -138,11 +146,17 @@ static void tinySVG_parse(const char * code, CGFloat scale, CGMutablePathRef pat
}
}
/// Helper method to scale `rect` according to svg size.
static inline CGRect scaledRect(CGRect rect, CGFloat scale) {
if (scale == 1.0) { return rect; }
return CGRectMake(rect.origin.x * scale, rect.origin.y * scale, rect.size.width * scale, rect.size.height * scale);
}
# pragma mark - External API
/// calls @c tinySVG_path and handles @c CGPath creation and release.
void svgAddPath(CGContextRef context, CGFloat scale, const char * code) {
void svgPath(CGContextRef context, CGFloat scale, const char * code) {
CGMutablePathRef path = CGPathCreateMutable();
tinySVG_parse(code, scale, path);
CGContextAddPath(context, path);
@@ -150,22 +164,24 @@ void svgAddPath(CGContextRef context, CGFloat scale, const char * code) {
}
/// calls @c CGPathAddArc with full circle
void svgAddCircle(CGContextRef context, CGFloat scale, CGFloat x, CGFloat y, CGFloat radius, bool clockwise) {
void svgCircle(CGContextRef context, CGFloat scale, CGFloat x, CGFloat y, CGFloat radius, bool clockwise) {
// No `CGContextAddArc` because that doesnt work well with overlapping counter-clockwise
CGMutablePathRef tmp = CGPathCreateMutable();
CGPathAddArc(tmp, NULL, x * scale, y * scale, radius * scale, 0, M_PI * 2, clockwise);
CGContextAddPath(context, tmp);
CGPathRelease(tmp);
}
/// Calls @c CGContextAddRect or @c CGPathAddRoundedRect (optional).
/// @param cornerRadius Use @c <=0 for no corners. Use half of @c min(w,h) for a full circle.
void svgAddRect(CGContextRef context, CGFloat scale, CGRect rect, CGFloat cornerRadius) {
if (cornerRadius > 0) {
CGMutablePathRef tmp = CGPathCreateMutable();
CGPathAddRoundedRect(tmp, NULL, rect, cornerRadius * scale, cornerRadius * scale);
CGContextAddPath(context, tmp);
CGPathRelease(tmp);
} else {
CGContextAddRect(context, rect);
}
/// Calls @c CGPathAddRoundedRect
/// @param cornerRadius Use half of @c min(w,h) for a full circle.
void svgRoundedRect(CGContextRef context, CGFloat scale, CGRect rect, CGFloat cornerRadius) {
CGMutablePathRef tmp = CGPathCreateMutable();
CGPathAddRoundedRect(tmp, NULL, scaledRect(rect, scale), cornerRadius * scale, cornerRadius * scale);
CGContextAddPath(context, tmp);
CGPathRelease(tmp);
}
/// Calls @c CGContextAddRect
void svgRect(CGContextRef context, CGFloat scale, CGRect rect) {
CGContextAddRect(context, scaledRect(rect, scale));
}

View File

@@ -15,31 +15,35 @@
/** default: @c nil */ static NSString* const Pref_defaultHttpApplication = @"defaultHttpApplication";
/** default: @c nil */ static NSString* const Pref_notificationType = @"notificationType";
// ------ Appearance matrix ------ (Preferences > Appearance Tab) ------
/** default: @c YES */ static NSString* const Pref_globalTintMenuIcon = @"globalTintMenuBarIcon";
/** default: @c YES */ static NSString* const Pref_globalUpdateAll = @"globalUpdateAll";
/** default: @c NO */ static NSString* const Pref_globalToggleHidden = @"globalToggleHidden";
/** default: @c YES */ static NSString* const Pref_globalOpenUnread = @"globalOpenUnread";
/** default: @c YES */ static NSString* const Pref_globalMarkRead = @"globalMarkRead";
/** default: @c YES */ static NSString* const Pref_globalMarkUnread = @"globalMarkUnread";
/** default: @c YES */ static NSString* const Pref_globalUnreadCount = @"globalUnreadCount";
/** default: @c YES */ static NSString* const Pref_groupOpenUnread = @"groupOpenUnread";
/** default: @c YES */ static NSString* const Pref_groupMarkRead = @"groupMarkRead";
/** default: @c YES */ static NSString* const Pref_groupMarkUnread = @"groupMarkUnread";
/** default: @c NO */ static NSString* const Pref_groupUnreadOnly = @"groupUnreadOnly";
/** default: @c YES */ static NSString* const Pref_groupUnreadCount = @"groupUnreadCount";
/** default: @c NO */ static NSString* const Pref_groupUnreadIndicator = @"groupUnreadIndicator";
/** default: @c YES */ static NSString* const Pref_feedOpenUnread = @"feedOpenUnread";
/** default: @c YES */ static NSString* const Pref_feedMarkRead = @"feedMarkRead";
/** default: @c YES */ static NSString* const Pref_feedMarkUnread = @"feedMarkUnread";
/** default: @c NO */ static NSString* const Pref_feedUnreadOnly = @"feedUnreadOnly";
/** default: @c YES */ static NSString* const Pref_feedUnreadCount = @"feedUnreadCount";
/** default: @c YES */ static NSString* const Pref_feedUnreadIndicator = @"feedUnreadIndicator";
/** default: @c NO */ static NSString* const Pref_feedTruncateTitle = @"feedTruncateTitle";
/** default: @c NO */ static NSString* const Pref_feedLimitArticles = @"feedLimitArticles";
// menu buttons
/** default: @c NO */ static NSString* const Pref_globalToggleHidden = @"globalToggleHidden";
/** default: @c YES */ static NSString* const Pref_globalUpdateAll = @"globalUpdateAll";
/** default: @c YES */ static NSString* const Pref_globalOpenUnread = @"globalOpenUnread";
/** default: @c YES */ static NSString* const Pref_groupOpenUnread = @"groupOpenUnread";
/** default: @c YES */ static NSString* const Pref_feedOpenUnread = @"feedOpenUnread";
/** default: @c YES */ static NSString* const Pref_globalMarkRead = @"globalMarkRead";
/** default: @c YES */ static NSString* const Pref_groupMarkRead = @"groupMarkRead";
/** default: @c YES */ static NSString* const Pref_feedMarkRead = @"feedMarkRead";
/** default: @c YES */ static NSString* const Pref_globalMarkUnread = @"globalMarkUnread";
/** default: @c YES */ static NSString* const Pref_groupMarkUnread = @"groupMarkUnread";
/** default: @c YES */ static NSString* const Pref_feedMarkUnread = @"feedMarkUnread";
// display options
/** default: @c YES */ static NSString* const Pref_globalUnreadCount = @"globalUnreadCount";
/** default: @c YES */ static NSString* const Pref_groupUnreadCount = @"groupUnreadCount";
/** default: @c YES */ static NSString* const Pref_feedUnreadCount = @"feedUnreadCount";
/** default: @c YES */ static NSString* const Pref_globalTintMenuIcon = @"globalTintMenuBarIcon";
/** default: @c NO */ static NSString* const Pref_groupUnreadIndicator = @"groupUnreadIndicator";
/** default: @c NO */ static NSString* const Pref_feedUnreadIndicator = @"feedUnreadIndicator";
/** default: @c YES */ static NSString* const Pref_articleUnreadIndicator = @"articleUnreadIndicator";
/** default: @c NO */ static NSString* const Pref_groupUnreadOnly = @"groupUnreadOnly";
/** default: @c NO */ static NSString* const Pref_feedUnreadOnly = @"feedUnreadOnly";
/** default: @c NO */ static NSString* const Pref_articleUnreadOnly = @"articleUnreadOnly";
// article display
/** default: @c -1 */ static NSString* const Pref_articleCountLimit = @"articleCountLimit";
/** default: @c -1 */ static NSString* const Pref_articleTitleLimit = @"articleTitleLimit";
/** default: @c 2k */ static NSString* const Pref_articleTooltipLimit = @"articleTooltipLimit";
// ------ Hidden preferences ------ only modifiable via `defaults write de.relikd.baRSS {KEY}` ------
/** default: @c 10 */ static NSString* const Pref_openFewLinksLimit = @"openFewLinksLimit";
/** default: @c 60 */ static NSString* const Pref_shortArticleNamesLimit = @"shortArticleNamesLimit";
/** default: @c 40 */ static NSString* const Pref_articlesInMenuLimit = @"articlesInMenuLimit";
/** default: @c nil */ static NSString* const Pref_colorStatusIconTint = @"colorStatusIconTint";
/** default: @c nil */ static NSString* const Pref_colorUnreadIndicator = @"colorUnreadIndicator";

View File

@@ -17,19 +17,18 @@ void UserPrefsInit(void) {
Pref_globalMarkRead, Pref_groupMarkRead, Pref_feedMarkRead,
Pref_globalMarkUnread, Pref_groupMarkUnread, Pref_feedMarkUnread,
Pref_globalUnreadCount, Pref_groupUnreadCount, Pref_feedUnreadCount,
Pref_feedUnreadIndicator
Pref_articleUnreadIndicator
]);
defaultsAppend(defs, @NO, @[
Pref_globalToggleHidden,
Pref_groupUnreadOnly, Pref_feedUnreadOnly,
Pref_groupUnreadIndicator,
Pref_feedTruncateTitle,
Pref_feedLimitArticles
Pref_groupUnreadOnly, Pref_feedUnreadOnly, Pref_articleUnreadOnly,
Pref_groupUnreadIndicator, Pref_feedUnreadIndicator,
]);
// Display limits & truncation ( defaults write de.relikd.baRSS {KEY} -int 10 )
[defs setObject:[NSNumber numberWithUnsignedInteger:10] forKey:Pref_openFewLinksLimit];
[defs setObject:[NSNumber numberWithUnsignedInteger:60] forKey:Pref_shortArticleNamesLimit];
[defs setObject:[NSNumber numberWithUnsignedInteger:40] forKey:Pref_articlesInMenuLimit];
[defs setObject:[NSNumber numberWithInteger:-1] forKey:Pref_articleCountLimit];
[defs setObject:[NSNumber numberWithInteger:-1] forKey:Pref_articleTitleLimit];
[defs setObject:[NSNumber numberWithInteger:2000] forKey:Pref_articleTooltipLimit];
[defs setObject:[NSNumber numberWithUnsignedInteger:1] forKey:Pref_prefSelectedTab]; // feed tab
[[NSUserDefaults standardUserDefaults] registerDefaults:defs];
}

View File

@@ -36,6 +36,7 @@ static inline CGFloat NSMaxWidth(NSView *a, NSView *b) { return Max(NSWidth(a.fr
// UI: TextFields
+ (NSTextField*)label:(NSString*)text;
+ (NSTextField*)inputField:(NSString*)placeholder width:(CGFloat)w;
+ (NSTextField*)integerField:(NSString*)placeholder unit:(nullable NSString*)unit width:(CGFloat)w;
+ (NSView*)labelColumn:(NSArray<NSString*>*)labels rowHeight:(CGFloat)h padding:(CGFloat)pad;
// UI: Buttons
+ (NSButton*)button:(NSString*)text;
@@ -52,7 +53,7 @@ static inline CGFloat NSMaxWidth(NSView *a, NSView *b) { return Max(NSWidth(a.fr
+ (nullable NSView*)radioGroup:(NSArray<NSString*>*)entries;
// UI: Enclosing Container
+ (NSPopover*)popover:(NSSize)size;
- (NSScrollView*)wrapContent:(NSView*)content inScrollView:(NSRect)rect;
- (NSScrollView*)wrapInScrollView:(NSSize)size;
+ (NSView*)wrapView:(NSView*)other withLabel:(NSString*)str padding:(CGFloat)pad;
// Insert UI elements in parent view
- (instancetype)placeIn:(NSView*)parent x:(CGFloat)x y:(CGFloat)y;
@@ -60,7 +61,10 @@ static inline CGFloat NSMaxWidth(NSView *a, NSView *b) { return Max(NSWidth(a.fr
- (instancetype)placeIn:(NSView*)parent xRight:(CGFloat)x y:(CGFloat)y;
- (instancetype)placeIn:(NSView*)parent xRight:(CGFloat)x yTop:(CGFloat)y;
// Modify existing UI elements
- (instancetype)alignTop;
- (instancetype)alignRight;
- (instancetype)sizableWidthAndHeight;
- (instancetype)sizableWidth;
- (instancetype)sizeToRight:(CGFloat)rightPadding;
- (instancetype)sizeWidthToFit;
- (instancetype)tooltip:(NSString*)tt;

View File

@@ -1,4 +1,5 @@
#import "NSView+Ext.h"
#import "StrictUIntFormatter.h"
@implementation NSView (Ext)
@@ -27,6 +28,15 @@
return input;
}
/// Create input text field which only accepts integer values. (calls `inputField`) `21px` height.
/// `field.formatter` is of type `StrictUIntFormatter`.
+ (NSTextField*)integerField:(NSString*)placeholder unit:(nullable NSString*)unit width:(CGFloat)w {
NSTextField *input = [self inputField:placeholder width:w];
input.formatter = [StrictUIntFormatter new];
((StrictUIntFormatter*)input.formatter).unit = unit;
return input;
}
/// Create view with @c NSTextField subviews with right-aligned and row-centered text from @c labels.
+ (NSView*)labelColumn:(NSArray<NSString*>*)labels rowHeight:(CGFloat)h padding:(CGFloat)pad {
CGFloat w = 0, y = 0;
@@ -170,17 +180,19 @@
return pop;
}
/// Insert @c scrollView, remove @c self from current view and set as @c documentView for the newly created scroll view.
- (NSScrollView*)wrapContent:(NSView*)content inScrollView:(NSRect)rect {
NSScrollView *scroll = [[[NSScrollView alloc] initWithFrame:rect] sizableWidthAndHeight];
/// Removes `self` from current view (if already added) and sets `documentView` content for the newly created scroll view.
/// You are responsible for adding this scroll view to the view hierarchy.
- (NSScrollView*)wrapInScrollView:(NSSize)size {
NSScrollView *scroll = [[[NSScrollView alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height)] sizableWidthAndHeight];
scroll.borderType = NSBezelBorder;
scroll.hasVerticalScroller = YES;
scroll.horizontalScrollElasticity = NSScrollElasticityNone;
[self addSubview:scroll];
if (content.superview) [content removeFromSuperview]; // remove if added already (e.g., helper methods above)
content.frame = NSMakeRect(0, 0, scroll.contentSize.width, scroll.contentSize.height);
scroll.documentView = content;
if (self.superview) [self removeFromSuperview]; // remove if added already (e.g., helper methods above)
if (self.frame.size.width == 0 && self.frame.size.height == 0) {
self.frame = NSMakeRect(0, 0, scroll.contentSize.width, scroll.contentSize.height);
}
scroll.documentView = self;
return scroll;
}
@@ -257,6 +269,9 @@
/// Modify @c .autoresizingMask; Add @c NSViewWidthSizable @c | @c NSViewHeightSizable flags
- (instancetype)sizableWidthAndHeight { self.autoresizingMask |= NSViewWidthSizable | NSViewHeightSizable; return self; }
/// Modify @c .autoresizingMask; Add @c NSViewWidthSizable flags
- (instancetype)sizableWidth { self.autoresizingMask |= NSViewWidthSizable; return self; }
/// Extend frame in its @c superview and stick to right with padding. Adds @c NSViewWidthSizable to @c autoresizingMask
- (instancetype)sizeToRight:(CGFloat)rightPadding {
SetFrameWidth(self, NSWidth(self.superview.frame) - NSMinX(self.frame) - rightPadding + self.alignmentRectInsets.right);
@@ -273,10 +288,12 @@
/// Set @c tooltip and @c accessibilityTitle of view and return self
- (instancetype)tooltip:(NSString*)tt {
self.toolTip = tt;
if (self.accessibilityLabel.length == 0)
self.accessibilityLabel = tt;
else
if ([self isKindOfClass:[NSTextField class]] && ((NSTextField*)self).editable == NO) {
// a label already shows text, so the tooltip will probably be extended information.
self.accessibilityHelp = tt;
} else {
self.accessibilityValueDescription = tt;
}
return self;
}

View File

@@ -24,7 +24,7 @@
tv.alignment = NSTextAlignmentCenter;
tv.editable = NO; // but selectable
[tv.textStorage setAttributedString:[self rtfDocument]];
[self wrapContent:tv inScrollView:NSMakeRect(-1, 20, NSWidth(self.frame) + 2, NSMinY(lblV.frame) - PAD_M - 20)];
[[tv wrapInScrollView:NSMakeSize(NSWidth(self.frame) + 2, NSMinY(lblV.frame) - PAD_M - 20)] placeIn:self x:-1 y:20];
return self;
}

View File

@@ -8,10 +8,12 @@
- (void)loadView {
self.view = [SettingsAppearanceView new];
for (NSButton *button in self.view.subviews) {
if ([button isKindOfClass:[NSButton class]]) { // for all checkboxes
[button setAction:@selector(didSelectCheckbox:)];
[button setTarget:self];
NSScrollView *scroll = self.view.subviews[0];
NSView *contentView = scroll.documentView.subviews[0];
for (NSControl *control in contentView.subviews) {
if ([control isKindOfClass:[NSButton class]]) { // for all checkboxes
[control setAction:@selector(didSelectCheckbox:)];
[control setTarget:self];
}
}
}

View File

@@ -1,6 +1,6 @@
@import Cocoa;
@class SettingsAppearance;
@interface SettingsAppearanceView : NSView
@interface SettingsAppearanceView : NSView <NSTextFieldDelegate>
@end

View File

@@ -2,139 +2,264 @@
#import "NSView+Ext.h"
#import "Constants.h" // column icons
#import "UserPrefs.h" // preference constants & UserPrefsBool()
#import "DrawImage.h" // DrawSeparator
@interface FlippedView : NSView @end
@implementation FlippedView
- (BOOL)isFlipped { return YES; }
@end
@interface SettingsAppearanceView()
@property (assign) CGFloat y;
@property (assign) NSView *content;
@property (strong) NSMutableArray<NSString*> *columns;
@end
/***/ static CGFloat const IconSize = 18;
/***/ static CGFloat const colWidth = (IconSize + PAD_M); // checkbox column width
/***/ static CGFloat const X__ = PAD_WIN + 0 * colWidth;
/***/ static CGFloat const _X_ = PAD_WIN + 1 * colWidth;
/***/ static CGFloat const __X = PAD_WIN + 2 * colWidth;
/***/ static CGFloat const X___ = PAD_WIN + 0 * colWidth;
/***/ static CGFloat const _X__ = PAD_WIN + 1 * colWidth;
/***/ static CGFloat const __X_ = PAD_WIN + 2 * colWidth;
/***/ static CGFloat const ___X = PAD_WIN + 3 * colWidth;
/***/ static CGFloat const lbl_start = PAD_WIN + 4 * colWidth;
@implementation SettingsAppearanceView
- (instancetype)init {
self = [super initWithFrame:NSMakeRect(0, 0, 320, 327)];
// Insert matrix header (icons above checkbox matrix)
ColumnIcon(self, X__, RSSImageSettingsGlobal);
ColumnIcon(self, _X_, RSSImageSettingsGroup);
ColumnIcon(self, __X, RSSImageSettingsFeed);
// Generate checkbox matrix
self.y = PAD_WIN + IconSize + PAD_S;
[self entry:NSLocalizedString(@"Tint menu bar icon on unread", nil)
help:NSLocalizedString(@"If active, a color will indicate if there are unread articles.", nil)
tip:nil
c1:Pref_globalTintMenuIcon c1tt:NSLocalizedString(@"menu bar icon", nil)
c2:nil c2tt:nil
c3:nil c3tt:nil];
self.y = PAD_WIN;
// stupidly complex, nested UI just because you cant top-align `.documentView`
// View is 0.5px shorter than self.frame because it will otherwise add a transparency to the TabBar
NSScrollView *scroll = [[[FlippedView new] wrapInScrollView:NSMakeSize(320, 326.5)] placeIn:self x:0 y:0];
self.content = [[[NSView alloc] initWithFrame:scroll.documentView.frame] placeIn:scroll.documentView x:0 y:0];
scroll.borderType = NSNoBorder;
// fix default window background color instead of pure black/white
scroll.drawsBackground = NO;
[self entry:NSLocalizedString(@"Update all feeds", nil)
help:NSLocalizedString(@"Show button in main menu to reload all feeds. This will force fetch new online content regardless of next-update timer.", nil)
tip:nil
c1:Pref_globalUpdateAll c1tt:NSLocalizedString(@"in main menu", nil)
c2:nil c2tt:nil
c3:nil c3tt:nil];
[self note:NSLocalizedString(@"Hover over the options for additional explanations and usage tips.", nil)];
[self entry:NSLocalizedString(@"Toggle “Show Hidden Articles”", nil)
help:NSLocalizedString(@"Show button in main menu to quickly toggle whether hidden articles should be shown. See option “Show only unread”.", nil)
tip:nil
c1:Pref_globalToggleHidden c1tt:NSLocalizedString(@"in main menu", nil)
c2:nil c2tt:nil
c3:nil c3tt:nil];
[self entry:NSLocalizedString(@"Open all unread", nil)
// Menu Buttons
[self section:NSLocalizedString(@"Menu buttons", nil)];
[self columns:@[
RSSImageSettingsGlobalMenu, NSLocalizedString(@"Main menu", nil),
RSSImageSettingsGroup, NSLocalizedString(@"Group menu", nil),
RSSImageSettingsFeed, NSLocalizedString(@"Feed menu", nil),
]];
[self entry:NSLocalizedString(@"“Show hidden feeds”", nil)
help:NSLocalizedString(@"Show button to quickly toggle whether hidden entries should be shown. See option “Show only unread”.", nil)
tip:NSLocalizedString(@"You can hold down option-key before opening the main menu to temporarily show all hidden entries.", nil)
c1:Pref_globalToggleHidden c2:nil c3:nil c4:nil];
[self entry:NSLocalizedString(@"“Update all feeds”", nil)
help:NSLocalizedString(@"Show button to reload all feeds. This will force fetch new online content regardless of next-update timer.", nil)
tip:nil
c1:Pref_globalUpdateAll c2:nil c3:nil c4:nil];
[self entry:NSLocalizedString(@"“Open all unread”", nil)
help:NSLocalizedString(@"Show button to open unread articles.", nil)
tip:NSLocalizedString(@"If you hold down option-key, this will become an “open a few” unread articles button.", nil)
c1:Pref_globalOpenUnread c1tt: NSLocalizedString(@"in main menu", nil)
c2:Pref_groupOpenUnread c2tt: NSLocalizedString(@"in group menu", nil)
c3:Pref_feedOpenUnread c3tt: NSLocalizedString(@"in feed menu", nil)];
tip:nil
c1:Pref_globalOpenUnread c2:Pref_groupOpenUnread c3:Pref_feedOpenUnread c4:nil];
[self entry:NSLocalizedString(@"Mark all read", nil)
[self entry:NSLocalizedString(@"Mark all read", nil)
help:NSLocalizedString(@"Show button to mark articles read.", nil)
tip:nil
c1:Pref_globalMarkRead c1tt: NSLocalizedString(@"in main menu", nil)
c2:Pref_groupMarkRead c2tt: NSLocalizedString(@"in group menu", nil)
c3:Pref_feedMarkRead c3tt: NSLocalizedString(@"in feed menu", nil)];
c1:Pref_globalMarkRead c2:Pref_groupMarkRead c3:Pref_feedMarkRead c4:nil];
[self entry:NSLocalizedString(@"Mark all unread", nil)
[self entry:NSLocalizedString(@"Mark all unread", nil)
help:NSLocalizedString(@"Show button to mark articles unread.", nil)
tip:NSLocalizedString(@"You can hold down option-key and click on an article to toggle that item (un-)read.", nil)
c1:Pref_globalMarkUnread c1tt: NSLocalizedString(@"in main menu", nil)
c2:Pref_groupMarkUnread c2tt: NSLocalizedString(@"in group menu", nil)
c3:Pref_feedMarkUnread c3tt: NSLocalizedString(@"in feed menu", nil)];
tip:NSLocalizedString(@"Alternatively, you can hold down option-key and click on an article to toggle that item (un-)read.", nil)
c1:Pref_globalMarkUnread c2:Pref_groupMarkUnread c3:Pref_feedMarkUnread c4:nil];
// self.y += PAD_M;
[self intInput:Pref_openFewLinksLimit
unit:NSLocalizedString(@"%ld unread", nil)
label:NSLocalizedString(@"“Open a few unread” ⌥", nil)
help:NSLocalizedString(@"If you hold down option-key, the “Open all unread” button becomes an “Open a few unread” button.", nil)];
// self.y += PAD_M;
// [self note:NSLocalizedString(@"Hold down option-key and click on an article to toggle that item (un-)read.", nil)];
// Display options
[self section:NSLocalizedString(@"Display options", nil)];
[self columns:@[
RSSImageSettingsGlobalIcon, NSLocalizedString(@"Menu bar icon", nil),
RSSImageSettingsGroup, NSLocalizedString(@"Group menu item", nil),
RSSImageSettingsFeed, NSLocalizedString(@"Feed menu item", nil),
RSSImageSettingsArticle, NSLocalizedString(@"Article menu item", nil),
]];
[self entry:NSLocalizedString(@"Number of unread articles", nil)
help:NSLocalizedString(@"Show count of unread articles in parenthesis.", nil)
tip:nil
c1:Pref_globalUnreadCount c1tt:NSLocalizedString(@"on menu bar icon", nil)
c2:Pref_groupUnreadCount c2tt:NSLocalizedString(@"on group folder", nil)
c3:Pref_feedUnreadCount c3tt:NSLocalizedString(@"on feed folder", nil)];
c1:Pref_globalUnreadCount c2:Pref_groupUnreadCount c3:Pref_feedUnreadCount c4:nil];
[self entry:NSLocalizedString(@"Indicator for unread articles", nil)
help:NSLocalizedString(@"Show blue dot on menu items with unread articles.", nil)
[self entry:NSLocalizedString(@"Color for unread articles", nil)
help:NSLocalizedString(@"Show color marker on menu items with unread articles.", nil)
tip:nil
c1:nil c1tt:nil
c2:Pref_groupUnreadIndicator c2tt:NSLocalizedString(@"on group & feed folder", nil)
c3:Pref_feedUnreadIndicator c3tt:NSLocalizedString(@"on article entry", nil)];
c1:Pref_globalTintMenuIcon c2:Pref_groupUnreadIndicator c3:Pref_feedUnreadIndicator c4:Pref_articleUnreadIndicator];
[self entry:NSLocalizedString(@"Show only unread", nil)
help:NSLocalizedString(@"Hide articles which have been read.", nil)
tip:NSLocalizedString(@"You can hold down option-key before opening the main menu to temporarily disable this setting.", nil)
c1:nil c1tt:nil
c2:Pref_groupUnreadOnly c2tt:NSLocalizedString(@"hide group & feed folders with 0 unread articles", nil)
c3:Pref_feedUnreadOnly c3tt:NSLocalizedString(@"hide articles inside of feed folder", nil)];
[self entry:NSLocalizedString(@"Truncate article title", nil)
help:NSLocalizedString(@"Truncate article title after 60 characters. If a title is longer than that, show an ellipsis character “…” instead.", nil)
tip:nil
c1:nil c1tt:nil
c2:nil c2tt:nil
c3:Pref_feedTruncateTitle c3tt:NSLocalizedString(@"article title", nil)];
c1:nil c2:Pref_groupUnreadOnly c3:Pref_feedUnreadOnly c4:Pref_articleUnreadOnly];
[self entry:NSLocalizedString(@"Limit number of articles", nil)
help:NSLocalizedString(@"Display at most 40 articles in feed menu. Remaining articles will be hidden from view but are still there. Unread count may be confusing as it will also count unread and hidden articles.", nil)
tip:nil
c1:nil c1tt:nil
c2:nil c2tt:nil
c3:Pref_feedLimitArticles c3tt:NSLocalizedString(@"in feed menu", nil)];
// self.y += PAD_M;
// [self note:NSLocalizedString(@"Hold down option-key before opening the main menu to temporarily show hidden feeds.", nil)];
[[[[[NSView label:@"Note: you can hover over all options to display explanatory tooltips."]
multiline:NSMakeSize(100, 2 * HEIGHT_LABEL)] gray]
placeIn:self x:PAD_WIN yTop:self.y + PAD_L] sizeToRight:PAD_WIN];
// Other UI elements
[self section:NSLocalizedString(@"Article display", nil)];
[self intInput:Pref_articleCountLimit
unit:NSLocalizedString(@"%ld entries", nil)
label:NSLocalizedString(@"Limit number of articles", nil)
help:NSLocalizedString(@"Display at most X articles in feed menu. Remaining articles will be hidden from view but are still there. Unread count may be confusing because hidden articles are counted too.", nil)];
[self intInput:Pref_articleTitleLimit
unit:NSLocalizedString(@"%ld chars", nil)
label:NSLocalizedString(@"Truncate article title", nil)
help:NSLocalizedString(@"Truncate article title after X characters. If a title is longer than that, show an ellipsis character “…”.", nil)];
[self intInput:Pref_articleTooltipLimit
unit:NSLocalizedString(@"%ld chars", nil)
label:NSLocalizedString(@"Truncate article tooltip", nil)
help:NSLocalizedString(@"Truncate article tooltip after X characters. This tooltip shows the whole article content (if provided by the server).", nil)];
self.y += PAD_WIN;
// sest final view size
[[self.content sizableWidth] setFrameSize:NSMakeSize(NSWidth(self.content.frame), self.y)];
[[scroll.documentView sizableWidth] setFrame:self.content.frame];
return self;
}
/// Helper method for matrix table header icons
static inline void ColumnIcon(id this, CGFloat x, const NSImageName img) {
[[NSView imageView:img size:IconSize] placeIn:this x:x yTop:PAD_WIN];
// MARK: - Section Header
- (void)section:(NSString*)title {
self.y += PAD_L;
NSTextField *label = [[[NSView label:title] placeIn:self.content x:PAD_WIN yTop:self.y] large];
[[[DrawSeparator withSize:NSMakeSize(100, NSHeight(label.frame))] placeIn:self.content x:NSMaxX(label.frame) + PAD_S yTop:self.y] sizeToRight:0];
self.y += NSHeight(label.frame) + PAD_M;
}
// MARK: - Column Icons
/// Helper method for matrix table header icons
- (void)columns:(NSArray<NSString*>*)columns {
self.columns = [NSMutableArray arrayWithCapacity:4];
for (NSUInteger i = 0; i < columns.count / 2; i++) {
NSString *img = columns[i*2];
NSString *ttip = columns[i*2 + 1];
[[[NSView imageView:img size:IconSize] tooltip:ttip]
placeIn:self.content x:PAD_WIN + i * colWidth yTop:self.y]
.accessibilityLabel = NSLocalizedString(@"Column header:", nil);
[self.columns addObject:ttip ? ttip : @""];
}
self.y += HEIGHT_INPUTFIELD + PAD_S;
}
// MARK: - Notes
- (void)note:(NSString*)text {
NSTextField *lbl = [[[NSView label:text] multiline:NSMakeSize(320 - 2*PAD_WIN, 7 * HEIGHT_LABEL)] gray];
NSSize bestSize = [lbl sizeThatFits:lbl.frame.size];
[lbl setFrameSize:bestSize];
[[lbl placeIn:self.content x:PAD_WIN yTop:self.y] sizeToRight:PAD_WIN];
self.y += NSHeight(lbl.frame);
}
// MARK: - Checkboxes
/// Helper method for generating a checkbox
static inline NSButton* Checkbox(id this, CGFloat x, CGFloat y, NSString *key) {
NSButton *check = [[NSView checkbox: UserPrefsBool(key)] placeIn:this x:x yTop:y];
static inline NSButton* Checkbox(SettingsAppearanceView *self, CGFloat x, NSString *key) {
NSButton *check = [[NSView checkbox:UserPrefsBool(key)] placeIn:self.content x:x+2 yTop:self.y+2];
check.identifier = key;
return check;
}
/// Create new entry with 1-3 checkboxes and a descriptive label
- (NSTextField*)entry:(NSString*)label help:(NSString*)ttip tip:(NSString*)extraTip
c1:(NSString*)pref1 c1tt:(NSString*)ttip1
c2:(NSString*)pref2 c2tt:(NSString*)ttip2
c3:(NSString*)pref3 c3tt:(NSString*)ttip3
c1:(NSString*)pref1 c2:(NSString*)pref2 c3:(NSString*)pref3 c4:(NSString*)pref4
{
CGFloat y = self.y;
self.y += (PAD_S + HEIGHT_LABEL);
// TODO: localize: global, group, feed
if (pref1) [Checkbox(self, X__ + 2, y + 2, pref1) tooltip:ttip1].accessibilityLabel = [label stringByAppendingString:@" (global)"];
if (pref2) [Checkbox(self, _X_ + 2, y + 2, pref2) tooltip:ttip2].accessibilityLabel = [label stringByAppendingString:@" (group)"];
if (pref3) [Checkbox(self, __X + 2, y + 2, pref3) tooltip:ttip3].accessibilityLabel = [label stringByAppendingString:@" (feed)"];
if (pref1) Checkbox(self, X___, pref1).accessibilityLabel = [NSString stringWithFormat:@"%@: %@", _columns[0], label];
if (pref2) Checkbox(self, _X__, pref2).accessibilityLabel = [NSString stringWithFormat:@"%@: %@", _columns[1], label];
if (pref3) Checkbox(self, __X_, pref3).accessibilityLabel = [NSString stringWithFormat:@"%@: %@", _columns[2], label];
if (pref4) Checkbox(self, ___X, pref4).accessibilityLabel = [NSString stringWithFormat:@"%@: %@", _columns[3], label];
if (extraTip != nil) {
label = [label stringByAppendingString:@" *"];
ttip = [ttip stringByAppendingFormat:@"\n\nTip: %@", extraTip];
label = [label stringByAppendingString:@" 💡"];
ttip = [ttip stringByAppendingFormat:@"\n\n💡 Tip: %@", extraTip];
}
return [[[[NSView label:label] placeIn:self x:PAD_WIN + 3 * colWidth yTop:y] sizeToRight:PAD_WIN] tooltip:ttip];
NSTextField *lbl = [[[[NSView label:label] tooltip:ttip] placeIn:self.content x:lbl_start yTop:self.y] sizeToRight:PAD_WIN];
self.y += (PAD_S + HEIGHT_LABEL);
return lbl;
}
// MARK: - Int Input Field
/// Create input field for integer numbers
- (NSTextField*)intInput:(NSString*)pref unit:(NSString*)unit label:(NSString*)label help:(NSString*)ttip {
// input field
NSTextField *rv = [[NSView integerField:@"" unit:unit width:3 * colWidth + IconSize] placeIn:self.content x:PAD_WIN yTop:self.y];
rv.placeholderString = NSLocalizedString(@"no limit", nil);
// sadly, setting `accessibilityLabel` will break VoiceOver on empty input.
// keep disabled so VoceOver will read the placeholder string if empty.
rv.accessibilityLabel = label;
rv.identifier = pref;
rv.delegate = self;
NSInteger val = UserPrefsInt(pref);
if (val >= 0) {
rv.stringValue = [NSString stringWithFormat:@"%ld", val];
} else {
rv.accessibilityValueDescription = rv.placeholderString;
}
// label
[[[[NSView label:label] tooltip:ttip] placeIn:self.content x:lbl_start yTop:self.y + (HEIGHT_INPUTFIELD - HEIGHT_LABEL) / 2] sizeToRight:PAD_WIN];
self.y += HEIGHT_INPUTFIELD + PAD_S;
return rv;
}
- (void)controlTextDidEndEditing:(NSNotification *)obj {
NSTextField *sender = obj.object;
NSString *pref = sender.identifier;
NSInteger newVal = sender.integerValue;
BOOL isEmpty = newVal == 0 && sender.stringValue.length == 0;
sender.accessibilityValueDescription = isEmpty ? sender.placeholderString : nil;
UserPrefsSetInt(pref, isEmpty ? -1 : newVal);
BOOL hitReturn = [[obj.userInfo valueForKey:NSTextMovementUserInfoKey] integerValue] == NSTextMovementReturn;
if (hitReturn) {
// Allow to deselect NSTextField (when pressing enter to confirm change)
[self.window performSelector:@selector(makeFirstResponder:) withObject:nil afterDelay:0];
}
}
// Allow to deselect all NSTextFields (via tab focus cycling)
// Also: opens view with no NSTextField selected.
- (BOOL)acceptsFirstResponder {
return YES;
}
// Allow to deselect all NSTextFields (by clicking outside / somewhere on the window)
- (void)mouseDown:(NSEvent *)event {
[self.window performSelector:@selector(makeFirstResponder:) withObject:nil afterDelay:0];
// perform selector because otherwise it will raise an issue of different QoS levels
}
@end

View File

@@ -3,8 +3,6 @@
#import "NSView+Ext.h"
#import "Constants.h"
@interface StrictUIntFormatter : NSFormatter
@end
@implementation ModalFeedEditView
@@ -34,7 +32,7 @@
self.name = [[[NSView inputField:NSLocalizedString(@"Example Title", nil) width:0] placeIn:self x:x yTop:rowHeight] sizeToRight:PAD_S + 18];
self.spinnerName = [[NSView activitySpinner] placeIn:self xRight:1 yTop:rowHeight + 2.5];
// 3. row
self.refreshNum = [[NSView inputField:@"30" width:85] placeIn:self x:x yTop:2*rowHeight];
self.refreshNum = [[NSView integerField:@"∞" unit:nil width:85] placeIn:self x:x yTop:2*rowHeight];
self.refreshUnit = [[NSView popupButton:120] placeIn:self x:NSMaxX(self.refreshNum.frame) + PAD_M yTop:2*rowHeight];
self.regexConverterButton = [[[[NSView buttonIcon:RSSImageRegexIcon size:19]
action:@selector(openRegexConverter) target:controller]
@@ -44,11 +42,11 @@
// initial state
self.url.accessibilityLabel = lbls[0];
self.name.accessibilityLabel = lbls[1];
self.favicon.accessibilityLabel = nil; // disable `accessibilityDescription` of `RSSImageDefaultRSSIcon`
self.refreshNum.accessibilityLabel = NSLocalizedString(@"Refresh interval", nil);
self.url.delegate = controller;
self.warningButton.hidden = YES;
self.regexConverterButton.hidden = YES;
self.refreshNum.formatter = [StrictUIntFormatter new]; // see below ...
[self prepareWarningPopover];
return self;
}
@@ -67,29 +65,3 @@
}
@end
#pragma mark - StrictUIntFormatter -
@implementation StrictUIntFormatter
/// Display object as integer formatted string.
- (NSString *)stringForObjectValue:(id)obj {
return [NSString stringWithFormat:@"%d", [[NSString stringWithFormat:@"%@", obj] intValue]];
}
/// Parse any pasted input as integer.
- (BOOL)getObjectValue:(out id _Nullable __autoreleasing *)obj forString:(NSString *)string errorDescription:(out NSString *__autoreleasing _Nullable *)error {
*obj = [[NSNumber numberWithInt:[string intValue]] stringValue];
return YES;
}
/// Only digits, no other character allowed
- (BOOL)isPartialStringValid:(NSString *__autoreleasing _Nonnull *)partialStringPtr proposedSelectedRange:(NSRangePointer)proposedSelRangePtr originalString:(NSString *)origString originalSelectedRange:(NSRange)origSelRange errorDescription:(NSString *__autoreleasing _Nullable *)error {
for (NSUInteger i = 0; i < [*partialStringPtr length]; i++) {
unichar c = [*partialStringPtr characterAtIndex:i];
if (c < '0' || c > '9')
return NO;
}
return YES;
}
@end

View File

@@ -17,7 +17,7 @@
if (self) {
self.controller = delegate; // make sure its first
self.outline = [self generateOutlineView]; // uses self.controller
[self wrapContent:self.outline inScrollView:NSMakeRect(0, 20, NSWidth(self.frame), NSHeight(self.frame) - 20)];
[[self.outline wrapInScrollView:NSMakeSize(NSWidth(self.frame), NSHeight(self.frame) - 20)] placeIn:self x:0 y:20];
self.outline.menu = [self generateCommandsMenu];
[self.outline.menu.itemArray makeObjectsPerformSelector:@selector(setTarget:) withObject:delegate];
CGFloat x = [self generateButtons]; // uses self.controller and self.outline
@@ -210,7 +210,8 @@ NSUserInterfaceItemIdentifier const CustomCellRefresh = @"RefreshColumnCell";
}
self.textField.objectValue = str;
self.textField.textColor = (str.length > 1 ? [NSColor controlTextColor] : [NSColor disabledControlTextColor]);
self.textField.accessibilityLabel = (str.length > 1 ? NSLocalizedString(@"Refresh interval", nil) : nil);
self.textField.accessibilityLabel = (str.length > 0 ? NSLocalizedString(@"Refresh interval", nil) : nil);
[self.textField tooltip:(str.length == 1 ? NSLocalizedString(@"manually", nil) : nil)];
}
@end

View File

@@ -59,7 +59,7 @@ static CGFloat const heightRow = PAD_S + HEIGHT_INPUTFIELD;
tv.editable = NO; // but selectable
tv.drawsBackground = NO;
tv.textContainer.textView.string = NSLocalizedString(@"DIY regex converter. Press enter to confirm. For help, refer to online tools (e.g., regex101 with options: global + single-line)", nil);
NSScrollView *scroll = [self wrapContent:tv inScrollView:NSMakeRect(-1, NSHeight(self.frame) - heightHowTo, NSWidth(self.frame) + 2, heightHowTo)];
NSScrollView *scroll = [[tv wrapInScrollView:NSMakeSize(NSWidth(self.frame) + 2, heightHowTo)] placeIn:self x:-1 y:NSHeight(self.frame) - heightHowTo];
scroll.drawsBackground = NO;
scroll.borderType = NSNoBorder;
scroll.verticalScrollElasticity = NSScrollElasticityNone;
@@ -71,7 +71,7 @@ static CGFloat const heightRow = PAD_S + HEIGHT_INPUTFIELD;
NSTextView *tv = [[NSTextView new] sizableWidthAndHeight];
tv.editable = NO; // but selectable
tv.backgroundColor = NSColor.whiteColor;
[self wrapContent:tv inScrollView:NSMakeRect(-1, 0, NSWidth(self.frame) + 2, heightOutput)];
[[tv wrapInScrollView:NSMakeSize(NSWidth(self.frame) + 2, heightOutput)] placeIn:self x:-1 y:0];
return tv;
}

View File

@@ -72,17 +72,18 @@
/// Generate items for @c FeedArticles menu.
- (void)setArticles:(NSArray<FeedArticle*>*)sortedList forMenu:(NSMenu*)menu {
[menu insertDefaultHeader];
NSInteger mc = NSIntegerMax;
if (UserPrefsBool(Pref_feedLimitArticles))
mc = UserPrefsInt(Pref_articlesInMenuLimit);
BOOL onlyUnread = UserPrefsBool(Pref_feedUnreadOnly);
for (FeedArticle *fa in sortedList) {
if (onlyUnread && !fa.unread && !_showHidden)
continue;
if (--mc < 0) // mc == 0 will first decrement to -1, then evaluate
break;
[menu addItem:[fa newMenuItem]];
NSInteger mc = UserPrefsInt(Pref_articleCountLimit);
if (mc < 0) mc = NSIntegerMax;
if (mc > 0) {
BOOL onlyUnread = UserPrefsBool(Pref_articleUnreadOnly);
for (FeedArticle *fa in sortedList) {
if (onlyUnread && !fa.unread && !_showHidden)
continue;
if (--mc < 0) // mc == 0 will first decrement to -1, then evaluate
break;
[menu addItem:[fa newMenuItem]];
}
}
[menu setHeaderHasUnread:self.unreadMap[menu.titleIndexPath]];
}

View File

@@ -14,7 +14,7 @@
@property (strong) NSStatusItem *statusItem;
@property (assign) NSInteger unreadCountTotal;
@property (weak) NSMenuItem *updateAllItem;
/// Set to `true` if user toggled the `"Show Hidden Articles"` menu option.
/// Set to `true` if user toggled the `"Show hidden feeds"` menu option.
@property (assign) BOOL optShowHidden;
/// Set to `true` if menu bar was opened while holding down option-key.
@property (assign) BOOL holdingOptKey;
@@ -107,6 +107,9 @@
BOOL hasNet = [UpdateScheduler allowNetworkConnection];
BOOL tint = (self.unreadCountTotal > 0 && hasNet && UserPrefsBool(Pref_globalTintMenuIcon));
self.statusItem.button.image = [NSImage imageNamed:(hasNet ? RSSImageMenuBarIconActive : RSSImageMenuBarIconPaused)];
self.statusItem.button.accessibilityLabel = hasNet
? NSLocalizedString(@"RSS menu bar", nil)
: NSLocalizedString(@"RSS menu bar, paused", nil);
if (@available(macOS 11, *)) {
self.statusItem.button.image.template = !tint;
@@ -176,16 +179,16 @@
- (void)insertMainMenuHeader:(NSMenu*)menu {
// 'Pause Updates' item
NSMenuItem *pause = [menu addItemWithTitle:NSLocalizedString(@"Pause Updates", nil) action:@selector(pauseUpdates) keyEquivalent:@""];
NSMenuItem *pause = [menu addItemWithTitle:NSLocalizedString(@"Pause updates", nil) action:@selector(pauseUpdates) keyEquivalent:@""];
pause.target = self;
if ([UpdateScheduler isPaused])
pause.title = NSLocalizedString(@"Resume Updates", nil);
pause.title = NSLocalizedString(@"Resume updates", nil);
// 'show hidden articles' item
// 'show hidden feeds' item
if (UserPrefsBool(Pref_globalToggleHidden)) {
NSMenuItem *toggleHidden = [menu addItemWithTitle:NSLocalizedString(@"Show Hidden Articles", nil) action:@selector(toggleHiddenArticles) keyEquivalent:@"h"];
NSMenuItem *toggleHidden = [menu addItemWithTitle:NSLocalizedString(@"Show hidden feeds", nil) action:@selector(toggleHiddenArticles) keyEquivalent:@"h"];
toggleHidden.target = self;
toggleHidden.enabled = !self.holdingOptKey && (UserPrefsBool(Pref_groupUnreadOnly) || UserPrefsBool(Pref_feedUnreadOnly));
toggleHidden.enabled = !self.holdingOptKey && (UserPrefsBool(Pref_groupUnreadOnly) || UserPrefsBool(Pref_feedUnreadOnly) || UserPrefsBool(Pref_articleUnreadOnly));
[toggleHidden setState:self.barMenu.showHidden ? NSControlStateValueOn : NSControlStateValueOff];
if (!toggleHidden.enabled) {
toggleHidden.toolTip = self.holdingOptKey
@@ -211,13 +214,13 @@
[self updateBarIcon];
}
/// Called when user clicks on 'Show Hidden Articles' (main menu only).
/// Called when user clicks on `Show hidden feeds` (main menu only).
- (void)toggleHiddenArticles {
self.optShowHidden = !self.optShowHidden;
self.barMenu.showHidden = self.optShowHidden;
}
/// Called when user clicks on 'Update all feeds' (main menu only).
/// Called when user clicks on `Update all feeds` (main menu only).
- (void)updateAllFeeds {
// [self asyncReloadUnreadCount]; // should not be necessary
[UpdateScheduler forceUpdateAllFeeds];

View File

@@ -58,8 +58,8 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
// Check user preferences to show only unread entries
if (unread == 0 && !showHidden
&& (fg.type == FEED || fg.type == GROUP)
&& UserPrefsBool(Pref_groupUnreadOnly)) {
&& ((fg.type == GROUP && UserPrefsBool(Pref_groupUnreadOnly))
|| (fg.type == FEED && UserPrefsBool(Pref_feedUnreadOnly)))) {
item.hidden = YES;
}
@@ -75,8 +75,11 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
self.autoenablesItems = NO;
NSMenuItem *itm = [self addItemIfAllowed:TagOpenAllUnread title:NSLocalizedString(@"Open all unread", nil)];
if (itm) {
NSString *altTitle = [NSString stringWithFormat:NSLocalizedString(@"Open a few unread (%lu)", nil), UserPrefsUInt(Pref_openFewLinksLimit)];
[self addItem:[itm alternateWithTitle:altTitle]];
NSInteger limit = UserPrefsInt(Pref_openFewLinksLimit);
if (limit > 0) {
NSString *altTitle = [NSString stringWithFormat:NSLocalizedString(@"Open a few unread (%ld)", nil), limit];
[self addItem:[itm alternateWithTitle:altTitle]];
}
}
[self addItemIfAllowed:TagMarkAllRead title:NSLocalizedString(@"Mark all read", nil)];
[self addItemIfAllowed:TagMarkAllUnread title:NSLocalizedString(@"Mark all unread", nil)];
@@ -165,7 +168,7 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
BOOL openLinks = NO;
NSUInteger limit = 0;
if (sender.tag == TagOpenAllUnread) {
if (sender.isAlternate)
if (sender.isAlternate) // if reaches this far, limit is guaranteed to be >0
limit = UserPrefsUInt(Pref_openFewLinksLimit);
openLinks = YES;
} else if (sender.tag != TagMarkAllRead && sender.tag != TagMarkAllUnread) {
@@ -212,11 +215,12 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
if (loc != NSNotFound)
self.title = [self.title substringToIndex:loc];
}
if (count > 0 && UserPrefsBool(self.submenu.isFeedMenu ? Pref_feedUnreadCount : Pref_groupUnreadCount)) {
BOOL isFeed = self.submenu.isFeedMenu;
if (count > 0 && UserPrefsBool(isFeed ? Pref_feedUnreadCount : Pref_groupUnreadCount)) {
self.tag = TagTitleCountVisible; // apply new mask
self.title = [self.title stringByAppendingFormat:@" (%ld)", count];
self.onStateImage = [NSImage imageNamed:RSSImageMenuItemUnread];
if (UserPrefsBool(Pref_groupUnreadIndicator))
if (UserPrefsBool(isFeed ? Pref_feedUnreadIndicator : Pref_groupUnreadIndicator))
self.state = NSControlStateValueOn;
}
}