From d3846a6a370af6870d3bb3d70c50d5165d46736c Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Wed, 21 Feb 2018 22:48:34 -0800 Subject: [PATCH] Continue progress toward variable row heights. --- Evergreen/AppDelegate.swift | 1 - .../Cell/MultilineTextFieldSizer.swift | 21 +++++- .../Cell/TimelineCellAppearance.swift | 9 ++- .../Timeline/Cell/TimelineCellData.swift | 42 ------------ .../Timeline/Cell/TimelineCellLayout.swift | 64 +++++++++++++++---- .../Cell/TimelineStringUtilities.swift | 3 +- .../Timeline/Cell/TimelineTableCellView.swift | 16 ++--- .../Timeline/TimelineViewController.swift | 4 +- Evergreen/Resources/DB5.plist | 2 + 9 files changed, 86 insertions(+), 76 deletions(-) diff --git a/Evergreen/AppDelegate.swift b/Evergreen/AppDelegate.swift index 25e298b3c..13c8fc80b 100644 --- a/Evergreen/AppDelegate.swift +++ b/Evergreen/AppDelegate.swift @@ -164,7 +164,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, func applicationDidResignActive(_ notification: Notification) { - TimelineCellData.emptyCache() timelineEmptyCaches() saveState() diff --git a/Evergreen/MainWindow/Timeline/Cell/MultilineTextFieldSizer.swift b/Evergreen/MainWindow/Timeline/Cell/MultilineTextFieldSizer.swift index 85b5da751..0f15b7676 100644 --- a/Evergreen/MainWindow/Timeline/Cell/MultilineTextFieldSizer.swift +++ b/Evergreen/MainWindow/Timeline/Cell/MultilineTextFieldSizer.swift @@ -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) diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift index ca40fb244..a552efeee 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift @@ -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 diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineCellData.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineCellData.swift index cfe2f33ab..c063859be 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineCellData.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineCellData.swift @@ -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]) -} - diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift index ab86a7b10..18392af84 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift @@ -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 { diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineStringUtilities.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineStringUtilities.swift index 6dd0acfdd..0d09d1a99 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineStringUtilities.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineStringUtilities.swift @@ -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() { diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift index 4dae483b2..9b6c5fe44 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift @@ -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 } diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController.swift b/Evergreen/MainWindow/Timeline/TimelineViewController.swift index d445e0150..a870518ef 100644 --- a/Evergreen/MainWindow/Timeline/TimelineViewController.swift +++ b/Evergreen/MainWindow/Timeline/TimelineViewController.swift @@ -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() } } diff --git a/Evergreen/Resources/DB5.plist b/Evergreen/Resources/DB5.plist index 888b6c830..9e50e9602 100644 --- a/Evergreen/Resources/DB5.plist +++ b/Evergreen/Resources/DB5.plist @@ -121,6 +121,8 @@ 7 starDimension 13 + titleMaximumLines + 2 Detail