diff --git a/docs/dnd-protocol.rst b/docs/dnd-protocol.rst index 487318881..8b55ffd26 100644 --- a/docs/dnd-protocol.rst +++ b/docs/dnd-protocol.rst @@ -68,6 +68,21 @@ with ``x, y = -1, -1`` to indicate that the drag has left the window. For such events the list of MIME types must be empty. Note that the terminal must never send negative cell co-ordinates for any other reason. +The client program must inform the terminal whether it will accept +the potential drop and which MIME types of the set of offered MIME types it +accepts. Until the client does so the terminal will indicate to the OS that +the drop is not accepted. To do so, the client sends an escape code of the +form:: + + OSC _dnd_code ; t=m:o=O ; MIME list ST + +Here the ``o`` key is the operation the client intends to perform if a drop +occurs which can be either ``1`` for copy or ``2`` for move or ``0`` for not +accepted. The MIME list is the ordered list of MIME types from the offered list +that the client wants. If no MIME type list is present, it is equivalent to no +change in the offered list of MIME types. The list should be ordered in order +of decreasing preference. Some platforms may assume show the user some +indication of the first MIME type in the list. Metadata reference --------------------------- @@ -89,6 +104,11 @@ Key Value Default Description When it is set, all responses from the terminal in that session will have it set to the same value. + +``o`` Positive integer ``0`` What drop operation to perform. ``0`` + means rejected, ``1`` means copy and + ``2`` means move. + **Keys for location** ----------------------------------------------------------- ``x`` Integer ``0`` Cell x-coordinate origin is 0, 0 at top left of screen diff --git a/gen/apc_parsers.py b/gen/apc_parsers.py index 24ce5e373..6fbb74df6 100755 --- a/gen/apc_parsers.py +++ b/gen/apc_parsers.py @@ -334,6 +334,11 @@ def parsers() -> None: 't': ('type', flag('aA')), 'm': ('more', 'uint'), 'i': ('client_id', 'uint'), + 'o': ('operation', 'uint'), + 'x': ('cell_x', 'int'), + 'y': ('cell_y', 'int'), + 'X': ('pixel_x', 'int'), + 'Y': ('pixel_y', 'int'), } text = generate( 'parse_dnd_code', 'screen_handle_dnd_command', 'dnd_command', keymap, 'DnDCommand', diff --git a/glfw/glfw3.h b/glfw/glfw3.h index 87438627d..497385dcd 100644 --- a/glfw/glfw3.h +++ b/glfw/glfw3.h @@ -4976,7 +4976,8 @@ GLFWAPI GLFWscrollfun glfwSetScrollCallback(GLFWwindow* window, GLFWscrollfun ca GLFWAPI GLFWliveresizefun glfwSetLiveResizeCallback(GLFWwindow* window, GLFWliveresizefun callback); GLFWAPI GLFWdropeventfun glfwSetDropEventCallback(GLFWwindow *window, GLFWdropeventfun callback); -GLFWAPI void glfwRequestDropUpdate(GLFWwindow *window); // ask for update before GLFW_DROP_DROP happens +// ask for update before GLFW_DROP_DROP happens +GLFWAPI void glfwRequestDropUpdate(GLFWwindow *window); GLFWAPI int glfwRequestDropData(GLFWwindow *window, const char *mime); GLFWAPI void glfwEndDrop(GLFWwindow *window, GLFWDragOperationType op); GLFWAPI GLFWdragsourcefun glfwSetDragSourceCallback(GLFWwindow* window, GLFWdragsourcefun callback); diff --git a/kitty/dnd.c b/kitty/dnd.c index be3e4d08a..4a11ab3b7 100644 --- a/kitty/dnd.c +++ b/kitty/dnd.c @@ -8,6 +8,7 @@ #include "dnd.h" #include "base64.h" #include "control-codes.h" +#include "iqsort.h" static void drop_free_offered_mimes(Window *w) { @@ -18,6 +19,12 @@ drop_free_offered_mimes(Window *w) { w->drop.num_offerred_mimes = 0; } +static void +drop_free_accepted_mimes(Window *w) { + free(w->drop.accepted_mimes); w->drop.accepted_mimes = NULL; + w->drop.accepted_mimes_sz = 0; +} + static void free_pending(PendingData *pending) { if (pending->items) { @@ -30,6 +37,7 @@ free_pending(PendingData *pending) { void drop_free_data(Window *w) { drop_free_offered_mimes(w); + drop_free_accepted_mimes(w); free_pending(&w->drop.pending); } @@ -154,11 +162,61 @@ drop_move_on_child(Window *w, const char** mimes, size_t num_mimes) { } } +void +drop_set_status(Window *w, int operation, const char *payload, size_t payload_sz, bool more) { + if (!w->drop.accept_in_progress) { + drop_free_accepted_mimes(w); w->drop.accept_in_progress = true; w->drop.accepted_operation = 0; + switch(operation) { + case 1: w->drop.accepted_operation = GLFW_DRAG_OPERATION_COPY; break; + case 2: w->drop.accepted_operation = GLFW_DRAG_OPERATION_MOVE; break; + default: w->drop.accepted_operation = GLFW_DRAG_OPERATION_NONE; break; + } + } + w->drop.accepted_mimes = realloc(w->drop.accepted_mimes, w->drop.accepted_mimes_sz + payload_sz + 2); + if (w->drop.accepted_mimes) { + memcpy(w->drop.accepted_mimes + w->drop.accepted_mimes_sz, payload, payload_sz); + w->drop.accepted_mimes_sz += payload_sz; + } + if (!more) { + w->drop.accept_in_progress = false; + if (w->drop.accepted_mimes) { + for (size_t i = 0; i < w->drop.accepted_mimes_sz; i++) + if (w->drop.accepted_mimes[i] == ' ') w->drop.accepted_mimes[i] = 0; + w->drop.accepted_mimes[w->drop.accepted_mimes_sz++] = 0; + } + OSWindow *osw = os_window_for_kitty_window(w->id); + if (osw) request_drop_status_update(osw); + } +} + + +size_t +drop_update_mimes(Window *w, const char **allowed_mimes, size_t allowed_mimes_count) { + if (w->drop.accept_in_progress) return allowed_mimes_count; + if (w->drop.accepted_operation == GLFW_DRAG_OPERATION_NONE) return 0; + typedef struct mime_sorter { const char *m; ssize_t key; } mime_sorter; + if (!w->drop.accepted_mimes) return allowed_mimes_count; + RAII_ALLOC(mime_sorter, ms, malloc(sizeof(mime_sorter) * allowed_mimes_count)); + if (!ms) return allowed_mimes_count; + const ssize_t sentinel = allowed_mimes_count; + for (size_t i = 0; i < allowed_mimes_count; i++) { + ms[i].m = allowed_mimes[i]; + const char *p = strstr(w->drop.accepted_mimes, ms[i].m); + ms[i].key = p ? p - w->drop.accepted_mimes : sentinel; + } +#define mimes_lt(a, b) ((a)->key < (b)->key) + QSORT(mime_sorter, ms, allowed_mimes_count, mimes_lt); +#undef mimes_lt + while(allowed_mimes_count && ms[allowed_mimes_count-1].key == sentinel) allowed_mimes_count--; + for (size_t i = 0; i < allowed_mimes_count; i++) allowed_mimes[i] = ms[i].m; + return allowed_mimes_count; +} + void drop_left_child(Window *w) { w->drop.hovered = false; drop_free_offered_mimes(w); - if (w->drop.allowed) { + if (w->drop.wanted) { char buf[128]; int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;i=%u:t=m:x=-1:y=-1", DND_CODE, w->drop.client_id); queue_payload_to_child(w->id, &w->drop.pending, buf, header_size, NULL, 0); diff --git a/kitty/dnd.h b/kitty/dnd.h index 59fc3ece8..29ae140f2 100644 --- a/kitty/dnd.h +++ b/kitty/dnd.h @@ -11,3 +11,5 @@ void drop_move_on_child(Window *w, const char **mimes, size_t num_mimes); void drop_left_child(Window *w); void drop_free_data(Window *w); +void drop_set_status(Window *w, int operation, const char *payload, size_t payload_sz, bool more); +size_t drop_update_mimes(Window *w, const char **allowed_mimes, size_t allowed_mimes_count); diff --git a/kitty/glfw-wrapper.c b/kitty/glfw-wrapper.c index c198e8186..da2d85780 100644 --- a/kitty/glfw-wrapper.c +++ b/kitty/glfw-wrapper.c @@ -359,6 +359,9 @@ load_glfw(const char* path) { *(void **) (&glfwSetDropEventCallback_impl) = dlsym(handle, "glfwSetDropEventCallback"); if (glfwSetDropEventCallback_impl == NULL) fail("Failed to load glfw function glfwSetDropEventCallback with error: %s", dlerror()); + *(void **) (&glfwRequestDropUpdate_impl) = dlsym(handle, "glfwRequestDropUpdate"); + if (glfwRequestDropUpdate_impl == NULL) fail("Failed to load glfw function glfwRequestDropUpdate with error: %s", dlerror()); + *(void **) (&glfwRequestDropData_impl) = dlsym(handle, "glfwRequestDropData"); if (glfwRequestDropData_impl == NULL) fail("Failed to load glfw function glfwRequestDropData with error: %s", dlerror()); diff --git a/kitty/glfw-wrapper.h b/kitty/glfw-wrapper.h index 0ebb45950..9ca42a9a6 100644 --- a/kitty/glfw-wrapper.h +++ b/kitty/glfw-wrapper.h @@ -2290,6 +2290,10 @@ typedef GLFWdropeventfun (*glfwSetDropEventCallback_func)(GLFWwindow*, GLFWdrope GFW_EXTERN glfwSetDropEventCallback_func glfwSetDropEventCallback_impl; #define glfwSetDropEventCallback glfwSetDropEventCallback_impl +typedef void (*glfwRequestDropUpdate_func)(GLFWwindow*); +GFW_EXTERN glfwRequestDropUpdate_func glfwRequestDropUpdate_impl; +#define glfwRequestDropUpdate glfwRequestDropUpdate_impl + typedef int (*glfwRequestDropData_func)(GLFWwindow*, const char*); GFW_EXTERN glfwRequestDropData_func glfwRequestDropData_impl; #define glfwRequestDropData glfwRequestDropData_impl diff --git a/kitty/glfw.c b/kitty/glfw.c index a6ad39a5e..4ca71caf1 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -771,7 +771,7 @@ register_drop_window(id_type window_id, const uint8_t *payload, size_t payload_s Window *w = window_for_window_id(window_id); OSWindow *osw = os_window_for_kitty_window(window_id); if (w && osw && osw->handle) { - w->drop.allowed = on; + w->drop.wanted = on; w->drop.client_id = client_id; if (!on) { drop_free_data(w); zero_at_ptr(&w->drop); } else if (payload && payload_sz) { @@ -795,7 +795,7 @@ static void on_drop(GLFWwindow *window, GLFWDropEvent *ev) { if (!set_callback_window(window)) return; OSWindow *os_window = global_state.callback_os_window; - Window *w = NULL; + Window *w = NULL; id_type wid = global_state.mouse_hover_in_window; bool is_kitty_ui_drag = false; for (size_t i = 0; i < ev->num_mimes; i++) { if (is_droppable_mime(ev->mimes[i]) >= TAB_DRAG_MIME_NUMBER) { is_kitty_ui_drag = true; break;} @@ -803,11 +803,13 @@ on_drop(GLFWwindow *window, GLFWDropEvent *ev) { switch (ev->type) { case GLFW_DROP_ENTER: case GLFW_DROP_MOVE: + global_state.drop_dest.drop_has_happened = false; os_window->last_drag_event.x = (int)(ev->xpos * os_window->viewport_x_ratio); os_window->last_drag_event.y = (int)(ev->ypos * os_window->viewport_y_ratio); on_mouse_position_update(ev->xpos, ev->ypos); - if (!is_kitty_ui_drag && global_state.mouse_hover_in_window && (w = window_for_window_id(global_state.mouse_hover_in_window)) && w->drop.allowed) { + if (!is_kitty_ui_drag && wid && (w = window_for_window_id(wid)) && w->drop.wanted) { drop_move_on_child(w, ev->mimes, ev->num_mimes); + ev->num_mimes = drop_update_mimes(w, ev->mimes, ev->num_mimes); return; } call_boss(on_drop_move, "KiiOO", @@ -816,8 +818,8 @@ on_drop(GLFWwindow *window, GLFWDropEvent *ev) { /* fallthrough */ case GLFW_DROP_STATUS_UPDATE: if (is_kitty_ui_drag) update_allowed_mimes_for_drop(ev); - else { - } + else if (wid && (w = window_for_window_id(wid)) && w->drop.wanted && w->drop.hovered) + ev->num_mimes = drop_update_mimes(w, ev->mimes, ev->num_mimes); break; case GLFW_DROP_LEAVE: for (size_t tc = 0; tc < os_window->num_tabs; tc++) { @@ -833,6 +835,7 @@ on_drop(GLFWwindow *window, GLFWDropEvent *ev) { break; case GLFW_DROP_DROP: Py_CLEAR(global_state.drop_dest.data); + global_state.drop_dest.drop_has_happened = true; if (ev->from_self) { if (global_state.drag_source.drag_data) { global_state.drag_source.was_dropped = true; @@ -856,6 +859,12 @@ on_drop(GLFWwindow *window, GLFWDropEvent *ev) { } } +void +request_drop_status_update(OSWindow *osw) { + if (osw && osw->handle && !global_state.drop_dest.drop_has_happened && global_state.drop_dest.os_window_id == osw->id) + glfwRequestDropUpdate(osw->handle); +} + static void application_close_requested_callback(int flags) { if (flags) { diff --git a/kitty/screen.c b/kitty/screen.c index 5bca56d0c..2f4c3b47b 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -15,6 +15,7 @@ #include "data-types.h" #include "control-codes.h" #include "screen.h" +#include "dnd.h" #include "state.h" #include "iqsort.h" #include "fonts.h" @@ -1513,6 +1514,10 @@ screen_handle_dnd_command(Screen *self, const DnDCommand *cmd, const uint8_t *pa switch(cmd->type) { case 'a': register_drop_window(self->window_id, payload, cmd->payload_sz, true, cmd->client_id); break; case 'A': register_drop_window(self->window_id, NULL, 0, false, cmd->client_id); break; + case 'm': { + Window *w = window_for_window_id(self->window_id); + if (w) drop_set_status(w, cmd->operation, (const char*)payload, cmd->payload_sz, cmd->more); + } break; } } diff --git a/kitty/screen.h b/kitty/screen.h index b8e15806c..345f912bb 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -19,6 +19,8 @@ typedef struct DnDCommand { unsigned more; uint32_t client_id; size_t payload_sz; + int32_t cell_x, cell_y, pixel_x, pixel_y; + uint32_t operation; } DnDCommand; typedef struct { diff --git a/kitty/state.h b/kitty/state.h index 9320e690d..4233f49ff 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -246,10 +246,15 @@ typedef struct Window { bool is_hovering; } scrollbar; struct { - bool allowed, hovered; + bool wanted, hovered; uint32_t client_id; - const char **offerred_mimes; size_t num_offerred_mimes; PendingData pending; + + const char **offerred_mimes; size_t num_offerred_mimes; + + char *accepted_mimes; size_t accepted_mimes_sz; + int accepted_operation; bool accept_in_progress; + } drop; } Window; @@ -402,6 +407,7 @@ typedef struct GlobalState { id_type os_window_id; double x, y; size_t num_left; + bool drop_has_happened; } drop_dest; struct { @@ -529,3 +535,4 @@ void setup_os_window_for_rendering(OSWindow*, Tab*, Window*, bool); void swap_window_buffers(OSWindow *w); void take_screenshot_of_rectangular_region(OSWindow *os_window, Region region, unsigned char *dst_buf, unsigned *thumb_w, unsigned *thumb_h); bool current_framebuffer_is_ok(void); +void request_drop_status_update(OSWindow *osw);