Drag-and-drop is little more than a flashy copy-and-paste. When the drag starts, some data are copied onto the dragging pasteboard. When the drop occurs, the data are read off the dragging pasteboard. The only thing that makes this technique trickier than copy-and-paste is that users need feedback: an image that appears as they drag, a view that becomes highlighted when they drag into it, and maybe a big gulping sound when they drop the image.
Several different things can happen when data are dragged from one application to another: Nothing may happen, a copy of the data may be created, or a link to the existing data may be created. Constants represent these operations:
NSDragOperationNone NSDragOperationCopy NSDragOperationLink
There are several other operations that you see less frequently:
NSDragOperationGeneric NSDragOperationPrivate NSDragOperationMove NSDragOperationDelete NSDragOperationEvery
Both the source and the destination must agree on the operation that will occur when the user drops the image.
When you add drag-and-drop to a view, there are two distinct parts of the change:
Make it a drag source.
Make it a drag destination.
Let's take these steps separately. First, you will make your view be a drag source. When that is working, you will make it be a drag destination.
When you finish this section, you will be able to drag a letter off the BigLetterView
and drop it into any text editor. It will look like Figure 20.1.
To be a drag source, your view must implement draggingSourceOperationMask ForLocal:
. This method declares what operations the view is willing to participate in as a source. Add the following method to your BigLetterView.m
:
This method is automatically called twice: once with isLocal
as YES
, which determines what operations it is willing to participate in for destinations within your application, and a second time with isLocal
as NO
, which determines what operations it is willing to participate in for destinations in other applications.
To start a drag operation, you will use a method on NSView
:
- (void)dragImage:(NSImage *)anImage at:(NSPoint)imageLoc offset:(NSSize)mouseOffset event:(NSEvent *)theEvent pasteboard:(NSPasteboard *)pboard source:(id)sourceObject slideBack:(BOOL)slideBack
You will supply it with the image to be dragged and the point at which you want the drag to begin. The documentation says to include the mouseDown
event, but a mouseDragged
event works well, too. The offset seems to be completely ignored. The pasteboard is usually the standard drag pasteboard. If the drop does not occur, you can choose whether the icon should slide back to the place from which it came.
You will also need to create an image to drag. You can draw on an image just as you can on a view. To make the drawing appear on the image instead of the screen, you must first lock focus on the image. When the drawing is complete, you must unlock the focus.
Here is the whole method to add to BigLetterView.m
:
- (void)mouseDragged:(NSEvent *)event { NSRect imageBounds; NSPasteboard *pb; NSImage *anImage; NSSize s; NSPoint p; // Get the size of the string s = [string sizeWithAttributes:attributes]; // Create the image that will be dragged anImage = [[NSImage alloc] initWithSize:s]; // Create a rect in which you will draw the letter // in the image imageBounds.origin = NSMakePoint(0,0); imageBounds.size = s; // Draw the letter on the image [anImage lockFocus]; [self drawStringCenteredIn:imageBounds]; [anImage unlockFocus]; // Get the location of the drag event p = [self convertPoint:[event locationInWindow] fromView:nil]; // Drag from the center of the image p.x = p.x - s.width/2; p.y = p.y - s.height/2; // Get the pasteboard pb = [NSPasteboard pasteboardWithName:NSDragPboard]; // Put the string on the pasteboard [self writeStringToPasteboard:pb]; // Start the drag [self dragImage:anImage at:p offset:NSMakeSize(0, 0) event:event pasteboard:pb source:self slideBack:YES]; [anImage release]; }
That's it. Build and run the application. You should be able to drag a letter off the view and into any text editor. (Try dragging it into Xcode.)
When a drop occurs, the drag source will be notified if you implement the following method:
- (void)draggedImage:(NSImage *)image endedAt:(NSPoint)screenPoint operation:(NSDragOperation)operation;
For example, to make it possible to clear the BigLetterView
by dragging the letter to the trashcan in the dock, advertise your willingness in draggingSourceOperationMaskForLocal:
- (unsigned int)draggingSourceOperationMaskForLocal:(BOOL) is Local { return NSDragOperationCopy | NSDragOperationDelete; }
Then implement draggedImage:endedAt:operation:
- (void)draggedImage:(NSImage *)image endedAt:(NSPoint)screenPoint operation:(NSDragOperation)operation { if (operation == NSDragOperationDelete) { [self setString:@" "]; } }
Build and run the application. Drag a letter into the trashcan. It should disappear from the view.
There are several parts to being a drag destination. First, you need to declare your view to be a destination for the dragging of certain types. NSView
has a method for this purpose:
- (void)registerForDraggedTypes:(NSArray *)pboardTypes
You typically call this method in your initWithFrame:
method.
Then you need to implement six methods. (Yes, six!) All six methods have the same argument: an NSDraggingInfo
object. It has the dragging pasteboard. The six methods are invoked as follows:
As the image is dragged into the destination, the destination is sent a draggingEntered:
message. Often, the destination view updates its appearance. For example, it might highlight itself.
While the image remains within the destination, a series of draggingUpdated:
messages are sent. Implementing draggingUpdated:
is optional.
If the image is dragged outside the destination, draggingExited:
is sent.
If the image is released on the destination, either it slides back to its source (and breaks the sequence) or a prepareForDragOperation:
message is sent to the destination, depending on the value returned by the most recent invocation of draggingEntered:
(or draggingUpdated:
if the view implemented it).
If the prepareForDragOperation:
message returns YES
, then a performDragOperation:
message is sent. This is typically where the application actually reads data off the pasteboard.
Finally, if performDragOperation:
returned YES
, concludeDragOperation:
is sent. The appearance may change. This is where you might generate the big gulping sound that implies a successful drop.
Add a call to registerForDraggedTypes:
to the initWithFrame:
method in BigLetterView.m
:
- (id)initWithFrame:(NSRect)rect { if (self = [super initWithFrame:rect]) { NSLog(@"initializing view"); [self prepareAttributes]; [self setBgColor:[NSColor yellowColor]]; [self setString:@" "]; [self registerForDraggedTypes: [NSArray arrayWithObject:NSStringPboardType]]; } return self; }
To signal the user that the drop is acceptable, your view will highlight itself. Add a highlighted
instance variable to BigLetterView.h
:
@interface BigLetterView : NSView
{
NSColor *bgColor;
NSString *string;
NSMutableDictionary *attributes;
BOOL highlighted;
}
...
Add highlighting to drawRect:
.
- (void)drawRect:(NSRect)rect { NSRect bounds = [self bounds]; // Draw white background if highlighted if (highlighted) { [[NSColor whiteColor] set]; } else { [bgColor set]; } [NSBezierPath fillRect:bounds]; // Draw the string [self drawStringCenteredIn:bounds]; // Draw blue rectangle if first responder if ([[self window] firstResponder] == self) { [[NSColor keyboardFocusIndicatorColor] set]; [NSBezierPath setDefaultLineWidth:4.0]; [NSBezierPath strokeRect:bounds]; } }
So far, we have seen two ways to declare a pointer to an object. If the pointer can refer to any type of object, we would declare it like this:
id foo;
If the pointer should refer to an instance of a particular class, we can declare it like this:
MyClass *foo;
A third possibility also exists. If we have a pointer that should refer to an object that conforms to a particular protocol, we can declare it like this:
id <MyProtocol> foo;
NSDraggingInfo
is actually a protocol, not a class. All of the dragging destination methods expect an object that conforms to the NSDraggingInfo
protocol.
Add the following methods to BigLetterView.m
:
- (unsigned int)draggingEntered:(id <NSDraggingInfo>)sender { NSLog(@"draggingEntered:"); if ([sender draggingSource] != self) { NSPasteboard *pb = [sender draggingPasteboard]; NSString *type = [pb availableTypeFromArray: [NSArray arrayWithObject:NSStringPboardType]]; if (type != nil) { highlighted = YES; [self setNeedsDisplay:YES]; return NSDragOperationCopy; } } return NSDragOperationNone; } - (void)draggingExited:(id <NSDraggingInfo>)sender { NSLog(@"draggingExited:"); highlighted = NO; [self setNeedsDisplay:YES]; } - (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender { return YES; } - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender { NSPasteboard *pb = [sender draggingPasteboard]; if(![self readStringFromPasteboard:pb]) { NSLog(@"Error: Could not read from dragging pasteboard"); return NO; } return YES; } - (void)concludeDragOperation:(id <NSDraggingInfo>)sender { NSLog(@"concludeDragOperation:"); highlighted = NO; [self setNeedsDisplay:YES]; }
Open the nib file, and add another BigLetterView
to the window. Delete the text fields. Make sure to set the nextKeyView
for each BigLetterView
so that you can tab between them (Figure 20.2).
Build and run the application. Note that you can drag characters between the views and from other applications.