mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-06 01:45:36 +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:
+2
-2
@@ -423,9 +423,9 @@ func (t OpType) Size() int {
|
|||||||
|
|
||||||
func (t OpType) NumRefs() int {
|
func (t OpType) NumRefs() int {
|
||||||
switch t {
|
switch t {
|
||||||
case TypeKeyInput, TypeKeyFocus, TypePointerInput, TypeProfile, TypeCall, TypeClipboardRead, TypeClipboardWrite, TypeSemanticLabel, TypeSemanticDesc, TypeSelection:
|
case TypeKeyFocus, TypePointerInput, TypeProfile, TypeCall, TypeClipboardRead, TypeClipboardWrite, TypeSemanticLabel, TypeSemanticDesc, TypeSelection:
|
||||||
return 1
|
return 1
|
||||||
case TypeImage, TypeSource, TypeTarget, TypeSnippet:
|
case TypeKeyInput, TypeImage, TypeSource, TypeTarget, TypeSnippet:
|
||||||
return 2
|
return 2
|
||||||
case TypeOffer:
|
case TypeOffer:
|
||||||
return 3
|
return 3
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ The following example declares a handler ready for key input:
|
|||||||
|
|
||||||
ops := new(op.Ops)
|
ops := new(op.Ops)
|
||||||
var h *Handler = ...
|
var h *Handler = ...
|
||||||
key.InputOp{Tag: h}.Add(ops)
|
key.InputOp{Tag: h, Filter: ...}.Add(ops)
|
||||||
|
|
||||||
*/
|
*/
|
||||||
package event
|
package event
|
||||||
|
|||||||
+110
-2
@@ -25,10 +25,30 @@ import (
|
|||||||
// Key events are in general only delivered to the
|
// Key events are in general only delivered to the
|
||||||
// focused key handler.
|
// focused key handler.
|
||||||
type InputOp struct {
|
type InputOp struct {
|
||||||
Tag event.Tag
|
Tag event.Tag
|
||||||
|
// Hint describes the type of text expected by Tag.
|
||||||
Hint InputHint
|
Hint InputHint
|
||||||
|
// Keys is the set of keys Tag can handle. That is, Tag will only
|
||||||
|
// receive an Event if its key and modifiers are accepted by Keys.Contains.
|
||||||
|
Keys Set
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set is an expression that describes a set of key combinations, in the form
|
||||||
|
// "<modifiers>-<keyset>|...". Modifiers are separated by dashes, optional
|
||||||
|
// modifiers are enclosed by parentheses. A key set is either a literal key
|
||||||
|
// name or a list of key names separated by commas and enclosed in brackets.
|
||||||
|
//
|
||||||
|
// The "Short" modifier matches the shortcut modifier (ModShortcut) and
|
||||||
|
// "ShortAlt" matches the alternative modifier (ModShortcutAlt).
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// - A|B matches the A and B keys
|
||||||
|
// - [A,B] also matches the A and B keys
|
||||||
|
// - Shift-A matches A key if shift is pressed, and no other modifier.
|
||||||
|
// - Shift-(Ctrl)-A matches A if shift is pressed, and optionally ctrl.
|
||||||
|
type Set string
|
||||||
|
|
||||||
// SoftKeyboardOp shows or hide the on-screen keyboard, if available.
|
// SoftKeyboardOp shows or hide the on-screen keyboard, if available.
|
||||||
// It replaces any previous SoftKeyboardOp.
|
// It replaces any previous SoftKeyboardOp.
|
||||||
type SoftKeyboardOp struct {
|
type SoftKeyboardOp struct {
|
||||||
@@ -207,11 +227,99 @@ func (m Modifiers) Contain(m2 Modifiers) bool {
|
|||||||
return m&m2 == m2
|
return m&m2 == m2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (k Set) Contains(name string, mods Modifiers) bool {
|
||||||
|
ks := string(k)
|
||||||
|
for len(ks) > 0 {
|
||||||
|
// Cut next key expression.
|
||||||
|
chord, rest, _ := cut(ks, "|")
|
||||||
|
ks = rest
|
||||||
|
// Separate key set and modifier set.
|
||||||
|
var modSet, keySet string
|
||||||
|
sep := strings.LastIndex(chord, "-")
|
||||||
|
if sep != -1 {
|
||||||
|
modSet, keySet = chord[:sep], chord[sep+1:]
|
||||||
|
} else {
|
||||||
|
modSet, keySet = "", chord
|
||||||
|
}
|
||||||
|
if !keySetContains(keySet, name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if modSetContains(modSet, mods) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func keySetContains(keySet, name string) bool {
|
||||||
|
// Check for single key match.
|
||||||
|
if keySet == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Check for set match.
|
||||||
|
if len(keySet) < 2 || keySet[0] != '[' || keySet[len(keySet)-1] != ']' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
keySet = keySet[1 : len(keySet)-1]
|
||||||
|
for len(keySet) > 0 {
|
||||||
|
key, rest, _ := cut(keySet, ",")
|
||||||
|
keySet = rest
|
||||||
|
if key == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func modSetContains(modSet string, mods Modifiers) bool {
|
||||||
|
var smods Modifiers
|
||||||
|
for len(modSet) > 0 {
|
||||||
|
mod, rest, _ := cut(modSet, "-")
|
||||||
|
modSet = rest
|
||||||
|
if len(mod) >= 2 && mod[0] == '(' && mod[len(mod)-1] == ')' {
|
||||||
|
mods &^= modFor(mod[1 : len(mod)-1])
|
||||||
|
} else {
|
||||||
|
smods |= modFor(mod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mods == smods
|
||||||
|
}
|
||||||
|
|
||||||
|
// cut is a copy of the standard library strings.Cut.
|
||||||
|
// TODO: remove when Go 1.18 is our minimum.
|
||||||
|
func cut(s, sep string) (before, after string, found bool) {
|
||||||
|
if i := strings.Index(s, sep); i >= 0 {
|
||||||
|
return s[:i], s[i+len(sep):], true
|
||||||
|
}
|
||||||
|
return s, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func modFor(name string) Modifiers {
|
||||||
|
switch name {
|
||||||
|
case NameCtrl:
|
||||||
|
return ModCtrl
|
||||||
|
case NameShift:
|
||||||
|
return ModShift
|
||||||
|
case NameAlt:
|
||||||
|
return ModAlt
|
||||||
|
case NameSuper:
|
||||||
|
return ModSuper
|
||||||
|
case NameCommand:
|
||||||
|
return ModCommand
|
||||||
|
case "Short":
|
||||||
|
return ModShortcut
|
||||||
|
case "ShortAlt":
|
||||||
|
return ModShortcutAlt
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func (h InputOp) Add(o *op.Ops) {
|
func (h InputOp) Add(o *op.Ops) {
|
||||||
if h.Tag == nil {
|
if h.Tag == nil {
|
||||||
panic("Tag must be non-nil")
|
panic("Tag must be non-nil")
|
||||||
}
|
}
|
||||||
data := ops.Write1(&o.Internal, ops.TypeKeyInputLen, h.Tag)
|
filter := h.Keys
|
||||||
|
data := ops.Write2(&o.Internal, ops.TypeKeyInputLen, h.Tag, &filter)
|
||||||
data[0] = byte(ops.TypeKeyInput)
|
data[0] = byte(ops.TypeKeyInput)
|
||||||
data[1] = byte(h.Hint)
|
data[1] = byte(h.Hint)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
package key
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKeySet(t *testing.T) {
|
||||||
|
const allMods = ModAlt | ModShift | ModSuper | ModCtrl | ModCommand
|
||||||
|
tests := []struct {
|
||||||
|
Set Set
|
||||||
|
Matches []Event
|
||||||
|
Mismatches []Event
|
||||||
|
}{
|
||||||
|
{"A", []Event{{Name: "A"}}, []Event{{Name: "A", Modifiers: ModShift}}},
|
||||||
|
{"[A,B,C]", []Event{{Name: "A"}, {Name: "B"}}, []Event{}},
|
||||||
|
{"Short-A", []Event{{Name: "A", Modifiers: ModShortcut}}, []Event{{Name: "A", Modifiers: ModShift}}},
|
||||||
|
{"(Ctrl)-A", []Event{{Name: "A", Modifiers: ModCtrl}, {Name: "A"}}, []Event{{Name: "A", Modifiers: ModShift}}},
|
||||||
|
{"Shift-[A,B,C]", []Event{{Name: "A", Modifiers: ModShift}}, []Event{{Name: "B", Modifiers: ModShift | ModCtrl}}},
|
||||||
|
{Set(allMods.String() + "-A"), []Event{{Name: "A", Modifiers: allMods}}, []Event{}},
|
||||||
|
}
|
||||||
|
for _, tst := range tests {
|
||||||
|
for _, e := range tst.Matches {
|
||||||
|
if !tst.Set.Contains(e.Name, e.Modifiers) {
|
||||||
|
t.Errorf("key set %q didn't contain %+v", tst.Set, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, e := range tst.Mismatches {
|
||||||
|
if tst.Set.Contains(e.Name, e.Modifiers) {
|
||||||
|
t.Errorf("key set %q contains %+v", tst.Set, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-2
@@ -5,6 +5,10 @@
|
|||||||
|
|
||||||
package key
|
package key
|
||||||
|
|
||||||
// ModShortcut is the platform's shortcut modifier, usually the Ctrl
|
// ModShortcut is the platform's shortcut modifier, usually the ctrl
|
||||||
// key. On Apple platforms it is the Cmd key.
|
// modifier. On Apple platforms it is the cmd key.
|
||||||
const ModShortcut = ModCtrl
|
const ModShortcut = ModCtrl
|
||||||
|
|
||||||
|
// ModShortcutAlt is the platform's alternative shortcut modifier,
|
||||||
|
// usually the ctrl modifier. On Apple platforms it is the alt modifier.
|
||||||
|
const ModShortcutAlt = ModCtrl
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
package key
|
package key
|
||||||
|
|
||||||
// ModShortcut is the platform's shortcut modifier, usually the Ctrl
|
// ModShortcut is the platform's shortcut modifier, usually the ctrl
|
||||||
// key. On Apple platforms it is the Cmd key.
|
// modifier. On Apple platforms it is the cmd key.
|
||||||
const ModShortcut = ModCommand
|
const ModShortcut = ModCommand
|
||||||
|
|
||||||
|
// ModShortcut is the platform's alternative shortcut modifier,
|
||||||
|
// usually the ctrl modifier. On Apple platforms it is the alt modifier.
|
||||||
|
const ModShortcutAlt = ModAlt
|
||||||
|
|||||||
+6
-6
@@ -41,6 +41,7 @@ type keyHandler struct {
|
|||||||
hint key.InputHint
|
hint key.InputHint
|
||||||
order int
|
order int
|
||||||
dirOrder int
|
dirOrder int
|
||||||
|
filter key.Set
|
||||||
}
|
}
|
||||||
|
|
||||||
// keyCollector tracks state required to update a keyQueue
|
// keyCollector tracks state required to update a keyQueue
|
||||||
@@ -254,12 +255,6 @@ func (q *keyQueue) MoveFocus(dir FocusDirection, events *handlerEvents) bool {
|
|||||||
return false
|
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 {
|
func (q *keyQueue) BoundsFor(t event.Tag) image.Rectangle {
|
||||||
order := q.handlers[t].dirOrder
|
order := q.handlers[t].dirOrder
|
||||||
return q.dirOrder[order].bounds
|
return q.dirOrder[order].bounds
|
||||||
@@ -270,6 +265,10 @@ func (q *keyQueue) AreaFor(t event.Tag) int {
|
|||||||
return q.dirOrder[order].area
|
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) {
|
func (q *keyQueue) setFocus(focus event.Tag, events *handlerEvents) {
|
||||||
if focus != nil {
|
if focus != nil {
|
||||||
if _, exists := q.handlers[focus]; !exists {
|
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 := k.handlerFor(op.Tag, area, bounds)
|
||||||
h.visible = true
|
h.visible = true
|
||||||
h.hint = op.Hint
|
h.hint = op.Hint
|
||||||
|
h.filter = op.Keys
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *keyCollector) selectionOp(t f32.Affine2D, op key.SelectionOp) {
|
func (k *keyCollector) selectionOp(t f32.Affine2D, op key.SelectionOp) {
|
||||||
|
|||||||
+28
-3
@@ -103,12 +103,12 @@ func TestKeyRemoveFocus(t *testing.T) {
|
|||||||
r := new(Router)
|
r := new(Router)
|
||||||
|
|
||||||
// New InputOp with Focus and Keyboard:
|
// 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.FocusOp{Tag: &handlers[0]}.Add(ops)
|
||||||
key.SoftKeyboardOp{Show: true}.Add(ops)
|
key.SoftKeyboardOp{Show: true}.Add(ops)
|
||||||
|
|
||||||
// New InputOp without any focus:
|
// 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)
|
r.Frame(ops)
|
||||||
|
|
||||||
@@ -320,6 +320,31 @@ func TestNoFocus(t *testing.T) {
|
|||||||
r.MoveFocus(FocusForward)
|
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) {
|
func assertKeyEvent(t *testing.T, events []event.Event, expected bool, expectedInputs ...event.Event) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
var evtFocus int
|
var evtFocus int
|
||||||
@@ -333,7 +358,7 @@ func assertKeyEvent(t *testing.T, events []event.Event, expected bool, expectedI
|
|||||||
evtFocus++
|
evtFocus++
|
||||||
case key.Event, key.EditEvent:
|
case key.Event, key.EditEvent:
|
||||||
if len(expectedInputs) <= evtKeyPress {
|
if len(expectedInputs) <= evtKeyPress {
|
||||||
t.Errorf("unexpected key events")
|
t.Fatalf("unexpected key events")
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(ev, expectedInputs[evtKeyPress]) {
|
if !reflect.DeepEqual(ev, expectedInputs[evtKeyPress]) {
|
||||||
t.Errorf("expected %v events, got %v", expectedInputs[evtKeyPress], ev)
|
t.Errorf("expected %v events, got %v", expectedInputs[evtKeyPress], ev)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
"gioui.org/internal/ops"
|
"gioui.org/internal/ops"
|
||||||
"gioui.org/io/event"
|
"gioui.org/io/event"
|
||||||
|
"gioui.org/io/key"
|
||||||
"gioui.org/io/pointer"
|
"gioui.org/io/pointer"
|
||||||
"gioui.org/io/semantic"
|
"gioui.org/io/semantic"
|
||||||
"gioui.org/io/transfer"
|
"gioui.org/io/transfer"
|
||||||
@@ -40,6 +41,7 @@ type hitNode struct {
|
|||||||
|
|
||||||
// For handler nodes.
|
// For handler nodes.
|
||||||
tag event.Tag
|
tag event.Tag
|
||||||
|
ktag event.Tag
|
||||||
pass bool
|
pass bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,6 +260,15 @@ func (c *pointerCollector) newHandler(tag event.Tag, events *handlerEvents) *poi
|
|||||||
return h
|
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) {
|
func (c *pointerCollector) inputOp(op pointer.InputOp, events *handlerEvents) {
|
||||||
areaID := c.currentArea()
|
areaID := c.currentArea()
|
||||||
area := &c.q.areas[areaID]
|
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) {
|
func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
|
||||||
if e.Type == pointer.Cancel {
|
if e.Type == pointer.Cancel {
|
||||||
q.pointers = q.pointers[:0]
|
q.pointers = q.pointers[:0]
|
||||||
|
|||||||
+39
-2
@@ -140,8 +140,42 @@ func (q *Router) Queue(events ...event.Event) bool {
|
|||||||
q.profile = e
|
q.profile = e
|
||||||
case pointer.Event:
|
case pointer.Event:
|
||||||
q.pointer.queue.Push(e, &q.handlers)
|
q.pointer.queue.Push(e, &q.handlers)
|
||||||
case key.EditEvent, key.Event, key.FocusEvent, key.SnippetEvent, key.SelectionEvent:
|
case key.Event:
|
||||||
q.key.queue.Push(e, &q.handlers)
|
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:
|
case clipboard.Event:
|
||||||
q.cqueue.Push(e, &q.handlers)
|
q.cqueue.Push(e, &q.handlers)
|
||||||
}
|
}
|
||||||
@@ -398,12 +432,15 @@ func (q *Router) collect() {
|
|||||||
}
|
}
|
||||||
kc.softKeyboard(op.Show)
|
kc.softKeyboard(op.Show)
|
||||||
case ops.TypeKeyInput:
|
case ops.TypeKeyInput:
|
||||||
|
filter := encOp.Refs[1].(*key.Set)
|
||||||
op := key.InputOp{
|
op := key.InputOp{
|
||||||
Tag: encOp.Refs[0].(event.Tag),
|
Tag: encOp.Refs[0].(event.Tag),
|
||||||
Hint: key.InputHint(encOp.Data[1]),
|
Hint: key.InputHint(encOp.Data[1]),
|
||||||
|
Keys: *filter,
|
||||||
}
|
}
|
||||||
a := pc.currentArea()
|
a := pc.currentArea()
|
||||||
b := pc.currentAreaBounds()
|
b := pc.currentAreaBounds()
|
||||||
|
pc.keyInputOp(op)
|
||||||
kc.inputOp(op, a, b)
|
kc.inputOp(op, a, b)
|
||||||
case ops.TypeSnippet:
|
case ops.TypeSnippet:
|
||||||
op := key.SnippetOp{
|
op := key.SnippetOp{
|
||||||
|
|||||||
+1
-1
@@ -111,7 +111,7 @@ func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimension
|
|||||||
semantic.DisabledOp(disabled).Add(gtx.Ops)
|
semantic.DisabledOp(disabled).Add(gtx.Ops)
|
||||||
b.click.Add(gtx.Ops)
|
b.click.Add(gtx.Ops)
|
||||||
if !disabled {
|
if !disabled {
|
||||||
key.InputOp{Tag: &b.keyTag}.Add(gtx.Ops)
|
key.InputOp{Tag: &b.keyTag, Keys: "⏎|Space"}.Add(gtx.Ops)
|
||||||
} else {
|
} else {
|
||||||
b.focused = false
|
b.focused = false
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-24
@@ -8,7 +8,6 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"runtime"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -364,10 +363,9 @@ func (e *Editor) processKey(gtx layout.Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if e.command(gtx, ke) {
|
e.command(gtx, ke)
|
||||||
e.caret.scroll = true
|
e.caret.scroll = true
|
||||||
e.scroller.Stop()
|
e.scroller.Stop()
|
||||||
}
|
|
||||||
case key.SnippetEvent:
|
case key.SnippetEvent:
|
||||||
e.updateSnippet(gtx, ke.Start, ke.End)
|
e.updateSnippet(gtx, ke.Start, ke.End)
|
||||||
case key.EditEvent:
|
case key.EditEvent:
|
||||||
@@ -403,16 +401,12 @@ func (e *Editor) moveLines(distance int, selAct selectionAction) {
|
|||||||
e.updateSelection(selAct)
|
e.updateSelection(selAct)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Editor) command(gtx layout.Context, k key.Event) bool {
|
func (e *Editor) command(gtx layout.Context, k key.Event) {
|
||||||
direction := 1
|
direction := 1
|
||||||
if e.locale.Direction.Progression() == system.TowardOrigin {
|
if e.locale.Direction.Progression() == system.TowardOrigin {
|
||||||
direction = -1
|
direction = -1
|
||||||
}
|
}
|
||||||
modSkip := key.ModCtrl
|
moveByWord := k.Modifiers.Contain(key.ModShortcutAlt)
|
||||||
if runtime.GOOS == "darwin" {
|
|
||||||
modSkip = key.ModAlt
|
|
||||||
}
|
|
||||||
moveByWord := k.Modifiers.Contain(modSkip)
|
|
||||||
selAct := selectionClear
|
selAct := selectionClear
|
||||||
if k.Modifiers.Contain(key.ModShift) {
|
if k.Modifiers.Contain(key.ModShift) {
|
||||||
selAct = selectionExtend
|
selAct = selectionExtend
|
||||||
@@ -465,15 +459,9 @@ func (e *Editor) command(gtx layout.Context, k key.Event) bool {
|
|||||||
// Initiate a paste operation, by requesting the clipboard contents; other
|
// Initiate a paste operation, by requesting the clipboard contents; other
|
||||||
// half is in Editor.processKey() under clipboard.Event.
|
// half is in Editor.processKey() under clipboard.Event.
|
||||||
case "V":
|
case "V":
|
||||||
if k.Modifiers != key.ModShortcut {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
clipboard.ReadOp{Tag: &e.eventKey}.Add(gtx.Ops)
|
clipboard.ReadOp{Tag: &e.eventKey}.Add(gtx.Ops)
|
||||||
// Copy or Cut selection -- ignored if nothing selected.
|
// Copy or Cut selection -- ignored if nothing selected.
|
||||||
case "C", "X":
|
case "C", "X":
|
||||||
if k.Modifiers != key.ModShortcut {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if text := e.SelectedText(); text != "" {
|
if text := e.SelectedText(); text != "" {
|
||||||
clipboard.WriteOp{Text: text}.Add(gtx.Ops)
|
clipboard.WriteOp{Text: text}.Add(gtx.Ops)
|
||||||
if k.Name == "X" {
|
if k.Name == "X" {
|
||||||
@@ -482,15 +470,9 @@ func (e *Editor) command(gtx layout.Context, k key.Event) bool {
|
|||||||
}
|
}
|
||||||
// Select all
|
// Select all
|
||||||
case "A":
|
case "A":
|
||||||
if k.Modifiers != key.ModShortcut {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
e.caret.end = 0
|
e.caret.end = 0
|
||||||
e.caret.start = e.Len()
|
e.caret.start = e.Len()
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focus requests the input focus for the Editor.
|
// Focus requests the input focus for the Editor.
|
||||||
@@ -625,7 +607,8 @@ func (e *Editor) layout(gtx layout.Context, content layout.Widget) layout.Dimens
|
|||||||
|
|
||||||
defer clip.Rect(image.Rectangle{Max: e.viewSize}).Push(gtx.Ops).Pop()
|
defer clip.Rect(image.Rectangle{Max: e.viewSize}).Push(gtx.Ops).Pop()
|
||||||
pointer.CursorText.Add(gtx.Ops)
|
pointer.CursorText.Add(gtx.Ops)
|
||||||
key.InputOp{Tag: &e.eventKey, Hint: e.InputHint}.Add(gtx.Ops)
|
const keyFilter = "(Short)-(Shift)-[←,→,↑,↓]|(Shift)-[⏎,⌤]|(Short)-(Shift)-[⌫,⌦]|(Shift)-[⇞,⇟,⇱,⇲]|Short-[C,V,X,A]"
|
||||||
|
key.InputOp{Tag: &e.eventKey, Hint: e.InputHint, Keys: keyFilter}.Add(gtx.Ops)
|
||||||
if e.requestFocus {
|
if e.requestFocus {
|
||||||
key.FocusOp{Tag: &e.eventKey}.Add(gtx.Ops)
|
key.FocusOp{Tag: &e.eventKey}.Add(gtx.Ops)
|
||||||
key.SoftKeyboardOp{Show: true}.Add(gtx.Ops)
|
key.SoftKeyboardOp{Show: true}.Add(gtx.Ops)
|
||||||
|
|||||||
+1
-1
@@ -120,7 +120,7 @@ func (e *Enum) Layout(gtx layout.Context, k string, content layout.Widget) layou
|
|||||||
clk.Add(gtx.Ops)
|
clk.Add(gtx.Ops)
|
||||||
disabled := gtx.Queue == nil
|
disabled := gtx.Queue == nil
|
||||||
if !disabled {
|
if !disabled {
|
||||||
key.InputOp{Tag: &state.tag}.Add(gtx.Ops)
|
key.InputOp{Tag: &state.tag, Keys: "⏎|Space"}.Add(gtx.Ops)
|
||||||
} else if e.focus == k {
|
} else if e.focus == k {
|
||||||
e.focused = false
|
e.focused = false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user