我的OS X应用程序如何接受来自Photos.app的图片文件的拖放?

14
当从新的Photos.app中拖动图片时,作为拖动信息的一部分未传递任何URL到剪贴板。我的应用程序已经正确地处理了来自iPhoto、Photo Booth、Aperture等应用程序传递的图像。
我尝试从Photos.app中拖动图片:Finder或Pages可以正确处理,但是TextEdit或Preview却不能。Photos.app与其库中存储的图片的处理方式似乎有所不同。
2个回答

14

在深入研究 NSPasteboard 并调试应用程序后,我意识到 Photos.app 在剪贴板中传递了“承诺的文件”,并在苹果邮件列表中找到了这个线程的一些答案:http://prod.lists.apple.com/archives/cocoa-dev/2015/Apr/msg00448.html

以下是我最终解决问题的方法,在处理将文件拖放到文档中的类中。该类是一个视图控制器,处理常规的拖放方法,因为它在响应链中。

一个方便的方法检测拖动发送者是否有任何与文件相关的内容:

- (BOOL)hasFileURLOrPromisedFileURLWithDraggingInfo:(id <NSDraggingInfo>)sender
{
    NSArray *relevantTypes = @[@"com.apple.pasteboard.promised-file-url", @"public.file-url"];
    for(NSPasteboardItem *item in [[sender draggingPasteboard] pasteboardItems])
    {
        if ([item availableTypeFromArray:relevantTypes] != nil)
        {
            return YES;
        }
    }
    return NO;
}

如果不是 "承诺的文件",我还有一种提取URL的方法:

- (NSURL *)fileURLWithDraggingInfo:(id <NSDraggingInfo>)sender
{
    NSPasteboard *pasteboard = [sender draggingPasteboard];
    NSDictionary *options = [NSDictionary dictionaryWithObject:@YES forKey:NSPasteboardURLReadingFileURLsOnlyKey];
    NSArray *results = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]] options:options];
    return [results lastObject];
}

这里最后介绍一下处理拖放事件的方法。这不是完全精确的代码,因为我简化了内部的拖放处理,并将其转换为方便的方法,以便隐藏应用程序特定部分。我还有一个专门处理文件系统事件的类FileSystemEventCenter,留给读者作为练习。此外,在这里呈现的情况下,我只处理一个文件的拖动。您需要根据自己的情况调整这些部分。

- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
{
    if ([self hasFileURLOrPromisedFileURLWithDraggingInfo:sender])
    {
        [self updateAppearanceWithDraggingInfo:sender];
        return NSDragOperationCopy;
    }
    else
    {
        return NSDragOperationNone;
    }
}

- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
{
    return [self draggingEntered:sender];
}

- (void)draggingExited:(id <NSDraggingInfo>)sender
{
    [self updateAppearanceWithDraggingInfo:nil];
}

- (void)draggingEnded:(id <NSDraggingInfo>)sender
{
    [self updateAppearanceWithDraggingInfo:nil];
}

- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
{
    return [self hasFileURLOrPromisedFileURLWithDraggingInfo:sender];
}

- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
    // promised URL
    NSPasteboard *pasteboard = [sender draggingPasteboard];
    if ([[pasteboard types] containsObject:NSFilesPromisePboardType])
    {
        // promised files have to be created in a specific directory
        NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]];
        if ([[NSFileManager defaultManager] createDirectoryAtPath:tempPath withIntermediateDirectories:NO attributes:nil error:NULL] == NO)
        {
            return NO;
        }

        // the files will be created later: we keep an eye on that using filesystem events
        // `FileSystemEventCenter` is a wrapper around FSEvent
        NSArray *filenames = [sender namesOfPromisedFilesDroppedAtDestination:[NSURL fileURLWithPath:tempPath]];
        DLog(@"file names: %@", filenames);
        if (filenames.count > 0)
        {
            self.promisedFileNames = filenames;
            self.directoryForPromisedFiles = tempPath.stringByStandardizingPath;
            self.targetForPromisedFiles = [self dropTargetForDraggingInfo:sender];
            [[FileSystemEventCenter defaultCenter] addObserver:self selector:@selector(promisedFilesUpdated:) path:tempPath];
            return YES;
        }
        else
        {
            return NO;
        }
    }

    // URL already here
    NSURL *fileURL = [self fileURLWithDraggingInfo:sender];
    if (fileURL)
    {
        [self insertURL:fileURL target:[self dropTargetForDraggingInfo:sender]];
        return YES;
    }
    else
    {
        return NO;
    }
}

- (void)promisedFilesUpdated:(FDFileSystemEvent *)event
{
    dispatch_async(dispatch_get_main_queue(),^
     {
         if (self.directoryForPromisedFiles == nil)
         {
             return;
         }

         NSString *eventPath = event.path.stringByStandardizingPath;
         if ([eventPath hasSuffix:self.directoryForPromisedFiles] == NO)
         {
             [[FileSystemEventCenter defaultCenter] removeObserver:self path:self.directoryForPromisedFiles];
             self.directoryForPromisedFiles = nil;
             self.promisedFileNames = nil;
             self.targetForPromisedFiles = nil;
             return;
         }

         for (NSString *fileName in self.promisedFileNames)
         {
             NSURL *fileURL = [NSURL fileURLWithPath:[self.directoryForPromisedFiles stringByAppendingPathComponent:fileName]];
             if ([[NSFileManager defaultManager] fileExistsAtPath:fileURL.path])
             {
                 [self insertURL:fileURL target:[self dropTargetForDraggingInfo:sender]];
                 [[FileSystemEventCenter defaultCenter] removeObserver:self path:self.directoryForPromisedFiles];
                 self.directoryForPromisedFiles = nil;
                 self.promisedFileNames = nil;
                 self.targetForPromisedFiles = nil;
                 return;
             }
         }
    });
}

谢谢您的回答。我已经实现了您的解决方案,并通过照片应用程序将文件复制到临时路径,但不幸的是,它似乎将它们全部转换为JPG(而不是保留它们作为PNG),这对我的应用程序很重要。我想知道是否还有其他人遇到了同样的问题,是否有任何方法可以指定“不要转换文件”?hasFileURLOrPromisedFileURLWithDraggingInfo似乎已被弃用,但我找不到解决方案... Promises API在我看来非常古老且非常不稳定,所以我目前的“解决方案”是放弃对它的支持... - Sam
@Sam 我相信这是与照片有关的。无论图片的原始格式是什么,它都会返回JPG格式。尝试将图片拖到Finder或预览中,看看会发生什么。 - charles
只是为了澄清,Sam:我的意思是从照片拖到Finder会导致一个jpg文件。它是一个jpg文件与Promise API无关:这只是照片的功能。它不会给你原始文件。即使它没有使用Promise API,该应用程序也可以决定交出一个jpg文件(但它必须在拖动开始之前即时生成并准备好)。我还没有测试过,这只是我的期望。 - charles
啊,好的。非常感谢您澄清。听起来远离照片应用程序是值得的 ;) - Sam

4

苹果在10.12版本中使用NSFilePromiseReceiver使这个过程变得更加简单了。虽然仍然是一个有点复杂的过程,但已经简化了一些。

这是我处理它的方法。实际上,我将其拆分为扩展,但我已经为这个示例进行了简化。

    override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {

        let pasteboard: NSPasteboard = sender.draggingPasteboard()

            guard let filePromises = pasteboard.readObjects(forClasses: [NSFilePromiseReceiver.self], options: nil) as? [NSFilePromiseReceiver] else {
                return
            }
            var images = [NSImage]()
            var errors = [Error]()

            let filePromiseGroup = DispatchGroup()
            let operationQueue = OperationQueue()
            let newTempDirectory: URL
            do {
        let newTempDirectory = (NSTemporaryDirectory() + (UUID().uuidString) + "/") as String
        let newTempDirectoryURL = URL(fileURLWithPath: newTempDirectory, isDirectory: true)

        try FileManager.default.createDirectory(at: newTempDirectoryURL, withIntermediateDirectories: true, attributes: nil)
            }
            catch {
                return
            }

            filePromises.forEach({ filePromiseReceiver in

                filePromiseGroup.enter()

                filePromiseReceiver.receivePromisedFiles(atDestination: newTempDirectory,
                                                         options: [:],
                                                         operationQueue: operationQueue,
                                                         reader: { (url, error) in

                                                            if let error = error {
                                                                errors.append(error)
                                                            }
                                                            else if let image = NSImage(contentsOf: url) {
                                                                images.append(image)
                                                            }
                                                            else {
                                                                errors.append(PasteboardError.noLoadableImagesFound)
                                                            }

                                                            filePromiseGroup.leave()
                })

            })

            filePromiseGroup.notify(queue: DispatchQueue.main,
                                    execute: {
// All done, check your images and errors array

            })
}

我使用了你的方法。但是,操作不会运行,直到我手动关闭应用程序。我的意思是,在我关闭应用程序之前,图像不会显示在缓存文件夹中。有什么建议吗? - Owen Zhao
它可以在Safari上运行。那么它也应该能在Photos上运行,对吧?但我的情况恰恰相反。如果有什么问题,Safari也不应该工作。 - Owen Zhao
当我尝试使用电子邮件拖放操作时,我会收到[Error Domain=NSCocoaErrorDomain Code=3072 "The operation was cancelled."] 错误。 - Alex
我已经使用了你的代码尝试从Apple Mail中删除一封电子邮件。它工作得很好,但是虽然我立即得到了文件,但代码会超时1分钟,然后返回一个错误和带有文件夹名称重复的url变量。例如/users/andrew/temporary/temporary。为了避免发布我的答案,我的代码发布在https://stackoverflow.com/questions/47991603/receive-promised-e-mail-in-macos-10-12上。 - iphaaw
这个之前是可以工作的。我怀疑是苹果公司出了什么问题。现在我遇到了这个错误:*** Assertion failure in -[NSFilePromiseReceiver receivePromisedFilesAtDestination:options:operationQueue:reader:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/AppKit/AppKit-1561.20.106/AppKit.subproj/NSFilePromiseReceiver.m:329 - Mark Bridges
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接