package widgets import ( "sync" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) // FocusList extends List to be disabled so that the focus manager // considers it unfocusable, and adds utilities for handling our // own focus navigation on the rows directly (with FocusListRow type FocusList struct { widget.List mutex sync.Mutex itemForIndex map[widget.ListItemID]FocusListRow } type FocusListRow interface { fyne.Focusable ItemID() widget.ListItemID } func NewFocusList(len func() int, create func() fyne.CanvasObject, update func(widget.GridWrapItemID, fyne.CanvasObject)) *FocusList { g := &FocusList{ List: widget.List{ Length: len, CreateItem: create, UpdateItem: update, }, itemForIndex: make(map[int]FocusListRow), } g.ExtendBaseWidget(g) return g } var _ fyne.Disableable = (*FocusList)(nil) func (g *FocusList) Disabled() bool { return true } func (g *FocusList) Disable() {} func (g *FocusList) Enable() {} // MUST be called *before* updating the ListItemID field to the // new ItemID this row will be bound to. func (g *FocusList) SetItemForID(id widget.ListItemID, item FocusListRow) { g.mutex.Lock() if other, ok := g.itemForIndex[id]; ok && other == item { delete(g.itemForIndex, other.ItemID()) } g.itemForIndex[id] = item g.mutex.Unlock() } func (g *FocusList) ClearItemForIDMap() { g.itemForIndex = make(map[int]FocusListRow) } func (g *FocusList) FocusNeighbor(curItem widget.ListItemID, up bool) { focusIdx := curItem + 1 if up { focusIdx = curItem - 1 } if focusIdx >= 0 && focusIdx < g.Length() { g.ScrollTo(focusIdx) } g.mutex.Lock() other, ok := g.itemForIndex[focusIdx] g.mutex.Unlock() if ok { fyne.CurrentApp().Driver().CanvasForObject(g).Focus(other) } } var _ fyne.Tappable = (*FocusListRowBase)(nil) var _ fyne.Widget = (*FocusListRowBase)(nil) var _ fyne.Focusable = (*FocusListRowBase)(nil) // Base type used for all list rows in widgets such as Tracklist, etc. type FocusListRowBase struct { widget.BaseWidget ListItemID widget.ListItemID Content fyne.CanvasObject Selected bool Focused bool OnTapped func() OnDoubleTapped func() OnFocusNeighbor func(up bool) //TODO: func(up, selecting bool) tappedAt int64 // unixMillis focusedRect *canvas.Rectangle selectionRect *canvas.Rectangle } func (l *FocusListRowBase) ItemID() widget.ListItemID { return l.ListItemID } func (l *FocusListRowBase) EnsureUnfocused() { if l.Focused { fyne.CurrentApp().Driver().CanvasForObject(l).Unfocus() } l.Focused = false } // We implement our own double tapping so that the Tapped behavior // can be triggered instantly. func (l *FocusListRowBase) Tapped(*fyne.PointEvent) { prevTap := l.tappedAt l.tappedAt = time.Now().UnixMilli() if l.tappedAt-prevTap < 300 { if l.OnDoubleTapped != nil { l.OnDoubleTapped() } } else { if l.OnTapped != nil { l.OnTapped() } } } func (l *FocusListRowBase) FocusGained() { l.Focused = true l.Refresh() } func (l *FocusListRowBase) FocusLost() { l.Focused = false l.Refresh() } func (l *FocusListRowBase) TypedKey(e *fyne.KeyEvent) { /** // TODO: enable shift+arrows for selection, but it's complicated to implement in the widgets desktop, ok := fyne.CurrentApp().Driver().(desktop.Driver) isSelecting := func() bool { return ok && desktop.CurrentKeyModifiers()&fyne.KeyModifierShift != 0 } */ switch { case e.Name == fyne.KeyUp: if l.OnFocusNeighbor != nil { l.OnFocusNeighbor(true) } case e.Name == fyne.KeyDown: if l.OnFocusNeighbor != nil { l.OnFocusNeighbor(false) } case e.Name == fyne.KeySpace: if l.OnTapped != nil { l.OnTapped() } case e.Name == fyne.KeyReturn || e.Name == fyne.KeyEnter: if l.OnDoubleTapped != nil { l.OnDoubleTapped() } else if l.OnTapped != nil { l.OnTapped() } } } func (l *FocusListRowBase) TypedRune(r rune) { } func (l *FocusListRowBase) Refresh() { l.focusedRect.FillColor = theme.HoverColor() l.focusedRect.Hidden = !l.Focused l.selectionRect.FillColor = theme.SelectionColor() l.selectionRect.Hidden = !l.Selected l.BaseWidget.Refresh() } func (l *FocusListRowBase) CreateRenderer() fyne.WidgetRenderer { if l.selectionRect == nil { l.selectionRect = canvas.NewRectangle(theme.SelectionColor()) l.selectionRect.CornerRadius = theme.SelectionRadiusSize() l.selectionRect.Hidden = !l.Selected } if l.focusedRect == nil { l.focusedRect = canvas.NewRectangle(theme.HoverColor()) l.focusedRect.CornerRadius = theme.SelectionRadiusSize() l.focusedRect.Hidden = !l.Focused } return widget.NewSimpleRenderer( container.NewStack(l.selectionRect, l.focusedRect, l.Content), ) }