Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring: focus handling #1294

Merged
merged 10 commits into from Sep 14, 2020
65 changes: 44 additions & 21 deletions internal/app/focus.go
@@ -1,19 +1,59 @@
package app

import (
"sync"

"fyne.io/fyne"
"fyne.io/fyne/internal/driver"
)

// FocusManager represents a standard manager of input focus for a canvas
type FocusManager struct {
canvas fyne.Canvas
sync.RWMutex

content fyne.CanvasObject
focused fyne.Focusable
}

// NewFocusManager returns a new instance of the standard focus manager for a canvas.
func NewFocusManager(c fyne.CanvasObject) *FocusManager {
return &FocusManager{content: c}
}

// Focus focuses the given obj.
func (f *FocusManager) Focus(obj fyne.Focusable) {
f.Lock()
defer f.Unlock()
f.focused = obj
}

// Focused returns the currently focused object or nil if none.
func (f *FocusManager) Focused() fyne.Focusable {
f.RLock()
defer f.RUnlock()
return f.focused
}

// FocusNext will find the item after the current that can be focused and focus it.
// If current is nil then the first focusable item in the canvas will be focused.
func (f *FocusManager) FocusNext() {
f.Lock()
defer f.Unlock()
f.focused = f.nextInChain(f.focused)
}

// FocusPrevious will find the item before the current that can be focused and focus it.
// If current is nil then the last focusable item in the canvas will be focused.
func (f *FocusManager) FocusPrevious() {
f.Lock()
defer f.Unlock()
f.focused = f.previousInChain(f.focused)
}

func (f *FocusManager) nextInChain(current fyne.Focusable) fyne.Focusable {
var first, next fyne.Focusable
found := current == nil // if we have no starting point then pretend we matched already
driver.WalkVisibleObjectTree(f.canvas.Content(), func(obj fyne.CanvasObject, _ fyne.Position, _ fyne.Position, _ fyne.Size) bool {
driver.WalkVisibleObjectTree(f.content, func(obj fyne.CanvasObject, _ fyne.Position, _ fyne.Position, _ fyne.Size) bool {
if w, ok := obj.(fyne.Disableable); ok && w.Disabled() {
// disabled widget cannot receive focus
return false
Expand All @@ -29,7 +69,7 @@ func (f *FocusManager) nextInChain(current fyne.Focusable) fyne.Focusable {
return true
}

if !found && obj == current.(fyne.CanvasObject) {
if obj == current.(fyne.CanvasObject) {
found = true
}
if first == nil {
Expand All @@ -48,7 +88,7 @@ func (f *FocusManager) nextInChain(current fyne.Focusable) fyne.Focusable {
func (f *FocusManager) previousInChain(current fyne.Focusable) fyne.Focusable {
var last, previous fyne.Focusable
found := false
driver.WalkVisibleObjectTree(f.canvas.Content(), func(obj fyne.CanvasObject, _ fyne.Position, _ fyne.Position, _ fyne.Size) bool {
driver.WalkVisibleObjectTree(f.content, func(obj fyne.CanvasObject, _ fyne.Position, _ fyne.Position, _ fyne.Size) bool {
if w, ok := obj.(fyne.Disableable); ok && w.Disabled() {
// disabled widget cannot receive focus
return false
Expand Down Expand Up @@ -76,20 +116,3 @@ func (f *FocusManager) previousInChain(current fyne.Focusable) fyne.Focusable {
}
return last
}

// FocusNext will find the item after the current that can be focused and focus it.
// If current is nil then the first focusable item in the canvas will be focused.
func (f *FocusManager) FocusNext(current fyne.Focusable) {
f.canvas.Focus(f.nextInChain(current))
}

// FocusPrevious will find the item before the current that can be focused and focus it.
// If current is nil then the last focusable item in the canvas will be focused.
func (f *FocusManager) FocusPrevious(current fyne.Focusable) {
f.canvas.Focus(f.previousInChain(current))
}

// NewFocusManager returns a new instance of the standard focus manager for a canvas.
func NewFocusManager(c fyne.Canvas) *FocusManager {
return &FocusManager{canvas: c}
}
69 changes: 46 additions & 23 deletions internal/app/focus_test.go
Expand Up @@ -4,62 +4,85 @@ import (
"testing"

"fyne.io/fyne/internal/app"
"fyne.io/fyne/test"
"fyne.io/fyne/widget"

"github.com/stretchr/testify/assert"
)

func TestFocusManager_Focus(t *testing.T) {
entry1 := widget.NewEntry()
hidden := widget.NewCheck("test", func(bool) {})
hidden.Hide()
entry2 := widget.NewEntry()
disabled := widget.NewCheck("test", func(bool) {})
disabled.Disable()
entry3 := widget.NewEntry()
c := widget.NewVBox(entry1, hidden, entry2, disabled, entry3)

manager := app.NewFocusManager(c)
assert.Nil(t, manager.Focused())

manager.Focus(entry2)
assert.Equal(t, entry2, manager.Focused())

manager.Focus(entry1)
assert.Equal(t, entry1, manager.Focused())

manager.Focus(entry3)
assert.Equal(t, entry3, manager.Focused())

manager.Focus(nil)
assert.Nil(t, manager.Focused())
}

func TestFocusManager_FocusNext(t *testing.T) {
c := test.NewCanvas()
entry1 := widget.NewEntry()
hidden := widget.NewCheck("test", func(bool) {})
hidden.Hide()
entry2 := widget.NewEntry()
disabled := widget.NewCheck("test", func(bool) {})
disabled.Disable()
entry3 := widget.NewEntry()
c.SetContent(widget.NewVBox(entry1, hidden, entry2, disabled, entry3))
c := widget.NewVBox(entry1, hidden, entry2, disabled, entry3)

manager := app.NewFocusManager(c)
assert.Nil(t, c.Focused())
assert.Nil(t, manager.Focused())

manager.FocusNext(nil)
assert.Equal(t, entry1, c.Focused())
manager.FocusNext()
assert.Equal(t, entry1, manager.Focused())

manager.FocusNext(entry1)
assert.Equal(t, entry2, c.Focused())
manager.FocusNext()
assert.Equal(t, entry2, manager.Focused())

manager.FocusNext(entry2)
assert.Equal(t, entry3, c.Focused())
manager.FocusNext()
assert.Equal(t, entry3, manager.Focused())

manager.FocusNext(entry3)
assert.Equal(t, entry1, c.Focused())
manager.FocusNext()
assert.Equal(t, entry1, manager.Focused())
}

func TestFocusManager_FocusPrevious(t *testing.T) {
c := test.NewCanvas()
entry1 := widget.NewEntry()
hidden := widget.NewCheck("test", func(bool) {})
hidden.Hide()
entry2 := widget.NewEntry()
disabled := widget.NewCheck("test", func(bool) {})
disabled.Disable()
entry3 := widget.NewEntry()
c.SetContent(widget.NewVBox(entry1, hidden, entry2, disabled, entry3))
c := widget.NewVBox(entry1, hidden, entry2, disabled, entry3)

manager := app.NewFocusManager(c)
assert.Nil(t, c.Focused())
assert.Nil(t, manager.Focused())

manager.FocusPrevious(nil)
assert.Equal(t, entry3, c.Focused())
manager.FocusPrevious()
assert.Equal(t, entry3, manager.Focused())

manager.FocusPrevious(entry3)
assert.Equal(t, entry2, c.Focused())
manager.FocusPrevious()
assert.Equal(t, entry2, manager.Focused())

manager.FocusPrevious(entry2)
assert.Equal(t, entry1, c.Focused())
manager.FocusPrevious()
assert.Equal(t, entry1, manager.Focused())

manager.FocusPrevious(entry1)
assert.Equal(t, entry3, c.Focused())
manager.FocusPrevious()
assert.Equal(t, entry3, manager.Focused())
}