Merge remote-tracking branch 'brentsimmons/master'
This commit is contained in:
commit
c91f4621e1
@ -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 there’s little text and when there’s no avatar or feed icon and one is hoped-for.</p>
|
||||
<p>Don’t show favicons in place of avatars and feed icons (when neither can be found) — they’re too small and don’t look good.</p>
|
||||
|
||||
<h4>Sidebar</h4>
|
||||
<p>Fixed a bug where deleting a thing wouldn’t 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[
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 they’re < 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
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -21,6 +21,8 @@ class SidebarOutlineView : NSOutlineView {
|
||||
// Don’t 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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
209
Evergreen/MainWindow/Timeline/Cell/MultilineTextFieldSizer.swift
Normal file
209
Evergreen/MainWindow/Timeline/Cell/MultilineTextFieldSizer.swift
Normal 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 {
|
||||
|
||||
// We’ll 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 neighbor’s height is single line height, then this wider width must also be single-line height.
|
||||
// If a wider neighbor’s 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
|
||||
}
|
||||
}
|
@ -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. It’s easiest.
|
||||
|
||||
static func size(for text: String, font: NSFont) -> NSSize {
|
||||
|
||||
return sizer(for: font).size(for: text)
|
||||
}
|
||||
|
||||
static func emptyCache() {
|
||||
|
||||
sizers = [NSFont: SingleLineTextFieldSizer]()
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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])
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -10,12 +10,13 @@ import Foundation
|
||||
import Data
|
||||
import RSParser
|
||||
|
||||
// TODO: Don’t 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
|
||||
}
|
||||
|
@ -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. They’re 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()
|
||||
|
@ -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))
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 */,
|
||||
|
17
Frameworks/RSCore/RSCore/AppKit/NSAttributedString+RSCore.h
Normal file
17
Frameworks/RSCore/RSCore/AppKit/NSAttributedString+RSCore.h
Normal 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
|
20
Frameworks/RSCore/RSCore/AppKit/NSAttributedString+RSCore.m
Normal file
20
Frameworks/RSCore/RSCore/AppKit/NSAttributedString+RSCore.m
Normal 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
|
@ -55,6 +55,7 @@
|
||||
#import <RSCore/NSAppleEventDescriptor+RSCore.h>
|
||||
#import <RSCore/SendToBlogEditorApp.h>
|
||||
|
||||
#import <RSCore/NSAttributedString+RSCore.h>
|
||||
#endif
|
||||
|
||||
|
||||
|
@ -171,7 +171,7 @@ private extension JSONFeedParser {
|
||||
// If we find more feeds like this, we’ll add them here. If these feeds get fixed, we’ll 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
|
||||
|
@ -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 */;
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:RSTextDrawing.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -16,6 +16,6 @@ It’s 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.
|
||||
|
||||
It’s probably a good idea to let us know first what you’d 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).)
|
||||
It’s probably a good idea to let us know first what you’d 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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user