Updates #22: add focus arrow key navigation to grid view

This commit is contained in:
Drew Weymouth
2024-02-02 08:44:52 -08:00
parent 5db80c7109
commit 7e8a4d6d37
3 changed files with 152 additions and 26 deletions
+9
View File
@@ -3,6 +3,7 @@ package util
import ( import (
"fmt" "fmt"
"image" "image"
"image/color"
"math" "math"
"strconv" "strconv"
"strings" "strings"
@@ -24,6 +25,14 @@ import (
var BoldRichTextStyle = widget.RichTextStyle{TextStyle: fyne.TextStyle{Bold: true}, Inline: true} var BoldRichTextStyle = widget.RichTextStyle{TextStyle: fyne.TextStyle{Bold: true}, Inline: true}
func MakeOpaque(c color.Color) color.Color {
if nrgba, ok := c.(color.NRGBA); ok {
nrgba.A = 255
return nrgba
}
return c
}
func SecondsToTimeString(s float64) string { func SecondsToTimeString(s float64) string {
if s < 0 { if s < 0 {
s = 0 s = 0
+83 -25
View File
@@ -2,6 +2,7 @@ package widgets
import ( import (
"context" "context"
"math"
"sync" "sync"
"github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/mediaprovider"
@@ -73,6 +74,9 @@ type GridView struct {
grid *disabledGridWrap grid *disabledGridWrap
menu *widget.PopUpMenu menu *widget.PopUpMenu
menuGridViewItemId string menuGridViewItemId string
itemForIndex map[int]*GridViewItem
itemWidth float32
numColsCached int
} }
type GridViewState struct { type GridViewState struct {
@@ -103,6 +107,8 @@ func NewFixedGridView(items []GridViewItemModel, fetch util.ImageFetcher, placeh
imageFetcher: fetch, imageFetcher: fetch,
Placeholder: placeholder, Placeholder: placeholder,
}, },
itemWidth: NewGridViewItem(nil).MinSize().Width,
itemForIndex: make(map[int]*GridViewItem),
} }
g.ExtendBaseWidget(g) g.ExtendBaseWidget(g)
g.createGridWrap() g.createGridWrap()
@@ -116,6 +122,8 @@ func NewGridView(iter GridViewIterator, fetch util.ImageFetcher, placeholder fyn
imageFetcher: fetch, imageFetcher: fetch,
Placeholder: placeholder, Placeholder: placeholder,
}, },
itemWidth: NewGridViewItem(nil).MinSize().Width,
itemForIndex: make(map[int]*GridViewItem),
} }
g.ExtendBaseWidget(g) g.ExtendBaseWidget(g)
g.createGridWrap() g.createGridWrap()
@@ -191,41 +199,91 @@ func (g *GridView) ScrollToOffset(offs float32) {
g.grid.ScrollToOffset(offs) g.grid.ScrollToOffset(offs)
} }
func (g *GridView) Resize(size fyne.Size) {
g.numColsCached = -1
g.BaseWidget.Resize(size)
}
func (g *GridView) numCols() int {
if g.numColsCached == -1 {
// logic here taken from gridwrap.go in Fyne codebase
colCount := 1
width := g.Size().Width
if width > g.itemWidth {
pad := theme.Padding()
colCount = int(math.Floor(float64(width+pad) / float64(g.itemWidth+pad)))
}
g.numColsCached = colCount
}
return g.numColsCached
}
func (g *GridView) createGridWrap() { func (g *GridView) createGridWrap() {
g.grid = NewDisabledGridWrap( g.grid = NewDisabledGridWrap(
func() int { g.lenItems,
return g.lenItems() g.createNewItemCard,
},
// create func
func() fyne.CanvasObject {
card := NewGridViewItem(g.Placeholder)
card.ImgLoader = util.NewThumbnailLoader(g.imageFetcher, card.Cover.SetImage)
card.ImgLoader.OnBeforeLoad = func() { card.Cover.SetImage(nil) }
card.OnPlay = func() { g.onPlay(card.ItemID(), false) }
card.OnShowSecondaryPage = func(id string) {
if g.OnShowSecondaryPage != nil {
g.OnShowSecondaryPage(id)
}
}
card.OnShowItemPage = func() {
if g.OnShowItemPage != nil {
g.OnShowItemPage(card.ItemID())
}
}
card.OnShowContextMenu = func(p fyne.Position) {
g.showContextMenu(card, p)
}
return card
},
// update func // update func
func(itemID widget.GridWrapItemID, obj fyne.CanvasObject) { func(itemID widget.GridWrapItemID, obj fyne.CanvasObject) {
ac := obj.(*GridViewItem) ac := obj.(*GridViewItem)
g.doUpdateItemCard(int(itemID), ac) if itemID != ac.ItemIndex {
g.doUpdateItemCard(int(itemID), ac)
} // else nothing to do
}, },
) )
} }
func (g *GridView) createNewItemCard() fyne.CanvasObject {
card := NewGridViewItem(g.Placeholder)
card.ItemIndex = -1
card.ImgLoader = util.NewThumbnailLoader(g.imageFetcher, card.Cover.SetImage)
card.ImgLoader.OnBeforeLoad = func() { card.Cover.SetImage(nil) }
card.OnPlay = func() { g.onPlay(card.ItemID(), false) }
card.OnShowSecondaryPage = func(id string) {
if g.OnShowSecondaryPage != nil {
g.OnShowSecondaryPage(id)
}
}
card.OnShowItemPage = func() {
if g.OnShowItemPage != nil {
g.OnShowItemPage(card.ItemID())
}
}
card.OnShowContextMenu = func(p fyne.Position) {
g.showContextMenu(card, p)
}
card.OnFocusNeighbor = func(neighbor int) {
focusIndex := -1
switch neighbor {
case 0: // left
focusIndex = card.ItemIndex - 1
case 1: // right
focusIndex = card.ItemIndex + 1
case 2: // up
focusIndex = card.ItemIndex - g.numCols()
case 3: // down
focusIndex = card.ItemIndex + g.numCols()
}
if focusIndex >= 0 && focusIndex < g.lenItems() {
g.grid.ScrollTo(focusIndex)
g.stateMutex.RLock()
if item, ok := g.itemForIndex[focusIndex]; ok {
fyne.CurrentApp().Driver().CanvasForObject(g).Focus(item)
}
g.stateMutex.RUnlock()
}
}
return card
}
func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) { func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) {
g.stateMutex.Lock()
if c, ok := g.itemForIndex[card.ItemIndex]; ok && c == card {
delete(g.itemForIndex, card.ItemIndex)
}
card.ItemIndex = itemIdx
g.itemForIndex[itemIdx] = card
g.stateMutex.Unlock()
if itemIdx > g.highestShown { if itemIdx > g.highestShown {
g.highestShown = itemIdx g.highestShown = itemIdx
} }
+60 -1
View File
@@ -2,6 +2,7 @@ package widgets
import ( import (
"image" "image"
"image/color"
"github.com/dweymouth/supersonic/res" "github.com/dweymouth/supersonic/res"
"github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/sharedutil"
@@ -12,10 +13,12 @@ import (
"fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
) )
var _ fyne.Widget = (*GridViewItem)(nil) var _ fyne.Widget = (*GridViewItem)(nil)
var _ fyne.Focusable = (*GridViewItem)(nil)
var _ fyne.Widget = (*coverImage)(nil) var _ fyne.Widget = (*coverImage)(nil)
@@ -138,15 +141,21 @@ type GridViewItem struct {
primaryText *widget.Hyperlink primaryText *widget.Hyperlink
secondaryText *MultiHyperlink secondaryText *MultiHyperlink
container *fyne.Container container *fyne.Container
focused bool
focusRect *canvas.Rectangle
// updated by GridView // updated by GridView
Cover *coverImage Cover *coverImage
ImgLoader util.ThumbnailLoader ImgLoader util.ThumbnailLoader
ItemIndex int
OnPlay func() OnPlay func()
OnShowContextMenu func(fyne.Position) OnShowContextMenu func(fyne.Position)
OnShowItemPage func() OnShowItemPage func()
OnShowSecondaryPage func(string) OnShowSecondaryPage func(string)
// Invoked with arg 0-3 when left, right, up, or down neighbor should be focused, respectively
OnFocusNeighbor func(int)
} }
func NewGridViewItem(placeholderResource fyne.Resource) *GridViewItem { func NewGridViewItem(placeholderResource fyne.Resource) *GridViewItem {
@@ -187,7 +196,10 @@ func NewGridViewItem(placeholderResource fyne.Resource) *GridViewItem {
func (g *GridViewItem) createContainer() { func (g *GridViewItem) createContainer() {
info := container.New(&layouts.VboxCustomPadding{ExtraPad: -16}, g.primaryText, g.secondaryText) info := container.New(&layouts.VboxCustomPadding{ExtraPad: -16}, g.primaryText, g.secondaryText)
c := container.New(&layouts.VboxCustomPadding{ExtraPad: -5}, g.Cover, info) g.focusRect = canvas.NewRectangle(color.Transparent)
g.focusRect.StrokeWidth = 3
coverStack := container.NewStack(g.Cover, g.focusRect)
c := container.New(&layouts.VboxCustomPadding{ExtraPad: -5}, coverStack, info)
pad := &layouts.CenterPadLayout{PadLeftRight: 20, PadTopBottom: 10} pad := &layouts.CenterPadLayout{PadLeftRight: 20, PadTopBottom: 10}
g.container = container.New(pad, c) g.container = container.New(pad, c)
} }
@@ -203,9 +215,15 @@ func (g *GridViewItem) Update(model GridViewItemModel) {
g.secondaryText.BuildSegments(model.Secondary, model.SecondaryIDs) g.secondaryText.BuildSegments(model.Secondary, model.SecondaryIDs)
g.secondaryText.Refresh() g.secondaryText.Refresh()
g.Cover.ResetPlayButton() g.Cover.ResetPlayButton()
if g.focused {
fyne.CurrentApp().Driver().CanvasForObject(g).Focus(nil)
g.FocusLost()
}
} }
func (g *GridViewItem) Refresh() { func (g *GridViewItem) Refresh() {
g.focusRect.StrokeColor = util.MakeOpaque(theme.FocusColor())
g.focusRect.Hidden = !g.focused
g.BaseWidget.Refresh() g.BaseWidget.Refresh()
} }
@@ -213,6 +231,47 @@ func (g *GridViewItem) ItemID() string {
return g.itemID return g.itemID
} }
func (g *GridViewItem) FocusGained() {
g.focused = true
g.Refresh()
}
func (g *GridViewItem) FocusLost() {
g.focused = false
g.Refresh()
}
func (g *GridViewItem) TypedKey(e *fyne.KeyEvent) {
if !g.focused {
return
}
focusArg := -1
switch e.Name {
case fyne.KeyLeft:
focusArg = 0
case fyne.KeyRight:
focusArg = 1
case fyne.KeyUp:
focusArg = 2
case fyne.KeyDown:
focusArg = 3
case fyne.KeyEnter:
fallthrough
case fyne.KeySpace:
if g.OnShowItemPage != nil {
g.OnShowItemPage()
return
}
}
if focusArg >= 0 && g.OnFocusNeighbor != nil {
g.OnFocusNeighbor(focusArg)
}
}
func (g *GridViewItem) TypedRune(rune) {
// intentionally blank
}
func (g *GridViewItem) CreateRenderer() fyne.WidgetRenderer { func (g *GridViewItem) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(g.container) return widget.NewSimpleRenderer(g.container)
} }