« Drag and Drop Destinations

Posted by Jeff Disher on November 24, 2002 [Feedback (4) & TrackBack (0)]

// Introduction

Drag and Drop is probably the user interface paradigm that most strongly screams "MACINTOSH!" It is most likely one of the biggest reasons why many of us fell in love with the Mac back in our youth. This one paradigm is so simple in its use yet so powerful in its ability to allow a program to receive data from other processes. Although this concept is a complicated one to tackle effectively, I think that you will agree that the Cocoa implementation is very clever and easy to use.

There are two parts of drag and drop: source and destination. In this tutorial, I will only explain the latter since it is much simpler and, thus, is a good introduction. Dragging sources will be described in a future article.

Drag & Drop in Action
Figure 1: Drag & Drop in Action

As a simple example of how to receive drop operations, we will be implementing our own sub-class of NSView that will be quite similar in behavior to the existing NSImageView implementation.

 

// The Interface

Create a new Cocoa Application in Project Builder. Open the MainMenu.nib file to bring up Interface Builder. You should now have the open nib file consisting of just one window. We will be using this to create the very limited and concise UI for our project.

Go through the Interface Builder palette and find the "NSCustomView" (it is in the palette with the "Tabs" in the picture). Drag this view into the window and resize it to fit nicely within the bounds of the window.

Layout of the main window
Figure 2: Layout of our Window

We now need to create a sub-class of NSView since our custom view isn't very useful as only an NSView object instance. In this tutorial, I will refer to this class as "OurImageView".

Go to the classes tab and create a subclass of NSView. We do not need to add any outlets or actions since we will create our dragging destination behavior purely by overriding methods in NSView and implementing the NSDraggingDestination informal protocol later on.

Setting up our Custom class
Figure 3: Setting the custom Class

Now be sure to set the "Custom Class" attribute for your NSView to be the type of view that was just defined (this is done in the info panel for the view). Now save your changes to the NIB, generate the files for your view and move over to Project Builder.

Our new class and its files
Figure 4: Our new class and its files

 

// The Header

Before we get down to the actual code, lets modify the header to do what we want. We need to add a data member to hold the image we are displaying and we need a corresponding mutator method and accessor method for that image. Open up the OurImageView file and make sure it has an interface like the one that follows:

#import <Cocoa/Cocoa.h>

@interface OurImageView : NSView
{
    NSImage *_ourImage;
}

- (void)setImage:(NSImage *)newImage;

- (NSImage *)image;

@end


// The Code

Open the .m file for the custom view you created. Below is the code for the methods of the NSDraggingDestination informal protocol that we are implementing in our project (note that, since this is an informal protocol, we don't have to implement every method but more about that as the tutorial progresses):

When the drag operation enters the view (that is, the user drags an item over our view) we may need to update some information. Traditionally this includes the concepts such as a "focus ring" to tell the user that we are willing to accept an operation. Turning this ring on when the drag operation enters the view provides very meaningful information to the user that this view allows this operation. There are other uses for this method, but the focus ring is a classic example.

Note that you must return the type of dragging operation you are intending on performing (this is needed since the OS likes to put those little symbols beside the cursor to hint at what will happen). This will be one of: NSDragOperationCopy, NSDragOperationLink, NSDragOperationGeneric, NSDragOperationPrivate, NSDragOperationMove, NSDragOperationDelete, NSDragOperationEvery, NSDragOperationNone. In our example, we will be using the NSDragOperationGeneric as our intended operation.

This is the code that we will use for our draggingEntered: method:

- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
{
    if ((NSDragOperationGeneric & [sender draggingSourceOperationMask]) 
                == NSDragOperationGeneric)
    {
        //this means that the sender is offering the type of operation we want
        //return that we want the NSDragOperationGeneric operation that they 
            //are offering
        return NSDragOperationGeneric;
    }
    else
    {
        //since they aren't offering the type of operation we want, we have 
            //to tell them we aren't interested
        return NSDragOperationNone;
    }
}

The complement of the draggingEntered: method is draggingExited:. Following our former example of the focus ring, this is the method where you would "turn off" the ring.

We have no use for this method so we will not put any code in it. Here is our implementation, for completeness, however:

- (void)draggingExited:(id <NSDraggingInfo>)sender
{
    //we aren't particularily interested in this so we will do nothing
    //this is one of the methods that we do not have to implement
}

There is still another method that allows a greater degree of granularity in our tracking of the drag operation. This is the draggingUpdated: method. This can be used for many things depending on the application. For example, one could use this method to change the type of drag operation depending on where in the view we are thinking of dropping it. We could even use it to change the operation on a timer firing and then use this method to provide the feedback that the change occurred.

Note that this method does not have to be implemented at all and it is often the case that it is not. If you do not implement this method, the default implementation will return the same value as draggingEntered:. We will implement it here for completeness.

In our implementation, we will simply do what we did in the draggingEntered: method since we have to return the operation we want to perform. Note that, if this were a complicated operation it might be best to cache the value so not to incur high over-head during dragging. For the general case, however, this implementation should work:

- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
{
    if ((NSDragOperationGeneric & [sender draggingSourceOperationMask]) 
                    == NSDragOperationGeneric)
    {
        //this means that the sender is offering the type of operation we want
        //return that we want the NSDragOperationGeneric operation that they 
            //are offering
        return NSDragOperationGeneric;
    }
    else
    {
        //since they aren't offering the type of operation we want, we have 
            //to tell them we aren't interested
        return NSDragOperationNone;
    }
}

When the dragging operation ends over our view (that is, the user releases the mouse button) the draggingEnded: method is called. Note that this is called before the performDragOperation: method which will be called later. This implies that we are not actually supposed to perform the operation in this method. Only use this method if you need to set some state, or something, that will allow the operation to be performed.

In our above "focus ring" example, this method would also be used to turn it off since we would be done with it by now.

Our implementation of this method won't actually do anything since we don't need to do anything. It is provided solely for completeness of this tutorial:

- (void)draggingEnded:(id <NSDraggingInfo>)sender
{
    //we don't do anything in our implementation
    //this could be ommitted since NSDraggingDestination is an infomal
        //protocol and returns nothing
}

After we have handled the information that the drag operation has ended in the view, we must now allow space for any work that must be done before actually performing the operation.

prepareForDragOperation: exists for this reason. It lets us prepare for the actual operation and decide whether or not we should actually proceed with the operation.

If you find that you are ready to proceed with the operation, return YES and we will proceed to actually performing the operation, return NO to terminate the operation.

In our implementation, we don't need to check anything so we will say we are ready for the operation:

- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
{
    return YES;
}

After the drag operation ends, draggingEnded:, and prepareForDragOperation: are called, the performDragOperation: method is called to tell us it is time to perform the actual operation. Don't forget that you must return your success. This information is used by the OS to decide if it needs to tell the source anything. A good example of this is the "slide-back" feature common in the OS. If you return YES from this method, the OS terminates the operation and tells the source that you accepted. If you return NO, the OS terminates the operation and tells the source that you failed to accept. This usually incurs the common GUI effect of the dragged image sliding back to the source from where the user dropped it.

In our example, this will involve getting the image we want from sender and setting it to one of our internal variables:

- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
    NSPasteboard *paste = [sender draggingPasteboard];
        //gets the dragging-specific pasteboard from the sender
    NSArray *types = [NSArray arrayWithObjects:NSTIFFPboardType, 
                    NSFilenamesPboardType, nil];
        //a list of types that we can accept
    NSString *desiredType = [paste availableTypeFromArray:types];
    NSData *carriedData = [paste dataForType:desiredType];

    if (nil == carriedData)
    {
        //the operation failed for some reason
        NSRunAlertPanel(@"Paste Error", @"Sorry, but the past operation failed", 
            nil, nil, nil);
        return NO;
    }
    else
    {
        //the pasteboard was able to give us some meaningful data
        if ([desiredType isEqualToString:NSTIFFPboardType])
        {
            //we have TIFF bitmap data in the NSData object
            NSImage *newImage = [[NSImage alloc] initWithData:carriedData];
            [self setImage:newImage];
            [newImage release];    
                //we are no longer interested in this so we need to release it
        }
        else if ([desiredType isEqualToString:NSFilenamesPboardType])
        {
            //we have a list of file names in an NSData object
            NSArray *fileArray = 
                [paste propertyListForType:@"NSFilenamesPboardType"];
                //be caseful since this method returns id.  
                //We just happen to know that it will be an array.
            NSString *path = [fileArray objectAtIndex:0];
                //assume that we can ignore all but the first path in the list
            NSImage *newImage = [[NSImage alloc] initWithContentsOfFile:path];

            if (nil == newImage)
            {
                //we failed for some reason
                NSRunAlertPanel(@"File Reading Error", 
                    [NSString stringWithFormat:
                    @"Sorry, but I failed to open the file at \"%@\"",
                    path], nil, nil, nil);
                return NO;
            }
            else
            {
                //newImage is now a new valid image
                [self setImage:newImage];
            }
            [newImage release];
        }
        else
        {
            //this can't happen
            NSAssert(NO, @"This can't happen");
            return NO;
        }
    }
    [self setNeedsDisplay:YES];    //redraw us with the new image
    return YES;
}

When a drag operation is finished (that is, once we have performed whatever operations we wanted) we might want to perform some kind of clean-up operation. The protocol defines the concludeDragOperation: method for this reason. It will be called after performDragOperation: if it returned YES.

In our example, this method will be used to simply issue an instruction to re-draw the view.

- (void)concludeDragOperation:(id <NSDraggingInfo>)sender
{
    //re-draw the view with our new data
    [self setNeedsDisplay:YES];
}

Don't be confused by the (id <NSDraggingInfo>) type of the sender in these methods. This notation means that the sender is expected to be any object (denoted by the id) that implements the NSDraggingInfo formal protocol.

Now, this isn't enough to simply implement this protocol and assume that everything will work as planned. We still need to tell someone that we are interested in receiving drag & drop events and we need to tell them what types of information we are willing to receive. So, who is this mysterious object that we should tell this to? Well, it turns out to be either our window or our view.

These are the methods that must be over-ridden in our NSView sub-class:

First of all, we need to over-ride our init method to initialize our variables. Since this is a sub-class of NSView, that init method is initWithFrame:.

Of specific relevance to this tutorial (and any view that accepts drag operations), we must call the NSView registerForDraggedTypes: method with an array of pasteboard types we are listening for.

Now, for our program, we want to accept images being dragged into the view both from other programs and from the Finder. Note that this requires us to register for two types of objects: NSTIFFPboardType and NSFilenamesPboardType (note that there are different pasteboards for the fundamentally different types of image data but we will stick to the TIFF one since it works for bitmaps, in general). These two pasteboards will allow us to receive someone dropping an actual file on the view and dragging a bitmap from another program into our view.

That said, here is the code that we need for our new initWithFrame: method:

- (id)initWithFrame:(NSRect)frame
{
    self = [super initWithFrame:frame];
    [self registerForDraggedTypes:[NSArray arrayWithObjects:NSTIFFPboardType, 
        NSFilenamesPboardType, nil]];
    return self;
}

Since we told our view to listen to types for drag operations in the init method, it is customary to tell it to ignore them in the the dealloc method (since these are both correspondingly complementary methods). We do this by calling the NSView unregisterDraggedTypes method.

Keeping this in mind, here is the code for our dealloc method:

- (void)dealloc
{
    [self unregisterDraggedTypes];
    [super dealloc];
}

Note that we also have to over-ride the drawRect: method to actually draw our image in the view.

In our implementation we must first call our super-class and then we will proceed to draw our image in the view. We won't do any sort of centering or scaling since it isn't related to the task at hand but it is not too difficult to add.

Be careful when using compositeToPoint:operation: since we may be using images with alpha channels. Be sure to read what the operation constants actually mean, since some will have the effect of not properly honouring alpha channels. For example, while preparing this tutorial, I had forgotten this issue and used NSCompositeCopy which drew the troll image above with a black background rather than a transparent one. After changing this to NSCompositeSourceOver, however, the background drew transparently as it should.

- (void)drawRect:(NSRect)rect
{
    NSRect ourBounds = [self bounds];
    NSImage *image = [self image];
    [super drawRect:rect];
    [image compositeToPoint:(ourBounds.origin) operation:NSCompositeSourceOver];
}

Also, we need to implement the mutator and accessor methods for our image data member:

- (void)setImage:(NSImage *)newImage
{
    NSImage *temp = [newImage retain];
    [_ourImage release];
    _ourImage = temp;
}
- (NSImage *)image
{
    return _ourImage;
}

 

// A Note About Design

Mutators and accessors are frequently over-looked by programmers yet they are a very powerful aspect of Object-Oriented-Design which requires so little work.

Without going into too much detail, here is an illustration of the power we have given our design with these two simple methods. If we want to create a view, later on, that does what this one does yet we want to check if it is safe to set the image we are using (this may be important with, for example, multi-threading). In our design, we simply have to create a sub-class that overrides our setImage: method to perform this check. Now, as long as we used this accessor whenever we needed to access the image in the class that defined it, we will now have implemented this functionality.

 

// Conclusion

Drag and Drop, as you have seen, is not too difficult to include in a project thanks to the design used in Cocoa. The most difficult part of it becomes ensuring that it actually helps the user use your product. Keeping that in mind, it is often a good idea to keep the use of drag and drop operations fairly simple and intuitive. It is a good idea to look at the behavior of other applications and try to keep your user experience similar to that of other applications that they use.


Comments

I love the tutorial, and I agree with you, Drag and drop Destinations do scream Macintosh. Ever since I updated to the december 2002 version of Dev. Tools, I've been able to sucessfully use the Cocoa Header file and the Foundation frameworks, this actually let me compile my applications (Woohoo!) Anyways, I'm having a problem During Compilation. In the building window i get the error "Method definition not in class context" error... In the code viewer at the bottom it highlites the

- (NSDragOperation)draggingEntered:(id )senderdraggingSourceOperationMask])
-- Basically the first line of code is problematic...
What can I do about this?

Also,
I was wondering if all the code on this page goes into the OurImageView.m file or if any goes into the Main.m file. Thanks a lot and I hope you respond to me, seeing as this is an old Tutorial. Thanks a lot.

Posted by: Rory Swanson on August 16, 2003 05:37 PM

Considering you talk about the focus ring so much I would have liked to have seen it implemented in the tutorial -- but I guess I'll take it as a personal challenge and try to do it on my own. I think I know how.

Thanks again!

To Rory: Yes, most of the code on this page goes in the .m file, except for code in the part at the top called "header" that goes in the .h file

Posted by: CyberZorn on September 3, 2003 11:07 PM

I've joined your blog today.

Posted by: zip codes on September 6, 2003 04:12 AM

I'm a bit new to Cocoa programming, so this was an excellent tutorial on how drag'n'drop works. Nice and easy.

Like Cyberzorn, I would like to know how to draw the focus ring. Is there a generic way to draw the various cocoa view borders? (i.e. something i can call from drawRect:)

thanks again.

Posted by: brian on November 7, 2003 12:26 PM
Post a comment