1

I want to implement drag and drop with NSOutlineView similar Mac Finder application. With my current implementation, drag and drop session validates drop to children of each parent. I don't want that. I only want to drop child from one parent to another parent. like moving file from one folder to another folder in Finder. How to do that? below is a sample code with my drag and drop code included.

import Cocoa

class ViewController: NSViewController {
    @IBOutlet weak var outlineView: NSOutlineView!
    private let treeController = NSTreeController()
    @objc dynamic var content = [Node]()

    override func viewDidLoad() {
        super.viewDidLoad()

        outlineView.delegate = self
        outlineView.dataSource = self

        treeController.objectClass = Node.self
        treeController.childrenKeyPath = "children"
        treeController.countKeyPath = "count"
        treeController.leafKeyPath = "isLeaf"

        outlineView.gridStyleMask = .solidHorizontalGridLineMask
        outlineView.autosaveExpandedItems = true

        treeController.bind(NSBindingName(rawValue: "contentArray"),
                            to: self,
                            withKeyPath: "content",
                            options: nil)


        outlineView.bind(NSBindingName(rawValue: "content"),
                         to: treeController,
                         withKeyPath: "arrangedObjects",
                         options: nil)

        content.append(contentsOf: NodeFactory().nodes())

        outlineView.registerForDraggedTypes([.string])
        outlineView.target = self
    }
}

extension ViewController: NSOutlineViewDelegate, NSOutlineViewDataSource {
    public func outlineView(_ outlineView: NSOutlineView,
                            viewFor tableColumn: NSTableColumn?,
                            item: Any) -> NSView? {
        var cellView: NSTableCellView?

        guard let identifier = tableColumn?.identifier else { return cellView }

        switch identifier {
        case .init("node"):
            if let view = outlineView.makeView(withIdentifier: identifier,
                                               owner: outlineView.delegate) as? NSTableCellView {
                view.textField?.bind(.value,
                                     to: view,
                                     withKeyPath: "objectValue.value",
                                     options: nil)
                cellView = view
            }
        case .init("count"):
            if let view = outlineView.makeView(withIdentifier: identifier,
                                               owner: outlineView.delegate) as? NSTableCellView {
                view.textField?.bind(.value,
                                     to: view,
                                     withKeyPath: "objectValue.childrenCount",
                                     options: nil)
                cellView = view
            }
        default:
            return cellView
        }
        return cellView
    }

    func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? {
        let row = self.outlineView.row(forItem: item)
        let pasteboardItem = NSPasteboardItem.init()
        pasteboardItem.setString("\(row)", forType: .string)
        return pasteboardItem
    }

    func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation {
        if let node = (item as? NSTreeNode)?.representedObject as? Node {
            if node.count >= 0 {
                return .move
            }
        }

        return .init(rawValue: 0)
    }
}



@objc public class Node: NSObject {

    @objc let value: String
    @objc var children: [Node]

    @objc var childrenCount: String? {
        let count = children.count
        guard count > 0 else { return nil }
        return "\(count) node\(count > 1 ? "s" : "")"
    }

    @objc var count: Int {
        children.count
    }

    @objc var isLeaf: Bool {
        children.isEmpty
    }

    init(value: String, children: [Node] = []) {
        self.value = value
        self.children = children
    }
}


final class NodeFactory {
    func nodes() -> [Node] {
        return [
            .init(value: " Offers", children: [
                .init(value: " Ice Cream"),
                .init(value: "☕️ Coffee"),
                .init(value: " Burger")
            ]),
            .init(value: "Retailers", children: [
                .init(value: "King Soopers"),
                .init(value: "Walmart"),
                .init(value: "Target"),
            ])
        ]
    }
}

Current result with above approach:

enter image description here

Expected result:

enter image description here

prabhu
  • 1,158
  • 1
  • 12
  • 27
  • What is preventing you from implementing this in `outlineView(_:validateDrop:proposedItem:proposedChildIndex:)`? – Willeke Feb 17 '20 at 12:31
  • @Willeke updated my question with current and expected result – prabhu Feb 17 '20 at 13:01
  • You have to do some coding, please post your attempt. Tip: use `collectionView(_:validateDrop:proposedIndexPath:dropOperation:)` instead of `collectionView(_:validateDrop:proposedIndex:dropOperation:)` and take a look at the documentation. – Willeke Feb 17 '20 at 13:57
  • @Willeke i have included other than storyboard board part my code in the question. Will try to include that as well. Btw im using outline view not collection view. – prabhu Feb 17 '20 at 14:38
  • Oops, mixed up `NSCollectionsView` and `NSOutlineVIew` again. I'm sorry. – Willeke Feb 17 '20 at 17:14

1 Answers1

1

Here you go:

func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo,
        proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation {

    // start at the proposed item
    var destinationItem = item as? NSTreeNode

    // if the drop is between two rows then find the row under the cursor
    if index != NSOutlineViewDropOnItemIndex {
        if let mouseLocation = NSApp.currentEvent?.locationInWindow {
            let point = outlineView.convert(mouseLocation, from: nil)
            let row = outlineView.row(at: point)
            if row >= 0 {
                destinationItem = outlineView.item(atRow: row) as? NSTreeNode
            }
            else {
                destinationItem = nil
            }
        }
    }

    // if the drop is on a leaf then the destination is the parent item
    if destinationItem?.isLeaf ?? false {
        destinationItem = destinationItem?.parent
    }

    // change the drop item
    if destinationItem != nil {
        outlineView.setDropItem(destinationItem, dropChildIndex: NSOutlineViewDropOnItemIndex)
        return .move
    }
    else {
        return []
    }
}
Willeke
  • 14,578
  • 4
  • 19
  • 47