Add FocusManager

This commit is contained in:
Trevor Slocum 2020-10-15 21:28:54 -07:00
parent ffd8b57dec
commit 597052a7e7
8 changed files with 274 additions and 24 deletions

View File

@ -1,4 +1,5 @@
v1.5.1 (WIP)
- Add FocusManager
- Add Slider
- Add TabbedPanels
- Add Application.GetScreen and Application.GetScreenSize

View File

@ -28,7 +28,7 @@ Available widgets:
- Selectable __lists__ with __context menus__
- Modal __dialogs__
- Horizontal and vertical __progress bars__
- __Grid__, __Flexbox__ and __panel layouts__
- __Grid__, __Flexbox__ and __tabbed panel layouts__
- Sophisticated navigable __table views__
- Flexible __tree views__
- Draggable and resizable __windows__

View File

@ -67,7 +67,7 @@ func (c *ContextMenu) AddContextItem(text string, shortcut rune, selected func(i
if text == "" && shortcut == 0 {
c.list.Lock()
index := len(c.list.items) - 1
c.list.items[index].enabled = false
c.list.items[index].disabled = true
c.list.Unlock()
}
}
@ -137,7 +137,7 @@ func (c *ContextMenu) show(item int, x int, y int, setFocus func(Primitive)) {
c.list.Lock()
for i, item := range c.list.items {
if item.enabled {
if !item.disabled {
c.list.currentItem = i
break
}

View File

@ -0,0 +1,82 @@
// Demo code for the FocusManager utility.
package main
import (
"log"
"github.com/gdamore/tcell/v2"
"gitlab.com/tslocum/cbind"
"gitlab.com/tslocum/cview"
)
func main() {
app := cview.NewApplication()
app.EnableMouse(true)
input1 := cview.NewInputField()
input1.SetLabel("InputField 1")
input2 := cview.NewInputField()
input2.SetLabel("InputField 2")
input3 := cview.NewInputField()
input3.SetLabel("InputField 3")
input4 := cview.NewInputField()
input4.SetLabel("InputField 4")
grid := cview.NewGrid()
grid.SetBorder(true)
grid.SetTitle(" Press Tab to advance focus ")
grid.AddItem(input1, 0, 0, 1, 1, 0, 0, true)
grid.AddItem(input2, 0, 1, 1, 1, 0, 0, false)
grid.AddItem(input3, 1, 1, 1, 1, 0, 0, false)
grid.AddItem(input4, 1, 0, 1, 1, 0, 0, false)
focusManager := cview.NewFocusManager(app.SetFocus)
focusManager.SetWrapAround(true)
focusManager.Add(input1, input2, input3, input4)
inputHandler := cbind.NewConfiguration()
for _, key := range cview.Keys.MovePreviousField {
mod, key, ch, err := cbind.Decode(key)
if err != nil {
log.Fatal(err)
}
if key == tcell.KeyRune {
inputHandler.SetRune(mod, ch, func(ev *tcell.EventKey) *tcell.EventKey {
focusManager.FocusPrevious()
return nil
})
} else {
inputHandler.SetKey(mod, key, func(ev *tcell.EventKey) *tcell.EventKey {
focusManager.FocusPrevious()
return nil
})
}
}
for _, key := range cview.Keys.MoveNextField {
mod, key, ch, err := cbind.Decode(key)
if err != nil {
log.Fatal(err)
}
if key == tcell.KeyRune {
inputHandler.SetRune(mod, ch, func(ev *tcell.EventKey) *tcell.EventKey {
focusManager.FocusNext()
return nil
})
} else {
inputHandler.SetKey(mod, key, func(ev *tcell.EventKey) *tcell.EventKey {
focusManager.FocusNext()
return nil
})
}
}
app.SetInputCapture(inputHandler.Capture)
app.SetRoot(grid, true)
if err := app.Run(); err != nil {
panic(err)
}
}

179
focus.go Normal file
View File

@ -0,0 +1,179 @@
package cview
import "sync"
// Focusable provides a method which determines if a primitive has focus.
// Composed primitives may be focused based on the focused state of their
// contained primitives.
type Focusable interface {
HasFocus() bool
}
type focusElement struct {
primitive Primitive
disabled bool
}
// FocusManager manages application focus.
type FocusManager struct {
elements []*focusElement
focused int
wrapAround bool
setFocus func(p Primitive)
sync.RWMutex
}
// NewFocusManager returns a new FocusManager object.
func NewFocusManager(setFocus func(p Primitive)) *FocusManager {
return &FocusManager{setFocus: setFocus}
}
// SetWrapAround sets the flag that determines whether navigation will wrap
// around. That is, navigating forwards on the last field will move the
// selection to the first field (similarly in the other direction). If set to
// false, the focus won't change when navigating forwards on the last element
// or navigating backwards on the first element.
func (f *FocusManager) SetWrapAround(wrapAround bool) {
f.Lock()
defer f.Unlock()
f.wrapAround = wrapAround
}
// Add adds an element to the focus handler.
func (f *FocusManager) Add(p ...Primitive) {
f.Lock()
defer f.Unlock()
for _, primitive := range p {
f.elements = append(f.elements, &focusElement{primitive: primitive})
}
}
// AddAt adds an element to the focus handler at the specified index.
func (f *FocusManager) AddAt(index int, p Primitive) {
f.Lock()
defer f.Unlock()
if index < 0 || index > len(f.elements) {
panic("index out of range")
}
element := &focusElement{primitive: p}
if index == len(f.elements) {
f.elements = append(f.elements, element)
return
}
f.elements = append(f.elements[:index+1], f.elements[index:]...)
f.elements[index] = element
}
// Focus focuses the provided element.
func (f *FocusManager) Focus(p Primitive) {
f.Lock()
defer f.Unlock()
for i, element := range f.elements {
if p == element.primitive && !element.disabled {
f.focused = i
break
}
}
f.setFocus(f.elements[f.focused].primitive)
}
// FocusPrevious focuses the previous element.
func (f *FocusManager) FocusPrevious() {
f.Lock()
defer f.Unlock()
f.focused--
f.updateFocusIndex(true)
f.setFocus(f.elements[f.focused].primitive)
}
// FocusNext focuses the next element.
func (f *FocusManager) FocusNext() {
f.Lock()
defer f.Unlock()
f.focused++
f.updateFocusIndex(false)
f.setFocus(f.elements[f.focused].primitive)
}
// FocusAt focuses the element at the provided index.
func (f *FocusManager) FocusAt(index int) {
f.Lock()
defer f.Unlock()
f.focused = index
f.setFocus(f.elements[f.focused].primitive)
}
// GetFocusIndex returns the index of the currently focused element.
func (f *FocusManager) GetFocusIndex() int {
f.Lock()
defer f.Unlock()
return f.focused
}
// GetFocusedPrimitive returns the currently focused primitive.
func (f *FocusManager) GetFocusedPrimitive() Primitive {
f.Lock()
defer f.Unlock()
return f.elements[f.focused].primitive
}
func (f *FocusManager) updateFocusIndex(decreasing bool) {
for i := 0; i < len(f.elements); i++ {
if f.focused < 0 {
if f.wrapAround {
f.focused = len(f.elements) - 1
} else {
f.focused = 0
}
} else if f.focused >= len(f.elements) {
if f.wrapAround {
f.focused = 0
} else {
f.focused = len(f.elements) - 1
}
}
item := f.elements[f.focused]
if !item.disabled {
break
}
if decreasing {
f.focused--
} else {
f.focused++
}
}
}
// Transform modifies the current focus.
func (f *FocusManager) Transform(tr Transformation) {
var decreasing bool
switch tr {
case TransformFirstItem:
f.focused = 0
decreasing = true
case TransformLastItem:
f.focused = len(f.elements) - 1
case TransformPreviousItem:
f.focused--
decreasing = true
case TransformNextItem:
f.focused++
}
f.updateFocusIndex(decreasing)
}

View File

@ -1,8 +0,0 @@
package cview
// Focusable provides a method which determines if a primitive has focus.
// Composed primitives may be focused based on the focused state of their
// contained primitives.
type Focusable interface {
HasFocus() bool
}

View File

@ -463,7 +463,6 @@ func (i *InputField) Autocomplete() {
currentEntry := -1
i.autocompleteList.Clear()
for index, entry := range entries {
entry.enabled = true
i.autocompleteList.AddItem(entry)
if currentEntry < 0 && entry.GetMainText() == string(i.text) {
currentEntry = index

21
list.go
View File

@ -11,7 +11,7 @@ import (
// ListItem represents an item in a List.
type ListItem struct {
enabled bool // Whether or not the list item is selectable.
disabled bool // Whether or not the list item is selectable.
mainText []byte // The main text of the list item.
secondaryText []byte // A secondary text to be shown underneath the main text.
shortcut rune // The key to select the list item directly, 0 if there is no shortcut.
@ -25,7 +25,6 @@ type ListItem struct {
func NewListItem(mainText string) *ListItem {
return &ListItem{
mainText: []byte(mainText),
enabled: true,
}
}
@ -550,8 +549,6 @@ func (l *List) AddItem(item *ListItem) {
func (l *List) InsertItem(index int, item *ListItem) {
l.Lock()
item.enabled = true
// Shift index to range.
if index < 0 {
index = len(l.items) + index + 1
@ -627,7 +624,7 @@ func (l *List) SetItemEnabled(index int, enabled bool) {
defer l.Unlock()
item := l.items[index]
item.enabled = enabled
item.disabled = !enabled
}
// FindItems searches the main and secondary texts for the given strings and
@ -772,7 +769,7 @@ func (l *List) transform(tr Transformation) {
}
item := l.items[l.currentItem]
if item.enabled && (item.shortcut > 0 || len(item.mainText) > 0 || len(item.secondaryText) > 0) {
if !item.disabled && (item.shortcut > 0 || len(item.mainText) > 0 || len(item.secondaryText) > 0) {
break
}
@ -929,7 +926,7 @@ func (l *List) Draw(screen tcell.Screen) {
RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor)
y++
continue
} else if !item.enabled { // Disabled item
} else if item.disabled {
// Shortcuts.
if showShortcuts && item.shortcut != 0 {
Print(screen, []byte(fmt.Sprintf("(%c)", item.shortcut)), x-5, y, 4, AlignRight, tcell.ColorDarkSlateGray.TrueColor())
@ -1086,7 +1083,7 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
} else if HitShortcut(event, Keys.Select, Keys.Select2) {
if l.currentItem >= 0 && l.currentItem < len(l.items) {
item := l.items[l.currentItem]
if item.enabled {
if !item.disabled {
if item.selected != nil {
l.Unlock()
item.selected()
@ -1111,7 +1108,7 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
if ch != ' ' {
// It's not a space bar. Is it a shortcut?
for index, item := range l.items {
if item.enabled && item.shortcut == ch {
if !item.disabled && item.shortcut == ch {
// We have a shortcut.
l.currentItem = index
@ -1241,7 +1238,7 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse,
index := l.indexAtPoint(event.Position())
if index != -1 {
item := l.items[index]
if item.enabled {
if !item.disabled {
l.currentItem = index
if item.selected != nil {
l.Unlock()
@ -1279,7 +1276,7 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse,
index := l.indexAtPoint(event.Position())
if index != -1 {
item := l.items[index]
if item.enabled {
if !item.disabled {
l.currentItem = index
if index != l.currentItem && l.changed != nil {
l.Unlock()
@ -1298,7 +1295,7 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse,
index := l.indexAtY(y)
if index >= 0 {
item := l.items[index]
if item.enabled {
if !item.disabled {
l.currentItem = index
}
}