14
votes

You know that effect when you drag out an item from the dock and that cloud drag cursor appears and when you let go it disappears with a poof effect? Similarly, in Xcode when you drag a breakpoint outside the line number gutter the same happens.

I would like to implement the same effect in my application but can't find the right way.

I have an NSImageView descendant to implement the NSDraggingSource and NSDraggingDestination protocols. I have several instances of this view which allow to drag their content between the others (a copy operation takes place in this scenario, but that's only relevant to show I have drag'n drop implmented and fully working for standard tasks).

Now, when I drag out an image from its view to anywhere (except another view instance) I want to have the delete operation taking place on drop. However the drag operation is fully controlled by the target view. I could manage to make them respond the way I want (even though this would be a lot of work), but it fails completely if I'm dragging outside my application.

If I could get the delete drag operation I could handle this however easily by:

- (void)draggedImage: (NSImage *)image
             endedAt: (NSPoint)screenPoint
           operation: (NSDragOperation)operation
{
    if (operation == NSDragOperationDelete) {
        NSRect rect = [self.window convertRectToScreen: [self convertRect: self.frame fromView: nil]];
        NSShowAnimationEffect(NSAnimationEffectPoof, rect.origin, self.bounds.size, nil, nil, NULL);
    }
}

I tried already to set the delete cursor like this:

- (void)draggingSession: (NSDraggingSession *)session
           movedToPoint: (NSPoint)screenPoint
{
    if (!NSPointInRect(screenPoint, self.window.frame)) {
        [[NSCursor disappearingItemCursor] set];
    }
}

(for simplicity this is for the entire windw at the moment). This works as long as I don't hit the desktop or a finder window. In starts flickering, probably because the Finder concurrently sets its own drag cursor. It is completely without effect when I hit the dock. This also happens when I define my own pasteboard data type.

Additionally, any other drop enabled view in my application will still accept my drag data (e.g. NSTextView) which I don't want to happen (I'm writing an NSURL to the dragging pasteboard with a custom scheme).

Update:

I've come a few steps further. As Peter already indicated it is essential to handle draggingSession:sourceOperationmaskForDraggingContext: which looks so in my code:

- (NSDragOperation)       draggingSession: (NSDraggingSession *)session
    sourceOperationMaskForDraggingContext: (NSDraggingContext)context;
{
    switch(context) {
        case NSDraggingContextOutsideApplication:
            return NSDragOperationDelete;
            break;

        case NSDraggingContextWithinApplication:
        default:
            return NSDragOperationDelete | NSDragOperationMove;
            break;
    }
}

This solves 2 problems: 1) outside the application the drag operation is not accepted at all, 2) it keeps all standard views from accepting this operation too (because NSOutlineView, NSTextView etc. don't handle the given drag operations). Additionally, I created an own pasteboard datatype, but this doesn't seem to be necessary. Still it's clearer to have an own one.

Unfortunately, dropping outside of my NSImageView descendant (both within and outside the application) does not give me NSDragOperationDelete in draggedImage:endedAt:operation: (what I specified above) but NSDragOperationNone. Additionally the drag cursor when moving the mouse outside the application is the not-allowed one, not the disappearing-item. So, if someone could solve these two things I'd accept it as answer to my question.

5
Please edit your question to show your dragging source's implementation of the draggingSession:sourceOperationMaskForDraggingContext: method.Peter Hosey

5 Answers

2
votes

There may be a less hacky way to do this, but I can think of one possibility: once the drag begins, create a transparent, borderless window the size of the desktop to be a dummy drag destination. You may need to call -setIgnoresMouseEvents: with NO to allow it to receive the drop even though it's transparent. You'll also have to set its window level above the menu bar (NSMainMenuWindowLevel + 1) to make sure that drags to the menu bar or Dock are still intercepted by your window.

As a drag destination, this window will have to check if one of your image views is under the cursor. You can use +[NSWindow windowNumberAtPoint:belowWindowWithWindowNumber:] to find the window below your transparent overlay window which is under the cursor. Then use -[NSApplication windowWithWindowNumber:] to determine if it's one of your app's windows and, if so, call -[NSView hitTest:] on its content view (converting the cursor coordinates as appropriate) to find the view. You can then forward NSDraggingDestination methods to that view as desired.

2
votes

My guess is that NSDragOperationDelete only concerns drag/drops targeting the dock's Trash, and nothing else.

NSDragOperationGeneric should be a better fit. Be sure not to mix methods, if you're going the 10.7 route, prefer :

-(void)draggingSession:(NSDraggingSession *)session endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation
1
votes

I just finished implementing something very similar to what you've described. Your code is quite similar to mine with a few exceptions:

Where you use draggedImage:endedAt:operation: I'm using draggingSession:session:endedAt:operation:. Also, in this method, I do not check the operation as all of my operations are set to generic. This is also where I perform the actual delete, so I only show the poof animation if the delete is successful.

In your draggingSession:session:movedToPoint:, you may also want to set the session's animatesToStartingPositionsOnCancelOrFail to false when the point is outside the window (this is also when you set the disappearingItemCursor) and set to true otherwise. This adds a final touch that once the deletion operation is completed, the dragged image doesn't rebound back to it's originating source location.

As for why you are not seeing the proper cursors, my guess is that you are using a Pasteboard type that other things (Finder, etc.) are willing to accept. I know you said you created your own Pastboard datatypes, but I would double-check that they are being used. When I made my own types, I gained control over the cursor. At least, I've not found anything that contends for my custom type.

0
votes

OS X 10.7 or better:

- (void) draggingEnded: (id<NSDraggingInfo>) aInfo {
    /*! Delete the current leaf if it is no longer represented in the subviews. */
    RETURN_IF ( , NSNotFound != [self.visibleLeafs indexOfObject: iSelectedControl.representedObject] );
    iIgnoreLeafsObservation = YES;
    [self.leafs removeObject: iSelectedControl.representedObject];
    iIgnoreLeafsObservation = NO;
}

This assumes that you have visually removed the object already. You could also set a flag during draggingEntered: and draggingExited: to indicate if drag session was last inside or outside of self.

In this program, leafs is the actual observed collection while visibleLeafs is a simulated collection of the representedObject of each visible control. Other NSDraggingDestination overloads present what will happen visually before it actually happens.

-3
votes

You can use the following command to make the poof appears at the mouse location:

NSShowAnimationEffect(NSAnimationEffectPoof, [NSEvent mouseLocation], NSZeroSize, NULL, NULL, NULL);

See the Application Kit Functions Reference for further information.