Port Cocoa drag source backend to new API

This commit is contained in:
Kovid Goyal 2026-02-18 11:20:17 +05:30
parent e5eb63fcd0
commit a2b7a0cd6d
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
9 changed files with 298 additions and 459 deletions

View file

@ -178,12 +178,6 @@ typedef struct _GLFWwindowNS
// Cached MIME types from drag enter (for move events)
_GLFWDropData drop_data;
// Pending drag source data requests (for cleanup on cancellation)
// Current drag operation type for NSDraggingSource
int dragOperations; // Bitfield of GLFWDragOperationType
GLFWDragSourceData** pendingDragSourceData;
int pendingDragSourceDataCount;
int pendingDragSourceDataCapacity;
} _GLFWwindowNS;
// Cocoa-specific global data
@ -221,7 +215,6 @@ typedef struct _GLFWlibraryNS
// the callback to handle url open events
GLFWhandleurlopen url_open_callback;
} _GLFWlibraryNS;
// Cocoa-specific per-monitor data

View file

@ -781,211 +781,31 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
// File Promise Provider Delegate for async drag data {{{
// Structure to hold async drag state
typedef struct {
void (^completionHandler)(NSError*); // Completion block to call
NSFileHandle* fileHandle; // File handle for writing
bool finished; // Whether writing is complete
int errorCode; // Error code if any
} GLFWFilePromiseState;
// Helper function to clean up a single drag source data
static void
cleanup_ns_drag_source_data(GLFWDragSourceData* data) {
if (!data) return;
if (data->platform_data) {
GLFWFilePromiseState* state = (GLFWFilePromiseState*)data->platform_data;
// If the data wasn't finished, call completion with error
if (!data->finished && state->completionHandler) {
NSError* error = [NSError errorWithDomain:NSPOSIXErrorDomain code:ECANCELED userInfo:nil];
state->completionHandler(error);
Block_release(state->completionHandler);
state->completionHandler = nil;
}
if (state->fileHandle) {
@try {
[state->fileHandle closeFile];
} @catch (NSException* e) {
(void)e;
}
[state->fileHandle release];
state->fileHandle = nil;
}
free(state);
}
free(data->mime_type);
free(data);
}
// Add a drag source data to the pending array for tracking
static bool
add_ns_pending_drag_source_data(_GLFWwindow* window, GLFWDragSourceData* data) {
if (!window || !data) return false;
// Grow array if needed
if (window->ns.pendingDragSourceDataCount >= window->ns.pendingDragSourceDataCapacity) {
// Cap maximum capacity to prevent excessive memory use
if (window->ns.pendingDragSourceDataCapacity >= 512) {
return false;
}
int new_capacity = window->ns.pendingDragSourceDataCapacity ? window->ns.pendingDragSourceDataCapacity * 2 : 4;
GLFWDragSourceData** new_array = realloc(window->ns.pendingDragSourceData,
new_capacity * sizeof(GLFWDragSourceData*));
if (!new_array) return false;
window->ns.pendingDragSourceData = new_array;
window->ns.pendingDragSourceDataCapacity = new_capacity;
}
window->ns.pendingDragSourceData[window->ns.pendingDragSourceDataCount++] = data;
return true;
}
// Remove a specific drag source data from the pending array
static void
remove_ns_pending_drag_source_data(_GLFWwindow* window, GLFWDragSourceData* data) {
if (!window || !data) return;
for (int i = 0; i < window->ns.pendingDragSourceDataCount; i++) {
if (window->ns.pendingDragSourceData[i] == data) {
// Shift remaining elements
for (int j = i; j < window->ns.pendingDragSourceDataCount - 1; j++) {
window->ns.pendingDragSourceData[j] = window->ns.pendingDragSourceData[j + 1];
}
window->ns.pendingDragSourceDataCount--;
return;
}
}
}
// Clean up all pending drag source data for a window
static void
cleanup_all_ns_pending_drag_source_data(_GLFWwindow* window) {
if (!window) return;
for (int i = 0; i < window->ns.pendingDragSourceDataCount; i++) {
cleanup_ns_drag_source_data(window->ns.pendingDragSourceData[i]);
}
free(window->ns.pendingDragSourceData);
window->ns.pendingDragSourceData = NULL;
window->ns.pendingDragSourceDataCount = 0;
window->ns.pendingDragSourceDataCapacity = 0;
}
@interface GLFWFilePromiseProviderDelegate : NSObject <NSFilePromiseProviderDelegate>
{
GLFWid windowId;
GLFWid windowId, instanceId;
char* mimeType; // MIME type for this provider
NSFileHandle *file_handle;
NSURL *file_url;
void (^completion_handler)(NSError*);
}
- (instancetype)initWithWindow:(_GLFWwindow*)initWindow mimeType:(const char*)mime;
- (instancetype)initWithWindow:(_GLFWwindow*)initWindow mimeType:(const char*)mime instanceId:(GLFWid)iid;
- (void)request_drag_data;
- (void)end_transfer:(int)errorCode;
- (void)end_transfer_with_error:(NSError*)err;
- (bool)is_mimetype:(const char*)mime_type;
@end
@implementation GLFWFilePromiseProviderDelegate
- (instancetype)initWithWindow:(_GLFWwindow*)initWindow mimeType:(const char*)mime {
self = [super init];
if (self) {
windowId = initWindow ? initWindow->id : 0;
mimeType = _glfw_strdup(mime);
}
return self;
@interface GLFWDraggingSource : NSObject <NSDraggingSource> {
}
- (void)dealloc {
free(mimeType);
[super dealloc];
}
- (NSString*)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider fileNameForType:(NSString*)fileType {
(void)filePromiseProvider;
(void)fileType;
// Generate a unique filename based on the MIME type
NSString* extension = @"data";
if (mimeType) {
UTType *type = [UTType typeWithMIMEType:@(mimeType)];
extension = type.preferredFilenameExtension;
}
return [NSString stringWithFormat:@"kitty-drag-%@.%@", [[NSUUID UUID] UUIDString], extension];
}
- (void)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider
writePromiseToURL:(NSURL*)url
completionHandler:(void (^)(NSError*))completionHandler {
(void)filePromiseProvider;
// Get the window from the ID
_GLFWwindow* window = _glfwWindowForId(windowId);
if (!window) {
completionHandler([NSError errorWithDomain:NSPOSIXErrorDomain code:EINVAL userInfo:nil]);
return;
}
// Create the file
NSError* error = nil;
if (![[NSFileManager defaultManager] createFileAtPath:url.path contents:nil attributes:nil]) {
error = [NSError errorWithDomain:NSPOSIXErrorDomain code:EIO userInfo:nil];
completionHandler(error);
return;
}
NSFileHandle* fileHandle = [NSFileHandle fileHandleForWritingToURL:url error:&error];
if (!fileHandle) {
completionHandler(error);
return;
}
// Create the drag source data with file promise state
GLFWDragSourceData* source_data = calloc(1, sizeof(GLFWDragSourceData));
if (!source_data) {
[fileHandle closeFile];
completionHandler([NSError errorWithDomain:NSPOSIXErrorDomain code:ENOMEM userInfo:nil]);
return;
}
// Create the file promise state
char *mt = _glfw_strdup(mimeType);
GLFWFilePromiseState* state = calloc(1, sizeof(GLFWFilePromiseState));
if (!state || !mt) {
free(source_data); free(mt); free(state);
[fileHandle closeFile];
completionHandler([NSError errorWithDomain:NSPOSIXErrorDomain code:ENOMEM userInfo:nil]);
return;
}
state->completionHandler = [completionHandler copy];
state->fileHandle = [fileHandle retain];
state->finished = false;
state->errorCode = 0;
source_data->window_id = windowId;
source_data->mime_type = mt;
source_data->write_fd = -1;
source_data->finished = false;
source_data->error_code = 0;
source_data->platform_data = state;
// Track this source data for cleanup on cancellation
if (!add_ns_pending_drag_source_data(window, source_data)) {
// Call completion handler with memory error before cleanup
completionHandler([NSError errorWithDomain:NSPOSIXErrorDomain code:ENOMEM userInfo:nil]);
// Mark as finished to prevent cleanup_ns_drag_source_data from calling completionHandler again
source_data->finished = true;
cleanup_ns_drag_source_data(source_data);
return;
}
// Notify the application via callback - this will trigger glfwSendDragData calls
_glfwInputDragSourceRequest(window, mimeType, source_data);
// Note: The completion handler will be called from glfwSendDragData when finished
// If the application didn't finish (sync callback didn't complete), we need to handle it
// The platform_data still holds the state for async completion
}
@end
// }}}
// Content view class for the GLFW window {{{
@interface GLFWContentView : NSView <NSTextInputClient, NSDraggingSource>
@interface GLFWContentView : NSView <NSTextInputClient>
{
_GLFWwindow* window;
NSTrackingArea* trackingArea;
@ -995,11 +815,12 @@ cleanup_all_ns_pending_drag_source_data(_GLFWwindow* window) {
bool marked_text_cleared_by_insert;
int in_key_handler;
NSString *input_source_at_last_key_event;
GLFWDraggingSource *dragging_source;
}
- (void) removeGLFWWindow;
- (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow;
- (GLFWDraggingSource*)draggingSource;
@end
@implementation GLFWContentView
@ -1012,6 +833,7 @@ cleanup_all_ns_pending_drag_source_data(_GLFWwindow* window) {
window = initWindow;
trackingArea = nil;
input_context = [[GLFWTextInputContext alloc] initWithClient:self];
dragging_source = [[GLFWDraggingSource alloc] init];
markedText = [[NSMutableAttributedString alloc] init];
markedRect = NSMakeRect(0.0, 0.0, 0.0, 0.0);
input_source_at_last_key_event = nil;
@ -1040,34 +862,23 @@ cleanup_all_ns_pending_drag_source_data(_GLFWwindow* window) {
{
[trackingArea release];
[markedText release];
[dragging_source release];
if (input_source_at_last_key_event) [input_source_at_last_key_event release];
[input_context release];
[super dealloc];
}
- (void) removeGLFWWindow
{
window = NULL;
}
- (void) removeGLFWWindow { window = NULL; }
- (_GLFWwindow*)glfwWindow {
return window;
}
- (GLFWDraggingSource*)draggingSource { return dragging_source; }
- (BOOL)isOpaque
{
return window && [window->ns.object isOpaque];
}
- (_GLFWwindow*)glfwWindow { return window; }
- (BOOL)canBecomeKeyView
{
return YES;
}
- (BOOL)isOpaque { return window && [window->ns.object isOpaque]; }
- (BOOL)acceptsFirstResponder
{
return YES;
}
- (BOOL)canBecomeKeyView { return YES; }
- (BOOL)acceptsFirstResponder { return YES; }
- (void) viewWillStartLiveResize
{
@ -1625,10 +1436,7 @@ update_drop_state(_GLFWwindow *window, size_t mime_count) {
// Pre-allocate C array for MIME types
const char** mime_array = (const char**)calloc(max_types, sizeof(const char*));
if (!mime_array) {
int accepted = _glfwInputDragEvent(window, GLFW_DRAG_ENTER, xpos, ypos, NULL, NULL);
return accepted ? NSDragOperationGeneric : NSDragOperationNone;
}
if (!mime_array) return NSDragOperationNone;
size_t mime_count = 0;
@ -1877,37 +1685,6 @@ _glfwPlatformEndDrop(GLFWwindow *w UNUSED, GLFWDragOperationType op UNUSED) {
}
// }}}
// NSDraggingSource protocol methods
- (NSDragOperation)draggingSession:(NSDraggingSession *)session
sourceOperationMaskForDraggingContext:(NSDraggingContext)context
{
(void)session;
(void)context;
// Return the operation based on the stored drag operations bitfield
NSDragOperation ops = 0;
if (window->ns.dragOperations & GLFW_DRAG_OPERATION_COPY)
ops |= NSDragOperationCopy;
if (window->ns.dragOperations & GLFW_DRAG_OPERATION_MOVE)
ops |= NSDragOperationMove;
if (window->ns.dragOperations & GLFW_DRAG_OPERATION_GENERIC)
ops |= NSDragOperationGeneric;
return ops ? ops : NSDragOperationCopy;
}
- (void)draggingSession:(NSDraggingSession *)session
endedAtPoint:(NSPoint)screenPoint
operation:(NSDragOperation)operation
{
(void)session;
(void)screenPoint;
if (operation == NSDragOperationNone) { // drag was canceled
// Clean up all pending drag source data
cleanup_all_ns_pending_drag_source_data(window);
// Notify the application that the drag source is closed
_glfwInputDragSourceRequest(window, NULL, NULL);
}
}
- (BOOL)hasMarkedText
{
return [markedText length] > 0;
@ -4099,201 +3876,278 @@ void _glfwCocoaPostEmptyEvent(void) {
[NSApp postEvent:event atStart:YES];
}
// Drag source implementation {{{
@implementation GLFWDraggingSource
- (NSDragOperation)draggingSession:(NSDraggingSession*)session
sourceOperationMaskForDraggingContext:(NSDraggingContext)context
{
(void)session; (void)context;
// Return the operation based on the stored drag operations bitfield
NSDragOperation ops = NSDragOperationCopy;
int q = _glfw.drag.operations;
if (q & GLFW_DRAG_OPERATION_COPY) ops |= NSDragOperationCopy;
if (q & GLFW_DRAG_OPERATION_MOVE) ops |= NSDragOperationMove;
if (q & GLFW_DRAG_OPERATION_GENERIC) ops |= NSDragOperationGeneric;
return ops;
}
- (void)draggingSession:(NSDraggingSession *)session
willBeginAtPoint:(NSPoint)screenPoint
{
(void)session; (void)screenPoint;
}
- (void)draggingSession:(NSDraggingSession *)session
movedToPoint:(NSPoint)screenPoint
{
(void)session; (void)screenPoint;
}
- (void)draggingSession:(NSDraggingSession *)session
endedAtPoint:(NSPoint)screenPoint
operation:(NSDragOperation)operation
{
(void)session; (void)screenPoint;
_GLFWwindow *window = _glfwWindowForId(_glfw.drag.window_id);
if (window) {
GLFWDragEvent ev = {0};
switch(operation) {
case NSDragOperationCopy: case NSDragOperationLink: ev.action = GLFW_DRAG_OPERATION_COPY; break;
case NSDragOperationMove: case NSDragOperationDelete: ev.action = GLFW_DRAG_OPERATION_MOVE; break;
case NSDragOperationNone: break;
default: ev.action = GLFW_DRAG_OPERATION_GENERIC; break;
}
ev.type = (operation == NSDragOperationNone) ? GLFW_DRAG_CANCELLED : GLFW_DRAG_DROPPED;
_glfwInputDragSourceRequest(window, &ev);
if (operation == NSDragOperationNone) _glfwFreeDragSourceData();
}
}
@end
static NSMutableArray<GLFWFilePromiseProviderDelegate*> *file_promise_providers = nil;
int
_glfwPlatformStartDrag(_GLFWwindow* window, const GLFWDragSourceItem *items, int item_count, const GLFWimage* thumbnail, int operations) {
// cleanup stored data from previous drag
cleanup_all_ns_pending_drag_source_data(window);
if (!items || !item_count) { cleanup_all_ns_pending_drag_source_data(window); return 0; }
// Store the operations for the dragging source callback
window->ns.dragOperations = operations;
@autoreleasepool {
// Create dragging items array - one NSFilePromiseProvider per MIME type
NSMutableArray<NSDraggingItem*>* dragItems = [[NSMutableArray alloc] init];
for (int i = 0; i < mime_count; i++) {
NSString* utiString = mime_to_uti(mime_types[i]);
_glfwPlatformStartDrag(_GLFWwindow* window, const GLFWimage* thumbnail) {@autoreleasepool{
if (file_promise_providers) {
for (NSInteger i = [file_promise_providers count] - 1; i >= 0; i--) {
GLFWFilePromiseProviderDelegate* d = file_promise_providers[i];
[d end_transfer:EINVAL];
}
}
NSMutableArray<NSDraggingItem*>* dragItems = [[[NSMutableArray alloc] init] autorelease];
for (size_t i = 0; i < _glfw.drag.item_count; i++) {
NSString* utiString = mime_to_uti(_glfw.drag.items[i].mime_type);
id w;
if (_glfw.drag.items[i].optional_data) {
NSPasteboardItem *pbItem = [[[NSPasteboardItem alloc] init] autorelease];
NSData *data = [NSData dataWithBytes:_glfw.drag.items[i].optional_data length:_glfw.drag.items[i].data_size];
[pbItem setData:data forType:utiString];
w = pbItem;
} else {
// Create file promise provider with our delegate
GLFWFilePromiseProviderDelegate* delegate = [[GLFWFilePromiseProviderDelegate alloc]
initWithWindow:window mimeType:mime_types[i]];
NSFilePromiseProvider* provider = [[NSFilePromiseProvider alloc]
initWithFileType:utiString delegate:delegate];
GLFWFilePromiseProviderDelegate* delegate = [[[GLFWFilePromiseProviderDelegate alloc]
initWithWindow:window mimeType:_glfw.drag.items[i].mime_type instanceId:_glfw.drag.instance_id] autorelease];
NSFilePromiseProvider *provider = [[[NSFilePromiseProvider alloc]
initWithFileType:utiString delegate:delegate] autorelease];
// Store the delegate in the provider's user info so it's retained
provider.userInfo = delegate;
w = provider;
}
NSDraggingItem* dragItem = [[[NSDraggingItem alloc] initWithPasteboardWriter:w] autorelease];
// Create the dragging item
NSDraggingItem* dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter:provider];
if (i == 0 && thumbnail && thumbnail->pixels) {
// Create NSImage from thumbnail for the first item
NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:NULL
pixelsWide:thumbnail->width
pixelsHigh:thumbnail->height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bytesPerRow:thumbnail->width * 4
bitsPerPixel:32];
if (i == 0 && thumbnail && thumbnail->pixels) {
// Create NSImage from thumbnail for the first item
NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:NULL
pixelsWide:thumbnail->width
pixelsHigh:thumbnail->height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bytesPerRow:thumbnail->width * 4
bitsPerPixel:32];
if (imageRep) {
memcpy([imageRep bitmapData], thumbnail->pixels,
thumbnail->width * thumbnail->height * 4);
NSImage* image = [[NSImage alloc] initWithSize:
NSMakeSize(thumbnail->width, thumbnail->height)];
if (imageRep) {
memcpy([imageRep bitmapData], thumbnail->pixels, thumbnail->width * thumbnail->height * 4);
NSWindow *nsw = window->ns.object;
CGFloat scaleFactor = [nsw backingScaleFactor];
if (scaleFactor == 0) scaleFactor = [NSScreen mainScreen].backingScaleFactor;
NSSize pointSize = NSMakeSize(thumbnail->width / scaleFactor, thumbnail->height / scaleFactor);
[imageRep setSize:pointSize];
NSImage* image = [[NSImage alloc] initWithSize: pointSize];
if (image) {
[image addRepresentation:imageRep];
[dragItem setDraggingFrame:NSMakeRect(0, 0, thumbnail->width, thumbnail->height)
contents:image];
[dragItem setDraggingFrame:NSMakeRect(0, 0, image.size.width, image.size.height) contents:image];
[image release];
}
} else {
[dragItem setDraggingFrame:NSMakeRect(0, 0, 32, 32) contents:nil];
[imageRep release];
}
[dragItems addObject:dragItem];
} else {
[dragItem setDraggingFrame:NSMakeRect(0, 0, 32, 32) contents:nil];
}
// Start the drag session - try current event first, then create a synthetic one
NSEvent* event = [NSApp currentEvent];
if (!event || ([event type] != NSEventTypeLeftMouseDown &&
[event type] != NSEventTypeLeftMouseDragged)) {
// Create a synthetic left mouse down event using stored cursor position
// Convert window coordinates to screen coordinates
NSRect contentRect = [window->ns.view frame];
NSPoint windowPos = NSMakePoint(window->virtualCursorPosX,
contentRect.size.height - window->virtualCursorPosY);
event = [NSEvent mouseEventWithType:NSEventTypeLeftMouseDown
location:windowPos
modifierFlags:0
timestamp:[[NSProcessInfo processInfo] systemUptime]
windowNumber:[window->ns.object windowNumber]
context:nil
eventNumber:0
clickCount:1
pressure:1.0];
}
if (event) {
[window->ns.view beginDraggingSessionWithItems:dragItems
event:event
source:window->ns.view];
return 0;
}
return EIO;
[dragItems addObject:dragItem];
}
// Start the drag session - try current event first, then create a synthetic one
NSEvent* event = [NSApp currentEvent];
if (!event || ([event type] != NSEventTypeLeftMouseDown &&
[event type] != NSEventTypeLeftMouseDragged)) {
// Create a synthetic left mouse down event using stored cursor position
// Convert window coordinates to screen coordinates
NSRect contentRect = [window->ns.view frame];
NSPoint windowPos = NSMakePoint(window->virtualCursorPosX,
contentRect.size.height - window->virtualCursorPosY);
event = [NSEvent mouseEventWithType:NSEventTypeLeftMouseDown
location:windowPos
modifierFlags:0
timestamp:[[NSProcessInfo processInfo] systemUptime]
windowNumber:[window->ns.object windowNumber]
context:nil
eventNumber:0
clickCount:1
pressure:1.0];
}
if (event) {
GLFWContentView *v = window->ns.view;
[v beginDraggingSessionWithItems:dragItems event:event source:[v draggingSource]];
return 0;
}
return EIO;
}}
@implementation GLFWFilePromiseProviderDelegate
- (void)end_transfer_with_error:(NSError*)err {
if (err && file_url) {
NSError *error;
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager removeItemAtURL:file_url error:&error];
}
if (file_handle) [file_handle release];
file_handle = nil;
if (completion_handler) {
completion_handler(err);
Block_release(completion_handler);
completion_handler = nil;
}
[file_promise_providers removeObject:self];
}
ssize_t
_glfwPlatformSendDragData(GLFWDragSourceData* source_data, const void* data, size_t size) {
if (!source_data || source_data->finished) return -EINVAL;
if (!source_data->platform_data) return -EINVAL;
- (void)end_transfer:(int)errorCode {
[self end_transfer_with_error:errorCode ? [NSError errorWithDomain:NSPOSIXErrorDomain code:errorCode userInfo:nil] : nil];
}
GLFWFilePromiseState* state = (GLFWFilePromiseState*)source_data->platform_data;
- (bool)is_mimetype:(const char*)q { return strcmp(q, mimeType) == 0; }
// End of data: NULL data pointer and size zero
if (!data && size == 0) {
source_data->finished = true;
// Close the file handle
@try {
[state->fileHandle closeFile];
} @catch (NSException* e) {
(void)e;
}
// Call the completion handler with success
if (state->completionHandler) {
state->completionHandler(nil);
Block_release(state->completionHandler);
state->completionHandler = nil;
}
// Remove from pending list and clean up
_GLFWwindow* window = _glfwWindowForId(source_data->window_id);
if (window) {
remove_ns_pending_drag_source_data(window, source_data);
}
// source_data->finished is true, so cleanup_ns_drag_source_data won't call completionHandler again
cleanup_ns_drag_source_data(source_data);
return 0;
}
// Error from application: NULL data pointer and size is error code
if (!data && size > 0) {
source_data->finished = true;
source_data->error_code = (int)size;
state->errorCode = (int)size;
// Close the file handle
@try {
[state->fileHandle closeFile];
} @catch (NSException* e) {
(void)e;
}
// Call the completion handler with error
if (state->completionHandler) {
NSError* error = [NSError errorWithDomain:NSPOSIXErrorDomain code:size userInfo:nil];
state->completionHandler(error);
Block_release(state->completionHandler);
state->completionHandler = nil;
}
// Remove from pending list and clean up
_GLFWwindow* window = _glfwWindowForId(source_data->window_id);
if (window) {
remove_ns_pending_drag_source_data(window, source_data);
}
// source_data->finished is true, so cleanup_ns_drag_source_data won't call completionHandler again
cleanup_ns_drag_source_data(source_data);
return 0;
}
// Write data to the file - Cocoa file operations are typically synchronous
// but we return the number of bytes written to match the non-blocking interface
if (state->fileHandle) {
@try {
NSData* nsData = [NSData dataWithBytes:data length:size];
if (@available(macOS 10.15, *)) {
- (void)request_drag_data {
if (instanceId != _glfw.drag.instance_id) { [self end_transfer:EINVAL]; return; }
_GLFWwindow *window = _glfwWindowForId(_glfw.drag.window_id);
if (!window) { [self end_transfer:EINVAL]; return; }
bool keep_going = true;
while (keep_going) {
GLFWDragEvent ev = {.type=GLFW_DRAG_DATA_REQUEST, .mime_type=mimeType};
_glfwInputDragSourceRequest(window, &ev);
if (ev.err_num) {
keep_going = false;
if (ev.err_num != EAGAIN) [self end_transfer:ev.err_num];
} else {
if (ev.data_sz) {
NSData* nsData = [NSData dataWithBytes:ev.data length:ev.data_sz];
NSError* error = nil;
if (![state->fileHandle writeData:nsData error:&error]) {
int errCode = error ? (int)error.code : EIO;
if (errCode == 0) errCode = EIO; // Ensure we have a valid error code
source_data->error_code = errCode;
_glfwInputError(GLFW_PLATFORM_ERROR,
"Cocoa: Failed to write drag data: %s",
error ? [[error localizedDescription] UTF8String] : "unknown error");
return -errCode;
if (![file_handle writeData:nsData error:&error]) {
keep_going = false;
[self end_transfer_with_error:error];
}
} else {
// Pre-10.15 writeData: writes all bytes synchronously or throws an exception.
// NSFileHandle.writeData: is documented to write all data atomically,
// so returning size is correct. Any failure throws NSFileHandleOperationException.
[state->fileHandle writeData:nsData];
keep_going = false;
[self end_transfer_with_error:nil];
}
// NSFileHandle.writeData writes all data atomically, so size == bytes written
return (ssize_t)size;
} @catch (NSException* e) {
source_data->error_code = EIO;
_glfwInputError(GLFW_PLATFORM_ERROR,
"Cocoa: Exception writing drag data: %s",
e ? [[e reason] UTF8String] : "unknown exception");
return -EIO;
_glfwInputDragSourceRequest(window, &ev);
}
}
// No file handle, consider all data accepted
return (ssize_t)size;
}
- (instancetype)initWithWindow:(_GLFWwindow*)initWindow mimeType:(const char*)mime instanceId:(GLFWid) instance_id {
self = [super init];
if (self) {
windowId = initWindow ? initWindow->id : 0;
mimeType = _glfw_strdup(mime);
instanceId = instance_id;
if (file_promise_providers == nil) file_promise_providers = [NSMutableArray array];
[file_promise_providers addObject:self];
}
return self;
}
- (void)dealloc {
free(mimeType); mimeType = NULL;
if (file_url) [file_url release];
file_url = nil;
[self end_transfer:EINVAL];
[super dealloc];
}
- (NSString*)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider fileNameForType:(NSString*)fileType {
(void)filePromiseProvider; (void)fileType;
// Generate a unique filename based on the MIME type
NSString* extension = @"data";
if (mimeType) {
UTType *type = [UTType typeWithMIMEType:@(mimeType)];
extension = type.preferredFilenameExtension;
}
return [NSString stringWithFormat:@"kitty-drag-source-%@.%@", [[NSUUID UUID] UUIDString], extension];
}
- (void)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider
writePromiseToURL:(NSURL*)url
completionHandler:(void (^)(NSError*))completionHandler
{
(void)filePromiseProvider;
_GLFWwindow* window = _glfwWindowForId(windowId);
if (!window || instanceId != _glfw.drag.instance_id) {
completionHandler([NSError errorWithDomain:NSPOSIXErrorDomain code:EINVAL userInfo:nil]);
return;
}
// Create the file
NSError* error = nil;
if (![[NSFileManager defaultManager] createFileAtPath:url.path contents:nil attributes:nil]) {
error = [NSError errorWithDomain:NSPOSIXErrorDomain code:EIO userInfo:nil];
completionHandler(error);
return;
}
NSFileHandle* fileHandle = [NSFileHandle fileHandleForWritingToURL:url error:&error];
if (!fileHandle) {
completionHandler(error);
NSError *error;
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager removeItemAtURL:url error:&error];
return;
}
file_handle = fileHandle; completion_handler = completionHandler;
file_url = [url retain];
[self request_drag_data];
}
@end
void
_glfwPlatformFreeDragSourceData(void) { }
int
_glfwPlatformDragDataReady(const char *mime_type) {
if (!file_promise_providers) return 0;
for (GLFWFilePromiseProviderDelegate *d in file_promise_providers) {
if ([d is_mimetype:mime_type]) [d request_drag_data];
}
return 0;
}

6
glfw/input.c vendored
View file

@ -1155,7 +1155,9 @@ _glfwFreeDragSourceData(void) {
}
free(_glfw.drag.items);
}
GLFWid iid = _glfw.drag.instance_id;
memset(&_glfw.drag, 0, sizeof(_glfw.drag));
_glfw.drag.instance_id = iid;
}
GLFWAPI int
@ -1165,6 +1167,7 @@ glfwStartDrag(GLFWwindow* handle, const GLFWDragSourceItem *items, size_t item_c
_GLFW_REQUIRE_INIT_OR_RETURN(EINVAL);
if (operations == -1) return _glfwPlatformDragDataReady(items[0].mime_type);
_glfwFreeDragSourceData();
_glfw.drag.instance_id++;
if (!items || !item_count) return 0;
_glfw.drag.items = calloc(item_count, sizeof(_glfw.drag.items[0]));
if (!_glfw.drag.items) return ENOMEM;
@ -1183,7 +1186,8 @@ glfwStartDrag(GLFWwindow* handle, const GLFWDragSourceItem *items, size_t item_c
_glfw.drag.items[i].data_size = items[i].data_size;
}
_glfw.drag.window_id = window->id;
int ans = _glfwPlatformStartDrag(window, thumbnail, operations);
_glfw.drag.operations = operations;
int ans = _glfwPlatformStartDrag(window, thumbnail);
if (ans != 0) _glfwFreeDragSourceData();
return ans;
}

4
glfw/internal.h vendored
View file

@ -667,7 +667,7 @@ struct _GLFWlibrary
struct {
GLFWDragSourceItem *items; size_t item_count;
GLFWid window_id;
GLFWid window_id, instance_id; int operations;
} drag;
};
@ -832,7 +832,7 @@ ssize_t _glfwPlatformReadAvailableDropData(GLFWwindow *w, GLFWDropEvent *ev, cha
void _glfwPlatformEndDrop(GLFWwindow *w, GLFWDragOperationType op);
int _glfwPlatformRequestDropData(_GLFWwindow *window, const char *mime);
// Platform functions for drag source
int _glfwPlatformStartDrag(_GLFWwindow* window, const GLFWimage* thumbnail, int operations);
int _glfwPlatformStartDrag(_GLFWwindow* window, const GLFWimage* thumbnail);
void _glfwFreeDragSourceData(void);
void _glfwPlatformFreeDragSourceData(void);
void _glfwInputDragSourceRequest(_GLFWwindow* window, GLFWDragEvent *ev);

24
glfw/null_window.c vendored
View file

@ -535,27 +535,13 @@ void _glfwPlatformCancelDrag(_GLFWwindow* window UNUSED)
{
// No-op for null platform
}
int _glfwPlatformStartDrag(_GLFWwindow* window UNUSED,
const char* const* mime_types UNUSED,
int mime_count UNUSED,
const GLFWimage* thumbnail UNUSED,
int operations UNUSED)
{
int
_glfwPlatformStartDrag(_GLFWwindow* window, const GLFWimage* thumbnail) {
(void)window; (void)thumbnail;
return ENOTSUP;
}
ssize_t _glfwPlatformSendDragData(GLFWDragSourceData* source_data UNUSED,
const void* data UNUSED,
size_t size UNUSED)
{
return -ENOTSUP;
}
void _glfwPlatformUpdateDragState(_GLFWwindow* window UNUSED)
{
// No-op for null platform
}
void _glfwPlatformFreeDragSourceData(void) {}
int _glfwPlatformDragDataReady(const char *mime_type) { (void) mime_type; return 0; }
const char** _glfwPlatformGetDropMimeTypes(GLFWDropData* drop UNUSED, int* count)
{

1
glfw/wl_platform.h vendored
View file

@ -419,6 +419,7 @@ typedef struct _GLFWlibraryWayland
size_t sz, offset;
} *data_requests;
size_t count, capacity;
GLFWDragOperationType action;
} drag;
} _GLFWlibraryWayland;

7
glfw/wl_window.c vendored
View file

@ -3217,6 +3217,7 @@ drag_source_action(void *data UNUSED, struct wl_data_source *source UNUSED, uint
case WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY: op = GLFW_DRAG_OPERATION_COPY; break;
case WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE: op = GLFW_DRAG_OPERATION_MOVE; break;
}
_glfw.wl.drag.action = op;
GLFWDragEvent ev = {.type=GLFW_DRAG_ACTION_CHANGED, .action=op};
_glfwInputDragSourceRequest(window, &ev);
} else drag_source_cancelled(data, source);
@ -3235,7 +3236,7 @@ static void
drag_source_dnd_finished(void *data UNUSED, struct wl_data_source *source UNUSED) {
_GLFWwindow *window = _glfwWindowForId(_glfw.drag.window_id);
if (window) {
GLFWDragEvent ev = {.type=GLFW_DRAG_FINSHED};
GLFWDragEvent ev = {.type=GLFW_DRAG_FINSHED, .action=_glfw.wl.drag.action};
_glfwInputDragSourceRequest(window, &ev);
}
_glfwFreeDragSourceData();
@ -3268,7 +3269,7 @@ _glfwPlatformFreeDragSourceData(void) {
}
int
_glfwPlatformStartDrag(_GLFWwindow* window, const GLFWimage* thumbnail, int operations) {
_glfwPlatformStartDrag(_GLFWwindow* window, const GLFWimage* thumbnail) {
if (!_glfw.wl.dataDeviceManager) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Data device manager not available");
return ENOTSUP;
@ -3287,7 +3288,7 @@ _glfwPlatformStartDrag(_GLFWwindow* window, const GLFWimage* thumbnail, int oper
}
// Set the DND action based on operation type (bitfield)
uint32_t wl_actions = 0;
uint32_t wl_actions = 0; int operations = _glfw.drag.operations;
if (operations & GLFW_DRAG_OPERATION_COPY)
wl_actions |= WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY;
if (operations & GLFW_DRAG_OPERATION_MOVE)

4
glfw/x11_window.c vendored
View file

@ -3815,8 +3815,8 @@ GLFWAPI int glfwSetX11LaunchCommand(GLFWwindow *handle, char **argv, int argc)
// Drag source {{{
int
_glfwPlatformStartDrag(_GLFWwindow* window, const GLFWimage* thumbnail, int operations) {
(void)window; (void)thumbnail; (void)operations;
_glfwPlatformStartDrag(_GLFWwindow* window, const GLFWimage* thumbnail) {
(void)window; (void)thumbnail;
return ENOTSUP;
}
void _glfwPlatformFreeDragSourceData(void) {}

View file

@ -2751,15 +2751,15 @@ start_drag_with_data(PyObject *self UNUSED, PyObject *args, PyObject *kw) {
&os_window_id, &PyDict_Type, &data_map, &thumbnail_data, &thumbnail_sz, &width, &height, &operations)) return NULL;
OSWindow *w = os_window_for_id(os_window_id);
if (!w || !w->handle) { PyErr_SetString(PyExc_KeyError, "OS Window with specified id does not exist"); return NULL; }
RAII_ALLOC(GLFWDragSourceItem, items, calloc(PyDict_Size(data_map), sizeof(const char*)));
RAII_ALLOC(GLFWDragSourceItem, items, calloc(PyDict_Size(data_map), sizeof(GLFWDragSourceItem)));
if (!items) { PyErr_NoMemory(); return NULL; }
PyObject *key, *value; Py_ssize_t pos = 0; size_t num = 0;
while (PyDict_Next(data_map, &pos, &key, &value)) {
if (!PyUnicode_Check(key)) { PyErr_SetString(PyExc_TypeError, "data_map must have string keys"); return NULL; }
if (!PyBytes_Check(value)) { PyErr_SetString(PyExc_TypeError, "data_map must have bytes values"); return NULL; }
items[num].mime_type = PyUnicode_AsUTF8(key);
items[num].optional_data = PyBytes_AS_STRING(value); items[num].data_size = PyBytes_GET_SIZE(value);
num++;
GLFWDragSourceItem *item = items + num++;
item->mime_type = PyUnicode_AsUTF8(key);
item->optional_data = PyBytes_AS_STRING(value); item->data_size = PyBytes_GET_SIZE(value);
}
GLFWimage thumbnail = {.pixels=thumbnail_data, .width=width, .height=height};
free_drag_source();