mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-07 18:35:34 +00:00
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 <inkeliz@inkeliz.com>
This commit is contained in:
@@ -168,6 +168,12 @@ func (w *Window) processFrame(frameStart time.Time, size image.Point, frame *op.
|
|||||||
case router.TextInputClose:
|
case router.TextInputClose:
|
||||||
w.driver.ShowTextInput(false)
|
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() {
|
if w.queue.q.Profiling() {
|
||||||
frameDur := time.Since(frameStart)
|
frameDur := time.Since(frameStart)
|
||||||
frameDur = frameDur.Truncate(100 * time.Microsecond)
|
frameDur = frameDur.Truncate(100 * time.Microsecond)
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ const (
|
|||||||
TypeArea
|
TypeArea
|
||||||
TypePointerInput
|
TypePointerInput
|
||||||
TypePass
|
TypePass
|
||||||
|
TypeClipboardRead
|
||||||
|
TypeClipboardWrite
|
||||||
TypeKeyInput
|
TypeKeyInput
|
||||||
TypeKeyFocus
|
TypeKeyFocus
|
||||||
TypeKeySoftKeyboard
|
TypeKeySoftKeyboard
|
||||||
@@ -43,6 +45,8 @@ const (
|
|||||||
TypeAreaLen = 1 + 1 + 4*4
|
TypeAreaLen = 1 + 1 + 4*4
|
||||||
TypePointerInputLen = 1 + 1 + 1
|
TypePointerInputLen = 1 + 1 + 1
|
||||||
TypePassLen = 1 + 1
|
TypePassLen = 1 + 1
|
||||||
|
TypeClipboardReadLen = 1
|
||||||
|
TypeClipboardWriteLen = 1
|
||||||
TypeKeyInputLen = 1
|
TypeKeyInputLen = 1
|
||||||
TypeKeyFocusLen = 1 + 1
|
TypeKeyFocusLen = 1 + 1
|
||||||
TypeKeySoftKeyboardLen = 1 + 1
|
TypeKeySoftKeyboardLen = 1 + 1
|
||||||
@@ -67,6 +71,8 @@ func (t OpType) Size() int {
|
|||||||
TypeAreaLen,
|
TypeAreaLen,
|
||||||
TypePointerInputLen,
|
TypePointerInputLen,
|
||||||
TypePassLen,
|
TypePassLen,
|
||||||
|
TypeClipboardReadLen,
|
||||||
|
TypeClipboardWriteLen,
|
||||||
TypeKeyInputLen,
|
TypeKeyInputLen,
|
||||||
TypeKeyFocusLen,
|
TypeKeyFocusLen,
|
||||||
TypeKeySoftKeyboardLen,
|
TypeKeySoftKeyboardLen,
|
||||||
@@ -80,7 +86,7 @@ func (t OpType) Size() int {
|
|||||||
|
|
||||||
func (t OpType) NumRefs() int {
|
func (t OpType) NumRefs() int {
|
||||||
switch t {
|
switch t {
|
||||||
case TypeKeyInput, TypePointerInput, TypeProfile, TypeCall:
|
case TypeKeyInput, TypePointerInput, TypeProfile, TypeCall, TypeClipboardRead, TypeClipboardWrite:
|
||||||
return 1
|
return 1
|
||||||
case TypeImage:
|
case TypeImage:
|
||||||
return 2
|
return 2
|
||||||
|
|||||||
@@ -2,9 +2,36 @@
|
|||||||
|
|
||||||
package clipboard
|
package clipboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gioui.org/internal/opconst"
|
||||||
|
"gioui.org/io/event"
|
||||||
|
"gioui.org/op"
|
||||||
|
)
|
||||||
|
|
||||||
// Event is generated when the clipboard content is requested.
|
// Event is generated when the clipboard content is requested.
|
||||||
type Event struct {
|
type Event struct {
|
||||||
Text string
|
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() {}
|
func (Event) ImplementsEvent() {}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
|
|
||||||
"gioui.org/internal/opconst"
|
"gioui.org/internal/opconst"
|
||||||
"gioui.org/internal/ops"
|
"gioui.org/internal/ops"
|
||||||
|
"gioui.org/io/clipboard"
|
||||||
"gioui.org/io/event"
|
"gioui.org/io/event"
|
||||||
"gioui.org/io/key"
|
"gioui.org/io/key"
|
||||||
"gioui.org/io/pointer"
|
"gioui.org/io/pointer"
|
||||||
@@ -28,6 +29,7 @@ import (
|
|||||||
type Router struct {
|
type Router struct {
|
||||||
pqueue pointerQueue
|
pqueue pointerQueue
|
||||||
kqueue keyQueue
|
kqueue keyQueue
|
||||||
|
cqueue clipboardQueue
|
||||||
|
|
||||||
handlers handlerEvents
|
handlers handlerEvents
|
||||||
|
|
||||||
@@ -88,6 +90,8 @@ func (q *Router) Add(events ...event.Event) bool {
|
|||||||
q.pqueue.Push(e, &q.handlers)
|
q.pqueue.Push(e, &q.handlers)
|
||||||
case key.EditEvent, key.Event, key.FocusEvent:
|
case key.EditEvent, key.Event, key.FocusEvent:
|
||||||
q.kqueue.Push(e, &q.handlers)
|
q.kqueue.Push(e, &q.handlers)
|
||||||
|
case clipboard.Event:
|
||||||
|
q.cqueue.Push(e, &q.handlers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return q.handlers.HadEvents()
|
return q.handlers.HadEvents()
|
||||||
@@ -99,6 +103,18 @@ func (q *Router) TextInputState() TextInputState {
|
|||||||
return q.kqueue.InputState()
|
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() {
|
func (q *Router) collect() {
|
||||||
for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
|
for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
|
||||||
switch opconst.OpType(encOp.Data[0]) {
|
switch opconst.OpType(encOp.Data[0]) {
|
||||||
@@ -115,6 +131,10 @@ func (q *Router) collect() {
|
|||||||
}
|
}
|
||||||
q.profiling = true
|
q.profiling = true
|
||||||
q.profHandlers[op.Tag] = struct{}{}
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user