mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-06 09:55:40 +00:00
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:
+6
-6
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user