Refactoring Interface Builder UI to code equivalent

This commit is contained in:
relikd
2019-07-02 11:10:34 +02:00
parent ba3310849c
commit 8e712cae20
47 changed files with 2072 additions and 2392 deletions

View File

@@ -6,6 +6,12 @@ and this project does NOT adhere to [Semantic Versioning](https://semver.org/spe
## [Unreleased]
### Fixed
- Changed error message text when user cancels creation of new feed item
- Comparing existing articles with nonexistent guid and link
### Changed
- Interface builder files replaced with code equivalent
## [0.9.4] - 2019-04-02

View File

@@ -10,19 +10,20 @@
540F704521B6C16C0022E69D /* FeedMeta+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 540F704421B6C16C0022E69D /* FeedMeta+Ext.m */; };
54195883218A061100581B79 /* Feed+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54195882218A061100581B79 /* Feed+Ext.m */; };
54195886218E1BDB00581B79 /* NSMenu+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54195885218E1BDB00581B79 /* NSMenu+Ext.m */; };
541C67C32255470B004D2CE6 /* SettingsAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 541C67C22255470B004D2CE6 /* SettingsAppearance.m */; };
54209E942117325100F3B5EF /* DrawImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 54209E932117325100F3B5EF /* DrawImage.m */; };
544936FB21F1E66100DEE9AA /* Statistics.m in Sources */ = {isa = PBXBuildFile; fileRef = 544936FA21F1E66100DEE9AA /* Statistics.m */; };
544B011A2114B41200386E5C /* ModalSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B01192114B41200386E5C /* ModalSheet.m */; };
544B011D2114EE9100386E5C /* AppHook.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B011C2114EE9100386E5C /* AppHook.m */; };
544DCCB9212A2B4D002DBC46 /* RSXML.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 544DCCB8212A2B4D002DBC46 /* RSXML.framework */; };
544DCCBA212A2B4D002DBC46 /* RSXML.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 544DCCB8212A2B4D002DBC46 /* RSXML.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
544DCCBE212A2B6F002DBC46 /* RSXML.framework.dSYM in CopyFiles */ = {isa = PBXBuildFile; fileRef = 544DCCBD212A2B6F002DBC46 /* RSXML.framework.dSYM */; };
546A6A2922C583390034E806 /* SettingsGeneralView.m in Sources */ = {isa = PBXBuildFile; fileRef = 54D857D122802309001BA1C8 /* SettingsGeneralView.m */; };
546A6A2C22C584AF0034E806 /* SettingsAppearanceView.m in Sources */ = {isa = PBXBuildFile; fileRef = 546A6A2A22C584AF0034E806 /* SettingsAppearanceView.m */; };
546A6A2F22C585580034E806 /* SettingsAboutView.m in Sources */ = {isa = PBXBuildFile; fileRef = 546A6A2D22C585580034E806 /* SettingsAboutView.m */; };
546FC43D21188AD5007CC3A3 /* SettingsFeeds.m in Sources */ = {isa = PBXBuildFile; fileRef = 546FC43C21188AD5007CC3A3 /* SettingsFeeds.m */; };
546FC43F21188C78007CC3A3 /* SettingsFeeds.xib in Resources */ = {isa = PBXBuildFile; fileRef = 546FC43E21188C78007CC3A3 /* SettingsFeeds.xib */; };
546FC44321189975007CC3A3 /* SettingsGeneral.m in Sources */ = {isa = PBXBuildFile; fileRef = 546FC44121189975007CC3A3 /* SettingsGeneral.m */; };
546FC44421189975007CC3A3 /* SettingsGeneral.xib in Resources */ = {isa = PBXBuildFile; fileRef = 546FC44221189975007CC3A3 /* SettingsGeneral.xib */; };
546FC4472118A8E6007CC3A3 /* Preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = 546FC4462118A8E6007CC3A3 /* Preferences.xib */; };
5477D34E21233C62002BA27F /* FeedGroup+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 5477D34D21233C62002BA27F /* FeedGroup+Ext.m */; };
5478DF04225A7AE200D30C64 /* SettingsFeedsView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5478DF03225A7AE200D30C64 /* SettingsFeedsView.m */; };
5496B511214D6275003ED4ED /* UserPrefs.m in Sources */ = {isa = PBXBuildFile; fileRef = 5496B510214D6275003ED4ED /* UserPrefs.m */; };
54A07A7F220E04CF00082C51 /* NSFetchRequest+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54A07A7E220E04CF00082C51 /* NSFetchRequest+Ext.m */; };
54A07A82220E723D00082C51 /* MapUnreadTotal.m in Sources */ = {isa = PBXBuildFile; fileRef = 54A07A81220E723D00082C51 /* MapUnreadTotal.m */; };
@@ -30,11 +31,14 @@
54ACC28C21061B3C0020715F /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC28B21061B3C0020715F /* main.m */; };
54ACC29521061E270020715F /* FeedDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC29421061E270020715F /* FeedDownload.m */; };
54ACC29821061FBA0020715F /* Preferences.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC29721061FBA0020715F /* Preferences.m */; };
54B51704226DC339006C1B29 /* ModalFeedEditView.m in Sources */ = {isa = PBXBuildFile; fileRef = 54B51703226DC339006C1B29 /* ModalFeedEditView.m */; };
54B517072270E990006C1B29 /* NSView+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54B517062270E92A006C1B29 /* NSView+Ext.m */; };
54B749DA2204A85C0022CC6D /* BarStatusItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 54B749D92204A85C0022CC6D /* BarStatusItem.m */; };
54B749E0220636200022CC6D /* FeedArticle+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54B749DF220635CD0022CC6D /* FeedArticle+Ext.m */; };
54BB048921FD2AB500C303A5 /* NSDate+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54BB048821FD2AB500C303A5 /* NSDate+Ext.m */; };
54D857CE227C5785001BA1C8 /* RefreshStatisticsView.m in Sources */ = {isa = PBXBuildFile; fileRef = 54D857CD227C5785001BA1C8 /* RefreshStatisticsView.m */; };
54E88320211B509D00064188 /* ModalFeedEdit.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E8831E211B509D00064188 /* ModalFeedEdit.m */; };
54E88321211B509D00064188 /* ModalFeedEdit.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54E8831F211B509D00064188 /* ModalFeedEdit.xib */; };
54E9CF32225914300023696F /* SettingsAbout.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9CF31225914300023696F /* SettingsAbout.m */; };
54F39C2E210BE1F700AEE730 /* DBv1.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC28221061B3B0020715F /* DBv1.xcdatamodeld */; };
54F6025D21C1D4170006D338 /* OpmlExport.m in Sources */ = {isa = PBXBuildFile; fileRef = 54F6025C21C1D4170006D338 /* OpmlExport.m */; };
54FE73D021220DEC003EAC65 /* StoreCoordinator.m in Sources */ = {isa = PBXBuildFile; fileRef = 54FE73CF21220DEC003EAC65 /* StoreCoordinator.m */; };
@@ -74,25 +78,28 @@
54195884218E1BDB00581B79 /* NSMenu+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSMenu+Ext.h"; sourceTree = "<group>"; };
54195885218E1BDB00581B79 /* NSMenu+Ext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSMenu+Ext.m"; sourceTree = "<group>"; };
541958872190FF1200581B79 /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = "<group>"; };
541C67C12255470B004D2CE6 /* SettingsAppearance.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SettingsAppearance.h; sourceTree = "<group>"; };
541C67C22255470B004D2CE6 /* SettingsAppearance.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SettingsAppearance.m; sourceTree = "<group>"; };
54209E922117325100F3B5EF /* DrawImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DrawImage.h; sourceTree = "<group>"; };
54209E932117325100F3B5EF /* DrawImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DrawImage.m; sourceTree = "<group>"; };
544936F921F1E66100DEE9AA /* Statistics.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Statistics.h; sourceTree = "<group>"; };
544936FA21F1E66100DEE9AA /* Statistics.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Statistics.m; sourceTree = "<group>"; };
544B01182114B41200386E5C /* ModalSheet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ModalSheet.h; sourceTree = "<group>"; };
544B01192114B41200386E5C /* ModalSheet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ModalSheet.m; sourceTree = "<group>"; };
544B011B2114EE9100386E5C /* AppHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppHook.h; sourceTree = "<group>"; };
544B011C2114EE9100386E5C /* AppHook.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppHook.m; sourceTree = "<group>"; };
544DCCB8212A2B4D002DBC46 /* RSXML.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RSXML.framework; path = Carthage/Build/Mac/RSXML.framework; sourceTree = "<group>"; };
544DCCBD212A2B6F002DBC46 /* RSXML.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = RSXML.framework.dSYM; path = Carthage/Build/Mac/RSXML.framework.dSYM; 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>"; };
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>"; };
546FC43E21188C78007CC3A3 /* SettingsFeeds.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SettingsFeeds.xib; sourceTree = "<group>"; };
546FC44021189975007CC3A3 /* SettingsGeneral.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsGeneral.h; sourceTree = "<group>"; };
546FC44121189975007CC3A3 /* SettingsGeneral.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsGeneral.m; sourceTree = "<group>"; };
546FC44221189975007CC3A3 /* SettingsGeneral.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SettingsGeneral.xib; sourceTree = "<group>"; };
546FC4462118A8E6007CC3A3 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Preferences.xib; sourceTree = "<group>"; };
5477D34C21233C62002BA27F /* FeedGroup+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FeedGroup+Ext.h"; sourceTree = "<group>"; };
5477D34D21233C62002BA27F /* FeedGroup+Ext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FeedGroup+Ext.m"; sourceTree = "<group>"; };
5478DF02225A7AE200D30C64 /* SettingsFeedsView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SettingsFeedsView.h; sourceTree = "<group>"; };
5478DF03225A7AE200D30C64 /* SettingsFeedsView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SettingsFeedsView.m; sourceTree = "<group>"; };
54892F1D2235285700271CBA /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
5496B50F214D6275003ED4ED /* UserPrefs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UserPrefs.h; sourceTree = "<group>"; };
5496B510214D6275003ED4ED /* UserPrefs.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UserPrefs.m; sourceTree = "<group>"; };
@@ -109,15 +116,24 @@
54ACC29421061E270020715F /* FeedDownload.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FeedDownload.m; sourceTree = "<group>"; };
54ACC29621061FBA0020715F /* Preferences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Preferences.h; sourceTree = "<group>"; };
54ACC29721061FBA0020715F /* Preferences.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Preferences.m; sourceTree = "<group>"; };
54B51702226DC339006C1B29 /* ModalFeedEditView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ModalFeedEditView.h; sourceTree = "<group>"; };
54B51703226DC339006C1B29 /* ModalFeedEditView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ModalFeedEditView.m; sourceTree = "<group>"; };
54B517052270E8C6006C1B29 /* NSView+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSView+Ext.h"; sourceTree = "<group>"; };
54B517062270E92A006C1B29 /* NSView+Ext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSView+Ext.m"; sourceTree = "<group>"; };
54B749D82204A85C0022CC6D /* BarStatusItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BarStatusItem.h; sourceTree = "<group>"; };
54B749D92204A85C0022CC6D /* BarStatusItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BarStatusItem.m; sourceTree = "<group>"; };
54B749DE220635BE0022CC6D /* FeedArticle+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FeedArticle+Ext.h"; sourceTree = "<group>"; };
54B749DF220635CD0022CC6D /* FeedArticle+Ext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FeedArticle+Ext.m"; sourceTree = "<group>"; };
54BB048721FD2AB500C303A5 /* NSDate+Ext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDate+Ext.h"; sourceTree = "<group>"; };
54BB048821FD2AB500C303A5 /* NSDate+Ext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDate+Ext.m"; sourceTree = "<group>"; };
54D857CC227C5785001BA1C8 /* RefreshStatisticsView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RefreshStatisticsView.h; sourceTree = "<group>"; };
54D857CD227C5785001BA1C8 /* RefreshStatisticsView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RefreshStatisticsView.m; sourceTree = "<group>"; };
54D857D022802309001BA1C8 /* SettingsGeneralView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SettingsGeneralView.h; sourceTree = "<group>"; };
54D857D122802309001BA1C8 /* SettingsGeneralView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SettingsGeneralView.m; sourceTree = "<group>"; };
54E8831D211B509D00064188 /* ModalFeedEdit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ModalFeedEdit.h; sourceTree = "<group>"; };
54E8831E211B509D00064188 /* ModalFeedEdit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ModalFeedEdit.m; sourceTree = "<group>"; };
54E8831F211B509D00064188 /* ModalFeedEdit.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ModalFeedEdit.xib; sourceTree = "<group>"; };
54E9CF30225914300023696F /* SettingsAbout.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SettingsAbout.h; sourceTree = "<group>"; };
54E9CF31225914300023696F /* SettingsAbout.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SettingsAbout.m; sourceTree = "<group>"; };
54F6025B21C1D4170006D338 /* OpmlExport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OpmlExport.h; sourceTree = "<group>"; };
54F6025C21C1D4170006D338 /* OpmlExport.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OpmlExport.m; sourceTree = "<group>"; };
54FE73CE21220DEC003EAC65 /* StoreCoordinator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StoreCoordinator.h; sourceTree = "<group>"; };
@@ -160,10 +176,10 @@
54209E932117325100F3B5EF /* DrawImage.m */,
54ACC29321061E270020715F /* FeedDownload.h */,
54ACC29421061E270020715F /* FeedDownload.m */,
544936F921F1E66100DEE9AA /* Statistics.h */,
544936FA21F1E66100DEE9AA /* Statistics.m */,
54BB048721FD2AB500C303A5 /* NSDate+Ext.h */,
54BB048821FD2AB500C303A5 /* NSDate+Ext.m */,
54B517052270E8C6006C1B29 /* NSView+Ext.h */,
54B517062270E92A006C1B29 /* NSView+Ext.m */,
);
path = Helper;
sourceTree = "<group>";
@@ -177,28 +193,16 @@
name = Frameworks;
sourceTree = "<group>";
};
546FC44521189ADC007CC3A3 /* General Tab */ = {
isa = PBXGroup;
children = (
5496B50F214D6275003ED4ED /* UserPrefs.h */,
5496B510214D6275003ED4ED /* UserPrefs.m */,
546FC44021189975007CC3A3 /* SettingsGeneral.h */,
546FC44121189975007CC3A3 /* SettingsGeneral.m */,
546FC44221189975007CC3A3 /* SettingsGeneral.xib */,
);
path = "General Tab";
sourceTree = "<group>";
};
546FC44D2118B357007CC3A3 /* Preferences */ = {
isa = PBXGroup;
children = (
54E9CF2F225913850023696F /* Helper */,
54ACC29621061FBA0020715F /* Preferences.h */,
54ACC29721061FBA0020715F /* Preferences.m */,
546FC4462118A8E6007CC3A3 /* Preferences.xib */,
544B01182114B41200386E5C /* ModalSheet.h */,
544B01192114B41200386E5C /* ModalSheet.m */,
546FC44521189ADC007CC3A3 /* General Tab */,
54D857CF228022AB001BA1C8 /* General Tab */,
54E88323211B542E00064188 /* Feeds Tab */,
54D857D3228035D4001BA1C8 /* Appearance Tab */,
54D857D72280C367001BA1C8 /* About Tab */,
);
path = Preferences;
sourceTree = "<group>";
@@ -259,21 +263,69 @@
path = baRSS;
sourceTree = "<group>";
};
54D857CF228022AB001BA1C8 /* General Tab */ = {
isa = PBXGroup;
children = (
546FC44021189975007CC3A3 /* SettingsGeneral.h */,
546FC44121189975007CC3A3 /* SettingsGeneral.m */,
54D857D022802309001BA1C8 /* SettingsGeneralView.h */,
54D857D122802309001BA1C8 /* SettingsGeneralView.m */,
);
path = "General Tab";
sourceTree = "<group>";
};
54D857D3228035D4001BA1C8 /* Appearance Tab */ = {
isa = PBXGroup;
children = (
541C67C12255470B004D2CE6 /* SettingsAppearance.h */,
541C67C22255470B004D2CE6 /* SettingsAppearance.m */,
546A6A2B22C584AF0034E806 /* SettingsAppearanceView.h */,
546A6A2A22C584AF0034E806 /* SettingsAppearanceView.m */,
);
path = "Appearance Tab";
sourceTree = "<group>";
};
54D857D72280C367001BA1C8 /* About Tab */ = {
isa = PBXGroup;
children = (
54E9CF30225914300023696F /* SettingsAbout.h */,
54E9CF31225914300023696F /* SettingsAbout.m */,
546A6A2E22C585580034E806 /* SettingsAboutView.h */,
546A6A2D22C585580034E806 /* SettingsAboutView.m */,
);
path = "About Tab";
sourceTree = "<group>";
};
54E88323211B542E00064188 /* Feeds Tab */ = {
isa = PBXGroup;
children = (
546FC43B21188AD5007CC3A3 /* SettingsFeeds.h */,
546FC43C21188AD5007CC3A3 /* SettingsFeeds.m */,
546FC43E21188C78007CC3A3 /* SettingsFeeds.xib */,
5478DF02225A7AE200D30C64 /* SettingsFeedsView.h */,
5478DF03225A7AE200D30C64 /* SettingsFeedsView.m */,
54E8831D211B509D00064188 /* ModalFeedEdit.h */,
54E8831E211B509D00064188 /* ModalFeedEdit.m */,
54E8831F211B509D00064188 /* ModalFeedEdit.xib */,
54B51702226DC339006C1B29 /* ModalFeedEditView.h */,
54B51703226DC339006C1B29 /* ModalFeedEditView.m */,
54D857CC227C5785001BA1C8 /* RefreshStatisticsView.h */,
54D857CD227C5785001BA1C8 /* RefreshStatisticsView.m */,
54F6025B21C1D4170006D338 /* OpmlExport.h */,
54F6025C21C1D4170006D338 /* OpmlExport.m */,
);
path = "Feeds Tab";
sourceTree = "<group>";
};
54E9CF2F225913850023696F /* Helper */ = {
isa = PBXGroup;
children = (
5496B50F214D6275003ED4ED /* UserPrefs.h */,
5496B510214D6275003ED4ED /* UserPrefs.m */,
544B01182114B41200386E5C /* ModalSheet.h */,
544B01192114B41200386E5C /* ModalSheet.m */,
);
path = Helper;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -343,11 +395,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
546FC4472118A8E6007CC3A3 /* Preferences.xib in Resources */,
54E88321211B509D00064188 /* ModalFeedEdit.xib in Resources */,
54ACC28621061B3C0020715F /* Assets.xcassets in Resources */,
546FC44421189975007CC3A3 /* SettingsGeneral.xib in Resources */,
546FC43F21188C78007CC3A3 /* SettingsFeeds.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -378,14 +426,18 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
54B51704226DC339006C1B29 /* ModalFeedEditView.m in Sources */,
546A6A2C22C584AF0034E806 /* SettingsAppearanceView.m in Sources */,
54E9CF32225914300023696F /* SettingsAbout.m in Sources */,
54B749E0220636200022CC6D /* FeedArticle+Ext.m in Sources */,
54F39C2E210BE1F700AEE730 /* DBv1.xcdatamodeld in Sources */,
544B011D2114EE9100386E5C /* AppHook.m in Sources */,
546FC44321189975007CC3A3 /* SettingsGeneral.m in Sources */,
546A6A2922C583390034E806 /* SettingsGeneralView.m in Sources */,
54ACC29521061E270020715F /* FeedDownload.m in Sources */,
54BB048921FD2AB500C303A5 /* NSDate+Ext.m in Sources */,
5477D34E21233C62002BA27F /* FeedGroup+Ext.m in Sources */,
544936FB21F1E66100DEE9AA /* Statistics.m in Sources */,
5478DF04225A7AE200D30C64 /* SettingsFeedsView.m in Sources */,
540F704521B6C16C0022E69D /* FeedMeta+Ext.m in Sources */,
54ACC28C21061B3C0020715F /* main.m in Sources */,
54FE73D3212316CD003EAC65 /* BarMenu.m in Sources */,
@@ -395,6 +447,10 @@
54195886218E1BDB00581B79 /* NSMenu+Ext.m in Sources */,
54F6025D21C1D4170006D338 /* OpmlExport.m in Sources */,
5496B511214D6275003ED4ED /* UserPrefs.m in Sources */,
546A6A2F22C585580034E806 /* SettingsAboutView.m in Sources */,
54B517072270E990006C1B29 /* NSView+Ext.m in Sources */,
54D857CE227C5785001BA1C8 /* RefreshStatisticsView.m in Sources */,
541C67C32255470B004D2CE6 /* SettingsAppearance.m in Sources */,
54B749DA2204A85C0022CC6D /* BarStatusItem.m in Sources */,
546FC43D21188AD5007CC3A3 /* SettingsFeeds.m in Sources */,
54E88320211B509D00064188 /* ModalFeedEdit.m in Sources */,

View File

@@ -26,7 +26,7 @@
#import "Preferences.h"
@interface AppHook()
@property (strong) Preferences *prefWindow;
@property (strong) NSWindowController *prefWindow;
@end
@implementation AppHook
@@ -76,9 +76,7 @@
/// Called whenever the user activates the preferences (either through menu click or hotkey).
- (void)openPreferences {
if (!self.prefWindow) {
self.prefWindow = [[Preferences alloc] initWithWindowNibName:@"Preferences"];
self.prefWindow.window.title = [NSString stringWithFormat:@"%@ %@", NSProcessInfo.processInfo.processName,
NSLocalizedString(@"Preferences", nil)];
self.prefWindow = [[NSWindowController alloc] initWithWindow:[Preferences window]];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(preferencesClosed:) name:NSWindowWillCloseNotification object:self.prefWindow.window];
}
[NSApp activateIgnoringOtherApps:YES];
@@ -196,12 +194,13 @@ static NSEventModifierFlags fnKeyFlags = NSEventModifierFlagShift | NSEventModif
if ([self sendAction:@selector(redo:) to:nil from:self])
return;
}
} else {
if (key == NSEnterCharacter || key == NSCarriageReturnCharacter) {
if ([self sendAction:@selector(enterPressed:) to:nil from:self])
return;
}
}
// else {
// if (key == NSEnterCharacter || key == NSCarriageReturnCharacter) {
// if ([self sendAction:@selector(enterPressed:) to:nil from:self])
// return;
// }
// }
#pragma clang diagnostic pop
}
[super sendEvent:event];

View File

@@ -189,8 +189,8 @@
BOOL linkIsNil = (searchLink == nil);
BOOL guidIsNil = (searchGuid == nil);
for (FeedArticle *art in localSet) {
if ((linkIsNil && art.link == nil) || [art.link isEqualToString:searchLink]) {
if ((guidIsNil && art.guid == nil) || [art.guid isEqualToString:searchGuid])
if ((linkIsNil && art.link == nil) || (!linkIsNil && [art.link isEqualToString:searchLink])) {
if ((guidIsNil && art.guid == nil) || (!guidIsNil && [art.guid isEqualToString:searchGuid]))
return art;
}
}
@@ -206,8 +206,8 @@
BOOL linkIsNil = (searchLink == nil);
BOOL guidIsNil = (searchGuid == nil);
for (RSParsedArticle *art in remoteSet) {
if ((linkIsNil && art.link == nil) || [art.link isEqualToString:searchLink]) {
if ((guidIsNil && art.guid == nil) || [art.guid isEqualToString:searchGuid])
if ((linkIsNil && art.link == nil) || (!linkIsNil && [art.link isEqualToString:searchLink])) {
if ((guidIsNil && art.guid == nil) || (!guidIsNil && [art.guid isEqualToString:searchGuid]))
return art;
}
}

View File

@@ -107,7 +107,7 @@
fr.propertiesToFetch = @[ @"indexPath" ];
[fr addFunctionExpression:@"sum:" onKeyPath:@"articles.unread" name:@"unread" type:NSInteger32AttributeType];
[fr addFunctionExpression:@"count:" onKeyPath:@"articles.unread" name:@"total" type:NSInteger32AttributeType];
return [fr fetchAllRows: [self getMainContext]];
return (NSArray<NSDictionary*>*)[fr fetchAllRows: [self getMainContext]];
}

View File

@@ -266,8 +266,11 @@ static BOOL _nextUpdateIsForced = NO;
dispatch_sync(dispatch_get_main_queue(), ^{ // sync! (thread is already in background)
chosenURL = askUser(parsedMeta);
});
if (!chosenURL || chosenURL.length == 0)
if (!chosenURL || chosenURL.length == 0) {
// User canceled operation, show appropriate error message
*err = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Operation canceled.", nil)}];
return NO;
}
[self parseFeedRequest:[self newRequestURL:chosenURL] xmlBlock:nil feedBlock:block];
return YES;
} feedBlock:block];

View File

@@ -33,6 +33,12 @@ typedef NS_ENUM(int32_t, TimeUnitType) {
};
@interface NSDate (Ext)
+ (NSString*)dayStringISO8601;
+ (NSString*)dayStringLocalized;
@end
@interface NSDate (Interval)
+ (nonnull NSString*)stringForInterval:(Interval)intv rounded:(BOOL)flag;
+ (TimeUnitType)unitForInterval:(Interval)intv rounded:(BOOL)flag;
@end
@@ -43,3 +49,8 @@ typedef NS_ENUM(int32_t, TimeUnitType) {
+ (void)setInterval:(Interval)intv forPopup:(NSPopUpButton*)popup andField:(NSTextField*)field animate:(BOOL)flag;
+ (void)populateUnitsMenu:(NSPopUpButton*)popup selected:(TimeUnitType)unit;
@end
@interface NSDate (Statistics)
+ (NSDictionary*)refreshIntervalStatistics:(NSArray<NSDate*> *)list;
@end

View File

@@ -38,6 +38,23 @@ static const TimeUnitType _values[] = {
@implementation NSDate (Ext)
/// @return Day as string in iso format: @c YYYY-MM-DD'T'hh:mm:ss'Z'
+ (NSString*)dayStringISO8601 {
return [[[NSISO8601DateFormatter alloc] init] stringFromDate:[NSDate date]];
// NSDateComponents *now = [[NSCalendar currentCalendar] components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear fromDate:[NSDate date]];
// return [NSString stringWithFormat:@"%04ld-%02ld-%02ld", now.year, now.month, now.day];
}
/// @return Day as string in localized short format, e.g., @c DD.MM.YY
+ (NSString*)dayStringLocalized {
return [NSDateFormatter localizedStringFromDate:[NSDate date] dateStyle:NSDateFormatterShortStyle timeStyle:NSDateFormatterNoStyle];
}
@end
@implementation NSDate (Interval)
/// If @c flag @c = @c YES, print @c 1.1f float string with single char unit: e.g., 3.3m, 1.7h.
+ (nonnull NSString*)stringForInterval:(Interval)intv rounded:(BOOL)flag {
if (flag) {
@@ -139,3 +156,48 @@ static const TimeUnitType _values[] = {
}
@end
@implementation NSDate (Statistics)
/**
@return @c nil if list contains less than 2 entries. Otherwise: @{min, max, avg, median, earliest, latest}
*/
+ (NSDictionary*)refreshIntervalStatistics:(NSArray<NSDate*> *)list {
if (!list || list.count == 0)
return nil;
NSDate *earliest = [NSDate distantFuture];
NSDate *latest = [NSDate distantPast];
NSDate *prev = nil;
NSMutableArray<NSNumber*> *differences = [NSMutableArray array];
for (NSDate *d in list) {
if (![d isKindOfClass:[NSDate class]]) // because valueForKeyPath: can return NSNull
continue;
earliest = [d earlierDate:earliest];
latest = [d laterDate:latest];
if (prev) {
int dif = abs((int)[d timeIntervalSinceDate:prev]);
[differences addObject:[NSNumber numberWithInt:dif]];
}
prev = d;
}
if (differences.count == 0)
return nil;
[differences sortUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"integerValue" ascending:YES]]];
NSUInteger i = (differences.count/2);
NSNumber *median = differences[i];
if ((differences.count % 2) == 0) { // even feed count, use median of two values
median = [NSNumber numberWithInteger:(median.integerValue + differences[i-1].integerValue) / 2];
}
return @{@"min" : differences.firstObject,
@"max" : differences.lastObject,
@"avg" : [differences valueForKeyPath:@"@avg.self"],
@"median" : median,
@"earliest" : earliest,
@"latest" : latest };
}
@end

101
baRSS/Helper/NSView+Ext.h Normal file
View File

@@ -0,0 +1,101 @@
//
// The MIT License (MIT)
// Copyright (c) 2019 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import <Cocoa/Cocoa.h>
/***/ static const CGFloat PAD_WIN = 20; // window padding
/***/ static const CGFloat PAD_L = 16;
/***/ static const CGFloat PAD_M = 8;
/***/ static const CGFloat PAD_S = 4;
/***/ static const CGFloat PAD_XS = 2;
/***/ static const CGFloat HEIGHT_LABEL = 17;
/***/ static const CGFloat HEIGHT_LABEL_SMALL = 14;
/***/ static const CGFloat HEIGHT_INPUTFIELD = 21;
/***/ static const CGFloat HEIGHT_BUTTON = 21;
/***/ static const CGFloat HEIGHT_INLINEBUTTON = 16;
/***/ static const CGFloat HEIGHT_POPUP = 21;
/***/ static const CGFloat HEIGHT_SPINNER = 16;
/***/ static const CGFloat HEIGHT_CHECKBOX = 14;
/// Static variable to calculate origin center coordinate in its @c superview. The value of this var isn't used.
static const CGFloat CENTER = -0.015625;
/// Calculate @c origin.y going down from the top border of its @c superview
NS_INLINE CGFloat YFromTop(NSView *view) { return NSHeight(view.superview.frame) - NSMinY(view.frame) - view.alignmentRectInsets.bottom; }
/*
Allmost all methods return @c self to allow method chaining
*/
@interface NSView (Ext)
// UI: TextFields
+ (NSTextField*)label:(NSString*)text;
+ (NSTextField*)inputField:(NSString*)placeholder width:(CGFloat)w;
+ (NSView*)labelColumn:(NSArray<NSString*>*)labels rowHeight:(CGFloat)h padding:(CGFloat)pad;
// UI: Buttons
+ (NSButton*)button:(NSString*)text;
+ (NSButton*)buttonImageSquare:(nonnull NSImageName)name;
+ (NSButton*)buttonIcon:(NSImage*)img size:(CGFloat)size;
+ (NSButton*)inlineButton:(NSString*)text;
+ (NSPopUpButton*)popupButton:(CGFloat)w;
// UI: Others
+ (NSImageView*)imageView:(NSImageName)name size:(CGFloat)size;
+ (NSButton*)checkbox:(BOOL)flag;
+ (NSProgressIndicator*)activitySpinner;
+ (NSView*)radioGroup:(NSArray<NSString*>*)entries target:(id)target action:(nonnull SEL)action;
+ (NSView*)radioGroup:(NSArray<NSString*>*)entries;
// UI: Enclosing Container
- (NSScrollView*)wrapContent:(NSView*)content inScrollView:(NSRect)rect;
+ (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;
- (instancetype)placeIn:(NSView*)parent x:(CGFloat)x yTop:(CGFloat)y;
- (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)sizableWidthAndHeight;
- (instancetype)sizeToRight:(CGFloat)rightPadding;
- (instancetype)sizeWidthToFit;
- (instancetype)tooltip:(NSString*)tt;
// Debugging
- (instancetype)colorLayer:(NSColor*)color;
+ (NSView*)redCube:(CGFloat)size;
@end
@interface NSControl (Ext)
- (instancetype)action:(SEL)selector target:(id)target;
- (instancetype)large;
- (instancetype)small;
- (instancetype)tiny;
- (instancetype)bold;
- (instancetype)textRight;
- (instancetype)textCenter;
@end
@interface NSTextField (Ext)
- (instancetype)gray;
- (instancetype)selectable;
@end

353
baRSS/Helper/NSView+Ext.m Normal file
View File

@@ -0,0 +1,353 @@
//
// The MIT License (MIT)
// Copyright (c) 2019 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "NSView+Ext.h"
@implementation NSView (Ext)
#pragma mark - UI: TextFields -
/// Create label with non-editable text. Ensures uniform fontsize and text color. @c 17px height.
+ (NSTextField*)label:(NSString*)text {
NSTextField *label = [NSTextField labelWithString:text];
[label setFrameSize: NSMakeSize(0, HEIGHT_LABEL)];
label.font = [NSFont systemFontOfSize: NSFont.systemFontSize];
label.textColor = [NSColor controlTextColor];
label.lineBreakMode = NSLineBreakByTruncatingTail;
// label.backgroundColor = [NSColor yellowColor];
// label.drawsBackground = YES;
return [label sizeWidthToFit];
}
/// Create input text field with placeholder text. @c 21px height.
+ (NSTextField*)inputField:(NSString*)placeholder width:(CGFloat)w {
NSTextField *input = [NSTextField textFieldWithString:@""];
[input setFrameSize: NSMakeSize(w, HEIGHT_INPUTFIELD)];
input.alignment = NSTextAlignmentJustified;
input.placeholderString = placeholder;
input.font = [NSFont systemFontOfSize: NSFont.systemFontSize];
input.textColor = [NSColor controlTextColor];
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;
CGFloat off = (h - HEIGHT_LABEL) / 2;
NSView *parent = [[NSView alloc] init];
for (NSUInteger i = 0; i < labels.count; i++) {
NSTextField *lbl = [[NSView label:labels[i]] placeIn:parent xRight:0 yTop:y + off];
if (w < NSWidth(lbl.frame))
w = NSWidth(lbl.frame);
y += h + pad;
}
[parent setFrameSize: NSMakeSize(w, y - pad)];
return parent;
}
#pragma mark - UI: Buttons -
/// Create button. @c 21px height.
+ (NSButton*)button:(NSString*)text {
NSButton *btn = [[NSButton alloc] initWithFrame: NSMakeRect(0, 0, 0, HEIGHT_BUTTON)];
btn.font = [NSFont systemFontOfSize:NSFont.systemFontSize];
btn.bezelStyle = NSBezelStyleRounded;
btn.title = text;
return [btn sizeWidthToFit];
}
/// Create @c NSBezelStyleSmallSquare image button. @c 25x21px
+ (NSButton*)buttonImageSquare:(nonnull NSImageName)name {
NSButton *btn = [[NSButton alloc] initWithFrame: NSMakeRect(0, 0, 25, HEIGHT_BUTTON)];
btn.bezelStyle = NSBezelStyleSmallSquare;
btn.image = [NSImage imageNamed:name];
if (!btn.image) btn.title = name; // fallback to text
return btn;
}
/// Create pure image button with no border.
+ (NSButton*)buttonIcon:(NSImage*)img size:(CGFloat)size {
NSButton *btn = [[NSButton alloc] initWithFrame: NSMakeRect(0, 0, size, size)];
btn.bezelStyle = NSBezelStyleRounded;
btn.bordered = NO;
btn.image = img;
return btn;
}
/// Create gray inline button with rounded corners. @c 16px height.
+ (NSButton*)inlineButton:(NSString*)text {
NSButton *btn = [[NSButton alloc] initWithFrame: NSMakeRect(0, 0, 0, HEIGHT_INLINEBUTTON)];
btn.font = [NSFont monospacedDigitSystemFontOfSize: NSFont.labelFontSize weight: NSFontWeightBold];
btn.bezelStyle = NSBezelStyleInline;
btn.controlSize = NSControlSizeSmall;
btn.title = text;
return [btn sizeWidthToFit];
}
/// Create empty drop down button. @c 21px height.
+ (NSPopUpButton*)popupButton:(CGFloat)w {
return [[NSPopUpButton alloc] initWithFrame: NSMakeRect(0, 0, w, HEIGHT_POPUP) pullsDown:NO];
}
#pragma mark - UI: Others -
/// Create @c ImageView with square @c size
+ (NSImageView*)imageView:(NSImageName)name size:(CGFloat)size {
NSImageView *imgView = [[NSImageView alloc] initWithFrame: NSMakeRect(0, 0, size, size)];
if (name) imgView.image = [NSImage imageNamed:name];
return imgView;
}
/// Create checkbox. @c 14px height.
+ (NSButton*)checkbox:(BOOL)flag {
NSButton *check = [NSButton checkboxWithTitle:@"" target:nil action:nil];
check.title = @""; // needed, otherwise will print "Button"
check.frame = NSMakeRect(0, 0, HEIGHT_CHECKBOX, HEIGHT_CHECKBOX);
check.state = (flag? NSControlStateValueOn : NSControlStateValueOff);
return check;
}
/// Create progress spinner. @c 16px size.
+ (NSProgressIndicator*)activitySpinner {
NSProgressIndicator *spin = [[NSProgressIndicator alloc] initWithFrame: NSMakeRect(0, 0, HEIGHT_SPINNER, HEIGHT_SPINNER)];
spin.indeterminate = YES;
spin.displayedWhenStopped = NO;
spin.style = NSProgressIndicatorSpinningStyle;
spin.controlSize = NSControlSizeSmall;
return spin;
}
/// Create grouping view with vertically, left-aligned radio buttons. Action is identical for all buttons (grouping).
+ (NSView*)radioGroup:(NSArray<NSString*>*)entries target:(id)target action:(nonnull SEL)action {
if (entries.count == 0)
return nil;
CGFloat w = 0, h = 0;
NSView *parent = [[NSView alloc] init];
for (NSUInteger i = entries.count; i > 0; i--) {
NSButton *btn = [NSButton radioButtonWithTitle:entries[i-1] target:target action:action];
btn.tag = (NSInteger)i-1;
if (btn.tag == 0)
btn.state = NSControlStateValueOn;
if (w < NSWidth(btn.frame)) // find max width (before alignmentRect:)
w = NSWidth(btn.frame);
[btn placeIn:parent x:0 y:h];
h += NSHeight([btn alignmentRectForFrame:btn.frame]) + PAD_XS;
}
[parent setFrameSize: NSMakeSize(w, h - PAD_XS)];
return parent;
}
/// Same as @c radioGroup:target:action: but using dummy action to ignore radio button click events.
+ (NSView*)radioGroup:(NSArray<NSString*>*)entries {
return [self radioGroup:entries target:self action:@selector(donothing)];
}
/// Solely used to group radio buttons
+ (void)donothing {}
#pragma mark - UI: Enclosing Container -
/// 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];
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;
return scroll;
}
/// Create view with @c NSTextField label in front of the view.
+ (NSView*)wrapView:(NSView*)other withLabel:(NSString*)str padding:(CGFloat)pad {
NSView *parent = [[NSView alloc] initWithFrame: NSZeroRect];
NSTextField *label = [NSView label:str];
[label placeIn:parent x:pad yTop:pad];
[other placeIn:parent x:pad + NSWidth(label.frame) yTop:pad];
[parent setFrameSize: NSMakeSize(NSMaxX(other.frame), NSHeight(other.frame) + 2 * pad)];
return parent;
}
#pragma mark - Insert UI elements in parent view -
/**
Set frame origin and insert @c self in @c parent view with @c frameForAlignmentRect:.
You may use @c CENTER to automatically calculate midpoint in parent view.
The @c autoresizingMask will be set accordingly.
*/
- (instancetype)placeIn:(NSView*)parent x:(CGFloat)x y:(CGFloat)y {
SetCenterableOrigin(self, parent, x, y);
if (x == CENTER) self.autoresizingMask |= NSViewMinXMargin | NSViewMaxXMargin;
if (y == CENTER) self.autoresizingMask |= NSViewMinYMargin | NSViewMaxYMargin;
self.frame = [self frameForAlignmentRect:self.frame];
[parent addSubview:self];
return self;
}
/// Same as @c placeIn:x:y: but measure position from top instead of bottom. Also sets @c autoresizingMask.
- (instancetype)placeIn:(NSView*)parent x:(CGFloat)x yTop:(CGFloat)y {
return [[self placeIn:parent x:x y:NSHeight(parent.frame) - NSHeight(self.frame) - y] alignTop];
}
/// Same as @c placeIn:x:y: but measure position from right instead of left. Also sets @c autoresizingMask.
- (instancetype)placeIn:(NSView*)parent xRight:(CGFloat)x y:(CGFloat)y {
return [[self placeIn:parent x:NSWidth(parent.frame) - NSWidth(self.frame) - x y:y] alignRight];
}
/// Set origin by measuring from top right (@c CENTER is not allowed here). Also sets @c autoresizingMask.
- (instancetype)placeIn:(NSView*)parent xRight:(CGFloat)x yTop:(CGFloat)y {
[self setFrameOrigin: NSMakePoint(NSWidth(parent.frame) - NSWidth(self.frame) - x,
NSHeight(parent.frame) - NSHeight(self.frame) - y)];
self.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin;
self.frame = [self frameForAlignmentRect:self.frame];
[parent addSubview:self];
return self;
}
#pragma mark - Modify existing UI elements -
// Aligned Frame Origins
// pad - view.alignmentRectInsets.left;
// pad - view.alignmentRectInsets.bottom;
// NSWidth(view.superview.frame) - NSWidth(view.frame) - pad + view.alignmentRectInsets.right;
// NSHeight(view.superview.frame) - NSHeight(view.frame) - pad + view.alignmentRectInsets.top;
/// Modify @c .autoresizingMask; Clear @c NSViewMaxYMargin flag and set @c NSViewMinYMargin
- (instancetype)alignTop { self.autoresizingMask = (self.autoresizingMask & ~NSViewMaxYMargin) | NSViewMinYMargin; return self; }
/// Modify @c .autoresizingMask; Clear @c NSViewMaxXMargin flag and set @c NSViewMinXMargin
- (instancetype)alignRight { self.autoresizingMask = (self.autoresizingMask & ~NSViewMaxXMargin) | NSViewMinXMargin; return self; }
/// Modify @c .autoresizingMask; Add @c NSViewWidthSizable @c | @c NSViewHeightSizable flags
- (instancetype)sizableWidthAndHeight { self.autoresizingMask |= NSViewWidthSizable | NSViewHeightSizable; 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);
self.autoresizingMask |= NSViewWidthSizable;
return self;
}
/// Set @c width to @c fittingSize.width but keep original height.
- (instancetype)sizeWidthToFit {
SetFrameWidth(self, self.fittingSize.width);
return self;
}
/// 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;
return self;
}
/// Helper method to get y origin point (from top) while respecting @c alignmentRectInsets and view sizes
NS_INLINE void SetCenterableOrigin(NSView *view, NSView *parent, CGFloat x, CGFloat y) {
if (x == CENTER) x = (NSWidth(parent.frame) - NSWidth(view.frame)) / 2;
if (y == CENTER) y = (NSHeight(parent.frame) - NSHeight(view.frame)) / 2;
[view setFrameOrigin: NSMakePoint(x, y)];
}
/// Helper method to set frame width and keep same height
NS_INLINE void SetFrameWidth(NSView *view, CGFloat w) {
[view setFrameSize: NSMakeSize(w, NSHeight(view.frame))];
}
#pragma mark - Debugging -
/// Set background color on @c .layer
- (instancetype)colorLayer:(NSColor*)color {
self.layer = [CALayer layer];
self.layer.backgroundColor = color.CGColor;
return self;
}
+ (NSView*)redCube:(CGFloat)size {
return [[[NSView alloc] initWithFrame: NSMakeRect(0, 0, size, size)] colorLayer:NSColor.redColor];
}
@end
#pragma mark - NSControl specific -
@implementation NSControl (Ext)
/// Set @c target and @c action simultaneously
- (instancetype)action:(SEL)selector target:(id)target {
self.action = selector;
self.target = target;
return self;
}
/// Set system font with current @c pointSize @c + @c 2. A label will be @c 19px height.
- (instancetype)large { SetFontAndResize(self, [NSFont systemFontOfSize: self.font.pointSize + 2]); return self; }
/// Set system font with @c smallSystemFontSize and perform @c sizeToFit. A label will be @c 14px height.
- (instancetype)small { SetFontAndResize(self, [NSFont systemFontOfSize: NSFont.smallSystemFontSize]); return self; }
/// Set monospaced font with @c labelFontSize regular and perform @c sizeToFit. A label will be @c 13px height.
- (instancetype)tiny { SetFontAndResize(self, [NSFont monospacedDigitSystemFontOfSize: NSFont.labelFontSize weight: NSFontWeightRegular]); return self; }
/// Set system bold font with current @c pointSize
- (instancetype)bold { SetFontAndResize(self, [NSFont boldSystemFontOfSize: self.font.pointSize]); return self; }
/// Set @c .alignment to @c NSTextAlignmentRight
- (instancetype)textRight { self.alignment = NSTextAlignmentRight; return self; }
/// Set @c .alignment to @c NSTextAlignmentCenter
- (instancetype)textCenter { self.alignment = NSTextAlignmentCenter; return self; }
/// Helper method to set new font, subsequently run @c sizeToFit
NS_INLINE void SetFontAndResize(NSControl *control, NSFont *font) {
control.font = font; [control sizeToFit];
}
@end
@implementation NSTextField (Ext)
/// Set text color to @c systemGrayColor
- (instancetype)gray { self.textColor = [NSColor systemGrayColor]; return self; }
/// Set @c .selectable to @c YES
- (instancetype)selectable { self.selectable = YES; return self; }
@end

View File

@@ -1,188 +0,0 @@
//
// The MIT License (MIT)
// Copyright (c) 2019 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "Statistics.h"
#import "NSDate+Ext.h"
@implementation Statistics
#pragma mark - Generate Refresh Interval Statistics
/**
@return @c nil if list contains less than 2 entries. Otherwise: @{min, max, avg, median, earliest, latest}
*/
+ (NSDictionary*)refreshInterval:(NSArray<NSDate*> *)list {
if (!list || list.count == 0)
return nil;
NSDate *earliest = [NSDate distantFuture];
NSDate *latest = [NSDate distantPast];
NSDate *prev = nil;
NSMutableArray<NSNumber*> *differences = [NSMutableArray array];
for (NSDate *d in list) {
if (![d isKindOfClass:[NSDate class]]) // because valueForKeyPath: can return NSNull
continue;
earliest = [d earlierDate:earliest];
latest = [d laterDate:latest];
if (prev) {
int dif = abs((int)[d timeIntervalSinceDate:prev]);
[differences addObject:[NSNumber numberWithInt:dif]];
}
prev = d;
}
if (differences.count == 0)
return nil;
[differences sortUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"integerValue" ascending:YES]]];
NSUInteger i = (differences.count/2);
NSNumber *median = differences[i];
if ((differences.count % 2) == 0) { // even feed count, use median of two values
median = [NSNumber numberWithInteger:(median.integerValue + differences[i-1].integerValue) / 2];
}
return @{@"min" : differences.firstObject,
@"max" : differences.lastObject,
@"avg" : [differences valueForKeyPath:@"@avg.self"],
@"median" : median,
@"earliest" : earliest,
@"latest" : latest };
}
#pragma mark - Feed Statistics UI
/**
Generate UI with buttons for min, max, avg and median. Also show number of articles and latest article date.
@param info The dictionary generated with @c -refreshInterval:
@param count Article count.
@param callback If set, @c sender will be called with @c -refreshIntervalButtonClicked:.
If not disable button border and display as bold inline text.
@return Centered view without autoresizing.
*/
+ (NSView*)viewForRefreshInterval:(NSDictionary*)info articleCount:(NSUInteger)count callback:(nullable id<RefreshIntervalButtonDelegate>)callback {
NSString *lbl = [NSString stringWithFormat:NSLocalizedString(@"%lu articles.", nil), count];
if (!info || info.count == 0)
return [self grayLabel:lbl];
// Subview with 4 button (min, max, avg, median)
NSView *buttonsView = [[NSView alloc] init];
NSPoint origin = NSZeroPoint;
for (NSString *str in @[@"min", @"max", @"avg", @"median"]) {
NSString *title = [str stringByAppendingString:@":"];
NSView *v = [self viewWithLabel:title andInterval:info[str] callback:callback];
[v setFrameOrigin:origin];
[buttonsView addSubview:v];
origin.x += NSWidth(v.frame);
}
[buttonsView setFrameSize:NSMakeSize(origin.x, NSHeight(buttonsView.subviews.firstObject.frame))];
// Subview with article count and latest article date
NSDate *lastUpdate = [info valueForKey:@"latest"];
NSString *mod = [NSDateFormatter localizedStringFromDate:lastUpdate dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterShortStyle];
NSTextField *dateView = [self grayLabel:[lbl stringByAppendingFormat:@" (latest: %@)", mod]];
// Feed wasn't updated in a while ...
if ([lastUpdate timeIntervalSinceNow] < (-360 * 24 * 60 * 60)) {
NSMutableAttributedString *as = dateView.attributedStringValue.mutableCopy;
[as addAttribute:NSForegroundColorAttributeName value:[NSColor systemRedColor] range:NSMakeRange(lbl.length, as.length - lbl.length)];
[dateView setAttributedStringValue:as];
}
// Calculate offset and align both horizontally centered
CGFloat maxWidth = NSWidth(buttonsView.frame);
if (maxWidth < NSWidth(dateView.frame))
maxWidth = NSWidth(dateView.frame);
[buttonsView setFrameOrigin:NSMakePoint(0.5f*(maxWidth - NSWidth(buttonsView.frame)), 0)];
[dateView setFrameOrigin:NSMakePoint(0.5f*(maxWidth - NSWidth(dateView.frame)), NSHeight(buttonsView.frame))];
// Dump both into single parent view and make that view centered during resize
NSView *parent = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, maxWidth, NSMaxY(dateView.frame))];
parent.autoresizingMask = NSViewMinXMargin | NSViewMaxXMargin;// | NSViewMinYMargin | NSViewMaxYMargin;
parent.autoresizesSubviews = NO;
// parent.layer = [CALayer layer];
// parent.layer.backgroundColor = [NSColor systemYellowColor].CGColor;
[parent addSubview:dateView];
[parent addSubview:buttonsView];
return parent;
}
/**
Create view with duration button, e.g., '3.4h' and label infornt of it.
*/
+ (NSView*)viewWithLabel:(NSString*)title andInterval:(NSNumber*)value callback:(nullable id<RefreshIntervalButtonDelegate>)callback {
static const int buttonPadding = 5;
NSButton *button = [self grayInlineButton:value];
if (callback) {
button.target = callback;
button.action = @selector(refreshIntervalButtonClicked:);
} else {
button.bordered = NO;
button.enabled = NO;
}
NSTextField *label;
if (title && title.length > 0) {
label = [self grayLabel:title];
[label setFrameOrigin:NSMakePoint(0, button.alignmentRectInsets.bottom + 0.5f*(NSHeight(button.frame) - NSHeight(label.frame)))];
}
[button setFrameOrigin:NSMakePoint(NSWidth(label.frame), 0)];
CGFloat maxHeight = NSHeight(button.frame);
if (maxHeight < NSHeight(label.frame))
maxHeight = NSHeight(label.frame);
NSView *parent = [[NSView alloc] initWithFrame: NSMakeRect(0, 0, NSMaxX(button.frame) + buttonPadding, maxHeight + buttonPadding)];
[parent addSubview:label];
[parent addSubview:button];
return parent;
}
/**
@return Rounded, gray inline button with tag equal to refresh interval.
*/
+ (NSButton*)grayInlineButton:(NSNumber*)num {
NSButton *button = [NSButton buttonWithTitle:[NSDate stringForInterval:num.intValue rounded:YES] target:nil action:nil];
button.font = [NSFont monospacedDigitSystemFontOfSize: NSFont.labelFontSize weight:NSFontWeightBold];
button.bezelStyle = NSBezelStyleInline;
button.controlSize = NSControlSizeSmall;
TimeUnitType unit = [NSDate unitForInterval:num.intValue rounded:YES];
button.tag = (NSInteger)(roundf(num.floatValue / unit) * unit); // rounded inteval
[button sizeToFit];
return button;
}
/**
@return Simple Label with smaller gray text, non-editable.
*/
+ (NSTextField*)grayLabel:(NSString*)text {
NSTextField *label = [NSTextField textFieldWithString:text];
label.font = [NSFont monospacedDigitSystemFontOfSize: NSFont.labelFontSize weight:NSFontWeightRegular];
label.textColor = [NSColor systemGrayColor];
label.drawsBackground = NO;
label.selectable = NO;
label.editable = NO;
label.bezeled = NO;
[label sizeToFit];
return label;
}
@end

View File

@@ -32,7 +32,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>1208</string>
<string>7288</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>LSUIElement</key>

View File

@@ -0,0 +1,26 @@
//
// The MIT License (MIT)
// Copyright (c) 2019 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import <Cocoa/Cocoa.h>
@interface SettingsAbout : NSViewController
@end

View File

@@ -0,0 +1,32 @@
//
// The MIT License (MIT)
// Copyright (c) 2019 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "SettingsAbout.h"
#import "SettingsAboutView.h"
@implementation SettingsAbout
- (void)loadView {
self.view = [SettingsAboutView new];
}
@end

View File

@@ -0,0 +1,27 @@
//
// The MIT License (MIT)
// Copyright (c) 2019 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import <Cocoa/Cocoa.h>
@interface SettingsAboutView : NSView
@end

View File

@@ -0,0 +1,87 @@
//
// The MIT License (MIT)
// Copyright (c) 2019 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "SettingsAboutView.h"
#import "NSView+Ext.h"
@implementation SettingsAboutView
- (instancetype)init {
self = [super initWithFrame: NSZeroRect];
NSDictionary *infoDict = [[NSBundle mainBundle] infoDictionary];
NSString *name = infoDict[@"CFBundleName"];
NSString *version = [NSString stringWithFormat:NSLocalizedString(@"Version %@", nil), infoDict[@"CFBundleShortVersionString"]];
#if DEBUG
version = [version stringByAppendingFormat:@" (%@)", infoDict[@"CFBundleVersion"]];
#endif
// Application icon image (top-centered)
NSImageView *logo = [[NSView imageView:NSImageNameApplicationIcon size:64] placeIn:self x:CENTER yTop:PAD_M];
// Add app name
NSTextField *lblN = [[[[NSView label:name] large] bold] placeIn:self x:CENTER yTop: YFromTop(logo) + PAD_M];
// Add version info
NSTextField *lblV = [[[[NSView label:version] small] selectable] placeIn:self x:CENTER yTop: YFromTop(lblN) + PAD_S];
// Add rtf document
NSTextView *tv = [[NSTextView new] sizableWidthAndHeight];
tv.textContainerInset = NSMakeSize(0, 15);
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)];
return self;
}
/// Construct attributed string by concatenating snippets of text.
- (NSMutableAttributedString*)rtfDocument {
NSMutableAttributedString *mas = [NSMutableAttributedString new];
[mas beginEditing];
[self str:mas add:@"Programming\n" bold:YES];
[self str:mas add:@"Oleg Geier\n\n" bold:NO];
[self str:mas add:@"Source Code available\n" bold:YES];
[self str:mas add:@"github.com" link:@"https://github.com/relikd/baRSS"];
[self str:mas add:@" (MIT License)\nor " bold:NO];
[self str:mas add:@"gitlab.com" link:@"https://gitlab.com/relikd/baRSS"];
[self str:mas add:@" (MIT License)\n\n" bold:NO];
[self str:mas add:@"3rd-Party Libraries\n" bold:YES];
[self str:mas add:@"RSXML" link:@"https://github.com/relikd/RSXML"];
[self str:mas add:@" (MIT License)" bold:NO];
[mas endEditing];
return mas;
}
/// Helper method to insert attributed (bold) text
- (void)str:(NSMutableAttributedString*)parent add:(NSString*)text bold:(BOOL)flag {
NSFont *font = [NSFont systemFontOfSize:NSFont.systemFontSize weight:(flag ? NSFontWeightMedium : NSFontWeightLight)];
[parent appendAttributedString:[[NSAttributedString alloc] initWithString:NonLocalized(text) attributes:@{ NSFontAttributeName : font }]];
}
/// Helper method to insert attributed hyperlink text
- (void)str:(NSMutableAttributedString*)parent add:(NSString*)text link:(NSString*)url {
[self str:parent add:text bold:NO];
[parent addAttribute:NSLinkAttributeName value:url range:NSMakeRange(parent.length - text.length, text.length)];
}
__attribute__((annotate("returns_localized_nsstring")))
static inline NSString *NonLocalized(NSString *s) { return s; }
@end

View File

@@ -0,0 +1,27 @@
//
// The MIT License (MIT)
// Copyright (c) 2019 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import <Cocoa/Cocoa.h>
@interface SettingsAppearance : NSViewController
- (void)didSelectCheckbox:(NSButton*)sender;
@end

View File

@@ -0,0 +1,53 @@
//
// The MIT License (MIT)
// Copyright (c) 2019 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "SettingsAppearance.h"
#import "SettingsAppearanceView.h"
#import "AppHook.h"
#import "BarStatusItem.h"
@implementation SettingsAppearance
- (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];
}
}
}
#pragma mark - Checkbox Callback Method
/// Sync new value with UserDefaults and update status bar icon
- (void)didSelectCheckbox:(NSButton*)sender {
BOOL state = (sender.state == NSControlStateValueOn);
[[NSUserDefaults standardUserDefaults] setBool:state forKey:sender.identifier];
if ([sender.identifier isEqualToString:@"globalUnreadCount"] ||
[sender.identifier isEqualToString:@"globalTintMenuBarIcon"]) {
[[(AppHook*)NSApp statusItem] updateBarIcon];
}
}
@end

View File

@@ -0,0 +1,29 @@
//
// The MIT License (MIT)
// Copyright (c) 2019 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import <Cocoa/Cocoa.h>
@class SettingsAppearance;
@interface SettingsAppearanceView : NSView
@end

View File

@@ -0,0 +1,88 @@
//
// The MIT License (MIT)
// Copyright (c) 2019 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "SettingsAppearanceView.h"
#import "NSView+Ext.h"
#import "DrawImage.h"
#import "UserPrefs.h"
@interface SettingsAppearanceView()
@property (assign) NSUInteger row;
@end
/***/ static const CGFloat IconSize = 18;
/***/ static const CGFloat colWidth = (IconSize + PAD_M); // checkbox column width
@implementation SettingsAppearanceView
- (instancetype)init {
self = [super initWithFrame: NSZeroRect];
self.row = 0;
// Insert matrix header (the three icons)
[self head:0 tooltip:NSLocalizedString(@"Show in menu bar", nil) class:[SettingsIconGlobal class]];
[self head:1 tooltip:NSLocalizedString(@"Show in group menu", nil) class:[SettingsIconGroup class]];
[self head:2 tooltip:NSLocalizedString(@"Show in feed menu", nil) class:[RSSIcon class]];
// Generate checkbox matrix (checkbox state, X: default ON, O: default OFF, blank: hidden)
[self entry:"X " label:NSLocalizedString(@"Tint menu bar icon on unread", nil)];
[self entry:"X " label:NSLocalizedString(@"Update all feeds", nil)];
[self entry:"XXX" label:NSLocalizedString(@"Open all unread", nil)];
[self entry:"XXX" label:NSLocalizedString(@"Mark all read", nil)];
[self entry:"XXX" label:NSLocalizedString(@"Mark all unread", nil)];
[self entry:"XXX" label:NSLocalizedString(@"Number of unread items", nil)];
[self entry:" X" label:NSLocalizedString(@"Tick mark unread items", nil)];
[[self entry:" O" label:NSLocalizedString(@"Short article names", nil)] tooltip:NSLocalizedString(@"Truncate article title after 60 characters", nil)];
[[self entry:" O" label:NSLocalizedString(@"Limit number of articles", nil)] tooltip:NSLocalizedString(@"Display at most 40 articles in feed menu", nil)];
return self;
}
/// Helper method for matrix table header icons
- (void)head:(int)x tooltip:(NSString*)ttip class:(Class)cls {
[[[[cls alloc] initWithFrame:NSMakeRect(0, 0, IconSize, IconSize)] tooltip:ttip] placeIn:self x:PAD_WIN + x * colWidth yTop:PAD_WIN];
}
/// Create new entry with 1-3 checkboxes and a descriptive label
- (NSTextField*)entry:(char*)m label:(NSString*)text {
static const char* scope[] = { "global", "group", "feed" };
static const char* ident[] = { "TintMenuBarIcon", "UpdateAll", "OpenUnread", "MarkRead", "MarkUnread", "UnreadCount", "TickMark", "ShortNames", "LimitArticles" };
CGFloat y = PAD_WIN + IconSize + PAD_S + self.row * (PAD_S + HEIGHT_LABEL);
// Add checkboxes: row 0 - 8, col 0 - 2
for (NSUInteger col = 0; col < 3; col++) {
NSString *key = [NSString stringWithFormat:@"%s%s", scope[col], ident[self.row]];
BOOL state;
switch (m[col]) {
case 'X': state = [UserPrefs defaultYES:key]; break;
case 'O': state = [UserPrefs defaultNO: key]; break;
default: continue; // ignore blanks
}
NSButton *check = [[NSView checkbox:state] placeIn:self x:PAD_WIN + col * colWidth + 2 yTop:y + 2]; // 2px checkbox offset
check.identifier = key;
check.accessibilityLabel = [text stringByAppendingFormat:@" (%s)", scope[col]]; // TODO: localize: global, group, feed
}
self.row += 1;
// Add label
return [[[NSView label:text] placeIn:self x:PAD_WIN + 3 * colWidth yTop:y] sizeToRight:PAD_WIN];
}
@end

View File

@@ -33,6 +33,7 @@
@interface ModalFeedEdit : ModalEditDialog <NSTextFieldDelegate>
- (void)didClickWarningButton:(NSButton*)sender;
@end
@interface ModalGroupEdit : ModalEditDialog

View File

@@ -26,8 +26,10 @@
#import "Feed+Ext.h"
#import "FeedMeta+Ext.h"
#import "FeedGroup+Ext.h"
#import "Statistics.h"
#import "ModalFeedEditView.h"
#import "RefreshStatisticsView.h"
#import "NSDate+Ext.h"
#import "NSView+Ext.h"
#pragma mark - ModalEditDialog -
@@ -65,36 +67,28 @@
@interface ModalFeedEdit() <RefreshIntervalButtonDelegate>
@property (weak) IBOutlet NSTextField *url;
@property (weak) IBOutlet NSTextField *name;
@property (weak) IBOutlet NSTextField *refreshNum;
@property (weak) IBOutlet NSPopUpButton *refreshUnit;
@property (weak) IBOutlet NSProgressIndicator *spinnerURL;
@property (weak) IBOutlet NSProgressIndicator *spinnerName;
@property (weak) IBOutlet NSButton *warningIndicator;
@property (weak) IBOutlet NSPopover *warningPopover;
@property (strong) NSView *statisticsView;
@property (strong) IBOutlet ModalFeedEditView *view; // override
@property (strong) RefreshStatisticsView *statisticsView;
@property (copy) NSString *previousURL; // check if changed and avoid multiple download
@property (copy) NSString *httpDate;
@property (copy) NSString *httpEtag;
@property (copy) NSString *faviconURL;
@property (strong) NSImage *favicon;
@property (strong) NSError *feedError; // download error or xml parser error
@property (strong) RSParsedFeed *feedResult; // parsed result
@property (assign) BOOL didDownloadFeed; // check if feed articles need update
@end
@implementation ModalFeedEdit
@dynamic view;
/// Init feed edit dialog with default values.
- (void)viewDidLoad {
[super viewDidLoad];
- (void)loadView {
self.view = [[ModalFeedEditView alloc] initWithController:self];
self.previousURL = @"";
self.refreshNum.intValue = 30;
[NSDate populateUnitsMenu:self.refreshUnit selected:TimeUnitMinutes];
self.warningIndicator.image = nil;
[self.warningIndicator.cell setHighlightsBy:NSNoCellMask];
self.view.refreshNum.intValue = 30;
[NSDate populateUnitsMenu:self.view.refreshUnit selected:TimeUnitMinutes];
[self populateTextFields:self.feedGroup];
}
@@ -103,11 +97,11 @@
*/
- (void)populateTextFields:(FeedGroup*)fg {
if (!fg || [fg hasChanges]) return; // hasChanges is true only if newly created
self.name.objectValue = fg.name;
self.url.objectValue = fg.feed.meta.url;
self.previousURL = self.url.stringValue;
self.warningIndicator.image = [fg.feed iconImage16];
[NSDate setInterval:fg.feed.meta.refresh forPopup:self.refreshUnit andField:self.refreshNum animate:NO];
self.view.name.objectValue = fg.name;
self.view.url.objectValue = fg.feed.meta.url;
self.previousURL = self.view.url.stringValue;
self.view.favicon.image = [fg.feed iconImage16];
[NSDate setInterval:fg.feed.meta.refresh forPopup:self.view.refreshUnit andField:self.view.refreshNum animate:NO];
[self statsForCoreDataObject];
}
@@ -119,15 +113,15 @@
*/
- (void)applyChangesToCoreDataObject {
Feed *feed = self.feedGroup.feed;
[self.feedGroup setNameIfChanged:self.name.stringValue];
[self.feedGroup setNameIfChanged:self.view.name.stringValue];
FeedMeta *meta = feed.meta;
[meta setUrlIfChanged:self.previousURL];
[meta setRefreshAndSchedule:[NSDate intervalForPopup:self.refreshUnit andField:self.refreshNum]];
[meta setRefreshAndSchedule:[NSDate intervalForPopup:self.view.refreshUnit andField:self.view.refreshNum]];
// updateTimer will be scheduled once preferences is closed
if (self.didDownloadFeed) {
[meta setEtag:self.httpEtag modified:self.httpDate];
[feed updateWithRSS:self.feedResult postUnreadCountChange:YES];
[feed setIconImage:self.favicon];
[feed setIconImage:self.view.favicon.image];
}
}
@@ -137,20 +131,20 @@
*/
- (void)preDownload {
[self.modalSheet setDoneEnabled:NO]; // prevent user from closing the dialog during download
[self.spinnerURL startAnimation:nil];
[self.spinnerName startAnimation:nil];
self.warningIndicator.image = nil;
[self.view.spinnerURL startAnimation:nil];
[self.view.spinnerName startAnimation:nil];
self.view.favicon.image = nil;
self.view.warningButton.hidden = YES;
self.didDownloadFeed = NO;
// Assuming the user has not changed title since the last fetch.
// Reset to "" because after download it will be pre-filled with new feed title
if ([self.name.stringValue isEqualToString:self.feedResult.title]) {
self.name.stringValue = @"";
if ([self.view.name.stringValue isEqualToString:self.feedResult.title]) {
self.view.name.stringValue = @"";
}
self.feedResult = nil;
self.feedError = nil;
self.httpEtag = nil;
self.httpDate = nil;
self.favicon = nil;
self.faviconURL = nil;
}
@@ -192,8 +186,8 @@
for (RSHTMLMetadataFeedLink *fl in list) {
[menu addItemWithTitle:fl.title action:nil keyEquivalent:@""];
}
NSPoint belowURL = NSMakePoint(0,self.url.frame.size.height);
if ([menu popUpMenuPositioningItem:nil atLocation:belowURL inView:self.url]) {
NSPoint belowURL = NSMakePoint(0, NSHeight(self.view.url.frame));
if ([menu popUpMenuPositioningItem:nil atLocation:belowURL inView:self.view.url]) {
NSInteger idx = [menu indexOfItem:menu.highlightedItem];
if (idx < 0) idx = 0; // User hit enter without selection. Assume first item, because PopUpMenu did return YES!
return [list objectAtIndex:(NSUInteger)idx].link;
@@ -210,22 +204,25 @@
if (self.modalSheet.didCloseAndCancel)
return;
// 1. Stop spinner animation for name field. (keep spinner for URL running until favicon downloaded)
[self.spinnerName stopAnimation:nil];
[self.view.spinnerName stopAnimation:nil];
// 2. If URL was redirected, replace original text field value with new one. (e.g., https redirect)
if (responseURL.length > 0 && ![responseURL isEqualToString:self.previousURL]) {
self.previousURL = responseURL;
self.url.stringValue = responseURL;
self.view.url.stringValue = responseURL;
}
// 3. Copy parsed feed title to text field. (only if user hasn't set anything else yet)
NSString *parsedTitle = self.feedResult.title;
if (parsedTitle.length > 0 && [self.name.stringValue isEqualToString:@""]) {
self.name.stringValue = parsedTitle; // no damage to replace an empty string
if (parsedTitle.length > 0 && [self.view.name.stringValue isEqualToString:@""]) {
self.view.name.stringValue = parsedTitle; // no damage to replace an empty string
}
// TODO: user preference to automatically select refresh interval (selection: None,min,max,avg,median)
[self statsForDownloadObject];
// 4. Continue with favicon download (or finish with error)
if (self.feedError) {
[self finishDownloadWithFavicon:[NSImage imageNamed:NSImageNameCaution]];
BOOL hasError = (self.feedError != nil);
self.view.favicon.hidden = hasError;
self.view.warningButton.hidden = !hasError;
if (hasError) {
[self finishDownloadWithFavicon];
} else {
if (!self.faviconURL)
self.faviconURL = self.feedResult.link;
@@ -234,8 +231,8 @@
[FeedDownload downloadFavicon:self.faviconURL finished:^(NSImage * _Nullable img) {
if (self.modalSheet.didCloseAndCancel)
return;
self.favicon = img;
[self finishDownloadWithFavicon:img];
self.view.favicon.image = img;
[self finishDownloadWithFavicon];
}];
}
}
@@ -244,12 +241,10 @@
The last step of the download process.
Stop spinning animation set favivon image preview (right of url bar) and re-enable 'Done' button.
*/
- (void)finishDownloadWithFavicon:(NSImage*)img {
- (void)finishDownloadWithFavicon {
if (self.modalSheet.didCloseAndCancel)
return;
[self.warningIndicator.cell setHighlightsBy: (self.feedError ? NSContentsCellMask : NSNoCellMask)];
self.warningIndicator.image = img;
[self.spinnerURL stopAnimation:nil];
[self.view.spinnerURL stopAnimation:nil];
[self.modalSheet setDoneEnabled:YES];
}
@@ -275,24 +270,22 @@
/// Generate statistics UI with buttons to quickly select refresh unit and duration.
- (void)appendViewWithFeedStatistics:(NSArray*)dates count:(NSUInteger)count {
static const CGFloat statsPadding = 15.f;
CGFloat prevHeight = 0.f;
if (self.statisticsView != nil) {
prevHeight = self.statisticsView.frame.size.height + statsPadding;
prevHeight = NSHeight(self.statisticsView.frame) + PAD_L;
[self.statisticsView removeFromSuperview];
self.statisticsView = nil;
}
NSDictionary *stats = [Statistics refreshInterval:dates];
NSView *v = [Statistics viewForRefreshInterval:stats articleCount:count callback:self];
[[self getModalSheet] extendContentViewBy:v.frame.size.height + statsPadding - prevHeight];
[v setFrameOrigin:NSMakePoint(0.5f*(NSWidth(self.view.frame) - NSWidth(v.frame)), 0)];
[self.view addSubview:v];
self.statisticsView = v;
NSDictionary *stats = [NSDate refreshIntervalStatistics:dates];
RefreshStatisticsView *rsv = [[RefreshStatisticsView alloc] initWithRefreshInterval:stats articleCount:count callback:self];
[[self getModalSheet] extendContentViewBy:NSHeight(rsv.frame) + PAD_L - prevHeight];
self.statisticsView = [rsv placeIn:self.view x:CENTER y:0];
}
/// Callback method for @c Statistics @c +viewForRefreshInterval:articleCount:callback:
/// Callback method @c RefreshStatisticsView
- (void)refreshIntervalButtonClicked:(NSButton *)sender {
[NSDate setInterval:(Interval)sender.tag forPopup:self.refreshUnit andField:self.refreshNum animate:YES];
[NSDate setInterval:(Interval)sender.tag forPopup:self.view.refreshUnit andField:self.view.refreshNum animate:YES];
}
@@ -301,8 +294,8 @@
/// Window delegate will be only called on button 'Done'.
- (BOOL)windowShouldClose:(NSWindow *)sender {
if (![self.previousURL isEqualToString:self.url.stringValue]) {
[[NSNotificationCenter defaultCenter] postNotificationName:NSControlTextDidEndEditingNotification object:self.url];
if (![self.previousURL isEqualToString:self.view.url.stringValue]) {
[[NSNotificationCenter defaultCenter] postNotificationName:NSControlTextDidEndEditingNotification object:self.view.url];
return NO;
}
return YES;
@@ -310,30 +303,26 @@
/// Whenever the user finished entering the url (return key or focus change) perform a download request.
- (void)controlTextDidEndEditing:(NSNotification *)obj {
if (obj.object == self.url) {
if (![self.previousURL isEqualToString:self.url.stringValue]) {
self.previousURL = self.url.stringValue;
if (obj.object == self.view.url) {
if (![self.previousURL isEqualToString:self.view.url.stringValue]) {
self.previousURL = self.view.url.stringValue;
[self downloadRSS];
}
}
}
/// Warning button next to url text field. Will be visible if an error occurs during download.
- (IBAction)didClickWarningButton:(NSButton*)sender {
- (void)didClickWarningButton:(NSButton*)sender {
if (!self.feedError)
return;
NSString *str = self.feedError.localizedDescription;
NSTextField *tf = self.warningPopover.contentViewController.view.subviews.firstObject;
tf.maximumNumberOfLines = 7;
tf.objectValue = str;
self.view.warningText.objectValue = self.feedError.localizedDescription;
NSSize newSize = self.view.warningText.fittingSize; // width is limited by the textfield's preferred width
newSize.width += 2 * self.view.warningText.frame.origin.x; // the padding
newSize.height += 2 * self.view.warningText.frame.origin.y;
NSSize newSize = tf.fittingSize; // width is limited by the textfield's preferred width
newSize.width += 2 * tf.frame.origin.x; // the padding
newSize.height += 2 * tf.frame.origin.y;
[self.warningPopover showRelativeToRect:sender.bounds ofView:sender preferredEdge:NSRectEdgeMinY];
[self.warningPopover setContentSize:newSize];
self.view.warningPopover.contentSize = newSize;
[self.view.warningPopover showRelativeToRect:sender.bounds ofView:sender preferredEdge:NSRectEdgeMinY];
}
@end
@@ -351,41 +340,10 @@
}
/// Set one single @c NSTextField as entire view. Populate with default value and placeholder.
- (void)loadView {
NSTextField *tf = [NSTextField textFieldWithString:NSLocalizedString(@"New Group", nil)];
tf.placeholderString = NSLocalizedString(@"New Group", nil);
tf.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
self.view = tf;
self.view = [[NSView inputField:NSLocalizedString(@"New Group Name", nil) width:0] sizeToRight:0];
}
/// Edit of group finished. Save changes to core data object and perform save operation on delegate.
- (void)applyChangesToCoreDataObject {
[self.feedGroup setNameIfChanged:((NSTextField*)self.view).stringValue];
}
@end
#pragma mark - StrictUIntFormatter -
@interface StrictUIntFormatter : NSFormatter
@end
@implementation StrictUIntFormatter
/// Display object as integer formatted string.
- (NSString *)stringForObjectValue:(id)obj {
return [NSString stringWithFormat:@"%d", [[NSString stringWithFormat:@"%@", obj] intValue]];
}
/// Parse any pasted input as integer.
- (BOOL)getObjectValue:(out id _Nullable __autoreleasing *)obj forString:(NSString *)string errorDescription:(out NSString *__autoreleasing _Nullable *)error {
*obj = [[NSNumber numberWithInt:[string intValue]] stringValue];
return YES;
}
/// Only digits, no other character allowed
- (BOOL)isPartialStringValid:(NSString *__autoreleasing _Nonnull *)partialStringPtr proposedSelectedRange:(NSRangePointer)proposedSelRangePtr originalString:(NSString *)origString originalSelectedRange:(NSRange)origSelRange errorDescription:(NSString *__autoreleasing _Nullable *)error {
for (NSUInteger i = 0; i < [*partialStringPtr length]; i++) {
unichar c = [*partialStringPtr characterAtIndex:i];
if (c < '0' || c > '9')
return NO;
}
return YES;
}
@end

View File

@@ -1,158 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14313.18"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="ModalFeedEdit">
<connections>
<outlet property="name" destination="ab8-rr-HbK" id="J4T-Zl-KF3"/>
<outlet property="refreshNum" destination="cNl-ht-xws" id="3cA-TW-qi5"/>
<outlet property="refreshUnit" destination="TUi-VS-ge4" id="dr6-GW-gU0"/>
<outlet property="spinnerName" destination="Afo-pQ-8Qx" id="DVx-vd-Zer"/>
<outlet property="spinnerURL" destination="H0a-x4-o4X" id="MgB-RI-yP5"/>
<outlet property="url" destination="Asm-D9-ZfT" id="3gO-Xc-2KJ"/>
<outlet property="view" destination="i0K-k8-GMU" id="qcu-Oh-rOj"/>
<outlet property="warningIndicator" destination="LWE-Y8-ebl" id="j9x-OY-2th"/>
<outlet property="warningPopover" destination="stq-gJ-ra0" id="rJy-GV-PHk"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="i0K-k8-GMU" userLabel="View">
<rect key="frame" x="0.0" y="0.0" width="320" height="79"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="MOX-a1-Yda" userLabel="URL Label">
<rect key="frame" x="-2" y="60" width="103" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="URL" id="6wE-lP-4xC">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Asm-D9-ZfT">
<rect key="frame" x="107" y="58" width="191" height="21"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" placeholderString="https://example.org/feed.rss" drawsBackground="YES" usesSingleLineMode="YES" id="0Sk-H2-VAC">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<outlet property="delegate" destination="-2" id="R3c-aF-If2"/>
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kVL-HV-oxU" userLabel="Name Label">
<rect key="frame" x="-2" y="31" width="103" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Name" id="2ls-F4-oUL">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ab8-rr-HbK">
<rect key="frame" x="107" y="29" width="191" height="21"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" placeholderString="Example Title" drawsBackground="YES" usesSingleLineMode="YES" id="1ku-vp-T5y">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5Tc-as-s1U" userLabel="Refresh Label">
<rect key="frame" x="-2" y="2" width="103" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Refresh" id="2IV-ec-RfH">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cNl-ht-xws">
<rect key="frame" x="107" y="0.0" width="85" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" placeholderString="30" drawsBackground="YES" usesSingleLineMode="YES" id="DqU-fT-cIf">
<customFormatter key="formatter" id="Lbd-r9-4bc" customClass="StrictUIntFormatter"/>
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TUi-VS-ge4">
<rect key="frame" x="198" y="-3" width="125" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="-- list --" bezelStyle="rounded" lineBreakMode="truncatingTail" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" autoenablesItems="NO" altersStateOfSelectedItem="NO" selectedItem="lQ1-ai-wYn" id="O0p-Tc-KQ1">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" showsStateColumn="NO" autoenablesItems="NO" id="7hX-7Y-rtT">
<items>
<menuItem title="-- list --" id="lQ1-ai-wYn">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
</items>
</menu>
</popUpButtonCell>
</popUpButton>
<progressIndicator wantsLayer="YES" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" controlSize="small" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="H0a-x4-o4X">
<rect key="frame" x="304" y="60" width="16" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMinY="YES"/>
</progressIndicator>
<progressIndicator wantsLayer="YES" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" controlSize="small" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="Afo-pQ-8Qx">
<rect key="frame" x="304" y="31" width="16" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMinY="YES"/>
</progressIndicator>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="LWE-Y8-ebl">
<rect key="frame" x="302" y="60" width="18" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="roundRect" bezelStyle="roundedRect" image="NSCaution" imagePosition="only" alignment="center" refusesFirstResponder="YES" state="on" imageScaling="proportionallyDown" inset="2" id="FAw-6c-Vij">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="cellTitle"/>
<string key="keyEquivalent">i</string>
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</buttonCell>
<connections>
<action selector="didClickWarningButton:" target="-2" id="wNa-Cc-jZb"/>
</connections>
</button>
</subviews>
<point key="canvasLocation" x="-137" y="586.5"/>
</customView>
<viewController id="xTH-2c-Ppt" userLabel="Popover View Controller">
<connections>
<outlet property="view" destination="bVj-RM-sjw" id="TP8-Eb-GVO"/>
</connections>
</viewController>
<popover behavior="t" id="stq-gJ-ra0">
<connections>
<outlet property="contentViewController" destination="xTH-2c-Ppt" id="ODh-uM-ARs"/>
</connections>
</popover>
<customView id="bVj-RM-sjw" userLabel="Popover View">
<rect key="frame" x="0.0" y="0.0" width="300" height="44"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField wantsLayer="YES" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" setsMaxLayoutWidthAtFirstLayout="YES" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nCT-Lc-wce">
<rect key="frame" x="2" y="2" width="296" height="40"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<textFieldCell key="cell" truncatesLastVisibleLine="YES" selectable="YES" allowsUndo="NO" sendsActionOnEndEditing="YES" id="YJs-n4-Lxb">
<font key="font" metaFont="system"/>
<string key="title">Couldn't load Feed
An additional line
and a third</string>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<point key="canvasLocation" x="14" y="477"/>
</customView>
</objects>
<resources>
<image name="NSCaution" width="32" height="32"/>
</resources>
</document>

View File

@@ -0,0 +1,45 @@
//
// The MIT License (MIT)
// Copyright (c) 2019 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import <Cocoa/Cocoa.h>
@class ModalFeedEdit;
@interface ModalFeedEditView : NSView
@property (weak) IBOutlet NSTextField *url;
@property (weak) IBOutlet NSProgressIndicator *spinnerURL;
@property (weak) IBOutlet NSImageView *favicon;
@property (weak) IBOutlet NSTextField *name;
@property (weak) IBOutlet NSProgressIndicator *spinnerName;
@property (weak) IBOutlet NSTextField *refreshNum;
@property (weak) IBOutlet NSPopUpButton *refreshUnit;
@property (weak) IBOutlet NSButton *warningButton;
@property NSPopover *warningPopover;
@property (weak) IBOutlet NSTextField *warningText;
- (instancetype)initWithController:(ModalFeedEdit*)controller NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithFrame:(NSRect)frameRect NS_UNAVAILABLE;
- (nullable instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE;
@end

View File

@@ -0,0 +1,120 @@
//
// The MIT License (MIT)
// Copyright (c) 2019 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "ModalFeedEditView.h"
#import "ModalFeedEdit.h"
#import "NSView+Ext.h"
@interface StrictUIntFormatter : NSFormatter
@end
@implementation ModalFeedEditView
- (instancetype)initWithController:(ModalFeedEdit*)controller {
NSArray *lbls = @[NSLocalizedString(@"URL", nil),
NSLocalizedString(@"Name", nil),
NSLocalizedString(@"Refresh", nil)];
NSView *labels = [NSView labelColumn:lbls rowHeight:HEIGHT_INPUTFIELD padding:PAD_S];
self = [super initWithFrame:NSMakeRect(0, 0, 0, NSHeight(labels.frame))];
self.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
CGFloat x = NSWidth(labels.frame) + PAD_S;
static const CGFloat rowHeight = PAD_S + HEIGHT_INPUTFIELD;
[labels placeIn:self x:0 yTop:0];
// 1. row
self.url = [[[NSView inputField:@"https://example.org/feed.rss" width:0] placeIn:self x:x yTop:0] sizeToRight:PAD_S + 18];
self.spinnerURL = [[NSView activitySpinner] placeIn:self xRight:1 yTop:2.5];
self.favicon = [[[NSView imageView:nil size:18] tooltip:NSLocalizedString(@"Favicon", nil)] placeIn:self xRight:0 yTop:1.5];
NSTextField *errorDesc = [self warningPopoverContentView];
self.warningPopover = [self warningPopoverControllerWith:errorDesc];
self.warningText = errorDesc; // after added to parent view, otherwise will be released immediatelly (weak ivar)
self.warningButton = [[[[NSView buttonIcon:[NSImage imageNamed:NSImageNameCaution] size:18] action:@selector(didClickWarningButton:) target:nil] // up the responder chain
tooltip:NSLocalizedString(@"Click here to show failure reason", nil)]
placeIn:self xRight:0 yTop:1.5];
// 2. row
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.refreshUnit = [[NSView popupButton:120] placeIn:self x:NSMaxX(self.refreshNum.frame) + PAD_M yTop:2*rowHeight];
// initial state
self.url.delegate = controller;
self.warningButton.hidden = YES;
self.refreshNum.formatter = [StrictUIntFormatter new]; // see below ...
//[self.warningButton.cell setHighlightsBy:(error ? NSContentsCellMask : NSNoCellMask)];
return self;
}
/// User visible error description text (after click on warning button)
- (NSTextField*)warningPopoverContentView {
NSTextField *txt = [[[NSView label:@""] selectable] sizableWidthAndHeight];
[txt setFrameSize: NSMakeSize(300, 100)];
txt.lineBreakMode = NSLineBreakByWordWrapping;
txt.maximumNumberOfLines = 7;
return txt;
}
/// Prepare popover controller to display download errors
- (NSPopover*)warningPopoverControllerWith:(NSTextField*)content {
NSPopover *pop = [[NSPopover alloc] init];
pop.behavior = NSPopoverBehaviorTransient;
pop.contentViewController = [[NSViewController alloc] init];
pop.contentViewController.view = [[NSView alloc] initWithFrame:content.frame];
[pop.contentViewController.view addSubview:content];
content.frame = NSInsetRect(content.frame, 4, 2);
content.preferredMaxLayoutWidth = NSWidth(content.frame);
return pop;
}
@end
#pragma mark - StrictUIntFormatter -
@implementation StrictUIntFormatter
/// Display object as integer formatted string.
- (NSString *)stringForObjectValue:(id)obj {
return [NSString stringWithFormat:@"%d", [[NSString stringWithFormat:@"%@", obj] intValue]];
}
/// Parse any pasted input as integer.
- (BOOL)getObjectValue:(out id _Nullable __autoreleasing *)obj forString:(NSString *)string errorDescription:(out NSString *__autoreleasing _Nullable *)error {
*obj = [[NSNumber numberWithInt:[string intValue]] stringValue];
return YES;
}
/// Only digits, no other character allowed
- (BOOL)isPartialStringValid:(NSString *__autoreleasing _Nonnull *)partialStringPtr proposedSelectedRange:(NSRangePointer)proposedSelRangePtr originalString:(NSString *)origString originalSelectedRange:(NSRange)origSelRange errorDescription:(NSString *__autoreleasing _Nullable *)error {
for (NSUInteger i = 0; i < [*partialStringPtr length]; i++) {
unichar c = [*partialStringPtr characterAtIndex:i];
if (c < '0' || c > '9')
return NO;
}
return YES;
}
@end

View File

@@ -26,6 +26,8 @@
#import "StoreCoordinator.h"
#import "FeedDownload.h"
#import "Constants.h"
#import "NSDate+Ext.h"
#import "NSView+Ext.h"
@implementation OpmlExport
@@ -54,12 +56,12 @@
/// Display Save File Panel to select export destination. All feeds from core data will be exported.
+ (void)showExportDialog:(NSWindow*)window withContext:(NSManagedObjectContext*)moc {
NSSavePanel *sp = [NSSavePanel savePanel];
sp.nameFieldStringValue = [NSString stringWithFormat:@"baRSS feeds %@", [self currentDayAsStringISO8601:NO]];
sp.nameFieldStringValue = [NSString stringWithFormat:@"baRSS feeds %@", [NSDate dayStringLocalized]];
sp.allowedFileTypes = @[@"opml"];
sp.allowsOtherFileTypes = YES;
NSView *radioView = [self radioGroupCreate:@[NSLocalizedString(@"Hierarchical", nil),
NSView *radioView = [NSView radioGroup:@[NSLocalizedString(@"Hierarchical", nil),
NSLocalizedString(@"Flattened", nil)]];
sp.accessoryView = [self viewByPrependingLabel:NSLocalizedString(@"Export format:", nil) toView:radioView];
sp.accessoryView = [NSView wrapView:radioView withLabel:NSLocalizedString(@"Export format:", nil) padding:PAD_M];
[sp beginSheetModalForWindow:window completionHandler:^(NSModalResponse result) {
if (result == NSModalResponseOK) {
@@ -94,7 +96,7 @@
alert.informativeText = NSLocalizedString(@"Do you want to append or replace existing items?", nil);
[alert addButtonWithTitle:NSLocalizedString(@"Import", nil)];
[alert addButtonWithTitle:NSLocalizedString(@"Cancel", nil)];
alert.accessoryView = [self radioGroupCreate:@[NSLocalizedString(@"Append", nil),
alert.accessoryView = [NSView radioGroup:@[NSLocalizedString(@"Append", nil),
NSLocalizedString(@"Overwrite", nil)]];
if ([alert runModal] == NSAlertFirstButtonReturn) {
@@ -196,7 +198,7 @@
NSXMLElement *head = [NSXMLElement elementWithName:@"head"];
head.children = @[[NSXMLElement elementWithName:@"title" stringValue:@"baRSS feeds"],
[NSXMLElement elementWithName:@"ownerName" stringValue:@"baRSS"],
[NSXMLElement elementWithName:@"dateCreated" stringValue:[self currentDayAsStringISO8601:YES]] ];
[NSXMLElement elementWithName:@"dateCreated" stringValue:[NSDate dayStringISO8601]] ];
NSXMLElement *body = [NSXMLElement elementWithName:@"body"];
for (FeedGroup *item in list) {
@@ -247,15 +249,6 @@
#pragma mark - Helper
/// @param flag If @c YES use long internet format for opml file. If @c NO use short format as filename.
+ (NSString*)currentDayAsStringISO8601:(BOOL)flag {
if (flag)
return [[[NSISO8601DateFormatter alloc] init] stringFromDate:[NSDate date]];
// NSDateComponents *now = [[NSCalendar currentCalendar] components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear fromDate:[NSDate date]];
// return [NSString stringWithFormat:@"%04ld-%02ld-%02ld", now.year, now.month, now.day];
return [NSDateFormatter localizedStringFromDate:[NSDate date] dateStyle:NSDateFormatterShortStyle timeStyle:NSDateFormatterNoStyle];
}
/// Count items where @c xmlURL key is set.
+ (NSUInteger)recursiveNumberOfFeeds:(RSOPMLItem*)document {
if ([document attributeForKey:OPMLXMLURLKey]) {
@@ -269,34 +262,6 @@
}
}
/// Solely used to group radio buttons
+ (void)donothing {}
/// Create a new view with as many @c NSRadioButton items as there are strings. Buttons @c tag is equal to the array index.
+ (NSView*)radioGroupCreate:(NSArray<NSString*>*)titles {
if (titles.count == 0)
return nil;
NSRect viewRect = NSMakeRect(0, 0, 0, 8);
NSInteger idx = (NSInteger)titles.count;
NSView *v = [[NSView alloc] init];
for (NSString *title in titles.reverseObjectEnumerator) {
idx -= 1;
NSButton *btn = [NSButton radioButtonWithTitle:title target:self action:@selector(donothing)];
btn.tag = idx;
btn.frame = NSOffsetRect(btn.frame, 0, viewRect.size.height);
viewRect.size.height += btn.frame.size.height + 2; // 2px padding
if (viewRect.size.width < btn.frame.size.width)
viewRect.size.width = btn.frame.size.width;
[v addSubview:btn];
if (idx == 0)
btn.state = NSControlStateValueOn;
}
viewRect.size.height += 6; // 8 - 2px padding
v.frame = viewRect;
return v;
}
/// Loop over all subviews and find the @c NSButton that is selected.
+ (NSInteger)radioGroupSelection:(NSView*)view {
for (NSButton *btn in view.subviews) {
@@ -307,25 +272,4 @@
return -1;
}
/// @return New view with @c NSTextField label in the top left corner and @c radioView on the right side.
+ (NSView*)viewByPrependingLabel:(NSString*)str toView:(NSView*)radioView {
NSTextField *label = [NSTextField textFieldWithString:str];
label.editable = NO;
label.selectable = NO;
label.bezeled = NO;
label.drawsBackground = NO;
NSRect fL = label.frame;
NSRect fR = radioView.frame;
fL.origin.y += fR.size.height - fL.size.height - 8;
fR.origin.x += fL.size.width;
label.frame = fL;
radioView.frame = fR;
NSView *view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, NSMaxX(fR), NSMaxY(fR))];
[view addSubview:label];
[view addSubview:radioView];
return view;
}
@end

View File

@@ -24,15 +24,13 @@
@protocol RefreshIntervalButtonDelegate <NSObject>
@required
/**
The interval-unit combination is stored as follows:
:: @c sender.tag @c >> @c 3 (Refresh Interval)
:: @c sender.tag @c & @c 0x7 (Refresh Unit, where 0: seconds and 4: weeks)
*/
/// @c sender.tag is refresh interval in seconds
- (void)refreshIntervalButtonClicked:(NSButton*)sender;
@end
@interface Statistics : NSObject
+ (NSDictionary*)refreshInterval:(NSArray<NSDate*> *)list;
+ (NSView*)viewForRefreshInterval:(NSDictionary*)info articleCount:(NSUInteger)count callback:(nullable id<RefreshIntervalButtonDelegate>)callback;
@interface RefreshStatisticsView : NSView
- (instancetype)initWithRefreshInterval:(NSDictionary*)info articleCount:(NSUInteger)count callback:(nullable id<RefreshIntervalButtonDelegate>)callback NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithFrame:(NSRect)frameRect NS_UNAVAILABLE;
- (nullable instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE;
@end

View File

@@ -0,0 +1,117 @@
//
// The MIT License (MIT)
// Copyright (c) 2019 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "RefreshStatisticsView.h"
#import "NSDate+Ext.h"
#import "NSView+Ext.h"
@implementation RefreshStatisticsView
/**
Generate UI with buttons for min, max, avg and median. Also show number of articles and latest article date.
@param info The dictionary generated with @c -refreshInterval:
@param count Article count.
@param callback If set, @c sender will be called with @c -refreshIntervalButtonClicked:.
If not disable button border and display as bold inline text.
@return Centered view without autoresizing.
*/
- (instancetype)initWithRefreshInterval:(NSDictionary*)info articleCount:(NSUInteger)count callback:(nullable id<RefreshIntervalButtonDelegate>)callback {
self = [super initWithFrame:NSZeroRect];
self.autoresizesSubviews = NO;
NSTextField *dateView = [self viewForArticlesCount:count latest:info];
if (!info || info.count == 0) {
[self setFrameSize:dateView.frame.size];
[dateView placeIn:self x:0 y:0];
} else {
NSArray *arr = @[GrayLabel(NSLocalizedString(@"min:", nil)), [self createInlineButton:info[@"min"] callback:callback],
GrayLabel(NSLocalizedString(@"max:", nil)), [self createInlineButton:info[@"max"] callback:callback],
GrayLabel(NSLocalizedString(@"avg:", nil)), [self createInlineButton:info[@"avg"] callback:callback],
GrayLabel(NSLocalizedString(@"median:", nil)), [self createInlineButton:info[@"median"] callback:callback]];
NSView *buttonsView = [self placeViewsHorizontally:arr];
CGFloat w = NSWidth(buttonsView.frame);
if (w < NSWidth(dateView.frame))
w = NSWidth(dateView.frame);
[self setFrameSize:NSMakeSize(w, NSHeight(buttonsView.frame) + PAD_M + NSHeight(dateView.frame))];
[dateView placeIn:self x:CENTER yTop:0];
[buttonsView placeIn:self x:CENTER y:0];
}
return self;
}
/// TextField with article count and latest article date.
- (NSTextField*)viewForArticlesCount:(NSUInteger)count latest:(nullable NSDictionary*)info {
NSString *text = [NSString stringWithFormat:NSLocalizedString(@"%lu articles.", nil), count];
if (!info || info.count == 0) {
return GrayLabel(text);
}
NSDate *lastUpdate = [info valueForKey:@"latest"];
NSString *mod = [NSDateFormatter localizedStringFromDate:lastUpdate dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterShortStyle];
NSTextField *label = GrayLabel([text stringByAppendingFormat:NSLocalizedString(@" (latest: %@)", nil), mod]);
// Feed wasn't updated in a while ...
if ([lastUpdate timeIntervalSinceNow] < (-360 * 24 * 60 * 60)) {
NSMutableAttributedString *as = label.attributedStringValue.mutableCopy;
[as addAttribute:NSForegroundColorAttributeName value:[NSColor systemRedColor] range:NSMakeRange(text.length, as.length - text.length)];
[label setAttributedStringValue:as]; // red colored date
}
return label;
}
/// Label with smaller gray text, non-editable. @c 13px height.
NS_INLINE NSTextField* GrayLabel(NSString *text) {
return [[[NSView label:text] tiny] gray];
}
/// Inline button with tag equal to refresh interval. @c 16px height.
- (NSButton*)createInlineButton:(NSNumber*)num callback:(nullable id<RefreshIntervalButtonDelegate>)callback {
NSButton *button = [NSView inlineButton:[NSDate stringForInterval:num.intValue rounded:YES]];
TimeUnitType unit = [NSDate unitForInterval:num.intValue rounded:YES];
button.tag = (NSInteger)(roundf(num.floatValue / unit) * unit); // rounded interval
// TODO: accessibility title: readable interval string
if (callback) {
[button action:@selector(refreshIntervalButtonClicked:) target:callback];
} else {
button.bordered = NO;
button.enabled = NO;
}
return button;
}
/// Helper method to arrange all views in a horizontal line (vertically centered).
- (NSView*)placeViewsHorizontally:(NSArray<NSView*>*)views {
CGFloat w = 0;
NSView *parent = [[NSView alloc] initWithFrame: NSZeroRect];
for (NSView *v in views) {
BOOL isButton = [v isKindOfClass:[NSButton class]];
[v setFrameOrigin:NSMakePoint(w, (isButton ? 0 : 2))];
[parent addSubview:v];
w += NSWidth(v.frame) + (isButton ? PAD_M : 0);
}
[parent setFrameSize:NSMakeSize(w - PAD_M, 16)];
return parent;
}
@end

View File

@@ -24,5 +24,14 @@
/** Manages the NSOutlineView and Feed creation and editing */
@interface SettingsFeeds : NSViewController <NSOutlineViewDataSource, NSOutlineViewDelegate>
@property (strong) NSTreeController *dataStore;
- (void)editSelectedItem;
- (void)doubleClickOutlineView:(NSOutlineView*)sender;
- (void)addFeed;
- (void)addGroup;
- (void)addSeparator;
- (void)remove:(id)sender;
- (void)openImportDialog;
- (void)openExportDialog;
@end

View File

@@ -28,12 +28,10 @@
#import "FeedGroup+Ext.h"
#import "OpmlExport.h"
#import "FeedDownload.h"
#import "SettingsFeedsView.h"
@interface SettingsFeeds ()
@property (weak) IBOutlet NSOutlineView *outlineView;
@property (weak) IBOutlet NSTreeController *dataStore;
@property (weak) IBOutlet NSProgressIndicator *spinner;
@property (weak) IBOutlet NSTextField *spinnerLabel;
@property (strong) SettingsFeedsView *view; // override super
@property (strong) NSArray<NSTreeNode*> *currentlyDraggedNodes;
@property (strong) NSUndoManager *undoManager;
@@ -43,23 +41,20 @@
@end
@implementation SettingsFeeds
@dynamic view;
// TODO: drag-n-drop feeds to opml file?
// Declare a string constant for the drag type - to be used when writing and retrieving pasteboard data...
static NSString *dragNodeType = @"baRSS-feed-drag";
- (void)loadView {
[self initCoreDataStore];
self.view = [[SettingsFeedsView alloc] initWithController:self];
[self.view.outline registerForDraggedTypes:[NSArray arrayWithObject:dragNodeType]];
}
- (void)viewDidLoad {
[super viewDidLoad];
[self.outlineView registerForDraggedTypes:[NSArray arrayWithObject:dragNodeType]];
[self.dataStore setSortDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:YES]]];
self.undoManager = [[NSUndoManager alloc] init];
self.undoManager.groupsByEvent = NO;
self.undoManager.levelsOfUndo = 30;
self.dataStore.managedObjectContext = [StoreCoordinator createChildContext];
self.dataStore.managedObjectContext.undoManager = self.undoManager;
// Register for notifications
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(feedUpdated:) name:kNotificationFeedUpdated object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(feedUpdated:) name:kNotificationFeedIconUpdated object:nil];
@@ -70,12 +65,35 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)initCoreDataStore {
self.undoManager = [[NSUndoManager alloc] init];
self.undoManager.groupsByEvent = NO;
self.undoManager.levelsOfUndo = 30;
self.dataStore = [[NSTreeController alloc] init];
self.dataStore.managedObjectContext = [StoreCoordinator createChildContext];
self.dataStore.managedObjectContext.undoManager = self.undoManager;
self.dataStore.childrenKeyPath = @"children";
self.dataStore.leafKeyPath = @"type";
self.dataStore.entityName = @"FeedGroup";
self.dataStore.objectClass = [FeedGroup class];
self.dataStore.fetchPredicate = [NSPredicate predicateWithFormat:@"parent == nil"];
self.dataStore.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:YES]];
NSError *error;
BOOL ok = [self.dataStore fetchWithRequest:nil merge:NO error:&error];
if (!ok || error) {
[[NSApplication sharedApplication] presentError:error];
}
}
#pragma mark - Activity Spinner & Status Info
/// Initialize status info timer
- (void)viewWillAppear {
[self.dataStore rearrangeObjects]; // needed to scroll outline view to top (if prefs open on another tab)
self.intervalFormatter = [[NSDateComponentsFormatter alloc] init];
self.intervalFormatter.unitsStyle = NSDateComponentsFormatterUnitsStyleShort; // e.g., '30 min'
self.intervalFormatter.maximumUnitCount = 1;
@@ -99,7 +117,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
if (date) {
double nextFire = fabs(date.timeIntervalSinceNow);
if (nextFire > 1e9) { // distance future, over 31 years
self.spinnerLabel.stringValue = @"";
self.view.status.stringValue = @"";
return;
}
if (nextFire > 60) { // update 1/min
@@ -108,7 +126,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
nextFire = 1; // update 1/sec
}
NSString *str = [self.intervalFormatter stringFromTimeInterval: date.timeIntervalSinceNow];
self.spinnerLabel.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Next update in %@", nil), str];
self.view.status.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Next update in %@", nil), str];
[self.timerStatusInfo setFireDate:[NSDate dateWithTimeIntervalSinceNow: nextFire]];
}
}
@@ -116,18 +134,18 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
/// Start ( @c c @c > @c 0 ) or stop ( @c c @c = @c 0 ) activity spinner. Also, sets status info.
- (void)activateSpinner:(NSInteger)c {
if (c == 0) {
[self.spinner stopAnimation:nil];
self.spinnerLabel.stringValue = @"";
[self.view.spinner stopAnimation:nil];
self.view.status.stringValue = @"";
[self.timerStatusInfo fire];
} else {
[self.timerStatusInfo setFireDate:[NSDate distantFuture]];
[self.spinner startAnimation:nil];
[self.view.spinner startAnimation:nil];
if (c == 1) { // exactly one feed
self.spinnerLabel.stringValue = NSLocalizedString(@"Updating 1 feed …", nil);
self.view.status.stringValue = NSLocalizedString(@"Updating 1 feed …", nil);
} else if (c < 0) { // unknown number of feeds
self.spinnerLabel.stringValue = NSLocalizedString(@"Updating feeds …", nil);
self.view.status.stringValue = NSLocalizedString(@"Updating feeds …", nil);
} else {
self.spinnerLabel.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Updating %lu feeds …", nil), c];
self.view.status.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Updating %lu feeds …", nil), c];
}
}
}
@@ -207,25 +225,39 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
#pragma mark - UI Button Interaction
/// Open clicked or selected item for editing.
- (void)editSelectedItem {
FeedGroup *chosen = [self clickedItem];
if (!chosen) chosen = self.dataStore.selectedObjects.firstObject;
[self showModalForFeedGroup:chosen isGroupEdit:YES]; // yes will be overwritten anyway
}
/// Open clicked item for editing.
- (void)doubleClickOutlineView:(NSOutlineView*)sender {
FeedGroup *fg = [self clickedItem];
if (!fg) return;
[self showModalForFeedGroup:fg isGroupEdit:YES]; // yes will be overwritten anyway
}
/// Add feed button.
- (IBAction)addFeed:(id)sender {
- (void)addFeed {
[self showModalForFeedGroup:nil isGroupEdit:NO];
}
/// Add group button.
- (IBAction)addGroup:(id)sender {
- (void)addGroup {
[self showModalForFeedGroup:nil isGroupEdit:YES];
}
/// Add separator button.
- (IBAction)addSeparator:(id)sender {
- (void)addSeparator {
[self beginCoreDataChange];
[self insertFeedGroupAtSelection:SEPARATOR].name = @"---";
[self endCoreDataChangeShouldUndo:NO];
}
/// Remove feed button. User has selected one or more item in outline view.
- (IBAction)remove:(id)sender {
- (void)remove:(id)sender {
[self beginCoreDataChange];
NSArray<NSTreeNode*> *parentNodes = [self.dataStore.selectedNodes valueForKeyPath:@"parentNode"];
[self.dataStore remove:sender];
@@ -236,31 +268,12 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
}
/// Open user selected item for editing.
- (IBAction)doubleClickOutlineView:(NSOutlineView*)sender {
if (sender.clickedRow == -1)
return; // ignore clicks on column headers and where no row was selected
FeedGroup *fg = [(NSTreeNode*)[sender itemAtRow:sender.clickedRow] representedObject];
[self showModalForFeedGroup:fg isGroupEdit:YES]; // yes will be overwritten anyway
- (void)openImportDialog {
[OpmlExport showImportDialog:self.view.window withContext:self.dataStore.managedObjectContext];
}
/// Share menu button. Currently only import & export feeds as OPML.
- (IBAction)shareMenu:(NSButton*)sender {
if (!sender.menu) {
sender.menu = [[NSMenu alloc] initWithTitle:NSLocalizedString(@"Import / Export menu", nil)];
sender.menu.autoenablesItems = NO;
[sender.menu addItemWithTitle:NSLocalizedString(@"Import Feeds …", nil) action:nil keyEquivalent:@""].tag = 101;
[sender.menu addItemWithTitle:NSLocalizedString(@"Export Feeds …", nil) action:nil keyEquivalent:@""].tag = 102;
// TODO: Add menus for online sync? email export? etc.
}
if ([sender.menu popUpMenuPositioningItem:nil atLocation:NSMakePoint(0,sender.frame.size.height) inView:sender]) {
NSInteger tag = sender.menu.highlightedItem.tag;
if (tag == 101) {
[OpmlExport showImportDialog:self.view.window withContext:self.dataStore.managedObjectContext];
} else if (tag == 102) {
- (void)openExportDialog {
[OpmlExport showExportDialog:self.view.window withContext:self.dataStore.managedObjectContext];
}
}
}
@@ -320,7 +333,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
- (NSIndexPath*)indexPathForInsertAtNode:(NSTreeNode*)node {
if (!node) { // append to root
return [NSIndexPath indexPathWithIndex:[self.dataStore arrangedObjects].childNodes.count]; // or 0 to append at front
} else if ([self.outlineView isItemExpanded:node]) { // append to group (if open)
} else if ([self.view.outline isItemExpanded:node]) { // append to group (if open)
return [node.indexPath indexPathByAddingIndex:0]; // or 'selection.childNodes.count' to append at end
} else { // append before / after selected item
NSIndexPath *pth = node.indexPath;
@@ -328,6 +341,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
NSUInteger lastIdx = [pth indexAtPosition:pth.length - 1];
return [[pth indexPathByRemovingLastIndex] indexPathByAddingIndex:lastIdx + 1];
}
// TODO: always append to end
}
/// Loop over all descendants and update @c sortIndex @c (FeedGroup) as well as all @c indexPath @c (Feed)
@@ -401,48 +415,59 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
#pragma mark - Data Source Delegate
// Data source is handled by bindings anyway. These methods can be ignored
- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item { return 0; }
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item { return YES; }
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item { return nil; }
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { return nil; }
/// Populate @c NSOutlineView data cells with core data object values.
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item {
FeedGroup *fg = [(NSTreeNode*)item representedObject];
BOOL isSeperator = (fg.type == SEPARATOR);
BOOL isRefreshColumn = [tableColumn.identifier isEqualToString:@"RefreshColumn"];
NSString *cellIdent = (isRefreshColumn ? @"cellRefresh" : (isSeperator ? @"cellSeparator" : @"cellFeed"));
// owner is nil to prohibit repeated awakeFromNib calls
NSTableCellView *cellView = [self.outlineView makeViewWithIdentifier:cellIdent owner:nil];
if (isRefreshColumn) {
NSString *str = [fg refreshString];
cellView.textField.stringValue = str;
cellView.textField.textColor = (str.length > 1 ? [NSColor controlTextColor] : [NSColor disabledControlTextColor]);
} else if (isSeperator) {
return cellView; // refresh cell already skipped with the above if condition
} else {
cellView.textField.objectValue = fg.name;
cellView.imageView.image = fg.iconImage16;
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(NSTreeNode*)item {
NSUserInterfaceItemIdentifier ident = tableColumn.identifier;
if (ident == CustomCellName) {
FeedGroup *fg = [item representedObject];
if (fg.type == SEPARATOR)
ident = CustomCellSeparator;
}
return cellView;
NSTableCellView *v = [outlineView makeViewWithIdentifier:ident owner:self];
if (v) return v;
if (ident == CustomCellName) return [NameColumnCell new];
if (ident == CustomCellRefresh) return [RefreshColumnCell new];
if (ident == CustomCellSeparator) return [SeparatorColumnCell new];
return nil;
}
/// @return User clicked cell item or @c nil if user did not click on a cell.
- (FeedGroup*)clickedItem {
NSOutlineView *ov = self.view.outline;
return [(NSTreeNode*)[ov itemAtRow:ov.clickedRow] representedObject];
}
#pragma mark - Keyboard Commands: undo, redo, copy, enter
/// Also look for commands right click menu of outline view
- (void)keyDown:(NSEvent *)event {
if (![self.view.outline.menu performKeyEquivalent:event]) {
[super keyDown:event];
}
}
/// Returning @c NO will result in a Action-Not-Available-Buzzer sound
- (BOOL)respondsToSelector:(SEL)aSelector {
if (aSelector == @selector(undo:))
return [self.undoManager canUndo] && self.undoManager.groupingLevel == 0 && ![FeedDownload isUpdating];
if (aSelector == @selector(redo:))
return [self.undoManager canRedo] && self.undoManager.groupingLevel == 0 && ![FeedDownload isUpdating];
if (aSelector == @selector(copy:) || aSelector == @selector(enterPressed:)) {
BOOL outlineHasFocus = [[self.view.window firstResponder] isKindOfClass:[NSOutlineView class]];
BOOL hasSelection = (self.dataStore.selectedNodes.count > 0);
if (!outlineHasFocus || !hasSelection)
if (aSelector == @selector(copy:) || aSelector == @selector(remove:))
return self.dataStore.selectedNodes.count > 0;
if (aSelector == @selector(editSelectedItem)) {
FeedGroup *chosen = [self clickedItem];
if (!chosen) chosen = self.dataStore.selectedObjects.firstObject;
if (chosen && chosen.type != SEPARATOR)
return YES; // can edit only if selection is not a separator
return NO;
if (aSelector == @selector(copy:))
return YES;
// can edit only if selection is not a separator
return (((FeedGroup*)self.dataStore.selectedNodes.firstObject.representedObject).type != SEPARATOR);
}
return [super respondsToSelector:aSelector];
}
@@ -459,11 +484,6 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
[self saveWithUnpredictableChange];
}
/// User pressed enter; open edit dialog for selected item.
- (void)enterPressed:(id)sender {
[self showModalForFeedGroup:self.dataStore.selectedObjects.firstObject isGroupEdit:YES]; // yes will be overwritten anyway
}
/// Copy human readable description of selected nodes to clipboard.
- (void)copy:(id)sender {
NSMutableString *str = [[NSMutableString alloc] init];

View File

@@ -1,250 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14313.18"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="SettingsFeeds">
<connections>
<outlet property="dataStore" destination="JPf-gH-wxm" id="9qy-D6-L4R"/>
<outlet property="outlineView" destination="wP9-Vd-f79" id="nKf-fc-7Np"/>
<outlet property="spinner" destination="fos-vP-s2s" id="zZp-Op-ftK"/>
<outlet property="spinnerLabel" destination="44U-lx-hnq" id="GGB-H5-7LV"/>
<outlet property="view" destination="zfc-Ie-Sdx" id="65R-bK-FDI"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<treeController mode="entity" entityName="FeedGroup" fetchPredicateFormat="parent == nil" automaticallyPreparesContent="YES" childrenKeyPath="children" leafKeyPath="type" id="JPf-gH-wxm"/>
<customView id="zfc-Ie-Sdx" userLabel="View">
<rect key="frame" x="0.0" y="0.0" width="320" height="327"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView fixedFrame="YES" autohidesScrollers="YES" horizontalLineScroll="20" horizontalPageScroll="10" verticalLineScroll="20" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="f1F-Mv-bod">
<rect key="frame" x="0.0" y="20" width="320" height="307"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<clipView key="contentView" ambiguous="YES" id="oIL-kH-Krb">
<rect key="frame" x="1" y="0.0" width="318" height="306"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<outlineView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="firstColumnOnly" alternatingRowBackgroundColors="YES" columnReordering="NO" columnResizing="NO" autosaveColumns="NO" typeSelect="NO" rowHeight="18" rowSizeStyle="automatic" headerView="uEa-oG-fr0" viewBased="YES" indentationPerLevel="15" outlineTableColumn="3Eq-bQ-AGJ" id="wP9-Vd-f79">
<rect key="frame" x="0.0" y="0.0" width="318" height="283"/>
<autoresizingMask key="autoresizingMask"/>
<size key="intercellSpacing" width="3" height="2"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
<tableColumns>
<tableColumn identifier="NameColumn" editable="NO" width="262" minWidth="40" maxWidth="10000" id="3Eq-bQ-AGJ">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Name">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="gLU-zA-WTf">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES"/>
<prototypeCellViews>
<tableCellView identifier="cellFeed" id="066-5N-dID" userLabel="Feed">
<rect key="frame" x="1" y="1" width="262" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="qHf-yW-Ks4" userLabel="img">
<rect key="frame" x="1" y="1" width="16" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSActionTemplate" id="NRq-gp-RJ5"/>
</imageView>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" allowsExpansionToolTips="YES" translatesAutoresizingMaskIntoConstraints="NO" id="n7N-Pk-80l" userLabel="str">
<rect key="frame" x="23" y="0.0" width="241" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="wHQ-uQ-pww">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<connections>
<outlet property="imageView" destination="qHf-yW-Ks4" id="LBQ-xL-3vr"/>
<outlet property="textField" destination="n7N-Pk-80l" id="ei3-ux-jga"/>
</connections>
</tableCellView>
<tableCellView identifier="cellSeparator" id="tjK-7n-uRz" userLabel="Separator">
<rect key="frame" x="1" y="21" width="262" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<customView fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="G7f-uh-abm" userLabel="img" customClass="DrawSeparator">
<rect key="frame" x="0.0" y="0.0" width="262" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</customView>
</subviews>
</tableCellView>
</prototypeCellViews>
<connections>
<binding destination="JPf-gH-wxm" name="value" keyPath="arrangedObjects" id="HfC-oh-cnN">
<dictionary key="options">
<bool key="NSConditionallySetsEditable" value="YES"/>
<bool key="NSCreatesSortDescriptor" value="NO"/>
</dictionary>
</binding>
</connections>
</tableColumn>
<tableColumn identifier="RefreshColumn" editable="NO" width="50" minWidth="40" maxWidth="100" id="N3k-JC-Czy">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Refresh">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="bQw-cL-PQs">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<prototypeCellViews>
<tableCellView identifier="cellRefresh" id="Qyt-7v-t3G" userLabel="cellView">
<rect key="frame" x="266" y="1" width="50" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" allowsExpansionToolTips="YES" translatesAutoresizingMaskIntoConstraints="NO" id="17I-Oo-q9s" userLabel="str">
<rect key="frame" x="-1" y="0.0" width="52" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" alignment="right" title="21042s" id="ZlY-7o-ZTa">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<connections>
<outlet property="textField" destination="17I-Oo-q9s" id="i0p-KF-aE8"/>
</connections>
</tableCellView>
</prototypeCellViews>
<connections>
<binding destination="JPf-gH-wxm" name="value" keyPath="arrangedObjects" id="aq0-dy-F1G">
<dictionary key="options">
<bool key="NSConditionallySetsEditable" value="YES"/>
<bool key="NSCreatesSortDescriptor" value="NO"/>
</dictionary>
</binding>
</connections>
</tableColumn>
</tableColumns>
<connections>
<action trigger="doubleAction" selector="doubleClickOutlineView:" target="-2" id="nqp-9A-7ac"/>
<outlet property="dataSource" destination="-2" id="3Iv-Pa-dvh"/>
<outlet property="delegate" destination="-2" id="eCu-Hd-4Ct"/>
</connections>
</outlineView>
</subviews>
</clipView>
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="xsa-8D-Emz">
<rect key="frame" x="1" y="7" width="0.0" height="16"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="NO" id="p12-eT-ex6">
<rect key="frame" x="-15" y="23" width="16" height="0.0"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<tableHeaderView key="headerView" id="uEa-oG-fr0">
<rect key="frame" x="0.0" y="0.0" width="318" height="23"/>
<autoresizingMask key="autoresizingMask"/>
</tableHeaderView>
</scrollView>
<button toolTip="Create new feed item" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="3dn-fo-MZT">
<rect key="frame" x="0.0" y="-1" width="25" height="23"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="smallSquare" alternateTitle="Add feed" bezelStyle="smallSquare" image="NSAddTemplate" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="mfH-K0-yNS">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent">n</string>
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</buttonCell>
<connections>
<action selector="addFeed:" target="-2" id="iWE-sh-KY1"/>
<binding destination="JPf-gH-wxm" name="enabled" keyPath="canInsert" id="TJb-gv-6gO"/>
</connections>
</button>
<button toolTip="Delete item" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Xxm-75-8K8">
<rect key="frame" x="24" y="-1" width="25" height="23"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="smallSquare" alternateTitle="Remove Feed" bezelStyle="smallSquare" image="NSRemoveTemplate" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="6iS-E4-jzq">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
CA
</string>
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</buttonCell>
<connections>
<action selector="remove:" target="-2" id="JeR-iq-Gjb"/>
<binding destination="JPf-gH-wxm" name="enabled" keyPath="canRemove" id="XYY-gx-tiN"/>
</connections>
</button>
<button toolTip="Add new grouping folder" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="jPg-sh-1Az">
<rect key="frame" x="64" y="-1" width="25" height="23"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="smallSquare" alternateTitle="Add group" bezelStyle="smallSquare" image="NSPathTemplate" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="rPk-c8-lMe">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent">g</string>
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</buttonCell>
<connections>
<action selector="addGroup:" target="-2" id="V3k-2H-4Kc"/>
<binding destination="JPf-gH-wxm" name="enabled" keyPath="canInsert" id="JCF-ey-YUL"/>
</connections>
</button>
<button toolTip="Add new line separator" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kn9-pd-A47">
<rect key="frame" x="88" y="-1" width="25" height="23"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="smallSquare" title="---" alternateTitle="Add separator" bezelStyle="smallSquare" image="NSPathTemplate" alignment="center" lineBreakMode="truncatingTail" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="r9B-nl-XkX">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="addSeparator:" target="-2" id="dVQ-ge-moI"/>
<binding destination="JPf-gH-wxm" name="enabled" keyPath="canInsert" id="2aK-XU-RUD"/>
</connections>
</button>
<button toolTip="Import or Export data" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6ul-3K-fOy">
<rect key="frame" x="128" y="-1" width="25" height="23"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="smallSquare" alternateTitle="Export" bezelStyle="smallSquare" image="NSShareTemplate" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="nrA-7c-1sL">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="shareMenu:" target="-2" id="JJq-7D-Bti"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="44U-lx-hnq">
<rect key="frame" x="166" y="4" width="141" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="&lt;string&gt;" id="yyA-K6-M3v">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="systemGrayColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<progressIndicator wantsLayer="YES" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" controlSize="small" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="fos-vP-s2s">
<rect key="frame" x="301" y="3" width="16" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
</progressIndicator>
</subviews>
<point key="canvasLocation" x="27" y="882.5"/>
</customView>
<viewController id="TaZ-4L-TdU" customClass="ModalFeedEdit"/>
</objects>
<resources>
<image name="NSActionTemplate" width="14" height="14"/>
<image name="NSAddTemplate" width="11" height="11"/>
<image name="NSPathTemplate" width="16" height="10"/>
<image name="NSRemoveTemplate" width="11" height="11"/>
<image name="NSShareTemplate" width="11" height="16"/>
</resources>
</document>

View File

@@ -0,0 +1,48 @@
//
// The MIT License (MIT)
// Copyright (c) 2019 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import <Cocoa/Cocoa.h>
@class SettingsFeeds;
@interface SettingsFeedsView : NSView
@property (weak) IBOutlet NSOutlineView *outline;
@property (weak) IBOutlet NSTextField *status;
@property (weak) IBOutlet NSProgressIndicator *spinner;
- (instancetype)initWithController:(SettingsFeeds*)delegate NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithFrame:(NSRect)frameRect NS_UNAVAILABLE;
- (nullable instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE;
@end
@interface NameColumnCell : NSTableCellView
extern NSUserInterfaceItemIdentifier const CustomCellName;
@end
@interface RefreshColumnCell : NSTableCellView
extern NSUserInterfaceItemIdentifier const CustomCellRefresh;
@end
@interface SeparatorColumnCell : NSTableCellView
extern NSUserInterfaceItemIdentifier const CustomCellSeparator;
@end

View File

@@ -0,0 +1,254 @@
//
// The MIT License (MIT)
// Copyright (c) 2019 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "SettingsFeedsView.h"
#import "StoreCoordinator.h"
#import "FeedGroup+Ext.h"
#import "FeedMeta+Ext.h"
#import "DrawImage.h"
#import "SettingsFeeds.h"
#import "NSDate+Ext.h"
#import "NSView+Ext.h"
@interface SettingsFeedsView()
@property (weak) SettingsFeeds *controller;
@end
@implementation SettingsFeedsView
- (instancetype)initWithController:(SettingsFeeds*)delegate {
self = [super initWithFrame:NSZeroRect];
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.menu = [self generateCommandsMenu];
[self.outline.menu.itemArray makeObjectsPerformSelector:@selector(setTarget:) withObject:delegate];
CGFloat x = [self generateButtons]; // uses self.controller and self.outline
// Setup status text field ('Next update in X min.' or 'Updating X feeds ...')
self.status = [[[[[[NSView label:@""] small] gray] textCenter] placeIn:self x:x + PAD_L y:3.5] sizeToRight:PAD_L];
self.spinner = [[NSView activitySpinner] placeIn:self xRight:2 y:2];
}
return self;
}
/**
Setup @c self.outline
@note Requires @c self.controller
*/
- (NSOutlineView*)generateOutlineView {
// Generate outline view
NSOutlineView *o = [[NSOutlineView alloc] init];
o.columnAutoresizingStyle = NSTableViewFirstColumnOnlyAutoresizingStyle;
o.usesAlternatingRowBackgroundColors = YES;
o.allowsMultipleSelection = YES;
o.allowsColumnReordering = NO;
o.allowsColumnSelection = NO;
o.allowsEmptySelection = YES;
//o.intercellSpacing = NSMakeSize(3, 2);
o.rowHeight = 18;
[self setOutlineColumns:o];
// Setup action and bindings
SettingsFeeds *sf = self.controller;
o.delegate = sf;
o.dataSource = sf;
o.target = sf;
o.doubleAction = @selector(doubleClickOutlineView:);
[o bind:NSContentBinding toObject:sf.dataStore withKeyPath:@"arrangedObjects" options:nil]; // @{NSAlwaysPresentsApplicationModalAlertsBindingOption:@YES}
[o bind:NSSelectionIndexPathsBinding toObject:sf.dataStore withKeyPath:@"selectionIndexPaths" options:nil];
return o;
}
/// Generate table columns 'Name' and 'Refresh'
- (void)setOutlineColumns:(NSOutlineView*)outline {
NSTableColumn *colName = [[NSTableColumn alloc] initWithIdentifier:CustomCellName];
colName.title = NSLocalizedString(@"Name", nil);
colName.width = 10000;
colName.maxWidth = 10000;
colName.resizingMask = NSTableColumnAutoresizingMask;
[outline addTableColumn:colName];
NSTableColumn *colRefresh = [[NSTableColumn alloc] initWithIdentifier:CustomCellRefresh];
colRefresh.title = NSLocalizedString(@"Refresh", nil);
colRefresh.width = 50;
colRefresh.resizingMask = NSTableColumnNoResizing;
[outline addTableColumn:colRefresh];
for (NSTableColumn *col in outline.tableColumns) {
col.headerCell.title = [NSString stringWithFormat:@" %@", col.title];
NSDictionary *attr = @{ NSFontAttributeName: [NSFont systemFontOfSize:NSFont.smallSystemFontSize weight:NSFontWeightMedium] };
col.headerCell.attributedStringValue = [[NSAttributedString alloc] initWithString:col.title attributes:attr];
}
outline.outlineTableColumn = colName;
}
/// Setup right click menu (also used for hotkeys).
- (NSMenu*)generateCommandsMenu {
NSMenu *m = [[NSMenu alloc] initWithTitle:@""];
[m addItemWithTitle:NSLocalizedString(@"Edit Item", nil) action:@selector(editSelectedItem) keyEquivalent:[NSString stringWithFormat:@"%c", NSCarriageReturnCharacter]].keyEquivalentModifierMask = 0;
[m addItemWithTitle:NSLocalizedString(@"Delete Item(s)", nil) action:@selector(remove:) keyEquivalent:[NSString stringWithFormat:@"%c", NSBackspaceCharacter]];
[m addItem:[NSMenuItem separatorItem]]; // index: 2
[m addItemWithTitle:NSLocalizedString(@"New Feed", nil) action:@selector(addFeed) keyEquivalent:@"n"];
[m addItemWithTitle:NSLocalizedString(@"New Group", nil) action:@selector(addGroup) keyEquivalent:@"g"];
[m addItemWithTitle:NSLocalizedString(@"New Separator", nil) action:@selector(addSeparator) keyEquivalent:@""];
[m addItem:[NSMenuItem separatorItem]]; // index: 6
[m addItemWithTitle:NSLocalizedString(@"Import Feeds …", nil) action:@selector(openImportDialog) keyEquivalent:@""];
[m addItemWithTitle:NSLocalizedString(@"Export Feeds …", nil) action:@selector(openExportDialog) keyEquivalent:@""];
[m addItem:[NSMenuItem separatorItem]]; // index: 9
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
[m addItemWithTitle:NSLocalizedString(@"Undo", nil) action:@selector(undo:) keyEquivalent:@"z"];
[m addItemWithTitle:NSLocalizedString(@"Redo", nil) action:@selector(redo:) keyEquivalent:@"Z"];
#pragma clang diagnostic pop
return m;
}
/**
Setup the bottom button bar. (e.g., add, remove, edit, export, import, etc.)
@note Requires @c self.controller and @c self.outline
@return Max x-value of last button frame
*/
- (CGFloat)generateButtons {
NSButton *add = [[NSView buttonImageSquare:NSImageNameAddTemplate] tooltip:NSLocalizedString(@"Add new item", nil)];
NSButton *del = [[NSView buttonImageSquare:NSImageNameRemoveTemplate] tooltip:NSLocalizedString(@"Delete selected item(s)", nil)];
NSButton *share = [[NSView buttonImageSquare:NSImageNameShareTemplate] tooltip:NSLocalizedString(@"Import or export data", nil)];
[self button:add copyActions:3 to:5];
[self button:del copyActions:1 to:1];
[self button:share copyActions:7 to:8]; // TODO: Add menus for online sync? email export? etc.
[add placeIn:self x:0 y:0];
[del placeIn:self x:24 y:0];
[share placeIn:self x:2 * 24 + PAD_L y:0];
NSTreeController *tc = self.controller.dataStore;
[add bind:NSEnabledBinding toObject:tc withKeyPath:@"canInsert" options:nil];
[del bind:NSEnabledBinding toObject:tc withKeyPath:@"canRemove" options:nil];
return NSMaxX(share.frame);
}
/**
Duplicate right click menu actions to button
@note Requires @c self.outline
*/
- (void)button:(NSButton*)btn copyActions:(NSInteger)start to:(NSInteger)end {
if (start < 0 || start > end || end >= self.outline.menu.numberOfItems) {
NSAssert(NO, @"Invalid index, can't copy command menu items.");
return;
}
if (start == end) {
// copy menu item action to button action
NSMenuItem *source = [self.outline.menu itemAtIndex:start];
[btn action:source.action target:source.target];
btn.keyEquivalent = source.keyEquivalent;
btn.keyEquivalentModifierMask = source.keyEquivalentModifierMask;
} else {
// create drop down menu with all options
btn.menu = [[NSMenu alloc] initWithTitle:@""];
[btn action:@selector(openButtonMenu:) target:self];
for (NSInteger i = start; i <= end; i++) {
[btn.menu addItem:[[self.outline.menu itemAtIndex:i] copy]];
}
}
}
/// Show drop down menu even for left click.
- (void)openButtonMenu:(NSButton*)sender {
//[NSMenu popUpContextMenu:sender.menu withEvent:[NSApp currentEvent] forView:sender];
[sender.menu popUpMenuPositioningItem:nil atLocation:NSMakePoint(0, NSHeight(sender.frame)) inView:sender];
}
@end
#pragma mark - Custom Outline View Cells -
/**
First outline view column, with textfield and feed icon
*/
@implementation NameColumnCell
/// Identifier for cell with @c .imageView (feed icon) and @c .textField (feed title)
NSUserInterfaceItemIdentifier const CustomCellName = @"NameColumnCell";
- (instancetype)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
self.identifier = CustomCellName;
self.imageView = [[NSView imageView:nil size:16] placeIn:self x:1 yTop:1];
self.textField = [[[NSView label:@""] placeIn:self x:25 yTop:0] sizeToRight:0];
return self;
}
- (void)setObjectValue:(FeedGroup*)fg {
self.textField.objectValue = fg.name;
self.imageView.image = fg.iconImage16;
}
@end
/**
Second outline view column, either refresh string or empty
*/
@implementation RefreshColumnCell
/// Identifier for cell with @c .textField (refresh string or empty)
NSUserInterfaceItemIdentifier const CustomCellRefresh = @"RefreshColumnCell";
- (instancetype)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
self.identifier = CustomCellRefresh;
self.textField = [[[[NSView label:@""] textRight] placeIn:self x:0 yTop:0] sizeToRight:0];
return self;
}
- (void)setObjectValue:(FeedGroup*)fg {
NSString *str = [fg refreshString];
self.textField.objectValue = str;
// TODO: accessibility title: readable interval string
self.textField.textColor = (str.length > 1 ? [NSColor controlTextColor] : [NSColor disabledControlTextColor]);
}
@end
/**
First outline view column, separator line
*/
@implementation SeparatorColumnCell
/// Identifier for cell with line separator
NSUserInterfaceItemIdentifier const CustomCellSeparator = @"SeparatorColumnCell";
- (instancetype)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
self.identifier = CustomCellSeparator;
[[[[DrawSeparator alloc] initWithFrame:self.frame] placeIn:self x:0 y:0] sizableWidthAndHeight];
return self;
}
- (void)setObjectValue:(FeedGroup*)fg { /* do nothing */ }
@end

View File

@@ -23,5 +23,7 @@
#import <Cocoa/Cocoa.h>
@interface SettingsGeneral : NSViewController
@property (assign) IBOutlet NSView *appearanceView;
- (void)fixCache:(NSButton *)sender;
- (void)changeHttpApplication:(NSPopUpButton *)sender;
- (void)changeDefaultRSSReader:(NSPopUpButton *)sender;
@end

View File

@@ -21,33 +21,32 @@
// SOFTWARE.
#import "SettingsGeneral.h"
#import "AppHook.h"
#import "BarStatusItem.h"
#import "UserPrefs.h"
#import "StoreCoordinator.h"
#import "Constants.h"
#import "SettingsGeneralView.h"
@interface SettingsGeneral()
@property (weak) IBOutlet NSPopUpButton *popupHttpApplication;
@property (weak) IBOutlet NSPopUpButton *popupDefaultRSSReader;
@property (strong) IBOutlet SettingsGeneralView *view; // override
@end
@implementation SettingsGeneral
@dynamic view;
- (void)viewDidLoad {
[super viewDidLoad];
- (void)loadView {
self.view = [[SettingsGeneralView alloc] initWithController:self];
// Default http application for opening the feed urls
[self generateMenuForPopup:self.popupHttpApplication withScheme:@"https"];
[self.popupHttpApplication insertItemWithTitle:NSLocalizedString(@"System Default", @"Default web browser application") atIndex:0];
[self selectBundleID:[UserPrefs getHttpApplication] inPopup:self.popupHttpApplication];
[self generateMenuForPopup:self.view.popupHttpApplication withScheme:@"https"];
[self.view.popupHttpApplication insertItemWithTitle:NSLocalizedString(@"System Default", @"Default web browser application") atIndex:0];
[self selectBundleID:[UserPrefs getHttpApplication] inPopup:self.view.popupHttpApplication];
// Default RSS Reader application
[self generateMenuForPopup:self.popupDefaultRSSReader withScheme:@"feed"];
[self selectBundleID:[self defaultBundleIdForScheme:@"feed"] inPopup:self.popupDefaultRSSReader];
[self generateMenuForPopup:self.view.popupDefaultRSSReader withScheme:@"feed"];
[self selectBundleID:[self defaultBundleIdForScheme:@"feed"] inPopup:self.view.popupDefaultRSSReader];
}
#pragma mark - UI interaction with IBAction
- (IBAction)fixCache:(NSButton *)sender {
- (void)fixCache:(NSButton *)sender {
NSUInteger deleted = [StoreCoordinator deleteUnreferenced];
[StoreCoordinator restoreFeedIndexPaths];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
@@ -58,15 +57,11 @@
[alert runModal];
}
- (IBAction)changeMenuBarIconSetting:(NSButton*)sender {
[[(AppHook*)NSApp statusItem] updateBarIcon];
}
- (IBAction)changeHttpApplication:(NSPopUpButton *)sender {
- (void)changeHttpApplication:(NSPopUpButton *)sender {
[UserPrefs setHttpApplication:sender.selectedItem.representedObject];
}
- (IBAction)changeDefaultRSSReader:(NSPopUpButton *)sender {
- (void)changeDefaultRSSReader:(NSPopUpButton *)sender {
if ([self setDefaultRSSApplication:sender.selectedItem.representedObject] == NO) {
// in case anything went wrong, restore previous selection
[self selectBundleID:[self defaultBundleIdForScheme:@"feed"] inPopup:sender];

View File

@@ -1,487 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14313.18"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="SettingsGeneral">
<connections>
<outlet property="appearanceView" destination="Wwh-0p-tPi" id="51l-Wp-k0J"/>
<outlet property="popupDefaultRSSReader" destination="tJe-jL-nUu" id="DUq-ti-Drf"/>
<outlet property="popupHttpApplication" destination="BcN-gW-jBg" id="X2r-Nn-igN"/>
<outlet property="view" destination="mbb-wD-pDD" id="Syb-4w-ekh"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<userDefaultsController representsSharedInstance="YES" id="iU7-KA-nY5"/>
<customView id="mbb-wD-pDD" userLabel="View">
<rect key="frame" x="0.0" y="0.0" width="320" height="327"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="QwE-M7-q2R">
<rect key="frame" x="151" y="13" width="155" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="push" title="Fix Cache" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="ady-2s-Ggm">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="fixCache:" target="-2" id="gbM-hA-UVF"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2sG-NO-OJz">
<rect key="frame" x="18" y="288" width="133" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Open URLs with:" id="vNb-i3-dvE">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="BcN-gW-jBg">
<rect key="frame" x="155" y="283" width="148" height="26"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="-- list --" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="qW6-vv-pdE" id="R91-En-pHg">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="M0i-AE-1LS">
<items>
<menuItem title="-- list --" state="on" id="qW6-vv-pdE"/>
</items>
</menu>
</popUpButtonCell>
<connections>
<action selector="changeHttpApplication:" target="-2" id="Cyb-ab-VNu"/>
</connections>
</popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TC5-cu-zUi">
<rect key="frame" x="18" y="261" width="133" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Default RSS Reader:" id="wvK-Oz-Kk3">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="tJe-jL-nUu">
<rect key="frame" x="155" y="256" width="148" height="26"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="-- list --" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="4Gg-hZ-mh4" id="saR-9h-TWE">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="8gY-aZ-fCb">
<items>
<menuItem title="-- list --" state="on" id="4Gg-hZ-mh4"/>
</items>
</menu>
</popUpButtonCell>
<connections>
<action selector="changeDefaultRSSReader:" target="-2" id="ul1-1K-oJb"/>
</connections>
</popUpButton>
</subviews>
<point key="canvasLocation" x="33" y="-153.5"/>
</customView>
<customView id="Wwh-0p-tPi" userLabel="Appearance View">
<rect key="frame" x="0.0" y="0.0" width="320" height="327"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="c5z-lV-vas">
<rect key="frame" x="18" y="241" width="22" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="fhM-ZU-dqf">
<behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.globalUpdateAll" id="ObW-85-BJh">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<integer key="NSNullPlaceholder" value="1"/>
</dictionary>
</binding>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="qwe-HI-3qV">
<rect key="frame" x="18" y="219" width="22" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="PFz-Ow-r4F">
<behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.globalOpenUnread" id="1gJ-DS-qv0">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<integer key="NSNullPlaceholder" value="1"/>
</dictionary>
</binding>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ROH-bm-RYb">
<rect key="frame" x="44" y="219" width="22" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="z0G-PF-7X4">
<behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.groupOpenUnread" id="IVo-sw-mcs">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<integer key="NSNullPlaceholder" value="1"/>
</dictionary>
</binding>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="a64-GA-uqO">
<rect key="frame" x="70" y="219" width="22" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="5lC-Kd-cxG">
<behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.feedOpenUnread" id="3NW-RY-kOa">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<integer key="NSNullPlaceholder" value="1"/>
</dictionary>
</binding>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="IAr-hA-5en">
<rect key="frame" x="18" y="197" width="22" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="pfa-9f-faM">
<behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.globalMarkRead" id="ZwQ-Dn-ocg">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<integer key="NSNullPlaceholder" value="1"/>
</dictionary>
</binding>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="HVG-vG-GIU">
<rect key="frame" x="44" y="197" width="22" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="oje-pE-GW8">
<behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.groupMarkRead" id="hya-HG-RtW">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<integer key="NSNullPlaceholder" value="1"/>
</dictionary>
</binding>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Gtu-6h-y3W">
<rect key="frame" x="70" y="197" width="22" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="t0S-h2-fFL">
<behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.feedMarkRead" id="ILe-xm-ITh">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<integer key="NSNullPlaceholder" value="1"/>
</dictionary>
</binding>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="utE-1U-oPJ">
<rect key="frame" x="18" y="175" width="22" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="HwB-CY-h1x">
<behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.globalMarkUnread" id="vc4-oK-5yY">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<integer key="NSNullPlaceholder" value="1"/>
</dictionary>
</binding>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6wd-KD-Vq2">
<rect key="frame" x="44" y="175" width="22" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="9UH-v7-h2R">
<behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.groupMarkUnread" id="bUj-qA-Wnt">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<integer key="NSNullPlaceholder" value="1"/>
</dictionary>
</binding>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="upF-tg-Zfs">
<rect key="frame" x="70" y="175" width="22" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="8d6-wr-mdT">
<behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.feedMarkUnread" id="0ES-Df-AI3">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<integer key="NSNullPlaceholder" value="1"/>
</dictionary>
</binding>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="E0O-SU-lzt">
<rect key="frame" x="18" y="153" width="22" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="Vyz-7h-H3B">
<behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="changeMenuBarIconSetting:" target="-2" id="0aa-UD-1gK"/>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.globalUnreadCount" id="2hk-H9-Oac">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<integer key="NSNullPlaceholder" value="1"/>
</dictionary>
</binding>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vye-pf-bkq">
<rect key="frame" x="44" y="153" width="22" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="dRK-ge-IL7">
<behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.groupUnreadCount" id="y2V-ws-n4p">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<integer key="NSNullPlaceholder" value="1"/>
</dictionary>
</binding>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="fmJ-ac-dcb">
<rect key="frame" x="70" y="153" width="22" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="Nwc-Rx-Wbu">
<behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.feedUnreadCount" id="OhX-uY-UA2">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<integer key="NSNullPlaceholder" value="1"/>
</dictionary>
</binding>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ijX-fP-IQG">
<rect key="frame" x="70" y="131" width="22" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="CiI-wC-qa8">
<behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.feedTickMark" id="Aia-Br-J5d">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<integer key="NSNullPlaceholder" value="1"/>
</dictionary>
</binding>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ibh-Ob-COI">
<rect key="frame" x="96" y="242" width="206" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Update all feeds" id="mqk-td-Ely">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="MAh-pk-fPm">
<rect key="frame" x="96" y="220" width="206" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Open all unread" id="3Wk-Ys-6Dg">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="fue-A5-JZt">
<rect key="frame" x="96" y="198" width="206" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Mark all read" id="qYo-AP-Ima">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="1d6-T8-AME">
<rect key="frame" x="96" y="176" width="206" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Mark all unread" id="sp9-DH-f2e">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="hQY-zw-PVG">
<rect key="frame" x="96" y="154" width="206" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Number of unread items" id="fya-vs-MV6">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="QZ1-Mq-gky">
<rect key="frame" x="96" y="132" width="206" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Tick mark unread items" id="IYd-BL-Sc8">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<customView toolTip="Show in menu bar" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0hm-pR-8ua" customClass="SettingsIconGlobal">
<rect key="frame" x="20" y="289" width="18" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="color" keyPath="color">
<color key="value" name="labelColor" catalog="System" colorSpace="catalog"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="roundness">
<real key="value" value="40"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</customView>
<customView toolTip="Show in group menu" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="lfC-et-W8m" customClass="SettingsIconGroup">
<rect key="frame" x="46" y="289" width="18" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="color" keyPath="color">
<color key="value" name="labelColor" catalog="System" colorSpace="catalog"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="roundness">
<real key="value" value="40"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</customView>
<customView toolTip="Show in feed menu" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="7Gn-Uq-6lG" customClass="RSSIcon">
<rect key="frame" x="72" y="289" width="18" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="roundness">
<real key="value" value="40"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="color" keyPath="color">
<color key="value" name="labelColor" catalog="System" colorSpace="catalog"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</customView>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Jug-kR-uf7">
<rect key="frame" x="18" y="263" width="22" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="JGj-fV-11r">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="changeMenuBarIconSetting:" target="-2" id="QXH-tb-Egy"/>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.tintMenuBarIcon" id="1N3-KQ-wbC">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<integer key="NSNullPlaceholder" value="1"/>
</dictionary>
</binding>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="i0v-Fd-POW">
<rect key="frame" x="70" y="109" width="22" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" inset="2" id="Wsi-Zb-ug5">
<behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.feedShortNames" id="dny-kJ-AZM">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<integer key="NSNullPlaceholder" value="0"/>
</dictionary>
</binding>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="saw-1G-eHz">
<rect key="frame" x="70" y="87" width="22" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" inset="2" id="8LB-X9-2tl">
<behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="iU7-KA-nY5" name="value" keyPath="values.feedLimitArticles" id="Hd2-Pr-n6T">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<integer key="NSNullPlaceholder" value="0"/>
</dictionary>
</binding>
</connections>
</button>
<textField toolTip="Truncate article title after 60 characters" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="p7p-HI-ePS">
<rect key="frame" x="96" y="110" width="206" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Short article names" id="S8K-hH-Ssj">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField toolTip="Display at most 40 articles in feed menu" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="b3d-WG-MiJ">
<rect key="frame" x="96" y="88" width="206" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Limit number of articles" id="vjz-OI-S9j">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="X7N-1T-bmw">
<rect key="frame" x="96" y="264" width="206" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Tint menu bar icon on unread" id="edV-Xi-cpf">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<point key="canvasLocation" x="404" y="-154"/>
</customView>
</objects>
</document>

View File

@@ -0,0 +1,35 @@
//
// The MIT License (MIT)
// Copyright (c) 2019 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import <Cocoa/Cocoa.h>
@class SettingsGeneral;
@interface SettingsGeneralView : NSView
@property (weak) IBOutlet NSPopUpButton* popupHttpApplication;
@property (weak) IBOutlet NSPopUpButton* popupDefaultRSSReader;
- (instancetype)initWithController:(SettingsGeneral*)controller NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithFrame:(NSRect)frameRect NS_UNAVAILABLE;
- (nullable instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE;
@end

View File

@@ -0,0 +1,51 @@
//
// The MIT License (MIT)
// Copyright (c) 2019 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "SettingsGeneralView.h"
#import "SettingsGeneral.h"
#import "NSView+Ext.h"
@implementation SettingsGeneralView
- (instancetype)initWithController:(SettingsGeneral*)controller {
self = [super initWithFrame:NSZeroRect];
NSArray *lbls = @[NSLocalizedString(@"Open URLs with:", nil),
NSLocalizedString(@"Default RSS Reader:", nil)];
NSView *labels = [[NSView labelColumn:lbls rowHeight:HEIGHT_POPUP padding:PAD_M] placeIn:self x:PAD_WIN yTop:PAD_WIN];
CGFloat x = NSMaxX(labels.frame) + PAD_S;
self.popupHttpApplication = [[self createPopup:x top: PAD_WIN + 1] action:@selector(changeHttpApplication:) target:controller];
self.popupDefaultRSSReader = [[self createPopup:x top: YFromTop(self.popupHttpApplication) + PAD_M] action:@selector(changeDefaultRSSReader:) target:controller];
// Add fix cache button
[[[[NSView button:NSLocalizedString(@"Fix Cache", nil)] action:@selector(fixCache:) target:controller]
tooltip:NSLocalizedString(@"Will remove unreferenced feed entries", nil)] placeIn:self xRight:PAD_WIN y:PAD_WIN];
return self;
}
/// Helper method to create sizable popup button
- (NSPopUpButton*)createPopup:(CGFloat)x top:(CGFloat)y {
return [[[NSView popupButton:0] placeIn:self x:x yTop:y] sizeToRight:PAD_WIN];
}
@end

View File

@@ -23,7 +23,6 @@
#import <Cocoa/Cocoa.h>
@interface ModalSheet : NSPanel
@property (readonly) BOOL didCloseAndSave;
@property (readonly) BOOL didCloseAndCancel;
- (instancetype)initWithContentRect:(NSRect)contentRect styleMask:(NSWindowStyleMask)style backing:(NSBackingStoreType)backingStoreType defer:(BOOL)flag NS_UNAVAILABLE;

View File

@@ -0,0 +1,109 @@
//
// The MIT License (MIT)
// Copyright (c) 2018 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "ModalSheet.h"
#import "NSView+Ext.h"
@interface ModalSheet()
@property (assign) BOOL respondToShouldClose;
@end
@implementation ModalSheet
/// Designated initializer. 'Done' and 'Cancel' buttons will be added automatically.
- (instancetype)initWithView:(NSView*)content {
static const NSInteger minWidth = 320;
static const NSInteger maxWidth = 1200;
static const CGFloat contentOffsetY = PAD_WIN + HEIGHT_BUTTON + PAD_L;
NSInteger w = [[NSUserDefaults standardUserDefaults] integerForKey:@"modalSheetWidth"];
if (w < minWidth) w = minWidth;
else if (w > maxWidth) w = maxWidth;
CGFloat h = NSHeight(content.frame);
[content setFrameSize: NSMakeSize(w, h)];
// after content size, increase to window size
w += 2 * PAD_WIN;
h += PAD_WIN + contentOffsetY; // the second PAD_WIN is already in contentOffsetY
NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable | NSWindowStyleMaskFullSizeContentView;
self = [super initWithContentRect:NSMakeRect(0, 0, w, h) styleMask:style backing:NSBackingStoreBuffered defer:NO];
[content placeIn:self.contentView x:PAD_WIN y:contentOffsetY];
// Restrict resizing to width only
self.minSize = NSMakeSize(minWidth + 2 * PAD_WIN, h);
self.maxSize = NSMakeSize(maxWidth + 2 * PAD_WIN, h);
// Add default interaction buttons
NSButton *btnDone = [self createButton:NSLocalizedString(@"Done", nil) atX:PAD_WIN];
NSButton *btnCancel = [self createButton:NSLocalizedString(@"Cancel", nil) atX:w - NSMinX(btnDone.frame) + PAD_M];
btnDone.tag = 42; // mark 'Done' button
btnDone.keyEquivalent = @"\r"; // Enter / Return
btnCancel.keyEquivalent = [NSString stringWithFormat:@"%c", 0x1b]; // ESC
return self;
}
/// Helper method to create bottom-right aligned button.
- (NSButton*)createButton:(NSString*)text atX:(CGFloat)x {
return [[[NSView button:text] action:@selector(didTapButton:) target:self] placeIn:self.contentView xRight:x y:PAD_WIN];
}
/// Manually disable 'Done' button if a task is still running.
- (void)setDoneEnabled:(BOOL)accept {
((NSButton*)[self.contentView viewWithTag:42]).enabled = accept;
}
/// Sets bool for future usage
- (void)setDelegate:(id<NSWindowDelegate>)delegate {
[super setDelegate:delegate];
self.respondToShouldClose = [delegate respondsToSelector:@selector(windowShouldClose:)];
}
/**
Called after user has clicked the 'Done' (Return) or 'Cancel' (Esc) button.
In the later case set @c .didCloseAndCancel @c = @c YES
*/
- (void)didTapButton:(NSButton*)sender {
BOOL successful = (sender.tag == 42); // 'Done' button
if (successful && self.respondToShouldClose && ![self.delegate windowShouldClose:self]) {
return;
}
_didCloseAndCancel = !successful;
// Save modal view width for next time
CGFloat w = NSWidth(self.contentView.frame) - 2 * PAD_WIN;
[[NSUserDefaults standardUserDefaults] setInteger:(NSInteger)w forKey:@"modalSheetWidth"];
// Remove subviews to avoid _NSKeyboardFocusClipView issues
[self.contentView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
[self.sheetParent endSheet:self returnCode:(successful ? NSModalResponseOK : NSModalResponseCancel)];
}
/// Resize modal window by @c dy. Makes room for additional content. Use negative values to shrink window.
- (void)extendContentViewBy:(CGFloat)dy {
self.minSize = NSMakeSize(self.minSize.width, self.minSize.height + dy);
self.maxSize = NSMakeSize(self.maxSize.width, self.maxSize.height + dy);
NSRect r = self.frame;
r.size.height += dy;
[self setFrame:r display:YES animate:YES];
}
@end

View File

@@ -1,141 +0,0 @@
//
// The MIT License (MIT)
// Copyright (c) 2018 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "ModalSheet.h"
@interface ModalSheet()
@property (weak) NSButton *btnDone;
@property (assign) BOOL respondToShouldClose;
@end
@implementation ModalSheet
@synthesize didCloseAndSave = _didCloseAndSave, didCloseAndCancel = _didCloseAndCancel;
/// User did click the 'Done' button.
- (void)didTapDoneButton:(id)sender { [self closeWithResponse:NSModalResponseOK]; }
/// User did click the 'Cancel' button.
- (void)didTapCancelButton:(id)sender { [self closeWithResponse:NSModalResponseCancel]; }
/// Manually disable 'Done' button if a task is still running.
- (void)setDoneEnabled:(BOOL)accept { self.btnDone.enabled = accept; }
- (void)setDelegate:(id<NSWindowDelegate>)delegate {
[super setDelegate:delegate];
self.respondToShouldClose = [delegate respondsToSelector:@selector(windowShouldClose:)];
}
/**
Called after user has clicked the 'Done' (Return) or 'Cancel' (Esc) button.
Flags controller as being closed @c .closeInitiated @c = @c YES.
And removes all subviews (clean up).
*/
- (void)closeWithResponse:(NSModalResponse)response {
if (response == NSModalResponseOK && self.respondToShouldClose && ![self.delegate windowShouldClose:self]) {
return;
}
_didCloseAndSave = (response == NSModalResponseOK);
_didCloseAndCancel = (response != NSModalResponseOK);
// store modal view width and remove subviews to avoid _NSKeyboardFocusClipView issues
// first object is always the view of the modal dialog
CGFloat w = self.contentView.subviews.firstObject.frame.size.width;
[[NSUserDefaults standardUserDefaults] setInteger:(NSInteger)w forKey:@"modalSheetWidth"];
[self.contentView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
[self.sheetParent endSheet:self returnCode:response];
}
/**
Designated initializer for @c ModalSheet. 'Done' and 'Cancel' button will be added automatically.
@param content @c NSView will be displayed in dialog box.
*/
- (instancetype)initWithView:(NSView*)content {
static const int padWindow = 20;
static const int minWidth = 320;
static const int maxWidth = 1200;
NSInteger prevWidth = [[NSUserDefaults standardUserDefaults] integerForKey:@"modalSheetWidth"];
if (prevWidth < minWidth) prevWidth = minWidth;
else if (prevWidth > maxWidth) prevWidth = maxWidth;
NSSize contentSize = NSMakeSize(prevWidth, content.frame.size.height);
[content setFrameSize:contentSize];
NSSize wSize = NSMakeSize(contentSize.width + 2 * padWindow, contentSize.height + 2 * padWindow);
NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable | NSWindowStyleMaskFullSizeContentView;
self = [super initWithContentRect:NSMakeRect(0, 0, wSize.width, wSize.height) styleMask:style backing:NSBackingStoreBuffered defer:NO];
if (self) {
NSButton *btnDone = [NSButton buttonWithTitle:NSLocalizedString(@"Done", nil) target:self action:@selector(didTapDoneButton:)];
NSButton *btnCancel = [NSButton buttonWithTitle:NSLocalizedString(@"Cancel", nil) target:self action:@selector(didTapCancelButton:)];
btnDone.keyEquivalent = @"\r"; // Enter / Return
btnCancel.keyEquivalent = [NSString stringWithFormat:@"%c", 0x1b]; // ESC
// Make room for buttons
wSize.height += btnDone.frame.size.height;
[self setContentSize:wSize];
// Restrict resizing to width only (after setContentSize:)
self.minSize = NSMakeSize(minWidth + 2 * padWindow, wSize.height);
self.maxSize = NSMakeSize(maxWidth + 2 * padWindow, wSize.height);
// Content view (set origin after setContentSize:)
[content setFrameOrigin:NSMakePoint(padWindow, wSize.height - padWindow - contentSize.height)];
[self.contentView addSubview:content];
// Respond buttons
[self placeButtons:@[btnDone, btnCancel] inBottomRightCornerWithPadding:padWindow];
[self.contentView addSubview:btnCancel];
[self.contentView addSubview:btnDone];
self.btnDone = btnDone;
}
return self;
}
/**
Buttons will stick to the right margin and bottom margin when resizing. Also sets autoresizingMask.
@param buttons First item is rightmost button. Next buttons will be appended left of that button and so on.
@param padding Distance between button and right / bottom edge.
*/
- (void)placeButtons:(NSArray<NSButton*> *)buttons inBottomRightCornerWithPadding:(int)padding {
NSEdgeInsets edge = buttons.firstObject.alignmentRectInsets;
NSPoint p = NSMakePoint(self.contentView.frame.size.width - padding + edge.right, padding - edge.bottom);
for (NSButton *btn in buttons) {
p.x -= btn.frame.size.width;
[btn setFrameOrigin:p];
btn.autoresizingMask = NSViewMinXMargin | NSViewMaxYMargin;
}
}
/**
Resize modal window by @c dy. Makes room for additional content. Use negative values to shrink window.
*/
- (void)extendContentViewBy:(CGFloat)dy {
self.minSize = NSMakeSize(self.minSize.width, self.minSize.height + dy);
self.maxSize = NSMakeSize(self.maxSize.width, self.maxSize.height + dy);
NSRect r = self.frame;
r.size.height += dy;
[self setFrame:r display:YES animate:YES];
}
@end

View File

@@ -22,5 +22,6 @@
#import <Cocoa/Cocoa.h>
@interface Preferences : NSWindowController <NSWindowDelegate>
@interface Preferences : NSWindow <NSWindowDelegate>
+ (instancetype)window;
@end

View File

@@ -21,74 +21,95 @@
// SOFTWARE.
#import "Preferences.h"
#import "SettingsFeeds.h"
#import "SettingsGeneral.h"
#import "SettingsFeeds.h"
#import "SettingsAppearance.h"
#import "SettingsAbout.h"
@interface Preferences ()
@property (weak) IBOutlet SettingsGeneral *settingsGeneral;
@property (weak) IBOutlet SettingsFeeds *settingsFeeds;
@property (weak) IBOutlet NSView *aboutView;
@property (weak) IBOutlet NSTextField *lblAppName;
@property (weak) IBOutlet NSTextField *lblAppVersion;
/// Managing individual tabs in application preferences
@interface PrefTabs : NSTabViewController
@end
@implementation PrefTabs
- (instancetype)init {
self = [super init];
if (self) {
self.tabStyle = NSTabViewControllerTabStyleToolbar;
self.transitionOptions = NSViewControllerTransitionNone;
NSTabViewItem *flexibleWidth = [[NSTabViewItem alloc] initWithIdentifier:NSToolbarFlexibleSpaceItemIdentifier];
flexibleWidth.viewController = [NSViewController new];
self.tabViewItems = @[
TabItem(NSImageNamePreferencesGeneral, NSLocalizedString(@"General", nil), [SettingsGeneral class]),
TabItem(NSImageNameUserAccounts, NSLocalizedString(@"Feeds", nil), [SettingsFeeds class]),
TabItem(NSImageNameFontPanel, NSLocalizedString(@"Appearance", nil), [SettingsAppearance class]),
flexibleWidth,
TabItem(NSImageNameInfo, NSLocalizedString(@"About", nil), [SettingsAbout class]),
];
NSInteger index = [[NSUserDefaults standardUserDefaults] integerForKey:@"preferencesTab"];
if (index > 0 || (NSUInteger)index < self.tabViewItems.count)
self.selectedTabViewItemIndex = index;
}
return self;
}
/// Helper method to generate tab item with image, label, and controller.
NS_INLINE NSTabViewItem* TabItem(NSImageName imageName, NSString *text, Class class) {
NSTabViewItem *item = [NSTabViewItem tabViewItemWithViewController: [class new]];
item.image = [NSImage imageNamed:imageName];
item.label = text;
return item;
}
/// Delegate method, store last selected tab to user preferences
- (void)tabView:(NSTabView*)tabView didSelectTabViewItem:(nullable NSTabViewItem*)tabViewItem {
[super tabView:tabView didSelectTabViewItem:tabViewItem];
NSInteger prevIndex = [[NSUserDefaults standardUserDefaults] integerForKey:@"preferencesTab"];
NSInteger newIndex = self.selectedTabViewItemIndex;
if (prevIndex != newIndex)
[[NSUserDefaults standardUserDefaults] setInteger:newIndex forKey:@"preferencesTab"];
}
@end
@implementation Preferences
/// Restore tab selection from previous session
- (void)windowDidLoad {
[super windowDidLoad];
NSUInteger idx = (NSUInteger)[[NSUserDefaults standardUserDefaults] integerForKey:@"preferencesTab"];
if (idx >= self.window.toolbar.items.count)
idx = 0;
[self tabClicked:self.window.toolbar.items[idx]];
+ (instancetype)window {
NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable | NSWindowStyleMaskUnifiedTitleAndToolbar;
Preferences *w = [[Preferences alloc] initWithContentRect:NSMakeRect(0, 0, 320, 327) styleMask:style backing:NSBackingStoreBuffered defer:YES];
w.contentMinSize = NSMakeSize(320, 327);
w.windowController.shouldCascadeWindows = YES;
w.title = [NSString stringWithFormat:NSLocalizedString(@"%@ Preferences", nil), NSProcessInfo.processInfo.processName];
w.contentViewController = [PrefTabs new];
w.delegate = w;
NSWindowPersistableFrameDescriptor prevFrame = [[NSUserDefaults standardUserDefaults] stringForKey:@"prefWindow"];
if (!prevFrame) {
[w setContentSize:NSMakeSize(320, 327)];
[w center];
} else {
[w setFrameFromString:prevFrame];
}
return w;
}
/// Replace content view according to selected tab
- (IBAction)tabClicked:(NSToolbarItem *)sender {
self.window.contentView = nil;
if ([sender.itemIdentifier isEqualToString:@"tabGeneral"]) {
self.window.contentView = self.settingsGeneral.view;
} else if ([sender.itemIdentifier isEqualToString:@"tabFeeds"]) {
self.window.contentView = self.settingsFeeds.view;
} else if ([sender.itemIdentifier isEqualToString:@"tabAppearance"]) {
if (self.settingsGeneral.view.frame.size.width > 0) {
// using side effect when reading settingsGeneral.view -> will load appearanceView too.
// TODO: generate view programmatically
self.window.contentView = nil;
}
self.window.contentView = self.settingsGeneral.appearanceView;
} else if ([sender.itemIdentifier isEqualToString:@"tabAbout"]) {
NSDictionary *infoDict = [[NSBundle mainBundle] infoDictionary];
self.lblAppName.objectValue = infoDict[@"CFBundleName"];
self.lblAppVersion.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Version %@", nil), infoDict[@"CFBundleShortVersionString"]];
self.window.contentView = self.aboutView;
}
self.window.toolbar.selectedItemIdentifier = sender.itemIdentifier;
[self.window recalculateKeyViewLoop];
[self.window setInitialFirstResponder:self.window.contentView];
NSInteger selectedIndex = (NSInteger)[self.window.toolbar.items indexOfObject:sender];
[[NSUserDefaults standardUserDefaults] setInteger:selectedIndex forKey:@"preferencesTab"];
- (void)windowWillClose:(NSNotification *)notification {
[[NSUserDefaults standardUserDefaults] setObject:self.stringWithSavedFrame forKey:@"prefWindow"];
}
@end
/// A window that does not respond to Cmd-C, Cmd-Z, Cmd-Shift-Z and Enter-pressed events.
@interface NonRespondingWindow : NSWindow
@end
@implementation NonRespondingWindow
/// Do not respond to Cmd-Z and Cmd-Shift-Z. Will be handled in subview controllers.
- (BOOL)respondsToSelector:(SEL)aSelector {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if (aSelector == @selector(enterPressed:) || aSelector == @selector(copy:)
|| aSelector == @selector(undo:) || aSelector == @selector(redo:)) {
if (aSelector == @selector(undo:) || aSelector == @selector(redo:)) {
#pragma clang diagnostic pop
return NO;
}
return [super respondsToSelector:aSelector];
}
@end

View File

@@ -1,783 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14313.18"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="Preferences">
<connections>
<outlet property="aboutView" destination="sUz-wX-Xkd" id="n1B-pf-o11"/>
<outlet property="lblAppName" destination="7NB-y1-8BH" id="tW6-iK-OEy"/>
<outlet property="lblAppVersion" destination="Uxl-GT-OeZ" id="vzP-9H-EwS"/>
<outlet property="settingsFeeds" destination="IYm-V7-352" id="KdS-eY-jdj"/>
<outlet property="settingsGeneral" destination="iap-X4-1Ef" id="VYy-lR-Cba"/>
<outlet property="window" destination="XQ4-ia-CCO" id="rse-30-FqG"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<window title="Preferences" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" visibleAtLaunch="NO" frameAutosaveName="prefWindow" animationBehavior="default" tabbingMode="disallowed" id="XQ4-ia-CCO" userLabel="Window" customClass="NonRespondingWindow">
<windowStyleMask key="styleMask" titled="YES" closable="YES" resizable="YES"/>
<rect key="contentRect" x="948" y="431" width="320" height="327"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="878"/>
<value key="minSize" type="size" width="320" height="327"/>
<view key="contentView" id="hcr-99-ABl">
<rect key="frame" x="0.0" y="0.0" width="320" height="327"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<toolbar key="toolbar" implicitIdentifier="5B23E7E6-13E5-4910-875D-2E3EA566F38B" autosavesConfiguration="NO" allowsUserCustomization="NO" displayMode="iconAndLabel" sizeMode="regular" id="vwy-mG-3U7">
<allowedToolbarItems>
<toolbarItem implicitItemIdentifier="AF4483E3-0457-47B7-AE52-AC11B1545455" explicitItemIdentifier="tabGeneral" label="General" paletteLabel="General" tag="-1" image="NSPreferencesGeneral" selectable="YES" id="WDq-RJ-C3X">
<connections>
<action selector="tabClicked:" target="-2" id="P5A-7V-1JL"/>
</connections>
</toolbarItem>
<toolbarItem implicitItemIdentifier="E8527558-3D5F-4B79-99E4-675C1557D10B" explicitItemIdentifier="tabFeeds" label="Feeds" paletteLabel="Feeds" tag="-1" image="NSUserAccounts" selectable="YES" id="atT-AS-vmR">
<connections>
<action selector="tabClicked:" target="-2" id="MVF-hq-6H4"/>
</connections>
</toolbarItem>
<toolbarItem implicitItemIdentifier="BC213BA1-C1C5-4EBA-8282-769344192482" explicitItemIdentifier="tabAppearance" label="Appearance" paletteLabel="Appearance" tag="-1" image="NSFontPanel" selectable="YES" id="5gP-ck-qVK">
<connections>
<action selector="tabClicked:" target="-2" id="BXs-NU-zQM"/>
</connections>
</toolbarItem>
<toolbarItem implicitItemIdentifier="NSToolbarFlexibleSpaceItem" id="kda-e8-akS"/>
<toolbarItem implicitItemIdentifier="7A0CF54C-C0BA-4E1D-9CD7-39FD39A6BA5A" explicitItemIdentifier="tabAbout" label="About" paletteLabel="About" tag="-1" image="NSInfo" selectable="YES" id="kob-4t-J64">
<connections>
<action selector="tabClicked:" target="-2" id="mh0-QQ-lzs"/>
</connections>
</toolbarItem>
</allowedToolbarItems>
<defaultToolbarItems>
<toolbarItem reference="WDq-RJ-C3X"/>
<toolbarItem reference="atT-AS-vmR"/>
<toolbarItem reference="5gP-ck-qVK"/>
<toolbarItem reference="kda-e8-akS"/>
<toolbarItem reference="kob-4t-J64"/>
</defaultToolbarItems>
</toolbar>
<connections>
<outlet property="delegate" destination="-2" id="QYC-U2-meR"/>
</connections>
<point key="canvasLocation" x="-96" y="741"/>
</window>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<viewController id="iap-X4-1Ef" customClass="SettingsGeneral"/>
<viewController id="IYm-V7-352" customClass="SettingsFeeds"/>
<customView id="sUz-wX-Xkd">
<rect key="frame" x="0.0" y="0.0" width="220" height="320"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView fixedFrame="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" hasVerticalScroller="NO" horizontalScrollElasticity="none" translatesAutoresizingMaskIntoConstraints="NO" id="txr-jS-bP9">
<rect key="frame" x="0.0" y="20" width="220" height="176"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<clipView key="contentView" ambiguous="YES" drawsBackground="NO" copiesOnScroll="NO" id="pBe-sS-jEC">
<rect key="frame" x="1" y="1" width="218" height="174"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView ambiguous="YES" editable="NO" importsGraphics="NO" richText="NO" verticallyResizable="YES" allowsCharacterPickerTouchBarItem="NO" textCompletion="NO" id="rDL-CY-yY7">
<rect key="frame" x="0.0" y="0.0" width="218" height="174"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<size key="minSize" width="218" height="174"/>
<size key="maxSize" width="333" height="10000000"/>
<attributedString key="textStorage">
<fragment>
<string key="content" base64-UTF8="YES">
Cg
</string>
<attributes>
<font key="NSFont" metaFont="systemLight" size="13"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0" allowsDefaultTighteningForTruncation="NO">
<tabStops>
<textTab alignment="left" location="28.299999237060547">
<options/>
</textTab>
<textTab alignment="left" location="56.650001525878906">
<options/>
</textTab>
<textTab alignment="left" location="85">
<options/>
</textTab>
<textTab alignment="left" location="113.34999847412109">
<options/>
</textTab>
<textTab alignment="left" location="141.69999694824219">
<options/>
</textTab>
<textTab alignment="left" location="170.05000305175781">
<options/>
</textTab>
<textTab alignment="left" location="198.39999389648438">
<options/>
</textTab>
<textTab alignment="left" location="226.75">
<options/>
</textTab>
<textTab alignment="left" location="255.10000610351562">
<options/>
</textTab>
<textTab alignment="left" location="283.45001220703125">
<options/>
</textTab>
<textTab alignment="left" location="311.79998779296875">
<options/>
</textTab>
<textTab alignment="left" location="340.14999389648438">
<options/>
</textTab>
</tabStops>
</paragraphStyle>
</attributes>
</fragment>
<fragment content="Programming">
<attributes>
<font key="NSFont" metaFont="systemMedium" size="13"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0" allowsDefaultTighteningForTruncation="NO">
<tabStops>
<textTab alignment="left" location="28.299999237060547">
<options/>
</textTab>
<textTab alignment="left" location="56.650001525878906">
<options/>
</textTab>
<textTab alignment="left" location="85">
<options/>
</textTab>
<textTab alignment="left" location="113.34999847412109">
<options/>
</textTab>
<textTab alignment="left" location="141.69999694824219">
<options/>
</textTab>
<textTab alignment="left" location="170.05000305175781">
<options/>
</textTab>
<textTab alignment="left" location="198.39999389648438">
<options/>
</textTab>
<textTab alignment="left" location="226.75">
<options/>
</textTab>
<textTab alignment="left" location="255.10000610351562">
<options/>
</textTab>
<textTab alignment="left" location="283.45001220703125">
<options/>
</textTab>
<textTab alignment="left" location="311.79998779296875">
<options/>
</textTab>
<textTab alignment="left" location="340.14999389648438">
<options/>
</textTab>
</tabStops>
</paragraphStyle>
</attributes>
</fragment>
<fragment>
<string key="content">
Oleg Geier
</string>
<attributes>
<font key="NSFont" metaFont="systemLight" size="13"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0" allowsDefaultTighteningForTruncation="NO">
<tabStops>
<textTab alignment="left" location="28.299999237060547">
<options/>
</textTab>
<textTab alignment="left" location="56.650001525878906">
<options/>
</textTab>
<textTab alignment="left" location="85">
<options/>
</textTab>
<textTab alignment="left" location="113.34999847412109">
<options/>
</textTab>
<textTab alignment="left" location="141.69999694824219">
<options/>
</textTab>
<textTab alignment="left" location="170.05000305175781">
<options/>
</textTab>
<textTab alignment="left" location="198.39999389648438">
<options/>
</textTab>
<textTab alignment="left" location="226.75">
<options/>
</textTab>
<textTab alignment="left" location="255.10000610351562">
<options/>
</textTab>
<textTab alignment="left" location="283.45001220703125">
<options/>
</textTab>
<textTab alignment="left" location="311.79998779296875">
<options/>
</textTab>
<textTab alignment="left" location="340.14999389648438">
<options/>
</textTab>
</tabStops>
</paragraphStyle>
</attributes>
</fragment>
<fragment content="Source Code">
<attributes>
<font key="NSFont" metaFont="systemMedium" size="13"/>
<font key="NSOriginalFont" size="12" name="Helvetica-Bold"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0" allowsDefaultTighteningForTruncation="NO">
<tabStops>
<textTab alignment="left" location="28.299999237060547">
<options/>
</textTab>
<textTab alignment="left" location="56.650001525878906">
<options/>
</textTab>
<textTab alignment="left" location="85">
<options/>
</textTab>
<textTab alignment="left" location="113.34999847412109">
<options/>
</textTab>
<textTab alignment="left" location="141.69999694824219">
<options/>
</textTab>
<textTab alignment="left" location="170.05000305175781">
<options/>
</textTab>
<textTab alignment="left" location="198.39999389648438">
<options/>
</textTab>
<textTab alignment="left" location="226.75">
<options/>
</textTab>
<textTab alignment="left" location="255.10000610351562">
<options/>
</textTab>
<textTab alignment="left" location="283.45001220703125">
<options/>
</textTab>
<textTab alignment="left" location="311.79998779296875">
<options/>
</textTab>
<textTab alignment="left" location="340.14999389648438">
<options/>
</textTab>
</tabStops>
</paragraphStyle>
</attributes>
</fragment>
<fragment content=" available">
<attributes>
<font key="NSFont" metaFont="systemMedium" size="13"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0" allowsDefaultTighteningForTruncation="NO">
<tabStops>
<textTab alignment="left" location="28.299999237060547">
<options/>
</textTab>
<textTab alignment="left" location="56.650001525878906">
<options/>
</textTab>
<textTab alignment="left" location="85">
<options/>
</textTab>
<textTab alignment="left" location="113.34999847412109">
<options/>
</textTab>
<textTab alignment="left" location="141.69999694824219">
<options/>
</textTab>
<textTab alignment="left" location="170.05000305175781">
<options/>
</textTab>
<textTab alignment="left" location="198.39999389648438">
<options/>
</textTab>
<textTab alignment="left" location="226.75">
<options/>
</textTab>
<textTab alignment="left" location="255.10000610351562">
<options/>
</textTab>
<textTab alignment="left" location="283.45001220703125">
<options/>
</textTab>
<textTab alignment="left" location="311.79998779296875">
<options/>
</textTab>
<textTab alignment="left" location="340.14999389648438">
<options/>
</textTab>
</tabStops>
</paragraphStyle>
</attributes>
</fragment>
<fragment>
<string key="content" base64-UTF8="YES">
Cg
</string>
<attributes>
<font key="NSFont" metaFont="systemLight" size="13"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0" allowsDefaultTighteningForTruncation="NO">
<tabStops>
<textTab alignment="left" location="28.299999237060547">
<options/>
</textTab>
<textTab alignment="left" location="56.650001525878906">
<options/>
</textTab>
<textTab alignment="left" location="85">
<options/>
</textTab>
<textTab alignment="left" location="113.34999847412109">
<options/>
</textTab>
<textTab alignment="left" location="141.69999694824219">
<options/>
</textTab>
<textTab alignment="left" location="170.05000305175781">
<options/>
</textTab>
<textTab alignment="left" location="198.39999389648438">
<options/>
</textTab>
<textTab alignment="left" location="226.75">
<options/>
</textTab>
<textTab alignment="left" location="255.10000610351562">
<options/>
</textTab>
<textTab alignment="left" location="283.45001220703125">
<options/>
</textTab>
<textTab alignment="left" location="311.79998779296875">
<options/>
</textTab>
<textTab alignment="left" location="340.14999389648438">
<options/>
</textTab>
</tabStops>
</paragraphStyle>
</attributes>
</fragment>
<fragment content="github.com">
<attributes>
<font key="NSFont" metaFont="systemLight" size="13"/>
<url key="NSLink" string="https://github.com/relikd/baRSS"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0" allowsDefaultTighteningForTruncation="NO">
<tabStops>
<textTab alignment="left" location="28.299999237060547">
<options/>
</textTab>
<textTab alignment="left" location="56.650001525878906">
<options/>
</textTab>
<textTab alignment="left" location="85">
<options/>
</textTab>
<textTab alignment="left" location="113.34999847412109">
<options/>
</textTab>
<textTab alignment="left" location="141.69999694824219">
<options/>
</textTab>
<textTab alignment="left" location="170.05000305175781">
<options/>
</textTab>
<textTab alignment="left" location="198.39999389648438">
<options/>
</textTab>
<textTab alignment="left" location="226.75">
<options/>
</textTab>
<textTab alignment="left" location="255.10000610351562">
<options/>
</textTab>
<textTab alignment="left" location="283.45001220703125">
<options/>
</textTab>
<textTab alignment="left" location="311.79998779296875">
<options/>
</textTab>
<textTab alignment="left" location="340.14999389648438">
<options/>
</textTab>
</tabStops>
</paragraphStyle>
</attributes>
</fragment>
<fragment>
<string key="content"> (MIT License)
or </string>
<attributes>
<font key="NSFont" metaFont="systemLight" size="13"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0" allowsDefaultTighteningForTruncation="NO">
<tabStops>
<textTab alignment="left" location="28.299999237060547">
<options/>
</textTab>
<textTab alignment="left" location="56.650001525878906">
<options/>
</textTab>
<textTab alignment="left" location="85">
<options/>
</textTab>
<textTab alignment="left" location="113.34999847412109">
<options/>
</textTab>
<textTab alignment="left" location="141.69999694824219">
<options/>
</textTab>
<textTab alignment="left" location="170.05000305175781">
<options/>
</textTab>
<textTab alignment="left" location="198.39999389648438">
<options/>
</textTab>
<textTab alignment="left" location="226.75">
<options/>
</textTab>
<textTab alignment="left" location="255.10000610351562">
<options/>
</textTab>
<textTab alignment="left" location="283.45001220703125">
<options/>
</textTab>
<textTab alignment="left" location="311.79998779296875">
<options/>
</textTab>
<textTab alignment="left" location="340.14999389648438">
<options/>
</textTab>
</tabStops>
</paragraphStyle>
</attributes>
</fragment>
<fragment content="gitlab.com">
<attributes>
<font key="NSFont" metaFont="systemLight" size="13"/>
<url key="NSLink" string="https://gitlab.com/relikd/baRSS"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0" allowsDefaultTighteningForTruncation="NO">
<tabStops>
<textTab alignment="left" location="28.299999237060547">
<options/>
</textTab>
<textTab alignment="left" location="56.650001525878906">
<options/>
</textTab>
<textTab alignment="left" location="85">
<options/>
</textTab>
<textTab alignment="left" location="113.34999847412109">
<options/>
</textTab>
<textTab alignment="left" location="141.69999694824219">
<options/>
</textTab>
<textTab alignment="left" location="170.05000305175781">
<options/>
</textTab>
<textTab alignment="left" location="198.39999389648438">
<options/>
</textTab>
<textTab alignment="left" location="226.75">
<options/>
</textTab>
<textTab alignment="left" location="255.10000610351562">
<options/>
</textTab>
<textTab alignment="left" location="283.45001220703125">
<options/>
</textTab>
<textTab alignment="left" location="311.79998779296875">
<options/>
</textTab>
<textTab alignment="left" location="340.14999389648438">
<options/>
</textTab>
</tabStops>
</paragraphStyle>
</attributes>
</fragment>
<fragment>
<string key="content"> (MIT License)
</string>
<attributes>
<font key="NSFont" metaFont="systemLight" size="13"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0" allowsDefaultTighteningForTruncation="NO">
<tabStops>
<textTab alignment="left" location="28.299999237060547">
<options/>
</textTab>
<textTab alignment="left" location="56.650001525878906">
<options/>
</textTab>
<textTab alignment="left" location="85">
<options/>
</textTab>
<textTab alignment="left" location="113.34999847412109">
<options/>
</textTab>
<textTab alignment="left" location="141.69999694824219">
<options/>
</textTab>
<textTab alignment="left" location="170.05000305175781">
<options/>
</textTab>
<textTab alignment="left" location="198.39999389648438">
<options/>
</textTab>
<textTab alignment="left" location="226.75">
<options/>
</textTab>
<textTab alignment="left" location="255.10000610351562">
<options/>
</textTab>
<textTab alignment="left" location="283.45001220703125">
<options/>
</textTab>
<textTab alignment="left" location="311.79998779296875">
<options/>
</textTab>
<textTab alignment="left" location="340.14999389648438">
<options/>
</textTab>
</tabStops>
</paragraphStyle>
</attributes>
</fragment>
<fragment content="3rd-Party Libraries">
<attributes>
<font key="NSFont" metaFont="systemMedium" size="13"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0" allowsDefaultTighteningForTruncation="NO">
<tabStops>
<textTab alignment="left" location="28.299999237060547">
<options/>
</textTab>
<textTab alignment="left" location="56.650001525878906">
<options/>
</textTab>
<textTab alignment="left" location="85">
<options/>
</textTab>
<textTab alignment="left" location="113.34999847412109">
<options/>
</textTab>
<textTab alignment="left" location="141.69999694824219">
<options/>
</textTab>
<textTab alignment="left" location="170.05000305175781">
<options/>
</textTab>
<textTab alignment="left" location="198.39999389648438">
<options/>
</textTab>
<textTab alignment="left" location="226.75">
<options/>
</textTab>
<textTab alignment="left" location="255.10000610351562">
<options/>
</textTab>
<textTab alignment="left" location="283.45001220703125">
<options/>
</textTab>
<textTab alignment="left" location="311.79998779296875">
<options/>
</textTab>
<textTab alignment="left" location="340.14999389648438">
<options/>
</textTab>
</tabStops>
</paragraphStyle>
</attributes>
</fragment>
<fragment>
<string key="content" base64-UTF8="YES">
Cg
</string>
<attributes>
<font key="NSFont" metaFont="systemLight" size="13"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0" allowsDefaultTighteningForTruncation="NO">
<tabStops>
<textTab alignment="left" location="28.299999237060547">
<options/>
</textTab>
<textTab alignment="left" location="56.650001525878906">
<options/>
</textTab>
<textTab alignment="left" location="85">
<options/>
</textTab>
<textTab alignment="left" location="113.34999847412109">
<options/>
</textTab>
<textTab alignment="left" location="141.69999694824219">
<options/>
</textTab>
<textTab alignment="left" location="170.05000305175781">
<options/>
</textTab>
<textTab alignment="left" location="198.39999389648438">
<options/>
</textTab>
<textTab alignment="left" location="226.75">
<options/>
</textTab>
<textTab alignment="left" location="255.10000610351562">
<options/>
</textTab>
<textTab alignment="left" location="283.45001220703125">
<options/>
</textTab>
<textTab alignment="left" location="311.79998779296875">
<options/>
</textTab>
<textTab alignment="left" location="340.14999389648438">
<options/>
</textTab>
</tabStops>
</paragraphStyle>
</attributes>
</fragment>
<fragment content="RSXML">
<attributes>
<font key="NSFont" metaFont="systemLight" size="13"/>
<url key="NSLink" string="https://github.com/relikd/RSXML"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0" allowsDefaultTighteningForTruncation="NO">
<tabStops>
<textTab alignment="left" location="28.299999237060547">
<options/>
</textTab>
<textTab alignment="left" location="56.650001525878906">
<options/>
</textTab>
<textTab alignment="left" location="85">
<options/>
</textTab>
<textTab alignment="left" location="113.34999847412109">
<options/>
</textTab>
<textTab alignment="left" location="141.69999694824219">
<options/>
</textTab>
<textTab alignment="left" location="170.05000305175781">
<options/>
</textTab>
<textTab alignment="left" location="198.39999389648438">
<options/>
</textTab>
<textTab alignment="left" location="226.75">
<options/>
</textTab>
<textTab alignment="left" location="255.10000610351562">
<options/>
</textTab>
<textTab alignment="left" location="283.45001220703125">
<options/>
</textTab>
<textTab alignment="left" location="311.79998779296875">
<options/>
</textTab>
<textTab alignment="left" location="340.14999389648438">
<options/>
</textTab>
</tabStops>
</paragraphStyle>
</attributes>
</fragment>
<fragment content=" (MIT License)">
<attributes>
<font key="NSFont" metaFont="systemLight" size="13"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0" allowsDefaultTighteningForTruncation="NO">
<tabStops>
<textTab alignment="left" location="28.299999237060547">
<options/>
</textTab>
<textTab alignment="left" location="56.650001525878906">
<options/>
</textTab>
<textTab alignment="left" location="85">
<options/>
</textTab>
<textTab alignment="left" location="113.34999847412109">
<options/>
</textTab>
<textTab alignment="left" location="141.69999694824219">
<options/>
</textTab>
<textTab alignment="left" location="170.05000305175781">
<options/>
</textTab>
<textTab alignment="left" location="198.39999389648438">
<options/>
</textTab>
<textTab alignment="left" location="226.75">
<options/>
</textTab>
<textTab alignment="left" location="255.10000610351562">
<options/>
</textTab>
<textTab alignment="left" location="283.45001220703125">
<options/>
</textTab>
<textTab alignment="left" location="311.79998779296875">
<options/>
</textTab>
<textTab alignment="left" location="340.14999389648438">
<options/>
</textTab>
</tabStops>
</paragraphStyle>
</attributes>
</fragment>
</attributedString>
<color key="insertionPointColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
</textView>
</subviews>
</clipView>
<scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="NO" id="dsS-uU-W0N">
<rect key="frame" x="-100" y="-100" width="16" height="139"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cM6-Hr-G1u">
<rect key="frame" x="78" y="248" width="64" height="64"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSApplicationIcon" id="N7F-dr-K0S"/>
</imageView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7NB-y1-8BH">
<rect key="frame" x="18" y="222" width="184" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" selectable="YES" allowsUndo="NO" alignment="center" title="$$AppNamed" id="BdY-x3-Yl2">
<font key="font" metaFont="systemBold" size="14"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Uxl-GT-OeZ">
<rect key="frame" x="18" y="204" width="184" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" selectable="YES" allowsUndo="NO" alignment="center" title="$$VersionNumber" id="enW-al-5bh">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<point key="canvasLocation" x="283" y="733"/>
</customView>
</objects>
<resources>
<image name="NSApplicationIcon" width="128" height="128"/>
<image name="NSFontPanel" width="32" height="32"/>
<image name="NSInfo" width="32" height="32"/>
<image name="NSPreferencesGeneral" width="32" height="32"/>
<image name="NSUserAccounts" width="32" height="32"/>
</resources>
</document>

View File

@@ -123,7 +123,7 @@
self.statusItem.title = @"";
}
BOOL hasNet = [FeedDownload allowNetworkConnection];
if (self.unreadCountTotal > 0 && hasNet && [UserPrefs defaultYES:@"tintMenuBarIcon"]) {
if (self.unreadCountTotal > 0 && hasNet && [UserPrefs defaultYES:@"globalTintMenuBarIcon"]) {
self.statusItem.image = [RSSIcon systemBarIcon:16 tint:[NSColor rssOrange] noConnection:!hasNet];
} else {
self.statusItem.image = [RSSIcon systemBarIcon:16 tint:nil noConnection:!hasNet];