Add attributed title support in the timeline
This commit is contained in:
parent
ec49030081
commit
c555646fb2
|
@ -34,6 +34,7 @@ final class MultilineTextFieldSizer {
|
|||
private let singleLineHeightEstimate: Int
|
||||
private let doubleLineHeightEstimate: Int
|
||||
private var cache = [String: WidthHeightCache]() // Each string has a cache.
|
||||
private var attributedCache = [NSAttributedString: WidthHeightCache]()
|
||||
private static var sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]()
|
||||
|
||||
private init(numberOfLines: Int, font: NSFont) {
|
||||
|
@ -51,6 +52,14 @@ final class MultilineTextFieldSizer {
|
|||
return sizer(numberOfLines: numberOfLines, font: font).sizeInfo(for: string, width: width)
|
||||
}
|
||||
|
||||
static func size(for attributedString: NSAttributedString, numberOfLines: Int, width: Int) -> TextFieldSizeInfo {
|
||||
|
||||
// Assumes the same font family/size for the whole string
|
||||
let font = attributedString.attribute(.font, at: 0, effectiveRange: nil) as! NSFont
|
||||
|
||||
return sizer(numberOfLines: numberOfLines, font: font).sizeInfo(for: attributedString, width: width)
|
||||
}
|
||||
|
||||
static func emptyCache() {
|
||||
|
||||
sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]()
|
||||
|
@ -83,6 +92,16 @@ private extension MultilineTextFieldSizer {
|
|||
return sizeInfo
|
||||
}
|
||||
|
||||
func sizeInfo(for attributedString: NSAttributedString, width: Int) -> TextFieldSizeInfo {
|
||||
|
||||
let textFieldHeight = height(for: attributedString, 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 {
|
||||
|
@ -103,6 +122,26 @@ private extension MultilineTextFieldSizer {
|
|||
return height
|
||||
}
|
||||
|
||||
func height(for attribtuedString: NSAttributedString, width: Int) -> Int {
|
||||
|
||||
if attributedCache[attribtuedString] == nil {
|
||||
attributedCache[attribtuedString] = WidthHeightCache()
|
||||
}
|
||||
|
||||
if let height = attributedCache[attribtuedString]![width] {
|
||||
return height
|
||||
}
|
||||
|
||||
if let height = heightConsideringNeighbors(attributedCache[attribtuedString]!, width) {
|
||||
return height
|
||||
}
|
||||
|
||||
let height = calculateHeight(attribtuedString, width)
|
||||
attributedCache[attribtuedString]![width] = height
|
||||
|
||||
return height
|
||||
}
|
||||
|
||||
static func createTextField(_ numberOfLines: Int, _ font: NSFont) -> NSTextField {
|
||||
|
||||
let textField = NSTextField(wrappingLabelWithString: "")
|
||||
|
@ -120,6 +159,14 @@ private extension MultilineTextFieldSizer {
|
|||
return MultilineTextFieldSizer.calculateHeight(string, width, textField)
|
||||
}
|
||||
|
||||
func calculateHeight(_ attributedString: NSAttributedString, _ width: Int) -> Int {
|
||||
|
||||
textField.attributedStringValue = attributedString
|
||||
textField.preferredMaxLayoutWidth = CGFloat(width)
|
||||
let size = textField.fittingSize
|
||||
return Int(ceil(size.height))
|
||||
}
|
||||
|
||||
static func calculateHeight(_ string: String, _ width: Int, _ textField: NSTextField) -> Int {
|
||||
|
||||
textField.stringValue = string
|
||||
|
|
|
@ -12,6 +12,7 @@ import Articles
|
|||
struct TimelineCellData {
|
||||
|
||||
let title: String
|
||||
let attributedTitle: NSAttributedString
|
||||
let text: String
|
||||
let dateString: String
|
||||
let feedName: String
|
||||
|
@ -26,6 +27,7 @@ struct TimelineCellData {
|
|||
init(article: Article, showFeedName: TimelineShowFeedName, feedName: String?, byline: String?, iconImage: IconImage?, showIcon: Bool, featuredImage: NSImage?) {
|
||||
|
||||
self.title = ArticleStringFormatter.truncatedTitle(article)
|
||||
self.attributedTitle = ArticleStringFormatter.attributedTruncatedTitle(article)
|
||||
self.text = ArticleStringFormatter.truncatedSummary(article)
|
||||
|
||||
self.dateString = ArticleStringFormatter.dateString(article.logicalDatePublished)
|
||||
|
@ -64,5 +66,6 @@ struct TimelineCellData {
|
|||
self.featuredImage = nil
|
||||
self.read = true
|
||||
self.starred = false
|
||||
self.attributedTitle = NSAttributedString()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -115,7 +115,8 @@ private extension TimelineCellLayout {
|
|||
return (r, 0)
|
||||
}
|
||||
|
||||
let sizeInfo = MultilineTextFieldSizer.size(for: cellData.title, font: appearance.titleFont, numberOfLines: appearance.titleNumberOfLines, width: Int(textBoxRect.width))
|
||||
let attributedTitle = cellData.attributedTitle.adding(font: appearance.titleFont)
|
||||
let sizeInfo = MultilineTextFieldSizer.size(for: attributedTitle, numberOfLines: appearance.titleNumberOfLines, width: Int(textBoxRect.width))
|
||||
r.size.height = sizeInfo.size.height
|
||||
if sizeInfo.numberOfLinesUsed < 1 {
|
||||
r.size.height = 0
|
||||
|
|
|
@ -222,6 +222,7 @@ private extension TimelineTableCellView {
|
|||
func updateTitleView() {
|
||||
|
||||
updateTextFieldText(titleView, cellData?.title)
|
||||
updateTextFieldAttributedText(titleView, cellData?.attributedTitle)
|
||||
}
|
||||
|
||||
func updateSummaryView() {
|
||||
|
@ -247,6 +248,19 @@ private extension TimelineTableCellView {
|
|||
}
|
||||
}
|
||||
|
||||
func updateTextFieldAttributedText(_ textField: NSTextField, _ text: NSAttributedString?) {
|
||||
var s = text ?? NSAttributedString(string: "")
|
||||
|
||||
if let fieldFont = textField.font, let color = textField.textColor {
|
||||
s = s.adding(font: fieldFont, color: color)
|
||||
}
|
||||
|
||||
if textField.attributedStringValue != s {
|
||||
textField.attributedStringValue = s
|
||||
needsLayout = true
|
||||
}
|
||||
}
|
||||
|
||||
func updateFeedNameView() {
|
||||
switch cellData.showFeedName {
|
||||
case .byline:
|
||||
|
|
|
@ -46,7 +46,7 @@ struct ArticleRenderer {
|
|||
self.article = article
|
||||
self.extractedArticle = extractedArticle
|
||||
self.articleStyle = style
|
||||
self.title = article?.title ?? ""
|
||||
self.title = article?.sanitizedTitle() ?? ""
|
||||
if let content = extractedArticle?.content {
|
||||
self.body = content
|
||||
self.baseURL = extractedArticle?.url
|
||||
|
|
|
@ -52,8 +52,8 @@ struct ArticleStringFormatter {
|
|||
return s
|
||||
}
|
||||
|
||||
static func truncatedTitle(_ article: Article) -> String {
|
||||
guard let title = article.title else {
|
||||
static func truncatedTitle(_ article: Article, forHTML: Bool = false) -> String {
|
||||
guard let title = article.sanitizedTitle(forHTML: forHTML) else {
|
||||
return ""
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,11 @@ struct ArticleStringFormatter {
|
|||
var s = title.replacingOccurrences(of: "\n", with: "")
|
||||
s = s.replacingOccurrences(of: "\r", with: "")
|
||||
s = s.replacingOccurrences(of: "\t", with: "")
|
||||
s = s.rsparser_stringByDecodingHTMLEntities()
|
||||
|
||||
if !forHTML {
|
||||
s = s.rsparser_stringByDecodingHTMLEntities()
|
||||
}
|
||||
|
||||
s = s.trimmingWhitespace
|
||||
s = s.collapsingWhitespace
|
||||
|
||||
|
@ -79,6 +83,13 @@ struct ArticleStringFormatter {
|
|||
return s
|
||||
}
|
||||
|
||||
static func attributedTruncatedTitle(_ article: Article) -> NSAttributedString {
|
||||
let title = truncatedTitle(article, forHTML: true)
|
||||
let data = title.data(using: .utf8)!
|
||||
let attributed = NSAttributedString(html: data, documentAttributes: nil)!
|
||||
return attributed
|
||||
}
|
||||
|
||||
static func truncatedSummary(_ article: Article) -> String {
|
||||
guard let body = article.body else {
|
||||
return ""
|
||||
|
|
Loading…
Reference in New Issue