NSTextView -cleanUpAfterDragOperation Being Called When Dragging Session Is Not Finished

I have an NSTextView subclass and implements drag and drop for custom draggable data, so I override -writablePasteboardTypes and add my own type as described in the header file:

// Returns an array of pasteboard types that can be provided from the current selection.  Overriders should copy the result from super and add their own new types.
@property (readonly, copy) NSArray<NSPasteboardType> *writablePasteboardTypes;

Now my textview also accepts the drop. Drag and drop can be used to move the custom data to a different location in the text view's character range. Like grabbing a block of text and moving it.

So I accept the drop in -readSelectionFromPasteboard:type:

I have a variable I cache at the start of dragging like:

_myDraggingItem = // Set at the start of dragging.

Then when I accept the drop I just use _myDraggingItem to move it to the drop location in the text view. I don't need to actually serialize the entire object and write it on the pasteboard I can just use _myDraggingItem to move from the source location to destination location. This is local only drag and drop.

it works, except when the mouse leaves the text view briefly during the dragging session. This is because I override NSTextView's - (void)cleanUpAfterDragOperation

// If you set up persistent state that should go away when the drag operation finishes, you can clean it up here.  Such state is usually set up in -dragOperationForDraggingInfo:type:.  You should probably never need to call this except to message super in an override.
- (void)cleanUpAfterDragOperation;

So documentation indicates -cleanUpAfterDragOperation is for clean up after drag operation finishes so I nil out _myDraggingItem here. But -cleanUpAfterDragOperation is getting called from [NSDragDestination _draggingExited]. -draggingExited: means the drag location moved outside the view it does not mean that the dragging session is over. The drag can move back inside the view after briefly exiting, so this isn't a usable place to clean up state tied to the dragging session as the header file indicates.

I must override -draggingEnded: instead.

If that sounds like a bug let me know.

Answered by Frameworks Engineer in 891527022

I think you've got the right read on this, and your fix sounds correct to me.

The confusing part is what "drag operation" means here. It's really about the destination's interaction with your view, i.e. the stretch between the drag entering and leaving, not the whole dragging session. The text view sets up some per-visit state when the drag enters (like the insertion indicator) and tears it back down when the drag leaves so that indicator goes away. That teardown happens in -draggingExited:, so it can fire several times in one session as the pointer wanders out and back in. It also runs on -concludeDragOperation: and -draggingEnded:.

So -cleanUpAfterDragOperation is more of a per-visit cleanup than a per-session one. Good fit for state you create in -dragOperationForDraggingInfo:type:, but not really for anything that's supposed to live for the whole drag, like your cached object.

One thing worth noting: on a successful drop the ordering still works out, since -performDragOperation: (where your -readSelectionFromPasteboard:type: reads the cached item) runs before the final cleanup. So the item's still there when you need it. The only trouble is that transient exit clearing it out partway through.

Since you set the item at the start of the drag, that's really source-session state, so I'd release it from the dragging-source callback instead:

- (void)draggingSession:(NSDraggingSession *)session
           endedAtPoint:(NSPoint)screenPoint
              operation:(NSDragOperation)operation {
    _myDraggingItem = nil;
}

That one gets called once at the end of the session no matter where the drop happened, and it lines up with where you set the state up in the first place. Overriding -draggingEnded: would work too, but the source callback feels like a more natural home for source-scoped state.

So I don't think it's a bug, just -cleanUpAfterDragOperation doing what it's meant to. Moving the session-lifetime state to a session-scoped hook should sort it out.

Accepted Answer

I think you've got the right read on this, and your fix sounds correct to me.

The confusing part is what "drag operation" means here. It's really about the destination's interaction with your view, i.e. the stretch between the drag entering and leaving, not the whole dragging session. The text view sets up some per-visit state when the drag enters (like the insertion indicator) and tears it back down when the drag leaves so that indicator goes away. That teardown happens in -draggingExited:, so it can fire several times in one session as the pointer wanders out and back in. It also runs on -concludeDragOperation: and -draggingEnded:.

So -cleanUpAfterDragOperation is more of a per-visit cleanup than a per-session one. Good fit for state you create in -dragOperationForDraggingInfo:type:, but not really for anything that's supposed to live for the whole drag, like your cached object.

One thing worth noting: on a successful drop the ordering still works out, since -performDragOperation: (where your -readSelectionFromPasteboard:type: reads the cached item) runs before the final cleanup. So the item's still there when you need it. The only trouble is that transient exit clearing it out partway through.

Since you set the item at the start of the drag, that's really source-session state, so I'd release it from the dragging-source callback instead:

- (void)draggingSession:(NSDraggingSession *)session
           endedAtPoint:(NSPoint)screenPoint
              operation:(NSDragOperation)operation {
    _myDraggingItem = nil;
}

That one gets called once at the end of the session no matter where the drop happened, and it lines up with where you set the state up in the first place. Overriding -draggingEnded: would work too, but the source callback feels like a more natural home for source-scoped state.

So I don't think it's a bug, just -cleanUpAfterDragOperation doing what it's meant to. Moving the session-lifetime state to a session-scoped hook should sort it out.

NSTextView -cleanUpAfterDragOperation Being Called When Dragging Session Is Not Finished
 
 
Q