1
votes

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

1
What is preventing you from implementing this in outlineView(_:validateDrop:proposedItem:proposedChildIndex:)?Willeke
@Willeke updated my question with current and expected resultprabhu
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
@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
Oops, mixed up NSCollectionsView and NSOutlineVIew again. I'm sorry.Willeke

1 Answers

1
votes

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 []
    }
}