Do some refactoring.

This commit is contained in:
Brent Simmons 2017-11-01 22:40:28 -07:00
parent af3f41fbda
commit 911e6b0879
7 changed files with 325 additions and 295 deletions

View File

@ -87,6 +87,7 @@
84F204CE1FAACB660076E152 /* FeedListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204CD1FAACB660076E152 /* FeedListViewController.swift */; };
84F204DE1FAACB8B0076E152 /* FeedListTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DD1FAACB8B0076E152 /* FeedListTimelineViewController.swift */; };
84F204E01FAACBB30076E152 /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; };
84F204E21FAAD4750076E152 /* TimelineTableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204E11FAAD4750076E152 /* TimelineTableViewDelegate.swift */; };
84FB3A6F1FA6612C00EFC320 /* TimelineTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FB3A6E1FA6612C00EFC320 /* TimelineTableViewDataSource.swift */; };
84FB9A2F1EDCD6C4003D53B9 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84FB9A2D1EDCD6B8003D53B9 /* Sparkle.framework */; };
84FB9A301EDCD6C4003D53B9 /* Sparkle.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84FB9A2D1EDCD6B8003D53B9 /* Sparkle.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@ -453,6 +454,7 @@
84F204CD1FAACB660076E152 /* FeedListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListViewController.swift; sourceTree = "<group>"; };
84F204DD1FAACB8B0076E152 /* FeedListTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListTimelineViewController.swift; sourceTree = "<group>"; };
84F204DF1FAACBB30076E152 /* ArticleArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleArray.swift; sourceTree = "<group>"; };
84F204E11FAAD4750076E152 /* TimelineTableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewDelegate.swift; sourceTree = "<group>"; };
84FB3A6E1FA6612C00EFC320 /* TimelineTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewDataSource.swift; sourceTree = "<group>"; };
84FB9A2D1EDCD6B8003D53B9 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = Frameworks/Vendor/Sparkle.framework; sourceTree = SOURCE_ROOT; };
/* End PBXFileReference section */
@ -589,6 +591,7 @@
849A976B1ED9EBC8007D329B /* TimelineViewController.swift */,
84F204DF1FAACBB30076E152 /* ArticleArray.swift */,
84FB3A6E1FA6612C00EFC320 /* TimelineTableViewDataSource.swift */,
84F204E11FAAD4750076E152 /* TimelineTableViewDelegate.swift */,
849A97691ED9EBC8007D329B /* TimelineTableRowView.swift */,
849A976A1ED9EBC8007D329B /* TimelineTableView.swift */,
849A976F1ED9EC04007D329B /* Cell */,
@ -1220,6 +1223,7 @@
849A97431ED9EAA9007D329B /* AddFolderWindowController.swift in Sources */,
849A97921ED9EF65007D329B /* IndeterminateProgressWindowController.swift in Sources */,
849A97801ED9EC42007D329B /* DetailViewController.swift in Sources */,
84F204E21FAAD4750076E152 /* TimelineTableViewDelegate.swift in Sources */,
849A976E1ED9EBC8007D329B /* TimelineViewController.swift in Sources */,
849A978D1ED9EE4D007D329B /* FeedListWindowController.swift in Sources */,
849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */,

View File

@ -9,6 +9,8 @@
import Foundation
import Data
typealias ArticleArray = [Article]
extension Array where Element == Article {
func articleAtRow(_ row: Int) -> Article? {
@ -19,4 +21,86 @@ extension Array where Element == Article {
return self[row]
}
func rowOfNextUnreadArticle(_ selectedRow: Int) -> Int? {
if isEmpty {
return nil
}
var rowIndex = selectedRow
while(true) {
rowIndex = rowIndex + 1
if rowIndex >= count {
break
}
let article = articleAtRow(rowIndex)!
if !article.status.read {
return rowIndex
}
}
return nil
}
func articlesForIndexes(_ indexes: IndexSet) -> Set<Article> {
return Set(indexes.flatMap{ (oneIndex) -> Article? in
return articleAtRow(oneIndex)
})
}
func indexesForArticleIDs(_ articleIDs: Set<String>) -> IndexSet {
var indexes = IndexSet()
articleIDs.forEach { (articleID) in
let oneIndex = rowForArticleID(articleID)
if oneIndex != NSNotFound {
indexes.insert(oneIndex)
}
}
return indexes
}
func sortedByDate() -> ArticleArray {
return sorted(by: articleComparator)
}
func canMarkAllAsRead() -> Bool {
for article in self {
if !article.status.read {
return true
}
}
return false
}
}
private extension Array where Element == Article {
func rowForArticleID(_ articleID: String) -> Int {
if let index = index(where: { $0.articleID == articleID }) {
return index
}
return NSNotFound
}
func rowForArticle(_ article: Article) -> Int {
return rowForArticleID(article.articleID)
}
// MARK: Sorting
func articleComparator(_ article1: Article, article2: Article) -> Bool {
return article1.logicalDatePublished > article2.logicalDatePublished
}
}

View File

@ -41,4 +41,16 @@ class TimelineTableView: NSTableView {
super.viewDidEndLiveResize()
}
func redrawGrid() {
guard let rowViews = visibleRowViews() else {
return
}
rowViews.forEach{ (rowView) in
if let rowView = rowView as? TimelineTableRowView {
rowView.invalidateGridRect()
}
}
}
}

View File

@ -21,12 +21,12 @@ import Cocoa
func numberOfRows(in tableView: NSTableView) -> Int {
return timelineViewController?.numberOfArticles ?? 0
return timelineViewController?.articles.count ?? 0
}
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
return timelineViewController?.articleAtRow(row) ?? nil
return timelineViewController?.articles.articleAtRow(row) ?? nil
}
}

View File

@ -0,0 +1,112 @@
//
// TimelineTableViewDelegate.swift
// Evergreen
//
// Created by Brent Simmons on 11/1/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Cocoa
import Data
@objc final class TimelineTableViewDelegate: NSObject, NSTableViewDelegate {
private weak var timelineViewController: TimelineViewController?
init(timelineViewController: TimelineViewController) {
self.timelineViewController = timelineViewController
}
// MARK: NSTableViewDelegate
func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
guard let timelineViewController = timelineViewController else {
return nil
}
let rowView: TimelineTableRowView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "timelineRow"), owner: self) as! TimelineTableRowView
rowView.cellAppearance = timelineViewController.cellAppearance
return rowView
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
guard let timelineViewController = timelineViewController else {
return nil
}
let cell: TimelineTableCellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "timelineCell"), owner: self) as! TimelineTableCellView
cell.cellAppearance = timelineViewController.cellAppearance
if let article = timelineViewController.articles.articleAtRow(row) {
configureTimelineCell(cell, article: article)
}
else {
makeTimelineCellEmpty(cell)
}
return cell
}
private func postTimelineSelectionDidChangeNotification(_ selectedArticle: Article?) {
guard let timelineViewController = timelineViewController else {
return
}
let appInfo = AppInfo()
if let article = selectedArticle {
appInfo.article = article
}
appInfo.view = timelineViewController.tableView
NotificationCenter.default.post(name: .TimelineSelectionDidChange, object: timelineViewController, userInfo: appInfo.userInfo)
}
func tableViewSelectionDidChange(_ notification: Notification) {
guard let timelineViewController = timelineViewController, let tableView = timelineViewController.tableView else {
return
}
tableView.redrawGrid()
let selectedRow = tableView.selectedRow
if selectedRow < 0 || selectedRow == NSNotFound || tableView.numberOfSelectedRows != 1 {
postTimelineSelectionDidChangeNotification(nil)
return
}
if let selectedArticle = timelineViewController.articles.articleAtRow(selectedRow) {
if (!selectedArticle.status.read) {
markArticles(Set([selectedArticle]), statusKey: .read, flag: true)
}
postTimelineSelectionDidChangeNotification(selectedArticle)
}
else {
postTimelineSelectionDidChangeNotification(nil)
}
}
}
private extension TimelineTableViewDelegate {
func configureTimelineCell(_ cell: TimelineTableCellView, article: Article) {
guard let timelineViewController = timelineViewController else {
return
}
cell.objectValue = article
cell.cellData = TimelineCellData(article: article, appearance: timelineViewController.cellAppearance, showFeedName: timelineViewController.showFeedNames)
}
func makeTimelineCellEmpty(_ cell: TimelineTableCellView) {
cell.objectValue = nil
cell.cellData = emptyCellData
}
}

View File

@ -9,30 +9,16 @@
import Foundation
import RSCore
import RSTextDrawing
import RSTree
import Data
import Account
class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource, KeyboardDelegate {
class TimelineViewController: NSViewController, KeyboardDelegate {
@IBOutlet var tableView: TimelineTableView!
private var undoableCommands = [UndoableCommand]()
private var dataSource: TimelineTableViewDataSource!
var didRegisterForNotifications = false
var fontSize: FontSize = AppDefaults.shared.timelineFontSize {
didSet {
fontSizeDidChange()
}
}
var cellAppearance: TimelineCellAppearance!
var showFeedNames = false
var numberOfArticles: Int {
get {
return articles.count
}
}
private var articles = [Article]() {
var articles = ArticleArray() {
didSet {
if articles != oldValue {
clearUndoableCommands()
@ -41,6 +27,29 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
}
}
var selectedArticles: [Article] {
get {
return Array(articles.articlesForIndexes(tableView.selectedRowIndexes))
}
}
private var undoableCommands = [UndoableCommand]()
private lazy var tableViewDataSource: TimelineTableViewDataSource! = {
return TimelineTableViewDataSource(timelineViewController: self)
}()
private lazy var tableViewDelegate: TimelineTableViewDelegate! = {
return TimelineTableViewDelegate(timelineViewController: self)
}()
private var didRegisterForNotifications = false
private var fontSize: FontSize = AppDefaults.shared.timelineFontSize {
didSet {
fontSizeDidChange()
}
}
private var representedObjects: [AnyObject]? {
didSet {
if !representedObjectArraysAreEqual(oldValue, representedObjects) {
@ -52,20 +61,6 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
}
}
private var showFeedNames: Bool {
// if let _ = node?.representedObject as? Feed {
return false
// }
// return true
}
var selectedArticles: [Article] {
get {
return Array(articlesForIndexes(tableView.selectedRowIndexes))
}
}
private var oneSelectedArticle: Article? {
get {
return selectedArticles.count == 1 ? selectedArticles.first : nil
@ -76,15 +71,13 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
override func viewDidLoad() {
dataSource = TimelineTableViewDataSource(timelineViewController: self)
tableView.dataSource = dataSource
cellAppearance = TimelineCellAppearance(theme: currentTheme, fontSize: fontSize)
tableView.rowHeight = calculateRowHeight()
tableView.dataSource = tableViewDataSource
tableView.delegate = tableViewDelegate
tableView.rowHeight = calculateRowHeight()
tableView.target = self
tableView.doubleAction = #selector(openArticleInBrowser(_:))
tableView.keyboardDelegate = self
if !didRegisterForNotifications {
@ -229,36 +222,13 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
}
func canMarkAllAsRead() -> Bool {
for article in articles {
if !article.status.read {
return true
}
}
return false
return articles.canMarkAllAsRead()
}
func indexOfNextUnreadArticle() -> Int? {
if articles.isEmpty {
return nil
}
var rowIndex = tableView.selectedRow
while(true) {
rowIndex = rowIndex + 1
if rowIndex >= articles.count {
break
}
let article = articleAtRow(rowIndex)!
if !article.status.read {
return rowIndex
}
}
return nil
return articles.rowOfNextUnreadArticle(tableView.selectedRow)
}
// MARK: - Notifications
@ -368,106 +338,10 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
private func reloadCellsForArticleIDs(_ articleIDs: Set<String>) {
let indexes = indexesForArticleIDs(articleIDs)
let indexes = articles.indexesForArticleIDs(articleIDs)
tableView.reloadData(forRowIndexes: indexes, columnIndexes: NSIndexSet(index: 0) as IndexSet)
}
// MARK: - Articles
private func indexesForArticleIDs(_ articleIDs: Set<String>) -> IndexSet {
var indexes = IndexSet()
articleIDs.forEach { (articleID) in
let oneIndex = rowForArticleID(articleID)
if oneIndex != NSNotFound {
indexes.insert(oneIndex)
}
}
return indexes
}
private func articlesForIndexes(_ indexes: IndexSet) -> Set<Article> {
return Set(indexes.flatMap{ (oneIndex) -> Article? in
return articleAtRow(oneIndex)
})
}
func articleAtRow(_ row: Int) -> Article? {
if row < 0 || row == NSNotFound || row > articles.count - 1 {
return nil
}
return articles[row]
}
private func rowForArticle(_ article: Article) -> Int {
return rowForArticleID(article.articleID)
}
private func rowForArticleID(_ articleID: String) -> Int {
if let index = articles.index(where: { $0.articleID == articleID }) {
return index
}
return NSNotFound
}
func selectedArticle() -> Article? {
return articleAtRow(tableView.selectedRow)
}
// MARK: Sorting Articles
private func articleComparator(_ article1: Article, article2: Article) -> Bool {
return article1.logicalDatePublished > article2.logicalDatePublished
}
private func articlesSortedByDate(_ articles: Set<Article>) -> [Article] {
return Array(articles).sorted(by: articleComparator)
}
// MARK: Fetching Articles
private func emptyTheTimeline() {
if !articles.isEmpty {
articles = [Article]()
}
}
private func fetchArticles() {
guard let representedObjects = representedObjects else {
emptyTheTimeline()
return
}
var fetchedArticles = Set<Article>()
for object in representedObjects {
if let feed = object as? Feed {
fetchedArticles.formUnion(feed.fetchArticles())
}
else if let folder = object as? Folder {
fetchedArticles.formUnion(folder.fetchArticles())
}
}
let sortedArticles = articlesSortedByDate(fetchedArticles)
if articles != sortedArticles {
articles = sortedArticles
}
}
// MARK: - Cell Configuring
private func calculateRowHeight() -> CGFloat {
@ -482,84 +356,47 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
return height
}
private func configureTimelineCell(_ cell: TimelineTableCellView, article: Article) {
cell.objectValue = article
cell.cellData = TimelineCellData(article: article, appearance: cellAppearance, showFeedName: showFeedNames)
}
}
private extension TimelineViewController {
private func makeTimelineCellEmpty(_ cell: TimelineTableCellView) {
cell.objectValue = nil
cell.cellData = emptyCellData
}
// MARK: - NSTableViewDataSource
func numberOfRows(in tableView: NSTableView) -> Int {
return articles.count
}
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
return articleAtRow(row)
}
// MARK: - NSTableViewDelegate
func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
let rowView: TimelineTableRowView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "timelineRow"), owner: self) as! TimelineTableRowView
rowView.cellAppearance = cellAppearance
return rowView
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let cell: TimelineTableCellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "timelineCell"), owner: self) as! TimelineTableCellView
cell.cellAppearance = cellAppearance
if let article = articleAtRow(row) {
configureTimelineCell(cell, article: article)
var hasAtLeastOneSelectedArticle: Bool {
get {
return tableView.selectedRow != -1
}
else {
makeTimelineCellEmpty(cell)
}
return cell
}
private func postTimelineSelectionDidChangeNotification(_ selectedArticle: Article?) {
func emptyTheTimeline() {
let appInfo = AppInfo()
if let article = selectedArticle {
appInfo.article = article
if !articles.isEmpty {
articles = [Article]()
}
appInfo.view = tableView
NotificationCenter.default.post(name: .TimelineSelectionDidChange, object: self, userInfo: appInfo.userInfo)
}
func tableViewSelectionDidChange(_ notification: Notification) {
// MARK: Fetching Articles
tableView.redrawGrid()
let selectedRow = tableView.selectedRow
if selectedRow < 0 || selectedRow == NSNotFound || tableView.numberOfSelectedRows != 1 {
postTimelineSelectionDidChangeNotification(nil)
func fetchArticles() {
guard let representedObjects = representedObjects else {
emptyTheTimeline()
return
}
if let selectedArticle = articleAtRow(selectedRow) {
if (!selectedArticle.status.read) {
markArticles(Set([selectedArticle]), statusKey: .read, flag: true)
var fetchedArticles = Set<Article>()
for object in representedObjects {
if let feed = object as? Feed {
fetchedArticles.formUnion(feed.fetchArticles())
}
else if let folder = object as? Folder {
fetchedArticles.formUnion(folder.fetchArticles())
}
postTimelineSelectionDidChangeNotification(selectedArticle)
}
else {
postTimelineSelectionDidChangeNotification(nil)
let sortedArticles = Array(fetchedArticles).sortedByDate()
if articles != sortedArticles {
articles = sortedArticles
}
}
@ -586,72 +423,3 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
}
}
private extension TimelineViewController {
var hasAtLeastOneSelectedArticle: Bool {
get {
return self.tableView.selectedRow != -1
}
}
}
// MARK: - NSTableView extension
private extension NSTableView {
func scrollTo(row: Int) {
guard let scrollView = self.enclosingScrollView else {
return
}
let documentVisibleRect = scrollView.documentVisibleRect
let r = rect(ofRow: row)
if NSContainsRect(documentVisibleRect, r) {
return
}
let rMidY = NSMidY(r)
var scrollPoint = NSZeroPoint;
let extraHeight = 150
scrollPoint.y = floor(rMidY - (documentVisibleRect.size.height / 2.0)) + CGFloat(extraHeight)
scrollPoint.y = max(scrollPoint.y, 0)
let maxScrollPointY = frame.size.height - documentVisibleRect.size.height
scrollPoint.y = min(maxScrollPointY, scrollPoint.y)
let clipView = scrollView.contentView
let rClipView = NSMakeRect(scrollPoint.x, scrollPoint.y, NSWidth(clipView.bounds), NSHeight(clipView.bounds))
clipView.animator().bounds = rClipView
}
func visibleRowViews() -> [TimelineTableRowView]? {
guard let scrollView = self.enclosingScrollView, numberOfRows > 0 else {
return nil
}
let range = rows(in: scrollView.documentVisibleRect)
let ixMax = numberOfRows - 1
let ixStart = min(range.location, ixMax)
let ixEnd = min(((range.location + range.length) - 1), ixMax)
var visibleRows = [TimelineTableRowView]()
for ixRow in ixStart...ixEnd {
if let oneRowView = rowView(atRow: ixRow, makeIfNecessary: false) as? TimelineTableRowView {
visibleRows += [oneRowView]
}
}
return visibleRows.isEmpty ? nil : visibleRows
}
func redrawGrid() {
visibleRowViews()?.forEach { $0.invalidateGridRect() }
}
}

View File

@ -15,4 +15,54 @@ public extension NSTableView {
return selectedRowIndexes.startIndex == selectedRowIndexes.endIndex
}
}
func scrollTo(row: Int) {
guard let scrollView = self.enclosingScrollView else {
return
}
let documentVisibleRect = scrollView.documentVisibleRect
let r = rect(ofRow: row)
if NSContainsRect(documentVisibleRect, r) {
return
}
let rMidY = NSMidY(r)
var scrollPoint = NSZeroPoint;
let extraHeight = 150
scrollPoint.y = floor(rMidY - (documentVisibleRect.size.height / 2.0)) + CGFloat(extraHeight)
scrollPoint.y = max(scrollPoint.y, 0)
let maxScrollPointY = frame.size.height - documentVisibleRect.size.height
scrollPoint.y = min(maxScrollPointY, scrollPoint.y)
let clipView = scrollView.contentView
let rClipView = NSMakeRect(scrollPoint.x, scrollPoint.y, NSWidth(clipView.bounds), NSHeight(clipView.bounds))
clipView.animator().bounds = rClipView
}
func visibleRowViews() -> [NSTableRowView]? {
guard let scrollView = self.enclosingScrollView, numberOfRows > 0 else {
return nil
}
let range = rows(in: scrollView.documentVisibleRect)
let ixMax = numberOfRows - 1
let ixStart = min(range.location, ixMax)
let ixEnd = min(((range.location + range.length) - 1), ixMax)
var visibleRows = [NSTableRowView]()
for ixRow in ixStart...ixEnd {
if let oneRowView = rowView(atRow: ixRow, makeIfNecessary: false) {
visibleRows += [oneRowView]
}
}
return visibleRows.isEmpty ? nil : visibleRows
}
}