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)
|
2019-02-06 07:28:30 +01:00
|
|
|
|
private var draggedNodes: Set<Node>? = nil
|
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
|
|
|
|
|
|
2019-02-06 06:01:53 +01:00
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItems draggedItems: [Any]) {
|
2019-02-06 07:28:30 +01:00
|
|
|
|
draggedNodes = Set(draggedItems.map { nodeForItem($0) })
|
2019-02-06 06:01:53 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-09-19 06:53:19 +02:00
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation {
|
2019-05-29 22:43:33 +02:00
|
|
|
|
let draggedFolders = PasteboardFolder.pasteboardFolders(with: info.draggingPasteboard)
|
|
|
|
|
let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard)
|
|
|
|
|
if (draggedFolders == nil && draggedFeeds == nil) || (draggedFolders != nil && draggedFeeds != nil) {
|
2018-09-20 06:49:13 +02:00
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
|
|
|
|
}
|
2018-09-23 06:27:28 +02:00
|
|
|
|
let parentNode = nodeForItem(item)
|
2019-05-29 22:43:33 +02:00
|
|
|
|
|
|
|
|
|
if let draggedFolders = draggedFolders {
|
|
|
|
|
if draggedFolders.count == 1 {
|
|
|
|
|
return validateLocalFolderDrop(outlineView, draggedFolders.first!, parentNode, index)
|
|
|
|
|
} else {
|
|
|
|
|
return validateLocalFoldersDrop(outlineView, draggedFolders, parentNode, index)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let draggedFeeds = draggedFeeds {
|
|
|
|
|
let contentsType = draggedFeedContentsType(draggedFeeds)
|
|
|
|
|
|
|
|
|
|
switch contentsType {
|
|
|
|
|
case .singleNonLocal:
|
|
|
|
|
let draggedNonLocalFeed = singleNonLocalFeed(from: draggedFeeds)!
|
|
|
|
|
return validateSingleNonLocalFeedDrop(outlineView, draggedNonLocalFeed, parentNode, index)
|
|
|
|
|
case .singleLocal:
|
|
|
|
|
let draggedFeed = draggedFeeds.first!
|
|
|
|
|
return validateSingleLocalFeedDrop(outlineView, draggedFeed, parentNode, index)
|
|
|
|
|
case .multipleLocal:
|
|
|
|
|
return validateLocalFeedsDrop(outlineView, draggedFeeds, parentNode, index)
|
|
|
|
|
case .multipleNonLocal, .mixed, .empty:
|
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
|
|
|
|
}
|
2018-09-22 20:54:02 +02:00
|
|
|
|
}
|
2019-05-29 22:43:33 +02:00
|
|
|
|
|
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
2018-09-19 06:53:19 +02:00
|
|
|
|
}
|
2018-09-23 06:27:28 +02:00
|
|
|
|
|
2018-09-19 06:53:19 +02:00
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool {
|
2019-05-29 22:43:33 +02:00
|
|
|
|
let draggedFolders = PasteboardFolder.pasteboardFolders(with: info.draggingPasteboard)
|
|
|
|
|
let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard)
|
|
|
|
|
if (draggedFolders == nil && draggedFeeds == nil) || (draggedFolders != nil && draggedFeeds != nil) {
|
2018-09-22 21:47:19 +02:00
|
|
|
|
return false
|
|
|
|
|
}
|
2018-09-23 06:42:57 +02:00
|
|
|
|
let parentNode = nodeForItem(item)
|
2019-05-29 22:43:33 +02:00
|
|
|
|
|
2019-05-30 21:36:21 +02:00
|
|
|
|
if let draggedFolders = draggedFolders {
|
|
|
|
|
return acceptLocalFoldersDrop(outlineView, draggedFolders, parentNode, index)
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-29 22:43:33 +02:00
|
|
|
|
if let draggedFeeds = draggedFeeds {
|
|
|
|
|
let contentsType = draggedFeedContentsType(draggedFeeds)
|
|
|
|
|
|
|
|
|
|
switch contentsType {
|
|
|
|
|
case .singleNonLocal:
|
|
|
|
|
let draggedNonLocalFeed = singleNonLocalFeed(from: draggedFeeds)!
|
|
|
|
|
return acceptSingleNonLocalFeedDrop(outlineView, draggedNonLocalFeed, parentNode, index)
|
|
|
|
|
case .singleLocal:
|
|
|
|
|
return acceptLocalFeedsDrop(outlineView, draggedFeeds, parentNode, index)
|
|
|
|
|
case .multipleLocal:
|
|
|
|
|
return acceptLocalFeedsDrop(outlineView, draggedFeeds, parentNode, index)
|
|
|
|
|
case .multipleNonLocal, .mixed, .empty:
|
|
|
|
|
return false
|
|
|
|
|
}
|
2018-09-22 21:47:19 +02:00
|
|
|
|
}
|
2019-05-29 22:43:33 +02:00
|
|
|
|
|
|
|
|
|
return false
|
2018-09-19 06:53:19 +02:00
|
|
|
|
}
|
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 {
|
2019-05-29 22:43:33 +02:00
|
|
|
|
// Don’t allow PseudoFeed to be dragged.
|
2018-09-19 22:22:22 +02:00
|
|
|
|
// This will have to be revisited later. For instance,
|
|
|
|
|
// user-created smart feeds should be draggable, maybe.
|
2019-05-29 22:43:33 +02:00
|
|
|
|
return node.representedObject is Folder || node.representedObject is Feed
|
2018-09-19 22:22:22 +02:00
|
|
|
|
}
|
|
|
|
|
|
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 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
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-23 06:27:28 +02:00
|
|
|
|
func validateSingleLocalFeedDrop(_ outlineView: NSOutlineView, _ draggedFeed: PasteboardFeed, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
|
|
|
|
// A local feed should always drag on to an Account or Folder node, and we can provide an index.
|
|
|
|
|
guard let dropTargetNode = ancestorThatCanAcceptLocalFeed(parentNode) else {
|
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
|
|
|
|
}
|
|
|
|
|
if nodeHasChildRepresentingDraggedFeed(dropTargetNode, draggedFeed) {
|
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
|
|
|
|
}
|
2019-05-29 00:42:19 +02:00
|
|
|
|
if violatesTagSpecificBehavior(dropTargetNode, draggedFeed) {
|
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
|
|
|
|
}
|
2019-02-08 06:27:24 +01:00
|
|
|
|
if parentNode == dropTargetNode && index == NSOutlineViewDropOnItemIndex {
|
2019-05-29 22:43:33 +02:00
|
|
|
|
return localDragOperation()
|
2019-02-08 06:27:24 +01:00
|
|
|
|
}
|
2018-09-23 06:27:28 +02:00
|
|
|
|
let updatedIndex = indexWhereDraggedFeedWouldAppear(dropTargetNode, draggedFeed)
|
|
|
|
|
if parentNode !== dropTargetNode || index != updatedIndex {
|
|
|
|
|
outlineView.setDropItem(dropTargetNode, dropChildIndex: updatedIndex)
|
|
|
|
|
}
|
2019-05-29 22:43:33 +02:00
|
|
|
|
return localDragOperation()
|
2018-09-23 06:27:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-02-06 07:28:30 +01:00
|
|
|
|
func validateLocalFeedsDrop(_ outlineView: NSOutlineView, _ draggedFeeds: Set<PasteboardFeed>, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
|
|
|
|
// Local feeds should always drag on to an Account or Folder node, and index should be NSOutlineViewDropOnItemIndex since we can’t provide multiple indexes.
|
|
|
|
|
guard let dropTargetNode = ancestorThatCanAcceptLocalFeed(parentNode) else {
|
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
2019-02-06 06:01:53 +01:00
|
|
|
|
}
|
2019-02-06 07:28:30 +01:00
|
|
|
|
if nodeHasChildRepresentingAnyDraggedFeed(dropTargetNode, draggedFeeds) {
|
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
|
|
|
|
}
|
2019-05-29 00:42:19 +02:00
|
|
|
|
if violatesTagSpecificBehavior(dropTargetNode, draggedFeeds) {
|
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
|
|
|
|
}
|
2019-02-06 07:28:30 +01:00
|
|
|
|
if parentNode !== dropTargetNode || index != NSOutlineViewDropOnItemIndex {
|
|
|
|
|
outlineView.setDropItem(dropTargetNode, dropChildIndex: NSOutlineViewDropOnItemIndex)
|
|
|
|
|
}
|
2019-05-29 22:43:33 +02:00
|
|
|
|
return localDragOperation()
|
2019-05-28 01:01:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-29 22:43:33 +02:00
|
|
|
|
func localDragOperation() -> NSDragOperation {
|
2019-05-28 01:01:24 +02:00
|
|
|
|
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
|
|
|
|
return .copy
|
|
|
|
|
} else {
|
|
|
|
|
return .move
|
|
|
|
|
}
|
2019-02-06 06:01:53 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-28 23:31:03 +02:00
|
|
|
|
func accountForNode(_ node: Node) -> Account? {
|
2019-02-06 06:01:53 +01:00
|
|
|
|
if let account = node.representedObject as? Account {
|
|
|
|
|
return account
|
|
|
|
|
}
|
|
|
|
|
if let folder = node.representedObject as? Folder {
|
|
|
|
|
return folder.account
|
|
|
|
|
}
|
|
|
|
|
if let feed = node.representedObject as? Feed {
|
|
|
|
|
return feed.account
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-28 23:31:03 +02:00
|
|
|
|
func commonAccountsFor(_ nodes: Set<Node>) -> Set<Account> {
|
2019-05-27 22:11:16 +02:00
|
|
|
|
|
|
|
|
|
var accounts = Set<Account>()
|
2019-02-06 07:28:30 +01:00
|
|
|
|
for node in nodes {
|
|
|
|
|
guard let oneAccount = accountForNode(node) else {
|
2019-05-27 22:11:16 +02:00
|
|
|
|
continue
|
2019-02-06 07:28:30 +01:00
|
|
|
|
}
|
2019-05-27 22:11:16 +02:00
|
|
|
|
accounts.insert(oneAccount)
|
2019-02-06 06:01:53 +01:00
|
|
|
|
}
|
2019-05-27 22:11:16 +02:00
|
|
|
|
return accounts
|
2019-02-06 07:28:30 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-31 00:57:06 +02:00
|
|
|
|
func accountHasFolderRepresentingAnyDraggedFolders(_ account: Account, _ draggedFolders: Set<PasteboardFolder>) -> Bool {
|
|
|
|
|
for draggedFolder in draggedFolders {
|
|
|
|
|
if account.existingFolder(with: draggedFolder.name) != nil {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-29 22:43:33 +02:00
|
|
|
|
func validateLocalFolderDrop(_ outlineView: NSOutlineView, _ draggedFolder: PasteboardFolder, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
|
|
|
|
guard let dropAccount = parentNode.representedObject as? Account, dropAccount.accountID != draggedFolder.accountID else {
|
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
|
|
|
|
}
|
2019-05-31 00:57:06 +02:00
|
|
|
|
if accountHasFolderRepresentingAnyDraggedFolders(dropAccount, Set([draggedFolder])) {
|
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
|
|
|
|
}
|
2019-05-29 22:43:33 +02:00
|
|
|
|
let updatedIndex = indexWhereDraggedFolderWouldAppear(parentNode, draggedFolder)
|
|
|
|
|
if index != updatedIndex {
|
|
|
|
|
outlineView.setDropItem(parentNode, dropChildIndex: updatedIndex)
|
|
|
|
|
}
|
|
|
|
|
return localDragOperation()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func validateLocalFoldersDrop(_ outlineView: NSOutlineView, _ draggedFolders: Set<PasteboardFolder>, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
|
|
|
|
guard let dropAccount = parentNode.representedObject as? Account else {
|
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
|
|
|
|
}
|
2019-05-31 00:57:06 +02:00
|
|
|
|
if accountHasFolderRepresentingAnyDraggedFolders(dropAccount, draggedFolders) {
|
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
|
|
|
|
}
|
2019-05-29 22:43:33 +02:00
|
|
|
|
for draggedFolder in draggedFolders {
|
|
|
|
|
if dropAccount.accountID == draggedFolder.accountID {
|
|
|
|
|
return SidebarOutlineDataSource.dragOperationNone
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if index != NSOutlineViewDropOnItemIndex {
|
|
|
|
|
outlineView.setDropItem(parentNode, dropChildIndex: NSOutlineViewDropOnItemIndex)
|
|
|
|
|
}
|
|
|
|
|
return localDragOperation()
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-30 21:36:21 +02:00
|
|
|
|
func copyFeedInAccount(node: Node, to parentNode: Node) {
|
2019-05-30 03:47:52 +02:00
|
|
|
|
guard let feed = node.representedObject as? Feed, let destination = parentNode.representedObject as? Container else {
|
2019-05-28 20:11:29 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-30 03:47:52 +02:00
|
|
|
|
destination.account?.addFeed(feed, to: destination) { result in
|
2019-05-28 20:11:29 +02:00
|
|
|
|
switch result {
|
|
|
|
|
case .success:
|
|
|
|
|
break
|
|
|
|
|
case .failure(let error):
|
|
|
|
|
NSApplication.shared.presentError(error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-30 21:36:21 +02:00
|
|
|
|
func moveFeedInAccount(node: Node, to parentNode: Node) {
|
2019-05-30 03:47:52 +02:00
|
|
|
|
guard let feed = node.representedObject as? Feed,
|
|
|
|
|
let source = node.parent?.representedObject as? Container,
|
|
|
|
|
let destination = parentNode.representedObject as? Container else {
|
2019-05-28 01:01:24 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-30 00:08:41 +02:00
|
|
|
|
BatchUpdate.shared.start()
|
2019-05-30 17:12:34 +02:00
|
|
|
|
source.account?.moveFeed(feed, from: source, to: destination) { result in
|
2019-06-13 21:50:41 +02:00
|
|
|
|
BatchUpdate.shared.end()
|
2019-05-28 01:01:24 +02:00
|
|
|
|
switch result {
|
|
|
|
|
case .success:
|
2019-06-13 21:50:41 +02:00
|
|
|
|
break
|
2019-05-28 01:01:24 +02:00
|
|
|
|
case .failure(let error):
|
|
|
|
|
NSApplication.shared.presentError(error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-30 21:36:21 +02:00
|
|
|
|
func copyFeedBetweenAccounts(node: Node, to parentNode: Node) {
|
2019-05-28 17:59:06 +02:00
|
|
|
|
guard let feed = node.representedObject as? Feed,
|
|
|
|
|
let destinationAccount = nodeAccount(parentNode),
|
|
|
|
|
let destinationContainer = parentNode.representedObject as? Container else {
|
2019-02-06 07:28:30 +01:00
|
|
|
|
return
|
2019-02-06 06:01:53 +01:00
|
|
|
|
}
|
2019-05-28 17:59:06 +02:00
|
|
|
|
|
2019-05-28 23:46:16 +02:00
|
|
|
|
if let existingFeed = destinationAccount.existingFeed(withURL: feed.url) {
|
2019-05-30 03:47:52 +02:00
|
|
|
|
destinationAccount.addFeed(existingFeed, to: destinationContainer) { result in
|
2019-05-28 23:46:16 +02:00
|
|
|
|
switch result {
|
|
|
|
|
case .success:
|
|
|
|
|
break
|
|
|
|
|
case .failure(let error):
|
|
|
|
|
NSApplication.shared.presentError(error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
destinationAccount.createFeed(url: feed.url, name: feed.editedName, container: destinationContainer) { result in
|
|
|
|
|
switch result {
|
|
|
|
|
case .success:
|
|
|
|
|
break
|
|
|
|
|
case .failure(let error):
|
|
|
|
|
NSApplication.shared.presentError(error)
|
|
|
|
|
}
|
2019-05-28 17:59:06 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-05-28 01:01:24 +02:00
|
|
|
|
|
2019-05-30 21:36:21 +02:00
|
|
|
|
func moveFeedBetweenAccounts(node: Node, to parentNode: Node) {
|
2019-05-28 17:59:06 +02:00
|
|
|
|
guard let feed = node.representedObject as? Feed,
|
|
|
|
|
let sourceAccount = nodeAccount(node),
|
2019-05-30 00:56:26 +02:00
|
|
|
|
let sourceContainer = node.parent?.representedObject as? Container,
|
2019-05-28 17:59:06 +02:00
|
|
|
|
let destinationAccount = nodeAccount(parentNode),
|
|
|
|
|
let destinationContainer = parentNode.representedObject as? Container else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-28 23:46:16 +02:00
|
|
|
|
if let existingFeed = destinationAccount.existingFeed(withURL: feed.url) {
|
|
|
|
|
|
2019-05-30 00:08:41 +02:00
|
|
|
|
BatchUpdate.shared.start()
|
2019-05-30 03:47:52 +02:00
|
|
|
|
destinationAccount.addFeed(existingFeed, to: destinationContainer) { result in
|
2019-05-28 23:46:16 +02:00
|
|
|
|
switch result {
|
|
|
|
|
case .success:
|
2019-05-30 04:04:44 +02:00
|
|
|
|
sourceAccount.removeFeed(feed, from: sourceContainer) { result in
|
2019-05-30 00:08:41 +02:00
|
|
|
|
BatchUpdate.shared.end()
|
2019-05-28 23:46:16 +02:00
|
|
|
|
switch result {
|
|
|
|
|
case .success:
|
|
|
|
|
break
|
|
|
|
|
case .failure(let error):
|
|
|
|
|
NSApplication.shared.presentError(error)
|
|
|
|
|
}
|
2019-05-09 20:31:18 +02:00
|
|
|
|
}
|
2019-05-28 23:46:16 +02:00
|
|
|
|
case .failure(let error):
|
2019-06-13 21:50:41 +02:00
|
|
|
|
BatchUpdate.shared.end()
|
2019-05-28 23:46:16 +02:00
|
|
|
|
NSApplication.shared.presentError(error)
|
2019-05-09 20:31:18 +02:00
|
|
|
|
}
|
2019-05-09 14:25:45 +02:00
|
|
|
|
}
|
2019-05-28 23:46:16 +02:00
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
2019-05-30 00:08:41 +02:00
|
|
|
|
BatchUpdate.shared.start()
|
2019-05-28 23:46:16 +02:00
|
|
|
|
destinationAccount.createFeed(url: feed.url, name: feed.editedName, container: destinationContainer) { result in
|
|
|
|
|
switch result {
|
|
|
|
|
case .success:
|
2019-05-30 04:04:44 +02:00
|
|
|
|
sourceAccount.removeFeed(feed, from: sourceContainer) { result in
|
2019-05-30 00:08:41 +02:00
|
|
|
|
BatchUpdate.shared.end()
|
2019-05-28 23:46:16 +02:00
|
|
|
|
switch result {
|
|
|
|
|
case .success:
|
|
|
|
|
break
|
|
|
|
|
case .failure(let error):
|
|
|
|
|
NSApplication.shared.presentError(error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case .failure(let error):
|
2019-06-13 21:50:41 +02:00
|
|
|
|
BatchUpdate.shared.end()
|
2019-05-28 23:46:16 +02:00
|
|
|
|
NSApplication.shared.presentError(error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-09 14:25:45 +02:00
|
|
|
|
}
|
2019-02-06 07:28:30 +01:00
|
|
|
|
}
|
2019-02-06 06:01:53 +01:00
|
|
|
|
|
2019-02-06 07:28:30 +01:00
|
|
|
|
func acceptLocalFeedsDrop(_ outlineView: NSOutlineView, _ draggedFeeds: Set<PasteboardFeed>, _ parentNode: Node, _ index: Int) -> Bool {
|
|
|
|
|
guard let draggedNodes = draggedNodes else {
|
2019-02-06 06:01:53 +01:00
|
|
|
|
return false
|
|
|
|
|
}
|
2019-05-27 22:11:16 +02:00
|
|
|
|
|
2019-05-30 00:08:41 +02:00
|
|
|
|
draggedNodes.forEach { node in
|
|
|
|
|
if sameAccount(node, parentNode) {
|
|
|
|
|
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
2019-05-30 21:36:21 +02:00
|
|
|
|
copyFeedInAccount(node: node, to: parentNode)
|
2019-05-28 01:01:24 +02:00
|
|
|
|
} else {
|
2019-05-30 21:36:21 +02:00
|
|
|
|
moveFeedInAccount(node: node, to: parentNode)
|
2019-05-30 00:08:41 +02:00
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
2019-05-30 21:36:21 +02:00
|
|
|
|
copyFeedBetweenAccounts(node: node, to: parentNode)
|
2019-05-30 00:08:41 +02:00
|
|
|
|
} else {
|
2019-05-30 21:36:21 +02:00
|
|
|
|
moveFeedBetweenAccounts(node: node, to: parentNode)
|
2019-05-28 01:01:24 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-02-06 06:01:53 +01:00
|
|
|
|
}
|
2019-05-28 01:01:24 +02:00
|
|
|
|
|
2019-02-06 06:01:53 +01:00
|
|
|
|
return true
|
2018-09-23 06:42:57 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-09-22 20:54:02 +02:00
|
|
|
|
func nodeIsAccountOrFolder(_ node: Node) -> Bool {
|
|
|
|
|
return node.representedObject is Account || node.representedObject is Folder
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-22 21:47:19 +02:00
|
|
|
|
func nodeIsDropTarget(_ node: Node) -> Bool {
|
|
|
|
|
return node.canHaveChildNodes && nodeIsAccountOrFolder(node)
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-23 06:27:28 +02:00
|
|
|
|
func ancestorThatCanAcceptLocalFeed(_ node: Node) -> Node? {
|
|
|
|
|
if nodeIsDropTarget(node) {
|
|
|
|
|
return node
|
|
|
|
|
}
|
|
|
|
|
guard let parentNode = node.parent else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return ancestorThatCanAcceptLocalFeed(parentNode)
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-22 20:54:02 +02:00
|
|
|
|
func ancestorThatCanAcceptNonLocalFeed(_ node: Node) -> Node? {
|
2018-09-22 23:44:16 +02:00
|
|
|
|
// Default to the On My Mac account, if needed, so we can always accept a nonlocal feed drop.
|
2018-09-22 21:47:19 +02:00
|
|
|
|
if nodeIsDropTarget(node) {
|
2018-09-22 20:54:02 +02:00
|
|
|
|
return node
|
|
|
|
|
}
|
|
|
|
|
guard let parentNode = node.parent else {
|
2019-05-01 12:53:18 +02:00
|
|
|
|
if let onMyMacAccountNode = treeController.nodeInTreeRepresentingObject(AccountManager.shared.defaultAccount) {
|
2018-09-22 23:44:16 +02:00
|
|
|
|
return onMyMacAccountNode
|
|
|
|
|
}
|
2018-09-22 20:54:02 +02:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return ancestorThatCanAcceptNonLocalFeed(parentNode)
|
|
|
|
|
}
|
2018-09-22 21:47:19 +02:00
|
|
|
|
|
2019-05-30 21:36:21 +02:00
|
|
|
|
func copyFolderBetweenAccounts(node: Node, to parentNode: Node) {
|
|
|
|
|
guard let sourceFolder = node.representedObject as? Folder,
|
|
|
|
|
let destinationAccount = nodeAccount(parentNode) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
replicateFolder(sourceFolder, destinationAccount: destinationAccount, completion: {})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func moveFolderBetweenAccounts(node: Node, to parentNode: Node) {
|
|
|
|
|
guard let sourceFolder = node.representedObject as? Folder,
|
|
|
|
|
let sourceAccount = nodeAccount(node),
|
|
|
|
|
let destinationAccount = nodeAccount(parentNode) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BatchUpdate.shared.start()
|
|
|
|
|
replicateFolder(sourceFolder, destinationAccount: destinationAccount) {
|
|
|
|
|
sourceAccount.removeFolder(sourceFolder) { result in
|
|
|
|
|
BatchUpdate.shared.end()
|
|
|
|
|
switch result {
|
|
|
|
|
case .success:
|
|
|
|
|
break
|
|
|
|
|
case .failure(let error):
|
|
|
|
|
NSApplication.shared.presentError(error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func replicateFolder(_ folder: Folder, destinationAccount: Account, completion: @escaping () -> Void) {
|
|
|
|
|
destinationAccount.addFolder(folder.name ?? "") { result in
|
|
|
|
|
switch result {
|
|
|
|
|
case .success(let destinationFolder):
|
|
|
|
|
let group = DispatchGroup()
|
|
|
|
|
for feed in folder.topLevelFeeds {
|
2019-05-30 21:44:13 +02:00
|
|
|
|
if let existingFeed = destinationAccount.existingFeed(withURL: feed.url) {
|
|
|
|
|
group.enter()
|
|
|
|
|
destinationAccount.addFeed(existingFeed, to: destinationFolder) { result in
|
|
|
|
|
group.leave()
|
|
|
|
|
switch result {
|
|
|
|
|
case .success:
|
|
|
|
|
break
|
|
|
|
|
case .failure(let error):
|
|
|
|
|
NSApplication.shared.presentError(error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
group.enter()
|
|
|
|
|
destinationAccount.createFeed(url: feed.url, name: feed.editedName, container: destinationFolder) { result in
|
|
|
|
|
group.leave()
|
|
|
|
|
switch result {
|
|
|
|
|
case .success:
|
|
|
|
|
break
|
|
|
|
|
case .failure(let error):
|
|
|
|
|
NSApplication.shared.presentError(error)
|
|
|
|
|
}
|
2019-05-30 21:36:21 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
group.notify(queue: DispatchQueue.main) {
|
|
|
|
|
completion()
|
|
|
|
|
}
|
|
|
|
|
case .failure(let error):
|
|
|
|
|
NSApplication.shared.presentError(error)
|
2019-05-30 21:44:13 +02:00
|
|
|
|
completion()
|
2019-05-30 21:36:21 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func acceptLocalFoldersDrop(_ outlineView: NSOutlineView, _ draggedFolders: Set<PasteboardFolder>, _ parentNode: Node, _ index: Int) -> Bool {
|
|
|
|
|
guard let draggedNodes = draggedNodes else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
draggedNodes.forEach { node in
|
|
|
|
|
if !sameAccount(node, parentNode) {
|
|
|
|
|
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
|
|
|
|
copyFolderBetweenAccounts(node: node, to: parentNode)
|
|
|
|
|
} else {
|
|
|
|
|
moveFolderBetweenAccounts(node: node, to: parentNode)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-22 21:47:19 +02:00
|
|
|
|
func acceptSingleNonLocalFeedDrop(_ outlineView: NSOutlineView, _ draggedFeed: PasteboardFeed, _ parentNode: Node, _ index: Int) -> Bool {
|
|
|
|
|
guard nodeIsDropTarget(parentNode), index == NSOutlineViewDropOnItemIndex else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2018-09-22 23:44:16 +02:00
|
|
|
|
|
2018-09-22 21:47:19 +02:00
|
|
|
|
// Show the add-feed sheet.
|
2019-05-02 00:33:08 +02:00
|
|
|
|
if let account = parentNode.representedObject as? Account {
|
|
|
|
|
appDelegate.addFeed(draggedFeed.url, name: draggedFeed.editedName ?? draggedFeed.name, account: account, folder: nil)
|
|
|
|
|
} else {
|
|
|
|
|
let account = parentNode.parent?.representedObject as? Account
|
|
|
|
|
let folder = parentNode.representedObject as? Folder
|
|
|
|
|
appDelegate.addFeed(draggedFeed.url, name: draggedFeed.editedName ?? draggedFeed.name, account: account, folder: folder)
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-22 21:47:19 +02:00
|
|
|
|
return true
|
|
|
|
|
}
|
2018-09-23 06:27:28 +02:00
|
|
|
|
|
|
|
|
|
func nodeHasChildRepresentingDraggedFeed(_ parentNode: Node, _ draggedFeed: PasteboardFeed) -> Bool {
|
|
|
|
|
return nodeHasChildRepresentingAnyDraggedFeed(parentNode, Set([draggedFeed]))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func nodeRepresentsAnyDraggedFeed(_ node: Node, _ draggedFeeds: Set<PasteboardFeed>) -> Bool {
|
|
|
|
|
guard let feed = node.representedObject as? Feed else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for draggedFeed in draggedFeeds {
|
|
|
|
|
if feed.url == draggedFeed.url {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
2019-05-02 00:49:25 +02:00
|
|
|
|
|
2019-05-28 01:01:24 +02:00
|
|
|
|
func sameAccount(_ node: Node, _ parentNode: Node) -> Bool {
|
|
|
|
|
if let accountID = nodeAccountID(node), let parentAccountID = nodeAccountID(parentNode) {
|
|
|
|
|
if accountID == parentAccountID {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-28 17:59:06 +02:00
|
|
|
|
func nodeAccount(_ node: Node) -> Account? {
|
2019-05-28 01:01:24 +02:00
|
|
|
|
if let account = node.representedObject as? Account {
|
2019-05-28 17:59:06 +02:00
|
|
|
|
return account
|
2019-05-28 01:01:24 +02:00
|
|
|
|
} else if let folder = node.representedObject as? Folder {
|
2019-05-28 17:59:06 +02:00
|
|
|
|
return folder.account
|
2019-05-28 01:01:24 +02:00
|
|
|
|
} else if let feed = node.representedObject as? Feed {
|
2019-05-28 17:59:06 +02:00
|
|
|
|
return feed.account
|
2019-05-28 01:01:24 +02:00
|
|
|
|
} else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2019-05-28 17:59:06 +02:00
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func nodeAccountID(_ node: Node) -> String? {
|
|
|
|
|
return nodeAccount(node)?.accountID
|
2019-05-28 01:01:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-09-23 06:27:28 +02:00
|
|
|
|
func nodeHasChildRepresentingAnyDraggedFeed(_ parentNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> Bool {
|
|
|
|
|
for node in parentNode.childNodes {
|
|
|
|
|
if nodeRepresentsAnyDraggedFeed(node, draggedFeeds) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-29 00:42:19 +02:00
|
|
|
|
func violatesTagSpecificBehavior(_ parentNode: Node, _ draggedFeed: PasteboardFeed) -> Bool {
|
|
|
|
|
return violatesTagSpecificBehavior(parentNode, Set([draggedFeed]))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func violatesTagSpecificBehavior(_ parentNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> Bool {
|
|
|
|
|
guard let parentAccount = nodeAccount(parentNode), parentAccount.usesTags else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for draggedFeed in draggedFeeds {
|
|
|
|
|
if parentAccount.accountID != draggedFeed.accountID {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Can't copy to the account when using tags
|
|
|
|
|
if parentNode.representedObject is Account && (NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-23 06:27:28 +02:00
|
|
|
|
func indexWhereDraggedFeedWouldAppear(_ parentNode: Node, _ draggedFeed: PasteboardFeed) -> Int {
|
|
|
|
|
let draggedFeedWrapper = PasteboardFeedObjectWrapper(pasteboardFeed: draggedFeed)
|
|
|
|
|
let draggedFeedNode = Node(representedObject: draggedFeedWrapper, parent: nil)
|
|
|
|
|
let nodes = parentNode.childNodes + [draggedFeedNode]
|
|
|
|
|
|
|
|
|
|
// Revisit if the tree controller can ever be sorted in some other way.
|
|
|
|
|
let sortedNodes = nodes.sortedAlphabeticallyWithFoldersAtEnd()
|
|
|
|
|
let index = sortedNodes.firstIndex(of: draggedFeedNode)!
|
|
|
|
|
return index
|
|
|
|
|
}
|
2019-05-29 22:43:33 +02:00
|
|
|
|
|
|
|
|
|
func indexWhereDraggedFolderWouldAppear(_ parentNode: Node, _ draggedFolder: PasteboardFolder) -> Int {
|
|
|
|
|
let draggedFolderWrapper = PasteboardFolderObjectWrapper(pasteboardFolder: draggedFolder)
|
|
|
|
|
let draggedFolderNode = Node(representedObject: draggedFolderWrapper, parent: nil)
|
|
|
|
|
draggedFolderNode.canHaveChildNodes = true
|
|
|
|
|
let nodes = parentNode.childNodes + [draggedFolderNode]
|
|
|
|
|
|
|
|
|
|
// Revisit if the tree controller can ever be sorted in some other way.
|
|
|
|
|
let sortedNodes = nodes.sortedAlphabeticallyWithFoldersAtEnd()
|
|
|
|
|
let index = sortedNodes.firstIndex(of: draggedFolderNode)!
|
|
|
|
|
return index
|
|
|
|
|
}
|
2018-09-23 06:27:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final class PasteboardFeedObjectWrapper: DisplayNameProvider {
|
|
|
|
|
|
|
|
|
|
var nameForDisplay: String {
|
|
|
|
|
return pasteboardFeed.editedName ?? pasteboardFeed.name ?? ""
|
|
|
|
|
}
|
|
|
|
|
let pasteboardFeed: PasteboardFeed
|
|
|
|
|
|
|
|
|
|
init(pasteboardFeed: PasteboardFeed) {
|
|
|
|
|
self.pasteboardFeed = pasteboardFeed
|
|
|
|
|
}
|
2018-02-13 07:02:51 +01:00
|
|
|
|
}
|
2019-05-29 22:43:33 +02:00
|
|
|
|
|
|
|
|
|
final class PasteboardFolderObjectWrapper: DisplayNameProvider {
|
|
|
|
|
|
|
|
|
|
var nameForDisplay: String {
|
|
|
|
|
return pasteboardFolder.name
|
|
|
|
|
}
|
|
|
|
|
let pasteboardFolder: PasteboardFolder
|
|
|
|
|
|
|
|
|
|
init(pasteboardFolder: PasteboardFolder) {
|
|
|
|
|
self.pasteboardFolder = pasteboardFolder
|
|
|
|
|
}
|
|
|
|
|
}
|