2018-02-13 07:02:51 +01:00
|
|
|
|
//
|
|
|
|
|
// SidebarOutlineDataSource.swift
|
2018-08-29 07:18:24 +02:00
|
|
|
|
// NetNewsWire
|
2018-02-13 07:02:51 +01:00
|
|
|
|
//
|
|
|
|
|
// Created by Brent Simmons on 2/12/18.
|
|
|
|
|
// Copyright © 2018 Ranchero Software. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import AppKit
|
|
|
|
|
import RSTree
|
2018-07-24 03:29:08 +02:00
|
|
|
|
import Articles
|
2018-02-13 07:02:51 +01:00
|
|
|
|
import RSCore
|
2018-09-19 22:22:22 +02:00
|
|
|
|
import Account
|
2018-02-13 07:02:51 +01:00
|
|
|
|
|
|
|
|
|
@objc final class SidebarOutlineDataSource: NSObject, NSOutlineViewDataSource {
|
|
|
|
|
|
|
|
|
|
let treeController: TreeController
|
2018-09-19 22:22:22 +02:00
|
|
|
|
static let dragOperationNone = NSDragOperation(rawValue: 0)
|
2018-02-13 07:02:51 +01:00
|
|
|
|
|
|
|
|
|
init(treeController: TreeController) {
|
|
|
|
|
self.treeController = treeController
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - NSOutlineViewDataSource
|
|
|
|
|
|
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
|
|
|
|
|
|
2018-09-20 06:49:13 +02:00
|
|
|
|
return nodeForItem(item).numberOfChildNodes
|
2018-02-13 07:02:51 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
|
|
|
|
|
|
2018-09-20 06:49:13 +02:00
|
|
|
|
return nodeForItem(item).childNodes[index]
|
2018-02-13 07:02:51 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
|
|
|
|
|
|
2018-09-20 06:49:13 +02:00
|
|
|
|
return nodeForItem(item).canHaveChildNodes
|
2018-02-13 07:02:51 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? {
|
2018-09-20 06:49:13 +02:00
|
|
|
|
let node = nodeForItem(item)
|
2018-09-19 22:22:22 +02:00
|
|
|
|
guard nodeRepresentsDraggableItem(node) else {
|
2018-09-19 06:12:11 +02:00
|
|
|
|
return nil
|
|
|
|
|
}
|
2018-02-13 07:02:51 +01:00
|
|
|
|
return (node.representedObject as? PasteboardWriterOwner)?.pasteboardWriter
|
|
|
|
|
}
|
2018-09-19 06:53:19 +02:00
|
|
|
|
|
|
|
|
|
// MARK: - Drag and Drop
|
|
|
|
|
|
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation {
|
2018-09-20 06:49:13 +02:00
|
|
|
|
let parentNode = nodeForItem(item)
|
|
|
|
|
if parentNode == treeController.rootNode {
|
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-22 20:54:02 +02:00
|
|
|
|
guard let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard()), !draggedFeeds.isEmpty else {
|
2018-09-20 06:49:13 +02:00
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-22 20:54:02 +02:00
|
|
|
|
let contentsType = draggedFeedContentsType(draggedFeeds)
|
|
|
|
|
if contentsType == .empty || contentsType == .mixed || contentsType == .multipleNonLocal {
|
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if contentsType == .singleNonLocal {
|
|
|
|
|
let draggedNonLocalFeed = singleNonLocalFeed(from: draggedFeeds)!
|
|
|
|
|
return validateSingleNonLocalFeedDrop(outlineView, draggedNonLocalFeed, parentNode, index)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// let draggingSourceOutlineView = info.draggingSource() as? NSOutlineView
|
|
|
|
|
// let isLocalDrop = draggingSourceOutlineView == outlineView
|
2018-09-20 06:49:13 +02:00
|
|
|
|
|
|
|
|
|
// // If NSOutlineViewDropOnItemIndex, retarget to parent of parent item, if possible.
|
|
|
|
|
// if index == NSOutlineViewDropOnItemIndex && !parentNode.canHaveChildNodes {
|
|
|
|
|
// guard let grandparentNode = parentNode.parent, grandparentNode.canHaveChildNodes else {
|
|
|
|
|
// return SidebarOutlineDataSource.dragOperationNone
|
|
|
|
|
// }
|
|
|
|
|
// outlineView.setDropItem(grandparentNode, dropChildIndex: NSOutlineViewDropOnItemIndex)
|
|
|
|
|
// return isLocalDrop ? .move : .copy
|
|
|
|
|
// }
|
|
|
|
|
|
2018-09-22 20:54:02 +02:00
|
|
|
|
// if isLocalDrop {
|
|
|
|
|
// return validateLocalDrop(draggedFeeds, parentNode: parentNode, proposedChildIndex: index)
|
|
|
|
|
// }
|
2018-09-19 22:22:22 +02:00
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
2018-09-19 06:53:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2018-02-13 07:02:51 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Private
|
|
|
|
|
|
|
|
|
|
private extension SidebarOutlineDataSource {
|
|
|
|
|
|
2018-09-20 06:49:13 +02:00
|
|
|
|
func nodeForItem(_ item: Any?) -> Node {
|
2018-02-13 07:02:51 +01:00
|
|
|
|
if item == nil {
|
|
|
|
|
return treeController.rootNode
|
|
|
|
|
}
|
|
|
|
|
return item as! Node
|
|
|
|
|
}
|
2018-09-19 22:22:22 +02:00
|
|
|
|
|
|
|
|
|
func nodeRepresentsDraggableItem(_ node: Node) -> Bool {
|
|
|
|
|
// Don’t allow PseudoFeed or Folder to be dragged.
|
|
|
|
|
// This will have to be revisited later. For instance,
|
|
|
|
|
// user-created smart feeds should be draggable, maybe.
|
|
|
|
|
// And we might allow dragging folders between accounts.
|
|
|
|
|
return node.representedObject is Feed
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-22 20:54:02 +02:00
|
|
|
|
// MARK: - Drag and Drop
|
|
|
|
|
|
|
|
|
|
enum DraggedFeedsContentsType {
|
|
|
|
|
case empty, singleLocal, singleNonLocal, multipleLocal, multipleNonLocal, mixed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func draggedFeedContentsType(_ draggedFeeds: Set<PasteboardFeed>) -> DraggedFeedsContentsType {
|
|
|
|
|
if draggedFeeds.isEmpty {
|
|
|
|
|
return .empty
|
|
|
|
|
}
|
|
|
|
|
if draggedFeeds.count == 1 {
|
|
|
|
|
let feed = draggedFeeds.first!
|
|
|
|
|
return feed.isLocalFeed ? .singleLocal : .singleNonLocal
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var hasLocalFeed = false
|
|
|
|
|
var hasNonLocalFeed = false
|
|
|
|
|
for feed in draggedFeeds {
|
|
|
|
|
if feed.isLocalFeed {
|
|
|
|
|
hasLocalFeed = true
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
hasNonLocalFeed = true
|
|
|
|
|
}
|
|
|
|
|
if hasLocalFeed && hasNonLocalFeed {
|
|
|
|
|
return .mixed
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if hasLocalFeed {
|
|
|
|
|
return .multipleLocal
|
|
|
|
|
}
|
|
|
|
|
return .multipleNonLocal
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func singleNonLocalFeed(from feeds: Set<PasteboardFeed>) -> PasteboardFeed? {
|
|
|
|
|
guard feeds.count == 1, let feed = feeds.first else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return feed.isLocalFeed ? nil : feed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func validateLocalDrop(_ draggedFeeds: Set<PasteboardFeed>, parentNode: Node, proposedChildIndex index: Int) -> NSDragOperation {
|
2018-09-19 22:22:22 +02:00
|
|
|
|
|
2018-09-20 06:49:13 +02:00
|
|
|
|
// let parentNode = nodeForItem(item)
|
2018-09-19 22:22:22 +02:00
|
|
|
|
|
2018-09-20 06:49:13 +02:00
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
2018-09-19 22:22:22 +02:00
|
|
|
|
}
|
2018-09-22 20:54:02 +02:00
|
|
|
|
|
|
|
|
|
func validateSingleNonLocalFeedDrop(_ outlineView: NSOutlineView, _ draggedFeed: PasteboardFeed, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
|
|
|
|
// A non-local feed should always drag on to an Account or Folder node, with NSOutlineViewDropOnItemIndex — since we don’t know where it would sort till we read the feed.
|
|
|
|
|
guard let dropTargetNode = ancestorThatCanAcceptNonLocalFeed(parentNode) else {
|
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
|
|
|
|
}
|
|
|
|
|
if parentNode !== dropTargetNode || index != NSOutlineViewDropOnItemIndex {
|
|
|
|
|
outlineView.setDropItem(dropTargetNode, dropChildIndex: NSOutlineViewDropOnItemIndex)
|
|
|
|
|
}
|
|
|
|
|
return .copy
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func nodeIsAccountOrFolder(_ node: Node) -> Bool {
|
|
|
|
|
return node.representedObject is Account || node.representedObject is Folder
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ancestorThatCanAcceptNonLocalFeed(_ node: Node) -> Node? {
|
|
|
|
|
if node.canHaveChildNodes && nodeIsAccountOrFolder(node) {
|
|
|
|
|
return node
|
|
|
|
|
}
|
|
|
|
|
guard let parentNode = node.parent else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return ancestorThatCanAcceptNonLocalFeed(parentNode)
|
|
|
|
|
}
|
2018-02-13 07:02:51 +01:00
|
|
|
|
}
|