Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4864208754 | ||
|
|
76c7263548 | ||
|
|
7f40bb259c | ||
|
|
68aa4ef94b | ||
|
|
217a91b23c | ||
|
|
f0299d8246 | ||
|
|
ae0d5967c7 | ||
|
|
d45d4864b0 | ||
|
|
ef2c588f4c | ||
|
|
03aecdfa4a | ||
|
|
3b65bca88f | ||
|
|
bd03059247 | ||
|
|
d03840757a | ||
|
|
2e77f67102 | ||
|
|
5d339b8125 | ||
|
|
65cac6b19a | ||
|
|
2ec1743dd9 | ||
|
|
ca2b3cb887 | ||
|
|
b1ca30f914 | ||
|
|
5427cb58ee | ||
|
|
b94dd030b4 | ||
|
|
469d7bcdd4 | ||
|
|
0806003fc3 | ||
|
|
385bcf99f3 | ||
|
|
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 | ||
|
|
5894b12c1d | ||
|
|
0700eebb13 | ||
|
|
4c4a133fe2 | ||
|
|
ccca329630 |
46
CHANGELOG.md
46
CHANGELOG.md
@@ -5,6 +5,47 @@ 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
|
||||
- *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
|
||||
|
||||
|
||||
## [1.5.2] – 2025-10-29
|
||||
### Added
|
||||
- *Notifications:* Reply with "Open in background", "Mark read & dismiss", or "Open but keep unread"
|
||||
|
||||
|
||||
## [1.5.1] – 2025-10-27
|
||||
### Fixed
|
||||
- *Status Bar Menu:* Simplified options for "Show only unread"
|
||||
@@ -220,6 +261,11 @@ 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
|
||||
[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
|
||||
[1.5.0]: https://github.com/relikd/baRSS/compare/v1.4.1...v1.5.0
|
||||
[1.4.1]: https://github.com/relikd/baRSS/compare/v1.4.0...v1.4.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.6.0
|
||||
PRODUCT_NAME = baRSS
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.baRSS
|
||||
CURRENT_PROJECT_VERSION = 17752
|
||||
45
README.md
45
README.md
@@ -1,4 +1,4 @@
|
||||
[](#download--install)
|
||||
[](#download--install)
|
||||
[](https://github.com/relikd/baRSS/releases)
|
||||
[](https://github.com/relikd/baRSS/releases)
|
||||
[](LICENSE)
|
||||
@@ -35,25 +35,20 @@ Further, tuning the update frequently will decrease the traffic even more.
|
||||
Download & Install
|
||||
------------------
|
||||
|
||||
Requires macOS High Sierra (10.13) or higher.
|
||||
Requires macOS Mojave (10.14) or higher.
|
||||
|
||||
### Easy way
|
||||
Go to [releases](https://github.com/relikd/baRSS/releases) and downloaded the latest version.
|
||||
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.
|
||||
@@ -69,19 +64,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
|
||||
|
||||
@@ -90,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"
|
||||
```
|
||||
@@ -183,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.
|
||||
@@ -192,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
|
||||
|
||||
@@ -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 */; };
|
||||
@@ -46,7 +47,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 +106,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 */
|
||||
@@ -150,12 +151,16 @@
|
||||
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>"; };
|
||||
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 +370,8 @@
|
||||
54ACC27321061B3B0020715F = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
546BD1882EDE156000943942 /* Config.xcconfig */,
|
||||
546BD1892EDE156000943942 /* Config-debug.xcconfig */,
|
||||
540CD14821C094A2004AB594 /* README.md */,
|
||||
54892F1D2235285700271CBA /* CHANGELOG.md */,
|
||||
54ACC27E21061B3B0020715F /* baRSS */,
|
||||
@@ -511,6 +518,8 @@
|
||||
54910066233A4D4000858AE2 /* URLScheme.m */,
|
||||
54229F532E02491A0019ACB0 /* TinySVG.h */,
|
||||
54229F542E02491A0019ACB0 /* TinySVG.m */,
|
||||
545EB5D62EE8620300FABBE0 /* StrictUIntFormatter.h */,
|
||||
545EB5D92EE8622200FABBE0 /* StrictUIntFormatter.m */,
|
||||
);
|
||||
path = Helper;
|
||||
sourceTree = "<group>";
|
||||
@@ -536,7 +545,7 @@
|
||||
54ACC27A21061B3B0020715F /* Resources */,
|
||||
544DCCBB212A2B4D002DBC46 /* Embed Frameworks */,
|
||||
54FB05D12305BFAB00A088AD /* dynamic app name in db migration */,
|
||||
54AD90F62E30C48400160925 /* Embed App Extensions */,
|
||||
54AD90F62E30C48400160925 /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -572,7 +581,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1640;
|
||||
LastUpgradeCheck = 2600;
|
||||
ORGANIZATIONNAME = relikd;
|
||||
TargetAttributes = {
|
||||
54ACC27B21061B3B0020715F = {
|
||||
@@ -728,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 */,
|
||||
@@ -772,6 +782,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 +816,6 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 16720;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = UY657LKNHJ;
|
||||
@@ -822,8 +832,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.1;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
@@ -832,6 +840,7 @@
|
||||
};
|
||||
54ACC28F21061B3C0020715F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 546BD1882EDE156000943942 /* Config.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
@@ -866,7 +875,6 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 16720;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = UY657LKNHJ;
|
||||
@@ -880,8 +888,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.1;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
};
|
||||
@@ -931,8 +937,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 +986,6 @@
|
||||
"@executable_path/../Frameworks",
|
||||
"$(FRAMEWORK_SEARCH_PATHS)",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.baRSS;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -1003,7 +1006,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 +1024,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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -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:);
|
||||
|
||||
@@ -225,9 +225,11 @@
|
||||
return nil; // if success == NO, do not modify unread state & exit
|
||||
}
|
||||
|
||||
NSInteger countChange = 0;
|
||||
for (FeedArticle *fa in list) {
|
||||
if (fa.unread == markRead) { // only if differs
|
||||
fa.unread = !markRead;
|
||||
countChange += markRead ? -1 : +1;
|
||||
}
|
||||
}
|
||||
[self saveContext:moc andParent:YES];
|
||||
@@ -242,8 +244,7 @@
|
||||
}
|
||||
|
||||
[moc reset];
|
||||
NSNumber *num = [NSNumber numberWithInteger: (markRead ? -1 : +1) * (NSInteger)list.count ];
|
||||
PostNotification(kNotificationTotalUnreadCountChanged, num);
|
||||
PostNotification(kNotificationTotalUnreadCountChanged, @(countChange));
|
||||
return dbRefs;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
/// Draw separator line in @c NSOutlineView
|
||||
IB_DESIGNABLE
|
||||
@interface DrawSeparator : NSView
|
||||
@property (assign) BOOL invert;
|
||||
+ (instancetype)withSize:(NSSize)size;
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@@ -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; });
|
||||
}
|
||||
|
||||
6
baRSS/Helper/StrictUIntFormatter.h
Normal file
6
baRSS/Helper/StrictUIntFormatter.h
Normal 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
|
||||
40
baRSS/Helper/StrictUIntFormatter.m
Normal file
40
baRSS/Helper/StrictUIntFormatter.m
Normal 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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -15,30 +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 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";
|
||||
|
||||
|
||||
@@ -17,18 +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_groupUnreadOnly, Pref_feedUnreadOnly,
|
||||
Pref_groupUnreadIndicator,
|
||||
Pref_feedTruncateTitle,
|
||||
Pref_feedLimitArticles
|
||||
Pref_globalToggleHidden,
|
||||
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];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#import "UserPrefs.h"
|
||||
#import "StoreCoordinator.h"
|
||||
#import "Feed+Ext.h"
|
||||
#import "FeedGroup+Ext.h"
|
||||
#import "FeedArticle+Ext.h"
|
||||
|
||||
/**
|
||||
@@ -9,6 +10,11 @@
|
||||
*/
|
||||
static NSString* const kNotifyIdGlobal = @"global";
|
||||
|
||||
static NSString* const kCategoryDismissable = @"DISMISSIBLE";
|
||||
static NSString* const kActionOpenBackground = @"OPEN_IN_BACKGROUND";
|
||||
static NSString* const kActionMarkRead = @"MARK_READ_DONT_OPEN";
|
||||
static NSString* const kActionOpenOnly = @"OPEN_ONLY_DONT_MARK_READ";
|
||||
|
||||
|
||||
@implementation NotifyEndpoint
|
||||
|
||||
@@ -18,20 +24,27 @@ static NotificationType notifyType;
|
||||
/// Ask user for permission to send notifications @b AND register delegate to respond to alert banner clicks.
|
||||
/// @note Called every time user changes notification settings
|
||||
+ (void)activate {
|
||||
UNUserNotificationCenter *center = UNUserNotificationCenter.currentNotificationCenter;
|
||||
notifyType = UserPrefsNotificationType();
|
||||
|
||||
// even if disabled, register delegate. This allows to open previously sent notifications
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
singleton = [NotifyEndpoint new];
|
||||
UNUserNotificationCenter.currentNotificationCenter.delegate = singleton;
|
||||
center.delegate = singleton;
|
||||
});
|
||||
|
||||
if (notifyType == NotificationTypeDisabled) {
|
||||
return;
|
||||
}
|
||||
// register action types (allow mark read without opening notification)
|
||||
UNNotificationAction *openBackgroundAction = [UNNotificationAction actionWithIdentifier:kActionOpenBackground title:NSLocalizedString(@"Open in background", nil) options:UNNotificationActionOptionNone];
|
||||
UNNotificationAction *dontOpenAction = [UNNotificationAction actionWithIdentifier:kActionMarkRead title:NSLocalizedString(@"Mark read & dismiss", nil) options:UNNotificationActionOptionNone];
|
||||
UNNotificationAction *dontReadAction = [UNNotificationAction actionWithIdentifier:kActionOpenOnly title:NSLocalizedString(@"Open but keep unread", nil) options:UNNotificationActionOptionNone];
|
||||
UNNotificationCategory *category = [UNNotificationCategory categoryWithIdentifier:kCategoryDismissable actions:@[openBackgroundAction, dontOpenAction, dontReadAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionNone];
|
||||
[center setNotificationCategories:[NSSet setWithObject:category]];
|
||||
|
||||
[UNUserNotificationCenter.currentNotificationCenter requestAuthorizationWithOptions:UNAuthorizationOptionAlert | UNAuthorizationOptionSound completionHandler:^(BOOL granted, NSError * _Nullable error) {
|
||||
[center requestAuthorizationWithOptions:UNAuthorizationOptionAlert | UNAuthorizationOptionSound completionHandler:^(BOOL granted, NSError * _Nullable error) {
|
||||
if (error) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSAlert *alert = [[NSAlert alloc] init];
|
||||
@@ -72,7 +85,7 @@ static NotificationType notifyType;
|
||||
if (count > 0) {
|
||||
[feed.managedObjectContext obtainPermanentIDsForObjects:@[feed] error:nil];
|
||||
[self send:feed.notificationID
|
||||
title:feed.title
|
||||
title:feed.group.anyName
|
||||
body:[NSString stringWithFormat:NSLocalizedString(@"%ld unread articles", nil), count]];
|
||||
}
|
||||
}
|
||||
@@ -84,7 +97,7 @@ static NotificationType notifyType;
|
||||
}
|
||||
[article.managedObjectContext obtainPermanentIDsForObjects:@[article] error:nil];
|
||||
[self send:article.notificationID
|
||||
title:article.feed.title
|
||||
title:article.feed.group.anyName
|
||||
body:article.title];
|
||||
}
|
||||
|
||||
@@ -105,6 +118,7 @@ static NotificationType notifyType;
|
||||
if (title != nil) msg.title = title;
|
||||
if (body != nil) msg.body = body;
|
||||
// common settings:
|
||||
msg.categoryIdentifier = kCategoryDismissable;
|
||||
// TODO: make sound configurable?
|
||||
msg.sound = [UNNotificationSound defaultSound];
|
||||
[self send:identifier content: msg];
|
||||
@@ -167,7 +181,12 @@ static NotificationType notifyType;
|
||||
return;
|
||||
}
|
||||
}
|
||||
[StoreCoordinator updateArticles:articles markRead:YES andOpen:YES inContext:moc];
|
||||
|
||||
// open-in-background performs the same operation as a normal click
|
||||
// the "background" part is triggered by _NOT_ having the UNNotificationActionOptionForeground option
|
||||
BOOL dontOpen = [response.actionIdentifier isEqualToString:kActionMarkRead];
|
||||
BOOL dontMarkRead = [response.actionIdentifier isEqualToString:kActionOpenOnly];
|
||||
[StoreCoordinator updateArticles:articles markRead:!dontMarkRead andOpen:!dontOpen inContext:moc];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@import Cocoa;
|
||||
@class SettingsAppearance;
|
||||
|
||||
@interface SettingsAppearanceView : NSView
|
||||
@interface SettingsAppearanceView : NSView <NSTextFieldDelegate>
|
||||
@end
|
||||
|
||||
|
||||
@@ -2,63 +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, 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));
|
||||
// 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.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 note:NSLocalizedString(@"Hover over the options for additional explanations and usage tips.", 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:nil
|
||||
c1:Pref_globalOpenUnread c2:Pref_groupOpenUnread c3:Pref_feedOpenUnread c4:nil];
|
||||
|
||||
[self entry:NSLocalizedString(@"“Mark all read”", nil)
|
||||
help:NSLocalizedString(@"Show button to mark articles read.", nil)
|
||||
tip:nil
|
||||
c1:Pref_globalMarkRead c2:Pref_groupMarkRead c3:Pref_feedMarkRead c4:nil];
|
||||
|
||||
[self entry:NSLocalizedString(@"“Mark all unread”", nil)
|
||||
help:NSLocalizedString(@"Show button to mark articles unread.", 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 c2:Pref_groupUnreadCount c3:Pref_feedUnreadCount c4:nil];
|
||||
|
||||
[self entry:NSLocalizedString(@"Color for unread articles", nil)
|
||||
help:NSLocalizedString(@"Show color marker on menu items with unread articles.", nil)
|
||||
tip: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:nil
|
||||
c1:nil c2:Pref_groupUnreadOnly c3:Pref_feedUnreadOnly c4:Pref_articleUnreadOnly];
|
||||
|
||||
// self.y += PAD_M;
|
||||
// [self note:NSLocalizedString(@"Hold down option-key before opening the main menu to temporarily show hidden feeds.", nil)];
|
||||
|
||||
|
||||
// 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, NSString *ttip) {
|
||||
[[[NSView imageView:img size:IconSize] placeIn:this x:x yTop:PAD_WIN] tooltip:ttip];
|
||||
|
||||
// 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 c1:(NSString*)pref1 c2:(NSString*)pref2 c3:(NSString*)pref3 {
|
||||
CGFloat y = self.y;
|
||||
- (NSTextField*)entry:(NSString*)label help:(NSString*)ttip tip:(NSString*)extraTip
|
||||
c1:(NSString*)pref1 c2:(NSString*)pref2 c3:(NSString*)pref3 c4:(NSString*)pref4
|
||||
{
|
||||
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\n💡 Tip: %@", extraTip];
|
||||
}
|
||||
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);
|
||||
// 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];
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -224,7 +225,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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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]];
|
||||
}
|
||||
@@ -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)
|
||||
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]];
|
||||
}
|
||||
|
||||
@@ -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 feeds"` 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
|
||||
@@ -103,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;
|
||||
@@ -150,8 +157,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 +174,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 feeds' item
|
||||
if (UserPrefsBool(Pref_globalToggleHidden)) {
|
||||
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) || UserPrefsBool(Pref_articleUnreadOnly));
|
||||
[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,7 +214,13 @@
|
||||
[self updateBarIcon];
|
||||
}
|
||||
|
||||
/// Called when user clicks on 'Update all feeds' (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).
|
||||
- (void)updateAllFeeds {
|
||||
// [self asyncReloadUnreadCount]; // should not be necessary
|
||||
[UpdateScheduler forceUpdateAllFeeds];
|
||||
|
||||
@@ -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,9 +57,9 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
|
||||
NSUInteger unread = unreadMap[[t substringFromIndex:2]].unread;
|
||||
|
||||
// Check user preferences to show only unread entries
|
||||
if (unread == 0
|
||||
&& (fg.type == FEED || fg.type == GROUP)
|
||||
&& UserPrefsBool(Pref_groupUnreadOnly)) {
|
||||
if (unread == 0 && !showHidden
|
||||
&& ((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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user