mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-08 14:18:26 +02:00
More work on DnD kitten
This commit is contained in:
@@ -75,6 +75,17 @@ func truncate_at_space(text string, width int) (string, string) {
|
|||||||
return text[:p], text[p:]
|
return text[:p], text[p:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type drop_status struct {
|
||||||
|
offered_mimes []string
|
||||||
|
accepted_mimes []string
|
||||||
|
cell_x, cell_y int
|
||||||
|
action int
|
||||||
|
in_window bool
|
||||||
|
reading_data bool
|
||||||
|
requesting_mime_idx_plus_one int
|
||||||
|
is_remote_client bool
|
||||||
|
}
|
||||||
|
|
||||||
func paragraph_as_lines(text string, width int) (ans []string) {
|
func paragraph_as_lines(text string, width int) (ans []string) {
|
||||||
for text != "" {
|
for text != "" {
|
||||||
var line string
|
var line string
|
||||||
@@ -87,6 +98,7 @@ func paragraph_as_lines(text string, width int) (ans []string) {
|
|||||||
|
|
||||||
func run_loop(opts *Options, drop_dests map[string]drop_dest, drag_sources map[string]drag_source, uri_list_buffer *bytes.Buffer) (err error) {
|
func run_loop(opts *Options, drop_dests map[string]drop_dest, drag_sources map[string]drag_source, uri_list_buffer *bytes.Buffer) (err error) {
|
||||||
allow_drops, allow_drags := len(drop_dests) > 0, len(drag_sources) > 0
|
allow_drops, allow_drags := len(drop_dests) > 0, len(drag_sources) > 0
|
||||||
|
data_has_been_dropped := false
|
||||||
drag_started := false
|
drag_started := false
|
||||||
in_test_mode := false
|
in_test_mode := false
|
||||||
lp, err := loop.New()
|
lp, err := loop.New()
|
||||||
@@ -98,13 +110,8 @@ func run_loop(opts *Options, drop_dests map[string]drop_dest, drag_sources map[s
|
|||||||
lp.DebugPrintln(payload)
|
lp.DebugPrintln(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
var drop_status struct {
|
drop_status := drop_status{cell_x: -1, cell_y: -1}
|
||||||
offered_mimes []string
|
reset_drop_status := drop_status
|
||||||
accepted_mimes []string
|
|
||||||
cell_x, cell_y int
|
|
||||||
action int
|
|
||||||
in_window bool
|
|
||||||
}
|
|
||||||
drop_status.cell_x, drop_status.cell_y = -1, -1
|
drop_status.cell_x, drop_status.cell_y = -1, -1
|
||||||
const copy_on_drop = 1
|
const copy_on_drop = 1
|
||||||
const move_on_drop = 2
|
const move_on_drop = 2
|
||||||
@@ -112,58 +119,6 @@ func run_loop(opts *Options, drop_dests map[string]drop_dest, drag_sources map[s
|
|||||||
var copy_button_region, move_button_region button_region
|
var copy_button_region, move_button_region button_region
|
||||||
var offered_mimes_buf strings.Builder
|
var offered_mimes_buf strings.Builder
|
||||||
|
|
||||||
on_drop_move := func(cell_x, cell_y int, has_more bool, offered_mimes string) (needs_rerender bool) {
|
|
||||||
prev_status := drop_status
|
|
||||||
drop_status.cell_x, drop_status.cell_y = cell_x, cell_y
|
|
||||||
if offered_mimes != "" {
|
|
||||||
offered_mimes_buf.WriteString(offered_mimes)
|
|
||||||
if has_more {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
offered_mimes := offered_mimes_buf.String()
|
|
||||||
drop_status.offered_mimes = strings.Fields(offered_mimes)
|
|
||||||
drop_status.accepted_mimes = make([]string, 0, len(drop_status.offered_mimes))
|
|
||||||
seen := utils.NewSet[string](len(drop_status.offered_mimes))
|
|
||||||
for _, x := range drop_status.offered_mimes {
|
|
||||||
if _, found := drop_dests[x]; found && !seen.Has(x) {
|
|
||||||
drop_status.accepted_mimes = append(drop_status.accepted_mimes, x)
|
|
||||||
seen.Add(x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
offered_mimes_buf.Reset()
|
|
||||||
if copy_button_region.has(cell_x, cell_y) {
|
|
||||||
drop_status.action = copy_on_drop
|
|
||||||
} else if move_button_region.has(cell_x, cell_y) {
|
|
||||||
drop_status.action = move_on_drop
|
|
||||||
} else {
|
|
||||||
switch opts.DropAnywhere {
|
|
||||||
case "disallowed":
|
|
||||||
drop_status.action = 0
|
|
||||||
drop_status.accepted_mimes = nil
|
|
||||||
case "copy":
|
|
||||||
drop_status.action = copy_on_drop
|
|
||||||
case "move":
|
|
||||||
drop_status.action = move_on_drop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
drop_status.in_window = cell_x > -1 && cell_y > -1
|
|
||||||
if !drop_status.in_window {
|
|
||||||
drop_status.offered_mimes = nil
|
|
||||||
}
|
|
||||||
mimes_changed := !slices.Equal(prev_status.accepted_mimes, drop_status.accepted_mimes)
|
|
||||||
needs_rerender = prev_status.action != drop_status.action || mimes_changed
|
|
||||||
if needs_rerender {
|
|
||||||
c := DC{Type: 'm', Operation: drop_status.action}
|
|
||||||
if drop_status.action != 0 && len(drop_status.accepted_mimes) > 0 {
|
|
||||||
c.Payload = utils.UnsafeStringToBytes(strings.Join(drop_status.accepted_mimes, " "))
|
|
||||||
}
|
|
||||||
lp.QueueDnDData(c)
|
|
||||||
}
|
|
||||||
needs_rerender = needs_rerender || drop_status.in_window != prev_status.in_window
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
render_screen := func() error { // {{{
|
render_screen := func() error { // {{{
|
||||||
if !in_test_mode {
|
if !in_test_mode {
|
||||||
lp.StartAtomicUpdate()
|
lp.StartAtomicUpdate()
|
||||||
@@ -175,6 +130,10 @@ func run_loop(opts *Options, drop_dests map[string]drop_dest, drag_sources map[s
|
|||||||
lp.Println("Dragging data...")
|
lp.Println("Dragging data...")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if drop_status.reading_data {
|
||||||
|
lp.Println("Reading dropped data, please wait...")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
y := 0
|
y := 0
|
||||||
sz, _ := lp.ScreenSize()
|
sz, _ := lp.ScreenSize()
|
||||||
render_paragraph := func(text string) {
|
render_paragraph := func(text string) {
|
||||||
@@ -204,9 +163,13 @@ func run_loop(opts *Options, drop_dests map[string]drop_dest, drag_sources map[s
|
|||||||
next_line()
|
next_line()
|
||||||
}
|
}
|
||||||
if allow_drops {
|
if allow_drops {
|
||||||
|
if data_has_been_dropped {
|
||||||
|
render_paragraph(`Data has been successfully dropped. You can drop more data or press Esc to quit.`)
|
||||||
|
} else {
|
||||||
render_paragraph(`Drag some data from another application into this window to transfer the files here.`)
|
render_paragraph(`Drag some data from another application into this window to transfer the files here.`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
frame_width, padding_width := 4, 8
|
frame_width, padding_width := 4, 8
|
||||||
text_width := len("copymove")
|
text_width := len("copymove")
|
||||||
scale := 5
|
scale := 5
|
||||||
@@ -265,6 +228,99 @@ func run_loop(opts *Options, drop_dests map[string]drop_dest, drag_sources map[s
|
|||||||
return nil
|
return nil
|
||||||
} // }}}
|
} // }}}
|
||||||
|
|
||||||
|
// Drop handling {{{
|
||||||
|
end_drop := func() {
|
||||||
|
lp.QueueDnDData(DC{Type: 'r'}) // end drop
|
||||||
|
drop_status = reset_drop_status
|
||||||
|
render_screen()
|
||||||
|
}
|
||||||
|
|
||||||
|
all_mime_data_dropped := func() {
|
||||||
|
if _, found := drop_dests["text/uri-list"]; found && drop_status.is_remote_client {
|
||||||
|
// TODO: Handle remote client
|
||||||
|
} else {
|
||||||
|
drop_status = reset_drop_status
|
||||||
|
data_has_been_dropped = true
|
||||||
|
render_screen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request_mime_data := func() {
|
||||||
|
drop_status.requesting_mime_idx_plus_one++
|
||||||
|
idx := drop_status.requesting_mime_idx_plus_one - 1
|
||||||
|
if idx >= len(drop_status.accepted_mimes) {
|
||||||
|
all_mime_data_dropped()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lp.QueueDnDData(DC{Type: 'r', X: idx + 1})
|
||||||
|
}
|
||||||
|
|
||||||
|
on_drop_move := func(cell_x, cell_y int, has_more bool, offered_mimes string, is_drop bool) (needs_rerender bool) {
|
||||||
|
prev_status := drop_status
|
||||||
|
drop_status.cell_x, drop_status.cell_y = cell_x, cell_y
|
||||||
|
if offered_mimes != "" {
|
||||||
|
offered_mimes_buf.WriteString(offered_mimes)
|
||||||
|
if has_more {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
offered_mimes := offered_mimes_buf.String()
|
||||||
|
drop_status.offered_mimes = strings.Fields(offered_mimes)
|
||||||
|
drop_status.accepted_mimes = make([]string, 0, len(drop_status.offered_mimes))
|
||||||
|
seen := utils.NewSet[string](len(drop_status.offered_mimes))
|
||||||
|
for _, x := range drop_status.offered_mimes {
|
||||||
|
if _, found := drop_dests[x]; found && !seen.Has(x) {
|
||||||
|
drop_status.accepted_mimes = append(drop_status.accepted_mimes, x)
|
||||||
|
seen.Add(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offered_mimes_buf.Reset()
|
||||||
|
if copy_button_region.has(cell_x, cell_y) {
|
||||||
|
drop_status.action = copy_on_drop
|
||||||
|
} else if move_button_region.has(cell_x, cell_y) {
|
||||||
|
drop_status.action = move_on_drop
|
||||||
|
} else {
|
||||||
|
switch opts.DropAnywhere {
|
||||||
|
case "disallowed":
|
||||||
|
drop_status.action = 0
|
||||||
|
drop_status.accepted_mimes = nil
|
||||||
|
case "copy":
|
||||||
|
drop_status.action = copy_on_drop
|
||||||
|
case "move":
|
||||||
|
drop_status.action = move_on_drop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop_status.in_window = cell_x > -1 && cell_y > -1
|
||||||
|
if !drop_status.in_window || drag_started { // disallow self drag and drop
|
||||||
|
drop_status = reset_drop_status
|
||||||
|
}
|
||||||
|
mimes_changed := !slices.Equal(prev_status.accepted_mimes, drop_status.accepted_mimes)
|
||||||
|
needs_rerender = prev_status.action != drop_status.action || mimes_changed
|
||||||
|
if needs_rerender && !is_drop {
|
||||||
|
c := DC{Type: 'm', Operation: drop_status.action}
|
||||||
|
if drop_status.action != 0 && len(drop_status.accepted_mimes) > 0 {
|
||||||
|
c.Payload = utils.UnsafeStringToBytes(strings.Join(drop_status.accepted_mimes, " "))
|
||||||
|
}
|
||||||
|
lp.QueueDnDData(c)
|
||||||
|
}
|
||||||
|
needs_rerender = needs_rerender || drop_status.in_window != prev_status.in_window
|
||||||
|
if is_drop {
|
||||||
|
needs_rerender = true
|
||||||
|
if drop_status.action == 0 || len(drop_status.accepted_mimes) == 0 || drag_started {
|
||||||
|
end_drop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
drop_status.reading_data = true
|
||||||
|
request_mime_data()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
on_drop_data := func(cmd DC) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
|
||||||
lp.OnInitialize = func() (string, error) {
|
lp.OnInitialize = func() (string, error) {
|
||||||
lp.AllowLineWrapping(false)
|
lp.AllowLineWrapping(false)
|
||||||
lp.SetCursorVisible(false)
|
lp.SetCursorVisible(false)
|
||||||
@@ -277,6 +333,7 @@ func run_loop(opts *Options, drop_dests map[string]drop_dest, drag_sources map[s
|
|||||||
lp.SetWindowTitle("Drag and drop")
|
lp.SetWindowTitle("Drag and drop")
|
||||||
return "", render_screen()
|
return "", render_screen()
|
||||||
}
|
}
|
||||||
|
|
||||||
lp.OnFinalize = func() string {
|
lp.OnFinalize = func() string {
|
||||||
lp.AllowLineWrapping(true)
|
lp.AllowLineWrapping(true)
|
||||||
lp.SetCursorVisible(true)
|
lp.SetCursorVisible(true)
|
||||||
@@ -288,6 +345,7 @@ func run_loop(opts *Options, drop_dests map[string]drop_dest, drag_sources map[s
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
lp.OnDnDData = func(cmd loop.DndCommand) error {
|
lp.OnDnDData = func(cmd loop.DndCommand) error {
|
||||||
// TODO: Use lp.QueueDnDData to implement drag and drop protocol
|
// TODO: Use lp.QueueDnDData to implement drag and drop protocol
|
||||||
// If allow_drags, start a drag when the terminal sends the t=o
|
// If allow_drags, start a drag when the terminal sends the t=o
|
||||||
@@ -350,9 +408,19 @@ func run_loop(opts *Options, drop_dests map[string]drop_dest, drag_sources map[s
|
|||||||
if cmd.Payload != nil {
|
if cmd.Payload != nil {
|
||||||
payload = utils.UnsafeBytesToString(cmd.Payload)
|
payload = utils.UnsafeBytesToString(cmd.Payload)
|
||||||
}
|
}
|
||||||
if on_drop_move(cmd.X, cmd.Y, cmd.Has_more, payload) {
|
if on_drop_move(cmd.X, cmd.Y, cmd.Has_more, payload, false) {
|
||||||
render_screen()
|
render_screen()
|
||||||
}
|
}
|
||||||
|
case 'M':
|
||||||
|
if on_drop_move(cmd.X, cmd.Y, cmd.Has_more, utils.UnsafeBytesToString(cmd.Payload), true) {
|
||||||
|
render_screen()
|
||||||
|
}
|
||||||
|
case 'R':
|
||||||
|
return fmt.Errorf("error from the terminal while reading dropped data: %s", string(cmd.Payload))
|
||||||
|
case 'r':
|
||||||
|
err := on_drop_data(cmd)
|
||||||
|
render_screen()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2061,8 +2061,9 @@ dnd_test_fake_drop_event(PyObject *self UNUSED, PyObject *args) {
|
|||||||
// None to simulate a leave event.
|
// None to simulate a leave event.
|
||||||
unsigned long long window_id;
|
unsigned long long window_id;
|
||||||
int is_drop;
|
int is_drop;
|
||||||
PyObject *mimes_seq;
|
PyObject *mimes_seq = Py_None;
|
||||||
if (!PyArg_ParseTuple(args, "KpO", &window_id, &is_drop, &mimes_seq)) return NULL;
|
int x = -2, y = -2;
|
||||||
|
if (!PyArg_ParseTuple(args, "Kp|Oii", &window_id, &is_drop, &mimes_seq, &x, &y)) return NULL;
|
||||||
Window *w = window_for_window_id((id_type)window_id);
|
Window *w = window_for_window_id((id_type)window_id);
|
||||||
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
|
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
|
||||||
if (mimes_seq == Py_None) {
|
if (mimes_seq == Py_None) {
|
||||||
@@ -2078,6 +2079,8 @@ dnd_test_fake_drop_event(PyObject *self UNUSED, PyObject *args) {
|
|||||||
mimes[i] = PyUnicode_AsUTF8(PySequence_Fast_GET_ITEM(fast_seq, i));
|
mimes[i] = PyUnicode_AsUTF8(PySequence_Fast_GET_ITEM(fast_seq, i));
|
||||||
if (!mimes[i]) return NULL;
|
if (!mimes[i]) return NULL;
|
||||||
}
|
}
|
||||||
|
if (x > -1) w->mouse_pos.cell_x = x;
|
||||||
|
if (y > -1) w->mouse_pos.cell_y = y;
|
||||||
drop_move_on_child(w, mimes, (size_t)num_mimes, is_drop ? true : false);
|
drop_move_on_child(w, mimes, (size_t)num_mimes, is_drop ? true : false);
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from kitty.fast_data_types import (
|
|||||||
dnd_set_test_write_func,
|
dnd_set_test_write_func,
|
||||||
dnd_test_cleanup_fake_window,
|
dnd_test_cleanup_fake_window,
|
||||||
dnd_test_create_fake_window,
|
dnd_test_create_fake_window,
|
||||||
|
dnd_test_fake_drop_event,
|
||||||
dnd_test_probe_state,
|
dnd_test_probe_state,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ from .dnd import WriteCapture
|
|||||||
class Capture(WriteCapture):
|
class Capture(WriteCapture):
|
||||||
|
|
||||||
def __call__(self, window_id: int, data: bytes) -> None:
|
def __call__(self, window_id: int, data: bytes) -> None:
|
||||||
self.pty.write(data)
|
self.pty.write_to_child(data)
|
||||||
|
|
||||||
|
|
||||||
class TestDnDKitten(BaseTest):
|
class TestDnDKitten(BaseTest):
|
||||||
@@ -71,6 +72,7 @@ class TestDnDKitten(BaseTest):
|
|||||||
if remote_client:
|
if remote_client:
|
||||||
cmd.append('--machine-id=remote-client-for-test')
|
cmd.append('--machine-id=remote-client-for-test')
|
||||||
self.pty = self.enterContext(PTY(argv=cmd, cwd=self.kitten_wd, rows=25, columns=80, window_id=self.capture.window_id))
|
self.pty = self.enterContext(PTY(argv=cmd, cwd=self.kitten_wd, rows=25, columns=80, window_id=self.capture.window_id))
|
||||||
|
self.capture.pty = self.pty
|
||||||
self.pty.callbacks.printbuf = self
|
self.pty.callbacks.printbuf = self
|
||||||
self.screen = self.pty.screen
|
self.screen = self.pty.screen
|
||||||
self.pty.wait_till(lambda: bool(self.pty.callbacks.titlebuf))
|
self.pty.wait_till(lambda: bool(self.pty.callbacks.titlebuf))
|
||||||
@@ -137,20 +139,25 @@ class TestDnDKitten(BaseTest):
|
|||||||
copy, move = self.get_button_geometry()
|
copy, move = self.get_button_geometry()
|
||||||
all_mimes = 'text/uri-list a/b c/d'
|
all_mimes = 'text/uri-list a/b c/d'
|
||||||
for b, expected in ((copy, GLFW_DRAG_OPERATION_COPY), (move, GLFW_DRAG_OPERATION_MOVE)):
|
for b, expected in ((copy, GLFW_DRAG_OPERATION_COPY), (move, GLFW_DRAG_OPERATION_MOVE)):
|
||||||
self.send_dnd_command_to_kitten(all_mimes, t='m', x=str(b[0] + 1), y=str(b[1] + 1))
|
dnd_test_fake_drop_event(self.capture.window_id, False, all_mimes.split(), b[0] + 1, b[1] + 1)
|
||||||
self.wait_for_state('drop_action', expected)
|
self.wait_for_state('drop_action', expected)
|
||||||
self.assertEqual('text/uri-list', self.probe_state('drop_mimes').rstrip('\x00'))
|
self.assertEqual('text/uri-list', self.probe_state('drop_mimes').rstrip('\x00'))
|
||||||
self.send_dnd_command_to_kitten('DROP_MIMES')
|
self.send_dnd_command_to_kitten('DROP_MIMES')
|
||||||
self.wait_for_responses(all_mimes)
|
self.wait_for_responses(all_mimes)
|
||||||
self.send_dnd_command_to_kitten(t='m', x='-1', y='-1')
|
dnd_test_fake_drop_event(self.capture.window_id, False)
|
||||||
self.send_dnd_command_to_kitten('DROP_MIMES')
|
self.send_dnd_command_to_kitten('DROP_MIMES')
|
||||||
self.wait_for_responses('')
|
self.wait_for_responses('')
|
||||||
large_mimes = (all_mimes + ' ') * 300
|
large_mimes = ((all_mimes + ' ') * 300).rstrip()
|
||||||
self.assertGreater(len(large_mimes), 4096)
|
self.assertGreater(len(large_mimes), 4096)
|
||||||
self.send_dnd_command_to_kitten(all_mimes, t='m', x=str(copy[0] + 1), y=str(copy[1] + 1))
|
dnd_test_fake_drop_event(self.capture.window_id, False, large_mimes.split(), copy[0] + 1, copy[1] + 1)
|
||||||
self.wait_for_state('drop_action', GLFW_DRAG_OPERATION_COPY)
|
self.wait_for_state('drop_action', GLFW_DRAG_OPERATION_COPY)
|
||||||
self.send_dnd_command_to_kitten('DROP_MIMES')
|
self.send_dnd_command_to_kitten('DROP_MIMES')
|
||||||
self.wait_for_responses(all_mimes)
|
self.wait_for_responses(large_mimes)
|
||||||
self.send_dnd_command_to_kitten(t='m', x='-1', y='-1')
|
dnd_test_fake_drop_event(self.capture.window_id, False)
|
||||||
self.send_dnd_command_to_kitten('DROP_MIMES')
|
self.send_dnd_command_to_kitten('DROP_MIMES')
|
||||||
self.wait_for_responses('')
|
self.wait_for_responses('')
|
||||||
|
dnd_test_fake_drop_event(self.capture.window_id, False, all_mimes.split(), copy[0] + 1, copy[1] + 1)
|
||||||
|
self.wait_for_state('drop_action', GLFW_DRAG_OPERATION_COPY)
|
||||||
|
dnd_test_fake_drop_event(self.capture.window_id, True, all_mimes.split(), copy[0] + 1, copy[1] + 1)
|
||||||
|
self.send_dnd_command_to_kitten('DROP_MIMES')
|
||||||
|
self.wait_for_responses(all_mimes)
|
||||||
|
|||||||
Reference in New Issue
Block a user