From a2b7a0cd6d07efccc59b346673c2ac3145aeef88 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 18 Feb 2026 11:20:17 +0530 Subject: [PATCH] Port Cocoa drag source backend to new API --- glfw/cocoa_platform.h | 7 - glfw/cocoa_window.m | 696 +++++++++++++++++------------------------- glfw/input.c | 6 +- glfw/internal.h | 4 +- glfw/null_window.c | 24 +- glfw/wl_platform.h | 1 + glfw/wl_window.c | 7 +- glfw/x11_window.c | 4 +- kitty/glfw.c | 8 +- 9 files changed, 298 insertions(+), 459 deletions(-) diff --git a/glfw/cocoa_platform.h b/glfw/cocoa_platform.h index 9d28eafa9..7faeb7296 100644 --- a/glfw/cocoa_platform.h +++ b/glfw/cocoa_platform.h @@ -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 diff --git a/glfw/cocoa_window.m b/glfw/cocoa_window.m index c487210d4..84d08ddfa 100644 --- a/glfw/cocoa_window.m +++ b/glfw/cocoa_window.m @@ -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 { - 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 { } - -- (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 +@interface GLFWContentView : NSView { _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 *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* 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* 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; +} diff --git a/glfw/input.c b/glfw/input.c index e26e4df18..0fa150392 100644 --- a/glfw/input.c +++ b/glfw/input.c @@ -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; } diff --git a/glfw/internal.h b/glfw/internal.h index 624d09784..b2ed61b28 100644 --- a/glfw/internal.h +++ b/glfw/internal.h @@ -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); diff --git a/glfw/null_window.c b/glfw/null_window.c index 9d90a791f..0c32cd40e 100644 --- a/glfw/null_window.c +++ b/glfw/null_window.c @@ -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) { diff --git a/glfw/wl_platform.h b/glfw/wl_platform.h index 8accfbe20..e53975424 100644 --- a/glfw/wl_platform.h +++ b/glfw/wl_platform.h @@ -419,6 +419,7 @@ typedef struct _GLFWlibraryWayland size_t sz, offset; } *data_requests; size_t count, capacity; + GLFWDragOperationType action; } drag; } _GLFWlibraryWayland; diff --git a/glfw/wl_window.c b/glfw/wl_window.c index 5ce601a63..92109f464 100644 --- a/glfw/wl_window.c +++ b/glfw/wl_window.c @@ -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) diff --git a/glfw/x11_window.c b/glfw/x11_window.c index c9741945e..94f132de1 100644 --- a/glfw/x11_window.c +++ b/glfw/x11_window.c @@ -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) {} diff --git a/kitty/glfw.c b/kitty/glfw.c index 581069c82..200f074bc 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -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();