1
0
mirror of https://github.com/taigrr/wtf synced 2025-01-18 04:03:14 -08:00
wtf/app/focus_tracker.go
Chris Cummer fd794707cd
☢️ WTF-1031 Support multiple simultaneous configurations (#1032)
* WTF-1031 Rename WtfApp.app to WtfApp.tviewApp

Signed-off-by: Chris Cummer <chriscummer@me.com>

* WTF-1031 Add scaffolding for main to support multiple WtfApp instances

Signed-off-by: Chris Cummer <chriscummer@me.com>

* WTF-1031 WIP

Signed-off-by: Chris Cummer <chriscummer@me.com>

* Remove common functionality from KeyboardWidget and into Base

Signed-off-by: Chris Cummer <chriscummer@me.com>

* Augment with some descriptive comments

Signed-off-by: Chris Cummer <chriscummer@me.com>

* Add full support for multiple app instances via the AppManager.

Still to do:

* Config support for multiple apps/multiple config files
* The ability to switch between apps

Signed-off-by: Chris Cummer <chriscummer@me.com>

* Move SetTerminal out of main and into its own file

Signed-off-by: Chris Cummer <chriscummer@me.com>
2020-12-21 03:25:41 -08:00

265 lines
5.6 KiB
Go

package app
import (
"fmt"
"sort"
"github.com/olebedev/config"
"github.com/rivo/tview"
"github.com/wtfutil/wtf/wtf"
)
// FocusState is a custom type that differentiates focusable scopes
type FocusState int
const (
widgetFocused FocusState = iota
appBoardFocused
neverFocused
)
// FocusTracker is used by the app to track which onscreen widget currently has focus,
// and to move focus between widgets.
type FocusTracker struct {
Idx int
IsFocused bool
Widgets []wtf.Wtfable
config *config.Config
tviewApp *tview.Application
}
// NewFocusTracker creates and returns an instance of FocusTracker
func NewFocusTracker(tviewApp *tview.Application, widgets []wtf.Wtfable, config *config.Config) FocusTracker {
focusTracker := FocusTracker{
tviewApp: tviewApp,
Idx: -1,
IsFocused: false,
Widgets: widgets,
config: config,
}
focusTracker.assignHotKeys()
return focusTracker
}
/* -------------------- Exported Functions -------------------- */
// FocusOn puts the focus on the item that belongs to the focus character passed in
func (tracker *FocusTracker) FocusOn(char string) bool {
if !tracker.useNavShortcuts() {
return false
}
if tracker.focusState() == appBoardFocused {
return false
}
hasFocusable := false
for idx, focusable := range tracker.focusables() {
if focusable.FocusChar() == char {
tracker.blur(tracker.Idx)
tracker.Idx = idx
tracker.focus(tracker.Idx)
hasFocusable = true
tracker.IsFocused = true
break
}
}
return hasFocusable
}
// Next sets the focus on the next widget in the widget list. If the current widget is
// the last widget, sets focus on the first widget.
func (tracker *FocusTracker) Next() {
if tracker.focusState() == appBoardFocused {
return
}
tracker.blur(tracker.Idx)
tracker.increment()
tracker.focus(tracker.Idx)
tracker.IsFocused = true
}
// None removes focus from the currently-focused widget.
func (tracker *FocusTracker) None() {
if tracker.focusState() == appBoardFocused {
return
}
tracker.blur(tracker.Idx)
}
// Prev sets the focus on the previous widget in the widget list. If the current widget is
// the last widget, sets focus on the last widget.
func (tracker *FocusTracker) Prev() {
if tracker.focusState() == appBoardFocused {
return
}
tracker.blur(tracker.Idx)
tracker.decrement()
tracker.focus(tracker.Idx)
tracker.IsFocused = true
}
// Refocus forces the focus back to the currently-selected item
func (tracker *FocusTracker) Refocus() {
tracker.focus(tracker.Idx)
}
/* -------------------- Unexported Functions -------------------- */
// AssignHotKeys assigns an alphabetic keyboard character to each focusable
// widget so that the widget can be brought into focus by pressing that keyboard key
// Valid numbers are between 1 and 9, inclusive
func (tracker *FocusTracker) assignHotKeys() {
if !tracker.useNavShortcuts() {
return
}
usedKeys := make(map[string]bool)
focusables := tracker.focusables()
// First, block out the explicitly-defined characters so they can't be automatically
// assigned to other modules
for _, focusable := range focusables {
if focusable.FocusChar() != "" {
usedKeys[focusable.FocusChar()] = true
}
}
focusNum := 1
// Range over all the modules and assign focus characters to any that are focusable
// and don't have explicitly-defined focus characters
for _, focusable := range focusables {
if focusable.FocusChar() != "" {
continue
}
if _, foundKey := usedKeys[fmt.Sprint(focusNum)]; foundKey {
for ; foundKey; _, foundKey = usedKeys[fmt.Sprint(focusNum)] {
focusNum++
}
}
// Don't allow focus characters > "9"
if focusNum >= 10 {
break
}
focusable.SetFocusChar(fmt.Sprint(focusNum))
focusNum++
}
}
func (tracker *FocusTracker) blur(idx int) {
widget := tracker.focusableAt(idx)
if widget == nil {
return
}
view := widget.TextView()
view.Blur()
view.SetBorderColor(
wtf.ColorFor(
widget.BorderColor(),
),
)
tracker.IsFocused = false
}
func (tracker *FocusTracker) decrement() {
tracker.Idx--
if tracker.Idx < 0 {
tracker.Idx = len(tracker.focusables()) - 1
}
}
func (tracker *FocusTracker) focus(idx int) {
widget := tracker.focusableAt(idx)
if widget == nil {
return
}
view := widget.TextView()
view.SetBorderColor(
wtf.ColorFor(
widget.CommonSettings().Colors.BorderTheme.Focused,
),
)
tracker.tviewApp.SetFocus(view)
}
func (tracker *FocusTracker) focusables() []wtf.Wtfable {
focusable := []wtf.Wtfable{}
for _, widget := range tracker.Widgets {
if widget.Focusable() {
focusable = append(focusable, widget)
}
}
// Sort for deterministic ordering
sort.SliceStable(focusable, func(i, j int) bool {
iTop := focusable[i].CommonSettings().Top
jTop := focusable[j].CommonSettings().Top
if iTop < jTop {
return true
}
if iTop == jTop {
return focusable[i].CommonSettings().Left < focusable[j].CommonSettings().Left
}
return false
})
return focusable
}
func (tracker *FocusTracker) focusableAt(idx int) wtf.Wtfable {
if idx < 0 || idx >= len(tracker.focusables()) {
return nil
}
return tracker.focusables()[idx]
}
func (tracker *FocusTracker) focusState() FocusState {
if tracker.Idx < 0 {
return neverFocused
}
for _, widget := range tracker.Widgets {
if widget.TextView() == tracker.tviewApp.GetFocus() {
return widgetFocused
}
}
return appBoardFocused
}
func (tracker *FocusTracker) increment() {
tracker.Idx++
if tracker.Idx == len(tracker.focusables()) {
tracker.Idx = 0
}
}
func (tracker *FocusTracker) useNavShortcuts() bool {
return tracker.config.UBool("wtf.navigation.shortcuts", true)
}