mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-05-13 08:26:56 +00:00
dd basic drag thumbnail support to dnd kitten
This commit is contained in:
parent
ee38d3f0c8
commit
4fe29f2630
6 changed files with 108 additions and 4 deletions
|
|
@ -3,6 +3,7 @@ package dnd
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"maps"
|
||||
"os"
|
||||
|
|
@ -11,6 +12,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/emmansun/base64"
|
||||
"github.com/kovidgoyal/imaging"
|
||||
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/streaming_base64"
|
||||
|
|
@ -50,6 +52,72 @@ type drag_status struct {
|
|||
remote_item_write_id loop.IdType
|
||||
}
|
||||
|
||||
func find_drag_image(drag_sources map[string]*drag_source) image.Image {
|
||||
for mime, ds := range drag_sources {
|
||||
if strings.HasPrefix(mime, "image/") && ds.path != "" {
|
||||
q, err := imaging.Open(ds.path)
|
||||
if err == nil {
|
||||
return q
|
||||
}
|
||||
}
|
||||
}
|
||||
var uri_list []string
|
||||
if ds := drag_sources["text/uri-list"]; ds != nil && len(ds.data) > 0 {
|
||||
if q, err := parse_uri_list(string(ds.data)); err == nil {
|
||||
for _, path := range q {
|
||||
if path != "" {
|
||||
uri_list = append(uri_list, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, path := range uri_list {
|
||||
q, err := imaging.Open(path)
|
||||
if err == nil {
|
||||
return q
|
||||
}
|
||||
}
|
||||
// TODO: Try to generate an image based preview using the machinery from the choose-files kitten
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dnd *dnd) set_drag_image() (err error) {
|
||||
img := dnd.drag_thumbnail
|
||||
if img == nil {
|
||||
img = find_drag_image(dnd.drag_sources)
|
||||
}
|
||||
if img == nil {
|
||||
return
|
||||
}
|
||||
num_channels := utils.IfElse(imaging.IsOpaque(img), 3, 4)
|
||||
sz := dnd.opts.DragThumbnailSize
|
||||
if max(img.Bounds().Dx(), img.Bounds().Dy()) > sz {
|
||||
w, h := 0, 0
|
||||
if img.Bounds().Dx() >= img.Bounds().Dy() {
|
||||
w = sz
|
||||
} else {
|
||||
h = sz
|
||||
}
|
||||
img = imaging.ResizeWithOpacity(img, w, h, imaging.Lanczos, num_channels == 3)
|
||||
if dnd.drag_thumbnail != nil {
|
||||
dnd.drag_thumbnail = img
|
||||
}
|
||||
}
|
||||
var pix []byte
|
||||
if imaging.IsOpaque(img) {
|
||||
_, pix = 3, imaging.AsRGBData8(img)
|
||||
} else {
|
||||
pix = imaging.AsRGBAData8(img)
|
||||
}
|
||||
cmd := DC{
|
||||
Type: 'p', X: -1, Y: utils.IfElse(num_channels == 3, 24, 32), Xp: img.Bounds().Dx(), Yp: img.Bounds().Dy(),
|
||||
Payload: pix}
|
||||
dnd.lp.QueueDnDData(cmd)
|
||||
cmd.Payload = nil
|
||||
dnd.lp.QueueDnDData(cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dnd *dnd) on_potential_drag_start(cell_x, cell_y int) (err error) {
|
||||
if !dnd.allow_drags || dnd.drag_status.active {
|
||||
return
|
||||
|
|
@ -72,7 +140,11 @@ func (dnd *dnd) on_potential_drag_start(cell_x, cell_y int) (err error) {
|
|||
}
|
||||
}
|
||||
dnd.drag_status.offered_mimes = mimes
|
||||
// TODO: set the drag image
|
||||
err = dnd.set_drag_image()
|
||||
if err != nil {
|
||||
dnd.finish_drag("EIO")
|
||||
return err
|
||||
}
|
||||
dnd.lp.QueueDnDData(DC{Type: 'P', X: -1}) // start drag
|
||||
dnd.drag_status.active = true
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package dnd
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"maps"
|
||||
"net/url"
|
||||
|
|
@ -15,6 +16,7 @@ import (
|
|||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/kovidgoyal/imaging"
|
||||
"github.com/kovidgoyal/kitty/tools/cli"
|
||||
"github.com/kovidgoyal/kitty/tools/tty"
|
||||
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
||||
|
|
@ -78,6 +80,7 @@ type dnd struct {
|
|||
opts *Options
|
||||
drop_dests map[string]*drop_dest
|
||||
drag_sources map[string]*drag_source
|
||||
drag_thumbnail image.Image
|
||||
allow_drops, allow_drags bool
|
||||
|
||||
lp *loop.Loop
|
||||
|
|
@ -382,6 +385,12 @@ func dnd_main(cmd *cli.Command, opts *Options, args []string) (rc int, err error
|
|||
}
|
||||
}
|
||||
dnd := dnd{opts: opts, drop_dests: drop_dests, drag_sources: drag_sources}
|
||||
if opts.DragThumbnail != "" {
|
||||
if dnd.drag_thumbnail, err = imaging.Open(opts.DragThumbnail); err != nil {
|
||||
return 1, err
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
dnd.end_drop(false)
|
||||
if dnd.confirm_drop.staging_dir != nil {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,19 @@ default=disallowed
|
|||
type=choices
|
||||
Allow dropping anywhere, not just on the Copy or Move drop regions. Dropping anywhere will perform
|
||||
the specified action.
|
||||
|
||||
|
||||
--drag-thumbnail
|
||||
Path to an image to use as the drag icon when starting a drag. Must be in PNG/JPEG/GIF/WEBP formats.
|
||||
Other formats will require the presence of ImageMagick on the system to load the image.
|
||||
If not specified, an image is derived based on the data being dragged.
|
||||
|
||||
|
||||
--drag-thumbnail-size
|
||||
default=512
|
||||
type=int
|
||||
The thumbnail size for the image used as the drag icon. Images larger than this size are downscaled.
|
||||
Note that the terminal may reject the drag if the image is too large.
|
||||
'''.format
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1405,6 +1405,8 @@ expand_png_data(Window *w, size_t idx) {
|
|||
}
|
||||
#undef fail
|
||||
|
||||
static size_t last_total_image_size = 0;
|
||||
|
||||
void
|
||||
drag_start(Window *w) {
|
||||
if (ds.state != DRAG_SOURCE_BEING_BUILT) abrt(EINVAL);
|
||||
|
|
@ -1424,6 +1426,7 @@ drag_start(Window *w) {
|
|||
if (img.sz != (size_t)img.width * (size_t)img.height * 4u) abrt(EINVAL);
|
||||
}
|
||||
}
|
||||
last_total_image_size = total_size;
|
||||
int err = start_window_drag(w, dnd_is_test_mode());
|
||||
if (err != 0) {
|
||||
abrt(err);
|
||||
|
|
@ -2261,6 +2264,9 @@ dnd_test_probe_state(PyObject *self UNUSED, PyObject *args) {
|
|||
ans, i, PyUnicode_FromString(w->drag_source.items[i].mime_type));
|
||||
return ans;
|
||||
}
|
||||
if (strcmp(q, "drag_thumbnail_size") == 0) {
|
||||
return PyLong_FromSize_t(last_total_image_size);
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -328,6 +328,10 @@ class TestDnDKitten(BaseTest):
|
|||
self.assertEqual(fa.read(), fb.read(), f'{a} ({os.path.getsize(a)}) != {b} ({os.path.getsize(b)})')
|
||||
|
||||
def test_dnd_kitten_drag(self):
|
||||
from .graphics import png_data
|
||||
drag_thumbnail = os.path.join(self.test_dir, 'drag.png')
|
||||
with open(drag_thumbnail, 'wb') as f:
|
||||
f.write(png_data)
|
||||
img_drag_path = 'image.png'
|
||||
def create_files():
|
||||
with open(os.path.join(self.kitten_wd, img_drag_path), 'wb') as f:
|
||||
|
|
@ -336,7 +340,7 @@ class TestDnDKitten(BaseTest):
|
|||
create_fs(self.src_data_dir)
|
||||
create_files()
|
||||
tl = tuple(os.path.join(self.src_data_dir, x) for x in os.listdir(self.src_data_dir))
|
||||
self.finish_setup(cli_args=(f'--drag=image/png:{img_drag_path}', ) + tl) # )))
|
||||
self.finish_setup(cli_args=(f'--drag-thumbnail={drag_thumbnail}', f'--drag=image/png:{img_drag_path}') + tl) # )))
|
||||
with self.subTest(remote_client=False):
|
||||
self.dnd_kitten_drag(False, img_drag_path)
|
||||
self.reset_kitten(True)
|
||||
|
|
@ -378,6 +382,7 @@ class TestDnDKitten(BaseTest):
|
|||
dnd_test_start_drag_offer(self.capture.window_id, x, y)
|
||||
wait_for_drag_active()
|
||||
self.wait_for_state('drag_operations', expected)
|
||||
self.wait_for_state('drag_thumbnail_size', 4)
|
||||
def end_drag(canceled=True):
|
||||
dnd_test_drag_finish(self.capture.window_id, canceled)
|
||||
wait_for_drag_active(False)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ try:
|
|||
from PIL import Image
|
||||
except ImportError:
|
||||
Image = None
|
||||
|
||||
png_data = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==')
|
||||
|
||||
def send_command(screen, cmd, payload=b''):
|
||||
cmd = '\033_G' + cmd
|
||||
|
|
@ -505,7 +505,6 @@ class TestGraphics(BaseTest):
|
|||
|
||||
def test_load_png_simple(self):
|
||||
# 1x1 transparent PNG
|
||||
png_data = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==')
|
||||
expected = b'\x00\xff\xff\x7f'
|
||||
self.ae(load_png_data(png_data), (expected, 1, 1))
|
||||
s, g, pl, sl = load_helpers(self)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue