Let an editable view opt into receiving printable keys as keybindings

Printable keys are withheld from keybindings while the user is typing in
a field, so that they end up as text. Decide that from the field that has
the focus rather than from the view a binding happens to be registered
for: a field can be embedded in another view, and that view's keys must
be withheld too, or its bindings would swallow the characters.

That makes it worth honouring KeybindOnEdit, which has been documented
but ignored ever since it was introduced. A field that sets it sees
printable keys offered to the keybindings first, and still gets them if
no binding handles them, which is what lets a view keep its keys until
the field has something to type into.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-08-25 09:17:00 +02:00
parent 28c5f5748c
commit 9e98b3d2f3
2 changed files with 51 additions and 3 deletions

View file

@ -2091,13 +2091,15 @@ func (g *Gui) isSuspended() bool {
return g.suspended
}
// matchView returns if the keybinding matches the current view (and the view's context)
// matchView returns if the keybinding matches the given view (and the view's context)
func (g *Gui) matchView(v *View, kb *keybinding) bool {
// if the user is typing in a field, ignore char keys
if v == nil {
return false
}
if v.Editable && kb.key.IsPrintable() {
// If the user is typing in a field, printable keys are theirs to type, so no
// keybinding gets a look at them: not the field's own, and not those of the
// view it is embedded in either.
if field := g.currentView; field != nil && field.Editable && !field.KeybindOnEdit && kb.key.IsPrintable() {
return false
}
if kb.viewName != v.name {

View file

@ -56,6 +56,52 @@ func TestFirstMatchingKeybindingOfParentViewWins(t *testing.T) {
assert.Equal(t, []string{"first"}, pressed)
}
func TestPrintableKeysGoToTheFieldBeingTypedIn(t *testing.T) {
for _, test := range []struct {
name string
keybindOnEdit bool
declineKeybinding bool
expectedPresses int
expectedEdits int
}{
{name: "the field gets the key", expectedEdits: 1},
{name: "the parent view gets the key", keybindOnEdit: true, expectedPresses: 1},
{
name: "the field gets the key the parent view declined",
keybindOnEdit: true,
declineKeybinding: true,
expectedPresses: 1,
expectedEdits: 1,
},
} {
t.Run(test.name, func(t *testing.T) {
g := newTestGui(t)
parent, child := setupParentAndChildView(t, g)
child.Editable = true
child.KeybindOnEdit = test.keybindOnEdit
edits := 0
child.Editor = EditorFunc(func(*View, Key) bool {
edits++
return true
})
presses := 0
g.SetKeybinding(parent.Name(), NewKeyRune('j'), func(*Gui, *View) error {
presses++
if test.declineKeybinding {
return ErrKeybindingNotHandled
}
return nil
})
assert.NoError(t, g.onKey(&GocuiEvent{Type: eventKey, Key: NewKeyRune('j')}))
assert.Equal(t, test.expectedPresses, presses)
assert.Equal(t, test.expectedEdits, edits)
})
}
}
func TestUnhandledKeybindingOfParentViewFallsThroughToEditor(t *testing.T) {
g := newTestGui(t)
parent, child := setupParentAndChildView(t, g)