From d51aea553f1e37d2b0b980515e777d3e4ab1ee55 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Mon, 9 Oct 2023 18:27:01 -0500 Subject: [PATCH] io/input,io/clipboard: [API] replace WriteOp with command Signed-off-by: Elias Naur --- app/os.go | 2 +- app/os_android.go | 4 ++-- app/os_ios.go | 4 ++-- app/os_js.go | 4 ++-- app/os_macos.go | 4 ++-- app/os_wayland.go | 4 ++-- app/os_windows.go | 4 ++-- app/os_x11.go | 4 ++-- app/window.go | 11 ++--------- internal/ops/ops.go | 5 ----- io/clipboard/clipboard.go | 20 ++++++++++---------- io/input/clipboard.go | 26 ++++++++++++++++++-------- io/input/clipboard_test.go | 24 +++++++++++++++--------- io/input/router.go | 8 ++++---- widget/editor.go | 2 +- widget/selectable.go | 3 ++- 16 files changed, 67 insertions(+), 62 deletions(-) diff --git a/app/os.go b/app/os.go index d030cabb..39595d8f 100644 --- a/app/os.go +++ b/app/os.go @@ -160,7 +160,7 @@ type driver interface { // ReadClipboard requests the clipboard content. ReadClipboard() // WriteClipboard requests a clipboard write. - WriteClipboard(s string) + WriteClipboard(mime string, s []byte) // Configure the window. Configure([]Option) // SetCursor updates the current cursor to name. diff --git a/app/os_android.go b/app/os_android.go index 9e790b52..c3d196cd 100644 --- a/app/os_android.go +++ b/app/os_android.go @@ -1295,9 +1295,9 @@ func newWindow(window *callbacks, options []Option) error { return <-mainWindow.errs } -func (w *window) WriteClipboard(s string) { +func (w *window) WriteClipboard(mime string, s []byte) { runInJVM(javaVM(), func(env *C.JNIEnv) { - jstr := javaString(env, s) + jstr := javaString(env, string(s)) callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard, jvalue(android.appCtx), jvalue(jstr)) }) diff --git a/app/os_ios.go b/app/os_ios.go index d890c265..7b9d22e2 100644 --- a/app/os_ios.go +++ b/app/os_ios.go @@ -268,8 +268,8 @@ func (w *window) ReadClipboard() { w.w.Event(clipboard.Event{Text: content}) } -func (w *window) WriteClipboard(s string) { - u16 := utf16.Encode([]rune(s)) +func (w *window) WriteClipboard(mime string, s []byte) { + u16 := utf16.Encode([]rune(string(s))) var chars *C.unichar if len(u16) > 0 { chars = (*C.unichar)(unsafe.Pointer(&u16[0])) diff --git a/app/os_js.go b/app/os_js.go index fbeaa97f..5b4d39ea 100644 --- a/app/os_js.go +++ b/app/os_js.go @@ -533,14 +533,14 @@ func (w *window) ReadClipboard() { w.clipboard.Call("readText", w.clipboard).Call("then", w.clipboardCallback) } -func (w *window) WriteClipboard(s string) { +func (w *window) WriteClipboard(mime string, s []byte) { if w.clipboard.IsUndefined() { return } if w.clipboard.Get("writeText").IsUndefined() { return } - w.clipboard.Call("writeText", s) + w.clipboard.Call("writeText", string(s)) } func (w *window) Configure(options []Option) { diff --git a/app/os_macos.go b/app/os_macos.go index 09d26121..c06afe9c 100644 --- a/app/os_macos.go +++ b/app/os_macos.go @@ -308,8 +308,8 @@ func (w *window) ReadClipboard() { w.w.Event(clipboard.Event{Text: content}) } -func (w *window) WriteClipboard(s string) { - cstr := stringToNSString(s) +func (w *window) WriteClipboard(mime string, s []byte) { + cstr := stringToNSString(string(s)) defer C.CFRelease(cstr) C.writeClipboard(cstr) } diff --git a/app/os_wayland.go b/app/os_wayland.go index 72e8131c..13e8fbc6 100644 --- a/app/os_wayland.go +++ b/app/os_wayland.go @@ -1033,8 +1033,8 @@ func (w *window) ReadClipboard() { }() } -func (w *window) WriteClipboard(s string) { - w.disp.writeClipboard([]byte(s)) +func (w *window) WriteClipboard(mime string, s []byte) { + w.disp.writeClipboard(s) } func (w *window) Configure(options []Option) { diff --git a/app/os_windows.go b/app/os_windows.go index 467dc91f..20351e61 100644 --- a/app/os_windows.go +++ b/app/os_windows.go @@ -735,8 +735,8 @@ func (w *window) Configure(options []Option) { w.update() } -func (w *window) WriteClipboard(s string) { - w.writeClipboard(s) +func (w *window) WriteClipboard(mime string, s []byte) { + w.writeClipboard(string(s)) } func (w *window) writeClipboard(s string) error { diff --git a/app/os_x11.go b/app/os_x11.go index 4ebc2af4..1ff5f97c 100644 --- a/app/os_x11.go +++ b/app/os_x11.go @@ -151,8 +151,8 @@ func (w *x11Window) ReadClipboard() { C.XConvertSelection(w.x, w.atoms.clipboard, w.atoms.utf8string, w.atoms.clipboardContent, w.xw, C.CurrentTime) } -func (w *x11Window) WriteClipboard(s string) { - w.clipboard.content = []byte(s) +func (w *x11Window) WriteClipboard(mime string, s []byte) { + w.clipboard.content = s C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime) C.XSetSelectionOwner(w.x, w.atoms.primary, w.xw, C.CurrentTime) } diff --git a/app/window.go b/app/window.go index aae43139..c63ac48d 100644 --- a/app/window.go +++ b/app/window.go @@ -316,8 +316,8 @@ func (w *Window) processFrame(d driver) { if hint, ok := q.TextInputHint(); ok { d.SetInputHint(hint) } - if txt, ok := q.WriteClipboard(); ok { - d.WriteClipboard(txt) + if mime, txt, ok := q.WriteClipboard(); ok { + d.WriteClipboard(mime, txt) } if q.ReadClipboard() { d.ReadClipboard() @@ -372,13 +372,6 @@ func (w *Window) Option(opts ...Option) { } } -// WriteClipboard writes a string to the clipboard. -func (w *Window) WriteClipboard(s string) { - w.driverDefer(func(d driver) { - d.WriteClipboard(s) - }) -} - // Run f in the same thread as the native window event loop, and wait for f to // return or the window to close. Run is guaranteed not to deadlock if it is // invoked during the handling of a ViewEvent, system.FrameEvent, diff --git a/internal/ops/ops.go b/internal/ops/ops.go index 23c23580..2747aae3 100644 --- a/internal/ops/ops.go +++ b/internal/ops/ops.go @@ -64,7 +64,6 @@ const ( TypePopPass TypePointerInput TypeClipboardRead - TypeClipboardWrite TypeSource TypeTarget TypeKeyInput @@ -144,7 +143,6 @@ const ( TypePopPassLen = 1 TypePointerInputLen = 1 + 1 + 1*2 + 2*4 + 2*4 TypeClipboardReadLen = 1 - TypeClipboardWriteLen = 1 TypeSourceLen = 1 TypeTargetLen = 1 TypeKeyInputLen = 1 + 1 @@ -422,7 +420,6 @@ var opProps = [0x100]opProp{ TypePopPass: {Size: TypePopPassLen, NumRefs: 0}, TypePointerInput: {Size: TypePointerInputLen, NumRefs: 1}, TypeClipboardRead: {Size: TypeClipboardReadLen, NumRefs: 1}, - TypeClipboardWrite: {Size: TypeClipboardWriteLen, NumRefs: 1}, TypeSource: {Size: TypeSourceLen, NumRefs: 2}, TypeTarget: {Size: TypeTargetLen, NumRefs: 2}, TypeKeyInput: {Size: TypeKeyInputLen, NumRefs: 2}, @@ -489,8 +486,6 @@ func (t OpType) String() string { return "PointerInput" case TypeClipboardRead: return "ClipboardRead" - case TypeClipboardWrite: - return "ClipboardWrite" case TypeSource: return "Source" case TypeTarget: diff --git a/io/clipboard/clipboard.go b/io/clipboard/clipboard.go index 474ae9bb..62b99906 100644 --- a/io/clipboard/clipboard.go +++ b/io/clipboard/clipboard.go @@ -3,6 +3,8 @@ package clipboard import ( + "io" + "gioui.org/internal/ops" "gioui.org/io/event" "gioui.org/op" @@ -13,25 +15,23 @@ type Event struct { Text string } +// WriteCmd copies Text to the clipboard. +type WriteCmd struct { + Type string + Data io.ReadCloser +} + // ReadOp requests the text of the clipboard, delivered to // the current handler through an Event. type ReadOp struct { Tag event.Tag } -// WriteOp copies Text to the clipboard. -type WriteOp struct { - Text string -} - func (h ReadOp) Add(o *op.Ops) { data := ops.Write1(&o.Internal, ops.TypeClipboardReadLen, h.Tag) data[0] = byte(ops.TypeClipboardRead) } -func (h WriteOp) Add(o *op.Ops) { - data := ops.Write1String(&o.Internal, ops.TypeClipboardWriteLen, h.Text) - data[0] = byte(ops.TypeClipboardWrite) -} - func (Event) ImplementsEvent() {} + +func (WriteCmd) ImplementsCommand() {} diff --git a/io/input/clipboard.go b/io/input/clipboard.go index 1f9cbb27..5d881d97 100644 --- a/io/input/clipboard.go +++ b/io/input/clipboard.go @@ -3,6 +3,9 @@ package input import ( + "io" + + "gioui.org/io/clipboard" "gioui.org/io/event" ) @@ -10,18 +13,19 @@ type clipboardQueue struct { receivers map[event.Tag]struct{} // request avoid read clipboard every frame while waiting. requested bool - text *string + mime string + text []byte } -// WriteClipboard returns the most recent text to be copied +// WriteClipboard returns the most recent data to be copied // to the clipboard, if any. -func (q *clipboardQueue) WriteClipboard() (string, bool) { +func (q *clipboardQueue) WriteClipboard() (mime string, content []byte, ok bool) { if q.text == nil { - return "", false + return "", nil, false } - text := *q.text + content = q.text q.text = nil - return text, true + return q.mime, content, true } // ReadClipboard reports if any new handler is waiting @@ -41,8 +45,14 @@ func (q *clipboardQueue) Push(e event.Event, events *handlerEvents) { } } -func (q *clipboardQueue) ProcessWriteClipboard(refs []interface{}) { - q.text = refs[0].(*string) +func (q *clipboardQueue) ProcessWriteClipboard(req clipboard.WriteCmd) { + defer req.Data.Close() + content, err := io.ReadAll(req.Data) + if err != nil { + return + } + q.mime = req.Type + q.text = content } func (q *clipboardQueue) ProcessReadClipboard(refs []interface{}) { diff --git a/io/input/clipboard_test.go b/io/input/clipboard_test.go index 673f1fc7..378c787c 100644 --- a/io/input/clipboard_test.go +++ b/io/input/clipboard_test.go @@ -3,6 +3,8 @@ package input import ( + "io" + "strings" "testing" "gioui.org/io/clipboard" @@ -84,23 +86,24 @@ func TestQueueProcessWriteClipboard(t *testing.T) { ops, router := new(op.Ops), new(Router) ops.Reset() - clipboard.WriteOp{Text: "Write 1"}.Add(ops) + const mime = "application/text" + router.Source().Queue(clipboard.WriteCmd{Type: mime, Data: io.NopCloser(strings.NewReader("Write 1"))}) router.Frame(ops) - assertClipboardWriteOp(t, router, "Write 1") + assertClipboardWriteCmd(t, router, mime, "Write 1") ops.Reset() - // No WriteOp + // No WriteCmd router.Frame(ops) - assertClipboardWriteOp(t, router, "") + assertClipboardWriteCmd(t, router, "", "") ops.Reset() - clipboard.WriteOp{Text: "Write 2"}.Add(ops) + router.Source().Queue(clipboard.WriteCmd{Type: mime, Data: io.NopCloser(strings.NewReader("Write 2"))}) router.Frame(ops) assertClipboardReadOp(t, router, 0) - assertClipboardWriteOp(t, router, "Write 2") + assertClipboardWriteCmd(t, router, mime, "Write 2") ops.Reset() } @@ -141,16 +144,19 @@ func assertClipboardReadOpDuplicated(t *testing.T, router *Router, expected int) } } -func assertClipboardWriteOp(t *testing.T, router *Router, expected string) { +func assertClipboardWriteCmd(t *testing.T, router *Router, mimeExp, expected string) { t.Helper() if (router.cqueue.text != nil) != (expected != "") { t.Error("text not defined") } - text, ok := router.cqueue.WriteClipboard() + mime, text, ok := router.cqueue.WriteClipboard() if ok != (expected != "") { t.Error("duplicated requests") } - if text != expected { + if string(mime) != mimeExp { + t.Errorf("got MIME type %s, expected %s", mime, mimeExp) + } + if string(text) != expected { t.Errorf("got text %s, expected %s", text, expected) } } diff --git a/io/input/router.go b/io/input/router.go index 291f7d9e..82786b67 100644 --- a/io/input/router.go +++ b/io/input/router.go @@ -222,6 +222,8 @@ func (q *Router) executeCommands() { q.key.queue.setSnippet(req) case transfer.OfferCmd: q.pointer.queue.offerData(req, &q.handlers) + case clipboard.WriteCmd: + q.cqueue.ProcessWriteClipboard(req) } } q.commands = nil @@ -374,9 +376,9 @@ func (q *Router) TextInputHint() (key.InputHint, bool) { return q.key.queue.InputHint() } -// WriteClipboard returns the most recent text to be copied +// WriteClipboard returns the most recent content to be copied // to the clipboard, if any. -func (q *Router) WriteClipboard() (string, bool) { +func (q *Router) WriteClipboard() (mime string, content []byte, ok bool) { return q.cqueue.WriteClipboard() } @@ -429,8 +431,6 @@ func (q *Router) collect() { } case ops.TypeClipboardRead: q.cqueue.ProcessReadClipboard(encOp.Refs) - case ops.TypeClipboardWrite: - q.cqueue.ProcessWriteClipboard(encOp.Refs) case ops.TypeSave: id := ops.DecodeSave(encOp.Data) if extra := id - len(q.savedTrans) + 1; extra > 0 { diff --git a/widget/editor.go b/widget/editor.go index c9e8866e..952c162a 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -417,7 +417,7 @@ func (e *Editor) command(gtx layout.Context, k key.Event) { case "C", "X": e.scratch = e.text.SelectedText(e.scratch) if text := string(e.scratch); text != "" { - clipboard.WriteOp{Text: text}.Add(gtx.Ops) + gtx.Queue(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(strings.NewReader(text))}) if k.Name == "X" && !e.ReadOnly { e.Delete(1) } diff --git a/widget/selectable.go b/widget/selectable.go index 10fe805d..adb813b3 100644 --- a/widget/selectable.go +++ b/widget/selectable.go @@ -2,6 +2,7 @@ package widget import ( "image" + "io" "math" "strings" @@ -332,7 +333,7 @@ func (e *Selectable) command(gtx layout.Context, k key.Event) { case "C", "X": e.scratch = e.text.SelectedText(e.scratch) if text := string(e.scratch); text != "" { - clipboard.WriteOp{Text: text}.Add(gtx.Ops) + gtx.Queue(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(strings.NewReader(text))}) } // Select all case "A":