Merge remote-tracking branch 'brentsimmons/master'

This commit is contained in:
Olof Hellman 2018-03-04 18:43:49 -08:00
commit c91f4621e1
44 changed files with 811 additions and 1118 deletions

View File

@ -6,6 +6,38 @@
<description>Most recent Evergreen changes with links to updates.</description>
<language>en</language>
<item>
<title>Evergreen 1.0d42</title>
<description><![CDATA[
<p>The app will now remember and restore the positions of split view dividers between runs. <i>Finally.</i></p>
<p>When a favicon is large, allow it to be drawn large in the detail view, instead of forcing it to be 16 x 16.</p>
]]></description>
<pubDate>Mon, 26 Feb 2018 22:30:00 -0800</pubDate>
<enclosure url="https://ranchero.com/downloads/Evergreen1.0d42.zip" sparkle:version="1090" sparkle:shortVersionString="1.0d42" length="7349890" type="application/zip" />
<sparkle:minimumSystemVersion>10.13</sparkle:minimumSystemVersion>
</item>
<item>
<title>Evergreen 1.0d41</title>
<description><![CDATA[
<h4>Timeline</h4>
<p>Text is now ellipsized.</p>
<p>Avatars and feed icons are now larger — 64pts instead of 48pts. This is the right size.</p>
<p>Improved the layout, especially when theres little text and when theres no avatar or feed icon and one is hoped-for.</p>
<p>Dont show favicons in place of avatars and feed icons (when neither can be found) — theyre too small and dont look good.</p>
<h4>Sidebar</h4>
<p>Fixed a bug where deleting a thing wouldnt stick — it could come back on the next run.</p>
<p>Fixed a bug where deleting a thing, and then deleting another thing, would result in the wrong thing being deleted.</p>
]]></description>
<pubDate>Sun, 25 Feb 2018 22:10:00 -0800</pubDate>
<enclosure url="https://ranchero.com/downloads/Evergreen1.0d41.zip" sparkle:version="1084" sparkle:shortVersionString="1.0d41" length="7346094" type="application/zip" />
<sparkle:minimumSystemVersion>10.13</sparkle:minimumSystemVersion>
</item>
<item>
<title>Evergreen 1.0d40</title>
<description><![CDATA[

View File

@ -14,6 +14,7 @@ import Data
final class DeleteFromSidebarCommand: UndoableCommand {
let treeController: TreeController
let undoManager: UndoManager
let undoActionName: String
var redoActionName: String {
@ -22,7 +23,7 @@ final class DeleteFromSidebarCommand: UndoableCommand {
private let itemSpecifiers: [SidebarItemSpecifier]
init?(nodesToDelete: [Node], undoManager: UndoManager) {
init?(nodesToDelete: [Node], treeController: TreeController, undoManager: UndoManager) {
guard DeleteFromSidebarCommand.canDelete(nodesToDelete) else {
return nil
@ -31,6 +32,7 @@ final class DeleteFromSidebarCommand: UndoableCommand {
return nil
}
self.treeController = treeController
self.undoActionName = actionName
self.undoManager = undoManager
@ -45,6 +47,7 @@ final class DeleteFromSidebarCommand: UndoableCommand {
BatchUpdate.shared.perform {
itemSpecifiers.forEach { $0.delete() }
treeController.rebuild()
}
registerUndo()
}
@ -53,6 +56,7 @@ final class DeleteFromSidebarCommand: UndoableCommand {
BatchUpdate.shared.perform {
itemSpecifiers.forEach { $0.restore() }
treeController.rebuild()
}
registerRedo()
}

View File

@ -123,8 +123,6 @@
84B06FEA1ED3803A00F0B54B /* RSFeedFinder.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84B06FE61ED3803200F0B54B /* RSFeedFinder.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
84B06FFD1ED3818D00F0B54B /* RSTree.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84B06FFA1ED3818000F0B54B /* RSTree.framework */; };
84B06FFE1ED3818D00F0B54B /* RSTree.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84B06FFA1ED3818000F0B54B /* RSTree.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
84B0700A1ED3822600F0B54B /* RSTextDrawing.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84B070071ED3821900F0B54B /* RSTextDrawing.framework */; };
84B0700B1ED3822600F0B54B /* RSTextDrawing.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84B070071ED3821900F0B54B /* RSTextDrawing.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
84B7178C201E66580091657D /* SidebarViewController+ContextualMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7178B201E66580091657D /* SidebarViewController+ContextualMenus.swift */; };
84B99C671FAE35E600ECDEDB /* FeedListTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C661FAE35E600ECDEDB /* FeedListTreeControllerDelegate.swift */; };
84B99C691FAE36B800ECDEDB /* FeedListFolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C681FAE36B800ECDEDB /* FeedListFolder.swift */; };
@ -140,6 +138,8 @@
84D5BA20201E8FB6009092BD /* SidebarGearMenuDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D5BA1F201E8FB6009092BD /* SidebarGearMenuDelegate.swift */; };
84DAEE301F86CAFE0058304B /* OPMLImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DAEE2F1F86CAFE0058304B /* OPMLImporter.swift */; };
84DAEE321F870B390058304B /* DockBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DAEE311F870B390058304B /* DockBadge.swift */; };
84E185B3203B74E500F69BFA /* SingleLineTextFieldSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E185B2203B74E500F69BFA /* SingleLineTextFieldSizer.swift */; };
84E185C3203BB12600F69BFA /* MultilineTextFieldSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */; };
84E46C7D1F75EF7B005ECFB3 /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E46C7C1F75EF7B005ECFB3 /* AppDefaults.swift */; };
84E850861FCB60CE0072EA88 /* AuthorAvatarDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E850851FCB60CE0072EA88 /* AuthorAvatarDownloader.swift */; };
84E8E0DB202EC49300562D8F /* TimelineViewController+ContextualMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */; };
@ -432,27 +432,6 @@
remoteGlobalIDString = 842A0BE01CFCB9BC00BF746C;
remoteInfo = RSTree;
};
84B070061ED3821900F0B54B /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 84B070011ED3821800F0B54B /* RSTextDrawing.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 8439D9FB1C8937C800E5E4B4;
remoteInfo = RSTextDrawing;
};
84B070081ED3821900F0B54B /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 84B070011ED3821800F0B54B /* RSTextDrawing.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 8439DA051C8937C800E5E4B4;
remoteInfo = RSTextDrawingTests;
};
84B0700C1ED3822600F0B54B /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 84B070011ED3821800F0B54B /* RSTextDrawing.xcodeproj */;
proxyType = 1;
remoteGlobalIDString = 8439D9FA1C8937C800E5E4B4;
remoteInfo = RSTextDrawing;
};
84BB4B671F1174D400858766 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 84BB4B611F1174D400858766 /* Data.xcodeproj */;
@ -488,7 +467,6 @@
849B897D1F0349D000578A8D /* RSParser.framework in Embed Frameworks */,
846E77421F6EF6A100A165E2 /* Database.framework in Embed Frameworks */,
84B06FB31ED37DBD00F0B54B /* RSDatabase.framework in Embed Frameworks */,
84B0700B1ED3822600F0B54B /* RSTextDrawing.framework in Embed Frameworks */,
84B06FEA1ED3803A00F0B54B /* RSFeedFinder.framework in Embed Frameworks */,
84B06FFE1ED3818D00F0B54B /* RSTree.framework in Embed Frameworks */,
84B06FAF1ED37DBD00F0B54B /* RSCore.framework in Embed Frameworks */,
@ -648,7 +626,6 @@
84B06FC61ED37F7200F0B54B /* DB5.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = DB5.xcodeproj; path = Frameworks/DB5/DB5.xcodeproj; sourceTree = "<group>"; };
84B06FE01ED3803200F0B54B /* RSFeedFinder.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSFeedFinder.xcodeproj; path = Frameworks/RSFeedFinder/RSFeedFinder.xcodeproj; sourceTree = "<group>"; };
84B06FF41ED3818000F0B54B /* RSTree.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSTree.xcodeproj; path = Frameworks/RSTree/RSTree.xcodeproj; sourceTree = "<group>"; };
84B070011ED3821800F0B54B /* RSTextDrawing.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSTextDrawing.xcodeproj; path = Frameworks/RSTextDrawing/RSTextDrawing.xcodeproj; sourceTree = "<group>"; };
84B7178B201E66580091657D /* SidebarViewController+ContextualMenus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SidebarViewController+ContextualMenus.swift"; sourceTree = "<group>"; };
84B99C661FAE35E600ECDEDB /* FeedListTreeControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListTreeControllerDelegate.swift; sourceTree = "<group>"; };
84B99C681FAE36B800ECDEDB /* FeedListFolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListFolder.swift; sourceTree = "<group>"; };
@ -665,6 +642,8 @@
84D5BA1F201E8FB6009092BD /* SidebarGearMenuDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarGearMenuDelegate.swift; sourceTree = "<group>"; };
84DAEE2F1F86CAFE0058304B /* OPMLImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLImporter.swift; sourceTree = "<group>"; };
84DAEE311F870B390058304B /* DockBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DockBadge.swift; path = Evergreen/DockBadge.swift; sourceTree = "<group>"; };
84E185B2203B74E500F69BFA /* SingleLineTextFieldSizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleLineTextFieldSizer.swift; sourceTree = "<group>"; };
84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextFieldSizer.swift; sourceTree = "<group>"; };
84E46C7C1F75EF7B005ECFB3 /* AppDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDefaults.swift; path = Evergreen/AppDefaults.swift; sourceTree = "<group>"; };
84E850851FCB60CE0072EA88 /* AuthorAvatarDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorAvatarDownloader.swift; sourceTree = "<group>"; };
84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimelineViewController+ContextualMenus.swift"; sourceTree = "<group>"; };
@ -746,7 +725,6 @@
84B06FB21ED37DBD00F0B54B /* RSDatabase.framework in Frameworks */,
849B897C1F0349D000578A8D /* RSParser.framework in Frameworks */,
846E77411F6EF6A100A165E2 /* Database.framework in Frameworks */,
84B0700A1ED3822600F0B54B /* RSTextDrawing.framework in Frameworks */,
84B06FE91ED3803A00F0B54B /* RSFeedFinder.framework in Frameworks */,
84B06FFD1ED3818D00F0B54B /* RSTree.framework in Frameworks */,
84B06FAE1ED37DBD00F0B54B /* RSCore.framework in Frameworks */,
@ -1014,6 +992,8 @@
849A97741ED9EC04007D329B /* TimelineTableCellView.swift */,
849A97701ED9EC04007D329B /* TimelineCellAppearance.swift */,
849A97721ED9EC04007D329B /* TimelineCellLayout.swift */,
84E185B2203B74E500F69BFA /* SingleLineTextFieldSizer.swift */,
84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */,
849A97711ED9EC04007D329B /* TimelineCellData.swift */,
849A97731ED9EC04007D329B /* TimelineStringUtilities.swift */,
849A97751ED9EC04007D329B /* UnreadIndicatorView.swift */,
@ -1148,7 +1128,6 @@
84B06F961ED37DA000F0B54B /* RSDatabase.xcodeproj */,
84B06FE01ED3803200F0B54B /* RSFeedFinder.xcodeproj */,
849B89681F0349C100578A8D /* RSParser.xcodeproj */,
84B070011ED3821800F0B54B /* RSTextDrawing.xcodeproj */,
84B06FF41ED3818000F0B54B /* RSTree.xcodeproj */,
84B06FB61ED37E8B00F0B54B /* RSWeb.xcodeproj */,
);
@ -1252,15 +1231,6 @@
name = Products;
sourceTree = "<group>";
};
84B070021ED3821800F0B54B /* Products */ = {
isa = PBXGroup;
children = (
84B070071ED3821900F0B54B /* RSTextDrawing.framework */,
84B070091ED3821900F0B54B /* RSTextDrawingTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
84BB4B621F1174D400858766 /* Products */ = {
isa = PBXGroup;
children = (
@ -1484,7 +1454,6 @@
84B06FD21ED37F7D00F0B54B /* PBXTargetDependency */,
84B06FEC1ED3803A00F0B54B /* PBXTargetDependency */,
84B070001ED3818D00F0B54B /* PBXTargetDependency */,
84B0700D1ED3822600F0B54B /* PBXTargetDependency */,
849B897F1F0349D000578A8D /* PBXTargetDependency */,
84BB4B7A1F11753300858766 /* PBXTargetDependency */,
846E77401F6EF67A00A165E2 /* PBXTargetDependency */,
@ -1598,10 +1567,6 @@
ProductGroup = 849B89691F0349C100578A8D /* Products */;
ProjectRef = 849B89681F0349C100578A8D /* RSParser.xcodeproj */;
},
{
ProductGroup = 84B070021ED3821800F0B54B /* Products */;
ProjectRef = 84B070011ED3821800F0B54B /* RSTextDrawing.xcodeproj */;
},
{
ProductGroup = 84B06FF51ED3818000F0B54B /* Products */;
ProjectRef = 84B06FF41ED3818000F0B54B /* RSTree.xcodeproj */;
@ -1770,20 +1735,6 @@
remoteRef = 84B06FFB1ED3818000F0B54B /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
84B070071ED3821900F0B54B /* RSTextDrawing.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
path = RSTextDrawing.framework;
remoteRef = 84B070061ED3821900F0B54B /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
84B070091ED3821900F0B54B /* RSTextDrawingTests.xctest */ = {
isa = PBXReferenceProxy;
fileType = wrapper.cfbundle;
path = RSTextDrawingTests.xctest;
remoteRef = 84B070081ED3821900F0B54B /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
84BB4B681F1174D400858766 /* Data.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
@ -1958,6 +1909,7 @@
849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */,
84E8E0DB202EC49300562D8F /* TimelineViewController+ContextualMenus.swift in Sources */,
849A97791ED9EC04007D329B /* TimelineStringUtilities.swift in Sources */,
84E185C3203BB12600F69BFA /* MultilineTextFieldSizer.swift in Sources */,
843A3B5620311E7700BF76EC /* FeedListOutlineView.swift in Sources */,
8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */,
84AD1EAA2031617300BC20B7 /* FolderPasteboardWriter.swift in Sources */,
@ -2010,6 +1962,7 @@
849A97851ED9ECCD007D329B /* PreferencesWindowController.swift in Sources */,
D5F4EDB720074D6500B9E363 /* Feed+Scriptability.swift in Sources */,
84E850861FCB60CE0072EA88 /* AuthorAvatarDownloader.swift in Sources */,
84E185B3203B74E500F69BFA /* SingleLineTextFieldSizer.swift in Sources */,
8414AD251FCF5A1E00955102 /* TimelineHeaderView.swift in Sources */,
849EE71F20391DF20082A1EA /* MainWindowToolbarDelegate.swift in Sources */,
849A977A1ED9EC04007D329B /* TimelineTableCellView.swift in Sources */,
@ -2106,11 +2059,6 @@
name = RSTree;
targetProxy = 84B06FFF1ED3818D00F0B54B /* PBXContainerItemProxy */;
};
84B0700D1ED3822600F0B54B /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = RSTextDrawing;
targetProxy = 84B0700C1ED3822600F0B54B /* PBXContainerItemProxy */;
};
84BB4B7A1F11753300858766 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = Data;

View File

@ -27,6 +27,7 @@ final class AppDefaults {
static let timelineSortDirection = "timelineSortDirection"
static let detailFontSize = "detailFontSize"
static let openInBrowserInBackground = "openInBrowserInBackground"
static let mainWindowWidths = "mainWindowWidths"
// Hidden prefs
static let showTitleOnMainWindow = "KafasisTitleMode"
@ -86,6 +87,15 @@ final class AppDefaults {
}
}
var mainWindowWidths: [Int]? {
get {
return UserDefaults.standard.object(forKey: Key.mainWindowWidths) as? [Int]
}
set {
UserDefaults.standard.set(newValue, forKey: Key.mainWindowWidths)
}
}
private init() {
AppDefaults.registerDefaults()

View File

@ -9,7 +9,6 @@
import AppKit
import DB5
import Data
import RSTextDrawing
import RSTree
import RSWeb
import Account
@ -165,9 +164,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
func applicationDidResignActive(_ notification: Notification) {
RSSingleLineRenderer.emptyCache()
RSMultiLineRenderer.emptyCache()
TimelineCellData.emptyCache()
timelineEmptyCaches()
saveState()

View File

@ -535,7 +535,7 @@
<constraint firstItem="KZz-oC-IY4" firstAttribute="leading" secondItem="rE6-fd-xjY" secondAttribute="leading" id="dqI-OO-8A0"/>
</constraints>
</customView>
<scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="96" horizontalPageScroll="10" verticalLineScroll="96" verticalPageScroll="10" hasHorizontalScroller="NO" horizontalScrollElasticity="none" translatesAutoresizingMaskIntoConstraints="NO" id="Kfs-n2-RYk">
<scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="97" horizontalPageScroll="10" verticalLineScroll="97" verticalPageScroll="10" hasHorizontalScroller="NO" horizontalScrollElasticity="none" translatesAutoresizingMaskIntoConstraints="NO" id="Kfs-n2-RYk">
<rect key="frame" x="0.0" y="0.0" width="450" height="198"/>
<clipView key="contentView" copiesOnScroll="NO" id="yAN-Ex-RC7">
<rect key="frame" x="0.0" y="0.0" width="450" height="198"/>
@ -544,6 +544,7 @@
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" columnReordering="NO" columnResizing="NO" autosaveColumns="NO" typeSelect="NO" rowHeight="96" viewBased="YES" id="DRs-j8-R9a" customClass="TimelineTableView" customModule="Evergreen" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="450" height="198"/>
<autoresizingMask key="autoresizingMask"/>
<size key="intercellSpacing" width="0.0" height="1"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
<color key="gridColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<tableColumns>
@ -565,7 +566,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</tableCellView>
<customView identifier="timelineRow" id="54E-Vz-WND" customClass="TimelineTableRowView" customModule="Evergreen" customModuleProvider="target">
<rect key="frame" x="0.0" y="68" width="447" height="96"/>
<rect key="frame" x="0.0" y="69" width="447" height="96"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
</customView>
</prototypeCellViews>
@ -740,7 +741,7 @@
</constraints>
</view>
<color key="borderColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<color key="fillColor" white="0.95999999999999996" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="fillColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</box>
</subviews>
<constraints>

View File

@ -91,7 +91,7 @@ extension FeedListViewController: NSOutlineViewDataSource {
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
return nodeForItem(item as AnyObject?).childNodes![index]
return nodeForItem(item as AnyObject?).childNodes[index]
}
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {

View File

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

View File

@ -147,9 +147,8 @@ private extension AddFeedWindowController {
menuItem.representedObject = folderTreeController.rootNode.representedObject
menu.addItem(menuItem)
if let childNodes = folderTreeController.rootNode.childNodes {
addFolderItemsToMenuWithNodes(menu: menu, nodes: childNodes, indentationLevel: 1)
}
let childNodes = folderTreeController.rootNode.childNodes
addFolderItemsToMenuWithNodes(menu: menu, nodes: childNodes, indentationLevel: 1)
return menu
}
@ -166,7 +165,7 @@ private extension AddFeedWindowController {
menu.addItem(menuItem)
if oneNode.numberOfChildNodes > 0 {
addFolderItemsToMenuWithNodes(menu: menu, nodes: oneNode.childNodes!, indentationLevel: indentationLevel + 1)
addFolderItemsToMenuWithNodes(menu: menu, nodes: oneNode.childNodes, indentationLevel: indentationLevel + 1)
}
}
}

View File

@ -163,12 +163,10 @@ class ArticleRenderer {
var d = [String: String]()
let title = titleOrTitleLink()
d["newsitem_title"] = title
d["article_title"] = title
d["title"] = title
let body = article.body == nil ? "" : article.body
d["article_description"] = body
d["newsitem_description"] = body
d["body"] = body
d["avatars"] = ""
var didAddAvatar = false
@ -222,7 +220,7 @@ class ArticleRenderer {
func html(dimension: Int) -> String {
let imageTag = "<img src=\"\(imageURL)\" width=\"\(dimension)\" height=\"\(dimension)\""
let imageTag = "<img src=\"\(imageURL)\" width=\(dimension) height=\(dimension) />"
if let url = url {
return linkWithText(imageTag, url)
}
@ -238,7 +236,20 @@ class ArticleRenderer {
if let favicon = appDelegate.faviconDownloader.favicon(for: feed) {
if let s = base64String(forImage: favicon) {
let imgTag = "<img src=\"data:image/tiff;base64, " + s + "\" height=16 width=16 />"
var dimension = min(favicon.size.height, CGFloat(avatarDimension)) // Assuming square images.
dimension = max(dimension, 16) // Some favicons say theyre < 16. Force them larger.
if dimension >= CGFloat(avatarDimension) * 0.8 { //Close enough to scale up.
dimension = CGFloat(avatarDimension)
}
let imgTag: String
if dimension >= CGFloat(avatarDimension) {
// Use rounded corners.
imgTag = "<img src=\"data:image/tiff;base64, " + s + "\" height=\(Int(dimension)) width=\(Int(dimension)) style=\"border-radius:4px\" />"
}
else {
imgTag = "<img src=\"data:image/tiff;base64, " + s + "\" height=\(Int(dimension)) width=\(Int(dimension)) />"
}
ArticleRenderer.faviconImgTagCache[feed] = imgTag
return imgTag
}
@ -449,7 +460,7 @@ class ArticleRenderer {
s += "\n\n</body></html>"
// print(s)
//print(s)
return s

View File

@ -1,10 +1,10 @@
body {
color: #444;
background-color: white;
margin-top: 42px;
margin-bottom: 100px;
margin-left: 80px;
margin-right: 80px;
margin-top: 20px;
margin-bottom: 64px;
margin-left: 64px;
margin-right: 64px;
font-family: -apple-system;
font-size: 18px;
}
@ -12,42 +12,34 @@ a {
text-decoration: none;
}
a, a:link, a:visited {
color: #015cdc;
color: #416ED2;
}
a:hover {
text-decoration: underline;
}
.articleDateline {
color: rgba(0, 0, 0, 0.3);
border-bottxom: 1px solid rgba(0, 0, 0, 0.1);
border-txop: 1px solid rgba(0, 0, 0, 0.1);
padding-bottom: 0px;
padding-top: 0px;
margin-bottom: 25px;
font-style: italic;
.headerTable {
width: 100%;
height: 68px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.articleDateline a:link, #articleDateline a:visited {
.header a:link, .header a:visited {
color: rgba(0, 0, 0, 0.3);
}
#articleDateline img {
border-radius: 7px;
.header {
color: rgba(0, 0, 0, 0.3);
}
.articleDate {
color: #2db6ff;
text-align: right;
border-bottom: 1px solid rgba(0, 0, 0, 0.5);
padding-bottom: 8px;
margin-bottom: 1em;
.feedlink {
font-weight: bold;
}
#articleDescription {
line-height: 1.5em;
.feedlink a:link, .feedlink a:visited {
color: rgba(0, 0, 0, 0.6);
}
#authorAvatar {
margin-top: 2em;
text-align: left;
.avatar img {
border-radius: 4px;
}
#authorAvatar img {
border-radius: 5px;
.feedIcon {
border-radius: 4px;
}
.rightAlign {
text-align: right;
@ -55,35 +47,26 @@ a:hover {
.leftAlign {
text-align: left;
}
.articleTitle {
border-top: 1px solid rgba(0, 0, 0, 0.1);
margin-top: 26px;
}
.headerContainer {
border-botxtom: 1px solid rgba(0, 0, 0, 0.1);
.articleDateline {
color: rgba(0, 0, 0, 0.3);
margin-bottom: 25px;
font-weight: bold;
}
.articleDateline, .articleDateline a:link, .articleDateline a:visited {
color: rgba(0, 0, 0, 0.3);
}
.header {
color: rgba(0, 0, 0, 0.3);
}
.headerTable {
width: 100%;
height: 68px;
}
.avatar img {
border-radius: 7px;
}
.headerContainer a:link, .headerContainer a:visited {
color: rgba(0, 0, 0, 0.3);
}
.feedIcon {
border-radius: 5px;
.articleBody {
line-height: 1.6em;
}
h1 {
line-height: 1.15em;
font-weight: medium;
}
h1, h2, h3, h4, h5, h6 {
font-family: -apple-system, "Helvetica Neue"
font-weight: bold;
}
code, pre {
font-family: "SF Mono", Menlo, "Courier New", Courier, monospace;
@ -129,6 +112,7 @@ img[src*="//ads."],
img[src*="doubleclick"],
img[src*="feedads"],
img[src*="feedburner"],
img[src*="feedblitz"],
img[src*="share-buttons"] {
display: none !important;
}

View File

@ -1,14 +1,12 @@
<!--<div class="headerContainer"><p>[[avatars]]</p>
<p>[[feedlink]] • [[byline]] • [[date_short]]</p></div>-->
<div class="headerContainer">
<table cellpadding=0 cellspacing=0 border=0 class="headerTable">
<tr>
<td class="header leftAlign"><b>[[feedlink]]</b><br />[[byline]]</td>
<td class="header leftAlign"><span class="feedlink">[[feedlink]]</span><br />[[byline]]</td>
[[avatars]]
</tr>
</table>
</div>
<div class="articleTitle"><h1>[[newsitem_title]]</h1></div>
<div class="articleTitle"><h1>[[title]]</h1></div>
<div class="articleDateline">[[date_medium]]</div>
<div id="articleDescription">[[newsitem_description]]</div>
<div class="articleBody">[[body]]</div>

View File

@ -31,6 +31,8 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
return window?.toolbar?.existingItem(withIdentifier: .Share)
}
private static var detailViewMinimumThickness = 384
// MARK: - NSWindowController
override func windowDidLoad() {
@ -53,8 +55,9 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
}
}
detailSplitViewItem?.minimumThickness = 384
detailSplitViewItem?.minimumThickness = CGFloat(MainWindowController.detailViewMinimumThickness)
restoreSplitViewState()
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillTerminate(_:)), name: NSApplication.willTerminateNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(refreshProgressDidChange(_:)), name: .AccountRefreshDidBegin, object: nil)
@ -72,11 +75,10 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
func saveState() {
// TODO: save width of split view and anything else that should be saved.
saveSplitViewState()
}
func selectedObjectsInSidebar() -> [AnyObject]? {
return sidebarViewController?.selectedObjects
@ -86,6 +88,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
@objc func applicationWillTerminate(_ note: Notification) {
saveState()
window?.saveFrame(usingName: windowAutosaveName)
}
@ -483,5 +486,39 @@ private extension MainWindowController {
window?.title = "\(appDelegate.appName!) (\(unreadCount))"
}
}
func saveSplitViewState() {
// TODO: Update this for multiple windows.
guard let splitView = splitViewController?.splitView else {
return
}
let widths = splitView.arrangedSubviews.map{ Int(floor($0.frame.width)) }
AppDefaults.shared.mainWindowWidths = widths
}
func restoreSplitViewState() {
// TODO: Update this for multiple windows.
guard let splitView = splitViewController?.splitView, let widths = AppDefaults.shared.mainWindowWidths, widths.count == 3, let window = window else {
return
}
let windowWidth = Int(floor(window.frame.width))
let dividerThickness: Int = Int(splitView.dividerThickness)
let sidebarWidth: Int = widths[0]
let timelineWidth: Int = widths[1]
// Make sure the detail view has its mimimum thickness, at least.
if windowWidth < sidebarWidth + dividerThickness + timelineWidth + dividerThickness + MainWindowController.detailViewMinimumThickness {
return
}
splitView.setPosition(CGFloat(sidebarWidth), ofDividerAt: 0)
splitView.setPosition(CGFloat(sidebarWidth + dividerThickness + timelineWidth), ofDividerAt: 1)
}
}

View File

@ -10,7 +10,7 @@ import AppKit
class MainWindowSplitView: NSSplitView {
private let splitViewDividerColor = NSColor(calibratedWhite: 0.60, alpha: 1.0)
private let splitViewDividerColor = NSColor(calibratedWhite: 0.75, alpha: 1.0)
override var dividerColor: NSColor {
return splitViewDividerColor

View File

@ -28,8 +28,10 @@ struct SidebarCellLayout {
}
self.faviconRect = rFavicon
textField.sizeToFit()
let textFieldSize = textField.frame.size
// textField.sizeToFit()
// let textFieldSize = textField.fittingSize//frame.size
let textFieldSize = SingleLineTextFieldSizer.size(for: textField.stringValue, font: textField.font!)
var rTextField = NSRect(x: 0.0, y: 0.0, width: textFieldSize.width, height: textFieldSize.height)
if shouldShowImage {
rTextField.origin.x = NSMaxX(rFavicon) + appearance.imageMarginRight

View File

@ -28,7 +28,7 @@ import RSCore
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
return nodeForItem(item as AnyObject?).childNodes![index]
return nodeForItem(item as AnyObject?).childNodes[index]
}
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {

View File

@ -21,6 +21,8 @@ class SidebarOutlineView : NSOutlineView {
// Dont allow the pseudo-feeds at the top level to be indented.
var frame = super.frameOfCell(atColumn: column, row: row)
frame.origin.x += 4.0
frame.size.width -= 4.0
let node = item(atRow: row) as! Node
guard let parentNode = node.parent, parentNode.isRoot else {

View File

@ -129,7 +129,7 @@ import RSCore
let nodesToDelete = treeController.normalizedSelectedNodes(selectedNodes)
guard let undoManager = undoManager, let deleteCommand = DeleteFromSidebarCommand(nodesToDelete: nodesToDelete, undoManager: undoManager) else {
guard let undoManager = undoManager, let deleteCommand = DeleteFromSidebarCommand(nodesToDelete: nodesToDelete, treeController: treeController, undoManager: undoManager) else {
return
}

View File

@ -0,0 +1,209 @@
//
// MultilineTextFieldSizer.swift
// Evergreen
//
// Created by Brent Simmons on 2/19/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import AppKit
// Get the height of an NSTextField given a string, font, and width.
// Uses a cache. Avoids actually measuring text as much as possible.
// Main thread only.
typealias WidthHeightCache = [Int: Int] // width: height
private struct TextFieldSizerSpecifier: Equatable, Hashable {
let numberOfLines: Int
let font: NSFont
let hashValue: Int
init(numberOfLines: Int, font: NSFont) {
self.numberOfLines = numberOfLines
self.font = font
self.hashValue = font.hashValue ^ numberOfLines
}
static func ==(lhs : TextFieldSizerSpecifier, rhs: TextFieldSizerSpecifier) -> Bool {
return lhs.numberOfLines == rhs.numberOfLines && lhs.font == rhs.font
}
}
struct TextFieldSizeInfo {
let size: NSSize // Integral size (ceiled)
let numberOfLinesUsed: Int // A two-line text field may only use one line, for instance. This would equal 1, then.
init(size: NSSize, numberOfLinesUsed: Int) {
self.size = size
self.numberOfLinesUsed = numberOfLinesUsed
}
}
final class MultilineTextFieldSizer {
private let numberOfLines: Int
private let font: NSFont
private let textField:NSTextField
private let singleLineHeightEstimate: Int
private let doubleLineHeightEstimate: Int
private var cache = [String: WidthHeightCache]() // Each string has a cache.
private static var sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]()
private init(numberOfLines: Int, font: NSFont) {
self.numberOfLines = numberOfLines
self.font = font
self.textField = MultilineTextFieldSizer.createTextField(numberOfLines, font)
self.singleLineHeightEstimate = MultilineTextFieldSizer.calculateHeight("AqLjJ0/y", 200, self.textField)
self.doubleLineHeightEstimate = MultilineTextFieldSizer.calculateHeight("AqLjJ0/y\nAqLjJ0/y", 200, self.textField)
}
static func size(for string: String, font: NSFont, numberOfLines: Int, width: Int) -> TextFieldSizeInfo {
return sizer(numberOfLines: numberOfLines, font: font).sizeInfo(for: string, width: width)
}
static func emptyCache() {
sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]()
}
}
// MARK: - Private
private extension MultilineTextFieldSizer {
static func sizer(numberOfLines: Int, font: NSFont) -> MultilineTextFieldSizer {
let specifier = TextFieldSizerSpecifier(numberOfLines: numberOfLines, font: font)
if let cachedSizer = sizers[specifier] {
return cachedSizer
}
let newSizer = MultilineTextFieldSizer(numberOfLines: numberOfLines, font: font)
sizers[specifier] = newSizer
return newSizer
}
func sizeInfo(for string: String, width: Int) -> TextFieldSizeInfo {
let textFieldHeight = height(for: string, width: width)
let numberOfLinesUsed = numberOfLines(for: textFieldHeight)
let size = NSSize(width: width, height: textFieldHeight)
let sizeInfo = TextFieldSizeInfo(size: size, numberOfLinesUsed: numberOfLinesUsed)
return sizeInfo
}
func height(for string: String, width: Int) -> Int {
if cache[string] == nil {
cache[string] = WidthHeightCache()
}
if let height = cache[string]![width] {
return height
}
if let height = heightConsideringNeighbors(cache[string]!, width) {
return height
}
let height = calculateHeight(string, width)
cache[string]![width] = height
return height
}
static func createTextField(_ numberOfLines: Int, _ font: NSFont) -> NSTextField {
let textField = NSTextField(wrappingLabelWithString: "")
textField.usesSingleLineMode = false
textField.maximumNumberOfLines = numberOfLines
textField.isEditable = false
textField.font = font
textField.allowsDefaultTighteningForTruncation = false
return textField
}
func calculateHeight(_ string: String, _ width: Int) -> Int {
return MultilineTextFieldSizer.calculateHeight(string, width, textField)
}
static func calculateHeight(_ string: String, _ width: Int, _ textField: NSTextField) -> Int {
textField.stringValue = string
textField.preferredMaxLayoutWidth = CGFloat(width)
let size = textField.fittingSize
return Int(ceil(size.height))
}
func numberOfLines(for height: Int) -> Int {
// Well have to see if this really works reliably.
let averageHeight = CGFloat(doubleLineHeightEstimate) / 2.0
let lines = Int(round(CGFloat(height) / averageHeight))
return lines
}
func heightIsProbablySingleLineHeight(_ height: Int) -> Bool {
return heightIsProbablyEqualToEstimate(height, singleLineHeightEstimate)
}
func heightIsProbablyDoubleLineHeight(_ height: Int) -> Bool {
return heightIsProbablyEqualToEstimate(height, doubleLineHeightEstimate)
}
func heightIsProbablyEqualToEstimate(_ height: Int, _ estimate: Int) -> Bool {
let slop = 4
let minimum = estimate - slop
let maximum = estimate + slop
return height >= minimum && height <= maximum
}
func heightConsideringNeighbors(_ heightCache: WidthHeightCache, _ width: Int) -> Int? {
// Given width, if the height at width - something and width + something is equal,
// then that height must be correct for the given width.
// Also:
// If a narrower neighbors height is single line height, then this wider width must also be single-line height.
// If a wider neighbors height is double line height, and numberOfLines == 2, then this narrower width must able be double-line height.
var smallNeighbor = (width: 0, height: 0)
var largeNeighbor = (width: 0, height: 0)
for (oneWidth, oneHeight) in heightCache {
if oneWidth < width && heightIsProbablySingleLineHeight(oneHeight) {
return oneHeight
}
if numberOfLines == 2 && oneWidth > width && heightIsProbablyDoubleLineHeight(oneHeight) {
return oneHeight
}
if oneWidth < width && (oneWidth > smallNeighbor.width || smallNeighbor.width == 0) {
smallNeighbor = (oneWidth, oneHeight)
}
else if oneWidth > width && (oneWidth < largeNeighbor.width || largeNeighbor.width == 0) {
largeNeighbor = (oneWidth, oneHeight)
}
if smallNeighbor.width != 0 && smallNeighbor.height == largeNeighbor.height {
return smallNeighbor.height
}
}
return nil
}
}

View File

@ -0,0 +1,68 @@
//
// SingleLineTextFieldSizer.swift
// Evergreen
//
// Created by Brent Simmons on 2/19/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import AppKit
// Get the size of an NSTextField configured with a specific font with a specific size.
// Uses a cache.
// Main thready only.
final class SingleLineTextFieldSizer {
let font: NSFont
private let textField: NSTextField
private var cache = [String: NSSize]()
init(font: NSFont) {
self.textField = NSTextField(labelWithString: "")
self.textField.font = font
self.font = font
}
func size(for text: String) -> NSSize {
if let cachedSize = cache[text] {
return cachedSize
}
textField.stringValue = text
var calculatedSize = textField.fittingSize
calculatedSize.height = ceil(calculatedSize.height)
calculatedSize.width = ceil(calculatedSize.width)
cache[text] = calculatedSize
return calculatedSize
}
static private var sizers = [NSFont: SingleLineTextFieldSizer]()
static func sizer(for font: NSFont) -> SingleLineTextFieldSizer {
if let cachedSizer = sizers[font] {
return cachedSizer
}
let newSizer = SingleLineTextFieldSizer(font: font)
sizers[font] = newSizer
return newSizer
}
// Use this call. Its easiest.
static func size(for text: String, font: NSFont) -> NSSize {
return sizer(for: font).size(for: text)
}
static func emptyCache() {
sizers = [NSFont: SingleLineTextFieldSizer]()
}
}

View File

@ -18,11 +18,13 @@ struct TimelineCellAppearance: Equatable {
let dateColor: NSColor
let dateMarginLeft: CGFloat
let dateMarginBottom: CGFloat
let dateFont: NSFont
let titleColor: NSColor
let titleFont: NSFont
let titleBottomMargin: CGFloat
let titleNumberOfLines: Int
let textColor: NSColor
let textFont: NSFont
@ -60,13 +62,15 @@ struct TimelineCellAppearance: Equatable {
self.feedNameFont = NSFont.systemFont(ofSize: smallItemFontSize)
self.dateColor = theme.color(forKey: "MainWindow.Timeline.cell.dateColor")
self.dateFont = NSFont.systemFont(ofSize: smallItemFontSize)
self.dateFont = NSFont.systemFont(ofSize: smallItemFontSize, weight: NSFont.Weight.bold)
self.dateMarginLeft = theme.float(forKey: "MainWindow.Timeline.cell.dateMarginLeft")
self.dateMarginBottom = theme.float(forKey: "MainWindow.Timeline.cell.dateMarginBottom")
self.titleColor = theme.color(forKey: "MainWindow.Timeline.cell.titleColor")
self.titleFont = NSFont.systemFont(ofSize: largeItemFontSize, weight: NSFont.Weight.semibold)
self.titleBottomMargin = theme.float(forKey: "MainWindow.Timeline.cell.titleMarginBottom")
self.titleNumberOfLines = theme.integer(forKey: "MainWindow.Timeline.cell.titleMaximumLines")
self.textColor = theme.color(forKey: "MainWindow.Timeline.cell.textColor")
self.textFont = NSFont.systemFont(ofSize: largeItemFontSize)
@ -90,12 +94,10 @@ struct TimelineCellAppearance: Equatable {
self.showAvatar = showAvatar
let margin = self.cellPadding.left + self.unreadCircleDimension + self.unreadCircleMarginRight
// if showAvatar {
// margin += (self.avatarSize.width + self.avatarMarginRight)
// }
self.boxLeftMargin = margin
}
// TODO: update the below
static func ==(lhs: TimelineCellAppearance, rhs: TimelineCellAppearance) -> Bool {
return lhs.boxLeftMargin == rhs.boxLeftMargin && lhs.showAvatar == rhs.showAvatar && lhs.cellPadding == rhs.cellPadding && lhs.feedNameColor == rhs.feedNameColor && lhs.feedNameFont == rhs.feedNameFont && lhs.dateColor == rhs.dateColor && lhs.dateMarginLeft == rhs.dateMarginLeft && lhs.dateFont == rhs.dateFont && lhs.titleColor == rhs.titleColor && lhs.titleFont == rhs.titleFont && lhs.titleBottomMargin == rhs.titleBottomMargin && lhs.textColor == rhs.textColor && lhs.textFont == rhs.textFont && lhs.unreadCircleColor == rhs.unreadCircleColor && lhs.unreadCircleDimension == rhs.unreadCircleDimension && lhs.unreadCircleMarginRight == rhs.unreadCircleMarginRight && lhs.gridColor == rhs.gridColor && lhs.avatarSize == rhs.avatarSize && lhs.avatarMarginRight == rhs.avatarMarginRight && lhs.avatarAdjustmentTop == rhs.avatarAdjustmentTop && lhs.avatarCornerRadius == rhs.avatarCornerRadius

View File

@ -9,19 +9,12 @@
import AppKit
import Data
var attributedTitleCache = [String: NSAttributedString]()
var attributedDateCache = [String: NSAttributedString]()
var attributedFeedNameCache = [String: NSAttributedString]()
struct TimelineCellData {
let title: String
let text: String
let attributedTitle: NSAttributedString //title + text
let dateString: String
let attributedDateString: NSAttributedString
let feedName: String
let attributedFeedName: NSAttributedString
let showFeedName: Bool
let avatar: NSImage? // feed icon, user avatar, or favicon
let showAvatar: Bool // Make space even when avatar is nil
@ -34,23 +27,7 @@ struct TimelineCellData {
self.title = timelineTruncatedTitle(article)
self.text = timelineTruncatedSummary(article)
let attributedTitleCacheKey = "_title: " + self.title + "_text: " + self.text
if let s = attributedTitleCache[attributedTitleCacheKey] {
self.attributedTitle = s
}
else {
self.attributedTitle = attributedTitleString(title, text, appearance)
attributedTitleCache[attributedTitleCacheKey] = self.attributedTitle
}
self.dateString = timelineDateString(article.logicalDatePublished)
if let s = attributedDateCache[self.dateString] {
self.attributedDateString = s
}
else {
self.attributedDateString = NSAttributedString(string: self.dateString, attributes: [NSAttributedStringKey.foregroundColor: appearance.dateColor, NSAttributedStringKey.font: appearance.dateFont])
attributedDateCache[self.dateString] = self.attributedDateString
}
if let feedName = feedName {
self.feedName = timelineTruncatedFeedName(feedName)
@ -58,13 +35,6 @@ struct TimelineCellData {
else {
self.feedName = ""
}
if let s = attributedFeedNameCache[self.feedName] {
self.attributedFeedName = s
}
else {
self.attributedFeedName = NSAttributedString(string: self.feedName, attributes: [NSAttributedStringKey.foregroundColor: appearance.feedNameColor, NSAttributedStringKey.font: appearance.feedNameFont])
attributedFeedNameCache[self.feedName] = self.attributedFeedName
}
self.showFeedName = showFeedName
@ -79,12 +49,9 @@ struct TimelineCellData {
init() { //Empty
self.title = ""
self.attributedTitle = NSAttributedString(string: "")
self.text = ""
self.dateString = ""
self.attributedDateString = NSAttributedString(string: "")
self.feedName = ""
self.attributedFeedName = NSAttributedString(string: "")
self.showFeedName = false
self.showAvatar = false
self.avatar = nil
@ -92,31 +59,4 @@ struct TimelineCellData {
self.read = true
self.starred = false
}
static func emptyCache() {
attributedTitleCache = [String: NSAttributedString]()
attributedDateCache = [String: NSAttributedString]()
attributedFeedNameCache = [String: NSAttributedString]()
}
}
let emptyCellData = TimelineCellData()
private func attributedTitleString(_ title: String, _ text: String, _ appearance: TimelineCellAppearance) -> NSAttributedString {
if !title.isEmpty && !text.isEmpty {
let titleMutable = NSMutableAttributedString(string: title, attributes: [NSAttributedStringKey.foregroundColor: appearance.titleColor, NSAttributedStringKey.font: appearance.titleFont])
let attributedText = NSAttributedString(string: "\n" + text, attributes: [NSAttributedStringKey.foregroundColor: appearance.textColor, NSAttributedStringKey.font: appearance.textFont])
titleMutable.append(attributedText)
return titleMutable
}
if !title.isEmpty && text.isEmpty {
return NSAttributedString(string: title, attributes: [NSAttributedStringKey.foregroundColor: appearance.titleColor, NSAttributedStringKey.font: appearance.titleFont])
}
return NSAttributedString(string: text, attributes: [NSAttributedStringKey.foregroundColor: appearance.textOnlyColor, NSAttributedStringKey.font: appearance.textOnlyFont])
}

View File

@ -7,7 +7,6 @@
//
import AppKit
import RSTextDrawing
import RSCore
struct TimelineCellLayout {
@ -17,46 +16,72 @@ struct TimelineCellLayout {
let feedNameRect: NSRect
let dateRect: NSRect
let titleRect: NSRect
let numberOfLinesForTitle: Int
let summaryRect: NSRect
let textRect: NSRect
let unreadIndicatorRect: NSRect
let starRect: NSRect
let avatarImageRect: NSRect
let paddingBottom: CGFloat
init(width: CGFloat, feedNameRect: NSRect, dateRect: NSRect, titleRect: NSRect, unreadIndicatorRect: NSRect, starRect: NSRect, avatarImageRect: NSRect, paddingBottom: CGFloat) {
init(width: CGFloat, height: CGFloat, feedNameRect: NSRect, dateRect: NSRect, titleRect: NSRect, numberOfLinesForTitle: Int, summaryRect: NSRect, textRect: NSRect, unreadIndicatorRect: NSRect, starRect: NSRect, avatarImageRect: NSRect, paddingBottom: CGFloat) {
self.width = width
self.feedNameRect = feedNameRect
self.dateRect = dateRect
self.titleRect = titleRect
self.numberOfLinesForTitle = numberOfLinesForTitle
self.summaryRect = summaryRect
self.textRect = textRect
self.unreadIndicatorRect = unreadIndicatorRect
self.starRect = starRect
self.avatarImageRect = avatarImageRect
self.paddingBottom = paddingBottom
self.height = [feedNameRect, dateRect, titleRect, unreadIndicatorRect, avatarImageRect].maxY() + paddingBottom
if height > 0.1 {
self.height = height
}
else {
self.height = [feedNameRect, dateRect, titleRect, summaryRect, textRect, unreadIndicatorRect, avatarImageRect].maxY() + paddingBottom
}
}
init(width: CGFloat, cellData: TimelineCellData, appearance: TimelineCellAppearance) {
init(width: CGFloat, height: CGFloat, cellData: TimelineCellData, appearance: TimelineCellAppearance, hasAvatar: Bool) {
var textBoxRect = TimelineCellLayout.rectForTextBox(appearance, cellData, width)
// If height == 0.0, then height is calculated.
let (titleRect, titleLine1Rect) = TimelineCellLayout.rectsForTitle(textBoxRect, cellData)
let dateRect = TimelineCellLayout.rectForDate(textBoxRect, titleRect, appearance, cellData)
let showAvatar = hasAvatar && cellData.showAvatar
var textBoxRect = TimelineCellLayout.rectForTextBox(appearance, cellData, showAvatar, width)
let (titleRect, numberOfLinesForTitle) = TimelineCellLayout.rectForTitle(textBoxRect, appearance, cellData)
let summaryRect = numberOfLinesForTitle > 0 ? TimelineCellLayout.rectForSummary(textBoxRect, titleRect, numberOfLinesForTitle, appearance, cellData) : NSRect.zero
let textRect = numberOfLinesForTitle > 0 ? NSRect.zero : TimelineCellLayout.rectForText(textBoxRect, appearance, cellData)
var lastTextRect = titleRect
if numberOfLinesForTitle == 0 {
lastTextRect = textRect
}
else if numberOfLinesForTitle == 1 {
if summaryRect.height > 0.1 {
lastTextRect = summaryRect
}
}
let dateRect = TimelineCellLayout.rectForDate(textBoxRect, lastTextRect, appearance, cellData)
let feedNameRect = TimelineCellLayout.rectForFeedName(textBoxRect, dateRect, appearance, cellData)
let unreadIndicatorRect = TimelineCellLayout.rectForUnreadIndicator(appearance, titleLine1Rect)
let starRect = TimelineCellLayout.rectForStar(appearance, unreadIndicatorRect)
textBoxRect.size.height = ceil([titleRect, dateRect, feedNameRect].maxY() - textBoxRect.origin.y)
let avatarImageRect = TimelineCellLayout.rectForAvatar(cellData, appearance, textBoxRect, width)
textBoxRect.size.height = ceil([titleRect, summaryRect, textRect, dateRect, feedNameRect].maxY() - textBoxRect.origin.y)
let avatarImageRect = TimelineCellLayout.rectForAvatar(cellData, appearance, showAvatar, textBoxRect, width, height)
let unreadIndicatorRect = TimelineCellLayout.rectForUnreadIndicator(appearance, textBoxRect)
let starRect = TimelineCellLayout.rectForStar(appearance, unreadIndicatorRect)
let paddingBottom = appearance.cellPadding.bottom
self.init(width: width, feedNameRect: feedNameRect, dateRect: dateRect, titleRect: titleRect, unreadIndicatorRect: unreadIndicatorRect, starRect: starRect, avatarImageRect: avatarImageRect, paddingBottom: paddingBottom)
self.init(width: width, height: height, feedNameRect: feedNameRect, dateRect: dateRect, titleRect: titleRect, numberOfLinesForTitle: numberOfLinesForTitle, summaryRect: summaryRect, textRect: textRect, unreadIndicatorRect: unreadIndicatorRect, starRect: starRect, avatarImageRect: avatarImageRect, paddingBottom: paddingBottom)
}
static func height(for width: CGFloat, cellData: TimelineCellData, appearance: TimelineCellAppearance) -> CGFloat {
let layout = TimelineCellLayout(width: width, cellData: cellData, appearance: appearance)
let layout = TimelineCellLayout(width: width, height: 0.0, cellData: cellData, appearance: appearance, hasAvatar: true)
return layout.height
}
}
@ -65,35 +90,64 @@ struct TimelineCellLayout {
private extension TimelineCellLayout {
static func rectForTextBox(_ appearance: TimelineCellAppearance, _ cellData: TimelineCellData, _ width: CGFloat) -> NSRect {
static func rectForTextBox(_ appearance: TimelineCellAppearance, _ cellData: TimelineCellData, _ showAvatar: Bool, _ width: CGFloat) -> NSRect {
// Returned height is a placeholder. Not needed when this is calculated.
let textBoxOriginX = appearance.cellPadding.left + appearance.unreadCircleDimension + appearance.unreadCircleMarginRight
let textBoxMaxX = floor((width - appearance.cellPadding.right) - (cellData.showAvatar ? appearance.avatarSize.width + appearance.avatarMarginLeft : 0.0))
let textBoxMaxX = floor((width - appearance.cellPadding.right) - (showAvatar ? appearance.avatarSize.width + appearance.avatarMarginLeft : 0.0))
let textBoxWidth = floor(textBoxMaxX - textBoxOriginX)
let textBoxRect = NSRect(x: textBoxOriginX, y: appearance.cellPadding.top, width: textBoxWidth, height: 1000000)
return textBoxRect
}
static func rectsForTitle(_ textBoxRect: NSRect, _ cellData: TimelineCellData) -> (NSRect, NSRect) {
static func rectForTitle(_ textBoxRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> (NSRect, Int) {
var r = textBoxRect
let renderer = RSMultiLineRenderer(attributedTitle: cellData.attributedTitle)
let measurements = renderer.measurements(forWidth: textBoxRect.width)
r.size.height = CGFloat(measurements.height)
var rline1 = r
rline1.size.height = CGFloat(measurements.heightOfFirstLine)
return (r, rline1)
if cellData.title.isEmpty {
r.size.height = 0
return (r, 0)
}
let sizeInfo = MultilineTextFieldSizer.size(for: cellData.title, font: appearance.titleFont, numberOfLines: appearance.titleNumberOfLines, width: Int(textBoxRect.width))
r.size.height = sizeInfo.size.height
if sizeInfo.numberOfLinesUsed < 1 {
r.size.height = 0
}
return (r, sizeInfo.numberOfLinesUsed)
}
static func rectForDate(_ textBoxRect: NSRect, _ titleRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
static func rectForSummary(_ textBoxRect: NSRect, _ titleRect: NSRect, _ titleNumberOfLines: Int, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
return rectOfLineBelow(textBoxRect, titleRect, appearance.titleBottomMargin, cellData.attributedDateString)
if titleNumberOfLines >= appearance.titleNumberOfLines || cellData.text.isEmpty {
return NSRect.zero
}
return rectOfLineBelow(titleRect, titleRect, 0, cellData.text, appearance.textFont)
}
static func rectForText(_ textBoxRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
var r = textBoxRect
if cellData.text.isEmpty {
r.size.height = 0
return r
}
let sizeInfo = MultilineTextFieldSizer.size(for: cellData.text, font: appearance.textOnlyFont, numberOfLines: appearance.titleNumberOfLines, width: Int(textBoxRect.width))
r.size.height = sizeInfo.size.height
if sizeInfo.numberOfLinesUsed < 1 {
r.size.height = 0
}
return r
}
static func rectForDate(_ textBoxRect: NSRect, _ rectAbove: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
return rectOfLineBelow(textBoxRect, rectAbove, appearance.titleBottomMargin, cellData.dateString, appearance.dateFont)
}
static func rectForFeedName(_ textBoxRect: NSRect, _ dateRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
@ -102,18 +156,18 @@ private extension TimelineCellLayout {
return NSZeroRect
}
return rectOfLineBelow(textBoxRect, dateRect, appearance.titleBottomMargin, cellData.attributedFeedName)
return rectOfLineBelow(textBoxRect, dateRect, appearance.dateMarginBottom, cellData.feedName, appearance.feedNameFont)
}
static func rectOfLineBelow(_ textBoxRect: NSRect, _ rectAbove: NSRect, _ topMargin: CGFloat, _ attributedString: NSAttributedString) -> NSRect {
static func rectOfLineBelow(_ textBoxRect: NSRect, _ rectAbove: NSRect, _ topMargin: CGFloat, _ value: String, _ font: NSFont) -> NSRect {
let renderer = RSSingleLineRenderer(attributedTitle: attributedString)
let textFieldSize = SingleLineTextFieldSizer.size(for: value, font: font)
var r = NSZeroRect
r.size = renderer.size
r.size = textFieldSize
r.origin.y = NSMaxY(rectAbove) + topMargin
r.origin.x = textBoxRect.origin.x
var width = renderer.size.width
var width = textFieldSize.width
width = min(width, textBoxRect.size.width)
width = max(width, 0.0)
r.size.width = width
@ -121,13 +175,14 @@ private extension TimelineCellLayout {
return r
}
static func rectForUnreadIndicator(_ appearance: TimelineCellAppearance, _ titleLine1Rect: NSRect) -> NSRect {
static func rectForUnreadIndicator(_ appearance: TimelineCellAppearance, _ titleRect: NSRect) -> NSRect {
var r = NSZeroRect
r.size = NSSize(width: appearance.unreadCircleDimension, height: appearance.unreadCircleDimension)
r.origin.x = appearance.cellPadding.left
r = RSRectCenteredVerticallyInRect(r, titleLine1Rect)
r.origin.y += 1
r.origin.y = titleRect.minY + 6
// r = RSRectCenteredVerticallyInRect(r, titleRect)
// r.origin.y += 1
return r
}
@ -138,19 +193,26 @@ private extension TimelineCellLayout {
r.size.width = appearance.starDimension
r.size.height = appearance.starDimension
r.origin.x = floor(unreadIndicatorRect.origin.x - ((appearance.starDimension - appearance.unreadCircleDimension) / 2.0))
r.origin.y = unreadIndicatorRect.origin.y - 3.0
r.origin.y = unreadIndicatorRect.origin.y - 4.0
return r
}
static func rectForAvatar(_ cellData: TimelineCellData, _ appearance: TimelineCellAppearance, _ textBoxRect: NSRect, _ width: CGFloat) -> NSRect {
static func rectForAvatar(_ cellData: TimelineCellData, _ appearance: TimelineCellAppearance, _ showAvatar: Bool, _ textBoxRect: NSRect, _ width: CGFloat, _ height: CGFloat) -> NSRect {
var r = NSRect.zero
if !cellData.showAvatar {
if !showAvatar {
return r
}
r.size = appearance.avatarSize
r.origin.x = (width - appearance.cellPadding.right) - r.size.width
r = RSRectCenteredVerticallyInRect(r, textBoxRect)
if height > 0.1 {
let bounds = NSRect(x: 0.0, y: 0.0, width: width, height: height)
r = RSRectCenteredVerticallyInRect(r, bounds)
}
else {
r = RSRectCenteredVerticallyInRect(r, textBoxRect)
}
return r
}

View File

@ -10,12 +10,13 @@ import Foundation
import Data
import RSParser
// TODO: Dont make all this at top level.
private var truncatedFeedNameCache = [String: String]()
private let truncatedTitleCache = NSMutableDictionary()
private let normalizedTextCache = NSMutableDictionary()
private let textCache = NSMutableDictionary()
private let summaryCache = NSMutableDictionary()
//private var summaryCache = [String: String]()
func timelineEmptyCaches() {
@ -128,7 +129,9 @@ func timelineSummaryForArticle(_ article: Article) -> String {
var s = body.rs_string(byStrippingHTML: 300)
s = timelineNormalizedText(s)
if s == "Comments" { // Hacker News.
s = ""
}
summaryCache[body] = s
return s
}

View File

@ -7,35 +7,33 @@
//
import Foundation
import RSTextDrawing
import RSCore
class TimelineTableCellView: NSTableCellView {
private let titleView = RSMultiLineView(frame: NSZeroRect)
private let titleView = TimelineTableCellView.multiLineTextField()
private let summaryView = TimelineTableCellView.singleLineTextField()
private let textView = TimelineTableCellView.multiLineTextField()
private let unreadIndicatorView = UnreadIndicatorView(frame: NSZeroRect)
private let dateView = RSSingleLineView(frame: NSZeroRect)
private let feedNameView = RSSingleLineView(frame: NSZeroRect)
private let dateView = TimelineTableCellView.singleLineTextField()
private let feedNameView = TimelineTableCellView.singleLineTextField()
private let avatarImageView: NSImageView = {
let imageView = NSImageView(frame: NSRect.zero)
imageView.imageScaling = .scaleProportionallyDown
imageView.animates = false
imageView.imageAlignment = .alignCenter
imageView.image = AppImages.genericFeedImage
private lazy var avatarImageView: NSImageView = {
let imageView = TimelineTableCellView.imageView(with: AppImages.genericFeedImage, scaling: .scaleProportionallyDown)
imageView.wantsLayer = true
return imageView
}()
private let starView: NSImageView = {
let imageView = NSImageView(frame: NSRect.zero)
imageView.imageScaling = .scaleNone
imageView.animates = false
imageView.imageAlignment = .alignCenter
imageView.image = AppImages.timelineStar
return imageView
private let starView = TimelineTableCellView.imageView(with: AppImages.timelineStar, scaling: .scaleNone)
private lazy var textFields = {
return [self.dateView, self.feedNameView, self.titleView, self.summaryView, self.textView]
}()
var cellAppearance: TimelineCellAppearance! {
didSet {
updateTextFields()
avatarImageView.layer?.cornerRadius = cellAppearance.avatarCornerRadius
needsLayout = true
}
}
@ -60,20 +58,18 @@ class TimelineTableCellView: NSTableCellView {
var isEmphasized = false {
didSet {
dateView.emphasized = isEmphasized
feedNameView.emphasized = isEmphasized
titleView.emphasized = isEmphasized
// titleView.emphasized = isEmphasized
unreadIndicatorView.isEmphasized = isEmphasized
updateTextFieldColors()
needsDisplay = true
}
}
var isSelected = false {
didSet {
dateView.selected = isSelected
feedNameView.selected = isSelected
titleView.selected = isSelected
// titleView.selected = isSelected
unreadIndicatorView.isSelected = isSelected
updateTextFieldColors()
needsDisplay = true
}
}
@ -114,9 +110,13 @@ class TimelineTableCellView: NSTableCellView {
override func resizeSubviews(withOldSize oldSize: NSSize) {
let layoutRects = updatedLayoutRects()
titleView.rs_setFrameIfNotEqual(layoutRects.titleRect)
unreadIndicatorView.rs_setFrameIfNotEqual(layoutRects.unreadIndicatorRect)
setFrame(for: titleView, rect: layoutRects.titleRect)
setFrame(for: summaryView, rect: layoutRects.summaryRect)
setFrame(for: textView, rect: layoutRects.textRect)
dateView.rs_setFrameIfNotEqual(layoutRects.dateRect)
unreadIndicatorView.rs_setFrameIfNotEqual(layoutRects.unreadIndicatorRect)
feedNameView.rs_setFrameIfNotEqual(layoutRects.feedNameRect)
avatarImageView.rs_setFrameIfNotEqual(layoutRects.avatarImageRect)
starView.rs_setFrameIfNotEqual(layoutRects.starRect)
@ -142,6 +142,80 @@ class TimelineTableCellView: NSTableCellView {
private extension TimelineTableCellView {
static func singleLineTextField() -> NSTextField {
let textField = NSTextField(labelWithString: "")
textField.usesSingleLineMode = true
textField.maximumNumberOfLines = 1
textField.isEditable = false
textField.lineBreakMode = .byTruncatingTail
textField.allowsDefaultTighteningForTruncation = false
return textField
}
static func multiLineTextField() -> NSTextField {
let textField = NSTextField(wrappingLabelWithString: "")
textField.usesSingleLineMode = false
textField.maximumNumberOfLines = 2
textField.isEditable = false
// textField.lineBreakMode = .byTruncatingTail
textField.cell?.truncatesLastVisibleLine = true
textField.allowsDefaultTighteningForTruncation = false
return textField
}
static func imageView(with image: NSImage?, scaling: NSImageScaling) -> NSImageView {
let imageView = image != nil ? NSImageView(image: image!) : NSImageView(frame: NSRect.zero)
imageView.animates = false
imageView.imageAlignment = .alignCenter
imageView.imageScaling = scaling
return imageView
}
func setFrame(for textField: NSTextField, rect: NSRect) {
if Int(floor(rect.height)) == 0 || Int(floor(rect.width)) == 0 {
hideView(textField)
}
else {
showView(textField)
textField.rs_setFrameIfNotEqual(rect)
}
}
func updateTextFieldColors() {
updateTitleView()
if isEmphasized && isSelected {
textFields.forEach { $0.textColor = NSColor.white }
}
else {
feedNameView.textColor = cellAppearance.feedNameColor
dateView.textColor = cellAppearance.dateColor
titleView.textColor = cellAppearance.titleColor
summaryView.textColor = cellAppearance.textColor
textView.textColor = cellAppearance.textOnlyColor
}
}
func updateTextFieldFonts() {
feedNameView.font = cellAppearance.feedNameFont
dateView.font = cellAppearance.dateFont
titleView.font = cellAppearance.titleFont
summaryView.font = cellAppearance.textFont
textView.font = cellAppearance.textOnlyFont
}
func updateTextFields() {
updateTextFieldColors()
updateTextFieldFonts()
}
func addSubviewAtInit(_ view: NSView, hidden: Bool) {
addSubview(view)
@ -152,16 +226,18 @@ private extension TimelineTableCellView {
func commonInit() {
addSubviewAtInit(titleView, hidden: false)
addSubviewAtInit(summaryView, hidden: true)
addSubviewAtInit(textView, hidden: true)
addSubviewAtInit(unreadIndicatorView, hidden: true)
addSubviewAtInit(dateView, hidden: false)
addSubviewAtInit(feedNameView, hidden: true)
addSubviewAtInit(avatarImageView, hidden: false)
addSubviewAtInit(starView, hidden: false)
addSubviewAtInit(avatarImageView, hidden: true)
addSubviewAtInit(starView, hidden: true)
}
func updatedLayoutRects() -> TimelineCellLayout {
return TimelineCellLayout(width: bounds.width, cellData: cellData, appearance: cellAppearance)
return TimelineCellLayout(width: bounds.width, height: bounds.height, cellData: cellData, appearance: cellAppearance, hasAvatar: avatarImageView.image != nil)
}
func updateAppearance() {
@ -178,70 +254,102 @@ private extension TimelineTableCellView {
func updateTitleView() {
titleView.attributedStringValue = cellData.attributedTitle
needsLayout = true
updateTextFieldText(titleView, cellData?.title)
}
func updateSummaryView() {
updateTextFieldText(summaryView, cellData?.text)
}
func updateTextView() {
updateTextFieldText(textView, cellData?.text)
}
func updateDateView() {
dateView.attributedStringValue = cellData.attributedDateString
needsLayout = true
updateTextFieldText(dateView, cellData.dateString)
}
func updateTextFieldText(_ textField: NSTextField, _ text: String?) {
let s = text ?? ""
if textField.stringValue != s {
textField.stringValue = s
needsLayout = true
}
}
func updateFeedNameView() {
if cellData.showFeedName {
if feedNameView.isHidden {
feedNameView.isHidden = false
}
feedNameView.attributedStringValue = cellData.attributedFeedName
showView(feedNameView)
updateTextFieldText(feedNameView, cellData.feedName)
}
else {
if !feedNameView.isHidden {
feedNameView.isHidden = true
}
hideView(feedNameView)
}
}
func updateUnreadIndicator() {
let shouldHide = cellData.read || cellData.starred
if unreadIndicatorView.isHidden != shouldHide {
unreadIndicatorView.isHidden = shouldHide
}
showOrHideView(unreadIndicatorView, cellData.read || cellData.starred)
}
func updateStarView() {
starView.isHidden = !cellData.starred
showOrHideView(starView, !cellData.starred)
}
func updateAvatar() {
if !cellData.showAvatar {
avatarImageView.image = nil
avatarImageView.isHidden = true
// The avatar should be bigger than a favicon. Theyre too small; they look weird.
guard let image = cellData.avatar, cellData.showAvatar, image.size.height >= 22.0, image.size.width >= 22.0 else {
makeAvatarEmpty()
return
}
avatarImageView.isHidden = false
if let image = cellData.avatar {
if avatarImageView.image !== image {
avatarImageView.image = image
}
showView(avatarImageView)
if avatarImageView.image !== image {
avatarImageView.image = image
needsLayout = true
}
else {
}
func makeAvatarEmpty() {
if avatarImageView.image != nil {
avatarImageView.image = nil
needsLayout = true
}
hideView(avatarImageView)
}
avatarImageView.wantsLayer = true
avatarImageView.layer?.cornerRadius = cellAppearance.avatarCornerRadius
func hideView(_ view: NSView) {
if !view.isHidden {
view.isHidden = true
}
}
func showView(_ view: NSView) {
if view.isHidden {
view.isHidden = false
}
}
func showOrHideView(_ view: NSView, _ shouldHide: Bool) {
shouldHide ? hideView(view) : showView(view)
}
func updateSubviews() {
updateTitleView()
updateSummaryView()
updateTextView()
updateDateView()
updateFeedNameView()
updateUnreadIndicator()

View File

@ -54,7 +54,7 @@ class TimelineTableRowView : NSTableRowView {
override func drawSeparator(in dirtyRect: NSRect) {
let path = NSBezierPath()
let originX = floor(cellAppearance.boxLeftMargin)
let originX = floor(cellAppearance.boxLeftMargin) + 2.0
let destinationX = ceil(NSMaxX(bounds))
let y = floor(NSMaxY(bounds)) - 0.5
path.move(to: NSPoint(x: originX, y: y))

View File

@ -8,7 +8,6 @@
import Foundation
import RSCore
import RSTextDrawing
import Data
import Account
@ -134,10 +133,6 @@ class TimelineViewController: NSViewController, UndoableCommandRunner {
private func fontSizeDidChange() {
TimelineCellData.emptyCache()
RSSingleLineRenderer.emptyCache()
RSMultiLineRenderer.emptyCache()
cellAppearance = TimelineCellAppearance(theme: appDelegate.currentTheme, showAvatar: false, fontSize: fontSize)
cellAppearanceWithAvatar = TimelineCellAppearance(theme: appDelegate.currentTheme, showAvatar: true, fontSize: fontSize)
updateRowHeights()
@ -516,7 +511,7 @@ extension TimelineViewController: NSTableViewDelegate {
func tableViewSelectionDidChange(_ notification: Notification) {
// tableView.redrawGrid()
tableView.redrawGrid()
if selectedArticles.isEmpty {
postTimelineSelectionDidChangeNotification(nil)
@ -594,7 +589,7 @@ extension TimelineViewController: NSTableViewDelegate {
private func makeTimelineCellEmpty(_ cell: TimelineTableCellView) {
cell.objectValue = nil
cell.cellData = emptyCellData
cell.cellData = TimelineCellData()
}
}

View File

@ -80,7 +80,7 @@
<key>paddingRight</key>
<integer>20</integer>
<key>paddingTop</key>
<integer>16</integer>
<integer>14</integer>
<key>paddingBottom</key>
<integer>16</integer>
<key>feedNameColor</key>
@ -92,15 +92,15 @@
<key>dateMarginLeft</key>
<integer>10</integer>
<key>dateMarginBottom</key>
<integer>2</integer>
<integer>1</integer>
<key>textColor</key>
<string>aaaaaa</string>
<string>999999</string>
<key>textOnlyColor</key>
<string>222222</string>
<key>titleColor</key>
<string>222222</string>
<key>titleMarginBottom</key>
<integer>2</integer>
<integer>1</integer>
<key>unreadCircleColor</key>
<string>#2db6ff</string>
<key>unreadCircleDimension</key>
@ -108,9 +108,9 @@
<key>unreadCircleMarginRight</key>
<integer>8</integer>
<key>avatarHeight</key>
<integer>48</integer>
<integer>64</integer>
<key>avatarWidth</key>
<integer>48</integer>
<integer>64</integer>
<key>avatarMarginRight</key>
<integer>20</integer>
<key>avatarMarginLeft</key>
@ -118,9 +118,11 @@
<key>avatarAdjustmentTop</key>
<integer>4</integer>
<key>avatarCornerRadius</key>
<integer>7</integer>
<integer>4</integer>
<key>starDimension</key>
<integer>13</integer>
<key>titleMaximumLines</key>
<integer>2</integer>
</dict>
</dict>
<key>Detail</key>

View File

@ -117,6 +117,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
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)
NotificationCenter.default.addObserver(self, selector: #selector(childrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
pullObjectsFromDisk()
@ -448,23 +449,34 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
}
}
@objc func displayNameDidChange(_ note: Notification) {
@objc func childrenDidChange(_ note: Notification) {
if let feed = note.object as? Feed, let feedAccount = feed.account, feedAccount === self {
guard let object = note.object else {
return
}
if let account = object as? Account, account === self {
dirty = true
}
if let folder = note.object as? Folder, let folderAccount = folder.account, folderAccount === self {
if let folder = object as? Folder, folder.account === self {
dirty = true
}
}
@objc func displayNameDidChange(_ note: Notification) {
if let feed = note.object as? Feed, feed.account === self {
dirty = true
}
if let folder = note.object as? Folder, folder.account === self {
dirty = true
}
}
@objc func saveToDiskIfNeeded() {
guard dirty else {
return
if dirty {
saveToDisk()
}
saveToDisk()
dirty = false
}
// MARK: - Equatable
@ -564,6 +576,7 @@ private extension Account {
catch let error as NSError {
NSApplication.shared.presentError(error)
}
dirty = false
}
}

View File

@ -161,6 +161,8 @@
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 */; };
84D5BA1E201E87E2009092BD /* URLPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D5BA1D201E87E2009092BD /* URLPasteboardWriter.swift */; };
84E185C6203BEA7900F69BFA /* NSAttributedString+RSCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 84E185C4203BEA7900F69BFA /* NSAttributedString+RSCore.h */; settings = {ATTRIBUTES = (Public, ); }; };
84E185C7203BEA7900F69BFA /* NSAttributedString+RSCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 84E185C5203BEA7900F69BFA /* NSAttributedString+RSCore.m */; };
84E34DA61F9FA1070077082F /* UndoableCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E34DA51F9FA1070077082F /* UndoableCommand.swift */; };
84E8E0D9202EC39800562D8F /* NSMenu+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E8E0D8202EC39800562D8F /* NSMenu+Extensions.swift */; };
84F20F831F16BA6200D8E682 /* PropertyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F20F821F16BA6200D8E682 /* PropertyList.swift */; };
@ -285,6 +287,8 @@
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>"; };
84D5BA1D201E87E2009092BD /* URLPasteboardWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = URLPasteboardWriter.swift; path = AppKit/URLPasteboardWriter.swift; sourceTree = "<group>"; };
84E185C4203BEA7900F69BFA /* NSAttributedString+RSCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "NSAttributedString+RSCore.h"; path = "AppKit/NSAttributedString+RSCore.h"; sourceTree = "<group>"; };
84E185C5203BEA7900F69BFA /* NSAttributedString+RSCore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "NSAttributedString+RSCore.m"; path = "AppKit/NSAttributedString+RSCore.m"; sourceTree = "<group>"; };
84E34DA51F9FA1070077082F /* UndoableCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UndoableCommand.swift; path = RSCore/UndoableCommand.swift; sourceTree = "<group>"; };
84E8E0D8202EC39800562D8F /* NSMenu+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "NSMenu+Extensions.swift"; path = "AppKit/NSMenu+Extensions.swift"; sourceTree = "<group>"; };
84F20F821F16BA6200D8E682 /* PropertyList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyList.swift; sourceTree = "<group>"; };
@ -460,6 +464,8 @@
84CFF5511AC3CF4700CEA6C8 /* NSColor+RSCore.h */,
84C6329E200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h */,
84C6329F200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m */,
84E185C4203BEA7900F69BFA /* NSAttributedString+RSCore.h */,
84E185C5203BEA7900F69BFA /* NSAttributedString+RSCore.m */,
84C632A2200D356E007BEEAA /* SendToBlogEditorApp.h */,
84C632A3200D356E007BEEAA /* SendToBlogEditorApp.m */,
84CFF5521AC3CF4700CEA6C8 /* NSColor+RSCore.m */,
@ -579,6 +585,7 @@
84CFF5271AC3C9A200CEA6C8 /* NSArray+RSCore.h in Headers */,
84CFF5531AC3CF4700CEA6C8 /* NSColor+RSCore.h in Headers */,
84CFF4FA1AC3C69700CEA6C8 /* RSCore.h in Headers */,
84E185C6203BEA7900F69BFA /* NSAttributedString+RSCore.h in Headers */,
844F91D51D90D86100820C48 /* RSTransparentContainerView.h in Headers */,
84CFF53F1AC3CD0100CEA6C8 /* NSMutableSet+RSCore.h in Headers */,
84C632A0200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h in Headers */,
@ -791,6 +798,7 @@
84CFF53C1AC3CCCA00CEA6C8 /* NSMutableDictionary+RSCore.m in Sources */,
8414CBAC1C95F8F700333C12 /* RSGeometry.m in Sources */,
84134D201C59D5450063FD24 /* NSCalendar+RSCore.m in Sources */,
84E185C7203BEA7900F69BFA /* NSAttributedString+RSCore.m in Sources */,
84CFF5651AC3D13C00CEA6C8 /* RSImageRenderer.m in Sources */,
849EE70D2039187D0082A1EA /* NSWindowController+RSCore.swift in Sources */,
84CFF5381AC3CBB200CEA6C8 /* NSMutableArray+RSCore.m in Sources */,

View File

@ -0,0 +1,17 @@
//
// NSAttributedString.h
// RSCore
//
// Created by Brent Simmons on 2/19/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
@import AppKit;
@interface NSAttributedString (RSCore)
// Useful for table/outline views when a row is selected.
- (NSAttributedString *)rs_attributedStringByMakingTextWhite;
@end

View File

@ -0,0 +1,20 @@
//
// NSAttributedString.m
// RSCore
//
// Created by Brent Simmons on 2/19/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
#import "NSAttributedString+RSCore.h"
@implementation NSAttributedString (RSCore)
- (NSAttributedString *)rs_attributedStringByMakingTextWhite {
NSMutableAttributedString *mutableString = [self mutableCopy];
[mutableString addAttribute:NSForegroundColorAttributeName value:NSColor.whiteColor range:NSMakeRange(0, mutableString.string.length)];
return [mutableString copy];
}
@end

View File

@ -55,6 +55,7 @@
#import <RSCore/NSAppleEventDescriptor+RSCore.h>
#import <RSCore/SendToBlogEditorApp.h>
#import <RSCore/NSAttributedString+RSCore.h>
#endif

View File

@ -171,7 +171,7 @@ private extension JSONFeedParser {
// If we find more feeds like this, well add them here. If these feeds get fixed, well remove them.
let lowerFeedURL = feedURL.lowercased()
let matchStrings = ["kottke.org", "pxlnv.com"]
let matchStrings = ["kottke.org", "pxlnv.com", "macstories.net"]
for matchString in matchStrings {
if lowerFeedURL.contains(matchString) {
return true

View File

@ -1,356 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
84193AB31CF4EEEB00EAC812 /* RSMultiLineRendererMeasurements.h in Headers */ = {isa = PBXBuildFile; fileRef = 84193AB11CF4EEEB00EAC812 /* RSMultiLineRendererMeasurements.h */; settings = {ATTRIBUTES = (Public, ); }; };
84193AB41CF4EEEB00EAC812 /* RSMultiLineRendererMeasurements.m in Sources */ = {isa = PBXBuildFile; fileRef = 84193AB21CF4EEEB00EAC812 /* RSMultiLineRendererMeasurements.m */; };
8439D9FF1C8937C800E5E4B4 /* RSTextDrawing.h in Headers */ = {isa = PBXBuildFile; fileRef = 8439D9FE1C8937C800E5E4B4 /* RSTextDrawing.h */; settings = {ATTRIBUTES = (Public, ); }; };
8439DA061C8937C800E5E4B4 /* RSTextDrawing.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8439D9FB1C8937C800E5E4B4 /* RSTextDrawing.framework */; };
8439DA0B1C8937C800E5E4B4 /* RSTextDrawingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8439DA0A1C8937C800E5E4B4 /* RSTextDrawingTests.m */; };
846416401C8938210064C661 /* RSSingleLineRenderer.h in Headers */ = {isa = PBXBuildFile; fileRef = 8464163E1C8938210064C661 /* RSSingleLineRenderer.h */; settings = {ATTRIBUTES = (Public, ); }; };
846416411C8938210064C661 /* RSSingleLineRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = 8464163F1C8938210064C661 /* RSSingleLineRenderer.m */; };
84B717761CF9629000FF029D /* RSSingleLineView.h in Headers */ = {isa = PBXBuildFile; fileRef = 84B717741CF9629000FF029D /* RSSingleLineView.h */; settings = {ATTRIBUTES = (Public, ); }; };
84B717771CF9629000FF029D /* RSSingleLineView.m in Sources */ = {isa = PBXBuildFile; fileRef = 84B717751CF9629000FF029D /* RSSingleLineView.m */; };
84B7177B1CF9665100FF029D /* RSMultiLineView.h in Headers */ = {isa = PBXBuildFile; fileRef = 84B717791CF9665100FF029D /* RSMultiLineView.h */; settings = {ATTRIBUTES = (Public, ); }; };
84B7177C1CF9665100FF029D /* RSMultiLineView.m in Sources */ = {isa = PBXBuildFile; fileRef = 84B7177A1CF9665100FF029D /* RSMultiLineView.m */; };
84B7177D1CF9834700FF029D /* RSMultiLineRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = 846416431C8938470064C661 /* RSMultiLineRenderer.m */; };
84B7177E1CF9834A00FF029D /* RSMultiLineRenderer.h in Headers */ = {isa = PBXBuildFile; fileRef = 846416421C8938470064C661 /* RSMultiLineRenderer.h */; settings = {ATTRIBUTES = (Public, ); }; };
84BA010F1C8D20C60029943B /* RSTextRendererProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 84BA010D1C8D20C60029943B /* RSTextRendererProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; };
84E697E41C8E6C01009C585A /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84E697E31C8E6C01009C585A /* RSCore.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
8439DA071C8937C800E5E4B4 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 8439D9F21C8937C800E5E4B4 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 8439D9FA1C8937C800E5E4B4;
remoteInfo = RSTextDrawing;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
84193AB11CF4EEEB00EAC812 /* RSMultiLineRendererMeasurements.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RSMultiLineRendererMeasurements.h; path = RSTextDrawing/RSMultiLineRendererMeasurements.h; sourceTree = "<group>"; };
84193AB21CF4EEEB00EAC812 /* RSMultiLineRendererMeasurements.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RSMultiLineRendererMeasurements.m; path = RSTextDrawing/RSMultiLineRendererMeasurements.m; sourceTree = "<group>"; };
8439D9FB1C8937C800E5E4B4 /* RSTextDrawing.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RSTextDrawing.framework; sourceTree = BUILT_PRODUCTS_DIR; };
8439D9FE1C8937C800E5E4B4 /* RSTextDrawing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RSTextDrawing.h; path = RSTextDrawing/RSTextDrawing.h; sourceTree = "<group>"; };
8439DA001C8937C800E5E4B4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = RSTextDrawing/Info.plist; sourceTree = "<group>"; };
8439DA051C8937C800E5E4B4 /* RSTextDrawingTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RSTextDrawingTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
8439DA0A1C8937C800E5E4B4 /* RSTextDrawingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RSTextDrawingTests.m; sourceTree = "<group>"; };
8439DA0C1C8937C800E5E4B4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
8464163E1C8938210064C661 /* RSSingleLineRenderer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RSSingleLineRenderer.h; path = RSTextDrawing/RSSingleLineRenderer.h; sourceTree = "<group>"; };
8464163F1C8938210064C661 /* RSSingleLineRenderer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RSSingleLineRenderer.m; path = RSTextDrawing/RSSingleLineRenderer.m; sourceTree = "<group>"; };
846416421C8938470064C661 /* RSMultiLineRenderer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RSMultiLineRenderer.h; path = RSTextDrawing/RSMultiLineRenderer.h; sourceTree = "<group>"; };
846416431C8938470064C661 /* RSMultiLineRenderer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RSMultiLineRenderer.m; path = RSTextDrawing/RSMultiLineRenderer.m; sourceTree = "<group>"; };
84B717741CF9629000FF029D /* RSSingleLineView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RSSingleLineView.h; path = RSTextDrawing/RSSingleLineView.h; sourceTree = "<group>"; };
84B717751CF9629000FF029D /* RSSingleLineView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RSSingleLineView.m; path = RSTextDrawing/RSSingleLineView.m; sourceTree = "<group>"; };
84B717791CF9665100FF029D /* RSMultiLineView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RSMultiLineView.h; path = RSTextDrawing/RSMultiLineView.h; sourceTree = "<group>"; };
84B7177A1CF9665100FF029D /* RSMultiLineView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RSMultiLineView.m; path = RSTextDrawing/RSMultiLineView.m; sourceTree = "<group>"; };
84BA010D1C8D20C60029943B /* RSTextRendererProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RSTextRendererProtocol.h; path = RSTextDrawing/RSTextRendererProtocol.h; sourceTree = "<group>"; };
84E697E31C8E6C01009C585A /* RSCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RSCore.framework; path = ../RSCore/build/Debug/RSCore.framework; sourceTree = "<group>"; };
D511EF1520242EB900712EC3 /* RSTextDrawing_project_release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = RSTextDrawing_project_release.xcconfig; sourceTree = "<group>"; };
D511EF1620242EB900712EC3 /* RSTextDrawingTests_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = RSTextDrawingTests_target.xcconfig; sourceTree = "<group>"; };
D511EF1720242EB900712EC3 /* RSTextDrawing_project_debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = RSTextDrawing_project_debug.xcconfig; sourceTree = "<group>"; };
D511EF1820242EB900712EC3 /* RSTextDrawing_project.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = RSTextDrawing_project.xcconfig; sourceTree = "<group>"; };
D511EF1920242EB900712EC3 /* RSTextDrawing_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = RSTextDrawing_target.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
8439D9F71C8937C800E5E4B4 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
84E697E41C8E6C01009C585A /* RSCore.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
8439DA021C8937C800E5E4B4 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
8439DA061C8937C800E5E4B4 /* RSTextDrawing.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
8439D9F11C8937C800E5E4B4 = {
isa = PBXGroup;
children = (
8439D9FE1C8937C800E5E4B4 /* RSTextDrawing.h */,
84B717741CF9629000FF029D /* RSSingleLineView.h */,
84B717751CF9629000FF029D /* RSSingleLineView.m */,
84B717791CF9665100FF029D /* RSMultiLineView.h */,
84B7177A1CF9665100FF029D /* RSMultiLineView.m */,
84BA010D1C8D20C60029943B /* RSTextRendererProtocol.h */,
8464163E1C8938210064C661 /* RSSingleLineRenderer.h */,
8464163F1C8938210064C661 /* RSSingleLineRenderer.m */,
846416421C8938470064C661 /* RSMultiLineRenderer.h */,
846416431C8938470064C661 /* RSMultiLineRenderer.m */,
84193AB11CF4EEEB00EAC812 /* RSMultiLineRendererMeasurements.h */,
84193AB21CF4EEEB00EAC812 /* RSMultiLineRendererMeasurements.m */,
8439DA001C8937C800E5E4B4 /* Info.plist */,
8439DA091C8937C800E5E4B4 /* RSTextDrawingTests */,
8439D9FC1C8937C800E5E4B4 /* Products */,
84E697E31C8E6C01009C585A /* RSCore.framework */,
D511EF1420242EB900712EC3 /* xcconfig */,
);
sourceTree = "<group>";
};
8439D9FC1C8937C800E5E4B4 /* Products */ = {
isa = PBXGroup;
children = (
8439D9FB1C8937C800E5E4B4 /* RSTextDrawing.framework */,
8439DA051C8937C800E5E4B4 /* RSTextDrawingTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
8439DA091C8937C800E5E4B4 /* RSTextDrawingTests */ = {
isa = PBXGroup;
children = (
8439DA0A1C8937C800E5E4B4 /* RSTextDrawingTests.m */,
8439DA0C1C8937C800E5E4B4 /* Info.plist */,
);
path = RSTextDrawingTests;
sourceTree = "<group>";
};
D511EF1420242EB900712EC3 /* xcconfig */ = {
isa = PBXGroup;
children = (
D511EF1820242EB900712EC3 /* RSTextDrawing_project.xcconfig */,
D511EF1720242EB900712EC3 /* RSTextDrawing_project_debug.xcconfig */,
D511EF1520242EB900712EC3 /* RSTextDrawing_project_release.xcconfig */,
D511EF1920242EB900712EC3 /* RSTextDrawing_target.xcconfig */,
D511EF1620242EB900712EC3 /* RSTextDrawingTests_target.xcconfig */,
);
path = xcconfig;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
8439D9F81C8937C800E5E4B4 /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
8439D9FF1C8937C800E5E4B4 /* RSTextDrawing.h in Headers */,
84B717761CF9629000FF029D /* RSSingleLineView.h in Headers */,
84B7177E1CF9834A00FF029D /* RSMultiLineRenderer.h in Headers */,
84BA010F1C8D20C60029943B /* RSTextRendererProtocol.h in Headers */,
84193AB31CF4EEEB00EAC812 /* RSMultiLineRendererMeasurements.h in Headers */,
84B7177B1CF9665100FF029D /* RSMultiLineView.h in Headers */,
846416401C8938210064C661 /* RSSingleLineRenderer.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
8439D9FA1C8937C800E5E4B4 /* RSTextDrawing */ = {
isa = PBXNativeTarget;
buildConfigurationList = 8439DA0F1C8937C800E5E4B4 /* Build configuration list for PBXNativeTarget "RSTextDrawing" */;
buildPhases = (
8439D9F61C8937C800E5E4B4 /* Sources */,
8439D9F71C8937C800E5E4B4 /* Frameworks */,
8439D9F81C8937C800E5E4B4 /* Headers */,
8439D9F91C8937C800E5E4B4 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = RSTextDrawing;
productName = RSTextDrawing;
productReference = 8439D9FB1C8937C800E5E4B4 /* RSTextDrawing.framework */;
productType = "com.apple.product-type.framework";
};
8439DA041C8937C800E5E4B4 /* RSTextDrawingTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 8439DA121C8937C800E5E4B4 /* Build configuration list for PBXNativeTarget "RSTextDrawingTests" */;
buildPhases = (
8439DA011C8937C800E5E4B4 /* Sources */,
8439DA021C8937C800E5E4B4 /* Frameworks */,
8439DA031C8937C800E5E4B4 /* Resources */,
);
buildRules = (
);
dependencies = (
8439DA081C8937C800E5E4B4 /* PBXTargetDependency */,
);
name = RSTextDrawingTests;
productName = RSTextDrawingTests;
productReference = 8439DA051C8937C800E5E4B4 /* RSTextDrawingTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
8439D9F21C8937C800E5E4B4 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0730;
LastUpgradeCheck = 0930;
ORGANIZATIONNAME = "Ranchero Software";
TargetAttributes = {
8439D9FA1C8937C800E5E4B4 = {
CreatedOnToolsVersion = 7.2.1;
LastSwiftMigration = 0800;
};
8439DA041C8937C800E5E4B4 = {
CreatedOnToolsVersion = 7.2.1;
LastSwiftMigration = 0800;
};
};
};
buildConfigurationList = 8439D9F51C8937C800E5E4B4 /* Build configuration list for PBXProject "RSTextDrawing" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
);
mainGroup = 8439D9F11C8937C800E5E4B4;
productRefGroup = 8439D9FC1C8937C800E5E4B4 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
8439D9FA1C8937C800E5E4B4 /* RSTextDrawing */,
8439DA041C8937C800E5E4B4 /* RSTextDrawingTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
8439D9F91C8937C800E5E4B4 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
8439DA031C8937C800E5E4B4 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
8439D9F61C8937C800E5E4B4 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
84B717771CF9629000FF029D /* RSSingleLineView.m in Sources */,
84B7177C1CF9665100FF029D /* RSMultiLineView.m in Sources */,
846416411C8938210064C661 /* RSSingleLineRenderer.m in Sources */,
84B7177D1CF9834700FF029D /* RSMultiLineRenderer.m in Sources */,
84193AB41CF4EEEB00EAC812 /* RSMultiLineRendererMeasurements.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
8439DA011C8937C800E5E4B4 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8439DA0B1C8937C800E5E4B4 /* RSTextDrawingTests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
8439DA081C8937C800E5E4B4 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 8439D9FA1C8937C800E5E4B4 /* RSTextDrawing */;
targetProxy = 8439DA071C8937C800E5E4B4 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
8439DA0D1C8937C800E5E4B4 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D511EF1720242EB900712EC3 /* RSTextDrawing_project_debug.xcconfig */;
buildSettings = {
CODE_SIGN_IDENTITY = "-";
};
name = Debug;
};
8439DA0E1C8937C800E5E4B4 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D511EF1520242EB900712EC3 /* RSTextDrawing_project_release.xcconfig */;
buildSettings = {
CODE_SIGN_IDENTITY = "-";
};
name = Release;
};
8439DA101C8937C800E5E4B4 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D511EF1920242EB900712EC3 /* RSTextDrawing_target.xcconfig */;
buildSettings = {
};
name = Debug;
};
8439DA111C8937C800E5E4B4 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D511EF1920242EB900712EC3 /* RSTextDrawing_target.xcconfig */;
buildSettings = {
};
name = Release;
};
8439DA131C8937C800E5E4B4 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D511EF1620242EB900712EC3 /* RSTextDrawingTests_target.xcconfig */;
buildSettings = {
};
name = Debug;
};
8439DA141C8937C800E5E4B4 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D511EF1620242EB900712EC3 /* RSTextDrawingTests_target.xcconfig */;
buildSettings = {
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
8439D9F51C8937C800E5E4B4 /* Build configuration list for PBXProject "RSTextDrawing" */ = {
isa = XCConfigurationList;
buildConfigurations = (
8439DA0D1C8937C800E5E4B4 /* Debug */,
8439DA0E1C8937C800E5E4B4 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
8439DA0F1C8937C800E5E4B4 /* Build configuration list for PBXNativeTarget "RSTextDrawing" */ = {
isa = XCConfigurationList;
buildConfigurations = (
8439DA101C8937C800E5E4B4 /* Debug */,
8439DA111C8937C800E5E4B4 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
8439DA121C8937C800E5E4B4 /* Build configuration list for PBXNativeTarget "RSTextDrawingTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
8439DA131C8937C800E5E4B4 /* Debug */,
8439DA141C8937C800E5E4B4 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 8439D9F21C8937C800E5E4B4 /* Project object */;
}

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:RSTextDrawing.xcodeproj">
</FileRef>
</Workspace>

View File

@ -1,24 +0,0 @@
//
// SingleLineRenderer.h
// RSTextDrawing
//
// Created by Brent Simmons on 3/3/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
@import AppKit;
#import <RSTextDrawing/RSTextRendererProtocol.h>
NS_ASSUME_NONNULL_BEGIN
@interface RSSingleLineRenderer : NSObject <RSTextRenderer>
+ (instancetype)rendererWithAttributedTitle:(NSAttributedString *)title;
@property (nonatomic, readonly) NSSize size;
@property (nonatomic, strong) NSColor *backgroundColor; // Default is white.
@end
NS_ASSUME_NONNULL_END

View File

@ -1,204 +0,0 @@
//
// RSSingleLineRenderer.m
// RSTextDrawing
//
// Created by Brent Simmons on 3/3/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
#import "RSSingleLineRenderer.h"
static NSMutableDictionary *rendererCache = nil;
@interface RSSingleLineRenderer ()
@property (nonatomic, readonly) NSAttributedString *title;
@property (nonatomic) NSRect rect;
@property (nonatomic, readonly) CTFramesetterRef framesetter;
@property (nonatomic) CTFrameRef frameref;
@end
@implementation RSSingleLineRenderer
@synthesize size = _size;
#pragma mark - Class Methods
+ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
rendererCache = [NSMutableDictionary new];
});
}
+ (instancetype)rendererWithAttributedTitle:(NSAttributedString *)title {
RSSingleLineRenderer *cachedRenderer = rendererCache[title];
if (cachedRenderer != nil) {
return cachedRenderer;
}
RSSingleLineRenderer *renderer = [[RSSingleLineRenderer alloc] initWithAttributedTitle:title];
rendererCache[title] = renderer;
return renderer;
}
+ (void)emptyCache {
rendererCache = [NSMutableDictionary new];
}
#pragma mark - Init
- (instancetype)initWithAttributedTitle:(NSAttributedString *)title {
self = [super init];
if (self == nil) {
return nil;
}
_title = title;
_framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)title);
_backgroundColor = NSColor.whiteColor;
return self;
}
#pragma mark - Dealloc
- (void)dealloc {
if (_framesetter) {
CFRelease(_framesetter);
_framesetter = nil;
}
if (_frameref) {
CFRelease(_frameref);
_frameref = nil;
}
}
#pragma mark - Accessors
- (void)setRect:(NSRect)r {
r.origin.y = floor(r.origin.y);
r.origin.x = floor(r.origin.x);
r.size.height = floor(r.size.height);
if (r.size.height > self.size.height) {
r.size.height = self.size.height;
}
r.size.width = floor(r.size.width);
if (r.size.width > self.size.width) {
r.size.width = self.size.width;
}
if (!NSEqualRects(r, _rect)) {
_rect = r;
[self releaseFrameref];
}
}
- (void)releaseFrameref {
if (_frameref) {
CFRelease(_frameref);
_frameref = nil;
}
}
- (NSSize)size {
if (self.title.string.length < 1) {
return NSZeroSize;
}
if (NSEqualSizes(_size, NSZeroSize)) {
_size = [self calculatedSize];
}
return _size;
}
#pragma mark - Measurements
static const CGFloat kMaxWidth = 10000.0;
static const CGFloat kMaxHeight = 10000.0;
- (NSSize)calculatedSize {
NSSize size = NSZeroSize;
@autoreleasepool {
CGRect r = CGRectMake(0.0f, 0.0f, kMaxWidth, kMaxHeight);
CGPathRef path = CGPathCreateWithRect(r, NULL);
CTFrameRef frameref = CTFramesetterCreateFrame(self.framesetter, CFRangeMake(0, (CFIndex)(self.title.length)), path, NULL);
NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frameref);
if (lines.count > 0) {
CTLineRef firstLine = (__bridge CTLineRef)lines[0];
CGRect firstLineRect = CTLineGetBoundsWithOptions(firstLine, 0);
CGFloat height = ceil(NSHeight(firstLineRect));
CGFloat width = ceil(NSWidth(firstLineRect));
size = NSMakeSize(width, height);
}
CFRelease(path);
CFRelease(frameref);
}
return size;
}
#pragma mark - Drawing
- (void)renderTextInRect:(CGRect)r {
self.rect = r;
CGContextRef context = [NSGraphicsContext currentContext].CGContext;
CGContextSaveGState(context);
CGContextSetFillColorWithColor(context, self.backgroundColor.CGColor);
CGContextFillRect(context, r);
CGContextSetShouldSmoothFonts(context, true);
CTFrameDraw(self.frameref, context);
CGContextRestoreGState(context);
}
- (CTFrameRef)frameref {
if (_frameref) {
return _frameref;
}
CGPathRef path = CGPathCreateWithRect(self.rect, NULL);
_frameref = CTFramesetterCreateFrame(self.framesetter, CFRangeMake(0, (CFIndex)(self.title.length)), path, NULL);
CFRelease(path);
return _frameref;
}
@end

View File

@ -1,22 +0,0 @@
//
// RSSingleLineView.h
// RSTextDrawing
//
// Created by Brent Simmons on 5/27/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
@import AppKit;
NS_ASSUME_NONNULL_BEGIN
@interface RSSingleLineView : NSView
@property (nonatomic, strong) NSAttributedString *attributedStringValue;
@property (nonatomic) BOOL selected;
@property (nonatomic) BOOL emphasized;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,158 +0,0 @@
//
// RSSingleLineView.m
// RSTextDrawing
//
// Created by Brent Simmons on 5/27/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
@import RSCore;
#import "RSSingleLineView.h"
#import "RSSingleLineRenderer.h"
@interface RSSingleLineView ()
@property (nonatomic) RSSingleLineRenderer *renderer;
@property (nonatomic) NSSize intrinsicSize;
@property (nonatomic) BOOL intrinsicSizeIsValid;
@property (nonatomic) RSSingleLineRenderer *selectedRenderer;
@property (nonatomic) NSAttributedString *selectedAttributedStringValue;
@end
static NSAttributedString *emptyAttributedString = nil;
@implementation RSSingleLineView
- (instancetype)initWithFrame:(NSRect)r {
self = [super initWithFrame:r];
if (!self) {
return nil;
}
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
emptyAttributedString = [[NSAttributedString alloc] initWithString:@""];
});
_renderer = [RSSingleLineRenderer rendererWithAttributedTitle:emptyAttributedString];
return self;
}
- (void)setAttributedStringValue:(NSAttributedString *)attributedStringValue {
_attributedStringValue = attributedStringValue;
self.selectedAttributedStringValue = nil;
self.selectedRenderer = nil;
self.renderer = [RSSingleLineRenderer rendererWithAttributedTitle:attributedStringValue];
}
- (void)setRenderer:(RSSingleLineRenderer *)renderer {
if (_renderer == renderer) {
return;
}
_renderer = renderer;
[self invalidateIntrinsicContentSize];
self.needsDisplay = YES;
}
- (RSSingleLineRenderer *)selectedRenderer {
if (_selectedRenderer) {
return _selectedRenderer;
}
_selectedRenderer = [RSSingleLineRenderer rendererWithAttributedTitle:self.selectedAttributedStringValue];
_selectedRenderer.backgroundColor = NSColor.alternateSelectedControlColor;
return _selectedRenderer;
}
- (void)setSelected:(BOOL)selected {
_selected = selected;
self.needsDisplay = YES;
}
- (void)setEmphasized:(BOOL)emphasized {
_emphasized = emphasized;
self.needsDisplay = YES;
}
- (NSAttributedString *)selectedAttributedStringValue {
if (!self.attributedStringValue) {
return emptyAttributedString;
}
NSMutableAttributedString *s = [self.attributedStringValue mutableCopy];
[s addAttribute:NSForegroundColorAttributeName value:NSColor.alternateSelectedControlTextColor range:NSMakeRange(0, s.string.length)];
_selectedAttributedStringValue = s;
return _selectedAttributedStringValue;
}
- (void)invalidateIntrinsicContentSize {
self.intrinsicSizeIsValid = NO;
}
- (NSSize)intrinsicContentSize {
if (!self.intrinsicSizeIsValid) {
if (!self.attributedStringValue) {
self.intrinsicSize = NSZeroSize;
}
else {
self.intrinsicSize = ((RSSingleLineRenderer *)(self.renderer)).size;
}
self.intrinsicSizeIsValid = YES;
}
return self.intrinsicSize;
}
- (NSMenu *)menuForEvent:(NSEvent *)event {
NSTableView *tableView = [self rs_enclosingTableView];
if (tableView) {
return [tableView menuForEvent:event];
}
return nil;
}
- (void)drawRect:(NSRect)r {
if (self.selected) {
if (self.emphasized) {
[self.selectedRenderer renderTextInRect:self.bounds];
}
else {
NSColor *savedBackgroundColor = self.renderer.backgroundColor;
self.renderer.backgroundColor = NSColor.secondarySelectedControlColor;
[self.renderer renderTextInRect:self.bounds];
self.renderer.backgroundColor = savedBackgroundColor;
}
}
else {
[self.renderer renderTextInRect:self.bounds];
}
}
@end

View File

@ -8,10 +8,8 @@
@import AppKit;
#import <RSTextDrawing/RSSingleLineView.h>
#import <RSTextDrawing/RSMultiLineView.h>
#import <RSTextDrawing/RSSingleLineRenderer.h>
#import <RSTextDrawing/RSMultiLineRenderer.h>
#import <RSTextDrawing/RSMultiLineRendererMeasurements.h>
#import <RSTextDrawing/RSTextRendererProtocol.h>

View File

@ -16,7 +16,7 @@ public final class Node: Hashable {
public let representedObject: AnyObject
public var canHaveChildNodes = false
public var isGroupItem = false
public var childNodes: [Node]?
public var childNodes = [Node]()
public let hashValue: Int
private static var incrementingID = 0
@ -28,7 +28,7 @@ public final class Node: Hashable {
}
public var numberOfChildNodes: Int {
return childNodes?.count ?? 0
return childNodes.count
}
public var indexPath: IndexPath {
@ -87,9 +87,6 @@ public final class Node: Hashable {
public func childAtIndex(_ index: Int) -> Node? {
guard let childNodes = childNodes else {
return nil
}
if index >= childNodes.count || index < 0 {
return nil
}
@ -98,7 +95,7 @@ public final class Node: Hashable {
public func indexOfChild(_ node: Node) -> Int? {
return childNodes?.index{ (oneChildNode) -> Bool in
return childNodes.index{ (oneChildNode) -> Bool in
oneChildNode === node
}
}
@ -189,9 +186,6 @@ private extension Node {
func findNodeRepresentingObject(_ obj: AnyObject, recursively: Bool = false) -> Node? {
guard let childNodes = childNodes else {
return nil
}
for childNode in childNodes {
if childNode.representedObject === obj {
return childNode

View File

@ -53,8 +53,8 @@ public final class TreeController {
return oneNode
}
if recurse, oneNode.canHaveChildNodes, let childNodes = oneNode.childNodes {
if let foundNode = nodeInArrayRepresentingObject(nodes: childNodes, representedObject: representedObject, recurse: recurse) {
if recurse, oneNode.canHaveChildNodes {
if let foundNode = nodeInArrayRepresentingObject(nodes: oneNode.childNodes, representedObject: representedObject, recurse: recurse) {
return foundNode
}
@ -89,7 +89,7 @@ private extension TreeController {
func visitNode(_ node: Node, _ visitBlock: NodeVisitBlock) {
visitBlock(node)
node.childNodes?.forEach{ (oneChildNode) in
node.childNodes.forEach{ (oneChildNode) in
visitNode(oneChildNode, visitBlock)
}
}
@ -117,14 +117,14 @@ private extension TreeController {
var childNodesDidChange = false
let childNodes = delegate?.treeController(treeController: self, childNodesFor: node)
let childNodes = delegate?.treeController(treeController: self, childNodesFor: node) ?? [Node]()
childNodesDidChange = !nodeArraysAreEqual(childNodes, node.childNodes)
if (childNodesDidChange) {
node.childNodes = childNodes
}
childNodes?.forEach{ (oneChildNode) in
childNodes.forEach{ (oneChildNode) in
if rebuildChildNodes(node: oneChildNode) {
childNodesDidChange = true
}

View File

@ -16,6 +16,6 @@ Its pretty early still, and we have strong opinions about how we want to do t
That said, we will seriously consider any pull requests we do get. Just note that we may not accept them, or we may accept them and do a bunch of revision.
Its probably a good idea to let us know first what youd like to do. ([Ask on Twitter](https://twitter.com/evergreen_mac), or email brent@ranchero.com, or post something to the [bug tracker](https://github.com/brentsimmons/Evergreen/issues).)
Its probably a good idea to let us know first what youd like to do. ([Ask on Micro.blog](https://micro.blog/brentsimmons), or email brent@ranchero.com, or post something to the [bug tracker](https://github.com/brentsimmons/Evergreen/issues).)
In the future — some time after 1.0 ships — we expect to want contributions. The plan is *not* to make this a one-person show forever.