From b357eaf10f030620236dcc2b5a89056c6054212a Mon Sep 17 00:00:00 2001 From: Oliver <480930+rivo@users.noreply.github.com> Date: Sun, 18 Mar 2018 20:42:51 +0100 Subject: [PATCH] Drop-down allows typing to directly jump to options. Resolves #77 --- dropdown.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++------ list.go | 8 +++++- 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/dropdown.go b/dropdown.go index b201be0..9f68b4b 100644 --- a/dropdown.go +++ b/dropdown.go @@ -1,7 +1,10 @@ package tview import ( + "strings" + "github.com/gdamore/tcell" + runewidth "github.com/mattn/go-runewidth" ) // 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. } -// DropDown is a one-line box (three lines if there is a title) where the -// user can enter text. +// DropDown implements a selection widget whose options become visible in a +// drop-down list when activated. // // See https://github.com/rivo/tview/wiki/DropDown for an example. type DropDown struct { @@ -27,6 +30,9 @@ type DropDown struct { // Set to true if the options are visible and selectable. open bool + // The runes typed so far to directly access one of the list items. + prefix string + // The list element for the options. list *List @@ -42,6 +48,9 @@ type DropDown struct { // The text color of the input area. 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 // possible. fieldWidth int @@ -67,6 +76,7 @@ func NewDropDown() *DropDown { labelColor: Styles.SecondaryTextColor, fieldBackgroundColor: Styles.ContrastBackgroundColor, fieldTextColor: Styles.PrimaryTextColor, + prefixTextColor: Styles.ContrastSecondaryTextColor, } d.focus = d @@ -121,6 +131,14 @@ func (d *DropDown) SetFieldTextColor(color tcell.Color) *DropDown { 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. func (d *DropDown) SetFormAttributes(label string, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem { d.label = label @@ -238,12 +256,23 @@ func (d *DropDown) Draw(screen tcell.Screen) { } // Draw selected text. - if d.currentOption >= 0 && d.currentOption < len(d.options) { - color := d.fieldTextColor - if d.GetFocusable().HasFocus() && !d.open { - color = d.fieldBackgroundColor + if d.open && len(d.prefix) > 0 { + // Show the prefix. + Print(screen, d.prefix, x, y, fieldWidth, AlignLeft, d.prefixTextColor) + 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. @@ -271,12 +300,34 @@ func (d *DropDown) Draw(screen tcell.Screen) { // InputHandler returns the handler for this primitive. func (d *DropDown) InputHandler() 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. switch key := event.Key(); key { case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown: - if key == tcell.KeyRune && event.Rune() != ' ' { - break + d.prefix = "" + + // 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.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) { // 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 { 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) case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab: diff --git a/list.go b/list.go index a045fc1..6edf8c4 100644 --- a/list.go +++ b/list.go @@ -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 { l.currentItem = index if l.currentItem < len(l.items) && l.changed != nil { @@ -79,6 +80,11 @@ func (l *List) SetCurrentItem(index int) *List { 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. func (l *List) SetMainTextColor(color tcell.Color) *List { l.mainTextColor = color