Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can my OS X app accept drag-and-drop of picture files from Photos.app?

When dragging pictures from the new Photos.app, no URL is passed in the pasteboard as part of the dragging info. My app already correctly handles images passed from e.g. iPhoto, Photo Booth, Aperture,...

I tried dragging pictures from Photos.app: Finder or Pages handle that properly, but not TextEdit or Preview. There seems to be something different about the way Photos.app works with the pictures stored in its library.

like image 427
charles Avatar asked May 07 '15 19:05

charles


2 Answers

After digging into NSPasteboard and stepping through the app, I realized Photos.app is passing "promised files" in the pasteboard, and found this thread in an Apple's mailing list with some answers: http://prod.lists.apple.com/archives/cocoa-dev/2015/Apr/msg00448.html

Here is how I finally solved it, in the class that handles drag and drop of files into a document. The class is a view controller that handles the usual drag/drop methods because it's in the responder chain.

A convenience method detects wether the sender of a drag has any file-related content:

- (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;
}

I also have a method to extract the URL in the case where it's not a "promised file":

- (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];
}

Here is finally the method used to handle a drop. It's not quite exactly my code, as I simplified the internal handling of dragging into convenience methods that allow me to hide the parts specific for the app. I also have a special class for handling file system events FileSystemEventCenter left as an exercise to the reader. Also, in the case presented here, I only handle dragging one file. You'll have to adapt those parts to your own case.

- (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;
             }
         }
    });
}
like image 134
charles Avatar answered Sep 29 '22 10:09

charles


Apple made this a bit easier in 10.12 with the NSFilePromiseReceiver. It's still a long a fiddly process, but a little less so.

Here's how I'm doing it. I've actually split this out into an extension, but I've simplified it for this example.

    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

            })
}
like image 28
Mark Bridges Avatar answered Sep 29 '22 10:09

Mark Bridges