Merge remote-tracking branch 'brentsimmons/master'

This commit is contained in:
Olof Hellman 2018-01-23 22:44:40 -08:00
commit c4542ac668
40 changed files with 1158 additions and 164 deletions

View File

@ -6,6 +6,27 @@
<description>Most recent Evergreen changes with links to updates.</description>
<language>en</language>
<item>
<title>Evergreen 1.0d34</title>
<description><![CDATA[
<h4>Icons</h4>
<p>Updated app icon — tree redrawn to make branches wider and have definition.</p>
<p>Updated next unread icon to fit better.</p>
<p>(Icons are by Brad Ellis.)</p>
<h4>Inspector</h4>
<p>Window > Info, or cmd-I, opens the Inspector. You can change the names of feeds and folders. You can get the home page URL and feed URL of a feed (which is pretty important when filing a bug for a feed).</p>
<h4>Misc.</h4>
<p>Reload the timeline when a feed updates (when feed is selected, or when a folder containing the feed is selected).</p>
]]></description>
<pubDate>Tue, 23 Jan 2018 22:00:00 -0800</pubDate>
<enclosure url="https://ranchero.com/downloads/Evergreen1.0d34.zip" sparkle:version="852" sparkle:shortVersionString="1.0d34" length="7455712" type="application/zip" />
<sparkle:minimumSystemVersion>10.13</sparkle:minimumSystemVersion>
</item>
<item>
<title>Evergreen 1.0d33</title>
<description><![CDATA[

View File

@ -8,6 +8,9 @@
/* Begin PBXBuildFile section */
8414AD251FCF5A1E00955102 /* TimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8414AD241FCF5A1E00955102 /* TimelineHeaderView.swift */; };
841ABA4E20145E7300980E11 /* NothingInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841ABA4D20145E7300980E11 /* NothingInspectorViewController.swift */; };
841ABA5E20145E9200980E11 /* FolderInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841ABA5D20145E9200980E11 /* FolderInspectorViewController.swift */; };
841ABA6020145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841ABA5F20145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift */; };
8426118A1FCB67AA0086A189 /* FeedIconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842611891FCB67AA0086A189 /* FeedIconDownloader.swift */; };
8426119E1FCB6ED40086A189 /* HTMLMetadataDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8426119D1FCB6ED40086A189 /* HTMLMetadataDownloader.swift */; };
842611A01FCB72600086A189 /* FeaturedImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8426119F1FCB72600086A189 /* FeaturedImageDownloader.swift */; };
@ -38,6 +41,7 @@
846E77411F6EF6A100A165E2 /* Database.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 846E77211F6EF5D100A165E2 /* Database.framework */; };
846E77421F6EF6A100A165E2 /* Database.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 846E77211F6EF5D100A165E2 /* Database.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
84702AA41FA27AC0006B8943 /* MarkReadOrUnreadCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkReadOrUnreadCommand.swift */; };
8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8472058020142E8900AD578B /* FeedInspectorViewController.swift */; };
848F6AE51FC29CFB002D422E /* FaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */; };
849A97431ED9EAA9007D329B /* AddFolderWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97421ED9EAA9007D329B /* AddFolderWindowController.swift */; };
849A97531ED9EAC0007D329B /* AddFeedController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97511ED9EAC0007D329B /* AddFeedController.swift */; };
@ -107,6 +111,8 @@
84B99C9D1FAE83C600ECDEDB /* DeleteFromSidebarCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C9C1FAE83C600ECDEDB /* DeleteFromSidebarCommand.swift */; };
84BB4B771F11753300858766 /* Data.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84BB4B681F1174D400858766 /* Data.framework */; };
84BB4B781F11753300858766 /* Data.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84BB4B681F1174D400858766 /* Data.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
84BBB12D20142A4700F054F5 /* Inspector.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84BBB12B20142A4700F054F5 /* Inspector.storyboard */; };
84BBB12E20142A4700F054F5 /* InspectorWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BBB12C20142A4700F054F5 /* InspectorWindowController.swift */; };
84C12A151FF5B0080009A267 /* FeedList.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84C12A141FF5B0080009A267 /* FeedList.storyboard */; };
84CC08061FF5D2E000C0C0ED /* FeedListSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CC08051FF5D2E000C0C0ED /* FeedListSplitViewController.swift */; };
84CC88181FE59CBF00644329 /* SmartFeedsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CC88171FE59CBF00644329 /* SmartFeedsController.swift */; };
@ -457,6 +463,9 @@
/* Begin PBXFileReference section */
8414AD241FCF5A1E00955102 /* TimelineHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineHeaderView.swift; sourceTree = "<group>"; };
841ABA4D20145E7300980E11 /* NothingInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NothingInspectorViewController.swift; sourceTree = "<group>"; };
841ABA5D20145E9200980E11 /* FolderInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderInspectorViewController.swift; sourceTree = "<group>"; };
841ABA5F20145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuiltinSmartFeedInspectorViewController.swift; sourceTree = "<group>"; };
842611891FCB67AA0086A189 /* FeedIconDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedIconDownloader.swift; sourceTree = "<group>"; };
8426119D1FCB6ED40086A189 /* HTMLMetadataDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLMetadataDownloader.swift; sourceTree = "<group>"; };
8426119F1FCB72600086A189 /* FeaturedImageDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedImageDownloader.swift; sourceTree = "<group>"; };
@ -486,6 +495,7 @@
846E77161F6EF5D000A165E2 /* Database.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Database.xcodeproj; path = Frameworks/Database/Database.xcodeproj; sourceTree = "<group>"; };
846E77301F6EF5D600A165E2 /* Account.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Account.xcodeproj; path = Frameworks/Account/Account.xcodeproj; sourceTree = "<group>"; };
84702AA31FA27AC0006B8943 /* MarkReadOrUnreadCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkReadOrUnreadCommand.swift; sourceTree = "<group>"; };
8472058020142E8900AD578B /* FeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedInspectorViewController.swift; sourceTree = "<group>"; };
847752FE2008879500D93690 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; };
848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaviconDownloader.swift; sourceTree = "<group>"; };
849A97421ED9EAA9007D329B /* AddFolderWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFolderWindowController.swift; sourceTree = "<group>"; };
@ -553,6 +563,8 @@
84B99C6A1FAE370B00ECDEDB /* FeedListFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListFeed.swift; sourceTree = "<group>"; };
84B99C9C1FAE83C600ECDEDB /* DeleteFromSidebarCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteFromSidebarCommand.swift; sourceTree = "<group>"; };
84BB4B611F1174D400858766 /* Data.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Data.xcodeproj; path = Frameworks/Data/Data.xcodeproj; sourceTree = "<group>"; };
84BBB12B20142A4700F054F5 /* Inspector.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Inspector.storyboard; sourceTree = "<group>"; };
84BBB12C20142A4700F054F5 /* InspectorWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InspectorWindowController.swift; sourceTree = "<group>"; };
84C12A141FF5B0080009A267 /* FeedList.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = FeedList.storyboard; sourceTree = "<group>"; };
84CBDDAE1FD3674C005A61AA /* Technotes */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Technotes; sourceTree = "<group>"; };
84CC08051FF5D2E000C0C0ED /* FeedListSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListSplitViewController.swift; sourceTree = "<group>"; };
@ -931,6 +943,7 @@
842E45DC1ED8C54B000A8B52 /* Browser.swift */,
84702AB31FA27AE8006B8943 /* Commands */,
842E45E11ED8C681000A8B52 /* MainWindow */,
84BBB12A20142A4700F054F5 /* Inspector */,
842E45E01ED8C587000A8B52 /* Preferences */,
849A97861ED9ECEF007D329B /* Article Styles */,
84A6B6921FB8D43C006754AC /* Dinosaurs */,
@ -1069,6 +1082,20 @@
name = Products;
sourceTree = "<group>";
};
84BBB12A20142A4700F054F5 /* Inspector */ = {
isa = PBXGroup;
children = (
84BBB12B20142A4700F054F5 /* Inspector.storyboard */,
84BBB12C20142A4700F054F5 /* InspectorWindowController.swift */,
8472058020142E8900AD578B /* FeedInspectorViewController.swift */,
841ABA5D20145E9200980E11 /* FolderInspectorViewController.swift */,
841ABA5F20145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift */,
841ABA4D20145E7300980E11 /* NothingInspectorViewController.swift */,
);
name = Inspector;
path = Evergreen/Inspector;
sourceTree = "<group>";
};
84DAEE201F86CAE00058304B /* Importers */ = {
isa = PBXGroup;
children = (
@ -1510,6 +1537,7 @@
849A97B21ED9FA69007D329B /* MainWindow.storyboard in Resources */,
849A979C1ED9EFEB007D329B /* styleSheet.css in Resources */,
849A97A61ED9F94D007D329B /* Preferences.storyboard in Resources */,
84BBB12D20142A4700F054F5 /* Inspector.storyboard in Resources */,
D5D1751220020B980047B29D /* Evergreen.sdef in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -1547,8 +1575,10 @@
84F204E01FAACBB30076E152 /* ArticleArray.swift in Sources */,
849C64641ED37A5D003D8FC0 /* AppDelegate.swift in Sources */,
84513F901FAA63950023A1A9 /* FeedListControlsView.swift in Sources */,
84BBB12E20142A4700F054F5 /* InspectorWindowController.swift in Sources */,
84E46C7D1F75EF7B005ECFB3 /* AppDefaults.swift in Sources */,
D5907D972004B7EB005947E5 /* Account+Scriptability.swift in Sources */,
841ABA4E20145E7300980E11 /* NothingInspectorViewController.swift in Sources */,
842E45CE1ED8C308000A8B52 /* AppNotifications.swift in Sources */,
844B5B5B1FEA00FB00C7C76A /* TimelineKeyboardDelegate.swift in Sources */,
84DAEE321F870B390058304B /* DockBadge.swift in Sources */,
@ -1585,6 +1615,7 @@
D5907DB22004BB37005947E5 /* ScriptingObjectContainer.swift in Sources */,
849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */,
849A97791ED9EC04007D329B /* TimelineStringUtilities.swift in Sources */,
8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */,
84F204CE1FAACB660076E152 /* FeedListViewController.swift in Sources */,
845A29241FC9255E007B49E3 /* SidebarCellAppearance.swift in Sources */,
845EE7B11FC2366500854A1F /* StarredFeedDelegate.swift in Sources */,
@ -1593,6 +1624,7 @@
849A97531ED9EAC0007D329B /* AddFeedController.swift in Sources */,
849A97831ED9EC63007D329B /* SidebarStatusBarView.swift in Sources */,
84F2D5381FC22FCC00998D64 /* TodayFeedDelegate.swift in Sources */,
841ABA5E20145E9200980E11 /* FolderInspectorViewController.swift in Sources */,
845213231FCA5B11003B6E93 /* ImageDownloader.swift in Sources */,
849A97431ED9EAA9007D329B /* AddFolderWindowController.swift in Sources */,
844B5B671FEA18E300C7C76A /* MainWIndowKeyboardHandler.swift in Sources */,
@ -1603,6 +1635,7 @@
849A978D1ED9EE4D007D329B /* FeedListWindowController.swift in Sources */,
849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */,
84B99C6B1FAE370B00ECDEDB /* FeedListFeed.swift in Sources */,
841ABA6020145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift in Sources */,
D5F4EDB5200744A700B9E363 /* ScriptingObject.swift in Sources */,
D5F4EDB920074D7C00B9E363 /* Folder+Scriptability.swift in Sources */,
842611A01FCB72600086A189 /* FeaturedImageDownloader.swift in Sources */,

View File

@ -69,6 +69,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
dockBadge.appDelegate = self
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(sidebarSelectionDidChange(_:)), name: .SidebarSelectionDidChange, object: nil)
appDelegate = self
}
@ -136,6 +138,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
self.unreadCount = AccountManager.shared.unreadCount
}
if InspectorWindowController.shouldOpenAtStartup {
self.toggleInspectorWindow(self)
}
#if RELEASE
DispatchQueue.main.async {
self.refreshAll(self)
@ -157,6 +163,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
RSMultiLineRenderer.emptyCache()
TimelineCellData.emptyCache()
timelineEmptyCaches()
saveState()
}
func applicationWillTerminate(_ notification: Notification) {
saveState()
}
// MARK: GetURL Apple Event
@ -195,6 +208,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
let _ = faviconDownloader.favicon(for: feed)
}
@objc func sidebarSelectionDidChange(_ note: Notification) {
guard let inspectorWindowController = inspectorWindowController, inspectorWindowController.isOpen else {
return
}
inspectorWindowController.objects = objectsForInspector()
}
// MARK: Main Window
func windowControllerWithName(_ storyboardName: String) -> NSWindowController {
@ -303,13 +324,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
@IBAction func toggleInspectorWindow(_ sender: Any?) {
if inspectorWindowController == nil {
inspectorWindowController = InspectorWindowController()
inspectorWindowController = (windowControllerWithName("Inspector") as! InspectorWindowController)
}
if inspectorWindowController!.isOpen {
inspectorWindowController!.window!.performClose(self)
}
else {
inspectorWindowController!.objects = objectsForInspector()
inspectorWindowController!.showWindow(self)
}
}
@ -425,4 +447,19 @@ private extension AppDelegate {
return windowControllerWithName("MainWindow")
}
func objectsForInspector() -> [Any]? {
guard let window = NSApplication.shared.mainWindow, let windowController = window.windowController as? MainWindowController else {
return nil
}
return windowController.selectedObjectsInSidebar()
}
func saveState() {
if let inspectorWindowController = inspectorWindowController {
inspectorWindowController.saveState()
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 B

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0d33</string>
<string>1.0d34</string>
<key>CFBundleVersion</key>
<string>522</string>
<key>LSMinimumSystemVersion</key>

View File

@ -0,0 +1,66 @@
//
// BuiltinSmartFeedInspectorViewController.swift
// Evergreen
//
// Created by Brent Simmons on 1/20/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import AppKit
final class BuiltinSmartFeedInspectorViewController: NSViewController, Inspector {
@IBOutlet var nameTextField: NSTextField?
private var smartFeed: PseudoFeed? {
didSet {
updateUI()
}
}
// MARK: Inspector
let isFallbackInspector = false
var objects: [Any]? {
didSet {
updateSmartFeed()
}
}
func canInspect(_ objects: [Any]) -> Bool {
guard let _ = singleSmartFeed(from: objects) else {
return false
}
return true
}
// MARK: NSViewController
override func viewDidLoad() {
updateUI()
}
}
private extension BuiltinSmartFeedInspectorViewController {
func singleSmartFeed(from objects: [Any]?) -> PseudoFeed? {
guard let objects = objects, objects.count == 1, let singleSmartFeed = objects.first as? PseudoFeed else {
return nil
}
return singleSmartFeed
}
func updateSmartFeed() {
smartFeed = singleSmartFeed(from: objects)
}
func updateUI() {
nameTextField?.stringValue = smartFeed?.nameForDisplay ?? ""
}
}

View File

@ -0,0 +1,139 @@
//
// FeedInspectorViewController.swift
// Evergreen
//
// Created by Brent Simmons on 1/20/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import AppKit
import Data
import DB5
final class FeedInspectorViewController: NSViewController, Inspector {
@IBOutlet var imageView: NSImageView?
@IBOutlet var nameTextField: NSTextField?
@IBOutlet var homePageURLTextField: NSTextField?
@IBOutlet var urlTextField: NSTextField?
private var feed: Feed? {
didSet {
if feed != oldValue {
updateUI()
}
}
}
// MARK: Inspector
let isFallbackInspector = false
var objects: [Any]? {
didSet {
updateFeed()
}
}
func canInspect(_ objects: [Any]) -> Bool {
return objects.count == 1 && objects.first is Feed
}
// MARK: NSViewController
override func viewDidLoad() {
imageView!.wantsLayer = true
let cornerRadius = appDelegate.currentTheme.float(forKey: "MainWindow.Timeline.cell.avatarCornerRadius")
imageView!.layer?.cornerRadius = cornerRadius
updateUI()
NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .ImageDidBecomeAvailable, object: nil)
}
// MARK: Notifications
@objc func imageDidBecomeAvailable(_ note: Notification) {
updateImage()
}
}
extension FeedInspectorViewController: NSTextFieldDelegate {
override func controlTextDidChange(_ note: Notification) {
guard let feed = feed, let nameTextField = nameTextField else {
return
}
feed.editedName = nameTextField.stringValue
}
}
private extension FeedInspectorViewController {
func updateFeed() {
guard let objects = objects, objects.count == 1, let singleFeed = objects.first as? Feed else {
feed = nil
return
}
feed = singleFeed
}
func updateUI() {
updateImage()
updateName()
updateHomePageURL()
updateFeedURL()
view.needsLayout = true
}
func updateImage() {
guard let feed = feed else {
imageView?.image = nil
return
}
if let feedIcon = appDelegate.feedIconDownloader.icon(for: feed) {
imageView?.image = feedIcon
return
}
if let favicon = appDelegate.faviconDownloader.favicon(for: feed) {
if favicon.size.height < 16.0 && favicon.size.width < 16.0 {
favicon.size = NSSize(width: 16, height: 16)
}
imageView?.image = favicon
return
}
imageView?.image = nil
}
func updateName() {
guard let nameTextField = nameTextField else {
return
}
let name = feed?.editedName ?? feed?.name ?? ""
if nameTextField.stringValue != name {
nameTextField.stringValue = name
}
}
func updateHomePageURL() {
homePageURLTextField?.stringValue = feed?.homePageURL ?? ""
}
func updateFeedURL() {
urlTextField?.stringValue = feed?.url ?? ""
}
}

View File

@ -0,0 +1,100 @@
//
// FolderInspectorViewController.swift
// Evergreen
//
// Created by Brent Simmons on 1/20/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import AppKit
import Account
import RSCore
final class FolderInspectorViewController: NSViewController, Inspector {
@IBOutlet var nameTextField: NSTextField?
private var folder: Folder? {
didSet {
if folder != oldValue {
updateUI()
}
}
}
// MARK: Inspector
let isFallbackInspector = false
var objects: [Any]? {
didSet {
updateFolder()
}
}
func canInspect(_ objects: [Any]) -> Bool {
guard let _ = singleFolder(from: objects) else {
return false
}
return true
}
// MARK: NSViewController
override func viewDidLoad() {
updateUI()
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
}
// MARK: Notifications
@objc func displayNameDidChange(_ note: Notification) {
guard let updatedFolder = note.object as? Folder, updatedFolder == folder else {
return
}
updateUI()
}
}
extension FolderInspectorViewController: NSTextFieldDelegate {
override func controlTextDidChange(_ note: Notification) {
guard let folder = folder, let nameTextField = nameTextField else {
return
}
folder.name = nameTextField.stringValue
}
}
private extension FolderInspectorViewController {
func singleFolder(from objects: [Any]?) -> Folder? {
guard let objects = objects, objects.count == 1, let singleFolder = objects.first as? Folder else {
return nil
}
return singleFolder
}
func updateFolder() {
folder = singleFolder(from: objects)
}
func updateUI() {
guard let nameTextField = nameTextField else {
return
}
let name = folder?.nameForDisplay ?? ""
if nameTextField.stringValue != name {
nameTextField.stringValue = name
}
}
}

View File

@ -0,0 +1,273 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="cfG-Pn-VJS">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="13771"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Window Controller-->
<scene sceneID="nxk-j5-jp4">
<objects>
<windowController storyboardIdentifier="WindowController" showSeguePresentationStyle="single" id="cfG-Pn-VJS" customClass="InspectorWindowController" customModule="Evergreen" customModuleProvider="target" sceneMemberID="viewController">
<window key="window" identifier="InspectorPanel" title="Inspector" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" tabbingMode="disallowed" id="bma-LM-jVu" customClass="NSPanel">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" utility="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="314" y="928" width="256" height="256"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1417"/>
<value key="minSize" type="size" width="256" height="256"/>
<value key="maxSize" type="size" width="256" height="2048"/>
<connections>
<outlet property="delegate" destination="cfG-Pn-VJS" id="nAa-rb-Rpi"/>
</connections>
</window>
<connections>
<segue destination="Fdj-2F-Kl1" kind="relationship" relationship="window.shadowedContentViewController" id="AOI-cR-OVA"/>
</connections>
</windowController>
<customObject id="THC-Ye-xbS" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-349" y="-167"/>
</scene>
<!--Feed-->
<scene sceneID="vUh-Rc-fPi">
<objects>
<viewController title="Feed" storyboardIdentifier="Feed" showSeguePresentationStyle="single" id="sfH-oR-GXm" customClass="FeedInspectorViewController" customModule="Evergreen" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="ecA-UY-KEd">
<rect key="frame" x="0.0" y="0.0" width="256" height="268"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="H9X-OG-K0p">
<rect key="frame" x="104" y="200" width="48" height="48"/>
<constraints>
<constraint firstAttribute="width" constant="48" id="1Cy-0w-dBg"/>
<constraint firstAttribute="height" constant="48" id="edb-lw-Ict"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSNetwork" id="MZ2-89-Bje"/>
</imageView>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="IWu-80-XC5">
<rect key="frame" x="20" y="136" width="216" height="56"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="56" id="zV3-AX-gyC"/>
</constraints>
<textFieldCell key="cell" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" drawsBackground="YES" id="tAO-GI-MT1">
<font key="font" metaFont="system"/>
<string key="title">Feed
Name
Field</string>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<outlet property="delegate" destination="sfH-oR-GXm" id="Dd0-5H-8HH"/>
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="2WO-Iu-p5e">
<rect key="frame" x="18" y="99" width="220" height="17"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" allowsUndo="NO" sendsActionOnEndEditing="YES" title="Home Page" usesSingleLineMode="YES" id="Fg8-rA-G5J">
<font key="font" metaFont="system"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="1000" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zm0-15-BFy">
<rect key="frame" x="18" y="78" width="220" height="17"/>
<textFieldCell key="cell" selectable="YES" allowsUndo="NO" sendsActionOnEndEditing="YES" title="http://example.com/" id="L2p-ur-j7a">
<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>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ju6-Zo-8X4">
<rect key="frame" x="18" y="41" width="220" height="17"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" allowsUndo="NO" sendsActionOnEndEditing="YES" title="Feed" usesSingleLineMode="YES" id="zzB-rX-1dK">
<font key="font" metaFont="system"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="1000" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="Vvk-KG-JlG">
<rect key="frame" x="18" y="20" width="220" height="17"/>
<textFieldCell key="cell" selectable="YES" allowsUndo="NO" sendsActionOnEndEditing="YES" title="http://example.com/feed" id="HpC-rK-YGK">
<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>
<constraints>
<constraint firstItem="zm0-15-BFy" firstAttribute="top" secondItem="2WO-Iu-p5e" secondAttribute="bottom" constant="4" id="2fb-QO-XIm"/>
<constraint firstItem="IWu-80-XC5" firstAttribute="top" secondItem="H9X-OG-K0p" secondAttribute="bottom" constant="8" symbolic="YES" id="4WB-WJ-3Z4"/>
<constraint firstItem="H9X-OG-K0p" firstAttribute="centerX" secondItem="ecA-UY-KEd" secondAttribute="centerX" id="9CA-KA-HEg"/>
<constraint firstAttribute="trailing" secondItem="ju6-Zo-8X4" secondAttribute="trailing" constant="20" symbolic="YES" id="Jzi-tP-TIw"/>
<constraint firstAttribute="trailing" secondItem="Vvk-KG-JlG" secondAttribute="trailing" constant="20" symbolic="YES" id="KAS-A7-TxB"/>
<constraint firstItem="ju6-Zo-8X4" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="NwI-2x-dAr"/>
<constraint firstItem="ju6-Zo-8X4" firstAttribute="top" secondItem="zm0-15-BFy" secondAttribute="bottom" constant="20" id="PFv-jF-JIZ"/>
<constraint firstItem="2WO-Iu-p5e" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="PeT-mm-2HJ"/>
<constraint firstAttribute="trailing" secondItem="IWu-80-XC5" secondAttribute="trailing" constant="20" symbolic="YES" id="WW6-xR-Zue"/>
<constraint firstItem="H9X-OG-K0p" firstAttribute="top" secondItem="ecA-UY-KEd" secondAttribute="top" constant="20" symbolic="YES" id="Z6q-PN-wOC"/>
<constraint firstItem="zm0-15-BFy" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="aho-BJ-kmB"/>
<constraint firstAttribute="trailing" secondItem="2WO-Iu-p5e" secondAttribute="trailing" constant="20" symbolic="YES" id="dLU-a6-nfx"/>
<constraint firstAttribute="trailing" secondItem="zm0-15-BFy" secondAttribute="trailing" constant="20" symbolic="YES" id="js6-b2-FIR"/>
<constraint firstItem="2WO-Iu-p5e" firstAttribute="top" secondItem="IWu-80-XC5" secondAttribute="bottom" constant="20" id="mlo-9L-OMV"/>
<constraint firstItem="IWu-80-XC5" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="r6h-Z0-g7b"/>
<constraint firstItem="Vvk-KG-JlG" firstAttribute="top" secondItem="ju6-Zo-8X4" secondAttribute="bottom" constant="4" id="sAt-dN-Taz"/>
<constraint firstAttribute="bottom" secondItem="Vvk-KG-JlG" secondAttribute="bottom" constant="20" symbolic="YES" id="uL4-qx-Mj1"/>
<constraint firstItem="Vvk-KG-JlG" firstAttribute="leading" secondItem="ecA-UY-KEd" secondAttribute="leading" constant="20" symbolic="YES" id="uS2-JS-PPg"/>
</constraints>
</view>
<connections>
<outlet property="homePageURLTextField" destination="zm0-15-BFy" id="0Jh-yy-mnF"/>
<outlet property="imageView" destination="H9X-OG-K0p" id="Rm6-X6-csH"/>
<outlet property="nameTextField" destination="IWu-80-XC5" id="zg4-5h-hoP"/>
<outlet property="urlTextField" destination="Vvk-KG-JlG" id="bcl-fq-3nQ"/>
</connections>
</viewController>
<customObject id="1ho-ZO-Gkb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="67" y="30"/>
</scene>
<!--Folder-->
<scene sceneID="8By-fa-WDQ">
<objects>
<viewController title="Folder" storyboardIdentifier="Folder" showSeguePresentationStyle="single" id="ylq-Dz-pnT" customClass="FolderInspectorViewController" customModule="Evergreen" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="DIQ-fD-5q6">
<rect key="frame" x="0.0" y="0.0" width="256" height="152"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="HJF-Gi-62u">
<rect key="frame" x="104" y="84" width="48" height="48"/>
<constraints>
<constraint firstAttribute="width" constant="48" id="dts-Bk-DzJ"/>
<constraint firstAttribute="height" constant="48" id="vtI-1q-OKy"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="NSFolder" id="C4n-vS-297"/>
</imageView>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="jHf-rc-GNr" userLabel="Folder Name Field">
<rect key="frame" x="20" y="20" width="216" height="56"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="56" id="Ele-JD-mB2"/>
</constraints>
<textFieldCell key="cell" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" drawsBackground="YES" id="Xl3-7D-9Mw">
<font key="font" metaFont="system"/>
<string key="title">Folder
Name
Field</string>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<outlet property="delegate" destination="ylq-Dz-pnT" id="30z-5n-OpR"/>
</connections>
</textField>
</subviews>
<constraints>
<constraint firstItem="HJF-Gi-62u" firstAttribute="top" secondItem="DIQ-fD-5q6" secondAttribute="top" constant="20" symbolic="YES" id="MKR-bW-qia"/>
<constraint firstItem="jHf-rc-GNr" firstAttribute="top" secondItem="HJF-Gi-62u" secondAttribute="bottom" constant="8" symbolic="YES" id="OC2-S5-vrw"/>
<constraint firstAttribute="trailing" secondItem="jHf-rc-GNr" secondAttribute="trailing" constant="20" symbolic="YES" id="XHK-0K-tgd"/>
<constraint firstItem="jHf-rc-GNr" firstAttribute="leading" secondItem="DIQ-fD-5q6" secondAttribute="leading" constant="20" symbolic="YES" id="cQE-0n-WN8"/>
<constraint firstItem="HJF-Gi-62u" firstAttribute="centerX" secondItem="DIQ-fD-5q6" secondAttribute="centerX" id="fA2-YY-hYx"/>
<constraint firstAttribute="bottom" secondItem="jHf-rc-GNr" secondAttribute="bottom" constant="20" symbolic="YES" id="n1f-HE-XZ9"/>
</constraints>
</view>
<connections>
<outlet property="nameTextField" destination="jHf-rc-GNr" id="ZBT-48-bbv"/>
</connections>
</viewController>
<customObject id="4SD-ni-Scy" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="67" y="329"/>
</scene>
<!--Builtin Smart Feed-->
<scene sceneID="dFq-3d-JKW">
<objects>
<viewController title="Builtin Smart Feed" storyboardIdentifier="BuiltinSmartFeed" showSeguePresentationStyle="single" id="ye3-co-8lc" customClass="BuiltinSmartFeedInspectorViewController" customModule="Evergreen" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="DXo-3M-jJQ">
<rect key="frame" x="0.0" y="0.0" width="256" height="113"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="cwK-Ep-mNL">
<rect key="frame" x="104" y="45" width="48" height="48"/>
<constraints>
<constraint firstAttribute="width" constant="48" id="33r-tp-RWH"/>
<constraint firstAttribute="height" constant="48" id="8F1-sH-5Xs"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="NSSmartBadgeTemplate" id="Z52-bd-Lgz"/>
</imageView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="4Xp-FX-kn3">
<rect key="frame" x="18" y="20" width="220" height="17"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Label" id="3v9-Z7-d7l">
<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>
<constraints>
<constraint firstItem="cwK-Ep-mNL" firstAttribute="top" secondItem="DXo-3M-jJQ" secondAttribute="top" constant="20" symbolic="YES" id="2YU-iy-D2t"/>
<constraint firstAttribute="bottom" secondItem="4Xp-FX-kn3" secondAttribute="bottom" constant="20" symbolic="YES" id="43t-uN-KJE"/>
<constraint firstItem="4Xp-FX-kn3" firstAttribute="leading" secondItem="DXo-3M-jJQ" secondAttribute="leading" constant="20" symbolic="YES" id="K5o-r4-LbL"/>
<constraint firstItem="cwK-Ep-mNL" firstAttribute="centerX" secondItem="DXo-3M-jJQ" secondAttribute="centerX" id="LUf-q3-0Xk"/>
<constraint firstItem="4Xp-FX-kn3" firstAttribute="top" secondItem="cwK-Ep-mNL" secondAttribute="bottom" constant="8" symbolic="YES" id="T0n-0d-nHb"/>
<constraint firstAttribute="trailing" secondItem="4Xp-FX-kn3" secondAttribute="trailing" constant="20" symbolic="YES" id="aEl-FU-VDu"/>
</constraints>
</view>
<connections>
<outlet property="nameTextField" destination="4Xp-FX-kn3" id="iJx-DZ-MjF"/>
</connections>
</viewController>
<customObject id="3Xn-vX-2s9" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="67" y="553"/>
</scene>
<!--Nothing to inspect-->
<scene sceneID="lUc-e1-dN7">
<objects>
<viewController title="Nothing to inspect" storyboardIdentifier="Nothing" showSeguePresentationStyle="single" id="Fdj-2F-Kl1" customClass="NothingInspectorViewController" customModule="Evergreen" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="FDE-PJ-bJl">
<rect key="frame" x="0.0" y="0.0" width="256" height="57"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="icb-M6-R2N">
<rect key="frame" x="18" y="20" width="220" height="17"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Nothing to inspect" id="iLD-8q-EAJ">
<font key="font" metaFont="system"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="zQp-oc-Qtc">
<rect key="frame" x="18" y="20" width="220" height="17"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Multiple selection" id="5oG-0x-T8O">
<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>
<constraints>
<constraint firstAttribute="trailing" secondItem="zQp-oc-Qtc" secondAttribute="trailing" constant="20" symbolic="YES" id="AX5-4f-csi"/>
<constraint firstItem="zQp-oc-Qtc" firstAttribute="leading" secondItem="FDE-PJ-bJl" secondAttribute="leading" constant="20" symbolic="YES" id="MQU-Eu-Fvu"/>
<constraint firstItem="zQp-oc-Qtc" firstAttribute="top" secondItem="FDE-PJ-bJl" secondAttribute="top" constant="20" symbolic="YES" id="Qd0-JW-cPX"/>
<constraint firstAttribute="trailing" secondItem="icb-M6-R2N" secondAttribute="trailing" constant="20" symbolic="YES" id="WFs-kc-gZB"/>
<constraint firstAttribute="bottom" secondItem="zQp-oc-Qtc" secondAttribute="bottom" constant="20" symbolic="YES" id="eY5-EF-RuC"/>
<constraint firstItem="icb-M6-R2N" firstAttribute="leading" secondItem="FDE-PJ-bJl" secondAttribute="leading" constant="20" symbolic="YES" id="kRD-dY-9aD"/>
<constraint firstAttribute="bottom" secondItem="icb-M6-R2N" secondAttribute="bottom" constant="20" symbolic="YES" id="pNA-7w-eB8"/>
<constraint firstItem="icb-M6-R2N" firstAttribute="top" secondItem="FDE-PJ-bJl" secondAttribute="top" constant="20" symbolic="YES" id="qis-c5-6m9"/>
</constraints>
</view>
<connections>
<outlet property="multipleTextField" destination="zQp-oc-Qtc" id="gIq-xK-QDe"/>
<outlet property="nothingTextField" destination="icb-M6-R2N" id="l68-BS-fhy"/>
</connections>
</viewController>
<customObject id="B1q-CC-IfW" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="67" y="-213"/>
</scene>
</scenes>
<resources>
<image name="NSFolder" width="32" height="32"/>
<image name="NSNetwork" width="32" height="32"/>
<image name="NSSmartBadgeTemplate" width="14" height="14"/>
</resources>
</document>

View File

@ -0,0 +1,139 @@
//
// InspectorWindowController.swift
// Evergreen
//
// Created by Brent Simmons on 1/20/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import AppKit
protocol Inspector: class {
var objects: [Any]? { get set }
var isFallbackInspector: Bool { get } // Can handle nothing-to-inspect or unexpected type of objects.
func canInspect(_ objects: [Any]) -> Bool
}
typealias InspectorViewController = Inspector & NSViewController
final class InspectorWindowController: NSWindowController {
class var shouldOpenAtStartup: Bool {
return UserDefaults.standard.bool(forKey: DefaultsKey.windowIsOpen)
}
var objects: [Any]? {
didSet {
let _ = window
currentInspector = inspector(for: objects)
}
}
var isOpen: Bool {
get {
return isWindowLoaded && window!.isVisible
}
}
private var inspectors: [InspectorViewController]!
private var currentInspector: InspectorViewController! {
didSet {
currentInspector.objects = objects
for inspector in inspectors {
if inspector !== currentInspector {
inspector.objects = nil
}
}
show(currentInspector)
}
}
private struct DefaultsKey {
static let windowIsOpen = "FloatingInspectorIsOpen"
static let windowOrigin = "FloatingInspectorOrigin"
}
override func windowDidLoad() {
let nothingInspector = window?.contentViewController as! InspectorViewController
let storyboard = NSStoryboard(name: NSStoryboard.Name(rawValue: "Inspector"), bundle: nil)
let feedInspector = inspector("Feed", storyboard)
let folderInspector = inspector("Folder", storyboard)
let builtinSmartFeedInspector = inspector("BuiltinSmartFeed", storyboard)
inspectors = [feedInspector, folderInspector, builtinSmartFeedInspector, nothingInspector]
currentInspector = nothingInspector
if let savedOrigin = originFromDefaults() {
window?.setFlippedOriginAdjustingForScreen(savedOrigin)
}
else {
window?.flippedOrigin = NSPoint(x: 256, y: 256)
}
}
func inspector(for objects: [Any]?) -> InspectorViewController {
var fallbackInspector: InspectorViewController? = nil
for inspector in inspectors {
if inspector.isFallbackInspector {
fallbackInspector = inspector
}
else if let objects = objects, inspector.canInspect(objects) {
return inspector
}
}
return fallbackInspector!
}
func saveState() {
UserDefaults.standard.set(isOpen, forKey: DefaultsKey.windowIsOpen)
if isOpen, let window = window, let flippedOrigin = window.flippedOrigin {
UserDefaults.standard.set(NSStringFromPoint(flippedOrigin), forKey: DefaultsKey.windowOrigin)
}
}
}
private extension InspectorWindowController {
func inspector(_ identifier: String, _ storyboard: NSStoryboard) -> InspectorViewController {
return storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier(rawValue: identifier)) as! InspectorViewController
}
func show(_ inspector: InspectorViewController) {
guard let window = window else {
return
}
let flippedOrigin = window.flippedOrigin
if window.contentViewController != inspector {
window.contentViewController = inspector
window.makeFirstResponder(nil)
}
window.layoutIfNeeded()
if let flippedOrigin = flippedOrigin {
window.setFlippedOriginAdjustingForScreen(flippedOrigin)
}
}
func originFromDefaults() -> NSPoint? {
guard let originString = UserDefaults.standard.string(forKey: DefaultsKey.windowOrigin) else {
return nil
}
let point = NSPointFromString(originString)
return point == NSPoint.zero ? nil : point
}
}

View File

@ -0,0 +1,47 @@
//
// NothingInspectorViewController.swift
// Evergreen
//
// Created by Brent Simmons on 1/20/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import AppKit
final class NothingInspectorViewController: NSViewController, Inspector {
@IBOutlet var nothingTextField: NSTextField?
@IBOutlet var multipleTextField: NSTextField?
let isFallbackInspector = true
var objects: [Any]? {
didSet {
updateTextFields()
}
}
func canInspect(_ objects: [Any]) -> Bool {
return true
}
override func viewDidLoad() {
updateTextFields()
}
}
private extension NothingInspectorViewController {
func updateTextFields() {
if let objects = objects, objects.count > 1 {
nothingTextField?.isHidden = true
multipleTextField?.isHidden = false
}
else {
nothingTextField?.isHidden = false
multipleTextField?.isHidden = true
}
}
}

View File

@ -63,7 +63,14 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
self.updateWindowTitle()
}
}
// MARK: Sidebar
func selectedObjectsInSidebar() -> [AnyObject]? {
return sidebarViewController?.selectedObjects
}
// MARK: Notifications
@objc func applicationWillTerminate(_ note: Notification) {

View File

@ -23,6 +23,10 @@ import RSCore
private var animatingChanges = false
private var sidebarCellAppearance: SidebarCellAppearance!
var selectedObjects: [AnyObject] {
return selectedNodes.representedObjects()
}
//MARK: NSViewController
override func viewDidLoad() {
@ -39,6 +43,7 @@ import RSCore
NotificationCenter.default.addObserver(self, selector: #selector(batchUpdateDidPerform(_:)), name: .BatchUpdateDidPerform, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(feedSettingDidChange(_:)), name: .FeedSettingDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
outlineView.reloadData()
@ -97,6 +102,14 @@ import RSCore
configureCellsForRepresentedObject(feed)
}
@objc func displayNameDidChange(_ note: Notification) {
guard let object = note.object else {
return
}
configureCellsForRepresentedObject(object as AnyObject)
}
// MARK: Actions
@IBAction func delete(_ sender: AnyObject?) {

View File

@ -642,7 +642,12 @@ private extension TimelineViewController {
}
let fetchedArticles = fetchUnsortedArticles(for: representedObjects)
let sortedArticles = Array(fetchedArticles).sortedByDate()
updateArticles(with: fetchedArticles)
}
func updateArticles(with unsortedArticles: Set<Article>) {
let sortedArticles = Array(unsortedArticles).sortedByDate()
if articles != sortedArticles {
articles = sortedArticles
}
@ -667,20 +672,27 @@ private extension TimelineViewController {
func fetchAndMergeArticles() {
guard let representedObjects = representedObjects else {
return
}
let selectedArticleIDs = selectedArticles.articleIDs()
var unsortedArticles = fetchUnsortedArticles(for: representedObjects)
unsortedArticles.formUnion(Set(articles))
updateArticles(with: unsortedArticles)
selectArticles(selectedArticleIDs)
}
func selectArticles(_ articleIDs: [String]) {
// let indexesToSelect = indexesOf(articleIDs)
// if indexesToSelect.isEmpty {
// tableView.deselectAll(self)
// return
// }
// tableView.selectRowIndexes(indexesToSelect, byExtendingSelection: false)
let indexesToSelect = articles.indexesForArticleIDs(Set(articleIDs))
if indexesToSelect.isEmpty {
tableView.deselectAll(self)
return
}
tableView.selectRowIndexes(indexesToSelect, byExtendingSelection: false)
}
func invalidateFetchAndMergeArticlesTimer() {

View File

@ -129,12 +129,11 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
self.database = Database(databaseFilePath: databaseFilePath, accountID: accountID)
NotificationCenter.default.addObserver(self, selector: #selector(downloadProgressDidChange(_:)), name: .DownloadProgressDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(batchUpdateDidPerform(_:)), name: .BatchUpdateDidPerform, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(batchUpdateDidPerform(_:)), name: .BatchUpdateDidPerform, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(feedSettingDidChange(_:)), name: .FeedSettingDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
pullObjectsFromDisk()
@ -438,6 +437,16 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
}
}
@objc func displayNameDidChange(_ note: Notification) {
if let feed = note.object as? Feed, let feedAccount = feed.account, feedAccount === self {
dirty = true
}
if let folder = note.object as? Folder, let folderAccount = folder.account, folderAccount === self {
dirty = true
}
}
// MARK: - Equatable
public class func ==(lhs: Account, rhs: Account) -> Bool {

View File

@ -11,7 +11,7 @@ import Foundation
import RSCore
import Data
extension NSNotification.Name {
extension Notification.Name {
public static let ChildrenDidChange = Notification.Name("ChildrenDidChange")
}

View File

@ -14,7 +14,13 @@ public final class Folder: DisplayNameProvider, Container, UnreadCountProvider,
public weak var account: Account?
public var children = [AnyObject]()
public private(set) var name: String?
public var name: String? {
didSet {
postDisplayNameDidChangeNotification()
}
}
static let untitledName = NSLocalizedString("Untitled ƒ", comment: "Folder name")
public let folderID: Int // not saved: per-run only
static var incrementingID = 0

View File

@ -20,7 +20,13 @@ public final class Feed: DisplayNameProvider, UnreadCountProvider, Hashable {
public var faviconURL: String?
public var name: String?
public var authors: Set<Author>?
public var editedName: String?
public var editedName: String? {
didSet {
postDisplayNameDidChangeNotification()
}
}
public var conditionalGetInfo: HTTPConditionalGetInfo?
public var contentHash: String?
public let hashValue: Int
@ -29,7 +35,13 @@ public final class Feed: DisplayNameProvider, UnreadCountProvider, Hashable {
public var nameForDisplay: String {
get {
return (editedName ?? name) ?? NSLocalizedString("Untitled", comment: "Feed name")
if let s = editedName, !s.isEmpty {
return s
}
if let s = name, !s.isEmpty {
return s
}
return NSLocalizedString("Untitled", comment: "Feed name")
}
}

View File

@ -157,11 +157,6 @@
84CFF56D1AC3D20A00CEA6C8 /* NSImage+RSCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 84CFF56B1AC3D20A00CEA6C8 /* NSImage+RSCore.h */; settings = {ATTRIBUTES = (Public, ); }; };
84CFF56E1AC3D20A00CEA6C8 /* NSImage+RSCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 84CFF56C1AC3D20A00CEA6C8 /* NSImage+RSCore.m */; };
84E34DA61F9FA1070077082F /* UndoableCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E34DA51F9FA1070077082F /* UndoableCommand.swift */; };
84E72E151FBD647500B873C1 /* InspectorItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E72E101FBD647500B873C1 /* InspectorItem.swift */; };
84E72E161FBD647500B873C1 /* InspectorItemContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E72E111FBD647500B873C1 /* InspectorItemContainerView.swift */; };
84E72E171FBD647500B873C1 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E72E121FBD647500B873C1 /* InspectorView.swift */; };
84E72E181FBD647500B873C1 /* InspectorWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 84E72E131FBD647500B873C1 /* InspectorWindow.xib */; };
84E72E191FBD647500B873C1 /* InspectorWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E72E141FBD647500B873C1 /* InspectorWindowController.swift */; };
84F20F831F16BA6200D8E682 /* PropertyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F20F821F16BA6200D8E682 /* PropertyList.swift */; };
84FE9FC31C00453900081CE9 /* NSStoryboard+RSCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 84FE9FC11C00453900081CE9 /* NSStoryboard+RSCore.h */; settings = {ATTRIBUTES = (Public, ); }; };
84FE9FC41C00453900081CE9 /* NSStoryboard+RSCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FE9FC21C00453900081CE9 /* NSStoryboard+RSCore.m */; };
@ -279,11 +274,6 @@
84CFF56B1AC3D20A00CEA6C8 /* NSImage+RSCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSImage+RSCore.h"; sourceTree = "<group>"; };
84CFF56C1AC3D20A00CEA6C8 /* NSImage+RSCore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSImage+RSCore.m"; sourceTree = "<group>"; };
84E34DA51F9FA1070077082F /* UndoableCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UndoableCommand.swift; path = RSCore/UndoableCommand.swift; sourceTree = "<group>"; };
84E72E101FBD647500B873C1 /* InspectorItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InspectorItem.swift; sourceTree = "<group>"; };
84E72E111FBD647500B873C1 /* InspectorItemContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InspectorItemContainerView.swift; sourceTree = "<group>"; };
84E72E121FBD647500B873C1 /* InspectorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = "<group>"; };
84E72E131FBD647500B873C1 /* InspectorWindow.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = InspectorWindow.xib; sourceTree = "<group>"; };
84E72E141FBD647500B873C1 /* InspectorWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InspectorWindowController.swift; sourceTree = "<group>"; };
84F20F821F16BA6200D8E682 /* PropertyList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyList.swift; sourceTree = "<group>"; };
84FE9FC11C00453900081CE9 /* NSStoryboard+RSCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSStoryboard+RSCore.h"; sourceTree = "<group>"; };
84FE9FC21C00453900081CE9 /* NSStoryboard+RSCore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSStoryboard+RSCore.m"; sourceTree = "<group>"; };
@ -363,7 +353,6 @@
844B5B561FE9D36000C7C76A /* Keyboard.swift */,
84CFF5241AC3C8A200CEA6C8 /* Foundation */,
84CFF5551AC3CF4A00CEA6C8 /* AppKit */,
84E72E0F1FBD647500B873C1 /* Inspector */,
84CFF5661AC3D13F00CEA6C8 /* Images */,
84CFF4F81AC3C69700CEA6C8 /* Info.plist */,
84CFF5031AC3C69700CEA6C8 /* RSCoreTests */,
@ -494,19 +483,6 @@
path = RSCore;
sourceTree = "<group>";
};
84E72E0F1FBD647500B873C1 /* Inspector */ = {
isa = PBXGroup;
children = (
84E72E131FBD647500B873C1 /* InspectorWindow.xib */,
84E72E141FBD647500B873C1 /* InspectorWindowController.swift */,
84E72E121FBD647500B873C1 /* InspectorView.swift */,
84E72E111FBD647500B873C1 /* InspectorItemContainerView.swift */,
84E72E101FBD647500B873C1 /* InspectorItem.swift */,
);
name = Inspector;
path = RSCore/Inspector;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@ -697,7 +673,6 @@
files = (
84C687301FBAA30800345C9E /* LogWindow.xib in Resources */,
8479213C1FBA426B004AD08C /* WebViewWindow.xib in Resources */,
84E72E181FBD647500B873C1 /* InspectorWindow.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -760,7 +735,6 @@
849BF8BA1C9130150071D1DA /* DiskSaver.swift in Sources */,
84FE9FC41C00453900081CE9 /* NSStoryboard+RSCore.m in Sources */,
84CFF5341AC3CB6800CEA6C8 /* NSDictionary+RSCore.m in Sources */,
84E72E161FBD647500B873C1 /* InspectorItemContainerView.swift in Sources */,
84CFF54C1AC3CDAC00CEA6C8 /* NSString+RSCore.m in Sources */,
84CFF5171AC3C73000CEA6C8 /* RSConstants.m in Sources */,
8432B1881DACA2060057D6DF /* NSWindow-Extensions.swift in Sources */,
@ -788,7 +762,6 @@
84CFF5301AC3CB1900CEA6C8 /* NSDate+RSCore.m in Sources */,
84CFF5281AC3C9A200CEA6C8 /* NSArray+RSCore.m in Sources */,
84C632A1200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m in Sources */,
84E72E171FBD647500B873C1 /* InspectorView.swift in Sources */,
84CFF5591AC3CF9100CEA6C8 /* NSView+RSCore.m in Sources */,
84CFF56A1AC3D1B000CEA6C8 /* RSScaling.m in Sources */,
84FEB4AC1D19D7F4004727E5 /* Date+Extensions.swift in Sources */,
@ -803,11 +776,9 @@
844C915C1B65753E0051FC1B /* RSPlist.m in Sources */,
84CFF5231AC3C89D00CEA6C8 /* NSObject+RSCore.m in Sources */,
8414CBA71C95F2EA00333C12 /* Set+Extensions.swift in Sources */,
84E72E191FBD647500B873C1 /* InspectorWindowController.swift in Sources */,
84B99C9A1FAE650100ECDEDB /* OPMLRepresentable.swift in Sources */,
84E34DA61F9FA1070077082F /* UndoableCommand.swift in Sources */,
844F91D61D90D86100820C48 /* RSTransparentContainerView.m in Sources */,
84E72E151FBD647500B873C1 /* InspectorItem.swift in Sources */,
84CFF56E1AC3D20A00CEA6C8 /* NSImage+RSCore.m in Sources */,
8453F7DF1BDF337800B1C8ED /* RSMacroProcessor.m in Sources */,
842E45CC1ED623C7000A8B52 /* UniqueIdentifier.swift in Sources */,

View File

@ -8,8 +8,21 @@
import Foundation
extension Notification.Name {
public static let DisplayNameDidChange = Notification.Name("DisplayNameDidChange")
}
public protocol DisplayNameProvider {
var nameForDisplay: String { get }
}
public extension DisplayNameProvider {
func postDisplayNameDidChangeNotification() {
NotificationCenter.default.post(name: .DisplayNameDidChange, object: self, userInfo: nil)
}
}

View File

@ -1,19 +0,0 @@
//
// InspectorItem.swift
// Evergreen
//
// Created by Brent Simmons on 11/15/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
protocol InspectorItem: class {
var localizedTitle: String { get }
var view: NSView { get }
var inspectedObjects: [Any]? { get set }
var expanded: Bool { get set }
func canInspect(_ objects: [Any]) -> Bool
}

View File

@ -1,20 +0,0 @@
//
// InspectorItemContainerView.swift
// Evergreen
//
// Created by Brent Simmons on 11/15/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Cocoa
class InspectorItemContainerView: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
}
}

View File

@ -1,29 +0,0 @@
//
// InspectorView.swift
// Evergreen
//
// Created by Brent Simmons on 11/15/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Cocoa
// The content view for the window.
//
// InspectorWindow
// InspectorView
// InspectorItemContainerView
// NSView (inspector item)
// InspectorItemContainerView
// NSView (inspector item)
// etc.
class InspectorView: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
}
}

View File

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="13770" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="13770"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="InspectorWindowController" customModule="RSCore" customModuleProvider="target">
<connections>
<outlet property="window" destination="xKZ-6p-7Rw" id="eII-Iv-tyT"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Inspector" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" tabbingMode="disallowed" id="xKZ-6p-7Rw" customClass="NSPanel">
<windowStyleMask key="styleMask" titled="YES" closable="YES" utility="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="272" y="172" width="276" height="378"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1417"/>
<view key="contentView" id="cBK-fp-tpk" customClass="InspectorView" customModule="RSCore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="276" height="378"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<point key="canvasLocation" x="171" y="-47"/>
</window>
</objects>
</document>

View File

@ -1,23 +0,0 @@
//
// InspectorWindowController.swift
// Evergreen
//
// Created by Brent Simmons on 11/15/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Cocoa
public class InspectorWindowController: NSWindowController {
public var isOpen: Bool {
get {
return isWindowLoaded && window!.isVisible
}
}
public convenience init() {
self.init(windowNibName: NSNib.Name(rawValue: "InspectorWindow"))
}
}

View File

@ -45,4 +45,45 @@ public extension NSWindow {
setFrame(frame, display: true)
setFrameTopLeftPoint(frame.origin)
}
public var flippedOrigin: NSPoint? {
// Screen coordinates start at lower-left.
// With this we can use upper-left, like sane people.
get {
guard let screenFrame = screen?.frame else {
return nil
}
let flippedPoint = NSPoint(x: frame.origin.x, y: screenFrame.maxY - frame.maxY)
return flippedPoint
}
set {
guard let screenFrame = screen?.frame else {
return
}
var point = newValue!
point.y = screenFrame.maxY - point.y
setFrameTopLeftPoint(point)
}
}
public func setFlippedOriginAdjustingForScreen(_ point: NSPoint) {
guard let screenFrame = screen?.frame else {
return
}
let paddingFromEdge: CGFloat = 8.0
var unflippedPoint = point
unflippedPoint.y = (screenFrame.maxY - point.y) - frame.height
if unflippedPoint.y < 0 {
unflippedPoint.y = paddingFromEdge
}
if unflippedPoint.x < 0 {
unflippedPoint.x = paddingFromEdge
}
setFrameOrigin(unflippedPoint)
}
}

View File

@ -190,3 +190,11 @@ public func ==(lhs: Node, rhs: Node) -> Bool {
return lhs === rhs
}
public extension Array where Element == Node {
public func representedObjects() -> [AnyObject] {
return self.map{ $0.representedObject }
}
}

View File

@ -0,0 +1,161 @@
# Coding Guidelines
Evergreens coding values are, in order:
* No data loss
* No crashes
* No other bugs
* Fast performance
* Developer productivity
These are not in opposition to each other: they work together.
The last one should be of particular interest: work often happens in small bursts, and anyone should be able to make progress on something in 15 minutes.
While making a great app is more important than being productive, being productive is a hugely important part — often underestimated — of making a great app.
### Problem solving
Youve seen how, in Auto Layout, there is a content compression resistance priority and a content hugging priority?
Thats how we think about problems: the problem compression resistance priority is at max, and the problem hugging priority is also at max.
In other words: solve the problem. Not less than the problem, but not more than the problem — dont over-generalize.
Similarly: always work at the highest level possible, but not higher and certainly not lower.
### Language
Write new code in Swift 4.
The one exception to this is when dealing with C APIs, which are often much easier to deal with in Objective-C than in Swift. Still, though, this is rare, and is much more likely to be needed in a lower-level framework such as RSParser — it shouldnt happen at the app level.
Swift code should be “pure” Swift as much as possible: avoid `@objc` except when needed for working with AppKit and other APIs.
Functions should tend to be small. One-liners are a-okay, especially when the function name explains intent more clearly than that one line.
We mostly avoid Swift generics, since generics is an advanced feature that can be relatively hard to understand. We *do* use them, though, when appropriate.
We use assertions and preconditions (assertions are hit only when running a debug build; preconditions will crash a release build). We also allow force-unwrapping of optionals as a shorthand for a precondition failure, though these should be used sparingly.
Extensions, including private extensions, are used — though we take care not to extend Foundation and AppKit objects too much, lest we end up with our own Cocoa dialect.
Things should be marked private as often as possible. APIs should be exactly whats needed and not more.
#### Code organization
Properties go at the top, then functions.
Then extensions for protocol conformances. Then a private extension for any private functions.
Use `// MARK:` as appropriate.
### Composition
#### No subclasses
Subclassing is inevitable — theres no way out of subclassing things like `NSView` and `NSViewController`, because thats how AppKit works.
But in all the rest of Evergreen, frameworks included, youd have a hard time finding a class that was designed to be subclassed. Its rare enough that one would have to look pretty hard to find an example, if there is one at all.
Consider this a hard rule: all Swift classes must be marked as `final`, and all Objective-C classes must be treated as if they were so marked.
#### Protocols and delegates
Protocols and delegates (which are also protocol-conforming) are preferred.
Default implementations in protocols are allowed but ever-so-slightly discouraged. Youll find several instances in the code, but this is done carefully — we dont want this to be just another form of inheritance, where you find that you have to bounce back-and-forth between files to figure out whats going on.
There is one unfortunate case about protocols to note: in Swift you cant create a Set of some protocol-conforming objects, and we use sets frequently. In those situations another solution — such as a thin object with a delegate — might be better.
#### Small objects
Giant objects with thousands of lines of code are to be avoided. Prefer multiple small objects. Its easier to focus on a small problem, and small objects are easier to maintain and compose with other objects.
That said, dont break up a larger object arbitrarily just because its large. It may be the honest answer (and it may not be). There should be a logic and reason to the smaller objects.
#### Code repetition
This policy of no-subclasses can lead to some code repetition, or almost-repetition. In small doses, thats fine, and is better than the alternatives — which tend to be complexifying.
But in larger doses some redesign is needed. It is often the case that breaking up the problem into smaller objects (see above) can solve the repetition problem.
### Model objects
Model objects are plain old objects. We dont use Core Data or any other system that requires subclassing.
Immutable Swift structs are strongly preferred. Theyre worth a little standing-on-your-head to get them — but only a little. Otherwise, use a mutable struct or reference-type object, depending on needs.
### Frameworks
#### Built-in
Dont fight the built-in frameworks and dont try to hide them. Lets not write our own Cocoa dialect.
#### Ours
Evergreen is layered into frameworks. Theres an app level and a bunch of frameworks below that. Each framework has its own reason for being. Dependencies between frameworks should be as minimal as possible, but those dependencies do exist.
Some frameworks are not permitted to add dependencies, and should be treated as at the bottom of the cake: RSCore, RSWeb, RSDatabase, RSParser, RSTree, and DB5. This simplifies things for us, and makes it easier for us and other people to use these frameworks in other apps.
### User Interface
Stick to stock elements, since this tends to eliminate bugs and future churn. This isnt always possible, of course, but any custom work should be the minimum possible. Were in this for the long haul.
Storyboards are preferred to xibs — except when the problem is xib-sized.
Use DB5 where parameters (sizes, colors, etc.) are needed.
Auto layout is used everywhere except in table and outline view cells, where performance is critical.
Stack views are not allowed in table and outline view cells, but they can be useful elsewhere. However, care must be taken that performance (of window resizing, for instance) is not affected. When it is, dont use a stack view.
Use nil-targeted actions and the responder chain when appropriate.
Use Cocoa bindings extremely rarely — for a checkbox in a preferences window, for instance.
### Notifications and Bindings
Key-Value Observing (KVO) is entirely forbidden. KVO is where the crashing bugs live. (The only possible exception to this is when an Apple API requires KVO, which is rare.)
`NSArrayController` and similar are never used. Binding via code is also not done.
Instead, we use NotificationCenter notifications, and we use Swifts `didSet` method on accessors.
All notifications must be posted on the main queue.
### Threading
Everything happens on the main thread. Period.
Well, no, not exactly. *Almost* everything happens on the main thread.
The exceptions are things that can be perfectly isolated, such as parsing an RSS feed or fetching from the database. We use `DispatchQueue` to run those in the background, often on a serial queue.
Those things must run without locks — locks are almost completely unused in Evergreen.
Any time a background task with a callback is finished, it must call back on the main queue (except for completely private cases, and then it must be noted in the code).
If this policy leads to a design that blocks the main thread unacceptably, then that design must be re-thought. Ask for help if needed.
### Cleanliness
No code that triggers compiler errors or even warnings may be checked in.
No code that writes to the console may be checked in — console spew is not allowed.
### Profiling
Use Instruments to look for leaks and to do profiling. Instruments is great at finding where the problems actually are, as opposed to where you think they are.
No shipping version gets released without looking for memory leaks.
### Version Control
Every commit message should begin with a present-tense verb.
### Last Thing
Dont show off. If your code looks like kindergarten code, then _good_.
Points are granted for not trying to amass points.

View File

@ -13,3 +13,7 @@
[Hidden Preferences](HiddenPrefs.md)
[Questions Answered](QuestionsAnswered.md)
## Contributing
[Coding Guidelines](CodingGuidelines.md)