Andrew
Andrew

Reputation: 11427

NSItemProvider[URL] - how to COPY with drag&Drop instead of MOVE?

I have implemented function that returns NSItemProvider

func dragOutsideWnd(url: URL?) -> NSItemProvider {
    if let url = url {
        TheApp.appDelegate.hideMainWnd()
        
        let provider = NSItemProvider(item: url as NSSecureCoding?, typeIdentifier: UTType.fileURL.identifier as String)
        
        provider.suggestedName = url.lastPathComponent
        //provider.copy()// This doesn't work :)
        
        //DispatchQueue.main.async {
        //    TheApp.appDelegate.hideMainWnd()
        //}
        
        return provider
    }
    
    return NSItemProvider()
}

and I have use it this way:

.onDrag {
   return dragOutsideWnd(url: itm.url)
}

This drag&drop action performs file MOVE action to any place of FINDER/HDD.

But how to perform COPY action?

Upvotes: 3

Views: 1009

Answers (2)

Andrew
Andrew

Reputation: 11427

Thanks a lot to answer of kakaiikaka!

The following solution works in swiftUI:

import Foundation
import SwiftUI

extension View {
    func asDragable(url: URL, tapAction: @escaping () -> () , dTapAction: @escaping () -> ()) -> some View {
        self.background {
            DragDropView(url: url, tapAction: tapAction, dTapAction: dTapAction)
        }
    }
}

struct DragDropView: NSViewRepresentable  {
    let url: URL
    let tapAction: () -> ()
    let dTapAction: () -> ()
    
    func makeNSView(context: Context) -> NSView {
        return DragDropNSView(url: url, tapAction: tapAction, dTapAction: dTapAction)
    }
    
    func updateNSView(_ nsView: NSView, context: Context) { }
}

class DragDropNSView: NSView, NSDraggingSource  {
    let url: URL
    let tapAction: () -> ()
    let dTapAction: () -> ()
    
    let imgMove: NSImage = NSImage(named: "arrow.down.doc.fill_cust")!
    
    init(url: URL, tapAction: @escaping () -> (), dTapAction: @escaping () -> ()) {
        self.url = url
        self.tapAction = tapAction
        self.dTapAction = dTapAction
        
        super.init(frame: .zero)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
        return mustBeMoveAction ? .move : .copy
    }
}

extension DragDropNSView: NSPasteboardItemDataProvider {
    func pasteboard(_ pasteboard: NSPasteboard?, item: NSPasteboardItem, provideDataForType type: NSPasteboard.PasteboardType) {
        // If the desired data type is fileURL, you load an file inside the bundle.
        if let pasteboard = pasteboard, type == NSPasteboard.PasteboardType.fileURL {
            pasteboard.setData(url.dataRepresentation, forType:type)
        }
    }
    
    override func mouseDown(with event: NSEvent) {
        super.mouseDown(with: event)
        
        tapAction()
        
        if event.clickCount == 2 {
            dTapAction()
        }
    }
    
    override func mouseDragged(with event: NSEvent) {
        //1. Creates an NSPasteboardItem and sets this class as its data provider. A NSPasteboardItem is the box that carries the info about the item being dragged. The NSPasteboardItemDataProvider provides data upon request. In this case a file url
        let pasteboardItem = NSPasteboardItem()
        pasteboardItem.setDataProvider(self, forTypes: [NSPasteboard.PasteboardType.fileURL])
        
        var rect = imgMove.alignmentRect
        rect.size = NSSize(width: imgMove.size.width/2, height: imgMove.size.height/2)
        
        //2. Creates a NSDraggingItem and assigns the pasteboard item to it
        let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem)
        
        draggingItem.setDraggingFrame(rect, contents: imgMove) // `contents` is the preview image when dragging happens.
        
        //3. Starts the dragging session. Here you trigger the dragging image to start following your mouse until you drop it.
        beginDraggingSession(with: [draggingItem], event: event, source: self)
    }
}

////////////////////////////////////////
///HELPERS
///////////////////////////////////////
extension DragDropNSView {
    var dragGoingOutsideWindow: Bool {
        guard let currEvent = NSApplication.shared.currentEvent else { return false }
        
        if let rect = self.window?.contentView?.visibleRect,
           rect.contains(currEvent.locationInWindow)
        {
            return false
        }
        
        return true
    }
    
    var mustBeMoveAction: Bool {
        guard let currEvent = NSApplication.shared.currentEvent else { return false }
        
        if currEvent.modifierFlags.check(equals: [.command]) {
            return true
        }
        
        return false
    }
}

extension NSEvent.ModifierFlags {
    func check(equals: [NSEvent.ModifierFlags] ) -> Bool {
        var notEquals: [NSEvent.ModifierFlags] = [.shift, .command, .control, .option]
        
        equals.forEach{ val in notEquals.removeFirst(where: { $0 == val }) }
        
        var result = true
        
        equals.forEach{ val in
            if result {
                result = self.contains(val)
            }
        }
        
        notEquals.forEach{ val in
            if result {
                result = !self.contains(val)
            }
        }
        
        return result
    }
}

usage:

FileIcon()
    .asDragable( url: recent.url, tapAction: {}, dTapAction: {})

this element will be draggable and perform MOVE in case .command key pressed.

And will perform COPY in another case

Also it performs drag action only outside widndow. But it's easy to change.

Upvotes: 2

kakaiikaka
kakaiikaka

Reputation: 4487

Remember Drag&Drop is actually implemented with NSPasteboard.

I have written an example for you: GitHub

enter image description here

Now the key to your questions:

To control dragging behavior(your window is the source):

Draggable objects conform to the NSDraggingSource protocol, so check the first method of the protocol:

@MainActor func draggingSession(
    _ session: NSDraggingSession,
    sourceOperationMaskFor context: NSDraggingContext
) -> NSDragOperation

As the method docsuggests, return different NSDragOperation in this delegation method. That includes: "Copy","Move", "Link", etc.

To control dropping behavior(your window is the destination):

NSView that accepts drop conforms to the NSDraggingDestination protocol, so you need to override the draggingEntered(_:) method by adding this code inside the DestinationView class implementation:

override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation 
{
   var allow = true
   //.copy .move, see more options in NSDragOperation, up to you.
   return allow ? .copy : NSDragOperation() 
}

More info form Apple's Documentation

For swiftUI, a simple show case SwiftUI Showcase

Further Reading: RayWenderlich.com has a detailed tutorial Drag and Drop Tutorial for macOS tutorial for you(needs a little swift upgrade).

Upvotes: 1

Related Questions