Updates #22: add focus arrow key navigation to grid view
This commit is contained in:
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user