Search code examples
objective-cmacosnspasteboard

How to use NSPasteboard with kPasteboardTypeFileURLPromise for copy/paste?


My application would like to add a promise to the pasteboard for a file that is stored remotely, and may never be pasted—similar to pasting a file copied from a session controlling a VM or other remote system. Ideally, a user can paste in a Finder folder (or the desktop) and the promise would trigger and away we go. I am willing to deal with the issues of fulfilling the promise once triggered, but I have been unable to get the promise to trigger.

All of the promise code I have found deals with drag and drop, which is not functionality what I need (though it is possible that something from DnD needs to be in place for promises to work?)

I have tried using NSFilePromiseProvider with a delegate, and adding that to the pasteboard. I can see the entries on the pasteboard using a clipboard viewer, but when I paste in Finder nothing happens and no delegate methods are called. I can trigger the delegate methods by having the clipboard viewer access the entries, so I know that much is hooked up.

@interface ClipboardMacPromise : NSFilePromiseProvider<NSFilePromiseProviderDelegate>
{
    NSString* m_file;
}
@end
@implementation ClipboardMacPromise
- (id)initWithFileType:(NSString*)type andFile:(NSString*)file
{
    m_file = file;
    return [super initWithFileType:type delegate:self];
}
- (NSString *)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider fileNameForType:(NSString *)fileType
{
    return m_file;
}
- (void)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider writePromiseToURL:(NSURL *)url completionHandler:(void (^)(NSError * _Nullable errorOrNil))completionHandler
{
    // Finder can't paste, so we never get here...
}
@end

NSPasteboard* pboard = [NSPasteboard generalPasteboard];
[pboard clearContents];
NSMutableArray* items = [[NSMutableArray alloc] init];
ClipboardMacPromise* promise = [[ClipboardMacPromise alloc] initWithFileType:(NSString*)kUTTypeFileURL andFile:@"dummy.txt"];
[items addObject:promise];
[pboard writeObjects:items];

I have also tried NSPasteboardItem with NSPasteboardItemDataProvider where I setup a promise for content on kUTITypeFileURL. It provided very similar entries on the pasteboard, but still no action when I paste in finder. Clipboard viewer will again trigger the provider fine when accessing the individual pasteboard entries. (NSPasteboard's declareTypes:owner: has the same behavior)

@interface ClipboardMacPromise : NSPasteboardItem<NSPasteboardItemDataProvider>
{
    NSString* m_file;
}
@end
@implementation ClipboardMacPromise
- (id)initWithFile:(NSString*)file
{
    m_file = file;
    id _self = [super init];
    if (_self) {
        [_self setDataProvider:_self forTypes:@[(NSString*)kPasteboardTypeFileURLPromise]];
        [_self setString:(NSString*)kUTTypeFileURL forType:(NSString*)kPasteboardTypeFilePromiseContent];
    }
    return _self;
}
- (void)pasteboard:(NSPasteboard *)pasteboard item:(NSPasteboardItem *)item provideDataForType:(NSPasteboardType)type
{
    // we don't get here when we paste in Finder because
    // Finder doesn't think there's anything to paste
    // but using a clipboard viewer, we can force the promise to
    // resolve and we do get here
}
@end

NSPasteboard* pboard = [NSPasteboard generalPasteboard];
[pboard clearContents];
NSMutableArray* items = [[NSMutableArray alloc] init];
ClipboardMacPromise* promise = [[ClipboardMacPromise alloc] initWithFile:@"file:///tmp/dummy.txt"];
[items addObject:promise];
[pboard writeObjects:items];

And for completeness, here is my Carbon attempt since Pasteboard.h seems to detail how this should work in a copy/paste scenario... but it still does not provide Finder what it is looking for. The generated clipboard entries look very similar between the three implementations.

OSStatus PasteboardPromiseKeeperProc(PasteboardRef pasteboard, PasteboardItemID item, CFStringRef flavorType, void * _Nullable context)
{
    // 6) The sender's promise callback for kPasteboardTypeFileURLPromise is called.
    string s = "dummy.txt";
    CFDataRef inData = CFDataCreate(kCFAllocatorDefault, (UInt8*)s.c_str(), s.size());
    PasteboardPutItemFlavor(pasteboard, item, flavorType, inData, 0);
    return noErr;
}

PasteboardRef p = NULL;
PasteboardCreate(kPasteboardClipboard, &p);
PasteboardClear(p);
PasteboardSetPromiseKeeper(p, &PasteboardPromiseKeeperProc, this);

// 1) The sender promises kPasteboardTypeFileURLPromise for a file yet to be created.
PasteboardPutItemFlavor(p, (PasteboardItemID)1, kPasteboardTypeFileURLPromise, kPasteboardPromisedData, 0);
// 2) The sender adds kPasteboardTypeFilePromiseContent containing the UTI describing the file's content.
PasteboardPutItemFlavor(p, (PasteboardItemID)2, kPasteboardTypeFilePromiseContent,CFStringCreateExternalRepresentation(NULL, kUTTypeFileURL, kCFStringEncodingUTF8, 0), 0);

It really seems that there is a certain UTI that Finder is looking for on the pasteboard, and I don't have it. If I put a kUTTypeFileURL directly on the clipboard, it appears that finder actually checks for the existence of the file (ie. triggers Catalina's Desktop access prompt) before offering it to paste.

Does anyone know if or how file promises can be provided to Finder through Copy/Paste instead of Drag-and-Drop?


Solution

  • It appears that the key piece here is that Finder requires that the file actually be present on disk for the paste action to be enabled for a file URL. This one detail rules out the possibility of promises working for copy/paste -- at least with Finder.

    The correct solution therefore requires a virtualized file system (like FUSE) so that the promises can be made and fulfilled at the filesystem level. Thus a collection of temporary zero-length files can be written to disk, and actual file URLs be added to the pasteboard. This fulfills the requirements that Finder has to enable paste. Then when a paste action is made, the file data is read from the virtualized file system which can in turn retrieve the actual data from the remote system. Finder is none the wiser. The copy will even have a built in progress bar!

    It appears that Microsoft's Mac RDP client mostly works this way, although I was only ever able to get it to copy zero length files so this may be harder to get right than it sounds.