diff --git a/kittens/dnd/drag.go b/kittens/dnd/drag.go index 34de698a5..6dd05aea9 100644 --- a/kittens/dnd/drag.go +++ b/kittens/dnd/drag.go @@ -33,7 +33,8 @@ func (dnd *dnd) on_potential_drag_start(cell_x, cell_y int) (err error) { s := dnd.drag_sources[mt] if len(s.data) > 0 && len(s.data)+total_preloaded_data_sz < 64*1024*1024 { total_preloaded_data_sz += len(s.data) - dnd.lp.QueueDnDData(DC{Type: 'p', X: i, Operation: actions, Payload: utils.UnsafeStringToBytes(strings.Join(mimes, " "))}) + dnd.lp.QueueDnDData(DC{Type: 'p', X: i, Operation: actions, Payload: s.data}) + dnd.lp.QueueDnDData(DC{Type: 'p', X: i, Operation: actions}) } } // TODO: set the drag image @@ -56,3 +57,16 @@ func (dnd *dnd) on_drag_error(cmd DC) (err error) { } return } + +func (dnd *dnd) reset_drag() { + dnd.drag_status = drag_status{} +} + +func (dnd *dnd) on_drag_event(x, y int) (err error) { + switch x { + case 4: + dnd.reset_drag() + return dnd.render_screen() + } + return +} diff --git a/kittens/dnd/main.go b/kittens/dnd/main.go index 64d7dc2d6..75e69a462 100644 --- a/kittens/dnd/main.go +++ b/kittens/dnd/main.go @@ -220,6 +220,8 @@ func (dnd *dnd) run_loop() (err error) { dnd.send_test_response(utils.IfElse(dnd.drop_status.is_remote_client, "True", "False")) case "DROP_URI_LIST": dnd.send_test_response(strings.Join(dnd.drop_status.uri_list, "|")) + case "DRAG_ACTIVE": + dnd.send_test_response(utils.IfElse(dnd.drag_status.active, "DRAG_ACTIVE", "DRAG_INACTIVE")) default: dnd.send_test_response("UNKNOWN TEST COMMAND: " + string(cmd.Payload)) } @@ -246,6 +248,8 @@ func (dnd *dnd) run_loop() (err error) { return dnd.on_potential_drag_start(cmd.X, cmd.Y) case 'E': return dnd.on_drag_error(cmd) + case 'e': + return dnd.on_drag_event(cmd.X, cmd.Y) } return nil } diff --git a/kitty/dnd.c b/kitty/dnd.c index d46ceee66..f373016c8 100644 --- a/kitty/dnd.c +++ b/kitty/dnd.c @@ -1424,7 +1424,7 @@ drag_start(Window *w) { if (img.sz != (size_t)img.width * (size_t)img.height * 4u) abrt(EINVAL); } } - int err = start_window_drag(w); + int err = start_window_drag(w, dnd_is_test_mode()); if (err != 0) { abrt(err); } else { @@ -2162,6 +2162,18 @@ dnd_test_request_drag_data(PyObject *self UNUSED, PyObject *args) { Py_RETURN_NONE; } +static PyObject * +dnd_test_drag_finish(PyObject *self UNUSED, PyObject *args) { + unsigned long long window_id; int canceled_by_user; int errcode = 0; + if (!PyArg_ParseTuple(args, "Kp|i", &window_id, &canceled_by_user, &errcode)) return NULL; + Window *w = window_for_window_id((id_type)window_id); + if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; } + global_state.drag_source.was_canceled = canceled_by_user; + if (!errcode) drag_notify(w, DRAG_NOTIFY_FINISHED); + cancel_drag(w, errcode); + Py_RETURN_NONE; +} + static PyObject * dnd_test_drag_notify(PyObject *self UNUSED, PyObject *args) { // Call drag_notify with a specific type for testing the protocol output. @@ -2173,7 +2185,7 @@ dnd_test_drag_notify(PyObject *self UNUSED, PyObject *args) { int type; const char *accepted_mime = NULL; int action = 0, was_canceled = 0; - if (!PyArg_ParseTuple(args, "Ki|sii", &window_id, &type, &accepted_mime, &action, &was_canceled)) return NULL; + if (!PyArg_ParseTuple(args, "Ki|sip", &window_id, &type, &accepted_mime, &action, &was_canceled)) return NULL; Window *w = window_for_window_id((id_type)window_id); if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; } if (type < 0 || type > 3) { PyErr_SetString(PyExc_ValueError, "Invalid type"); return NULL; } @@ -2229,6 +2241,9 @@ dnd_test_probe_state(PyObject *self UNUSED, PyObject *args) { if (strcmp(q, "drop_getting_data_for_mime") == 0) { return PyUnicode_FromString(w->drop.getting_data_for_mime ? w->drop.getting_data_for_mime : ""); } + if (strcmp(q, "can_offer") == 0) { + return Py_NewRef(w->drag_source.can_offer ? Py_True : Py_False); + } if (strcmp(q, "drag_operations") == 0) { return PyLong_FromLong((long)w->drag_source.allowed_operations); } @@ -2262,6 +2277,7 @@ static PyMethodDef dnd_methods[] = { METHODB(dnd_test_force_drag_dropped, METH_VARARGS), METHODB(dnd_test_request_drag_data, METH_VARARGS), METHODB(dnd_test_drag_notify, METH_VARARGS), + METHODB(dnd_test_drag_finish, METH_VARARGS), METHODB(dnd_test_probe_state, METH_VARARGS), {NULL, NULL, 0, NULL} }; diff --git a/kitty/glfw.c b/kitty/glfw.c index a3e0d77a7..a9c3ad134 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -3063,12 +3063,14 @@ change_drag_thumbnail(PyObject *self UNUSED, PyObject *args) { } int -start_window_drag(Window *w) { +start_window_drag(Window *w, bool in_test_mode) { OSWindow *osw = os_window_for_kitty_window(w->id); - if (!osw || !osw->handle) return EINVAL; - // Deny the drag start if mouse is not still pressed and over the originating window - if (!osw->mouse_button_pressed[GLFW_MOUSE_BUTTON_LEFT]) return EPERM; - if (global_state.mouse_hover_in_window != global_state.drag_source.from_window) return EPERM; + if (!osw || (!in_test_mode && !osw->handle)) return EINVAL; + if (!in_test_mode) { + // Deny the drag start if mouse is not still pressed and over the originating window + if (!osw->mouse_button_pressed[GLFW_MOUSE_BUTTON_LEFT]) return EPERM; + if (global_state.mouse_hover_in_window != global_state.drag_source.from_window) return EPERM; + } RAII_ALLOC(GLFWDragSourceItem, items, calloc(w->drag_source.num_mimes, sizeof(GLFWDragSourceItem))); if (!items) return ENOMEM; for (size_t i = 0; i < w->drag_source.num_mimes; i++) { @@ -3097,6 +3099,7 @@ start_window_drag(Window *w) { global_state.drag_source.from_window = w->id; global_state.drag_source.from_os_window = osw->id; global_state.drag_source.thumbnail_idx = w->drag_source.img_idx < num_images ? (int)w->drag_source.img_idx : -1; + if (in_test_mode) return 0; int ret = glfwStartDrag(osw->handle, items, w->drag_source.num_mimes, thumbnail.pixels ? &thumbnail : NULL, w->drag_source.allowed_operations, true); if (ret != 0) free_drag_source(); return ret; diff --git a/kitty/state.h b/kitty/state.h index 551bc9811..586781207 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -639,6 +639,6 @@ void register_mimes_for_drop(OSWindow *w, const char **mimes, size_t sz); void request_drop_data(OSWindow *w, id_type wid, const char* mime); void cancel_current_drag_source(void); bool change_drag_image(int idx); -int start_window_drag(Window *w); +int start_window_drag(Window *w, bool in_test_mode); int notify_drag_data_ready(id_type os_window_id, const char *mime_type); BackgroundImage* background_image_for_os_window(OSWindow *w); diff --git a/kitty_tests/__init__.py b/kitty_tests/__init__.py index c189ca365..759792d22 100644 --- a/kitty_tests/__init__.py +++ b/kitty_tests/__init__.py @@ -337,8 +337,10 @@ class PTY: def __init__( self, argv=None, rows=25, columns=80, scrollback=100, cell_width=10, cell_height=20, cwd=None, env=None, stdin_fd=None, stdout_fd=None, needs_da1=True, window_id=0, + log_data_flow=False, ): self.is_child = False + self.log_data_flow = log_data_flow if isinstance(argv, str): argv = shlex.split(argv) self.write_buf = b'' @@ -423,6 +425,8 @@ class PTY: def write_to_child(self, data, flush=False): if isinstance(data, str): data = data.encode('utf-8') + if self.log_data_flow: + print('t -> c:', data) self.write_buf += data if flush: self.process_input_from_child(0) @@ -446,6 +450,8 @@ class PTY: data = os.read(self.master_fd, io.DEFAULT_BUFFER_SIZE) bytes_read += len(data) self.received_bytes += data + if self.log_data_flow: + print('c -> t:', data) parse_bytes(self.screen, data) return bytes_read diff --git a/kitty_tests/dnd_kitten.py b/kitty_tests/dnd_kitten.py index 806e02673..9e4e756e0 100644 --- a/kitty_tests/dnd_kitten.py +++ b/kitty_tests/dnd_kitten.py @@ -21,6 +21,7 @@ from kitty.fast_data_types import ( dnd_set_test_write_func, dnd_test_cleanup_fake_window, dnd_test_create_fake_window, + dnd_test_drag_finish, dnd_test_fake_drop_data, dnd_test_fake_drop_event, dnd_test_probe_state, @@ -328,10 +329,29 @@ class TestDnDKitten(BaseTest): f.write(os.urandom(1113)) create_fs(self.src_data_dir) self.finish_setup(cli_args=(f'--drag=image/png:{img_drag_path}', self.src_data_dir)) # ))) - with self.subTest(remote=False): - self.dnd_kitten_drag(False, img_drag_path) + self.dnd_kitten_drag(False, img_drag_path) + self.exit_kitten() def dnd_kitten_drag(self, remote_client, img_drop_path): + # self.pty.log_data_flow = True copy, move = self.get_button_geometry() - dnd_test_start_drag_offer(self.capture.window_id, 1, 1) - self.wait_for_state('drag_operations', 3) + self.wait_for_state('can_offer', True) + self.wait_for_state('drag_operations', 0) + def wait_for_drag_active(active=True): + self.send_dnd_command_to_kitten('DRAG_ACTIVE') + self.wait_for_responses('DRAG_ACTIVE' if active else 'DRAG_INACTIVE') + def start_drag(x, y, expected): + dnd_test_start_drag_offer(self.capture.window_id, x, y) + wait_for_drag_active() + self.wait_for_state('drag_operations', expected) + def end_drag(canceled=True): + dnd_test_drag_finish(self.capture.window_id, canceled) + wait_for_drag_active(False) + self.wait_for_state('drag_operations', 0) + start_drag(1, 1, 3) + self.assertEqual(set(self.probe_state('drag_mimes')), {'image/png', 'text/uri-list'}) + end_drag() + start_drag(copy[0] + 1, copy[1] + 1, 1) + end_drag() + start_drag(move[0] + 1, move[1] + 1, 2) + end_drag() diff --git a/tools/tui/loop/api.go b/tools/tui/loop/api.go index 8fecc87e2..891ba5438 100644 --- a/tools/tui/loop/api.go +++ b/tools/tui/loop/api.go @@ -672,7 +672,7 @@ func (self *Loop) DrawSizedText(text string, spec SizedText) { func (self *Loop) QueueDnDData(cmd DndCommand) IdType { b := strings.Builder{} b.Grow(64) - as_base64 := cmd.Type == 'r' + as_base64 := cmd.Type == 'r' || cmd.Type == 'p' fmt.Fprintf(&b, "\x1b]%d;t=%c", kitty.DndCode, cmd.Type) add := func(key byte, val int) { if val != 0 {