diff --git a/NetNewsWire/MainWindow/Sidebar/PasteboardFeed.swift b/NetNewsWire/MainWindow/Sidebar/PasteboardFeed.swift index 3a30b3e12..1e8c0b9cb 100644 --- a/NetNewsWire/MainWindow/Sidebar/PasteboardFeed.swift +++ b/NetNewsWire/MainWindow/Sidebar/PasteboardFeed.swift @@ -32,6 +32,7 @@ struct PasteboardFeed: Hashable { let name: String? let editedName: String? let accountID: String? + let isLocalFeed: Bool init(url: String, feedID: String?, homePageURL: String?, name: String?, editedName: String?, accountID: String?) { self.url = url.rs_normalizedURL() @@ -40,6 +41,7 @@ struct PasteboardFeed: Hashable { self.name = name self.editedName = editedName self.accountID = accountID + self.isLocalFeed = accountID != nil } // MARK: - Reading diff --git a/NetNewsWire/MainWindow/Sidebar/SidebarOutlineDataSource.swift b/NetNewsWire/MainWindow/Sidebar/SidebarOutlineDataSource.swift index d4bd75f35..74c8210dc 100644 --- a/NetNewsWire/MainWindow/Sidebar/SidebarOutlineDataSource.swift +++ b/NetNewsWire/MainWindow/Sidebar/SidebarOutlineDataSource.swift @@ -54,12 +54,22 @@ import Account return SidebarOutlineDataSource.dragOperationNone } - guard let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard()) else { + guard let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard()), !draggedFeeds.isEmpty else { return SidebarOutlineDataSource.dragOperationNone } - let draggingSourceOutlineView = info.draggingSource() as? NSOutlineView - let isLocalDrop = draggingSourceOutlineView == outlineView + 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 // // If NSOutlineViewDropOnItemIndex, retarget to parent of parent item, if possible. // if index == NSOutlineViewDropOnItemIndex && !parentNode.canHaveChildNodes { @@ -70,9 +80,9 @@ import Account // return isLocalDrop ? .move : .copy // } - if isLocalDrop { - return validateLocalDrop(draggedFeeds, proposedItem: item, proposedChildIndex: index) - } +// if isLocalDrop { +// return validateLocalDrop(draggedFeeds, parentNode: parentNode, proposedChildIndex: index) +// } return SidebarOutlineDataSource.dragOperationNone } @@ -100,10 +110,76 @@ private extension SidebarOutlineDataSource { return node.representedObject is Feed } - func validateLocalDrop(_ draggedFeeds: Set, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation { + // MARK: - Drag and Drop + + enum DraggedFeedsContentsType { + case empty, singleLocal, singleNonLocal, multipleLocal, multipleNonLocal, mixed + } + + func draggedFeedContentsType(_ draggedFeeds: Set) -> 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? { + guard feeds.count == 1, let feed = feeds.first else { + return nil + } + 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 { + 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) + } }