2017-05-27 10:43:27 -07:00
|
|
|
|
//
|
|
|
|
|
// SidebarViewController.swift
|
2018-08-28 22:18:24 -07:00
|
|
|
|
// NetNewsWire
|
2017-05-27 10:43:27 -07:00
|
|
|
|
//
|
|
|
|
|
// Created by Brent Simmons on 7/26/15.
|
|
|
|
|
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
2018-01-28 16:09:18 -08:00
|
|
|
|
import AppKit
|
2017-05-27 10:43:27 -07:00
|
|
|
|
import RSTree
|
2018-07-23 18:29:08 -07:00
|
|
|
|
import Articles
|
2017-09-17 12:34:10 -07:00
|
|
|
|
import Account
|
2017-11-04 14:53:21 -07:00
|
|
|
|
import RSCore
|
2017-05-27 10:43:27 -07:00
|
|
|
|
|
2019-02-17 18:46:28 -08:00
|
|
|
|
protocol SidebarDelegate: class {
|
2019-02-17 21:43:51 -08:00
|
|
|
|
func sidebarSelectionDidChange(_: SidebarViewController, selectedObjects: [AnyObject]?)
|
2019-07-27 22:53:27 -07:00
|
|
|
|
func unreadCount(for: AnyObject) -> Int
|
2020-03-03 10:54:37 -08:00
|
|
|
|
func sidebarInvalidatedRestorationState(_: SidebarViewController)
|
2019-02-17 18:46:28 -08:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-03 17:10:03 -08:00
|
|
|
|
@objc class SidebarViewController: NSViewController, NSOutlineViewDelegate, NSMenuDelegate, UndoableCommandRunner {
|
2017-05-27 10:43:27 -07:00
|
|
|
|
|
2017-10-21 12:14:45 -07:00
|
|
|
|
@IBOutlet var outlineView: SidebarOutlineView!
|
2019-02-17 18:46:28 -08:00
|
|
|
|
|
|
|
|
|
weak var delegate: SidebarDelegate?
|
|
|
|
|
|
2020-02-29 14:15:37 -08:00
|
|
|
|
private let rebuildTreeAndRestoreSelectionQueue = CoalescingQueue(name: "Rebuild Tree Queue", interval: 0.5)
|
2019-11-14 20:11:41 -06:00
|
|
|
|
let treeControllerDelegate = WebFeedTreeControllerDelegate()
|
2017-10-19 13:27:59 -07:00
|
|
|
|
lazy var treeController: TreeController = {
|
2018-02-12 22:02:51 -08:00
|
|
|
|
return TreeController(delegate: treeControllerDelegate)
|
2017-10-19 13:27:59 -07:00
|
|
|
|
}()
|
2018-02-12 22:02:51 -08:00
|
|
|
|
lazy var dataSource: SidebarOutlineDataSource = {
|
|
|
|
|
return SidebarOutlineDataSource(treeController: treeController)
|
|
|
|
|
}()
|
2020-03-02 17:46:31 -08:00
|
|
|
|
|
2019-11-22 10:55:54 -06:00
|
|
|
|
var isReadFiltered: Bool {
|
2020-03-02 17:46:31 -08:00
|
|
|
|
get {
|
|
|
|
|
return treeControllerDelegate.isReadFiltered
|
|
|
|
|
}
|
|
|
|
|
set {
|
|
|
|
|
treeControllerDelegate.isReadFiltered = newValue
|
|
|
|
|
}
|
2019-11-22 10:55:54 -06:00
|
|
|
|
}
|
2020-03-03 17:10:03 -08:00
|
|
|
|
var expandedTable = Set<ContainerIdentifier>()
|
2018-02-12 22:02:51 -08:00
|
|
|
|
|
2017-11-04 22:51:14 -07:00
|
|
|
|
var undoableCommands = [UndoableCommand]()
|
2017-11-05 12:14:36 -08:00
|
|
|
|
private var animatingChanges = false
|
2017-11-24 21:39:59 -08:00
|
|
|
|
private var sidebarCellAppearance: SidebarCellAppearance!
|
2017-10-05 13:15:32 -07:00
|
|
|
|
|
2018-02-04 11:19:24 -08:00
|
|
|
|
var renameWindowController: RenameWindowController?
|
|
|
|
|
|
2018-01-21 11:35:50 -08:00
|
|
|
|
var selectedObjects: [AnyObject] {
|
|
|
|
|
return selectedNodes.representedObjects()
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-17 21:08:36 -08:00
|
|
|
|
// MARK: - NSViewController
|
2017-05-27 10:43:27 -07:00
|
|
|
|
|
|
|
|
|
override func viewDidLoad() {
|
2019-04-10 22:23:31 -07:00
|
|
|
|
sidebarCellAppearance = SidebarCellAppearance(fontSize: AppDefaults.sidebarFontSize)
|
2017-11-24 21:39:59 -08:00
|
|
|
|
|
2018-02-12 22:02:51 -08:00
|
|
|
|
outlineView.dataSource = dataSource
|
2019-10-05 11:35:33 +10:00
|
|
|
|
outlineView.doubleAction = #selector(doubleClickedSidebar(_:))
|
2019-05-27 18:01:24 -05:00
|
|
|
|
outlineView.setDraggingSourceOperationMask([.move, .copy], forLocal: true)
|
2019-11-14 20:11:41 -06:00
|
|
|
|
outlineView.registerForDraggedTypes([WebFeedPasteboardWriter.webFeedUTIInternalType, WebFeedPasteboardWriter.webFeedUTIType, .URL, .string])
|
2017-11-07 21:14:58 -08:00
|
|
|
|
|
2020-03-04 15:40:40 -07:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidInitialize(_:)), name: .UnreadCountDidInitialize, object: nil)
|
2017-05-27 10:43:27 -07:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
2017-10-19 13:27:59 -07:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(containerChildrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
|
2019-09-08 09:43:51 -05:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .UserDidAddAccount, object: nil)
|
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .UserDidDeleteAccount, object: nil)
|
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(accountStateDidChange(_:)), name: .AccountStateDidChange, object: nil)
|
2017-10-21 15:56:01 -07:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(userDidAddFeed(_:)), name: .UserDidAddFeed, object: nil)
|
2017-11-15 13:26:10 -08:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(batchUpdateDidPerform(_:)), name: .BatchUpdateDidPerform, object: nil)
|
2017-11-25 11:14:42 -08:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
2019-11-14 20:11:41 -06:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(webFeedSettingDidChange(_:)), name: .WebFeedSettingDidChange, object: nil)
|
2018-01-23 21:07:29 -08:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
|
2019-12-10 18:17:54 -07:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(downloadArticlesDidUpdateUnreadCounts(_:)), name: .DownloadArticlesDidUpdateUnreadCounts, object: nil)
|
2017-05-27 10:43:27 -07:00
|
|
|
|
|
|
|
|
|
outlineView.reloadData()
|
2017-11-18 17:10:47 -08:00
|
|
|
|
|
2020-03-03 17:10:03 -08:00
|
|
|
|
// Expand top level items by default. If there is state to restore, overlay this.
|
|
|
|
|
for topLevelNode in treeController.rootNode.childNodes {
|
|
|
|
|
if let containerID = (topLevelNode.representedObject as? ContainerIdentifiable)?.containerID {
|
|
|
|
|
expandedTable.insert(containerID)
|
2017-11-18 17:10:47 -08:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-03-03 17:10:03 -08:00
|
|
|
|
expandNodes()
|
|
|
|
|
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-02 17:55:36 -08:00
|
|
|
|
// MARK: State Restoration
|
|
|
|
|
|
2020-03-03 15:36:22 -08:00
|
|
|
|
func saveState(to coder: NSCoder) {
|
2020-03-02 17:55:36 -08:00
|
|
|
|
coder.encode(isReadFiltered, forKey: UserInfoKey.readFeedsFilterState)
|
2020-03-03 17:10:03 -08:00
|
|
|
|
coder.encode(expandedTable.map( { $0.userInfo }), forKey: UserInfoKey.containerExpandedWindowState)
|
2020-03-04 15:40:40 -07:00
|
|
|
|
coder.encode(selectedFeeds.compactMap({ $0.feedID?.userInfo }), forKey: UserInfoKey.selectedFeedsState)
|
2020-03-02 17:55:36 -08:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-03 15:36:22 -08:00
|
|
|
|
func restoreState(from coder: NSCoder) {
|
2020-03-03 17:10:03 -08:00
|
|
|
|
|
|
|
|
|
if let containerExpandedWindowState = try? coder.decodeTopLevelObject(forKey: UserInfoKey.containerExpandedWindowState) as? [[AnyHashable: AnyHashable]] {
|
|
|
|
|
let containerIdentifers = containerExpandedWindowState.compactMap( { ContainerIdentifier(userInfo: $0) })
|
|
|
|
|
expandedTable = Set(containerIdentifers)
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-04 15:40:40 -07:00
|
|
|
|
guard let selectedFeedsState = try? coder.decodeTopLevelObject(forKey: UserInfoKey.selectedFeedsState) as? [[AnyHashable: AnyHashable]] else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let selectedFeedIdentifers = Set(selectedFeedsState.compactMap( { FeedIdentifier(userInfo: $0) }))
|
|
|
|
|
selectedFeedIdentifers.forEach { treeControllerDelegate.addFilterException($0) }
|
|
|
|
|
|
|
|
|
|
rebuildTreeAndReloadDataIfNeeded()
|
|
|
|
|
|
|
|
|
|
var selectIndexes = IndexSet()
|
|
|
|
|
|
|
|
|
|
func selectFeedsVisitor(node: Node) {
|
|
|
|
|
if let feedID = (node.representedObject as? FeedIdentifiable)?.feedID {
|
|
|
|
|
if selectedFeedIdentifers.contains(feedID) {
|
|
|
|
|
selectIndexes.insert(outlineView.row(forItem: node) )
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
treeController.visitNodes(selectFeedsVisitor(node:))
|
|
|
|
|
outlineView.selectRowIndexes(selectIndexes, byExtendingSelection: false)
|
2020-03-04 18:16:58 -07:00
|
|
|
|
focus()
|
2020-03-04 15:40:40 -07:00
|
|
|
|
|
|
|
|
|
isReadFiltered = coder.decodeBool(forKey: UserInfoKey.readFeedsFilterState)
|
2020-03-02 17:55:36 -08:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-17 21:08:36 -08:00
|
|
|
|
// MARK: - Notifications
|
2017-05-27 10:43:27 -07:00
|
|
|
|
|
2020-03-04 15:40:40 -07:00
|
|
|
|
@objc func unreadCountDidInitialize(_ notification: Notification) {
|
|
|
|
|
guard notification.object is AccountManager else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if isReadFiltered {
|
|
|
|
|
rebuildTreeAndRestoreSelection()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-17 21:08:36 -08:00
|
|
|
|
@objc func unreadCountDidChange(_ note: Notification) {
|
2017-11-04 23:05:20 -07:00
|
|
|
|
guard let representedObject = note.object else {
|
2017-05-27 10:43:27 -07:00
|
|
|
|
return
|
|
|
|
|
}
|
2020-02-29 14:15:37 -08:00
|
|
|
|
|
2019-08-13 21:07:39 -07:00
|
|
|
|
if let timelineViewController = representedObject as? TimelineViewController {
|
|
|
|
|
configureUnreadCountForCellsForRepresentedObjects(timelineViewController.representedObjects)
|
2020-02-29 14:15:37 -08:00
|
|
|
|
} else {
|
2019-08-13 21:07:39 -07:00
|
|
|
|
configureUnreadCountForCellsForRepresentedObjects([representedObject as AnyObject])
|
|
|
|
|
}
|
2020-02-29 14:15:37 -08:00
|
|
|
|
|
2020-03-04 15:40:40 -07:00
|
|
|
|
guard AccountManager.shared.isUnreadCountsInitialized else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-29 15:10:41 -08:00
|
|
|
|
if let feed = representedObject as? Feed, isReadFiltered, feed.unreadCount > 0 {
|
|
|
|
|
addTreeControllerToFilterExceptions()
|
2020-02-29 14:15:37 -08:00
|
|
|
|
queueRebuildTreeAndRestoreSelection()
|
|
|
|
|
}
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-17 21:08:36 -08:00
|
|
|
|
@objc func containerChildrenDidChange(_ note: Notification) {
|
2019-02-05 21:00:53 -08:00
|
|
|
|
rebuildTreeAndRestoreSelection()
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-08 09:43:51 -05:00
|
|
|
|
@objc func accountsDidChange(_ notification: Notification) {
|
2019-05-01 12:37:13 -05:00
|
|
|
|
rebuildTreeAndRestoreSelection()
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-08 09:43:51 -05:00
|
|
|
|
@objc func accountStateDidChange(_ notification: Notification) {
|
2019-05-02 06:01:30 -05:00
|
|
|
|
rebuildTreeAndRestoreSelection()
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-17 21:08:36 -08:00
|
|
|
|
@objc func batchUpdateDidPerform(_ notification: Notification) {
|
2019-02-05 21:00:53 -08:00
|
|
|
|
rebuildTreeAndRestoreSelection()
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-17 21:08:36 -08:00
|
|
|
|
@objc func userDidAddFeed(_ notification: Notification) {
|
2019-11-14 20:11:41 -06:00
|
|
|
|
guard let feed = notification.userInfo?[UserInfoKey.webFeed] else {
|
2017-05-27 10:43:27 -07:00
|
|
|
|
return
|
|
|
|
|
}
|
2017-12-18 12:34:07 -08:00
|
|
|
|
revealAndSelectRepresentedObject(feed as AnyObject)
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-11-25 11:14:42 -08:00
|
|
|
|
@objc func faviconDidBecomeAvailable(_ note: Notification) {
|
2017-12-13 21:16:52 -08:00
|
|
|
|
applyToAvailableCells(configureFavicon)
|
2017-11-25 11:14:42 -08:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-14 20:11:41 -06:00
|
|
|
|
@objc func webFeedSettingDidChange(_ note: Notification) {
|
|
|
|
|
guard let webFeed = note.object as? WebFeed, let key = note.userInfo?[WebFeed.WebFeedSettingUserInfoKey] as? String else {
|
2017-11-25 13:48:14 -08:00
|
|
|
|
return
|
|
|
|
|
}
|
2019-11-14 20:11:41 -06:00
|
|
|
|
if key == WebFeed.WebFeedSettingKey.homePageURL || key == WebFeed.WebFeedSettingKey.faviconURL {
|
|
|
|
|
configureCellsForRepresentedObject(webFeed)
|
2019-03-17 13:54:30 -07:00
|
|
|
|
}
|
2017-11-25 13:48:14 -08:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-23 21:07:29 -08:00
|
|
|
|
@objc func displayNameDidChange(_ note: Notification) {
|
|
|
|
|
guard let object = note.object else {
|
|
|
|
|
return
|
|
|
|
|
}
|
2019-02-02 17:01:40 -08:00
|
|
|
|
let savedSelection = selectedNodes
|
2019-02-02 12:36:07 -08:00
|
|
|
|
rebuildTreeAndReloadDataIfNeeded()
|
2018-01-23 21:07:29 -08:00
|
|
|
|
configureCellsForRepresentedObject(object as AnyObject)
|
2019-02-02 17:01:40 -08:00
|
|
|
|
restoreSelection(to: savedSelection, sendNotificationIfChanged: true)
|
2018-01-23 21:07:29 -08:00
|
|
|
|
}
|
|
|
|
|
|
2018-09-25 19:20:43 -05:00
|
|
|
|
@objc func userDidRequestSidebarSelection(_ note: Notification) {
|
2019-11-14 20:11:41 -06:00
|
|
|
|
guard let feed = note.userInfo?[UserInfoKey.webFeed] else {
|
2018-09-25 19:20:43 -05:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
revealAndSelectRepresentedObject(feed as AnyObject)
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-10 18:17:54 -07:00
|
|
|
|
@objc func downloadArticlesDidUpdateUnreadCounts(_ note: Notification) {
|
|
|
|
|
rebuildTreeAndRestoreSelection()
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-17 21:08:36 -08:00
|
|
|
|
// MARK: - Actions
|
2017-05-27 10:43:27 -07:00
|
|
|
|
|
|
|
|
|
@IBAction func delete(_ sender: AnyObject?) {
|
|
|
|
|
if outlineView.selectionIsEmpty {
|
|
|
|
|
return
|
|
|
|
|
}
|
2018-09-25 21:10:54 -05:00
|
|
|
|
deleteNodes(selectedNodes)
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
2019-10-05 11:35:33 +10:00
|
|
|
|
|
|
|
|
|
@IBAction func doubleClickedSidebar(_ sender: Any?) {
|
|
|
|
|
guard outlineView.clickedRow == outlineView.selectedRow else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
openInBrowser(sender)
|
|
|
|
|
}
|
2017-05-27 10:43:27 -07:00
|
|
|
|
|
2017-12-20 12:59:31 -08:00
|
|
|
|
@IBAction func openInBrowser(_ sender: Any?) {
|
2020-02-29 15:10:41 -08:00
|
|
|
|
guard let feed = singleSelectedWebFeed, let homePageURL = feed.homePageURL else {
|
2017-12-20 12:59:31 -08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
Browser.open(homePageURL)
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-27 12:39:07 -08:00
|
|
|
|
@IBAction func gotoToday(_ sender: Any?) {
|
2020-02-29 16:30:13 -08:00
|
|
|
|
selectFeed(SmartFeedsController.shared.todayFeed)
|
2020-02-27 16:50:35 -08:00
|
|
|
|
focus()
|
2018-01-27 12:39:07 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@IBAction func gotoAllUnread(_ sender: Any?) {
|
2020-02-29 16:30:13 -08:00
|
|
|
|
selectFeed(SmartFeedsController.shared.unreadFeed)
|
2020-02-27 16:50:35 -08:00
|
|
|
|
focus()
|
2018-01-27 12:39:07 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@IBAction func gotoStarred(_ sender: Any?) {
|
2020-02-29 16:30:13 -08:00
|
|
|
|
selectFeed(SmartFeedsController.shared.starredFeed)
|
2020-02-27 16:50:35 -08:00
|
|
|
|
focus()
|
2018-01-27 12:39:07 -08:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-11 18:58:50 -08:00
|
|
|
|
@IBAction func copy(_ sender: Any?) {
|
2018-02-11 22:10:28 -08:00
|
|
|
|
NSPasteboard.general.copyObjects(selectedObjects)
|
2018-02-11 18:58:50 -08:00
|
|
|
|
}
|
2018-02-11 22:10:28 -08:00
|
|
|
|
|
2018-02-17 21:08:36 -08:00
|
|
|
|
// MARK: - Navigation
|
2017-05-27 10:43:27 -07:00
|
|
|
|
|
|
|
|
|
func canGoToNextUnread() -> Bool {
|
2018-02-15 17:50:31 -08:00
|
|
|
|
if let _ = nextSelectableRowWithUnreadArticle() {
|
2017-05-27 10:43:27 -07:00
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func goToNextUnread() {
|
2018-02-15 17:50:31 -08:00
|
|
|
|
guard let row = nextSelectableRowWithUnreadArticle() else {
|
2017-05-27 10:43:27 -07:00
|
|
|
|
assertionFailure("goToNextUnread called before checking if there is a next unread.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-04 21:18:59 -07:00
|
|
|
|
NSCursor.setHiddenUntilMouseMoves(true)
|
2017-05-27 10:43:27 -07:00
|
|
|
|
outlineView.selectRowIndexes(IndexSet([row]), byExtendingSelection: false)
|
2018-09-13 15:36:07 -05:00
|
|
|
|
outlineView.scrollTo(row: row)
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
2017-12-19 15:24:38 -08:00
|
|
|
|
|
|
|
|
|
func focus() {
|
2019-07-27 19:49:33 -07:00
|
|
|
|
outlineView.window?.makeFirstResponderUnlessDescendantIsFirstResponder(outlineView)
|
2017-12-19 15:24:38 -08:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-17 21:08:36 -08:00
|
|
|
|
// MARK: - Contextual Menu
|
2018-01-28 16:09:18 -08:00
|
|
|
|
|
|
|
|
|
func contextualMenuForSelectedObjects() -> NSMenu? {
|
2018-02-04 11:19:24 -08:00
|
|
|
|
return menu(for: selectedObjects)
|
2018-01-28 16:09:18 -08:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-07 13:25:17 -08:00
|
|
|
|
func contextualMenuForClickedRows() -> NSMenu? {
|
|
|
|
|
let row = outlineView.clickedRow
|
|
|
|
|
guard row != -1, let node = nodeForRow(row) else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-07 21:23:18 -08:00
|
|
|
|
if outlineView.selectedRowIndexes.contains(row) {
|
|
|
|
|
// If the clickedRow is part of the selected rows, then do a contextual menu for all the selected rows.
|
|
|
|
|
return contextualMenuForSelectedObjects()
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-07 13:25:17 -08:00
|
|
|
|
let object = node.representedObject
|
|
|
|
|
return menu(for: [object])
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-27 19:49:33 -07:00
|
|
|
|
// MARK: - NSMenuDelegate
|
2019-02-10 12:27:22 -08:00
|
|
|
|
|
|
|
|
|
public func menuNeedsUpdate(_ menu: NSMenu) {
|
|
|
|
|
menu.removeAllItems()
|
|
|
|
|
guard let contextualMenu = contextualMenuForClickedRows() else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
menu.takeItems(from: contextualMenu)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2018-02-17 21:08:36 -08:00
|
|
|
|
// MARK: - NSOutlineViewDelegate
|
2017-05-27 10:43:27 -07:00
|
|
|
|
|
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
|
|
|
|
|
let node = item as! Node
|
2017-11-18 16:56:36 -08:00
|
|
|
|
|
|
|
|
|
if node.isGroupItem {
|
|
|
|
|
let cell = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "HeaderCell"), owner: self) as! NSTableCellView
|
|
|
|
|
configureGroupCell(cell, node)
|
|
|
|
|
return cell
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let cell = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "DataCell"), owner: self) as! SidebarCell
|
2017-05-27 10:43:27 -07:00
|
|
|
|
configure(cell, node)
|
|
|
|
|
|
|
|
|
|
return cell
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-18 16:56:36 -08:00
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, isGroupItem item: Any) -> Bool {
|
|
|
|
|
let node = item as! Node
|
|
|
|
|
return node.isGroupItem
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-19 21:01:16 -08:00
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, selectionIndexesForProposedSelection proposedSelectionIndexes: IndexSet) -> IndexSet {
|
|
|
|
|
// Don’t allow selecting group items.
|
|
|
|
|
// If any index in IndexSet contains a group item,
|
|
|
|
|
// return the current selection (not a modified version of the proposed selection).
|
|
|
|
|
|
|
|
|
|
for index in proposedSelectionIndexes {
|
|
|
|
|
if let node = nodeForRow(index), node.isGroupItem {
|
|
|
|
|
return outlineView.selectedRowIndexes
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return proposedSelectionIndexes
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-21 14:43:29 -08:00
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, shouldSelectItem item: Any) -> Bool {
|
|
|
|
|
return !self.outlineView(outlineView, isGroupItem: item)
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-27 10:43:27 -07:00
|
|
|
|
func outlineViewSelectionDidChange(_ notification: Notification) {
|
2019-02-17 18:52:34 -08:00
|
|
|
|
selectionDidChange(selectedObjects.isEmpty ? nil : selectedObjects)
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
2020-03-03 17:10:03 -08:00
|
|
|
|
|
|
|
|
|
func outlineViewItemDidExpand(_ notification: Notification) {
|
|
|
|
|
guard let node = notification.userInfo?["NSObject"] as? Node,
|
|
|
|
|
let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !expandedTable.contains(containerID) {
|
|
|
|
|
expandedTable.insert(containerID)
|
|
|
|
|
delegate?.sidebarInvalidatedRestorationState(self)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func outlineViewItemDidCollapse(_ notification: Notification) {
|
|
|
|
|
guard let node = notification.userInfo?["NSObject"] as? Node,
|
|
|
|
|
let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if expandedTable.contains(containerID) {
|
|
|
|
|
expandedTable.remove(containerID)
|
|
|
|
|
delegate?.sidebarInvalidatedRestorationState(self)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-25 21:10:54 -05:00
|
|
|
|
//MARK: - Node Manipulation
|
|
|
|
|
|
|
|
|
|
func deleteNodes(_ nodes: [Node]) {
|
|
|
|
|
let nodesToDelete = treeController.normalizedSelectedNodes(nodes)
|
|
|
|
|
|
2020-03-01 16:32:31 -08:00
|
|
|
|
guard let undoManager = undoManager, let deleteCommand = DeleteCommand(nodesToDelete: nodesToDelete, treeController: treeController, undoManager: undoManager, errorHandler: ErrorHandler.present) else {
|
2018-09-25 21:10:54 -05:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
animatingChanges = true
|
|
|
|
|
outlineView.beginUpdates()
|
|
|
|
|
|
|
|
|
|
let indexSetsGroupedByParent = Node.indexSetsGroupedByParent(nodesToDelete)
|
|
|
|
|
for (parent, indexSet) in indexSetsGroupedByParent {
|
|
|
|
|
outlineView.removeItems(at: indexSet, inParent: parent.isRoot ? nil : parent, withAnimation: [.slideDown])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
outlineView.endUpdates()
|
|
|
|
|
|
|
|
|
|
runCommand(deleteCommand)
|
|
|
|
|
animatingChanges = false
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-05 21:00:53 -08:00
|
|
|
|
// MARK: - API
|
2020-02-29 15:50:13 -08:00
|
|
|
|
|
2020-02-29 16:24:14 -08:00
|
|
|
|
func selectFeed(_ feed: Feed) {
|
|
|
|
|
if isReadFiltered, let feedID = feed.feedID {
|
|
|
|
|
self.treeControllerDelegate.addFilterException(feedID)
|
|
|
|
|
|
|
|
|
|
if let webFeed = feed as? WebFeed, let account = webFeed.account {
|
|
|
|
|
let parentFolder = account.sortedFolders?.first(where: { $0.objectIsChild(webFeed) })
|
|
|
|
|
if let parentFolderFeedID = parentFolder?.feedID {
|
|
|
|
|
self.treeControllerDelegate.addFilterException(parentFolderFeedID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-29 16:30:13 -08:00
|
|
|
|
addTreeControllerToFilterExceptions()
|
2020-02-29 16:24:14 -08:00
|
|
|
|
rebuildTreeAndRestoreSelection()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
revealAndSelectRepresentedObject(feed as AnyObject)
|
2020-02-29 15:50:13 -08:00
|
|
|
|
}
|
2019-02-05 21:00:53 -08:00
|
|
|
|
|
2019-10-03 11:39:48 -05:00
|
|
|
|
func deepLinkRevealAndSelect(for userInfo: [AnyHashable : Any]) {
|
2020-02-29 16:24:14 -08:00
|
|
|
|
guard let accountNode = findAccountNode(userInfo),
|
|
|
|
|
let feedNode = findFeedNode(userInfo, beginningAt: accountNode),
|
|
|
|
|
let feed = feedNode.representedObject as? Feed else {
|
2019-10-03 11:39:48 -05:00
|
|
|
|
return
|
|
|
|
|
}
|
2020-02-29 16:24:14 -08:00
|
|
|
|
selectFeed(feed)
|
2019-10-03 11:39:48 -05:00
|
|
|
|
}
|
2019-11-22 10:55:54 -06:00
|
|
|
|
|
|
|
|
|
func toggleReadFilter() {
|
|
|
|
|
if treeControllerDelegate.isReadFiltered {
|
2020-03-02 17:46:31 -08:00
|
|
|
|
isReadFiltered = false
|
2019-11-22 10:55:54 -06:00
|
|
|
|
} else {
|
2020-03-02 17:46:31 -08:00
|
|
|
|
isReadFiltered = true
|
2019-11-22 10:55:54 -06:00
|
|
|
|
}
|
2020-03-03 17:10:03 -08:00
|
|
|
|
delegate?.sidebarInvalidatedRestorationState(self)
|
2020-03-03 10:54:37 -08:00
|
|
|
|
rebuildTreeAndRestoreSelection()
|
2019-11-22 10:55:54 -06:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-12 22:02:51 -08:00
|
|
|
|
// MARK: - NSUserInterfaceValidations
|
2018-02-11 22:10:28 -08:00
|
|
|
|
|
|
|
|
|
extension SidebarViewController: NSUserInterfaceValidations {
|
|
|
|
|
|
|
|
|
|
func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
|
|
|
|
if item.action == #selector(copy(_:)) {
|
|
|
|
|
return NSPasteboard.general.canCopyAtLeastOneObject(selectedObjects)
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-27 10:43:27 -07:00
|
|
|
|
//MARK: - Private
|
|
|
|
|
|
|
|
|
|
private extension SidebarViewController {
|
|
|
|
|
|
2019-05-19 09:10:19 -05:00
|
|
|
|
var accountNodes: [Account] {
|
|
|
|
|
return treeController.rootNode.childNodes.compactMap { $0.representedObject as? Account }
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-27 10:43:27 -07:00
|
|
|
|
var selectedNodes: [Node] {
|
2017-12-20 12:59:31 -08:00
|
|
|
|
if let nodes = outlineView.selectedItems as? [Node] {
|
|
|
|
|
return nodes
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
2017-12-20 12:59:31 -08:00
|
|
|
|
return [Node]()
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
2020-02-29 15:10:41 -08:00
|
|
|
|
|
|
|
|
|
var selectedFeeds: [Feed] {
|
|
|
|
|
selectedNodes.compactMap { $0.representedObject as? Feed }
|
|
|
|
|
}
|
2017-12-20 12:59:31 -08:00
|
|
|
|
|
|
|
|
|
var singleSelectedNode: Node? {
|
|
|
|
|
guard selectedNodes.count == 1 else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return selectedNodes.first!
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-29 15:10:41 -08:00
|
|
|
|
var singleSelectedWebFeed: WebFeed? {
|
2017-12-20 12:59:31 -08:00
|
|
|
|
guard let node = singleSelectedNode else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2019-11-14 20:11:41 -06:00
|
|
|
|
return node.representedObject as? WebFeed
|
2017-12-20 12:59:31 -08:00
|
|
|
|
}
|
2020-02-29 15:10:41 -08:00
|
|
|
|
|
|
|
|
|
func addAllSelectedToFilterExceptions() {
|
|
|
|
|
selectedFeeds.forEach { addToFilterExeptionsIfNecessary($0) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func addToFilterExeptionsIfNecessary(_ feed: Feed?) {
|
|
|
|
|
if isReadFiltered, let feedID = feed?.feedID {
|
|
|
|
|
if feed is SmartFeed {
|
|
|
|
|
treeControllerDelegate.addFilterException(feedID)
|
|
|
|
|
} else if let folderFeed = feed as? Folder {
|
|
|
|
|
if folderFeed.account?.existingFolder(withID: folderFeed.folderID) != nil {
|
|
|
|
|
treeControllerDelegate.addFilterException(feedID)
|
|
|
|
|
}
|
|
|
|
|
} else if let webFeed = feed as? WebFeed {
|
|
|
|
|
if webFeed.account?.existingWebFeed(withWebFeedID: webFeed.webFeedID) != nil {
|
|
|
|
|
treeControllerDelegate.addFilterException(feedID)
|
|
|
|
|
addParentFolderToFilterExceptions(webFeed)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func addParentFolderToFilterExceptions(_ feed: Feed) {
|
|
|
|
|
guard let node = treeController.rootNode.descendantNodeRepresentingObject(feed as AnyObject),
|
|
|
|
|
let folder = node.parent?.representedObject as? Folder,
|
|
|
|
|
let folderFeedID = folder.feedID else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
treeControllerDelegate.addFilterException(folderFeedID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func queueRebuildTreeAndRestoreSelection() {
|
|
|
|
|
rebuildTreeAndRestoreSelectionQueue.add(self, #selector(rebuildTreeAndRestoreSelection))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func rebuildTreeAndRestoreSelection() {
|
|
|
|
|
let savedAccounts = accountNodes
|
|
|
|
|
let savedSelection = selectedNodes
|
|
|
|
|
|
|
|
|
|
rebuildTreeAndReloadDataIfNeeded()
|
|
|
|
|
restoreSelection(to: savedSelection, sendNotificationIfChanged: true)
|
|
|
|
|
|
|
|
|
|
// Automatically expand any new or newly active accounts
|
|
|
|
|
AccountManager.shared.activeAccounts.forEach { account in
|
|
|
|
|
if !savedAccounts.contains(account) {
|
|
|
|
|
let accountNode = treeController.nodeInTreeRepresentingObject(account)
|
|
|
|
|
outlineView.expandItem(accountNode)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-27 10:43:27 -07:00
|
|
|
|
func rebuildTreeAndReloadDataIfNeeded() {
|
2017-11-05 12:14:36 -08:00
|
|
|
|
if !animatingChanges && !BatchUpdate.shared.isPerforming {
|
2020-02-29 15:10:41 -08:00
|
|
|
|
addAllSelectedToFilterExceptions()
|
2019-08-23 15:52:07 -07:00
|
|
|
|
treeController.rebuild()
|
2020-02-29 15:10:41 -08:00
|
|
|
|
treeControllerDelegate.resetFilterExceptions()
|
2019-08-23 15:52:07 -07:00
|
|
|
|
outlineView.reloadData()
|
2020-03-03 17:10:03 -08:00
|
|
|
|
expandNodes()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func expandNodes() {
|
|
|
|
|
treeController.visitNodes(expandNodesVisitor(node:))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func expandNodesVisitor(node: Node) {
|
|
|
|
|
if let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID {
|
|
|
|
|
if expandedTable.contains(containerID) {
|
|
|
|
|
outlineView.expandItem(node)
|
|
|
|
|
} else {
|
|
|
|
|
outlineView.collapseItem(node)
|
|
|
|
|
}
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-02-29 15:10:41 -08:00
|
|
|
|
|
|
|
|
|
func addTreeControllerToFilterExceptions() {
|
|
|
|
|
treeController.visitNodes(addTreeControllerToFilterExceptionsVisitor(node:))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func addTreeControllerToFilterExceptionsVisitor(node: Node) {
|
|
|
|
|
if let feed = node.representedObject as? Feed, let feedID = feed.feedID {
|
|
|
|
|
treeControllerDelegate.addFilterException(feedID)
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-02-02 17:01:40 -08:00
|
|
|
|
|
|
|
|
|
func restoreSelection(to nodes: [Node], sendNotificationIfChanged: Bool) {
|
|
|
|
|
if selectedNodes == nodes { // Nothing to do?
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var indexes = IndexSet()
|
|
|
|
|
for node in nodes {
|
|
|
|
|
let row = outlineView.row(forItem: node as Any)
|
|
|
|
|
if row > -1 {
|
|
|
|
|
indexes.insert(row)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
outlineView.selectRowIndexes(indexes, byExtendingSelection: false)
|
|
|
|
|
|
|
|
|
|
if selectedNodes != nodes && sendNotificationIfChanged {
|
2019-02-17 18:52:34 -08:00
|
|
|
|
selectionDidChange(selectedObjects)
|
2019-02-02 17:01:40 -08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-17 18:52:34 -08:00
|
|
|
|
func selectionDidChange(_ selectedObjects: [AnyObject]?) {
|
2019-02-17 21:43:51 -08:00
|
|
|
|
delegate?.sidebarSelectionDidChange(self, selectedObjects: selectedObjects)
|
2020-03-04 15:40:40 -07:00
|
|
|
|
delegate?.sidebarInvalidatedRestorationState(self)
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-11-04 23:05:20 -07:00
|
|
|
|
func updateUnreadCounts(for objects: [AnyObject]) {
|
2017-10-18 21:53:45 -07:00
|
|
|
|
// On selection, update unread counts for folders and feeds.
|
|
|
|
|
// For feeds, actually fetch from database.
|
|
|
|
|
|
|
|
|
|
for object in objects {
|
2019-11-14 20:11:41 -06:00
|
|
|
|
if let feed = object as? WebFeed, let account = feed.account {
|
2017-10-18 21:53:45 -07:00
|
|
|
|
account.updateUnreadCounts(for: Set([feed]))
|
|
|
|
|
}
|
2017-10-21 16:37:40 -07:00
|
|
|
|
else if let folder = object as? Folder, let account = folder.account {
|
2019-11-14 20:11:41 -06:00
|
|
|
|
account.updateUnreadCounts(for: folder.flattenedWebFeeds())
|
2017-10-18 21:53:45 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-27 10:43:27 -07:00
|
|
|
|
func nodeForItem(_ item: AnyObject?) -> Node {
|
|
|
|
|
if item == nil {
|
|
|
|
|
return treeController.rootNode
|
|
|
|
|
}
|
|
|
|
|
return item as! Node
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func nodeForRow(_ row: Int) -> Node? {
|
|
|
|
|
if row < 0 || row >= outlineView.numberOfRows {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let node = outlineView.item(atRow: row) as? Node {
|
|
|
|
|
return node
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func rowHasAtLeastOneUnreadArticle(_ row: Int) -> Bool {
|
|
|
|
|
if let oneNode = nodeForRow(row) {
|
|
|
|
|
if let unreadCountProvider = oneNode.representedObject as? UnreadCountProvider {
|
|
|
|
|
if unreadCountProvider.unreadCount > 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-15 17:50:31 -08:00
|
|
|
|
func rowIsGroupItem(_ row: Int) -> Bool {
|
|
|
|
|
if let node = nodeForRow(row), outlineView.isGroupItem(node) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func nextSelectableRowWithUnreadArticle() -> Int? {
|
|
|
|
|
// Skip group items, because they should never be selected.
|
|
|
|
|
|
2017-05-27 10:43:27 -07:00
|
|
|
|
let selectedRow = outlineView.selectedRow
|
|
|
|
|
let numberOfRows = outlineView.numberOfRows
|
|
|
|
|
var row = selectedRow + 1
|
|
|
|
|
|
|
|
|
|
while (row < numberOfRows) {
|
2018-02-15 17:50:31 -08:00
|
|
|
|
if rowHasAtLeastOneUnreadArticle(row) && !rowIsGroupItem(row) {
|
2017-05-27 10:43:27 -07:00
|
|
|
|
return row
|
|
|
|
|
}
|
|
|
|
|
row += 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
row = 0
|
|
|
|
|
while (row <= selectedRow) {
|
2018-02-15 17:50:31 -08:00
|
|
|
|
if rowHasAtLeastOneUnreadArticle(row) && !rowIsGroupItem(row) {
|
2017-05-27 10:43:27 -07:00
|
|
|
|
return row
|
|
|
|
|
}
|
|
|
|
|
row += 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-03 11:39:48 -05:00
|
|
|
|
func findAccountNode(_ userInfo: [AnyHashable : Any]?) -> Node? {
|
2019-11-14 15:35:19 -06:00
|
|
|
|
guard let accountID = userInfo?[ArticlePathKey.accountID] as? String else {
|
2019-10-03 11:39:48 -05:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.accountID == accountID }) {
|
|
|
|
|
return node
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-14 15:35:19 -06:00
|
|
|
|
guard let accountName = userInfo?[ArticlePathKey.accountName] as? String else {
|
2019-10-03 11:39:48 -05:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-03 15:49:27 -05:00
|
|
|
|
if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.nameForDisplay == accountName }) {
|
2019-10-03 11:39:48 -05:00
|
|
|
|
return node
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func findFeedNode(_ userInfo: [AnyHashable : Any]?, beginningAt startingNode: Node) -> Node? {
|
2019-11-14 20:11:41 -06:00
|
|
|
|
guard let webFeedID = userInfo?[ArticlePathKey.webFeedID] as? String else {
|
2019-10-03 11:39:48 -05:00
|
|
|
|
return nil
|
|
|
|
|
}
|
2019-11-14 20:11:41 -06:00
|
|
|
|
if let node = startingNode.descendantNode(where: { ($0.representedObject as? WebFeed)?.webFeedID == webFeedID }) {
|
2019-10-03 11:39:48 -05:00
|
|
|
|
return node
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-27 10:43:27 -07:00
|
|
|
|
func configure(_ cell: SidebarCell, _ node: Node) {
|
2017-11-24 21:39:59 -08:00
|
|
|
|
cell.cellAppearance = sidebarCellAppearance
|
2017-05-27 10:43:27 -07:00
|
|
|
|
cell.name = nameFor(node)
|
2017-11-25 16:16:03 -08:00
|
|
|
|
configureUnreadCount(cell, node)
|
2017-12-13 21:16:52 -08:00
|
|
|
|
configureFavicon(cell, node)
|
2017-12-17 10:51:46 -08:00
|
|
|
|
cell.shouldShowImage = node.representedObject is SmallIconProvider
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-11-25 15:21:35 -08:00
|
|
|
|
func configureUnreadCount(_ cell: SidebarCell, _ node: Node) {
|
|
|
|
|
cell.unreadCount = unreadCountFor(node)
|
2017-12-13 21:16:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func configureFavicon(_ cell: SidebarCell, _ node: Node) {
|
2019-11-05 18:05:57 -06:00
|
|
|
|
cell.image = imageFor(node)?.image
|
2017-11-25 15:21:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
2017-11-18 16:56:36 -08:00
|
|
|
|
func configureGroupCell(_ cell: NSTableCellView, _ node: Node) {
|
|
|
|
|
cell.textField?.stringValue = nameFor(node)
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-05 18:05:57 -06:00
|
|
|
|
func imageFor(_ node: Node) -> IconImage? {
|
2017-12-17 10:51:46 -08:00
|
|
|
|
if let smallIconProvider = node.representedObject as? SmallIconProvider {
|
|
|
|
|
return smallIconProvider.smallIcon
|
2017-11-24 21:57:28 -08:00
|
|
|
|
}
|
2017-12-17 10:51:46 -08:00
|
|
|
|
return nil
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func nameFor(_ node: Node) -> String {
|
|
|
|
|
if let displayNameProvider = node.representedObject as? DisplayNameProvider {
|
|
|
|
|
return displayNameProvider.nameForDisplay
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func unreadCountFor(_ node: Node) -> Int {
|
2019-07-27 22:53:27 -07:00
|
|
|
|
// If this node is the one and only selection,
|
|
|
|
|
// then the unread count comes from the timeline.
|
|
|
|
|
// This ensures that any transients in the timeline
|
|
|
|
|
// are accounted for in the unread count.
|
2019-08-13 20:29:04 -07:00
|
|
|
|
if nodeShouldGetUnreadCountFromTimeline(node) {
|
2019-07-27 22:53:27 -07:00
|
|
|
|
return delegate?.unreadCount(for: node.representedObject) ?? 0
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-27 10:43:27 -07:00
|
|
|
|
if let unreadCountProvider = node.representedObject as? UnreadCountProvider {
|
|
|
|
|
return unreadCountProvider.unreadCount
|
|
|
|
|
}
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-13 20:29:04 -07:00
|
|
|
|
func nodeShouldGetUnreadCountFromTimeline(_ node: Node) -> Bool {
|
2019-08-13 21:07:39 -07:00
|
|
|
|
// Only if it’s selected and it’s the only node selected.
|
|
|
|
|
return selectedNodes.count == 1 && selectedNodes.first! === node
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func nodeRepresentsTodayFeed(_ node: Node) -> Bool {
|
2019-08-13 20:29:04 -07:00
|
|
|
|
guard let smartFeed = node.representedObject as? SmartFeed else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2019-08-13 21:07:39 -07:00
|
|
|
|
return smartFeed === SmartFeedsController.shared.todayFeed
|
2019-08-13 20:29:04 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-11-25 11:14:42 -08:00
|
|
|
|
func cellForRowView(_ rowView: NSTableRowView) -> SidebarCell? {
|
|
|
|
|
return rowView.view(atColumn: 0) as? SidebarCell
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-14 18:01:34 -07:00
|
|
|
|
func applyToAvailableCells(_ completion: (SidebarCell, Node) -> Void) {
|
2017-11-25 11:14:42 -08:00
|
|
|
|
outlineView.enumerateAvailableRowViews { (rowView: NSTableRowView, row: Int) -> Void in
|
|
|
|
|
guard let cell = cellForRowView(rowView), let node = nodeForRow(row) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
2019-12-14 18:01:34 -07:00
|
|
|
|
completion(cell, node)
|
2017-11-25 11:14:42 -08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-14 18:01:34 -07:00
|
|
|
|
func applyToCellsForRepresentedObject(_ representedObject: AnyObject, _ completion: (SidebarCell, Node) -> Void) {
|
2017-11-25 16:16:03 -08:00
|
|
|
|
applyToAvailableCells { (cell, node) in
|
2019-05-21 22:23:26 -07:00
|
|
|
|
if node.representsSidebarObject(representedObject) {
|
2019-12-14 18:01:34 -07:00
|
|
|
|
completion(cell, node)
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-25 16:16:03 -08:00
|
|
|
|
func configureCellsForRepresentedObject(_ representedObject: AnyObject) {
|
|
|
|
|
applyToCellsForRepresentedObject(representedObject, configure)
|
2017-05-27 10:43:27 -07:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-13 21:07:39 -07:00
|
|
|
|
func configureUnreadCountForCellsForRepresentedObjects(_ representedObjects: [AnyObject]?) {
|
|
|
|
|
guard let representedObjects = representedObjects else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
for object in representedObjects {
|
|
|
|
|
applyToCellsForRepresentedObject(object, configureUnreadCount)
|
|
|
|
|
}
|
2017-11-25 15:21:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-27 10:43:27 -07:00
|
|
|
|
@discardableResult
|
2017-11-04 23:05:20 -07:00
|
|
|
|
func revealAndSelectRepresentedObject(_ representedObject: AnyObject) -> Bool {
|
2017-05-27 10:43:27 -07:00
|
|
|
|
return outlineView.revealAndSelectRepresentedObject(representedObject, treeController)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-21 22:23:26 -07:00
|
|
|
|
private extension Node {
|
|
|
|
|
|
|
|
|
|
func representsSidebarObject(_ object: AnyObject) -> Bool {
|
|
|
|
|
if representedObject === object {
|
|
|
|
|
return true
|
|
|
|
|
}
|
2019-11-14 20:11:41 -06:00
|
|
|
|
if let feed1 = object as? WebFeed, let feed2 = representedObject as? WebFeed {
|
2019-05-21 22:23:26 -07:00
|
|
|
|
return feed1 == feed2
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|