HFS Promise Drags from IKImageBrowserView

One of the pieces of hard-won knowledge in Viewfinder is the ability to drag from the thumbnail grid to the Finder to initiate a download. This is known as an "HFS Promise" drag and is widely used in Cocoa to allow dragging and dropping of files that do not yet exist.

Viewfinder uses this to allow drag-drop of files that have not yet been downloaded.

At first glance, it seems hard to do this with IKImageBrowserView, because of the way that class interacts with its delegate to get dragged items onto the pasteboard.

To perform HFS Promise drags out of IKImageBrowserView, you have to get a little deeper and subclass IKImageBrowserView.

Interface

Firstly, here's the interface declaration:

@interface VFImageBrowserView : IKImageBrowserView {
    NSArray *itemsInCurrentPromiseDragOperation;
    BOOL dragSelectInProgress;
}
@end

I keep two variables around:

  • itemsInCurrentPromiseDragOperation - to store the items that are being promise-dragged.
  • dragSelectInProgress - is set to YES when I detect that the user is performing a drag-select.

The definition of a 'drag select' being in progress in this case is: the period between a mouseDown event whose point is not over any image in the browser and the next mouseUp event.

-mouseDown:

In -mouseDown:, I check to see if the user has clicked on an image in the browser, or on the background. To do that, I get the clickPosition of the event, then query self for the index of the image that's under that point. If there's no image (i.e. the index is NSNotFound), I assume the user is doing a drag selection. Then I let super handle the event.

- (void)mouseDown:(NSEvent *)theEvent {
    // If the mouse first goes down on the background, this is a drag-select and
    // we don't want to handle any mouseDragged events until the mouse comes up 
    // again.
    NSPoint clickPosition = [self convertPoint:[theEvent locationInWindow]
                                      fromView:nil];
    NSInteger indexOfItemUnderClick = [self indexOfItemAtPoint: clickPosition];
    dragSelectInProgress = (indexOfItemUnderClick == NSNotFound);
    [super mouseDown: theEvent];
}

-mouseUp:

The implementation of -mouseUp: is simple. Set the dragSelectInProgress variable to NO and let super handle the rest:

- (void)mouseUp:(NSEvent *)theEvent {
    dragSelectInProgress = NO;
    [super mouseUp: theEvent];
}

-mouseDragged:

The real action happens in the override of -mouseDragged:. Firstly, if a drag-select is in progress, we pass the event to super and ignore. If that's not the case, we check for an item under the click point. Again, if there's none, we pass to super and ignore.

Next, we use some Snow Leopard block goodness to store the currently selected items into the itemsInCurrentPromiseDragOperation array and finally call -dragPromisedFilesOfTypes:fromRect:source:slideBack:. This sets up the promise drag.

- (void)mouseDragged:(NSEvent *)theEvent;
{
    // If there's a drag-select in progress, we don't want to know.
    if(dragSelectInProgress) {
        [super mouseDragged: theEvent];
        return;
    }

    // Otherwise, the mouse went down on an image and we should drag it
    NSPoint dragPosition = [self convertPoint:[theEvent locationInWindow] 
                                     fromView:nil];
    NSInteger indexOfItemUnderClick = [self indexOfItemAtPoint: dragPosition];

    if(indexOfItemUnderClick == NSNotFound) {
        [super mouseDragged: theEvent];
        return;
    }

    // Store the selected browser items
    __block NSMutableArray *tempItems = [NSMutableArray array];
    [[self selectionIndexes] enumerateIndexesUsingBlock:
    ^(NSUInteger idx, BOOL *stop) {
        [tempItems addObject: [self.dataSource imageBrowser: self itemAtIndex: idx]];
    }];
    itemsInCurrentPromiseDragOperation = tempItems;

    dragPosition.x -= 16;
    dragPosition.y -= 16;

    NSRect imageLocation;
    imageLocation.origin = dragPosition;
    imageLocation.size = NSMakeSize(64, 64);

    [self dragPromisedFilesOfTypes:[NSArray arrayWithObject:@"jpg"]
                          fromRect:imageLocation
                            source:self
                         slideBack:YES
                             event:theEvent];
}

-namesOfPromisedFilesDroppedAtDestination:

Finally, we have to implement -namesOfPromisedFilesDroppedAtDestination:, which is called once the Finder accepts the drag. All we need to do is assemble a list of file names and return it. I also set the itemsInCurrentPromiseDragOperation variable to nil.

- (NSArray *)namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination;
{
    __block NSMutableArray *fileNames = [NSMutableArray array];

    [itemsInCurrentPromiseDragOperation enumerateObjectsUsingBlock:^(VFBrowserItem *obj, NSUInteger idx, BOOL *stop) {
        // Here, do whatever action makes sense for your application.
        // Viewfinder kicks off a download here.    
        // In this block, you have to insert a file name into the fileNames 
        // array.
    }];

    itemsInCurrentPromiseDragOperation = nil;
    return fileNames;
}

Note that this code is written for 10.6 using garbage collection. I hope this is useful.