From a76f816ae994934c325f9150ab5aadc0a2307376 Mon Sep 17 00:00:00 2001 From: Inkeliz Date: Sun, 6 Dec 2020 20:17:25 +0000 Subject: [PATCH] io/clipboard,app: add WriteOp, ReadOp Previously, the only way to manipulate the clipboard (read or write) is using the `app.Window`. The new `clipboard.ReadOp` and `clipboard.WriteOp`makes possible to read/write from the widget. Signed-off-by: Inkeliz --- app/window.go | 6 ++ internal/opconst/ops.go | 8 +- io/clipboard/clipboard.go | 27 +++++++ io/router/clipboard.go | 66 ++++++++++++++++ io/router/clipboard_test.go | 153 ++++++++++++++++++++++++++++++++++++ io/router/router.go | 20 +++++ 6 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 io/router/clipboard.go create mode 100644 io/router/clipboard_test.go diff --git a/app/window.go b/app/window.go index bd5c5bb7..a2b5c0ec 100644 --- a/app/window.go +++ b/app/window.go @@ -168,6 +168,12 @@ func (w *Window) processFrame(frameStart time.Time, size image.Point, frame *op. case router.TextInputClose: w.driver.ShowTextInput(false) } + if txt, ok := w.queue.q.WriteClipboard(); ok { + go w.WriteClipboard(txt) + } + if w.queue.q.ReadClipboard() { + go w.ReadClipboard() + } if w.queue.q.Profiling() { frameDur := time.Since(frameStart) frameDur = frameDur.Truncate(100 * time.Microsecond) diff --git a/internal/opconst/ops.go b/internal/opconst/ops.go index dba0f542..a1d1e62c 100644 --- a/internal/opconst/ops.go +++ b/internal/opconst/ops.go @@ -20,6 +20,8 @@ const ( TypeArea TypePointerInput TypePass + TypeClipboardRead + TypeClipboardWrite TypeKeyInput TypeKeyFocus TypeKeySoftKeyboard @@ -43,6 +45,8 @@ const ( TypeAreaLen = 1 + 1 + 4*4 TypePointerInputLen = 1 + 1 + 1 TypePassLen = 1 + 1 + TypeClipboardReadLen = 1 + TypeClipboardWriteLen = 1 TypeKeyInputLen = 1 TypeKeyFocusLen = 1 + 1 TypeKeySoftKeyboardLen = 1 + 1 @@ -67,6 +71,8 @@ func (t OpType) Size() int { TypeAreaLen, TypePointerInputLen, TypePassLen, + TypeClipboardReadLen, + TypeClipboardWriteLen, TypeKeyInputLen, TypeKeyFocusLen, TypeKeySoftKeyboardLen, @@ -80,7 +86,7 @@ func (t OpType) Size() int { func (t OpType) NumRefs() int { switch t { - case TypeKeyInput, TypePointerInput, TypeProfile, TypeCall: + case TypeKeyInput, TypePointerInput, TypeProfile, TypeCall, TypeClipboardRead, TypeClipboardWrite: return 1 case TypeImage: return 2 diff --git a/io/clipboard/clipboard.go b/io/clipboard/clipboard.go index 5acd7685..a899f39a 100644 --- a/io/clipboard/clipboard.go +++ b/io/clipboard/clipboard.go @@ -2,9 +2,36 @@ package clipboard +import ( + "gioui.org/internal/opconst" + "gioui.org/io/event" + "gioui.org/op" +) + // Event is generated when the clipboard content is requested. type Event struct { Text string } +// 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 := o.Write1(opconst.TypeClipboardReadLen, h.Tag) + data[0] = byte(opconst.TypeClipboardRead) +} + +func (h WriteOp) Add(o *op.Ops) { + data := o.Write1(opconst.TypeClipboardWriteLen, &h.Text) + data[0] = byte(opconst.TypeClipboardWrite) +} + func (Event) ImplementsEvent() {} diff --git a/io/router/clipboard.go b/io/router/clipboard.go new file mode 100644 index 00000000..935dfb75 --- /dev/null +++ b/io/router/clipboard.go @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package router + +import ( + "gioui.org/internal/opconst" + "gioui.org/internal/ops" + "gioui.org/io/event" +) + +type clipboardQueue struct { + receivers map[event.Tag]struct{} + // request avoid read clipboard every frame while waiting. + requested bool + text *string + reader ops.Reader +} + +// WriteClipboard returns the most recent text to be copied +// to the clipboard, if any. +func (q *clipboardQueue) WriteClipboard() (string, bool) { + if q.text == nil { + return "", false + } + text := *q.text + q.text = nil + return text, true +} + +// ReadClipboard reports if any new handler is waiting +// to read the clipboard. +func (q *clipboardQueue) ReadClipboard() bool { + if len(q.receivers) <= 0 || q.requested { + return false + } + q.requested = true + return true +} + +func (q *clipboardQueue) Push(e event.Event, events *handlerEvents) { + for r := range q.receivers { + events.Add(r, e) + delete(q.receivers, r) + } +} + +func (q *clipboardQueue) ProcessWriteClipboard(d []byte, refs []interface{}) { + if opconst.OpType(d[0]) != opconst.TypeClipboardWrite { + panic("invalid op") + } + q.text = refs[0].(*string) +} + +func (q *clipboardQueue) ProcessReadClipboard(d []byte, refs []interface{}) { + if opconst.OpType(d[0]) != opconst.TypeClipboardRead { + panic("invalid op") + } + if q.receivers == nil { + q.receivers = make(map[event.Tag]struct{}) + } + tag := refs[0].(event.Tag) + if _, ok := q.receivers[tag]; !ok { + q.receivers[tag] = struct{}{} + q.requested = false + } +} diff --git a/io/router/clipboard_test.go b/io/router/clipboard_test.go new file mode 100644 index 00000000..6aa30798 --- /dev/null +++ b/io/router/clipboard_test.go @@ -0,0 +1,153 @@ +package router + +import ( + "gioui.org/io/clipboard" + "gioui.org/io/event" + "gioui.org/op" + "testing" +) + +func TestClipboardDuplicateEvent(t *testing.T) { + ops, router, handler := new(op.Ops), new(Router), make([]int, 2) + + // Both must receive the event once + clipboard.ReadOp{Tag: &handler[0]}.Add(ops) + clipboard.ReadOp{Tag: &handler[1]}.Add(ops) + + router.Frame(ops) + event := clipboard.Event{Text: "Test"} + router.Add(event) + assertClipboardReadOp(t, router, 0) + assertClipboardEvent(t, router.Events(&handler[0]), true) + assertClipboardEvent(t, router.Events(&handler[1]), true) + ops.Reset() + + // No ReadOp + + router.Frame(ops) + assertClipboardReadOp(t, router, 0) + assertClipboardEvent(t, router.Events(&handler[0]), false) + assertClipboardEvent(t, router.Events(&handler[1]), false) + ops.Reset() + + clipboard.ReadOp{Tag: &handler[0]}.Add(ops) + + router.Frame(ops) + // No ClipboardEvent sent + assertClipboardReadOp(t, router, 1) + assertClipboardEvent(t, router.Events(&handler[0]), false) + assertClipboardEvent(t, router.Events(&handler[1]), false) + ops.Reset() +} + +func TestQueueProcessReadClipboard(t *testing.T) { + ops, router, handler := new(op.Ops), new(Router), make([]int, 2) + ops.Reset() + + // Request read + clipboard.ReadOp{Tag: &handler[0]}.Add(ops) + + router.Frame(ops) + assertClipboardReadOp(t, router, 1) + ops.Reset() + + for i := 0; i < 3; i++ { + // No ReadOp + // One receiver must still wait for response + + router.Frame(ops) + assertClipboardReadOpDuplicated(t, router, 1) + ops.Reset() + } + + router.Frame(ops) + // Send the clipboard event + event := clipboard.Event{Text: "Text 2"} + router.Add(event) + assertClipboardReadOp(t, router, 0) + assertClipboardEvent(t, router.Events(&handler[0]), true) + ops.Reset() + + // No ReadOp + // There's no receiver waiting + + router.Frame(ops) + assertClipboardReadOp(t, router, 0) + assertClipboardEvent(t, router.Events(&handler[0]), false) + ops.Reset() +} + +func TestQueueProcessWriteClipboard(t *testing.T) { + ops, router := new(op.Ops), new(Router) + ops.Reset() + + clipboard.WriteOp{Text: "Write 1"}.Add(ops) + + router.Frame(ops) + assertClipboardWriteOp(t, router, "Write 1") + ops.Reset() + + // No WriteOp + + router.Frame(ops) + assertClipboardWriteOp(t, router, "") + ops.Reset() + + clipboard.WriteOp{Text: "Write 2"}.Add(ops) + + router.Frame(ops) + assertClipboardReadOp(t, router, 0) + assertClipboardWriteOp(t, router, "Write 2") + ops.Reset() +} + +func assertClipboardEvent(t *testing.T, events []event.Event, expected bool) { + t.Helper() + var evtClipboard int + for _, e := range events { + switch e.(type) { + case clipboard.Event: + evtClipboard++ + } + } + if evtClipboard <= 0 && expected { + t.Error("expected to receive some event") + } + if evtClipboard > 0 && !expected { + t.Error("unexpected event received") + } +} + +func assertClipboardReadOp(t *testing.T, router *Router, expected int) { + t.Helper() + if len(router.cqueue.receivers) != expected { + t.Error("unexpected number of receivers") + } + if router.cqueue.ReadClipboard() != (expected > 0) { + t.Error("missing requests") + } +} + +func assertClipboardReadOpDuplicated(t *testing.T, router *Router, expected int) { + t.Helper() + if len(router.cqueue.receivers) != expected { + t.Error("receivers removed") + } + if router.cqueue.ReadClipboard() != false { + t.Error("duplicated requests") + } +} + +func assertClipboardWriteOp(t *testing.T, router *Router, expected string) { + t.Helper() + if (router.cqueue.text != nil) != (expected != "") { + t.Error("text not defined") + } + text, ok := router.cqueue.WriteClipboard() + if ok != (expected != "") { + t.Error("duplicated requests") + } + if text != expected { + t.Errorf("got text %s, expected %s", text, expected) + } +} diff --git a/io/router/router.go b/io/router/router.go index d24d2146..803efa7c 100644 --- a/io/router/router.go +++ b/io/router/router.go @@ -16,6 +16,7 @@ import ( "gioui.org/internal/opconst" "gioui.org/internal/ops" + "gioui.org/io/clipboard" "gioui.org/io/event" "gioui.org/io/key" "gioui.org/io/pointer" @@ -28,6 +29,7 @@ import ( type Router struct { pqueue pointerQueue kqueue keyQueue + cqueue clipboardQueue handlers handlerEvents @@ -88,6 +90,8 @@ func (q *Router) Add(events ...event.Event) bool { q.pqueue.Push(e, &q.handlers) case key.EditEvent, key.Event, key.FocusEvent: q.kqueue.Push(e, &q.handlers) + case clipboard.Event: + q.cqueue.Push(e, &q.handlers) } } return q.handlers.HadEvents() @@ -99,6 +103,18 @@ func (q *Router) TextInputState() TextInputState { return q.kqueue.InputState() } +// WriteClipboard returns the most recent text to be copied +// to the clipboard, if any. +func (q *Router) WriteClipboard() (string, bool) { + return q.cqueue.WriteClipboard() +} + +// ReadClipboard reports if any new handler is waiting +// to read the clipboard. +func (q *Router) ReadClipboard() bool { + return q.cqueue.ReadClipboard() +} + func (q *Router) collect() { for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() { switch opconst.OpType(encOp.Data[0]) { @@ -115,6 +131,10 @@ func (q *Router) collect() { } q.profiling = true q.profHandlers[op.Tag] = struct{}{} + case opconst.TypeClipboardRead: + q.cqueue.ProcessReadClipboard(encOp.Data, encOp.Refs) + case opconst.TypeClipboardWrite: + q.cqueue.ProcessWriteClipboard(encOp.Data, encOp.Refs) } } }