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

Bugfix #948: support focus traversal in dialogs #1318

Merged
merged 8 commits into from Sep 22, 2020
3 changes: 3 additions & 0 deletions canvasobject.go
Expand Up @@ -66,6 +66,9 @@ type Draggable interface {
// It will receive the FocusGained and FocusLost events appropriately.
// When focused it will also have TypedRune called as text is input and
// TypedKey called when other keys are pressed.
//
// Note: You must not change canvas state (including overlays or focus) in FocusGained or FocusLost
// or you would end up with a dead-lock.
type Focusable interface {
FocusGained()
FocusLost()
Expand Down
97 changes: 80 additions & 17 deletions internal/driver/glfw/canvas.go
Expand Up @@ -112,9 +112,7 @@ func (c *glCanvas) OnTypedRune() func(rune) {

// Deprecated: Use Overlays() instead.
func (c *glCanvas) Overlay() fyne.CanvasObject {
c.RLock()
defer c.RUnlock()
return c.overlays.Top()
return c.Overlays().Top()
}

func (c *glCanvas) Overlays() fyne.OverlayStack {
Expand Down Expand Up @@ -210,13 +208,10 @@ func (c *glCanvas) SetOnTypedRune(typed func(rune)) {

// Deprecated: Use Overlays() instead.
func (c *glCanvas) SetOverlay(overlay fyne.CanvasObject) {
c.Lock()
defer c.Unlock()
if len(c.overlays.List()) > 0 {
c.overlays.Remove(c.overlays.List()[0])
}
c.overlays.Add(overlay)
c.setDirty(true)
c.RLock()
o := c.overlays
c.RUnlock()
o.setOverlay(overlay)
}

func (c *glCanvas) SetPadded(padded bool) {
Expand Down Expand Up @@ -352,6 +347,9 @@ func (c *glCanvas) ensureMinSize() bool {
func (c *glCanvas) focusManager() *app.FocusManager {
c.RLock()
defer c.RUnlock()
if focusMgr := c.overlays.TopFocusManager(); focusMgr != nil {
return focusMgr
}
return c.focusMgr
}

Expand Down Expand Up @@ -386,6 +384,20 @@ func (c *glCanvas) objectTrees() []fyne.CanvasObject {
return trees
}

func (c *glCanvas) overlayChanged(focusMgr, prevFocusMgr *app.FocusManager) {
c.Lock()
defer c.Unlock()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: It might be better to not use a defer here, and instead move c.Unlock() down to after line 396. In case dev is doing something weird in Focus{Lost|Gained} which could cause deadlock

c.dirty = true
if prevFocusMgr == nil {
prevFocusMgr = c.focusMgr
}
if focusMgr == nil {
focusMgr = c.focusMgr
}
prevFocusMgr.FocusLost()
focusMgr.FocusGained()
Comment on lines +401 to +402
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible for prevFocusMgr == focusMgr?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! It might be possible with concurrent changes to the overlay stack.
I’ll add missing locking to the stack and also change the callback to be safe by design.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of locking, what if you just return early if they are equal?

}

func (c *glCanvas) paint(size fyne.Size) {
if c.Content() == nil {
return
Expand Down Expand Up @@ -520,23 +532,74 @@ func (c *glCanvas) walkTrees(
type overlayStack struct {
internal.OverlayStack

onChange func()
renderCaches []*renderCacheTree
focusManagers []*app.FocusManager
onChange func(focusMgr, prevFocusMgr *app.FocusManager)
propertyLock sync.RWMutex
renderCaches []*renderCacheTree
}

func (o *overlayStack) Add(overlay fyne.CanvasObject) {
if overlay == nil {
return
}
o.propertyLock.Lock()
defer o.propertyLock.Unlock()
pfm := o.topFocusManager()
o.add(overlay)
o.onChange(o.topFocusManager(), pfm)
}

func (o *overlayStack) Remove(overlay fyne.CanvasObject) {
if overlay == nil || len(o.List()) == 0 {
return
}
o.propertyLock.Lock()
defer o.propertyLock.Unlock()
pfm := o.topFocusManager()
o.remove(overlay)
o.onChange(o.topFocusManager(), pfm)
}

func (o *overlayStack) TopFocusManager() *app.FocusManager {
o.propertyLock.RLock()
defer o.propertyLock.RUnlock()
return o.topFocusManager()
}

func (o *overlayStack) add(overlay fyne.CanvasObject) {
o.OverlayStack.Add(overlay)
o.renderCaches = append(o.renderCaches, &renderCacheTree{root: &renderCacheNode{obj: overlay}})
o.onChange()
o.focusManagers = append(o.focusManagers, app.NewFocusManager(overlay))
}

func (o *overlayStack) Remove(overlay fyne.CanvasObject) {
func (o *overlayStack) remove(overlay fyne.CanvasObject) {
o.OverlayStack.Remove(overlay)
o.renderCaches = o.renderCaches[:len(o.List())]
o.onChange()
overlayCount := len(o.List())
o.renderCaches = o.renderCaches[:overlayCount]
o.focusManagers = o.focusManagers[:overlayCount]
}

// concurrency safe implementation of deprecated c.SetOverlay
func (o *overlayStack) setOverlay(overlay fyne.CanvasObject) {
o.propertyLock.Lock()
defer o.propertyLock.Unlock()

pfm := o.topFocusManager()
if len(o.List()) > 0 {
o.remove(o.List()[0])
}
if overlay != nil {
o.add(overlay)
}
o.onChange(o.topFocusManager(), pfm)
}

func (o *overlayStack) topFocusManager() *app.FocusManager {
var fm *app.FocusManager
if len(o.focusManagers) > 0 {
fm = o.focusManagers[len(o.focusManagers)-1]
}
return fm
}

type renderCacheNode struct {
Expand Down Expand Up @@ -564,7 +627,7 @@ func newCanvas() *glCanvas {
c.setContent(&canvas.Rectangle{FillColor: theme.BackgroundColor()})
c.padded = true

c.overlays = &overlayStack{onChange: func() { c.setDirty(true) }}
c.overlays = &overlayStack{onChange: c.overlayChanged}

c.refreshQueue = make(chan fyne.CanvasObject, 4096)
c.dirtyMutex = &sync.Mutex{}
Expand Down