Writing file path to the macOS pasteboard as a string using NSFilePromiseProvider

huangapple go评论58阅读模式
英文:

Writing file path to the macOS pasteboard as a string using NSFilePromiseProvider

问题

I can help with the translation of the provided code and text. Here's the translated content:

我有一个代表文件的表格。我想要能够将1个或多个行拖放到 iTerm  TextEdit 中作为文件路径字符串。我正在使用现代的 `NSFilePromiseProvider` 来实现这个功能。我已经按照 [Support Image Export by Providing File Promises][1] 示例代码的指导,现在我有了一个 `FilePromiseProvider`

```swift
class FilePromiseProvider: NSFilePromiseProvider {
    
    struct UserInfoKeys {
        static let url = "url"
    }
    
    override func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
        var types = super.writableTypes(for: pasteboard)

        types.append(contentsOf: [.fileURL, .string]) // <-- 添加了 .string
        return types
    }
    
    override func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? {
        guard let userInfoDict = userInfo as? [String: Any] else { return nil }
    
        switch type {
        case .fileURL:
            // 输入的类型是 "public.file-url",从我们的 userInfo 中返回 URL。
            if let url = userInfoDict[FilePromiseProvider.UserInfoKeys.url] as? NSURL {
                return url.pasteboardPropertyList(forType: type)
            }

        case .string:
            if let url = userInfoDict[FilePromiseProvider.UserInfoKeys.url] as? NSURL {
                return url.path
            }
        default: break
        }
        
        return super.pasteboardPropertyList(forType: type)
    }
    
    public override func writingOptions(forType type: NSPasteboard.PasteboardType, pasteboard: NSPasteboard) -> NSPasteboard.WritingOptions {
        return super.writingOptions(forType: type, pasteboard: pasteboard)
    }

}

此外,我已将此内容与表格视图连接起来,如下所示:

class ResultsViewController: NSTableViewDelegate {
    // ... 等等

    // 用于读写文件承诺的队列。
    var filePromiseQueue: OperationQueue = {
        let queue = OperationQueue()
        return queue
    }()

    func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? {
        var provider: FilePromiseProvider

        if let assets = filesArrayController.arrangedObjects as? [MyFile] {
            let asset = assets[row]
            let ext = asset.url.pathExtension
            
            if #available(macOS 11.0, *) {
                let typeIdentifier = UTType(filenameExtension: ext)
                provider = FilePromiseProvider(fileType: typeIdentifier!.identifier, delegate: self)
                provider.userInfo = [FilePromiseProvider.UserInfoKeys.url: asset.url as Any]
            } else {
                let typeIdentifier = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, ext as CFString, nil)
                provider = FilePromiseProvider(fileType: typeIdentifier!.takeRetainedValue() as String, delegate: self)
            }
            return provider
        }

        return nil
    }
}

extension ResultsViewController: NSFilePromiseProviderDelegate {
    func operationQueue(for filePromiseProvider: NSFilePromiseProvider) -> OperationQueue {
        return filePromiseQueue
    }

    // 从未被调用。为什么?
    func filePromiseProvider(_ provider: NSFilePromiseProvider, fileNameForType fileType: String) -> String {
        let asset = assetFromFilePromiseProvider(provider: provider)
        let fileName = (asset?.url.lastPathComponent)!
        return (asset?.url.lastPathComponent)!
    }
    
    func filePromiseProvider(_ provider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: @escaping (Error?) -> Void) {
        do {
            if let asset = assetFromFilePromiseProvider(provider: provider) {
                /** 复制文件到我们提供的位置。我们总是复制,而不是移动。
                 重要的是要调用完成处理程序。
                 */
                try FileManager.default.copyItem(at: asset.url, to: url)
            }
            completionHandler(nil)
        } catch let error {
            OperationQueue.main.addOperation {
                self.presentError(error, modalFor: self.view.window!,
                                  delegate: nil, didPresent: nil, contextInfo: nil)
            }
            completionHandler(error)
        }
    }
    
    func assetFromFilePromiseProvider(provider: NSFilePromiseProvider) -> MyFile? {
        var returnAsset: MyFile?
        
        if  let userInfo = provider.userInfo as? [String: Any],
            let row = userInfo[FilePromiseProvider.UserInfoKeys.rowNumber] as? Int {
            if let assets = filesArrayController.arrangedObjects as? [MyFile] {
                returnAsset = assets[row]
            }
        }
        return returnAsset
    }
}

这个代码能够正常工作!我可以将表格中的一行拖放到 Finder 中,例如,会在拖放的文件夹中创建一个副本。我还可以将其拖放到打开/保存对话框中以切换文件夹/文件名。我还可以将其拖放到 Terminal.app 中,文件的路径将被插入。

然而,从我的应用程序拖放到 TextEdit 或 iTerm 2 中却不起作用。拖放操作简单地结束,拖放的文档/终端窗口中什么也不改变。请注意,从 Finder 或 Panic 的 Transmit 中拖放文件到这些应用程序中会插入路径,因此似乎是我的实现存在问题。

我已经模仿了 Panic 的粘贴板。以下是来自我的应用程序的第一个拖放项目的类型标识符,如 Pasteboard Viewer 报告的:

类型
com.apple.NSFilePromiseItemMetaData 二进制属性列表显示 public.jpeg
com.apple.pasteboard.promised-file-name
com.apple.pasteboard.promised-suggested-file-name
com.apple.pasteboard.promised-file-content-type public.jpeg
com.apple.pasteboard.NSFilePromiseID 每个拖放操作都唯一
public.utf8-plain-text 例如 /path/to/file name.jpg
public.file-url 例如 file:///path/to/file%20name.jpg
dyn.xxxxx 两个,分别为 56 字节和 0 字节。不确定是什么?
com.apple.pasteboard.promised-file-url

有什么想

英文:

I have a table which represents files. I want to be able to drag and drop 1+ rows into iTerm or TextEdit as file path strings. I'm using the modern NSFilePromiseProvider for this. I have followed the Support Image Export by Providing File Promises sample code and I now have a FilePromiseProvider:

class FilePromiseProvider: NSFilePromiseProvider {
    
    struct UserInfoKeys {
        static let url = &quot;url&quot;
    }
    
    override func writableTypes(for pasteboard: NSPasteboard) -&gt; [NSPasteboard.PasteboardType] {
        var types = super.writableTypes(for: pasteboard)

        types.append(contentsOf: [.fileURL, .string]) // &lt;-- Added .string
        return types
    }
    
    override func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -&gt; Any? {
        guard let userInfoDict = userInfo as? [String: Any] else { return nil }
    
        switch type {
        case .fileURL:
            // Incoming type is &quot;public.file-url&quot;, return (from our userInfo) the URL.
            if let url = userInfoDict[FilePromiseProvider.UserInfoKeys.url] as? NSURL {
                return url.pasteboardPropertyList(forType: type)
            }

        case .string:
            if let url = userInfoDict[FilePromiseProvider.UserInfoKeys.url] as? NSURL {
                return url.path
            }
        default: break
        }
        
        return super.pasteboardPropertyList(forType: type)
    }
    
    public override func writingOptions(forType type: NSPasteboard.PasteboardType, pasteboard: NSPasteboard)
        -&gt; NSPasteboard.WritingOptions {
        return super.writingOptions(forType: type, pasteboard: pasteboard)
    }

}

In addition, I have wired this into the table view like so:

class ResultsViewController: NSTableViewDelegate {
    // ... etc


    // Queue used for reading and writing file promises.
    var filePromiseQueue: OperationQueue = {
        let queue = OperationQueue()
        return queue
    }()

    func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -&gt; NSPasteboardWriting? {
        var provider: FilePromiseProvider

        if let assets = filesArrayController.arrangedObjects as? [MyFile] {
            let asset = assets[row]
            let ext = asset.url.pathExtension
            
            if #available(macOS 11.0, *) {
                let typeIdentifier = UTType(filenameExtension: ext)
                provider = FilePromiseProvider(fileType: typeIdentifier!.identifier, delegate: self)
                provider.userInfo = [FilePromiseProvider.UserInfoKeys.url: asset.url as Any]
            } else {
                let typeIdentifier =
                      UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, ext as CFString, nil)
                provider = FilePromiseProvider(fileType: typeIdentifier!.takeRetainedValue() as String, delegate: self)
            }
            return provider
        }

        return nil
    }
}


extension ResultsViewController: NSFilePromiseProviderDelegate {
    func operationQueue(for filePromiseProvider: NSFilePromiseProvider) -&gt; OperationQueue {
        return filePromiseQueue
    }

    // Never called. Why not?
    func filePromiseProvider(_ provider: NSFilePromiseProvider, fileNameForType fileType: String) -&gt; String {
        let asset = assetFromFilePromiseProvider(provider: provider)
        let fileName = (asset?.url.lastPathComponent)!
        return (asset?.url.lastPathComponent)!
    }
    
    func filePromiseProvider(_ provider: NSFilePromiseProvider,
                                         writePromiseTo url: URL,
                                         completionHandler: @escaping (Error?) -&gt; Void) {
        do {
            if let asset = assetFromFilePromiseProvider(provider: provider) {
                /** Copy the file to the location provided to us. We always do a copy, not a move.
                 It&#39;s important you call the completion handler.
                 */
                try FileManager.default.copyItem(at: asset.url, to: url)
            }
            completionHandler(nil)
        } catch let error {
            OperationQueue.main.addOperation {
                self.presentError(error, modalFor: self.view.window!,
                                  delegate: nil, didPresent: nil, contextInfo: nil)
            }
            completionHandler(error)
        }
    }
    
    func assetFromFilePromiseProvider(provider: NSFilePromiseProvider) -&gt; MyFile? {
        var returnAsset: MyFile?
        
        if  let userInfo = provider.userInfo as? [String: Any],
            let row = userInfo[FilePromiseProvider.UserInfoKeys.rowNumber] as? Int {
            if let assets = filesArrayController.arrangedObjects as? [MyFile] {
                returnAsset = assets[row]
            }
        }
        return returnAsset
    }
}

And this works! I can drag a row from the table into Finder, for example, and a copy will be made in the dropped folder. I can drop into the Open/Save dialog to switch the folder/filename. I can also drop into Terminal.app and the file's path will be inserted.

However, dropping from my app does NOT work for TextEdit or iTerm 2. The drag simply ends and nothing changes in the dropped document/terminal window. Note that dropping files from Finder or Panic's Transmit in these apps DOES insert the path, so it seems to be some issue with my implementation.

I've modeled my pasteboard on Panic's. Here are the type identifiers for the first dragged item from my app, as reported by Pasteboard Viewer:

Type Value
com.apple.NSFilePromiseItemMetaData binary plist showing public.jpeg
com.apple.pasteboard.promised-file-name empty
com.apple.pasteboard.promised-suggested-file-name empty
com.apple.pasteboard.promised-file-content-type public.jpeg
com.apple.pasteboard.NSFilePromiseID unique for each drag
public.utf8-plain-text E.g. /path/to/file name.jpg
public.file-url E.g. file:///path/to/file%20name.jpg
dyn.xxxxx Two of these, 56 bytes and 0 bytes. Not sure what they are?
com.apple.pasteboard.promised-file-url empty

Any ideas what I'm doing wrong? One odd thing I've noticed is that filePromiseProvider(_:fileNameForType:) is never called. And indeed, the promised-file-name type is empty. But it's empty in Panic's implementation too, so maybe that's expected?

UPDATE: One quirk I've noticed: when I drag a file from Transmit over a Finder window, I get the green Copy Pointer, indicating that dropping will make a copy of the file. I get the same pointer when I drag from Transmit over iTerm 2.

I don't get this pointer when dragging from my app. However, I don't see it for drags from Finder either; Finder drops work correctly, though. All things being equal, I think showing the correct drag pointer would be preferred. I'm actually returning .copy from tableView(:validateDrop:proposedRow:proposedDropOperation:), but unsurprisingly this delegate method isn't called when the drop target is outside of the table view itself.

答案1

得分: 0

问题是我声明了一个.move的拖拽操作。当我将其更改为.copy时,拖放到iTerm和TextEdit中开始正常工作。

filesTable.setDraggingSourceOperationMask(.copy, forLocal: false)

感谢@Willeke提供的提示,帮助我解决了这个问题!

英文:

The problem was that I was declaring a dragging operation of .move. When I changed that to .copy, the drop into iTerm and TextEdit started working.

filesTable.setDraggingSourceOperationMask(.copy, forLocal: false)

Thanks to @Willeke for the pointer that helped me resolve this!

huangapple
  • 本文由 发表于 2023年5月22日 21:20:03
  • 转载请务必保留本文链接:https://go.coder-hub.com/76306626.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定