Chapter 20. Drag-and-Drop

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.

Make BigLetterView Be a Drag Source

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.

Completed Application

Figure 20.1. Completed Application

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:

- (unsigned int)draggingSourceOperationMaskForLocal:(BOOL)isLocal
{
    return NSDragOperationCopy; 
}

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.)

After the Drop

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.

Make BigLetterView Be a Drag Destination

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.

registerForDraggedTypes

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

Add Highlighting

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

Implement the Dragging Destination Methods

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

Testing

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).

Set nextKeyView for Each BigLetterView

Figure 20.2. Set nextKeyView for Each BigLetterView

Build and run the application. Note that you can drag characters between the views and from other applications.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset