forked from tslocum/cview
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
824 lines
24 KiB
824 lines
24 KiB
package cview |
|
|
|
import ( |
|
"strings" |
|
"sync" |
|
|
|
"github.com/gdamore/tcell/v2" |
|
"github.com/mattn/go-runewidth" |
|
) |
|
|
|
// DropDownOption is one option that can be selected in a drop-down primitive. |
|
type DropDownOption struct { |
|
text string // The text to be displayed in the drop-down. |
|
selected func(index int, option *DropDownOption) // The (optional) callback for when this option was selected. |
|
reference interface{} // An optional reference object. |
|
|
|
sync.RWMutex |
|
} |
|
|
|
// NewDropDownOption returns a new option for a dropdown. |
|
func NewDropDownOption(text string) *DropDownOption { |
|
return &DropDownOption{text: text} |
|
} |
|
|
|
// GetText returns the text of this dropdown option. |
|
func (d *DropDownOption) GetText() string { |
|
d.RLock() |
|
defer d.RUnlock() |
|
|
|
return d.text |
|
} |
|
|
|
// SetText returns the text of this dropdown option. |
|
func (d *DropDownOption) SetText(text string) { |
|
d.text = text |
|
} |
|
|
|
// SetSelectedFunc sets the handler to be called when this option is selected. |
|
func (d *DropDownOption) SetSelectedFunc(handler func(index int, option *DropDownOption)) { |
|
d.selected = handler |
|
} |
|
|
|
// GetReference returns the reference object of this dropdown option. |
|
func (d *DropDownOption) GetReference() interface{} { |
|
d.RLock() |
|
defer d.RUnlock() |
|
|
|
return d.reference |
|
} |
|
|
|
// SetReference allows you to store a reference of any type in this option. |
|
func (d *DropDownOption) SetReference(reference interface{}) { |
|
d.reference = reference |
|
} |
|
|
|
// DropDown implements a selection widget whose options become visible in a |
|
// drop-down list when activated. |
|
type DropDown struct { |
|
*Box |
|
|
|
// The options from which the user can choose. |
|
options []*DropDownOption |
|
|
|
// Strings to be placed before and after each drop-down option. |
|
optionPrefix, optionSuffix string |
|
|
|
// The index of the currently selected option. Negative if no option is |
|
// currently selected. |
|
currentOption int |
|
|
|
// Strings to be placed before and after the current option. |
|
currentOptionPrefix, currentOptionSuffix string |
|
|
|
// The text to be displayed when no option has yet been selected. |
|
noSelection string |
|
|
|
// 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 |
|
|
|
// The text to be displayed before the input area. |
|
label string |
|
|
|
// The label color. |
|
labelColor tcell.Color |
|
|
|
// The label color when focused. |
|
labelColorFocused tcell.Color |
|
|
|
// The background color of the input area. |
|
fieldBackgroundColor tcell.Color |
|
|
|
// The background color of the input area when focused. |
|
fieldBackgroundColorFocused tcell.Color |
|
|
|
// The text color of the input area. |
|
fieldTextColor tcell.Color |
|
|
|
// The text color of the input area when focused. |
|
fieldTextColorFocused tcell.Color |
|
|
|
// The color for prefixes. |
|
prefixTextColor tcell.Color |
|
|
|
// The screen width of the label area. A value of 0 means use the width of |
|
// the label text. |
|
labelWidth int |
|
|
|
// The screen width of the input area. A value of 0 means extend as much as |
|
// possible. |
|
fieldWidth int |
|
|
|
// An optional function which is called when the user indicated that they |
|
// are done selecting options. The key which was pressed is provided (tab, |
|
// shift-tab, or escape). |
|
done func(tcell.Key) |
|
|
|
// A callback function set by the Form class and called when the user leaves |
|
// this form item. |
|
finished func(tcell.Key) |
|
|
|
// A callback function which is called when the user changes the drop-down's |
|
// selection. |
|
selected func(index int, option *DropDownOption) |
|
|
|
// Set to true when mouse dragging is in progress. |
|
dragging bool |
|
|
|
// The chars to show when the option's text gets shortened. |
|
abbreviationChars string |
|
|
|
// The symbol to draw at the end of the field when closed. |
|
dropDownSymbol rune |
|
|
|
// The symbol to draw at the end of the field when opened. |
|
dropDownOpenSymbol rune |
|
|
|
// A flag that determines whether the drop down symbol is always drawn. |
|
alwaysDrawDropDownSymbol bool |
|
|
|
sync.RWMutex |
|
} |
|
|
|
// NewDropDown returns a new drop-down. |
|
func NewDropDown() *DropDown { |
|
list := NewList() |
|
list.ShowSecondaryText(false) |
|
list.SetMainTextColor(Styles.SecondaryTextColor) |
|
list.SetSelectedTextColor(Styles.PrimitiveBackgroundColor) |
|
list.SetSelectedBackgroundColor(Styles.PrimaryTextColor) |
|
list.SetHighlightFullLine(true) |
|
list.SetBackgroundColor(Styles.MoreContrastBackgroundColor) |
|
|
|
d := &DropDown{ |
|
Box: NewBox(), |
|
currentOption: -1, |
|
list: list, |
|
labelColor: Styles.SecondaryTextColor, |
|
fieldBackgroundColor: Styles.ContrastBackgroundColor, |
|
fieldTextColor: Styles.PrimaryTextColor, |
|
prefixTextColor: Styles.ContrastSecondaryTextColor, |
|
dropDownSymbol: Styles.DropDownSymbol, |
|
dropDownOpenSymbol: Styles.DropDownOpenSymbol, |
|
abbreviationChars: Styles.DropDownAbbreviationChars, |
|
labelColorFocused: ColorUnset, |
|
fieldBackgroundColorFocused: ColorUnset, |
|
fieldTextColorFocused: ColorUnset, |
|
} |
|
|
|
d.focus = d |
|
|
|
return d |
|
} |
|
|
|
// SetDropDownSymbolRune sets the rune to be drawn at the end of the dropdown field |
|
// to indicate that this field is a dropdown. |
|
func (d *DropDown) SetDropDownSymbolRune(symbol rune) { |
|
d.Lock() |
|
defer d.Unlock() |
|
d.dropDownSymbol = symbol |
|
} |
|
|
|
// SetDropDownOpenSymbolRune sets the rune to be drawn at the end of the |
|
// dropdown field to indicate that the a dropdown is open. |
|
func (d *DropDown) SetDropDownOpenSymbolRune(symbol rune) { |
|
d.Lock() |
|
defer d.Unlock() |
|
d.dropDownOpenSymbol = symbol |
|
} |
|
|
|
// SetAlwaysDrawDropDownSymbol sets a flad that determines whether the drop |
|
// down symbol is always drawn. The symbol is normally only drawn when focused. |
|
func (d *DropDown) SetAlwaysDrawDropDownSymbol(alwaysDraw bool) { |
|
d.Lock() |
|
defer d.Unlock() |
|
d.alwaysDrawDropDownSymbol = alwaysDraw |
|
} |
|
|
|
// SetCurrentOption sets the index of the currently selected option. This may |
|
// be a negative value to indicate that no option is currently selected. Calling |
|
// this function will also trigger the "selected" callback (if there is one). |
|
func (d *DropDown) SetCurrentOption(index int) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
if index >= 0 && index < len(d.options) { |
|
d.currentOption = index |
|
d.list.SetCurrentItem(index) |
|
if d.selected != nil { |
|
d.Unlock() |
|
d.selected(index, d.options[index]) |
|
d.Lock() |
|
} |
|
if d.options[index].selected != nil { |
|
d.Unlock() |
|
d.options[index].selected(index, d.options[index]) |
|
d.Lock() |
|
} |
|
} else { |
|
d.currentOption = -1 |
|
d.list.SetCurrentItem(0) // Set to 0 because -1 means "last item". |
|
if d.selected != nil { |
|
d.Unlock() |
|
d.selected(-1, nil) |
|
d.Lock() |
|
} |
|
} |
|
} |
|
|
|
// GetCurrentOption returns the index of the currently selected option as well |
|
// as the option itself. If no option was selected, -1 and nil is returned. |
|
func (d *DropDown) GetCurrentOption() (int, *DropDownOption) { |
|
d.RLock() |
|
defer d.RUnlock() |
|
|
|
var option *DropDownOption |
|
if d.currentOption >= 0 && d.currentOption < len(d.options) { |
|
option = d.options[d.currentOption] |
|
} |
|
return d.currentOption, option |
|
} |
|
|
|
// SetTextOptions sets the text to be placed before and after each drop-down |
|
// option (prefix/suffix), the text placed before and after the currently |
|
// selected option (currentPrefix/currentSuffix) as well as the text to be |
|
// displayed when no option is currently selected. Per default, all of these |
|
// strings are empty. |
|
func (d *DropDown) SetTextOptions(prefix, suffix, currentPrefix, currentSuffix, noSelection string) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
d.currentOptionPrefix = currentPrefix |
|
d.currentOptionSuffix = currentSuffix |
|
d.noSelection = noSelection |
|
d.optionPrefix = prefix |
|
d.optionSuffix = suffix |
|
for index := 0; index < d.list.GetItemCount(); index++ { |
|
d.list.SetItemText(index, prefix+d.options[index].text+suffix, "") |
|
} |
|
} |
|
|
|
// SetLabel sets the text to be displayed before the input area. |
|
func (d *DropDown) SetLabel(label string) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
d.label = label |
|
} |
|
|
|
// GetLabel returns the text to be displayed before the input area. |
|
func (d *DropDown) GetLabel() string { |
|
d.RLock() |
|
defer d.RUnlock() |
|
|
|
return d.label |
|
} |
|
|
|
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the |
|
// primitive to use the width of the label string. |
|
func (d *DropDown) SetLabelWidth(width int) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
d.labelWidth = width |
|
} |
|
|
|
// SetLabelColor sets the color of the label. |
|
func (d *DropDown) SetLabelColor(color tcell.Color) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
d.labelColor = color |
|
} |
|
|
|
// SetLabelColorFocused sets the color of the label when focused. |
|
func (d *DropDown) SetLabelColorFocused(color tcell.Color) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
d.labelColorFocused = color |
|
} |
|
|
|
// SetFieldBackgroundColor sets the background color of the options area. |
|
func (d *DropDown) SetFieldBackgroundColor(color tcell.Color) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
d.fieldBackgroundColor = color |
|
} |
|
|
|
// SetFieldBackgroundColorFocused sets the background color of the options area when focused. |
|
func (d *DropDown) SetFieldBackgroundColorFocused(color tcell.Color) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
d.fieldBackgroundColorFocused = color |
|
} |
|
|
|
// SetFieldTextColor sets the text color of the options area. |
|
func (d *DropDown) SetFieldTextColor(color tcell.Color) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
d.fieldTextColor = color |
|
} |
|
|
|
// SetFieldTextColorFocused sets the text color of the options area when focused. |
|
func (d *DropDown) SetFieldTextColorFocused(color tcell.Color) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
d.fieldTextColorFocused = color |
|
} |
|
|
|
// SetDropDownTextColor sets text color of the drop-down list. |
|
func (d *DropDown) SetDropDownTextColor(color tcell.Color) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
d.list.SetMainTextColor(color) |
|
} |
|
|
|
// SetDropDownBackgroundColor sets the background color of the drop-down list. |
|
func (d *DropDown) SetDropDownBackgroundColor(color tcell.Color) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
d.list.SetBackgroundColor(color) |
|
} |
|
|
|
// SetDropDownSelectedTextColor sets the text color of the selected option in |
|
// the drop-down list. |
|
func (d *DropDown) SetDropDownSelectedTextColor(color tcell.Color) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
d.list.SetSelectedTextColor(color) |
|
} |
|
|
|
// SetDropDownSelectedBackgroundColor sets the background color of the selected |
|
// option in the drop-down list. |
|
func (d *DropDown) SetDropDownSelectedBackgroundColor(color tcell.Color) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
d.list.SetSelectedBackgroundColor(color) |
|
} |
|
|
|
// 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) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
d.prefixTextColor = color |
|
} |
|
|
|
// SetFieldWidth sets the screen width of the options area. A value of 0 means |
|
// extend to as long as the longest option text. |
|
func (d *DropDown) SetFieldWidth(width int) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
d.fieldWidth = width |
|
} |
|
|
|
// GetFieldHeight returns the height of the field. |
|
func (d *DropDown) GetFieldHeight() int { |
|
return 1 |
|
} |
|
|
|
// GetFieldWidth returns this primitive's field screen width. |
|
func (d *DropDown) GetFieldWidth() int { |
|
d.RLock() |
|
defer d.RUnlock() |
|
return d.getFieldWidth() |
|
} |
|
|
|
func (d *DropDown) getFieldWidth() int { |
|
if d.fieldWidth > 0 { |
|
return d.fieldWidth |
|
} |
|
fieldWidth := 0 |
|
for _, option := range d.options { |
|
width := TaggedStringWidth(option.text) |
|
if width > fieldWidth { |
|
fieldWidth = width |
|
} |
|
} |
|
fieldWidth += len(d.optionPrefix) + len(d.optionSuffix) |
|
fieldWidth += len(d.currentOptionPrefix) + len(d.currentOptionSuffix) |
|
fieldWidth += 3 // space + dropDownSymbol + space |
|
return fieldWidth |
|
} |
|
|
|
// AddOptionsSimple adds new selectable options to this drop-down. |
|
func (d *DropDown) AddOptionsSimple(options ...string) { |
|
optionsToAdd := make([]*DropDownOption, len(options)) |
|
for i, option := range options { |
|
optionsToAdd[i] = NewDropDownOption(option) |
|
} |
|
d.AddOptions(optionsToAdd...) |
|
} |
|
|
|
// AddOptions adds new selectable options to this drop-down. |
|
func (d *DropDown) AddOptions(options ...*DropDownOption) { |
|
d.Lock() |
|
defer d.Unlock() |
|
d.addOptions(options...) |
|
} |
|
|
|
func (d *DropDown) addOptions(options ...*DropDownOption) { |
|
d.options = append(d.options, options...) |
|
for _, option := range options { |
|
d.list.AddItem(NewListItem(d.optionPrefix + option.text + d.optionSuffix)) |
|
} |
|
} |
|
|
|
// SetOptionsSimple replaces all current options with the ones provided and installs |
|
// one callback function which is called when one of the options is selected. |
|
// It will be called with the option's index and the option itself |
|
// The "selected" parameter may be nil. |
|
func (d *DropDown) SetOptionsSimple(selected func(index int, option *DropDownOption), options ...string) { |
|
optionsToSet := make([]*DropDownOption, len(options)) |
|
for i, option := range options { |
|
optionsToSet[i] = NewDropDownOption(option) |
|
} |
|
d.SetOptions(selected, optionsToSet...) |
|
} |
|
|
|
// SetOptions replaces all current options with the ones provided and installs |
|
// one callback function which is called when one of the options is selected. |
|
// It will be called with the option's index and the option itself. |
|
// The "selected" parameter may be nil. |
|
func (d *DropDown) SetOptions(selected func(index int, option *DropDownOption), options ...*DropDownOption) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
d.list.Clear() |
|
d.options = nil |
|
d.addOptions(options...) |
|
d.selected = selected |
|
} |
|
|
|
// SetChangedFunc sets a handler which is called when the user changes the |
|
// focused drop-down option. The handler is provided with the selected option's |
|
// index and the option itself. If "no option" was selected, these values are |
|
// -1 and nil. |
|
func (d *DropDown) SetChangedFunc(handler func(index int, option *DropDownOption)) { |
|
d.list.SetChangedFunc(func(index int, item *ListItem) { |
|
handler(index, d.options[index]) |
|
}) |
|
} |
|
|
|
// SetSelectedFunc sets a handler which is called when the user selects a |
|
// drop-down's option. This handler will be called in addition and prior to |
|
// an option's optional individual handler. The handler is provided with the |
|
// selected option's index and the option itself. If "no option" was selected, these values |
|
// are -1 and nil. |
|
func (d *DropDown) SetSelectedFunc(handler func(index int, option *DropDownOption)) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
d.selected = handler |
|
} |
|
|
|
// SetDoneFunc sets a handler which is called when the user is done selecting |
|
// options. The callback function is provided with the key that was pressed, |
|
// which is one of the following: |
|
// |
|
// - KeyEscape: Abort selection. |
|
// - KeyTab: Move to the next field. |
|
// - KeyBacktab: Move to the previous field. |
|
func (d *DropDown) SetDoneFunc(handler func(key tcell.Key)) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
d.done = handler |
|
} |
|
|
|
// SetFinishedFunc sets a callback invoked when the user leaves this form item. |
|
func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) { |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
d.finished = handler |
|
} |
|
|
|
// Draw draws this primitive onto the screen. |
|
func (d *DropDown) Draw(screen tcell.Screen) { |
|
d.Box.Draw(screen) |
|
hasFocus := d.GetFocusable().HasFocus() |
|
|
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
// Select colors |
|
labelColor := d.labelColor |
|
fieldBackgroundColor := d.fieldBackgroundColor |
|
fieldTextColor := d.fieldTextColor |
|
if hasFocus { |
|
if d.labelColorFocused != ColorUnset { |
|
labelColor = d.labelColorFocused |
|
} |
|
if d.fieldBackgroundColorFocused != ColorUnset { |
|
fieldBackgroundColor = d.fieldBackgroundColorFocused |
|
} |
|
if d.fieldTextColorFocused != ColorUnset { |
|
fieldTextColor = d.fieldTextColorFocused |
|
} |
|
} |
|
|
|
// Prepare. |
|
x, y, width, height := d.GetInnerRect() |
|
rightLimit := x + width |
|
if height < 1 || rightLimit <= x { |
|
return |
|
} |
|
|
|
// Draw label. |
|
if d.labelWidth > 0 { |
|
labelWidth := d.labelWidth |
|
if labelWidth > rightLimit-x { |
|
labelWidth = rightLimit - x |
|
} |
|
Print(screen, []byte(d.label), x, y, labelWidth, AlignLeft, labelColor) |
|
x += labelWidth |
|
} else { |
|
_, drawnWidth := Print(screen, []byte(d.label), x, y, rightLimit-x, AlignLeft, labelColor) |
|
x += drawnWidth |
|
} |
|
|
|
// What's the longest option text? |
|
maxWidth := 0 |
|
optionWrapWidth := TaggedStringWidth(d.optionPrefix + d.optionSuffix) |
|
for _, option := range d.options { |
|
strWidth := TaggedStringWidth(option.text) + optionWrapWidth |
|
if strWidth > maxWidth { |
|
maxWidth = strWidth |
|
} |
|
} |
|
|
|
// Draw selection area. |
|
fieldWidth := d.getFieldWidth() |
|
if fieldWidth == 0 { |
|
fieldWidth = maxWidth |
|
if d.currentOption < 0 { |
|
noSelectionWidth := TaggedStringWidth(d.noSelection) |
|
if noSelectionWidth > fieldWidth { |
|
fieldWidth = noSelectionWidth |
|
} |
|
} else if d.currentOption < len(d.options) { |
|
currentOptionWidth := TaggedStringWidth(d.currentOptionPrefix + d.options[d.currentOption].text + d.currentOptionSuffix) |
|
if currentOptionWidth > fieldWidth { |
|
fieldWidth = currentOptionWidth |
|
} |
|
} |
|
} |
|
if rightLimit-x < fieldWidth { |
|
fieldWidth = rightLimit - x |
|
} |
|
fieldStyle := tcell.StyleDefault.Background(fieldBackgroundColor) |
|
for index := 0; index < fieldWidth; index++ { |
|
screen.SetContent(x+index, y, ' ', nil, fieldStyle) |
|
} |
|
|
|
// Draw selected text. |
|
if d.open && len(d.prefix) > 0 { |
|
// Show the prefix. |
|
currentOptionPrefixWidth := TaggedStringWidth(d.currentOptionPrefix) |
|
prefixWidth := runewidth.StringWidth(d.prefix) |
|
listItemText := d.options[d.list.GetCurrentItemIndex()].text |
|
Print(screen, []byte(d.currentOptionPrefix), x, y, fieldWidth, AlignLeft, fieldTextColor) |
|
Print(screen, []byte(d.prefix), x+currentOptionPrefixWidth, y, fieldWidth-currentOptionPrefixWidth, AlignLeft, d.prefixTextColor) |
|
if len(d.prefix) < len(listItemText) { |
|
Print(screen, []byte(listItemText[len(d.prefix):]+d.currentOptionSuffix), x+prefixWidth+currentOptionPrefixWidth, y, fieldWidth-prefixWidth-currentOptionPrefixWidth, AlignLeft, fieldTextColor) |
|
} |
|
} else { |
|
color := fieldTextColor |
|
text := d.noSelection |
|
if d.currentOption >= 0 && d.currentOption < len(d.options) { |
|
text = d.currentOptionPrefix + d.options[d.currentOption].text + d.currentOptionSuffix |
|
} |
|
// Abbreviate text when not fitting |
|
if fieldWidth > len(d.abbreviationChars)+3 && len(text) > fieldWidth { |
|
text = text[0:fieldWidth-3-len(d.abbreviationChars)] + d.abbreviationChars |
|
} |
|
|
|
// Just show the current selection. |
|
Print(screen, []byte(text), x, y, fieldWidth, AlignLeft, color) |
|
} |
|
|
|
// Draw drop-down symbol |
|
if d.alwaysDrawDropDownSymbol || d._hasFocus() { |
|
symbol := d.dropDownSymbol |
|
if d.open { |
|
symbol = d.dropDownOpenSymbol |
|
} |
|
screen.SetContent(x+fieldWidth-2, y, symbol, nil, new(tcell.Style).Foreground(fieldTextColor).Background(fieldBackgroundColor)) |
|
} |
|
|
|
// Draw options list. |
|
if hasFocus && d.open { |
|
// We prefer to drop-down but if there is no space, maybe drop up? |
|
lx := x |
|
ly := y + 1 |
|
lheight := len(d.options) |
|
_, sheight := screen.Size() |
|
if ly+lheight >= sheight && ly-2 > lheight-ly { |
|
ly = y - lheight |
|
if ly < 0 { |
|
ly = 0 |
|
} |
|
} |
|
if ly+lheight >= sheight { |
|
lheight = sheight - ly |
|
} |
|
lwidth := maxWidth |
|
if d.list.scrollBarVisibility == ScrollBarAlways || (d.list.scrollBarVisibility == ScrollBarAuto && len(d.options) > lheight) { |
|
lwidth++ // Add space for scroll bar |
|
} |
|
if lwidth < fieldWidth { |
|
lwidth = fieldWidth |
|
} |
|
d.list.SetRect(lx, ly, lwidth, lheight) |
|
d.list.Draw(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)) { |
|
// Process key event. |
|
switch key := event.Key(); key { |
|
case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown: |
|
d.Lock() |
|
defer d.Unlock() |
|
|
|
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) |
|
d.evalPrefix() |
|
} |
|
|
|
d.openList(setFocus) |
|
case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab: |
|
if d.done != nil { |
|
d.done(key) |
|
} |
|
if d.finished != nil { |
|
d.finished(key) |
|
} |
|
} |
|
}) |
|
} |
|
|
|
// evalPrefix selects an item in the drop-down list based on the current prefix. |
|
func (d *DropDown) evalPrefix() { |
|
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]) |
|
} |
|
} |
|
|
|
// openList hands control over to the embedded List primitive. |
|
func (d *DropDown) openList(setFocus func(Primitive)) { |
|
d.open = true |
|
optionBefore := d.currentOption |
|
|
|
d.list.SetSelectedFunc(func(index int, item *ListItem) { |
|
if d.dragging { |
|
return // If we're dragging the mouse, we don't want to trigger any events. |
|
} |
|
|
|
// An option was selected. Close the list again. |
|
d.currentOption = index |
|
d.closeList(setFocus) |
|
|
|
// Trigger "selected" event. |
|
if d.selected != nil { |
|
d.selected(d.currentOption, d.options[d.currentOption]) |
|
} |
|
if d.options[d.currentOption].selected != nil { |
|
d.options[d.currentOption].selected(d.currentOption, d.options[d.currentOption]) |
|
} |
|
}) |
|
d.list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { |
|
if event.Key() == tcell.KeyRune { |
|
d.prefix += string(event.Rune()) |
|
d.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]) |
|
} |
|
d.evalPrefix() |
|
} else if event.Key() == tcell.KeyEscape { |
|
d.currentOption = optionBefore |
|
d.list.SetCurrentItem(d.currentOption) |
|
d.closeList(setFocus) |
|
if d.selected != nil { |
|
if d.currentOption > -1 { |
|
d.selected(d.currentOption, d.options[d.currentOption]) |
|
} |
|
} |
|
} else { |
|
d.prefix = "" |
|
} |
|
|
|
return event |
|
}) |
|
|
|
setFocus(d.list) |
|
} |
|
|
|
// closeList closes the embedded List element by hiding it and removing focus |
|
// from it. |
|
func (d *DropDown) closeList(setFocus func(Primitive)) { |
|
d.open = false |
|
if d.list.HasFocus() { |
|
setFocus(d) |
|
} |
|
} |
|
|
|
// Focus is called by the application when the primitive receives focus. |
|
func (d *DropDown) Focus(delegate func(p Primitive)) { |
|
d.Box.Focus(delegate) |
|
if d.open { |
|
delegate(d.list) |
|
} |
|
} |
|
|
|
// HasFocus returns whether or not this primitive has focus. |
|
func (d *DropDown) HasFocus() bool { |
|
d.RLock() |
|
defer d.RUnlock() |
|
|
|
return d._hasFocus() |
|
} |
|
|
|
func (d *DropDown) _hasFocus() bool { |
|
if d.open { |
|
return d.list.HasFocus() |
|
} |
|
return d.hasFocus |
|
} |
|
|
|
// MouseHandler returns the mouse handler for this primitive. |
|
func (d *DropDown) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { |
|
return d.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { |
|
// Was the mouse event in the drop-down box itself (or on its label)? |
|
x, y := event.Position() |
|
_, rectY, _, _ := d.GetInnerRect() |
|
inRect := y == rectY |
|
if !d.open && !inRect { |
|
return d.InRect(x, y), nil // No, and it's not expanded either. Ignore. |
|
} |
|
|
|
// Handle dragging. Clicks are implicitly handled by this logic. |
|
switch action { |
|
case MouseLeftDown: |
|
consumed = d.open || inRect |
|
capture = d |
|
if !d.open { |
|
d.openList(setFocus) |
|
d.dragging = true |
|
} else if consumed, _ := d.list.MouseHandler()(MouseLeftClick, event, setFocus); !consumed { |
|
d.closeList(setFocus) // Close drop-down if clicked outside of it. |
|
} |
|
case MouseMove: |
|
if d.dragging { |
|
// We pretend it's a left click so we can see the selection during |
|
// dragging. Because we don't act upon it, it's not a problem. |
|
d.list.MouseHandler()(MouseLeftClick, event, setFocus) |
|
consumed = true |
|
capture = d |
|
} |
|
case MouseLeftUp: |
|
if d.dragging { |
|
d.dragging = false |
|
d.list.MouseHandler()(MouseLeftClick, event, setFocus) |
|
consumed = true |
|
} |
|
} |
|
|
|
return |
|
}) |
|
}
|
|
|