Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b194a1427d | ||
|
|
ff34781fea | ||
|
|
4edd4448ae | ||
|
|
33f907228b | ||
|
|
673e0d3d48 | ||
|
|
b3fdadb9f4 | ||
|
|
9fc513254f | ||
|
|
881b9db02c | ||
|
|
3a14c90f37 | ||
|
|
96884474ac | ||
|
|
82ae18c8a5 | ||
|
|
6eddb57651 | ||
|
|
67d17599b5 | ||
|
|
3507fd8e27 | ||
|
|
ca417f35b6 | ||
|
|
6e5326f913 | ||
|
|
1589b23aa9 | ||
|
|
e0cd04b882 | ||
|
|
6b4c38ec21 | ||
|
|
e7208ae2ab | ||
|
|
508377a823 | ||
|
|
2185eb76fb | ||
|
|
8de163859b | ||
|
|
f739b64ceb | ||
|
|
c2fda881b1 | ||
|
|
a0a5b5b82d | ||
|
|
43e32b2286 | ||
|
|
205b544acd | ||
|
|
56f6ec1356 | ||
|
|
ab71c51380 | ||
|
|
7a805ccdc4 | ||
|
|
f4f4bc9271 | ||
|
|
64637243b5 |
17
CHANGELOG.md
17
CHANGELOG.md
@@ -5,6 +5,21 @@ 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.5.5] – 2025-12-03
|
||||
### Added
|
||||
- *Settings, Appearance:* Improved tooltips on individual options
|
||||
- *Status Bar Menu:* Toggle button to show hidden articles without holding down option-key.
|
||||
|
||||
|
||||
## [1.5.4] – 2025-12-02
|
||||
### Added
|
||||
- *Settings, Appearance:* Tooltip explanation for all options
|
||||
- *Status Bar Menu:* Hold down option key before opening the menu bar icon to show hidden articles (if option "Show only unread" is active)
|
||||
|
||||
### Fixed
|
||||
- *UI:* Table cells were rendered slightly off bounds.
|
||||
|
||||
|
||||
## [1.5.3] – 2025-10-29
|
||||
### Fixed
|
||||
- *Notifications:* Use user-provided feed title instead of server provided title
|
||||
@@ -230,6 +245,8 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
|
||||
Initial release
|
||||
|
||||
|
||||
[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
|
||||
[1.5.2]: https://github.com/relikd/baRSS/compare/v1.5.1...v1.5.2
|
||||
[1.5.1]: https://github.com/relikd/baRSS/compare/v1.5.0...v1.5.1
|
||||
|
||||
6
Config-debug.xcconfig
Normal file
6
Config-debug.xcconfig
Normal file
@@ -0,0 +1,6 @@
|
||||
// Configuration settings file format documentation can be found at:
|
||||
// https://help.apple.com/xcode/#/dev745c5c974
|
||||
|
||||
#include "Config.xcconfig"
|
||||
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.baRSS.beta
|
||||
12
Config.xcconfig
Normal file
12
Config.xcconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
// Configuration settings file format documentation can be found at:
|
||||
// https://help.apple.com/xcode/#/dev745c5c974
|
||||
|
||||
CODE_SIGN_STYLE = Manual
|
||||
CODE_SIGN_IDENTITY = Apple Development
|
||||
ENABLE_HARDENED_RUNTIME = YES
|
||||
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14
|
||||
MARKETING_VERSION = 1.5.5
|
||||
PRODUCT_NAME = baRSS
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.baRSS
|
||||
CURRENT_PROJECT_VERSION = 16970
|
||||
14
README.md
14
README.md
@@ -69,19 +69,24 @@ Hidden options
|
||||
baRSS has no option to launch it on start.
|
||||
However, you can still add the application to auto boot by adding it to the system login items:
|
||||
|
||||
`System Preferences > User > Login Items` (macOS 10-12)
|
||||
`System Preferences > User > Login Items` (macOS 10.x-12)
|
||||
`System Preferences > General > Login Items & Extensions` (macOS 13+)
|
||||
|
||||
|
||||
### UI options
|
||||
|
||||
1. If you hold down the option key and click on an article item, you can mark a single item (un-)read without opening it.
|
||||
I am still searching for a way to keep the menu open after click (if you know of a way, let me know!).
|
||||
|
||||
2. To add websites without RSS feed you can use the regex converter.
|
||||
Hold down the option key in the feed edit modal and click the red regex button.
|
||||
Though, admittedly, this is for experts only.
|
||||
I still have to find a nice user-friendly way to achieve this.
|
||||
|
||||
3. The option “Show only unread” will hide all items which have been read.
|
||||
You can hold down option key before opening the menu bar icon to show hidden articles regardless.
|
||||
This is a nice way to quickly lookup a hidden article without going into settings and twiddling with the checkbox.
|
||||
|
||||
|
||||
### CLI options
|
||||
|
||||
@@ -103,6 +108,13 @@ With this Terminal command you can customize this limit:
|
||||
defaults write de.relikd.baRSS shortArticleNamesLimit -int 50
|
||||
```
|
||||
|
||||
3. Each article menu item shows a summary tooltip (if the server provides one).
|
||||
By default, the tooltip is limited to 2000 characters.
|
||||
You can change the limit with this command:
|
||||
```
|
||||
defaults write de.relikd.baRSS tooltipCharacterLimit -int 500
|
||||
```
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
54AD90EA2E30C48400160925 /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54AD90E92E30C48400160925 /* Quartz.framework */; };
|
||||
54AD90EE2E30C48400160925 /* PreviewViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 54AD90ED2E30C48400160925 /* PreviewViewController.m */; };
|
||||
54AD90F12E30C48400160925 /* PreviewViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54AD90EF2E30C48400160925 /* PreviewViewController.xib */; };
|
||||
54AD90F72E30C48400160925 /* QLOPML.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 54AD90E72E30C48400160925 /* QLOPML.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
54AD90F72E30C48400160925 /* QLOPML.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 54AD90E72E30C48400160925 /* QLOPML.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
54B51704226DC339006C1B29 /* ModalFeedEditView.m in Sources */ = {isa = PBXBuildFile; fileRef = 54B51703226DC339006C1B29 /* ModalFeedEditView.m */; };
|
||||
54B517072270E990006C1B29 /* NSView+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54B517062270E92A006C1B29 /* NSView+Ext.m */; };
|
||||
54B6F14A231551B3002C94C9 /* FaviconDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 54B6F149231551B3002C94C9 /* FaviconDownload.m */; };
|
||||
@@ -105,15 +105,15 @@
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
54AD90F62E30C48400160925 /* Embed App Extensions */ = {
|
||||
54AD90F62E30C48400160925 /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
54AD90F72E30C48400160925 /* QLOPML.appex in Embed App Extensions */,
|
||||
54AD90F72E30C48400160925 /* QLOPML.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed App Extensions";
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
@@ -156,6 +156,8 @@
|
||||
546A6A2B22C584AF0034E806 /* SettingsAppearanceView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsAppearanceView.h; sourceTree = "<group>"; };
|
||||
546A6A2D22C585580034E806 /* SettingsAboutView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsAboutView.m; sourceTree = "<group>"; };
|
||||
546A6A2E22C585580034E806 /* SettingsAboutView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsAboutView.h; sourceTree = "<group>"; };
|
||||
546BD1882EDE156000943942 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
|
||||
546BD1892EDE156000943942 /* Config-debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Config-debug.xcconfig"; sourceTree = "<group>"; };
|
||||
546FC43B21188AD5007CC3A3 /* SettingsFeeds.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsFeeds.h; sourceTree = "<group>"; };
|
||||
546FC43C21188AD5007CC3A3 /* SettingsFeeds.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsFeeds.m; sourceTree = "<group>"; };
|
||||
546FC44021189975007CC3A3 /* SettingsGeneral.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsGeneral.h; sourceTree = "<group>"; };
|
||||
@@ -365,6 +367,8 @@
|
||||
54ACC27321061B3B0020715F = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
546BD1882EDE156000943942 /* Config.xcconfig */,
|
||||
546BD1892EDE156000943942 /* Config-debug.xcconfig */,
|
||||
540CD14821C094A2004AB594 /* README.md */,
|
||||
54892F1D2235285700271CBA /* CHANGELOG.md */,
|
||||
54ACC27E21061B3B0020715F /* baRSS */,
|
||||
@@ -536,7 +540,7 @@
|
||||
54ACC27A21061B3B0020715F /* Resources */,
|
||||
544DCCBB212A2B4D002DBC46 /* Embed Frameworks */,
|
||||
54FB05D12305BFAB00A088AD /* dynamic app name in db migration */,
|
||||
54AD90F62E30C48400160925 /* Embed App Extensions */,
|
||||
54AD90F62E30C48400160925 /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -572,7 +576,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1640;
|
||||
LastUpgradeCheck = 2600;
|
||||
ORGANIZATIONNAME = relikd;
|
||||
TargetAttributes = {
|
||||
54ACC27B21061B3B0020715F = {
|
||||
@@ -772,6 +776,7 @@
|
||||
/* Begin XCBuildConfiguration section */
|
||||
54ACC28E21061B3C0020715F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 546BD1892EDE156000943942 /* Config-debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
@@ -805,7 +810,6 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 16785;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = UY657LKNHJ;
|
||||
@@ -822,8 +826,6 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 1.5.3;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
@@ -832,6 +834,7 @@
|
||||
};
|
||||
54ACC28F21061B3C0020715F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 546BD1882EDE156000943942 /* Config.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
@@ -866,7 +869,6 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 16785;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = UY657LKNHJ;
|
||||
@@ -880,8 +882,6 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 1.5.3;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
};
|
||||
@@ -931,8 +931,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
"$(FRAMEWORK_SEARCH_PATHS)",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.baRSS.beta;
|
||||
PRODUCT_NAME = "$(TARGET_NAME) Beta";
|
||||
PRODUCT_NAME = "$(inherited) Beta";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -981,8 +980,6 @@
|
||||
"@executable_path/../Frameworks",
|
||||
"$(FRAMEWORK_SEARCH_PATHS)",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.baRSS;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -1003,7 +1000,7 @@
|
||||
);
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.baRSS.beta.QLOPML;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).QLOPML";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
};
|
||||
@@ -1021,7 +1018,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.baRSS.QLOPML;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).QLOPML";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1640"
|
||||
LastUpgradeVersion = "2600"
|
||||
version = "1.8">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
8
baRSS/Artwork/icon-appearance-article.svg
Normal file
8
baRSS/Artwork/icon-appearance-article.svg
Normal 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 |
7
baRSS/Artwork/icon-appearance-group.svg
Normal file
7
baRSS/Artwork/icon-appearance-group.svg
Normal 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 |
11
baRSS/Artwork/icon-appearance-main-menu.svg
Normal file
11
baRSS/Artwork/icon-appearance-main-menu.svg
Normal 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 |
7
baRSS/Artwork/icon-regex.svg
Normal file
7
baRSS/Artwork/icon-regex.svg
Normal 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 |
7
baRSS/Artwork/icon-rss-plain-paused.svg
Normal file
7
baRSS/Artwork/icon-rss-plain-paused.svg
Normal 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 |
6
baRSS/Artwork/icon-rss-plain.svg
Normal file
6
baRSS/Artwork/icon-rss-plain.svg
Normal 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 |
@@ -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
|
||||
|
||||
@@ -63,7 +63,14 @@
|
||||
item.state = (self.unread && UserPrefsBool(Pref_feedUnreadIndicator) ? 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_tooltipCharacterLimit);
|
||||
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:);
|
||||
|
||||
@@ -23,126 +23,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 +60,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;
|
||||
CGContextSetFillColorWithColor(c, [NSColor controlTextColor].CGColor);
|
||||
|
||||
// menu bar
|
||||
CGContextSetAlpha(c, .23);
|
||||
const CGFloat barHeightInset = round(size*.06);
|
||||
svgRect(c, 1, CGRectInset(r, 0, barHeightInset));
|
||||
CGContextFillPath(c);
|
||||
|
||||
const CGFloat offset = round(size*.75);
|
||||
const CGFloat iconInset = round(size*.2);
|
||||
const CGFloat iconCorner = size*.12;
|
||||
CGContextSetAlpha(c, .66);
|
||||
|
||||
// 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 +277,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; });
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
// ------ 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";
|
||||
@@ -38,6 +39,7 @@
|
||||
// ------ 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 2k */ static NSString* const Pref_tooltipCharacterLimit = @"tooltipCharacterLimit";
|
||||
/** 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";
|
||||
|
||||
@@ -20,6 +20,7 @@ void UserPrefsInit(void) {
|
||||
Pref_feedUnreadIndicator
|
||||
]);
|
||||
defaultsAppend(defs, @NO, @[
|
||||
Pref_globalToggleHidden,
|
||||
Pref_groupUnreadOnly, Pref_feedUnreadOnly,
|
||||
Pref_groupUnreadIndicator,
|
||||
Pref_feedTruncateTitle,
|
||||
@@ -28,6 +29,7 @@ void UserPrefsInit(void) {
|
||||
// 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:2000] forKey:Pref_tooltipCharacterLimit];
|
||||
[defs setObject:[NSNumber numberWithUnsignedInteger:40] forKey:Pref_articlesInMenuLimit];
|
||||
[defs setObject:[NSNumber numberWithUnsignedInteger:1] forKey:Pref_prefSelectedTab]; // feed tab
|
||||
[[NSUserDefaults standardUserDefaults] registerDefaults:defs];
|
||||
|
||||
@@ -18,29 +18,97 @@
|
||||
- (instancetype)init {
|
||||
self = [super initWithFrame:NSMakeRect(0, 0, 320, 327)];
|
||||
// Insert matrix header (icons above checkbox matrix)
|
||||
ColumnIcon(self, X__, RSSImageSettingsGlobal, NSLocalizedString(@"Show in menu bar", nil));
|
||||
ColumnIcon(self, _X_, RSSImageSettingsGroup, NSLocalizedString(@"Show in group menu", nil));
|
||||
ColumnIcon(self, __X, RSSImageSettingsFeed, NSLocalizedString(@"Show in feed menu", nil));
|
||||
ColumnIcon(self, X__, RSSImageSettingsGlobalMenu);
|
||||
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) c1:Pref_globalTintMenuIcon c2:nil c3:nil];
|
||||
[self entry:NSLocalizedString(@"Update all feeds", nil) c1:Pref_globalUpdateAll c2:nil c3:nil];
|
||||
[self entry:NSLocalizedString(@"Open all unread", nil) c1:Pref_globalOpenUnread c2:Pref_groupOpenUnread c3:Pref_feedOpenUnread];
|
||||
[self entry:NSLocalizedString(@"Mark all read", nil) c1:Pref_globalMarkRead c2:Pref_groupMarkRead c3:Pref_feedMarkRead];
|
||||
[self entry:NSLocalizedString(@"Mark all unread", nil) c1:Pref_globalMarkUnread c2:Pref_groupMarkUnread c3:Pref_feedMarkUnread];
|
||||
[self entry:NSLocalizedString(@"Number of unread articles", nil) c1:Pref_globalUnreadCount c2:Pref_groupUnreadCount c3:Pref_feedUnreadCount];
|
||||
[self entry:NSLocalizedString(@"Indicator for unread articles", nil) c1:nil c2:Pref_groupUnreadIndicator c3:Pref_feedUnreadIndicator];
|
||||
[self entry:NSLocalizedString(@"Show only unread / hide read", nil) c1:nil c2:Pref_groupUnreadOnly c3:Pref_feedUnreadOnly];
|
||||
[[self entry:NSLocalizedString(@"Truncate article title", nil) c1:nil c2:nil c3:Pref_feedTruncateTitle]
|
||||
tooltip:NSLocalizedString(@"Truncate article title after 60 characters", nil)];
|
||||
[[self entry:NSLocalizedString(@"Limit number of articles", nil) c1:nil c2:nil c3:Pref_feedLimitArticles]
|
||||
tooltip:NSLocalizedString(@"Display at most 40 articles in feed menu", nil)];
|
||||
[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 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 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)
|
||||
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)];
|
||||
|
||||
[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)];
|
||||
|
||||
[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)];
|
||||
|
||||
[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)];
|
||||
|
||||
[self entry:NSLocalizedString(@"Indicator for unread articles", nil)
|
||||
help:NSLocalizedString(@"Show blue dot 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)];
|
||||
|
||||
[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)];
|
||||
|
||||
[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)];
|
||||
|
||||
[[[[[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];
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Helper method for matrix table header icons
|
||||
static inline void ColumnIcon(id this, CGFloat x, const NSImageName img, NSString *ttip) {
|
||||
[[[NSView imageView:img size:IconSize] placeIn:this x:x yTop:PAD_WIN] tooltip:ttip];
|
||||
static inline void ColumnIcon(id this, CGFloat x, const NSImageName img) {
|
||||
[[NSView imageView:img size:IconSize] placeIn:this x:x yTop:PAD_WIN];
|
||||
}
|
||||
|
||||
/// Helper method for generating a checkbox
|
||||
@@ -51,14 +119,22 @@ static inline NSButton* Checkbox(id this, CGFloat x, CGFloat y, NSString *key) {
|
||||
}
|
||||
|
||||
/// Create new entry with 1-3 checkboxes and a descriptive label
|
||||
- (NSTextField*)entry:(NSString*)label c1:(NSString*)pref1 c2:(NSString*)pref2 c3:(NSString*)pref3 {
|
||||
- (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
|
||||
{
|
||||
CGFloat y = self.y;
|
||||
self.y += (PAD_S + HEIGHT_LABEL);
|
||||
// TODO: localize: global, group, feed
|
||||
if (pref1) Checkbox(self, X__ + 2, y + 2, pref1).accessibilityLabel = [label stringByAppendingString:@" (global)"];
|
||||
if (pref2) Checkbox(self, _X_ + 2, y + 2, pref2).accessibilityLabel = [label stringByAppendingString:@" (group)"];
|
||||
if (pref3) Checkbox(self, __X + 2, y + 2, pref3).accessibilityLabel = [label stringByAppendingString:@" (feed)"];
|
||||
return [[[NSView label:label] placeIn:self x:PAD_WIN + 3 * colWidth yTop:y] sizeToRight:PAD_WIN];
|
||||
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 (extraTip != nil) {
|
||||
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];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
NSUserInterfaceItemIdentifier const CustomCellName = @"NameColumnCell";
|
||||
|
||||
- (instancetype)initWithFrame:(NSRect)frameRect {
|
||||
self = [super initWithFrame:frameRect];
|
||||
self = [super initWithFrame:NSMakeRect(0, 0, 100, 0)];
|
||||
self.identifier = CustomCellName;
|
||||
self.imageView = [[NSView imageView:nil size:16] placeIn:self x:1 yTop:1];
|
||||
self.imageView.accessibilityLabel = NSLocalizedString(@"Feed icon", nil);
|
||||
@@ -195,7 +195,7 @@ NSUserInterfaceItemIdentifier const CustomCellName = @"NameColumnCell";
|
||||
NSUserInterfaceItemIdentifier const CustomCellRefresh = @"RefreshColumnCell";
|
||||
|
||||
- (instancetype)initWithFrame:(NSRect)frameRect {
|
||||
self = [super initWithFrame:frameRect];
|
||||
self = [super initWithFrame:NSMakeRect(0, 0, 100, 0)];
|
||||
self.identifier = CustomCellRefresh;
|
||||
self.textField = [[[[NSView label:@""] textRight] placeIn:self x:0 yTop:0] sizeToRight:0];
|
||||
self.textField.accessibilityTitle = @" "; // otherwise groups and separators will say 'text'
|
||||
@@ -224,7 +224,7 @@ NSUserInterfaceItemIdentifier const CustomCellRefresh = @"RefreshColumnCell";
|
||||
NSUserInterfaceItemIdentifier const CustomCellSeparator = @"SeparatorColumnCell";
|
||||
|
||||
- (instancetype)initWithFrame:(NSRect)frameRect {
|
||||
self = [super initWithFrame:frameRect];
|
||||
self = [super initWithFrame:NSMakeRect(0, 0, 100, 0)];
|
||||
self.identifier = CustomCellSeparator;
|
||||
[[[[DrawSeparator alloc] initWithFrame:self.frame] placeIn:self x:0 y:0] sizableWidthAndHeight];
|
||||
return self;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface BarMenu : NSObject <NSMenuDelegate>
|
||||
@property (assign) BOOL showHidden;
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (instancetype)initWithStatusItem:(BarStatusItem*)statusItem NS_DESIGNATED_INITIALIZER;
|
||||
@end
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
- (void)setFeedGroups:(NSArray<FeedGroup*>*)sortedList forMenu:(NSMenu*)menu {
|
||||
[menu insertDefaultHeader];
|
||||
for (FeedGroup *fg in sortedList) {
|
||||
[menu insertFeedGroupItem:fg withUnread:self.unreadMap].submenu.delegate = self;
|
||||
[menu insertFeedGroupItem:fg withUnread:self.unreadMap showHidden:_showHidden].submenu.delegate = self;
|
||||
}
|
||||
[menu setHeaderHasUnread:self.unreadMap[menu.titleIndexPath]];
|
||||
}
|
||||
@@ -78,7 +78,7 @@
|
||||
BOOL onlyUnread = UserPrefsBool(Pref_feedUnreadOnly);
|
||||
|
||||
for (FeedArticle *fa in sortedList) {
|
||||
if (onlyUnread && !fa.unread)
|
||||
if (onlyUnread && !fa.unread && !_showHidden)
|
||||
continue;
|
||||
if (--mc < 0) // mc == 0 will first decrement to -1, then evaluate
|
||||
break;
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
@property (strong) NSStatusItem *statusItem;
|
||||
@property (assign) NSInteger unreadCountTotal;
|
||||
@property (weak) NSMenuItem *updateAllItem;
|
||||
/// Set to `true` if user toggled the `"Show Hidden Articles"` menu option.
|
||||
@property (assign) BOOL optShowHidden;
|
||||
/// Set to `true` if menu bar was opened while holding down option-key.
|
||||
@property (assign) BOOL holdingOptKey;
|
||||
@end
|
||||
|
||||
@implementation BarStatusItem
|
||||
@@ -150,8 +154,10 @@
|
||||
#pragma mark - Main Menu Handling
|
||||
|
||||
-(void)menuWillOpen:(NSMenu *)menu {
|
||||
self.holdingOptKey = NSEvent.modifierFlags & NSEventModifierFlagOption;
|
||||
_mainMenu = menu; // autoreleased once closed
|
||||
self.barMenu = [[BarMenu alloc] initWithStatusItem:self];
|
||||
self.barMenu.showHidden = self.optShowHidden || self.holdingOptKey;
|
||||
|
||||
[self insertMainMenuHeader:menu];
|
||||
[self.barMenu menuNeedsUpdate:menu];
|
||||
@@ -165,14 +171,29 @@
|
||||
self.barMenu = nil;
|
||||
self.statusItem.menu = [[NSMenu alloc] initWithTitle:@"M"];
|
||||
self.statusItem.menu.delegate = self;
|
||||
self.holdingOptKey = NO;
|
||||
}
|
||||
|
||||
- (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
|
||||
if (UserPrefsBool(Pref_globalToggleHidden)) {
|
||||
NSMenuItem *toggleHidden = [menu addItemWithTitle:NSLocalizedString(@"Show hidden articles", nil) action:@selector(toggleHiddenArticles) keyEquivalent:@"h"];
|
||||
toggleHidden.target = self;
|
||||
toggleHidden.enabled = !self.holdingOptKey && (UserPrefsBool(Pref_groupUnreadOnly) || UserPrefsBool(Pref_feedUnreadOnly));
|
||||
[toggleHidden setState:self.barMenu.showHidden ? NSControlStateValueOn : NSControlStateValueOff];
|
||||
if (!toggleHidden.enabled) {
|
||||
toggleHidden.toolTip = self.holdingOptKey
|
||||
? NSLocalizedString(@"Option disabled because overwritten by holding down option-key.", nil)
|
||||
: NSLocalizedString(@"Option disabled because appearance setting for “Show only unread” is disabled.", nil);
|
||||
}
|
||||
}
|
||||
|
||||
// 'Update all feeds' item
|
||||
if (UserPrefsBool(Pref_globalUpdateAll)) {
|
||||
NSMenuItem *updateAll = [menu addItemWithTitle:NSLocalizedString(@"Update all feeds", nil) action:@selector(updateAllFeeds) keyEquivalent:@""];
|
||||
@@ -190,6 +211,12 @@
|
||||
[self updateBarIcon];
|
||||
}
|
||||
|
||||
/// Called when user clicks on 'Show Hidden Articles' (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).
|
||||
- (void)updateAllFeeds {
|
||||
// [self asyncReloadUnreadCount]; // should not be necessary
|
||||
|
||||
@@ -9,7 +9,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@property (readonly) BOOL isFeedMenu;
|
||||
|
||||
// Generator
|
||||
- (nullable NSMenuItem*)insertFeedGroupItem:(FeedGroup*)fg withUnread:(MapUnreadTotal*)unreadMap;
|
||||
- (nullable NSMenuItem*)insertFeedGroupItem:(FeedGroup*)fg withUnread:(MapUnreadTotal*)unreadMap showHidden:(BOOL)showHidden;
|
||||
- (void)insertDefaultHeader;
|
||||
// Update menu
|
||||
- (void)setHeaderHasUnread:(UnreadTotal*)count;
|
||||
|
||||
@@ -44,7 +44,7 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
|
||||
#pragma mark - Generator -
|
||||
|
||||
/// Create new @c NSMenuItem with empty submenu and append it to the menu. @return Inserted item.
|
||||
- (nullable NSMenuItem*)insertFeedGroupItem:(FeedGroup*)fg withUnread:(MapUnreadTotal*)unreadMap {
|
||||
- (nullable NSMenuItem*)insertFeedGroupItem:(FeedGroup*)fg withUnread:(MapUnreadTotal*)unreadMap showHidden:(BOOL)showHidden {
|
||||
unichar chr = '-';
|
||||
NSMenuItem *item = nil;
|
||||
switch (fg.type) {
|
||||
@@ -57,7 +57,7 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
|
||||
NSUInteger unread = unreadMap[[t substringFromIndex:2]].unread;
|
||||
|
||||
// Check user preferences to show only unread entries
|
||||
if (unread == 0
|
||||
if (unread == 0 && !showHidden
|
||||
&& (fg.type == FEED || fg.type == GROUP)
|
||||
&& UserPrefsBool(Pref_groupUnreadOnly)) {
|
||||
item.hidden = YES;
|
||||
|
||||
Reference in New Issue
Block a user