github.com/wtfutil/wtf@v0.43.0/app/focus_tracker.go (about) 1 package app 2 3 import ( 4 "fmt" 5 "sort" 6 7 "github.com/olebedev/config" 8 "github.com/rivo/tview" 9 "github.com/wtfutil/wtf/wtf" 10 ) 11 12 // FocusState is a custom type that differentiates focusable scopes 13 type FocusState int 14 15 const ( 16 widgetFocused FocusState = iota 17 appBoardFocused 18 neverFocused 19 ) 20 21 // FocusTracker is used by the app to track which onscreen widget currently has focus, 22 // and to move focus between widgets. 23 type FocusTracker struct { 24 Idx int 25 IsFocused bool 26 Widgets []wtf.Wtfable 27 28 config *config.Config 29 tviewApp *tview.Application 30 } 31 32 // NewFocusTracker creates and returns an instance of FocusTracker 33 func NewFocusTracker(tviewApp *tview.Application, widgets []wtf.Wtfable, config *config.Config) FocusTracker { 34 focusTracker := FocusTracker{ 35 tviewApp: tviewApp, 36 Idx: -1, 37 IsFocused: false, 38 Widgets: widgets, 39 40 config: config, 41 } 42 43 focusTracker.assignHotKeys() 44 45 return focusTracker 46 } 47 48 /* -------------------- Exported Functions -------------------- */ 49 50 // FocusOn puts the focus on the item that belongs to the focus character passed in 51 func (tracker *FocusTracker) FocusOn(char string) bool { 52 if !tracker.useNavShortcuts() { 53 return false 54 } 55 56 if tracker.focusState() == appBoardFocused { 57 return false 58 } 59 60 hasFocusable := false 61 62 for idx, focusable := range tracker.focusables() { 63 if focusable.FocusChar() == char { 64 tracker.blur(tracker.Idx) 65 tracker.Idx = idx 66 tracker.focus(tracker.Idx) 67 68 hasFocusable = true 69 tracker.IsFocused = true 70 break 71 } 72 } 73 74 return hasFocusable 75 } 76 77 // Next sets the focus on the next widget in the widget list. If the current widget is 78 // the last widget, sets focus on the first widget. 79 func (tracker *FocusTracker) Next() { 80 if tracker.focusState() == appBoardFocused { 81 return 82 } 83 84 tracker.blur(tracker.Idx) 85 tracker.increment() 86 tracker.focus(tracker.Idx) 87 88 tracker.IsFocused = true 89 } 90 91 // None removes focus from the currently-focused widget. 92 func (tracker *FocusTracker) None() { 93 if tracker.focusState() == appBoardFocused { 94 return 95 } 96 97 tracker.blur(tracker.Idx) 98 } 99 100 // Prev sets the focus on the previous widget in the widget list. If the current widget is 101 // the last widget, sets focus on the last widget. 102 func (tracker *FocusTracker) Prev() { 103 if tracker.focusState() == appBoardFocused { 104 return 105 } 106 107 tracker.blur(tracker.Idx) 108 tracker.decrement() 109 tracker.focus(tracker.Idx) 110 111 tracker.IsFocused = true 112 } 113 114 // Refocus forces the focus back to the currently-selected item 115 func (tracker *FocusTracker) Refocus() { 116 tracker.focus(tracker.Idx) 117 } 118 119 /* -------------------- Unexported Functions -------------------- */ 120 121 // AssignHotKeys assigns an alphabetic keyboard character to each focusable 122 // widget so that the widget can be brought into focus by pressing that keyboard key 123 // Valid numbers are between 1 and 9, inclusive 124 func (tracker *FocusTracker) assignHotKeys() { 125 if !tracker.useNavShortcuts() { 126 return 127 } 128 129 usedKeys := make(map[string]bool) 130 focusables := tracker.focusables() 131 132 // First, block out the explicitly-defined characters so they can't be automatically 133 // assigned to other modules 134 for _, focusable := range focusables { 135 if focusable.FocusChar() != "" { 136 usedKeys[focusable.FocusChar()] = true 137 } 138 } 139 140 focusNum := 1 141 142 // Range over all the modules and assign focus characters to any that are focusable 143 // and don't have explicitly-defined focus characters 144 for _, focusable := range focusables { 145 if focusable.FocusChar() != "" { 146 continue 147 } 148 149 if _, foundKey := usedKeys[fmt.Sprint(focusNum)]; foundKey { 150 for ; foundKey; _, foundKey = usedKeys[fmt.Sprint(focusNum)] { 151 focusNum++ 152 } 153 } 154 155 // Don't allow focus characters > "9" 156 if focusNum >= 10 { 157 break 158 } 159 160 focusable.SetFocusChar(fmt.Sprint(focusNum)) 161 focusNum++ 162 } 163 } 164 165 func (tracker *FocusTracker) blur(idx int) { 166 widget := tracker.focusableAt(idx) 167 if widget == nil { 168 return 169 } 170 171 view := widget.TextView() 172 view.Blur() 173 174 view.SetBorderColor( 175 wtf.ColorFor( 176 widget.BorderColor(), 177 ), 178 ) 179 180 tracker.IsFocused = false 181 } 182 183 func (tracker *FocusTracker) decrement() { 184 tracker.Idx-- 185 186 if tracker.Idx < 0 { 187 tracker.Idx = len(tracker.focusables()) - 1 188 } 189 } 190 191 func (tracker *FocusTracker) focus(idx int) { 192 widget := tracker.focusableAt(idx) 193 if widget == nil { 194 return 195 } 196 197 view := widget.TextView() 198 view.SetBorderColor( 199 wtf.ColorFor( 200 widget.CommonSettings().Colors.BorderTheme.Focused, 201 ), 202 ) 203 tracker.tviewApp.SetFocus(view) 204 } 205 206 func (tracker *FocusTracker) focusables() []wtf.Wtfable { 207 focusable := []wtf.Wtfable{} 208 209 for _, widget := range tracker.Widgets { 210 if widget.Focusable() { 211 focusable = append(focusable, widget) 212 } 213 } 214 215 // Sort for deterministic ordering 216 sort.SliceStable(focusable, func(i, j int) bool { 217 iTop := focusable[i].CommonSettings().Top 218 jTop := focusable[j].CommonSettings().Top 219 220 if iTop < jTop { 221 return true 222 } 223 if iTop == jTop { 224 return focusable[i].CommonSettings().Left < focusable[j].CommonSettings().Left 225 } 226 return false 227 }) 228 229 return focusable 230 } 231 232 func (tracker *FocusTracker) focusableAt(idx int) wtf.Wtfable { 233 if idx < 0 || idx >= len(tracker.focusables()) { 234 return nil 235 } 236 237 return tracker.focusables()[idx] 238 } 239 240 func (tracker *FocusTracker) focusState() FocusState { 241 if tracker.Idx < 0 { 242 return neverFocused 243 } 244 245 for _, widget := range tracker.Widgets { 246 if widget.TextView() == tracker.tviewApp.GetFocus() { 247 return widgetFocused 248 } 249 } 250 251 return appBoardFocused 252 } 253 254 func (tracker *FocusTracker) increment() { 255 tracker.Idx++ 256 257 if tracker.Idx == len(tracker.focusables()) { 258 tracker.Idx = 0 259 } 260 } 261 262 func (tracker *FocusTracker) useNavShortcuts() bool { 263 return tracker.config.UBool("wtf.navigation.shortcuts", true) 264 }