io/key: improve InputOp focus and blur

The existing implementation cannot remove the focus of some widget,
doesn't have an option to focus without display the on-screen keyboard
and it automatically focuses the first InputOp, aggressively.

That change aims to make possible: remove focus from any widget. Add
focus without displaying the on-screen-keyboard/soft keyboard. Don't
automatically focus any widget. Don't recover focus when the widget is
visible again.

Fixes gio#180.

Signed-off-by: Inkeliz <inkeliz@inkeliz.com>
This commit is contained in:
Inkeliz
2020-11-30 23:40:20 +00:00
committed by Elias Naur
parent e9cd8958de
commit cd3b4561cf
5 changed files with 430 additions and 76 deletions
+25 -11
View File
@@ -20,16 +20,22 @@ import (
// InputOp declares a handler ready for key events.
// Key events are in general only delivered to the
// focused key handler. Set the Focus flag to request
// the focus.
// focused key handler.
type InputOp struct {
Tag event.Tag
Focus bool
Tag event.Tag
}
// HideInputOp request that any on screen text input
// be hidden.
type HideInputOp struct{}
// SoftKeyboardOp shows or hide the on-screen keyboard, if available.
type SoftKeyboardOp struct {
Show bool
}
// FocusOp sets or clears the keyboard focus.
type FocusOp struct {
// Focus, if set, moves the focus to the current InputOp. If Focus
// is false, the focus is cleared.
Focus bool
}
// A FocusEvent is generated when a handler gains or loses
// focus.
@@ -115,14 +121,22 @@ func (m Modifiers) Contain(m2 Modifiers) bool {
func (h InputOp) Add(o *op.Ops) {
data := o.Write1(opconst.TypeKeyInputLen, h.Tag)
data[0] = byte(opconst.TypeKeyInput)
if h.Focus {
}
func (h SoftKeyboardOp) Add(o *op.Ops) {
data := o.Write(opconst.TypeKeySoftKeyboardLen)
data[0] = byte(opconst.TypeKeySoftKeyboard)
if h.Show {
data[1] = 1
}
}
func (h HideInputOp) Add(o *op.Ops) {
data := o.Write(opconst.TypeHideInputLen)
data[0] = byte(opconst.TypeHideInput)
func (h FocusOp) Add(o *op.Ops) {
data := o.Write(opconst.TypeKeyFocusLen)
data[0] = byte(opconst.TypeKeyFocus)
if h.Focus {
data[1] = 1
}
}
func (EditEvent) ImplementsEvent() {}
+65 -43
View File
@@ -20,15 +20,18 @@ type keyQueue struct {
}
type keyHandler struct {
active bool
// visible will be true if the InputOp is present
// in the current frame.
visible bool
new bool
}
type listenerPriority uint8
const (
priNone listenerPriority = iota
priDefault
priDefault listenerPriority = iota
priCurrentFocus
priNone
priNewFocus
)
@@ -49,18 +52,27 @@ func (q *keyQueue) Frame(root *op.Ops, events *handlerEvents) {
q.handlers = make(map[event.Tag]*keyHandler)
}
for _, h := range q.handlers {
h.active = false
h.visible, h.new = false, false
}
q.reader.Reset(root)
focus, pri, hide := q.resolveFocus(events)
focus, pri, keyboard := q.resolveFocus(events)
if pri == priNone {
focus = nil
}
for k, h := range q.handlers {
if !h.active {
if !h.visible {
delete(q.handlers, k)
if q.focus == k {
// Remove the focus from the handler that is no longer visible.
q.focus = nil
hide = true
keyboard = TextInputClose
}
}
if h.new && k != focus {
// Reset the handler on (each) first appearance.
events.Add(k, key.FocusEvent{Focus: false})
}
}
if focus != q.focus {
if q.focus != nil {
@@ -70,17 +82,10 @@ func (q *keyQueue) Frame(root *op.Ops, events *handlerEvents) {
if q.focus != nil {
events.Add(q.focus, key.FocusEvent{Focus: true})
} else {
hide = true
keyboard = TextInputClose
}
}
switch {
case pri == priNewFocus:
q.state = TextInputOpen
case hide:
q.state = TextInputClose
default:
q.state = TextInputKeep
}
q.state = keyboard
}
func (q *keyQueue) Push(e event.Event, events *handlerEvents) {
@@ -89,49 +94,49 @@ func (q *keyQueue) Push(e event.Event, events *handlerEvents) {
}
}
func (q *keyQueue) resolveFocus(events *handlerEvents) (event.Tag, listenerPriority, bool) {
var k event.Tag
var pri listenerPriority
var hide bool
func (q *keyQueue) resolveFocus(events *handlerEvents) (tag event.Tag, pri listenerPriority, keyboard TextInputState) {
loop:
for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
switch opconst.OpType(encOp.Data[0]) {
case opconst.TypeKeyFocus:
op := decodeFocusOp(encOp.Data, encOp.Refs)
if op.Focus {
pri = priNewFocus
} else {
pri, keyboard = priNone, TextInputClose
}
case opconst.TypeKeySoftKeyboard:
op := decodeSoftKeyboardOp(encOp.Data, encOp.Refs)
if op.Show {
keyboard = TextInputOpen
} else {
keyboard = TextInputClose
}
case opconst.TypeKeyInput:
op := decodeKeyInputOp(encOp.Data, encOp.Refs)
var newPri listenerPriority
switch {
case op.Focus:
newPri = priNewFocus
case op.Tag == q.focus:
newPri = priCurrentFocus
default:
newPri = priDefault
}
// Switch focus if higher priority or if focus requested.
if newPri.replaces(pri) {
k, pri = op.Tag, newPri
if op.Tag == q.focus && pri < priCurrentFocus {
pri = priCurrentFocus
}
h, ok := q.handlers[op.Tag]
if !ok {
h = new(keyHandler)
h = &keyHandler{new: true}
q.handlers[op.Tag] = h
// Reset the handler on (each) first appearance.
events.Add(op.Tag, key.FocusEvent{Focus: false})
}
h.active = true
case opconst.TypeHideInput:
hide = true
h.visible = true
tag = op.Tag
case opconst.TypePush:
newK, newPri, h := q.resolveFocus(events)
hide = hide || h
newK, newPri, newKeyboard := q.resolveFocus(events)
if newKeyboard > keyboard {
keyboard = newKeyboard
}
if newPri.replaces(pri) {
k, pri = newK, newPri
tag, pri = newK, newPri
}
case opconst.TypePop:
break loop
}
}
return k, pri, hide
return tag, pri, keyboard
}
func (p listenerPriority) replaces(p2 listenerPriority) bool {
@@ -144,7 +149,24 @@ func decodeKeyInputOp(d []byte, refs []interface{}) key.InputOp {
panic("invalid op")
}
return key.InputOp{
Tag: refs[0].(event.Tag),
Tag: refs[0].(event.Tag),
}
}
func decodeSoftKeyboardOp(d []byte, refs []interface{}) key.SoftKeyboardOp {
if opconst.OpType(d[0]) != opconst.TypeKeySoftKeyboard {
panic("invalid op")
}
return key.SoftKeyboardOp{
Show: d[1] != 0,
}
}
func decodeFocusOp(d []byte, refs []interface{}) key.FocusOp {
if opconst.OpType(d[0]) != opconst.TypeKeyFocus {
panic("invalid op")
}
return key.FocusOp{
Focus: d[1] != 0,
}
}
+311
View File
@@ -0,0 +1,311 @@
// SPDX-License-Identifier: Unlicense OR MIT
package router
import (
"reflect"
"testing"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/op"
)
func TestKeyMultiples(t *testing.T) {
handlers := make([]int, 3)
ops := new(op.Ops)
r := new(Router)
key.SoftKeyboardOp{Show: true}.Add(ops)
key.InputOp{Tag: &handlers[0]}.Add(ops)
key.FocusOp{Focus: true}.Add(ops)
key.InputOp{Tag: &handlers[1]}.Add(ops)
// The last one must be focused:
key.InputOp{Tag: &handlers[2]}.Add(ops)
r.Frame(ops)
assertKeyEvent(t, r.Events(&handlers[0]), false)
assertKeyEvent(t, r.Events(&handlers[1]), false)
assertKeyEvent(t, r.Events(&handlers[2]), true)
assertFocus(t, r, &handlers[2])
assertKeyboard(t, r, TextInputOpen)
}
func TestKeyStacked(t *testing.T) {
handlers := make([]int, 4)
ops := new(op.Ops)
r := new(Router)
s := op.Push(ops)
key.InputOp{Tag: &handlers[0]}.Add(ops)
// FocusOp must not overwrite the
// FocusOp{Focus: true}.
key.FocusOp{Focus: false}.Add(ops)
s.Pop()
s = op.Push(ops)
key.SoftKeyboardOp{Show: false}.Add(ops)
key.InputOp{Tag: &handlers[1]}.Add(ops)
key.FocusOp{Focus: true}.Add(ops)
s.Pop()
s = op.Push(ops)
key.InputOp{Tag: &handlers[2]}.Add(ops)
// SoftwareKeyboardOp will open the keyboard,
// overwriting `SoftKeyboardOp{Show: false}`.
key.SoftKeyboardOp{Show: true}.Add(ops)
s.Pop()
s = op.Push(ops)
key.SoftKeyboardOp{Show: false}.Add(ops)
key.InputOp{Tag: &handlers[3]}.Add(ops)
// FocusOp must not overwrite the
// FocusOp{Focus: true}.
key.FocusOp{Focus: false}.Add(ops)
s.Pop()
r.Frame(ops)
assertKeyEvent(t, r.Events(&handlers[0]), false)
assertKeyEvent(t, r.Events(&handlers[1]), true)
assertKeyEvent(t, r.Events(&handlers[2]), false)
assertKeyEvent(t, r.Events(&handlers[3]), false)
assertFocus(t, r, &handlers[1])
assertKeyboard(t, r, TextInputOpen)
}
func TestKeySoftKeyboardNoFocus(t *testing.T) {
ops := new(op.Ops)
r := new(Router)
// It's possible to open the keyboard
// without any active focus:
key.SoftKeyboardOp{Show: true}.Add(ops)
r.Frame(ops)
assertFocus(t, r, nil)
assertKeyboard(t, r, TextInputOpen)
}
func TestKeyRemoveFocus(t *testing.T) {
handlers := make([]int, 2)
ops := new(op.Ops)
r := new(Router)
// New InputOp with Focus and Keyboard:
s := op.Push(ops)
key.InputOp{Tag: &handlers[0]}.Add(ops)
key.FocusOp{Focus: true}.Add(ops)
key.SoftKeyboardOp{Show: true}.Add(ops)
s.Pop()
// New InputOp without any focus:
s = op.Push(ops)
key.InputOp{Tag: &handlers[1]}.Add(ops)
s.Pop()
r.Frame(ops)
// Add some key events:
event := event.Event(key.Event{Name: key.NameTab, Modifiers: key.ModShortcut, State: key.Press})
r.Add(event)
assertKeyEvent(t, r.Events(&handlers[0]), true, event)
assertKeyEvent(t, r.Events(&handlers[1]), false)
assertFocus(t, r, &handlers[0])
assertKeyboard(t, r, TextInputOpen)
ops.Reset()
// Will get the focus removed:
s = op.Push(ops)
key.InputOp{Tag: &handlers[0]}.Add(ops)
s.Pop()
// Unchanged:
s = op.Push(ops)
key.InputOp{Tag: &handlers[1]}.Add(ops)
s.Pop()
// Removing any Focus:
s = op.Push(ops)
key.FocusOp{Focus: false}.Add(ops)
s.Pop()
r.Frame(ops)
assertKeyEvent(t, r.Events(&handlers[0]), false)
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
assertFocus(t, r, nil)
assertKeyboard(t, r, TextInputClose)
ops.Reset()
s = op.Push(ops)
key.InputOp{Tag: &handlers[0]}.Add(ops)
s.Pop()
// Setting Focus without InputOp:
s = op.Push(ops)
key.FocusOp{Focus: true}.Add(ops)
s.Pop()
s = op.Push(ops)
key.InputOp{Tag: &handlers[1]}.Add(ops)
s.Pop()
r.Frame(ops)
assertKeyEventUnexpected(t, r.Events(&handlers[0]))
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
assertFocus(t, r, nil)
assertKeyboard(t, r, TextInputKeep)
ops.Reset()
// Set focus to InputOp which already
// exists in the previous frame:
s = op.Push(ops)
key.FocusOp{Focus: true}.Add(ops)
key.InputOp{Tag: &handlers[0]}.Add(ops)
key.SoftKeyboardOp{Show: true}.Add(ops)
s.Pop()
// Tries to remove focus:
// It must not overwrite the previous `FocusOp`.
s = op.Push(ops)
key.InputOp{Tag: &handlers[1]}.Add(ops)
key.FocusOp{Focus: false}.Add(ops)
s.Pop()
r.Frame(ops)
assertKeyEvent(t, r.Events(&handlers[0]), true)
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
assertFocus(t, r, &handlers[0])
assertKeyboard(t, r, TextInputOpen)
}
func TestKeyFocusedInvisible(t *testing.T) {
handlers := make([]int, 2)
ops := new(op.Ops)
r := new(Router)
// Set new InputOp with focus:
s := op.Push(ops)
key.FocusOp{Focus: true}.Add(ops)
key.InputOp{Tag: &handlers[0]}.Add(ops)
key.SoftKeyboardOp{Show: true}.Add(ops)
s.Pop()
// Set new InputOp without focus:
s = op.Push(ops)
key.InputOp{Tag: &handlers[1]}.Add(ops)
s.Pop()
r.Frame(ops)
assertKeyEvent(t, r.Events(&handlers[0]), true)
assertKeyEvent(t, r.Events(&handlers[1]), false)
assertFocus(t, r, &handlers[0])
assertKeyboard(t, r, TextInputOpen)
ops.Reset()
//
// Removed first (focused) element!
//
// Unchanged:
s = op.Push(ops)
key.InputOp{Tag: &handlers[1]}.Add(ops)
s.Pop()
r.Frame(ops)
assertKeyEventUnexpected(t, r.Events(&handlers[0]))
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
assertFocus(t, r, nil)
assertKeyboard(t, r, TextInputClose)
ops.Reset()
// Respawn the first element:
// It must receive one `Event{Focus: false}`.
s = op.Push(ops)
key.InputOp{Tag: &handlers[0]}.Add(ops)
s.Pop()
// Unchanged
s = op.Push(ops)
key.InputOp{Tag: &handlers[1]}.Add(ops)
s.Pop()
r.Frame(ops)
assertKeyEvent(t, r.Events(&handlers[0]), false)
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
assertFocus(t, r, nil)
assertKeyboard(t, r, TextInputKeep)
}
func assertKeyEvent(t *testing.T, events []event.Event, expected bool, expectedInputs ...event.Event) {
t.Helper()
var evtFocus int
var evtKeyPress int
for _, e := range events {
switch ev := e.(type) {
case key.FocusEvent:
if ev.Focus != expected {
t.Errorf("focus is expected to be %v, got %v", expected, ev.Focus)
}
evtFocus++
case key.Event, key.EditEvent:
if len(expectedInputs) <= evtKeyPress {
t.Errorf("unexpected key events")
}
if !reflect.DeepEqual(ev, expectedInputs[evtKeyPress]) {
t.Errorf("expected %v events, got %v", expectedInputs[evtKeyPress], ev)
}
evtKeyPress++
}
}
if evtFocus <= 0 {
t.Errorf("expected focus event")
}
if evtFocus > 1 {
t.Errorf("expected single focus event")
}
if evtKeyPress != len(expectedInputs) {
t.Errorf("expected key events")
}
}
func assertKeyEventUnexpected(t *testing.T, events []event.Event) {
t.Helper()
var evtFocus int
for _, e := range events {
switch e.(type) {
case key.FocusEvent:
evtFocus++
}
}
if evtFocus > 1 {
t.Errorf("unexpected focus event")
}
}
func assertFocus(t *testing.T, router *Router, expected event.Tag) {
t.Helper()
if router.kqueue.focus != expected {
t.Errorf("expected %v to be focused, got %v", expected, router.kqueue.focus)
}
}
func assertKeyboard(t *testing.T, router *Router, expected TextInputState) {
t.Helper()
if router.kqueue.state != expected {
t.Errorf("expected %v keyboard, got %v", expected, router.kqueue.state)
}
}