Drop-down allows typing to directly jump to options. Resolves #77

This commit is contained in:
Oliver 2018-03-18 20:42:51 +01:00
parent 258c9d1f8e
commit b357eaf10f
2 changed files with 81 additions and 10 deletions

View File

@ -1,7 +1,10 @@
package tview package tview
import ( import (
"strings"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
runewidth "github.com/mattn/go-runewidth"
) )
// dropDownOption is one option that can be selected in a drop-down primitive. // dropDownOption is one option that can be selected in a drop-down primitive.
@ -10,8 +13,8 @@ type dropDownOption struct {
Selected func() // The (optional) callback for when this option was selected. Selected func() // The (optional) callback for when this option was selected.
} }
// DropDown is a one-line box (three lines if there is a title) where the // DropDown implements a selection widget whose options become visible in a
// user can enter text. // drop-down list when activated.
// //
// See https://github.com/rivo/tview/wiki/DropDown for an example. // See https://github.com/rivo/tview/wiki/DropDown for an example.
type DropDown struct { type DropDown struct {
@ -27,6 +30,9 @@ type DropDown struct {
// Set to true if the options are visible and selectable. // Set to true if the options are visible and selectable.
open bool open bool
// The runes typed so far to directly access one of the list items.
prefix string
// The list element for the options. // The list element for the options.
list *List list *List
@ -42,6 +48,9 @@ type DropDown struct {
// The text color of the input area. // The text color of the input area.
fieldTextColor tcell.Color fieldTextColor tcell.Color
// The color for prefixes.
prefixTextColor tcell.Color
// The screen width of the input area. A value of 0 means extend as much as // The screen width of the input area. A value of 0 means extend as much as
// possible. // possible.
fieldWidth int fieldWidth int
@ -67,6 +76,7 @@ func NewDropDown() *DropDown {
labelColor: Styles.SecondaryTextColor, labelColor: Styles.SecondaryTextColor,
fieldBackgroundColor: Styles.ContrastBackgroundColor, fieldBackgroundColor: Styles.ContrastBackgroundColor,
fieldTextColor: Styles.PrimaryTextColor, fieldTextColor: Styles.PrimaryTextColor,
prefixTextColor: Styles.ContrastSecondaryTextColor,
} }
d.focus = d d.focus = d
@ -121,6 +131,14 @@ func (d *DropDown) SetFieldTextColor(color tcell.Color) *DropDown {
return d return d
} }
// SetPrefixTextColor sets the color of the prefix string. The prefix string is
// shown when the user starts typing text, which directly selects the first
// option that starts with the typed string.
func (d *DropDown) SetPrefixTextColor(color tcell.Color) *DropDown {
d.prefixTextColor = color
return d
}
// SetFormAttributes sets attributes shared by all form items. // SetFormAttributes sets attributes shared by all form items.
func (d *DropDown) SetFormAttributes(label string, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem { func (d *DropDown) SetFormAttributes(label string, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
d.label = label d.label = label
@ -238,12 +256,23 @@ func (d *DropDown) Draw(screen tcell.Screen) {
} }
// Draw selected text. // Draw selected text.
if d.currentOption >= 0 && d.currentOption < len(d.options) { if d.open && len(d.prefix) > 0 {
color := d.fieldTextColor // Show the prefix.
if d.GetFocusable().HasFocus() && !d.open { Print(screen, d.prefix, x, y, fieldWidth, AlignLeft, d.prefixTextColor)
color = d.fieldBackgroundColor prefixWidth := runewidth.StringWidth(d.prefix)
listItemText := d.options[d.list.GetCurrentItem()].Text
if prefixWidth < fieldWidth && len(d.prefix) < len(listItemText) {
Print(screen, listItemText[len(d.prefix):], x+prefixWidth, y, fieldWidth-prefixWidth, AlignLeft, d.fieldTextColor)
}
} else {
if d.currentOption >= 0 && d.currentOption < len(d.options) {
color := d.fieldTextColor
// Just show the current selection.
if d.GetFocusable().HasFocus() && !d.open {
color = d.fieldBackgroundColor
}
Print(screen, d.options[d.currentOption].Text, x, y, fieldWidth, AlignLeft, color)
} }
Print(screen, d.options[d.currentOption].Text, x, y, fieldWidth, AlignLeft, color)
} }
// Draw options list. // Draw options list.
@ -271,12 +300,34 @@ func (d *DropDown) Draw(screen tcell.Screen) {
// InputHandler returns the handler for this primitive. // InputHandler returns the handler for this primitive.
func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return d.wrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { return d.wrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
// A helper function which selects an item in the drop-down list based on
// the current prefix.
evalPrefix := func() {
if len(d.prefix) > 0 {
for index, option := range d.options {
if strings.HasPrefix(strings.ToLower(option.Text), d.prefix) {
d.list.SetCurrentItem(index)
return
}
}
// Prefix does not match any item. Remove last rune.
r := []rune(d.prefix)
d.prefix = string(r[:len(r)-1])
}
}
// Process key event. // Process key event.
switch key := event.Key(); key { switch key := event.Key(); key {
case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown: case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown:
if key == tcell.KeyRune && event.Rune() != ' ' { d.prefix = ""
break
// If the first key was a letter already, it becomes part of the prefix.
if r := event.Rune(); key == tcell.KeyRune && r != ' ' {
d.prefix += string(r)
evalPrefix()
} }
// Hand control over to the list.
d.open = true d.open = true
d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) { d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) {
// An option was selected. Close the list again. // An option was selected. Close the list again.
@ -288,6 +339,20 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
if d.options[d.currentOption].Selected != nil { if d.options[d.currentOption].Selected != nil {
d.options[d.currentOption].Selected() d.options[d.currentOption].Selected()
} }
}).SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyRune {
d.prefix += string(event.Rune())
evalPrefix()
} else if event.Key() == tcell.KeyBackspace || event.Key() == tcell.KeyBackspace2 {
if len(d.prefix) > 0 {
r := []rune(d.prefix)
d.prefix = string(r[:len(r)-1])
}
evalPrefix()
} else {
d.prefix = ""
}
return event
}) })
setFocus(d.list) setFocus(d.list)
case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab: case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab:

View File

@ -69,7 +69,8 @@ func NewList() *List {
} }
} }
// SetCurrentItem sets the currently selected item by its index. // SetCurrentItem sets the currently selected item by its index. This triggers
// a "changed" event.
func (l *List) SetCurrentItem(index int) *List { func (l *List) SetCurrentItem(index int) *List {
l.currentItem = index l.currentItem = index
if l.currentItem < len(l.items) && l.changed != nil { if l.currentItem < len(l.items) && l.changed != nil {
@ -79,6 +80,11 @@ func (l *List) SetCurrentItem(index int) *List {
return l return l
} }
// GetCurrentItem returns the index of the currently selected list item.
func (l *List) GetCurrentItem() int {
return l.currentItem
}
// SetMainTextColor sets the color of the items' main text. // SetMainTextColor sets the color of the items' main text.
func (l *List) SetMainTextColor(color tcell.Color) *List { func (l *List) SetMainTextColor(color tcell.Color) *List {
l.mainTextColor = color l.mainTextColor = color