Refactoring Interface Builder UI to code equivalent
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]];
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
101
baRSS/Helper/NSView+Ext.h
Normal 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
353
baRSS/Helper/NSView+Ext.m
Normal 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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
26
baRSS/Preferences/About Tab/SettingsAbout.h
Normal file
26
baRSS/Preferences/About Tab/SettingsAbout.h
Normal 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
|
||||
32
baRSS/Preferences/About Tab/SettingsAbout.m
Normal file
32
baRSS/Preferences/About Tab/SettingsAbout.m
Normal 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
|
||||
27
baRSS/Preferences/About Tab/SettingsAboutView.h
Normal file
27
baRSS/Preferences/About Tab/SettingsAboutView.h
Normal 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
|
||||
|
||||
87
baRSS/Preferences/About Tab/SettingsAboutView.m
Normal file
87
baRSS/Preferences/About Tab/SettingsAboutView.m
Normal 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
|
||||
27
baRSS/Preferences/Appearance Tab/SettingsAppearance.h
Normal file
27
baRSS/Preferences/Appearance Tab/SettingsAppearance.h
Normal 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
|
||||
53
baRSS/Preferences/Appearance Tab/SettingsAppearance.m
Normal file
53
baRSS/Preferences/Appearance Tab/SettingsAppearance.m
Normal 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
|
||||
29
baRSS/Preferences/Appearance Tab/SettingsAppearanceView.h
Normal file
29
baRSS/Preferences/Appearance Tab/SettingsAppearanceView.h
Normal 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
|
||||
|
||||
88
baRSS/Preferences/Appearance Tab/SettingsAppearanceView.m
Normal file
88
baRSS/Preferences/Appearance Tab/SettingsAppearanceView.m
Normal 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
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
|
||||
@interface ModalFeedEdit : ModalEditDialog <NSTextFieldDelegate>
|
||||
- (void)didClickWarningButton:(NSButton*)sender;
|
||||
@end
|
||||
|
||||
@interface ModalGroupEdit : ModalEditDialog
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
45
baRSS/Preferences/Feeds Tab/ModalFeedEditView.h
Normal file
45
baRSS/Preferences/Feeds Tab/ModalFeedEditView.h
Normal 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
|
||||
120
baRSS/Preferences/Feeds Tab/ModalFeedEditView.m
Normal file
120
baRSS/Preferences/Feeds Tab/ModalFeedEditView.m
Normal 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
117
baRSS/Preferences/Feeds Tab/RefreshStatisticsView.m
Normal file
117
baRSS/Preferences/Feeds Tab/RefreshStatisticsView.m
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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,32 +268,13 @@ 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Insert & Edit Feed Items / Modal Dialog
|
||||
@@ -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];
|
||||
|
||||
@@ -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="<string>" 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>
|
||||
48
baRSS/Preferences/Feeds Tab/SettingsFeedsView.h
Normal file
48
baRSS/Preferences/Feeds Tab/SettingsFeedsView.h
Normal 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
|
||||
254
baRSS/Preferences/Feeds Tab/SettingsFeedsView.m
Normal file
254
baRSS/Preferences/Feeds Tab/SettingsFeedsView.m
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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>
|
||||
35
baRSS/Preferences/General Tab/SettingsGeneralView.h
Normal file
35
baRSS/Preferences/General Tab/SettingsGeneralView.h
Normal 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
|
||||
|
||||
51
baRSS/Preferences/General Tab/SettingsGeneralView.m
Normal file
51
baRSS/Preferences/General Tab/SettingsGeneralView.m
Normal 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
|
||||
@@ -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;
|
||||
109
baRSS/Preferences/Helper/ModalSheet.m
Normal file
109
baRSS/Preferences/Helper/ModalSheet.m
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -22,5 +22,6 @@
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface Preferences : NSWindowController <NSWindowDelegate>
|
||||
@interface Preferences : NSWindow <NSWindowDelegate>
|
||||
+ (instancetype)window;
|
||||
@end
|
||||
|
||||
@@ -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;
|
||||
- (void)windowWillClose:(NSNotification *)notification {
|
||||
[[NSUserDefaults standardUserDefaults] setObject:self.stringWithSavedFrame forKey:@"prefWindow"];
|
||||
}
|
||||
|
||||
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"];
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user