Skip to content

Commit

Permalink
Merge pull request #1294 from toaster/refactoring/focus_handling
Browse files Browse the repository at this point in the history
Refactoring: focus handling
  • Loading branch information
toaster committed Sep 14, 2020
2 parents b4978f2 + 10aa3ac commit 4cd6804
Show file tree
Hide file tree
Showing 4 changed files with 297 additions and 209 deletions.
83 changes: 62 additions & 21 deletions internal/app/focus.go
@@ -1,19 +1,77 @@
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.focus(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.focus(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.focus(f.previousInChain(f.focused))
}

func (f *FocusManager) focus(obj fyne.Focusable) {
if f.focused == obj {
return
}

if dis, ok := obj.(fyne.Disableable); ok && dis.Disabled() {
obj = nil
}

if f.focused != nil {
f.focused.FocusLost()
}
f.focused = obj
if obj != nil {
obj.FocusGained()
}
}

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 +87,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 +106,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 +134,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}
}
117 changes: 88 additions & 29 deletions internal/app/focus_test.go
Expand Up @@ -4,62 +4,121 @@ 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 := &focusable{}
hidden := widget.NewCheck("test", func(bool) {})
hidden.Hide()
entry2 := &focusable{}
disabled := widget.NewCheck("test", func(bool) {})
disabled.Disable()
entry3 := &focusable{}
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())
assert.True(t, entry2.focused)

manager.Focus(entry1)
assert.Equal(t, entry1, manager.Focused())
assert.True(t, entry1.focused)
assert.False(t, entry2.focused)

manager.Focus(entry3)
assert.Equal(t, entry3, manager.Focused())
assert.True(t, entry3.focused)
assert.False(t, entry1.focused)

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

func TestFocusManager_FocusNext(t *testing.T) {
c := test.NewCanvas()
entry1 := widget.NewEntry()
entry1 := &focusable{}
hidden := widget.NewCheck("test", func(bool) {})
hidden.Hide()
entry2 := widget.NewEntry()
entry2 := &focusable{}
disabled := widget.NewCheck("test", func(bool) {})
disabled.Disable()
entry3 := widget.NewEntry()
c.SetContent(widget.NewVBox(entry1, hidden, entry2, disabled, entry3))
entry3 := &focusable{}
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())
assert.True(t, entry1.focused)

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

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

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

func TestFocusManager_FocusPrevious(t *testing.T) {
c := test.NewCanvas()
entry1 := widget.NewEntry()
entry1 := &focusable{}
hidden := widget.NewCheck("test", func(bool) {})
hidden.Hide()
entry2 := widget.NewEntry()
entry2 := &focusable{}
disabled := widget.NewCheck("test", func(bool) {})
disabled.Disable()
entry3 := widget.NewEntry()
c.SetContent(widget.NewVBox(entry1, hidden, entry2, disabled, entry3))
entry3 := &focusable{}
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())
assert.True(t, entry3.focused)

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

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

manager.FocusPrevious()
assert.Equal(t, entry3, manager.Focused())
assert.True(t, entry3.focused)
assert.False(t, entry1.focused)
}

type focusable struct {
widget.Entry
focused bool
}

func (f *focusable) FocusGained() {
if f.Disabled() {
return
}
f.focused = true
}

manager.FocusPrevious(entry1)
assert.Equal(t, entry3, c.Focused())
func (f *focusable) FocusLost() {
f.focused = false
}

0 comments on commit 4cd6804

Please sign in to comment.