diff --git a/glfw/cocoa_window.m b/glfw/cocoa_window.m index 976abc6d7..b79c561a0 100644 --- a/glfw/cocoa_window.m +++ b/glfw/cocoa_window.m @@ -1521,11 +1521,8 @@ - (BOOL)performDragOperation:(id )sender drop_data->eof_reached = false; drop_data->current_data = NULL; drop_data->data_offset = 0; - _glfwInputDrop(window, drop_data); - // Note: drop_data is NOT freed here - application must call glfwFinishDrop - return YES; } @@ -3884,9 +3881,7 @@ void _glfwPlatformUpdateDragState(_GLFWwindow* window) { ssize_t _glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, size_t capacity, monotonic_t timeout UNUSED) { - if (!drop || !mime || !buffer || capacity == 0) return -EINVAL; - - NSPasteboard* pasteboard = (__bridge NSPasteboard*)drop->platform_data; + NSPasteboard* pasteboard = (NSPasteboard*)drop->platform_data; if (!pasteboard) return -EINVAL; // Check if the MIME type is available @@ -3906,14 +3901,12 @@ void _glfwPlatformUpdateDragState(_GLFWwindow* window) { drop->current_data = NULL; } drop->data_offset = 0; + free(drop->current_mime); drop->current_mime = NULL; } // If we need to fetch data for this MIME type - if (drop->current_data == NULL || drop->current_mime == NULL || - strcmp(drop->current_mime, mime) != 0) { - + if (drop->current_data == NULL || drop->current_mime == NULL) { NSData* data = nil; - // Handle special MIME types if (strcmp(mime, "text/uri-list") == 0) { NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES}; @@ -3943,11 +3936,9 @@ void _glfwPlatformUpdateDragState(_GLFWwindow* window) { } } } - if (!data) return -ENOENT; - drop->current_data = [data retain]; - drop->current_mime = mime; + drop->current_mime = _glfw_strdup(mime); drop->data_offset = 0; } @@ -3955,23 +3946,19 @@ void _glfwPlatformUpdateDragState(_GLFWwindow* window) { NSData* data = (NSData*)drop->current_data; NSUInteger dataLength = [data length]; - if (drop->data_offset >= dataLength) { - return 0; // EOF - } - + if (drop->data_offset >= dataLength) return 0; // EOF NSUInteger remaining = dataLength - drop->data_offset; NSUInteger to_read = (remaining < capacity) ? remaining : capacity; [data getBytes:buffer range:NSMakeRange(drop->data_offset, to_read)]; drop->data_offset += to_read; - return (ssize_t)to_read; } void _glfwPlatformFinishDrop(GLFWDropData* drop, GLFWDragOperationType operation UNUSED, bool success UNUSED) { if (!drop) return; - + free(drop->current_mime); drop->current_mime = NULL; // Release the retained current data if (drop->current_data) { [(NSData*)drop->current_data release]; diff --git a/glfw/input.c b/glfw/input.c index 33c3191d8..4a1a61845 100644 --- a/glfw/input.c +++ b/glfw/input.c @@ -1161,11 +1161,7 @@ GLFWAPI const char** glfwGetDropMimeTypes(GLFWDropData* drop, int* count) GLFWAPI ssize_t glfwReadDropData(GLFWDropData* drop, const char* mime, void* buffer, size_t capacity, monotonic_t timeout) { - assert(drop != NULL); - assert(mime != NULL); - assert(buffer != NULL); - assert(capacity > 0); - + if (drop == NULL || mime == NULL || buffer == NULL || capacity < 1) return -EINVAL; _GLFW_REQUIRE_INIT_OR_RETURN(-1); return _glfwPlatformReadDropData(drop, mime, buffer, capacity, timeout); } diff --git a/glfw/internal.h b/glfw/internal.h index d7a8b0be4..b58cc155b 100644 --- a/glfw/internal.h +++ b/glfw/internal.h @@ -90,7 +90,7 @@ struct GLFWDropData { const char** mime_types; // Array of available MIME types (owned by drop object) int mime_count; // Number of MIME types int mime_array_size; // Original array size for proper cleanup - const char* current_mime; // Currently being read MIME type + char* current_mime; // Currently being read MIME type int read_fd; // File descriptor for reading data (Wayland/X11) size_t bytes_read; // Total bytes read so far for current mime void* platform_data; // Platform-specific data (offer for Wayland, pasteboard for Cocoa) diff --git a/glfw/wl_window.c b/glfw/wl_window.c index 0cf71b41b..948d0a66f 100644 --- a/glfw/wl_window.c +++ b/glfw/wl_window.c @@ -2522,14 +2522,14 @@ static void drag_leave(void *data UNUSED, struct wl_data_device *wl_data_device } } -static void drop(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED) { +static void +drop(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED) { for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) { if (_glfw.wl.dataOffers[i].offer_type == DRAG_AND_DROP) { _GLFWWaylandDataOffer *offer = &_glfw.wl.dataOffers[i]; _GLFWwindow* window = _glfw.windowListHead; - while (window) - { + while (window) { if (window->wl.surface == offer->surface) { // Heap-allocate drop data structure for chunked reading // The application is responsible for freeing this via glfwFinishDrop @@ -2543,26 +2543,20 @@ static void drop(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED drop_data->mime_types = offer->mimes; drop_data->mime_count = (int)offer->mimes_count; drop_data->mime_array_size = (int)offer->mimes_count; - // Clear offer's references since drop object now owns the mimes - offer->mimes = NULL; - offer->mimes_count = 0; - drop_data->current_mime = NULL; drop_data->read_fd = -1; drop_data->bytes_read = 0; - drop_data->platform_data = offer; // Store the offer for later use + drop_data->platform_data = offer->id; // Store the offer for later use + offer->mimes = NULL; offer->id = NULL; + destroy_data_offer(offer); drop_data->eof_reached = false; - + memset(offer, 0, sizeof(offer[0])); _glfwInputDrop(window, drop_data); - // Note: drop_data is NOT freed here - application must call glfwFinishDrop break; } window = window->next; } - - // Note: We no longer destroy the offer here as the drop_data holds a reference - // The offer will be destroyed when glfwFinishDrop is called break; } } @@ -3189,10 +3183,7 @@ _glfwPlatformGetDropMimeTypes(GLFWDropData* drop, int* count) { ssize_t _glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, size_t capacity, monotonic_t timeout) { - if (!drop || !mime || !buffer || capacity == 0) return -EINVAL; - - _GLFWWaylandDataOffer *offer = (_GLFWWaylandDataOffer*)drop->platform_data; - if (!offer || !offer->id) return -EINVAL; + if (!drop->platform_data) return -EINVAL; // Check if the MIME type is available bool mime_found = false; @@ -3212,27 +3203,23 @@ _glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, si } drop->bytes_read = 0; drop->eof_reached = false; + free(drop->current_mime); drop->current_mime = NULL; } // If we've reached EOF for this MIME type, return 0 - if (drop->eof_reached && drop->current_mime && strcmp(drop->current_mime, mime) == 0) { - return 0; - } + if (drop->eof_reached && drop->current_mime && strcmp(drop->current_mime, mime) == 0) return 0; // Open a new pipe if we don't have one for this MIME type - if (drop->read_fd < 0 || drop->current_mime == NULL || strcmp(drop->current_mime, mime) != 0) { + if (drop->read_fd < 0 || drop->current_mime == NULL) { int pipefd[2]; if (pipe2(pipefd, O_CLOEXEC | O_NONBLOCK) != 0) return -EIO; - - wl_data_offer_receive(offer->id, mime, pipefd[1]); + wl_data_offer_receive(drop->platform_data, mime, pipefd[1]); close(pipefd[1]); - - // Round trip to ensure the compositor processes the receive request - wl_display_roundtrip(_glfw.wl.display); - + // Flush to ensure the compositor processes the receive request + wl_display_flush(_glfw.wl.display); drop->read_fd = pipefd[0]; // mime points to drop->mime_types entry, valid for duration of drop callback - drop->current_mime = mime; + drop->current_mime = _glfw_strdup(mime); drop->bytes_read = 0; drop->eof_reached = false; } @@ -3264,20 +3251,20 @@ _glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, si } // Read data from the pipe - ssize_t bytes_read = read(drop->read_fd, buffer, capacity); + ssize_t bytes_read = -1; + errno = EINTR; + while (errno == EINTR) { errno = 0; bytes_read = read(drop->read_fd, buffer, capacity); } if (bytes_read < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { return -ETIME; // No data available yet } return -EIO; } - if (bytes_read == 0) { // EOF reached drop->eof_reached = true; return 0; } - drop->bytes_read += bytes_read; return bytes_read; } @@ -3285,6 +3272,7 @@ _glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, si void _glfwPlatformFinishDrop(GLFWDropData* drop, GLFWDragOperationType operation UNUSED, bool success UNUSED) { if (!drop) return; + free(drop->current_mime); drop->current_mime = NULL; // Close any open file descriptor if (drop->read_fd >= 0) { @@ -3293,19 +3281,15 @@ _glfwPlatformFinishDrop(GLFWDropData* drop, GLFWDragOperationType operation UNUS } // Destroy the associated data offer + // Note: Wayland doesn't have a way to report the operation type or success back to the source // in the same way as X11, as the source is notified through other means - _GLFWWaylandDataOffer* offer = (_GLFWWaylandDataOffer*)drop->platform_data; - if (offer) { - destroy_data_offer(offer); - } + if (drop->platform_data) wl_data_offer_destroy(drop->platform_data); + drop->platform_data = NULL; // Free the mime types array (owned by drop object) if (drop->mime_types) { - for (int i = 0; i < drop->mime_array_size; i++) { - if (drop->mime_types[i]) - free((char*)drop->mime_types[i]); - } + for (int i = 0; i < drop->mime_array_size; i++) free((char*)drop->mime_types[i]); free(drop->mime_types); drop->mime_types = NULL; } diff --git a/glfw/x11_window.c b/glfw/x11_window.c index 01f2e9813..e5f4e3697 100644 --- a/glfw/x11_window.c +++ b/glfw/x11_window.c @@ -3883,8 +3883,6 @@ _glfwPlatformGetDropMimeTypes(GLFWDropData* drop, int* count) { ssize_t _glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, size_t capacity, monotonic_t timeout) { - if (!drop || !mime || !buffer || capacity == 0) return -EINVAL; - // Check if the MIME type is available bool mime_found = false; for (int i = 0; i < drop->mime_count; i++) { @@ -3903,15 +3901,12 @@ _glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, si } drop->x11_data_size = 0; drop->data_offset = 0; - drop->current_mime = NULL; + free(drop->current_mime); drop->current_mime = NULL; } // If we need to fetch data for this MIME type - if (drop->current_data == NULL || drop->current_mime == NULL || - strcmp(drop->current_mime, mime) != 0) { - + if (drop->current_data == NULL || drop->current_mime == NULL) { Atom target_atom = XInternAtom(_glfw.x11.display, mime, False); - // Request the data via XConvertSelection XConvertSelection(_glfw.x11.display, _glfw.x11.XdndSelection, @@ -3963,7 +3958,7 @@ _glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, si drop->x11_data_size = size; drop->data_offset = 0; // mime points to drop->mime_types entry, valid for duration of drop callback - drop->current_mime = mime; + drop->current_mime = _glfw_strdup(mime); got_data = true; } else { if (data) XFree(data); @@ -3983,22 +3978,18 @@ _glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, si } // Read data from buffer - if (drop->data_offset >= drop->x11_data_size) { - return 0; // EOF - } - + if (drop->data_offset >= drop->x11_data_size) return 0; // EOF size_t remaining = drop->x11_data_size - drop->data_offset; size_t to_read = (remaining < capacity) ? remaining : capacity; - memcpy(buffer, (unsigned char*)drop->current_data + drop->data_offset, to_read); drop->data_offset += to_read; - return (ssize_t)to_read; } void _glfwPlatformFinishDrop(GLFWDropData* drop, GLFWDragOperationType operation, bool success) { if (!drop) return; + free(drop->current_mime); drop->current_mime = NULL; // Free current data if any if (drop->current_data) { diff --git a/kitty/boss.py b/kitty/boss.py index e84a92d4a..821d3a31d 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -1883,20 +1883,28 @@ def update_tab_bar_data(self, os_window_id: int) -> None: if tm is not None: tm.update_tab_bar_data() - def on_drop(self, os_window_id: int, mime: str, data: bytes) -> None: + def on_drop(self, os_window_id: int, drop: dict[str, bytes] | Exception) -> None: + if isinstance(drop, Exception): + self.show_error(_('Drop failed'), str(drop)) + return tm = self.os_window_map.get(os_window_id) if tm is not None: w = tm.active_window if w is not None: - text = data.decode('utf-8', 'replace') - if mime == 'text/uri-list': - urls = parse_uri_list(text) + text = '' + if uri_list := drop.pop('text/uri-list', b''): + urls = parse_uri_list(uri_list.decode('utf-8', 'replace')) if w.at_prompt: import shlex text = ' '.join(map(shlex.quote, urls)) else: text = '\n'.join(urls) - w.paste_text(text) + elif tp := drop.pop('text/plain', b''): + text = tp.decode('utf-8', 'replace') + elif tp := drop.pop('text/plain;charset=utf-8', b''): + text = tp.decode('utf-8', 'replace') + if text: + w.paste_text(text) @ac('win', ''' Focus the nth OS window if positive or the previously active OS windows if negative. When the number is larger diff --git a/kitty/glfw-wrapper.c b/kitty/glfw-wrapper.c index dc1174a12..c5653bf9a 100644 --- a/kitty/glfw-wrapper.c +++ b/kitty/glfw-wrapper.c @@ -368,6 +368,15 @@ load_glfw(const char* path) { *(void **) (&glfwUpdateDragState_impl) = dlsym(handle, "glfwUpdateDragState"); if (glfwUpdateDragState_impl == NULL) fail("Failed to load glfw function glfwUpdateDragState with error: %s", dlerror()); + *(void **) (&glfwGetDropMimeTypes_impl) = dlsym(handle, "glfwGetDropMimeTypes"); + if (glfwGetDropMimeTypes_impl == NULL) fail("Failed to load glfw function glfwGetDropMimeTypes with error: %s", dlerror()); + + *(void **) (&glfwReadDropData_impl) = dlsym(handle, "glfwReadDropData"); + if (glfwReadDropData_impl == NULL) fail("Failed to load glfw function glfwReadDropData with error: %s", dlerror()); + + *(void **) (&glfwFinishDrop_impl) = dlsym(handle, "glfwFinishDrop"); + if (glfwFinishDrop_impl == NULL) fail("Failed to load glfw function glfwFinishDrop with error: %s", dlerror()); + *(void **) (&glfwJoystickPresent_impl) = dlsym(handle, "glfwJoystickPresent"); if (glfwJoystickPresent_impl == NULL) fail("Failed to load glfw function glfwJoystickPresent with error: %s", dlerror()); diff --git a/kitty/glfw-wrapper.h b/kitty/glfw-wrapper.h index bb0ca3504..ce9cf8046 100644 --- a/kitty/glfw-wrapper.h +++ b/kitty/glfw-wrapper.h @@ -1005,6 +1005,22 @@ typedef struct GLFWwindow GLFWwindow; */ typedef struct GLFWcursor GLFWcursor; +/*! @brief Opaque drop data object. + * + * Opaque drop data object representing data from a drag and drop operation. + * This object is passed to the drop callback and can be used to query + * available MIME types and read the dropped data in chunks. + * + * @see @ref path_drop + * @see @ref glfwGetDropMimeTypes + * @see @ref glfwReadDropData + * + * @since Added in version 4.0. + * + * @ingroup input + */ +typedef struct GLFWDropData GLFWDropData; + typedef enum { GLFW_RELEASE = 0, GLFW_PRESS = 1, @@ -1499,25 +1515,30 @@ typedef void (* GLFWkeyboardfun)(GLFWwindow*, GLFWkeyevent*); * This is the function pointer type for drop callbacks. A drop * callback function has the following signature: * @code - * void function_name(GLFWwindow* window, const char* mime, const char* data, size_t sz) + * void function_name(GLFWwindow* window, GLFWDropData* drop) * @endcode * * @param[in] window The window that received the event. - * @param[in] mime The UTF-8 encoded drop mime-type - * @param[in] data The dropped data. - * @param[in] sz The size of the dropped data + * @param[in] drop A heap-allocated opaque pointer representing the dropped data. Use + * @ref glfwGetDropMimeTypes to get available MIME types and + * @ref glfwReadDropData to read the data in chunks. * - * @pointer_lifetime The data is valid until the - * callback function returns. + * @note The drop object is heap-allocated and remains valid until the + * application calls @ref glfwFinishDrop to free it. The application is + * responsible for calling glfwFinishDrop when it has finished reading + * the dropped data, even if reading fails or is not needed. * * @sa @ref path_drop * @sa @ref glfwSetDropCallback + * @sa @ref glfwGetDropMimeTypes + * @sa @ref glfwReadDropData + * @sa @ref glfwFinishDrop * - * @since Added in version 3.1. + * @since Changed in version 4.0 to receive opaque drop data pointer. * * @ingroup input */ -typedef void (* GLFWdropfun)(GLFWwindow*, const char *, const char*, size_t); +typedef void (* GLFWdropfun)(GLFWwindow*, GLFWDropData*); /*! @brief Drag event types. * @@ -2302,6 +2323,18 @@ typedef void (*glfwUpdateDragState_func)(GLFWwindow*); GFW_EXTERN glfwUpdateDragState_func glfwUpdateDragState_impl; #define glfwUpdateDragState glfwUpdateDragState_impl +typedef const char** (*glfwGetDropMimeTypes_func)(GLFWDropData*, int*); +GFW_EXTERN glfwGetDropMimeTypes_func glfwGetDropMimeTypes_impl; +#define glfwGetDropMimeTypes glfwGetDropMimeTypes_impl + +typedef ssize_t (*glfwReadDropData_func)(GLFWDropData*, const char*, void*, size_t, monotonic_t); +GFW_EXTERN glfwReadDropData_func glfwReadDropData_impl; +#define glfwReadDropData glfwReadDropData_impl + +typedef void (*glfwFinishDrop_func)(GLFWDropData*, GLFWDragOperationType, bool); +GFW_EXTERN glfwFinishDrop_func glfwFinishDrop_impl; +#define glfwFinishDrop glfwFinishDrop_impl + typedef int (*glfwJoystickPresent_func)(int); GFW_EXTERN glfwJoystickPresent_func glfwJoystickPresent_impl; #define glfwJoystickPresent glfwJoystickPresent_impl diff --git a/kitty/glfw.c b/kitty/glfw.c index e73e67076..690335da4 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -700,10 +700,53 @@ end: return ret; } +static PyObject* +read_drop_data(GLFWDropData *drop, const char *mime) { + RAII_PyObject(ans, PyBytes_FromStringAndSize(NULL, 8192)); + if (!ans) return NULL; + size_t pos = 0; + monotonic_t timeout = s_double_to_monotonic_t(2); + while (true) { + int ret = glfwReadDropData(drop, mime, PyBytes_AS_STRING(ans) + pos, PyBytes_GET_SIZE(ans) - pos, timeout); + if (ret > 0) { + pos += ret; + if (pos >= (size_t)PyBytes_GET_SIZE(ans)) { + if (_PyBytes_Resize(&ans, pos * 2) != 0) return NULL; + } + } else if (ret == 0) { + if (_PyBytes_Resize(&ans, pos) != 0) return NULL; + return Py_NewRef(ans); + } + else { + errno = -ret; + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + } +} + static void -drop_callback(GLFWwindow *w, const char *mime, const char *data, size_t sz) { +get_mime_data(GLFWDropData *drop, const char **mimes, int mime_count, PyObject *ans) { + for (int i = 0; i < mime_count; i++) { + if (is_droppable_mime(mimes[i])) { + RAII_PyObject(data, read_drop_data(drop, mimes[i])); + if (data == NULL) return ; + if (PyDict_SetItemString(ans, mimes[i], data) != 0) return ; + } + } +} + +static void +drop_callback(GLFWwindow *w, GLFWDropData *drop) { + int num_mimes; + const char** mimes = glfwGetDropMimeTypes(drop, &num_mimes); + RAII_PyObject(ans, PyDict_New()); + get_mime_data(drop, mimes, num_mimes, ans); + RAII_PyObject(exc, PyErr_GetRaisedException()); + glfwFinishDrop(drop, GLFW_DRAG_OPERATION_COPY, true); if (!set_callback_window(w)) return; - WINDOW_CALLBACK(on_drop, "sy#", mime, data, (Py_ssize_t)sz); + if (exc != NULL) { WINDOW_CALLBACK(on_drop, "O", exc); } + else if (PyDict_Size(ans)) WINDOW_CALLBACK(on_drop, "O", ans); request_tick_callback(); global_state.callback_os_window = NULL; }