mirror of
https://github.com/Ranchero-Software/NetNewsWire.git
synced 2025-02-02 20:16:54 +01:00
Continue progress toward variable row heights.
This commit is contained in:
parent
ab6d232377
commit
d3846a6a37
@ -164,7 +164,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
|
||||
func applicationDidResignActive(_ notification: Notification) {
|
||||
|
||||
TimelineCellData.emptyCache()
|
||||
timelineEmptyCaches()
|
||||
|
||||
saveState()
|
||||
|
@ -90,7 +90,17 @@ private extension MultilineTextFieldSizer {
|
||||
return newSizer
|
||||
}
|
||||
|
||||
func sizeInfo(for string: String, width: Int) -> Int {
|
||||
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()
|
||||
@ -135,6 +145,15 @@ private extension MultilineTextFieldSizer {
|
||||
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)
|
||||
|
@ -23,7 +23,7 @@ struct TimelineCellAppearance: Equatable {
|
||||
let titleColor: NSColor
|
||||
let titleFont: NSFont
|
||||
let titleBottomMargin: CGFloat
|
||||
let titleNumberOfLines = 2
|
||||
let titleNumberOfLines: Int
|
||||
|
||||
let textColor: NSColor
|
||||
let textFont: NSFont
|
||||
@ -67,7 +67,8 @@ struct TimelineCellAppearance: Equatable {
|
||||
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)
|
||||
|
||||
@ -91,12 +92,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,15 +9,10 @@
|
||||
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 feedName: String
|
||||
let showFeedName: Bool
|
||||
@ -32,15 +27,6 @@ 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 feedName = feedName {
|
||||
@ -63,7 +49,6 @@ struct TimelineCellData {
|
||||
init() { //Empty
|
||||
|
||||
self.title = ""
|
||||
self.attributedTitle = NSAttributedString(string: "")
|
||||
self.text = ""
|
||||
self.dateString = ""
|
||||
self.feedName = ""
|
||||
@ -74,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: " " + 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])
|
||||
}
|
||||
|
||||
|
@ -16,41 +16,49 @@ 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, 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
|
||||
self.height = [feedNameRect, dateRect, titleRect, summaryRect, textRect, unreadIndicatorRect, avatarImageRect].maxY() + paddingBottom
|
||||
}
|
||||
|
||||
init(width: CGFloat, cellData: TimelineCellData, appearance: TimelineCellAppearance) {
|
||||
|
||||
var textBoxRect = TimelineCellLayout.rectForTextBox(appearance, cellData, width)
|
||||
|
||||
let titleRect = TimelineCellLayout.rectForTitle(textBoxRect, cellData)
|
||||
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)
|
||||
let dateRect = TimelineCellLayout.rectForDate(textBoxRect, titleRect, appearance, cellData)
|
||||
let feedNameRect = TimelineCellLayout.rectForFeedName(textBoxRect, dateRect, appearance, cellData)
|
||||
|
||||
textBoxRect.size.height = ceil([titleRect, dateRect, feedNameRect].maxY() - textBoxRect.origin.y)
|
||||
textBoxRect.size.height = ceil([titleRect, summaryRect, textRect, dateRect, feedNameRect].maxY() - textBoxRect.origin.y)
|
||||
let avatarImageRect = TimelineCellLayout.rectForAvatar(cellData, appearance, textBoxRect, width)
|
||||
let unreadIndicatorRect = TimelineCellLayout.rectForUnreadIndicator(appearance, titleRect)
|
||||
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, 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 {
|
||||
@ -76,18 +84,52 @@ private extension TimelineCellLayout {
|
||||
return textBoxRect
|
||||
}
|
||||
|
||||
static func rectForTitle(_ textBoxRect: NSRect, _ cellData: TimelineCellData) -> NSRect {
|
||||
static func rectForTitle(_ textBoxRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> (NSRect, Int) {
|
||||
|
||||
var r = textBoxRect
|
||||
let height = MultilineTextFieldSizer.size(for: cellData.title, font: appearance.titleFont, numberOfLines: 2, width: Int(textBoxRect.width))
|
||||
r.size.height = CGFloat(height)
|
||||
|
||||
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 rectForSummary(_ textBoxRect: NSRect, _ titleRect: NSRect, _ titleNumberOfLines: Int, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
|
||||
|
||||
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, _ titleRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
|
||||
static func rectForDate(_ textBoxRect: NSRect, _ rectAbove: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
|
||||
|
||||
return rectOfLineBelow(textBoxRect, titleRect, appearance.titleBottomMargin, cellData.dateString, appearance.dateFont)
|
||||
return rectOfLineBelow(textBoxRect, rectAbove, appearance.titleBottomMargin, cellData.dateString, appearance.dateFont)
|
||||
}
|
||||
|
||||
static func rectForFeedName(_ textBoxRect: NSRect, _ dateRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
|
||||
|
@ -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() {
|
||||
|
||||
|
@ -146,7 +146,7 @@ private extension TimelineTableCellView {
|
||||
textField.usesSingleLineMode = false
|
||||
textField.maximumNumberOfLines = 2
|
||||
textField.isEditable = false
|
||||
textField.lineBreakMode = .byTruncatingTail
|
||||
// textField.lineBreakMode = .byTruncatingTail
|
||||
textField.cell?.truncatesLastVisibleLine = true
|
||||
textField.allowsDefaultTighteningForTruncation = false
|
||||
return textField
|
||||
@ -171,6 +171,7 @@ private extension TimelineTableCellView {
|
||||
else {
|
||||
feedNameView.textColor = cellAppearance.feedNameColor
|
||||
dateView.textColor = cellAppearance.dateColor
|
||||
titleView.textColor = cellAppearance.titleColor
|
||||
}
|
||||
}
|
||||
|
||||
@ -178,6 +179,7 @@ private extension TimelineTableCellView {
|
||||
|
||||
feedNameView.font = cellAppearance.feedNameFont
|
||||
dateView.font = cellAppearance.dateFont
|
||||
titleView.font = cellAppearance.titleFont
|
||||
}
|
||||
|
||||
func updateTextFields() {
|
||||
@ -222,17 +224,7 @@ private extension TimelineTableCellView {
|
||||
|
||||
func updateTitleView() {
|
||||
|
||||
if isEmphasized && isSelected {
|
||||
if let attributedTitle = cellData?.attributedTitle {
|
||||
titleView.attributedStringValue = attributedTitle.rs_attributedStringByMakingTextWhite()
|
||||
}
|
||||
}
|
||||
else {
|
||||
if let attributedTitle = cellData?.attributedTitle {
|
||||
titleView.attributedStringValue = attributedTitle
|
||||
}
|
||||
}
|
||||
|
||||
titleView.stringValue = cellData?.title ?? ""
|
||||
needsLayout = true
|
||||
}
|
||||
|
||||
|
@ -133,8 +133,6 @@ class TimelineViewController: NSViewController, UndoableCommandRunner {
|
||||
|
||||
private func fontSizeDidChange() {
|
||||
|
||||
TimelineCellData.emptyCache()
|
||||
|
||||
cellAppearance = TimelineCellAppearance(theme: appDelegate.currentTheme, showAvatar: false, fontSize: fontSize)
|
||||
cellAppearanceWithAvatar = TimelineCellAppearance(theme: appDelegate.currentTheme, showAvatar: true, fontSize: fontSize)
|
||||
updateRowHeights()
|
||||
@ -591,7 +589,7 @@ extension TimelineViewController: NSTableViewDelegate {
|
||||
private func makeTimelineCellEmpty(_ cell: TimelineTableCellView) {
|
||||
|
||||
cell.objectValue = nil
|
||||
cell.cellData = emptyCellData
|
||||
cell.cellData = TimelineCellData()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -121,6 +121,8 @@
|
||||
<integer>7</integer>
|
||||
<key>starDimension</key>
|
||||
<integer>13</integer>
|
||||
<key>titleMaximumLines</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>Detail</key>
|
||||
|
Loading…
x
Reference in New Issue
Block a user