From 911e6b0879eecf66e4566a0e7b0924a32ecdb1c0 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Wed, 1 Nov 2017 22:40:28 -0700 Subject: [PATCH] Do some refactoring. --- Evergreen.xcodeproj/project.pbxproj | 4 + .../MainWindow/Timeline/ArticleArray.swift | 84 +++++ .../Timeline/TimelineTableView.swift | 12 + .../TimelineTableViewDataSource.swift | 4 +- .../Timeline/TimelineTableViewDelegate.swift | 112 ++++++ .../Timeline/TimelineViewController.swift | 354 +++--------------- .../RSCore/NSTableView+Extensions.swift | 50 +++ 7 files changed, 325 insertions(+), 295 deletions(-) create mode 100644 Evergreen/MainWindow/Timeline/TimelineTableViewDelegate.swift diff --git a/Evergreen.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index 8cf6efa2e..ecc81ec7e 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -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 = ""; }; 84F204DD1FAACB8B0076E152 /* FeedListTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListTimelineViewController.swift; sourceTree = ""; }; 84F204DF1FAACBB30076E152 /* ArticleArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleArray.swift; sourceTree = ""; }; + 84F204E11FAAD4750076E152 /* TimelineTableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewDelegate.swift; sourceTree = ""; }; 84FB3A6E1FA6612C00EFC320 /* TimelineTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewDataSource.swift; sourceTree = ""; }; 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 */, diff --git a/Evergreen/MainWindow/Timeline/ArticleArray.swift b/Evergreen/MainWindow/Timeline/ArticleArray.swift index 2968a81d8..26df12010 100644 --- a/Evergreen/MainWindow/Timeline/ArticleArray.swift +++ b/Evergreen/MainWindow/Timeline/ArticleArray.swift @@ -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
{ + + return Set(indexes.flatMap{ (oneIndex) -> Article? in + return articleAtRow(oneIndex) + }) + } + + func indexesForArticleIDs(_ articleIDs: Set) -> 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 + } + } diff --git a/Evergreen/MainWindow/Timeline/TimelineTableView.swift b/Evergreen/MainWindow/Timeline/TimelineTableView.swift index 406ed730a..332201e8e 100644 --- a/Evergreen/MainWindow/Timeline/TimelineTableView.swift +++ b/Evergreen/MainWindow/Timeline/TimelineTableView.swift @@ -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() + } + } + } } diff --git a/Evergreen/MainWindow/Timeline/TimelineTableViewDataSource.swift b/Evergreen/MainWindow/Timeline/TimelineTableViewDataSource.swift index 3ef05d89b..3dab3bc7c 100644 --- a/Evergreen/MainWindow/Timeline/TimelineTableViewDataSource.swift +++ b/Evergreen/MainWindow/Timeline/TimelineTableViewDataSource.swift @@ -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 } } diff --git a/Evergreen/MainWindow/Timeline/TimelineTableViewDelegate.swift b/Evergreen/MainWindow/Timeline/TimelineTableViewDelegate.swift new file mode 100644 index 000000000..c08f0e6b2 --- /dev/null +++ b/Evergreen/MainWindow/Timeline/TimelineTableViewDelegate.swift @@ -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 + } +} diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController.swift b/Evergreen/MainWindow/Timeline/TimelineViewController.swift index 6682052f4..f87dca555 100644 --- a/Evergreen/MainWindow/Timeline/TimelineViewController.swift +++ b/Evergreen/MainWindow/Timeline/TimelineViewController.swift @@ -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) { - 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) -> 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
{ - - 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] { - - 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
() - - 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
() + + 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() } - } -} - diff --git a/Frameworks/RSCore/RSCore/NSTableView+Extensions.swift b/Frameworks/RSCore/RSCore/NSTableView+Extensions.swift index a32325699..6a0e3d520 100755 --- a/Frameworks/RSCore/RSCore/NSTableView+Extensions.swift +++ b/Frameworks/RSCore/RSCore/NSTableView+Extensions.swift @@ -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 + } }