diff --git a/NetNewsWire/MainWindow/Sidebar/SidebarOutlineDataSource.swift b/NetNewsWire/MainWindow/Sidebar/SidebarOutlineDataSource.swift index fd0083d06..37aabb84a 100644 --- a/NetNewsWire/MainWindow/Sidebar/SidebarOutlineDataSource.swift +++ b/NetNewsWire/MainWindow/Sidebar/SidebarOutlineDataSource.swift @@ -49,43 +49,27 @@ import Account // MARK: - Drag and Drop func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation { - let parentNode = nodeForItem(item) - if parentNode == treeController.rootNode { - return SidebarOutlineDataSource.dragOperationNone - } - guard let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard()), !draggedFeeds.isEmpty else { return SidebarOutlineDataSource.dragOperationNone } + let parentNode = nodeForItem(item) let contentsType = draggedFeedContentsType(draggedFeeds) - if contentsType == .empty || contentsType == .mixed || contentsType == .multipleNonLocal { - return SidebarOutlineDataSource.dragOperationNone - } - if contentsType == .singleNonLocal { + 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 } - -// let draggingSourceOutlineView = info.draggingSource() as? NSOutlineView -// let isLocalDrop = draggingSourceOutlineView == outlineView - -// // 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 -// } - -// if isLocalDrop { -// return validateLocalDrop(draggedFeeds, parentNode: parentNode, proposedChildIndex: index) -// } - return SidebarOutlineDataSource.dragOperationNone } - + func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool { let parentNode = nodeForItem(item) if parentNode == treeController.rootNode { @@ -168,13 +152,6 @@ private extension SidebarOutlineDataSource { return feed.isLocalFeed ? nil : feed } - func validateLocalDrop(_ draggedFeeds: Set, parentNode: Node, proposedChildIndex index: Int) -> NSDragOperation { - -// let parentNode = nodeForItem(item) - - return SidebarOutlineDataSource.dragOperationNone - } - 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 { @@ -186,6 +163,35 @@ private extension SidebarOutlineDataSource { return .copy } + 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 + } + let updatedIndex = indexWhereDraggedFeedWouldAppear(dropTargetNode, draggedFeed) + if parentNode !== dropTargetNode || index != updatedIndex { + outlineView.setDropItem(dropTargetNode, dropChildIndex: updatedIndex) + } + return .move + } + + func validateLocalFeedsDrop(_ outlineView: NSOutlineView, _ draggedFeeds: Set, _ 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 + } + if nodeHasChildRepresentingAnyDraggedFeed(dropTargetNode, draggedFeeds) { + return SidebarOutlineDataSource.dragOperationNone + } + if parentNode !== dropTargetNode || index != NSOutlineViewDropOnItemIndex { + outlineView.setDropItem(dropTargetNode, dropChildIndex: NSOutlineViewDropOnItemIndex) + } + return .move + } + func nodeIsAccountOrFolder(_ node: Node) -> Bool { return node.representedObject is Account || node.representedObject is Folder } @@ -194,6 +200,16 @@ private extension SidebarOutlineDataSource { return node.canHaveChildNodes && nodeIsAccountOrFolder(node) } + func ancestorThatCanAcceptLocalFeed(_ node: Node) -> Node? { + if nodeIsDropTarget(node) { + return node + } + guard let parentNode = node.parent else { + return nil + } + return ancestorThatCanAcceptLocalFeed(parentNode) + } + func ancestorThatCanAcceptNonLocalFeed(_ node: Node) -> Node? { // Default to the On My Mac account, if needed, so we can always accept a nonlocal feed drop. if nodeIsDropTarget(node) { @@ -218,4 +234,52 @@ private extension SidebarOutlineDataSource { appDelegate.addFeed(draggedFeed.url, name: draggedFeed.editedName ?? draggedFeed.name, folder: folder) return true } + + func nodeHasChildRepresentingDraggedFeed(_ parentNode: Node, _ draggedFeed: PasteboardFeed) -> Bool { + return nodeHasChildRepresentingAnyDraggedFeed(parentNode, Set([draggedFeed])) + } + + func nodeRepresentsAnyDraggedFeed(_ node: Node, _ draggedFeeds: Set) -> Bool { + guard let feed = node.representedObject as? Feed else { + return false + } + for draggedFeed in draggedFeeds { + if feed.url == draggedFeed.url { + return true + } + } + return false + } + + func nodeHasChildRepresentingAnyDraggedFeed(_ parentNode: Node, _ draggedFeeds: Set) -> Bool { + for node in parentNode.childNodes { + if nodeRepresentsAnyDraggedFeed(node, draggedFeeds) { + return true + } + } + return false + } + + 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 + } +} + +final class PasteboardFeedObjectWrapper: DisplayNameProvider { + + var nameForDisplay: String { + return pasteboardFeed.editedName ?? pasteboardFeed.name ?? "" + } + let pasteboardFeed: PasteboardFeed + + init(pasteboardFeed: PasteboardFeed) { + self.pasteboardFeed = pasteboardFeed + } }