diff --git a/CHANGELOG b/CHANGELOG index be486ce..b7bfa57 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,5 @@ v1.5.1 (WIP) +- Add FocusManager - Add Slider - Add TabbedPanels - Add Application.GetScreen and Application.GetScreenSize diff --git a/README.md b/README.md index 89965ad..cc2cb17 100644 --- a/README.md +++ b/README.md @@ -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__ diff --git a/contextmenu.go b/contextmenu.go index 5b57b2b..f767f69 100644 --- a/contextmenu.go +++ b/contextmenu.go @@ -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 } diff --git a/demos/focusmanager/main.go b/demos/focusmanager/main.go new file mode 100644 index 0000000..f756962 --- /dev/null +++ b/demos/focusmanager/main.go @@ -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) + } +} diff --git a/focus.go b/focus.go new file mode 100644 index 0000000..c8b08c1 --- /dev/null +++ b/focus.go @@ -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) +} diff --git a/focusable.go b/focusable.go deleted file mode 100644 index 1e6fee5..0000000 --- a/focusable.go +++ /dev/null @@ -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 -} diff --git a/inputfield.go b/inputfield.go index dfed8e1..11763a5 100644 --- a/inputfield.go +++ b/inputfield.go @@ -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 diff --git a/list.go b/list.go index 40c4d6c..2ed0977 100644 --- a/list.go +++ b/list.go @@ -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 } }