2017-05-27 19:43:27 +02:00
|
|
|
//
|
|
|
|
// SidebarViewController.swift
|
|
|
|
// Evergreen
|
|
|
|
//
|
|
|
|
// Created by Brent Simmons on 7/26/15.
|
|
|
|
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Cocoa
|
|
|
|
import RSTree
|
2017-09-17 21:22:15 +02:00
|
|
|
import Data
|
2017-09-17 21:34:10 +02:00
|
|
|
import Account
|
2017-11-04 22:53:21 +01:00
|
|
|
import RSCore
|
2017-05-27 19:43:27 +02:00
|
|
|
|
2017-11-05 06:51:14 +01:00
|
|
|
@objc class SidebarViewController: NSViewController, NSOutlineViewDelegate, NSOutlineViewDataSource, UndoableCommandRunner {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
2017-10-21 21:14:45 +02:00
|
|
|
@IBOutlet var outlineView: SidebarOutlineView!
|
2017-10-19 22:27:59 +02:00
|
|
|
let treeControllerDelegate = SidebarTreeControllerDelegate()
|
|
|
|
lazy var treeController: TreeController = {
|
|
|
|
TreeController(delegate: treeControllerDelegate)
|
|
|
|
}()
|
2017-11-05 06:51:14 +01:00
|
|
|
var undoableCommands = [UndoableCommand]()
|
2017-11-05 21:14:36 +01:00
|
|
|
private var animatingChanges = false
|
2017-10-05 22:15:32 +02:00
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
//MARK: NSViewController
|
|
|
|
|
|
|
|
override func viewDidLoad() {
|
|
|
|
|
2017-10-21 21:14:45 +02:00
|
|
|
outlineView.sidebarViewController = self
|
2017-11-08 06:14:58 +01:00
|
|
|
outlineView.setDraggingSourceOperationMask(.move, forLocal: true)
|
|
|
|
outlineView.setDraggingSourceOperationMask(.copy, forLocal: false)
|
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
2017-10-19 22:27:59 +02:00
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(containerChildrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
|
2017-10-22 00:56:01 +02:00
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(userDidAddFeed(_:)), name: .UserDidAddFeed, object: nil)
|
2017-10-07 20:56:22 +02:00
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(batchUpdateDidFinish(_:)), name: .BatchUpdateDidFinish, object: nil)
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
|
|
outlineView.reloadData()
|
|
|
|
}
|
|
|
|
|
|
|
|
//MARK: Notifications
|
|
|
|
|
2017-09-17 21:34:10 +02:00
|
|
|
@objc dynamic func unreadCountDidChange(_ note: Notification) {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
2017-11-05 07:05:20 +01:00
|
|
|
guard let representedObject = note.object else {
|
2017-05-27 19:43:27 +02:00
|
|
|
return
|
|
|
|
}
|
2017-11-05 07:05:20 +01:00
|
|
|
let _ = configureCellsForRepresentedObject(representedObject as AnyObject)
|
2017-05-27 19:43:27 +02:00
|
|
|
}
|
|
|
|
|
2017-10-19 22:27:59 +02:00
|
|
|
@objc dynamic func containerChildrenDidChange(_ note: Notification) {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
|
|
rebuildTreeAndReloadDataIfNeeded()
|
|
|
|
}
|
|
|
|
|
2017-10-07 20:56:22 +02:00
|
|
|
@objc dynamic func batchUpdateDidFinish(_ notification: Notification) {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
|
|
rebuildTreeAndReloadDataIfNeeded()
|
|
|
|
}
|
|
|
|
|
2017-09-17 21:34:10 +02:00
|
|
|
@objc dynamic func userDidAddFeed(_ note: Notification) {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
2017-10-22 00:56:01 +02:00
|
|
|
guard let appInfo = note.appInfo, let feed = appInfo.feed else {
|
2017-05-27 19:43:27 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
revealAndSelectRepresentedObject(feed)
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: Actions
|
|
|
|
|
|
|
|
@IBAction func delete(_ sender: AnyObject?) {
|
|
|
|
|
|
|
|
if outlineView.selectionIsEmpty {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-10-21 21:14:45 +02:00
|
|
|
let nodesToDelete = treeController.normalizedSelectedNodes(selectedNodes)
|
2017-11-05 06:51:14 +01:00
|
|
|
|
|
|
|
guard let undoManager = undoManager, let deleteCommand = DeleteFromSidebarCommand(nodesToDelete: nodesToDelete, undoManager: undoManager) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
let selectedRows = outlineView.selectedRowIndexes
|
|
|
|
|
2017-11-05 21:14:36 +01:00
|
|
|
animatingChanges = true
|
2017-05-27 19:43:27 +02:00
|
|
|
outlineView.beginUpdates()
|
|
|
|
outlineView.removeItems(at: selectedRows, inParent: nil, withAnimation: [.slideDown])
|
|
|
|
outlineView.endUpdates()
|
|
|
|
|
2017-11-05 06:51:14 +01:00
|
|
|
runCommand(deleteCommand)
|
2017-11-05 21:14:36 +01:00
|
|
|
animatingChanges = false
|
2017-05-27 19:43:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: Navigation
|
|
|
|
|
|
|
|
|
|
|
|
func canGoToNextUnread() -> Bool {
|
|
|
|
|
|
|
|
if let _ = rowContainingNextUnread() {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func goToNextUnread() {
|
|
|
|
|
|
|
|
guard let row = rowContainingNextUnread() else {
|
|
|
|
assertionFailure("goToNextUnread called before checking if there is a next unread.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
outlineView.selectRowIndexes(IndexSet([row]), byExtendingSelection: false)
|
|
|
|
|
2017-09-17 21:54:08 +02:00
|
|
|
NSApplication.shared.sendAction(NSSelectorFromString("nextUnread:"), to: nil, from: self)
|
2017-05-27 19:43:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: NSOutlineViewDelegate
|
|
|
|
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
|
|
|
|
|
2017-09-18 02:12:42 +02:00
|
|
|
let cell = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "DataCell"), owner: self) as! SidebarCell
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
|
|
let node = item as! Node
|
|
|
|
configure(cell, node)
|
|
|
|
|
|
|
|
return cell
|
|
|
|
}
|
|
|
|
|
|
|
|
func outlineViewSelectionDidChange(_ notification: Notification) {
|
|
|
|
|
|
|
|
// TODO: support multiple selection
|
|
|
|
|
|
|
|
let selectedRow = self.outlineView.selectedRow
|
|
|
|
|
|
|
|
if selectedRow < 0 || selectedRow == NSNotFound {
|
|
|
|
postSidebarSelectionDidChangeNotification(nil)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if let selectedNode = self.outlineView.item(atRow: selectedRow) as? Node {
|
2017-10-02 09:53:58 +02:00
|
|
|
postSidebarSelectionDidChangeNotification([selectedNode.representedObject])
|
2017-05-27 19:43:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-08 06:14:58 +01:00
|
|
|
// MARK: NSOutlineViewDataSource
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
|
|
|
|
|
|
|
|
return nodeForItem(item as AnyObject?).numberOfChildNodes
|
|
|
|
}
|
|
|
|
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
|
|
|
|
|
|
|
|
return nodeForItem(item as AnyObject?).childNodes![index]
|
|
|
|
}
|
|
|
|
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
|
|
|
|
|
|
|
|
return nodeForItem(item as AnyObject?).canHaveChildNodes
|
|
|
|
}
|
2017-11-08 06:14:58 +01:00
|
|
|
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? {
|
|
|
|
|
|
|
|
let node = nodeForItem(item as AnyObject?)
|
|
|
|
if let feed = node.representedObject as? Feed {
|
|
|
|
return FeedPasteboardWriter(feed: feed)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2017-05-27 19:43:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
//MARK: - Private
|
|
|
|
|
|
|
|
private extension SidebarViewController {
|
|
|
|
|
|
|
|
var selectedNodes: [Node] {
|
|
|
|
get {
|
|
|
|
if let nodes = outlineView.selectedItems as? [Node] {
|
|
|
|
return nodes
|
|
|
|
}
|
|
|
|
return [Node]()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func rebuildTreeAndReloadDataIfNeeded() {
|
|
|
|
|
2017-11-05 21:14:36 +01:00
|
|
|
if !animatingChanges && !BatchUpdate.shared.isPerforming {
|
2017-05-27 19:43:27 +02:00
|
|
|
treeController.rebuild()
|
|
|
|
outlineView.reloadData()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-05 07:05:20 +01:00
|
|
|
func postSidebarSelectionDidChangeNotification(_ selectedObjects: [AnyObject]?) {
|
2017-09-24 21:24:44 +02:00
|
|
|
|
|
|
|
let appInfo = AppInfo()
|
2017-10-20 06:56:30 +02:00
|
|
|
if let objects = selectedObjects {
|
|
|
|
appInfo.objects = objects
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.updateUnreadCounts(for: objects)
|
|
|
|
}
|
2017-05-27 19:43:27 +02:00
|
|
|
}
|
2017-09-24 21:24:44 +02:00
|
|
|
appInfo.view = outlineView
|
2017-10-20 06:56:30 +02:00
|
|
|
|
2017-09-24 21:24:44 +02:00
|
|
|
NotificationCenter.default.post(name: .SidebarSelectionDidChange, object: self, userInfo: appInfo.userInfo)
|
2017-05-27 19:43:27 +02:00
|
|
|
}
|
|
|
|
|
2017-11-05 07:05:20 +01:00
|
|
|
func updateUnreadCounts(for objects: [AnyObject]) {
|
2017-10-19 06:53:45 +02:00
|
|
|
|
|
|
|
// On selection, update unread counts for folders and feeds.
|
|
|
|
// For feeds, actually fetch from database.
|
|
|
|
|
|
|
|
for object in objects {
|
|
|
|
if let feed = object as? Feed, let account = feed.account {
|
|
|
|
account.updateUnreadCounts(for: Set([feed]))
|
|
|
|
}
|
2017-10-22 01:37:40 +02:00
|
|
|
else if let folder = object as? Folder, let account = folder.account {
|
|
|
|
account.updateUnreadCounts(for: folder.flattenedFeeds())
|
2017-10-19 06:53:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-27 19:43:27 +02: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
|
|
|
|
}
|
|
|
|
|
|
|
|
func rowContainingNextUnread() -> Int? {
|
|
|
|
|
|
|
|
let selectedRow = outlineView.selectedRow
|
|
|
|
let numberOfRows = outlineView.numberOfRows
|
|
|
|
var row = selectedRow + 1
|
|
|
|
|
|
|
|
while (row < numberOfRows) {
|
|
|
|
if rowHasAtLeastOneUnreadArticle(row) {
|
|
|
|
return row
|
|
|
|
}
|
|
|
|
row += 1
|
|
|
|
}
|
|
|
|
|
|
|
|
row = 0
|
|
|
|
while (row <= selectedRow) {
|
|
|
|
if rowHasAtLeastOneUnreadArticle(row) {
|
|
|
|
return row
|
|
|
|
}
|
|
|
|
row += 1
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func configure(_ cell: SidebarCell, _ node: Node) {
|
|
|
|
|
|
|
|
cell.objectValue = node
|
|
|
|
cell.name = nameFor(node)
|
|
|
|
cell.unreadCount = unreadCountFor(node)
|
|
|
|
cell.image = imageFor(node)
|
|
|
|
}
|
|
|
|
|
|
|
|
func imageFor(_ node: Node) -> NSImage? {
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func nameFor(_ node: Node) -> String {
|
|
|
|
|
|
|
|
if let displayNameProvider = node.representedObject as? DisplayNameProvider {
|
|
|
|
return displayNameProvider.nameForDisplay
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func unreadCountFor(_ node: Node) -> Int {
|
|
|
|
|
|
|
|
if let unreadCountProvider = node.representedObject as? UnreadCountProvider {
|
|
|
|
return unreadCountProvider.unreadCount
|
|
|
|
}
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
func availableSidebarCells() -> [SidebarCell] {
|
|
|
|
|
|
|
|
var cells = [SidebarCell]()
|
|
|
|
|
|
|
|
outlineView.enumerateAvailableRowViews { (rowView: NSTableRowView, _: Int) -> Void in
|
|
|
|
|
|
|
|
if let oneSidebarCell = rowView.view(atColumn: 0) as? SidebarCell {
|
|
|
|
cells += [oneSidebarCell]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return cells
|
|
|
|
}
|
|
|
|
|
2017-11-05 07:05:20 +01:00
|
|
|
func cellsForRepresentedObject(_ representedObject: AnyObject) -> [SidebarCell] {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
|
|
let availableCells = availableSidebarCells()
|
|
|
|
return availableCells.filter{ (oneSidebarCell) -> Bool in
|
|
|
|
|
|
|
|
guard let oneNode = oneSidebarCell.objectValue as? Node else {
|
|
|
|
return false
|
|
|
|
}
|
2017-11-05 07:05:20 +01:00
|
|
|
return oneNode.representedObject === representedObject
|
2017-05-27 19:43:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-05 07:05:20 +01:00
|
|
|
func configureCellsForRepresentedObject(_ representedObject: AnyObject) -> Bool {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
|
|
//Return true if any cells were configured.
|
|
|
|
|
|
|
|
let cells = cellsForRepresentedObject(representedObject)
|
|
|
|
if cells.isEmpty {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
cells.forEach { (oneSidebarCell) in
|
|
|
|
guard let oneNode = oneSidebarCell.objectValue as? Node else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
configure(oneSidebarCell, oneNode)
|
|
|
|
oneSidebarCell.needsDisplay = true
|
|
|
|
oneSidebarCell.needsLayout = true
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
@discardableResult
|
2017-11-05 07:05:20 +01:00
|
|
|
func revealAndSelectRepresentedObject(_ representedObject: AnyObject) -> Bool {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
|
|
return outlineView.revealAndSelectRepresentedObject(representedObject, treeController)
|
|
|
|
}
|
|
|
|
|
2017-10-02 22:15:07 +02:00
|
|
|
func folderParentForNode(_ node: Node) -> Container? {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
2017-10-02 22:15:07 +02:00
|
|
|
if let folder = node.parent?.representedObject as? Container {
|
2017-05-27 19:43:27 +02:00
|
|
|
return folder
|
|
|
|
}
|
|
|
|
if let feed = node.representedObject as? Feed {
|
|
|
|
return feed.account
|
|
|
|
}
|
|
|
|
if let folder = node.representedObject as? Folder {
|
|
|
|
return folder.account
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func deleteItemForNode(_ node: Node) {
|
|
|
|
|
2017-10-02 22:15:07 +02:00
|
|
|
// if let folder = folderParentForNode(node) {
|
|
|
|
// folder.deleteItems([node.representedObject])
|
|
|
|
// }
|
2017-05-27 19:43:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func deleteItemsForNodes(_ nodes: [Node]) {
|
|
|
|
|
|
|
|
nodes.forEach { (oneNode) in
|
|
|
|
|
|
|
|
deleteItemForNode(oneNode)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|