io/key: [API] implement key event propagation

Before this change, every Event would be passed to the focused InputOp
tag, making it impossible to implement, say, program-wide shortcuts.
This change implements key.Event routing similar to how pointer.Events
are routed: every InputOp describes the set of keys it can handle, and
the router use that information to deliver an Event to the matching
handler.

This is an API change, because every InputOp must now include a filter
matching the keys it wants to handle.

Fixes: https://todo.sr.ht/~eliasnaur/gio/395
Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2022-04-14 12:18:00 +02:00
parent bec0283e54
commit 380f96b3fc
13 changed files with 266 additions and 46 deletions
+6 -6
View File
@@ -41,6 +41,7 @@ type keyHandler struct {
hint key.InputHint
order int
dirOrder int
filter key.Set
}
// keyCollector tracks state required to update a keyQueue
@@ -254,12 +255,6 @@ func (q *keyQueue) MoveFocus(dir FocusDirection, events *handlerEvents) bool {
return false
}
func (q *keyQueue) Push(e event.Event, events *handlerEvents) {
if q.focus != nil {
events.Add(q.focus, e)
}
}
func (q *keyQueue) BoundsFor(t event.Tag) image.Rectangle {
order := q.handlers[t].dirOrder
return q.dirOrder[order].bounds
@@ -270,6 +265,10 @@ func (q *keyQueue) AreaFor(t event.Tag) int {
return q.dirOrder[order].area
}
func (q *keyQueue) Accepts(t event.Tag, e key.Event) bool {
return q.handlers[t].filter.Contains(e.Name, e.Modifiers)
}
func (q *keyQueue) setFocus(focus event.Tag, events *handlerEvents) {
if focus != nil {
if _, exists := q.handlers[focus]; !exists {
@@ -323,6 +322,7 @@ func (k *keyCollector) inputOp(op key.InputOp, area int, bounds image.Rectangle)
h := k.handlerFor(op.Tag, area, bounds)
h.visible = true
h.hint = op.Hint
h.filter = op.Keys
}
func (k *keyCollector) selectionOp(t f32.Affine2D, op key.SelectionOp) {
+28 -3
View File
@@ -103,12 +103,12 @@ func TestKeyRemoveFocus(t *testing.T) {
r := new(Router)
// New InputOp with Focus and Keyboard:
key.InputOp{Tag: &handlers[0]}.Add(ops)
key.InputOp{Tag: &handlers[0], Keys: "Short-Tab"}.Add(ops)
key.FocusOp{Tag: &handlers[0]}.Add(ops)
key.SoftKeyboardOp{Show: true}.Add(ops)
// New InputOp without any focus:
key.InputOp{Tag: &handlers[1]}.Add(ops)
key.InputOp{Tag: &handlers[1], Keys: "Short-Tab"}.Add(ops)
r.Frame(ops)
@@ -320,6 +320,31 @@ func TestNoFocus(t *testing.T) {
r.MoveFocus(FocusForward)
}
func TestKeyRouting(t *testing.T) {
handlers := make([]int, 3)
ops := new(op.Ops)
r := new(Router)
rect := clip.Rect{Max: image.Pt(10, 10)}
key.InputOp{Tag: &handlers[0], Keys: "A"}.Add(ops)
cl1 := rect.Push(ops)
key.InputOp{Tag: &handlers[1], Keys: "B"}.Add(ops)
key.InputOp{Tag: &handlers[2], Keys: "A"}.Add(ops)
cl1.Pop()
key.FocusOp{Tag: &handlers[2]}.Add(ops)
r.Frame(ops)
A, B := key.Event{Name: "A"}, key.Event{Name: "B"}
r.Queue(A, B)
assertKeyEvent(t, r.Events(&handlers[2]), true, A)
assertKeyEvent(t, r.Events(&handlers[1]), false, B)
assertKeyEvent(t, r.Events(&handlers[0]), false)
}
func assertKeyEvent(t *testing.T, events []event.Event, expected bool, expectedInputs ...event.Event) {
t.Helper()
var evtFocus int
@@ -333,7 +358,7 @@ func assertKeyEvent(t *testing.T, events []event.Event, expected bool, expectedI
evtFocus++
case key.Event, key.EditEvent:
if len(expectedInputs) <= evtKeyPress {
t.Errorf("unexpected key events")
t.Fatalf("unexpected key events")
}
if !reflect.DeepEqual(ev, expectedInputs[evtKeyPress]) {
t.Errorf("expected %v events, got %v", expectedInputs[evtKeyPress], ev)
+24
View File
@@ -9,6 +9,7 @@ import (
"gioui.org/f32"
"gioui.org/internal/ops"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/semantic"
"gioui.org/io/transfer"
@@ -40,6 +41,7 @@ type hitNode struct {
// For handler nodes.
tag event.Tag
ktag event.Tag
pass bool
}
@@ -258,6 +260,15 @@ func (c *pointerCollector) newHandler(tag event.Tag, events *handlerEvents) *poi
return h
}
func (c *pointerCollector) keyInputOp(op key.InputOp) {
areaID := c.currentArea()
c.addHitNode(hitNode{
area: areaID,
ktag: op.Tag,
pass: true,
})
}
func (c *pointerCollector) inputOp(op pointer.InputOp, events *handlerEvents) {
areaID := c.currentArea()
area := &c.q.areas[areaID]
@@ -636,6 +647,19 @@ func (q *pointerQueue) Deliver(areaIdx int, e pointer.Event, events *handlerEven
}
}
// SemanticArea returns the sematic content for area, and its parent area.
func (q *pointerQueue) SemanticArea(areaIdx int) (semanticContent, int) {
for areaIdx != -1 {
a := &q.areas[areaIdx]
areaIdx = a.parent
if !a.semantic.valid {
continue
}
return a.semantic.content, areaIdx
}
return semanticContent{}, -1
}
func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
if e.Type == pointer.Cancel {
q.pointers = q.pointers[:0]
+39 -2
View File
@@ -140,8 +140,42 @@ func (q *Router) Queue(events ...event.Event) bool {
q.profile = e
case pointer.Event:
q.pointer.queue.Push(e, &q.handlers)
case key.EditEvent, key.Event, key.FocusEvent, key.SnippetEvent, key.SelectionEvent:
q.key.queue.Push(e, &q.handlers)
case key.Event:
f := q.key.queue.focus
if f == nil {
break
}
kq := &q.key.queue
if kq.Accepts(f, e) {
q.handlers.Add(f, e)
break
}
a := kq.AreaFor(f)
pq := &q.pointer.queue
idx := len(pq.hitTree) - 1
// Locate first potential receiver.
for idx != -1 {
n := &pq.hitTree[idx]
if n.area == a {
break
}
idx--
}
for idx != -1 {
n := &pq.hitTree[idx]
idx = n.next
if n.ktag == nil {
continue
}
if n.ktag != nil && kq.Accepts(n.ktag, e) {
q.handlers.Add(n.ktag, e)
break
}
}
case key.EditEvent, key.FocusEvent, key.SnippetEvent, key.SelectionEvent:
if f := q.key.queue.focus; f != nil {
q.handlers.Add(f, e)
}
case clipboard.Event:
q.cqueue.Push(e, &q.handlers)
}
@@ -398,12 +432,15 @@ func (q *Router) collect() {
}
kc.softKeyboard(op.Show)
case ops.TypeKeyInput:
filter := encOp.Refs[1].(*key.Set)
op := key.InputOp{
Tag: encOp.Refs[0].(event.Tag),
Hint: key.InputHint(encOp.Data[1]),
Keys: *filter,
}
a := pc.currentArea()
b := pc.currentAreaBounds()
pc.keyInputOp(op)
kc.inputOp(op, a, b)
case ops.TypeSnippet:
op := key.SnippetOp{