2017-12-17 05:48:26 +08:00
|
|
|
package tview
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2019-01-13 04:22:58 +08:00
|
|
|
"strings"
|
2017-12-17 05:48:26 +08:00
|
|
|
|
2020-10-18 20:15:57 +08:00
|
|
|
"github.com/gdamore/tcell/v2"
|
2017-12-17 05:48:26 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
// listItem represents one item in a List.
|
|
|
|
type listItem struct {
|
|
|
|
MainText string // The main text of the list item.
|
|
|
|
SecondaryText string // 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.
|
2017-12-19 03:04:52 +08:00
|
|
|
Selected func() // The optional function which is called when the item is selected.
|
2017-12-17 05:48:26 +08:00
|
|
|
}
|
|
|
|
|
2023-03-26 04:22:23 +08:00
|
|
|
// List displays rows of items, each of which can be selected. List items can be
|
|
|
|
// shown as a single line or as two lines. They can be selected by pressing
|
|
|
|
// their assigned shortcut key, navigating to them and pressing Enter, or
|
|
|
|
// clicking on them with the mouse. The following key binds are available:
|
|
|
|
//
|
|
|
|
// - Down arrow / tab: Move down one item.
|
|
|
|
// - Up arrow / backtab: Move up one item.
|
|
|
|
// - Home: Move to the first item.
|
|
|
|
// - End: Move to the last item.
|
|
|
|
// - Page down: Move down one page.
|
|
|
|
// - Page up: Move up one page.
|
|
|
|
// - Enter / Space: Select the current item.
|
|
|
|
// - Right / left: Scroll horizontally. Only if the list is wider than the
|
|
|
|
// available space.
|
|
|
|
//
|
|
|
|
// See [List.SetChangedFunc] for a way to be notified when the user navigates
|
|
|
|
// to a list item. See [List.SetSelectedFunc] for a way to be notified when a
|
|
|
|
// list item was selected.
|
2018-01-07 23:39:06 +08:00
|
|
|
//
|
|
|
|
// See https://github.com/rivo/tview/wiki/List for an example.
|
2017-12-17 05:48:26 +08:00
|
|
|
type List struct {
|
|
|
|
*Box
|
|
|
|
|
|
|
|
// The items of the list.
|
|
|
|
items []*listItem
|
|
|
|
|
|
|
|
// The index of the currently selected item.
|
|
|
|
currentItem int
|
|
|
|
|
|
|
|
// Whether or not to show the secondary item texts.
|
|
|
|
showSecondaryText bool
|
|
|
|
|
2022-02-16 00:59:36 +08:00
|
|
|
// The item main text style.
|
|
|
|
mainTextStyle tcell.Style
|
2017-12-17 05:48:26 +08:00
|
|
|
|
2022-02-16 00:59:36 +08:00
|
|
|
// The item secondary text style.
|
|
|
|
secondaryTextStyle tcell.Style
|
2017-12-17 05:48:26 +08:00
|
|
|
|
2022-02-16 00:59:36 +08:00
|
|
|
// The item shortcut text style.
|
|
|
|
shortcutStyle tcell.Style
|
2017-12-17 05:48:26 +08:00
|
|
|
|
2022-02-16 00:59:36 +08:00
|
|
|
// The style for selected items.
|
|
|
|
selectedStyle tcell.Style
|
2017-12-19 03:04:52 +08:00
|
|
|
|
2018-11-26 18:00:48 +08:00
|
|
|
// If true, the selection is only shown when the list has focus.
|
|
|
|
selectedFocusOnly bool
|
|
|
|
|
2019-02-21 00:58:59 +08:00
|
|
|
// If true, the entire row is highlighted when selected.
|
|
|
|
highlightFullLine bool
|
2019-01-03 14:51:11 +08:00
|
|
|
|
2019-12-30 00:47:05 +08:00
|
|
|
// Whether or not navigating the list will wrap around.
|
|
|
|
wrapAround bool
|
|
|
|
|
2021-02-16 01:26:27 +08:00
|
|
|
// The number of list items skipped at the top before the first item is
|
|
|
|
// drawn.
|
|
|
|
itemOffset int
|
|
|
|
|
|
|
|
// The number of cells skipped on the left side of an item text. Shortcuts
|
|
|
|
// are not affected.
|
|
|
|
horizontalOffset int
|
|
|
|
|
|
|
|
// Set to true if a currently visible item flows over the right border of
|
|
|
|
// the box. This is set by the Draw() function. It determines the behaviour
|
|
|
|
// of the right arrow key.
|
|
|
|
overflowing bool
|
2019-01-24 04:40:01 +08:00
|
|
|
|
2023-03-26 04:22:23 +08:00
|
|
|
// An optional function which is called when the user has navigated to a
|
|
|
|
// list item.
|
2018-01-02 00:17:20 +08:00
|
|
|
changed func(index int, mainText, secondaryText string, shortcut rune)
|
|
|
|
|
2017-12-19 03:04:52 +08:00
|
|
|
// An optional function which is called when a list item was selected. This
|
|
|
|
// function will be called even if the list item defines its own callback.
|
2017-12-17 05:48:26 +08:00
|
|
|
selected func(index int, mainText, secondaryText string, shortcut rune)
|
2018-01-02 00:17:20 +08:00
|
|
|
|
|
|
|
// An optional function which is called when the user presses the Escape key.
|
|
|
|
done func()
|
2017-12-17 05:48:26 +08:00
|
|
|
}
|
|
|
|
|
2022-12-27 04:55:31 +08:00
|
|
|
// NewList returns a new list.
|
2017-12-17 05:48:26 +08:00
|
|
|
func NewList() *List {
|
|
|
|
return &List{
|
2022-02-16 00:59:36 +08:00
|
|
|
Box: NewBox(),
|
|
|
|
showSecondaryText: true,
|
|
|
|
wrapAround: true,
|
|
|
|
mainTextStyle: tcell.StyleDefault.Foreground(Styles.PrimaryTextColor),
|
|
|
|
secondaryTextStyle: tcell.StyleDefault.Foreground(Styles.TertiaryTextColor),
|
|
|
|
shortcutStyle: tcell.StyleDefault.Foreground(Styles.SecondaryTextColor),
|
|
|
|
selectedStyle: tcell.StyleDefault.Foreground(Styles.PrimitiveBackgroundColor).Background(Styles.PrimaryTextColor),
|
2017-12-17 05:48:26 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-13 04:22:58 +08:00
|
|
|
// SetCurrentItem sets the currently selected item by its index, starting at 0
|
|
|
|
// for the first item. If a negative index is provided, items are referred to
|
|
|
|
// from the back (-1 = last item, -2 = second-to-last item, and so on). Out of
|
|
|
|
// range indices are clamped to the beginning/end.
|
|
|
|
//
|
|
|
|
// Calling this function triggers a "changed" event if the selection changes.
|
2017-12-19 03:04:52 +08:00
|
|
|
func (l *List) SetCurrentItem(index int) *List {
|
2019-01-13 04:22:58 +08:00
|
|
|
if index < 0 {
|
|
|
|
index = len(l.items) + index
|
|
|
|
}
|
|
|
|
if index >= len(l.items) {
|
|
|
|
index = len(l.items) - 1
|
|
|
|
}
|
|
|
|
if index < 0 {
|
|
|
|
index = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
if index != l.currentItem && l.changed != nil {
|
2019-07-11 18:37:27 +08:00
|
|
|
item := l.items[index]
|
|
|
|
l.changed(index, item.MainText, item.SecondaryText, item.Shortcut)
|
2018-01-02 00:17:20 +08:00
|
|
|
}
|
2019-01-13 04:22:58 +08:00
|
|
|
|
2019-07-11 18:37:27 +08:00
|
|
|
l.currentItem = index
|
|
|
|
|
2023-01-01 22:08:11 +08:00
|
|
|
l.adjustOffset()
|
|
|
|
|
2017-12-19 03:04:52 +08:00
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
2019-01-13 04:22:58 +08:00
|
|
|
// GetCurrentItem returns the index of the currently selected list item,
|
|
|
|
// starting at 0 for the first item.
|
2018-03-19 03:42:51 +08:00
|
|
|
func (l *List) GetCurrentItem() int {
|
|
|
|
return l.currentItem
|
|
|
|
}
|
|
|
|
|
2021-02-16 01:26:27 +08:00
|
|
|
// SetOffset sets the number of items to be skipped (vertically) as well as the
|
|
|
|
// number of cells skipped horizontally when the list is drawn. Note that one
|
|
|
|
// item corresponds to two rows when there are secondary texts. Shortcuts are
|
|
|
|
// always drawn.
|
|
|
|
//
|
|
|
|
// These values may change when the list is drawn to ensure the currently
|
|
|
|
// selected item is visible and item texts move out of view. Users can also
|
|
|
|
// modify these values by interacting with the list.
|
|
|
|
func (l *List) SetOffset(items, horizontal int) *List {
|
|
|
|
l.itemOffset = items
|
|
|
|
l.horizontalOffset = horizontal
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetOffset returns the number of items skipped while drawing, as well as the
|
|
|
|
// number of cells item text is moved to the left. See also SetOffset() for more
|
|
|
|
// information on these values.
|
|
|
|
func (l *List) GetOffset() (int, int) {
|
|
|
|
return l.itemOffset, l.horizontalOffset
|
|
|
|
}
|
|
|
|
|
2018-09-05 18:57:35 +08:00
|
|
|
// RemoveItem removes the item with the given index (starting at 0) from the
|
2019-01-13 04:22:58 +08:00
|
|
|
// list. If a negative index is provided, items are referred to from the back
|
|
|
|
// (-1 = last item, -2 = second-to-last item, and so on). Out of range indices
|
|
|
|
// are clamped to the beginning/end, i.e. unless the list is empty, an item is
|
|
|
|
// always removed.
|
|
|
|
//
|
|
|
|
// The currently selected item is shifted accordingly. If it is the one that is
|
2022-11-28 05:21:15 +08:00
|
|
|
// removed, a "changed" event is fired, unless no items are left.
|
2018-09-05 18:57:35 +08:00
|
|
|
func (l *List) RemoveItem(index int) *List {
|
2019-01-13 04:22:58 +08:00
|
|
|
if len(l.items) == 0 {
|
2018-09-05 18:57:35 +08:00
|
|
|
return l
|
|
|
|
}
|
2019-01-13 04:22:58 +08:00
|
|
|
|
|
|
|
// Adjust index.
|
|
|
|
if index < 0 {
|
|
|
|
index = len(l.items) + index
|
|
|
|
}
|
|
|
|
if index >= len(l.items) {
|
|
|
|
index = len(l.items) - 1
|
|
|
|
}
|
|
|
|
if index < 0 {
|
|
|
|
index = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove item.
|
2018-09-05 18:57:35 +08:00
|
|
|
l.items = append(l.items[:index], l.items[index+1:]...)
|
2019-01-13 04:22:58 +08:00
|
|
|
|
|
|
|
// If there is nothing left, we're done.
|
|
|
|
if len(l.items) == 0 {
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
|
|
|
// Shift current item.
|
|
|
|
previousCurrentItem := l.currentItem
|
2022-11-28 05:21:15 +08:00
|
|
|
if l.currentItem > index || l.currentItem == len(l.items) {
|
2019-01-13 04:22:58 +08:00
|
|
|
l.currentItem--
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fire "changed" event for removed items.
|
|
|
|
if previousCurrentItem == index && l.changed != nil {
|
|
|
|
item := l.items[l.currentItem]
|
|
|
|
l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
|
2018-09-05 18:57:35 +08:00
|
|
|
}
|
2019-01-13 04:22:58 +08:00
|
|
|
|
2018-09-05 18:57:35 +08:00
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
2017-12-19 03:04:52 +08:00
|
|
|
// SetMainTextColor sets the color of the items' main text.
|
|
|
|
func (l *List) SetMainTextColor(color tcell.Color) *List {
|
2022-02-16 00:59:36 +08:00
|
|
|
l.mainTextStyle = l.mainTextStyle.Foreground(color)
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetMainTextStyle sets the style of the items' main text. Note that the
|
|
|
|
// background color is ignored in order not to override the background color of
|
|
|
|
// the list itself.
|
|
|
|
func (l *List) SetMainTextStyle(style tcell.Style) *List {
|
|
|
|
l.mainTextStyle = style
|
2017-12-17 05:48:26 +08:00
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
2017-12-19 03:04:52 +08:00
|
|
|
// SetSecondaryTextColor sets the color of the items' secondary text.
|
|
|
|
func (l *List) SetSecondaryTextColor(color tcell.Color) *List {
|
2022-02-16 00:59:36 +08:00
|
|
|
l.secondaryTextStyle = l.secondaryTextStyle.Foreground(color)
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetSecondaryTextStyle sets the style of the items' secondary text. Note that
|
|
|
|
// the background color is ignored in order not to override the background color
|
|
|
|
// of the list itself.
|
|
|
|
func (l *List) SetSecondaryTextStyle(style tcell.Style) *List {
|
|
|
|
l.secondaryTextStyle = style
|
2017-12-17 05:48:26 +08:00
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetShortcutColor sets the color of the items' shortcut.
|
|
|
|
func (l *List) SetShortcutColor(color tcell.Color) *List {
|
2022-02-16 00:59:36 +08:00
|
|
|
l.shortcutStyle = l.shortcutStyle.Foreground(color)
|
2017-12-17 05:48:26 +08:00
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
2022-02-16 00:59:36 +08:00
|
|
|
// SetShortcutStyle sets the style of the items' shortcut. Note that the
|
|
|
|
// background color is ignored in order not to override the background color of
|
|
|
|
// the list itself.
|
|
|
|
func (l *List) SetShortcutStyle(style tcell.Style) *List {
|
|
|
|
l.shortcutStyle = style
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetSelectedTextColor sets the text color of selected items. Note that the
|
|
|
|
// color of main text characters that are different from the main text color
|
|
|
|
// (e.g. color tags) is maintained.
|
2017-12-19 03:04:52 +08:00
|
|
|
func (l *List) SetSelectedTextColor(color tcell.Color) *List {
|
2022-02-16 00:59:36 +08:00
|
|
|
l.selectedStyle = l.selectedStyle.Foreground(color)
|
2017-12-19 03:04:52 +08:00
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetSelectedBackgroundColor sets the background color of selected items.
|
|
|
|
func (l *List) SetSelectedBackgroundColor(color tcell.Color) *List {
|
2022-02-16 00:59:36 +08:00
|
|
|
l.selectedStyle = l.selectedStyle.Background(color)
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetSelectedStyle sets the style of the selected items. Note that the color of
|
|
|
|
// main text characters that are different from the main text color (e.g. color
|
|
|
|
// tags) is maintained.
|
|
|
|
func (l *List) SetSelectedStyle(style tcell.Style) *List {
|
|
|
|
l.selectedStyle = style
|
2017-12-19 03:04:52 +08:00
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
2018-11-26 18:00:48 +08:00
|
|
|
// SetSelectedFocusOnly sets a flag which determines when the currently selected
|
|
|
|
// list item is highlighted. If set to true, selected items are only highlighted
|
|
|
|
// when the list has focus. If set to false, they are always highlighted.
|
|
|
|
func (l *List) SetSelectedFocusOnly(focusOnly bool) *List {
|
|
|
|
l.selectedFocusOnly = focusOnly
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
2019-02-21 00:58:59 +08:00
|
|
|
// SetHighlightFullLine sets a flag which determines whether the colored
|
|
|
|
// background of selected items spans the entire width of the view. If set to
|
|
|
|
// true, the highlight spans the entire view. If set to false, only the text of
|
|
|
|
// the selected item from beginning to end is highlighted.
|
|
|
|
func (l *List) SetHighlightFullLine(highlight bool) *List {
|
|
|
|
l.highlightFullLine = highlight
|
2019-01-03 14:51:11 +08:00
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
2017-12-17 05:48:26 +08:00
|
|
|
// ShowSecondaryText determines whether or not to show secondary item texts.
|
|
|
|
func (l *List) ShowSecondaryText(show bool) *List {
|
|
|
|
l.showSecondaryText = show
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
2019-12-30 00:47:05 +08:00
|
|
|
// SetWrapAround sets the flag that determines whether navigating the list will
|
|
|
|
// wrap around. That is, navigating downwards on the last item will move the
|
|
|
|
// selection to the first item (similarly in the other direction). If set to
|
|
|
|
// false, the selection won't change when navigating downwards on the last item
|
|
|
|
// or navigating upwards on the first item.
|
|
|
|
func (l *List) SetWrapAround(wrapAround bool) *List {
|
|
|
|
l.wrapAround = wrapAround
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
2018-01-02 00:17:20 +08:00
|
|
|
// SetChangedFunc sets the function which is called when the user navigates to
|
|
|
|
// a list item. The function receives the item's index in the list of items
|
|
|
|
// (starting with 0), its main text, secondary text, and its shortcut rune.
|
|
|
|
//
|
|
|
|
// This function is also called when the first item is added or when
|
|
|
|
// SetCurrentItem() is called.
|
2018-09-05 18:57:35 +08:00
|
|
|
func (l *List) SetChangedFunc(handler func(index int, mainText string, secondaryText string, shortcut rune)) *List {
|
2018-01-02 00:17:20 +08:00
|
|
|
l.changed = handler
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
2017-12-17 05:48:26 +08:00
|
|
|
// SetSelectedFunc sets the function which is called when the user selects a
|
2017-12-30 05:27:10 +08:00
|
|
|
// list item by pressing Enter on the current selection. The function receives
|
|
|
|
// the item's index in the list of items (starting with 0), its main text,
|
|
|
|
// secondary text, and its shortcut rune.
|
2017-12-17 05:48:26 +08:00
|
|
|
func (l *List) SetSelectedFunc(handler func(int, string, string, rune)) *List {
|
|
|
|
l.selected = handler
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
2018-01-02 00:17:20 +08:00
|
|
|
// SetDoneFunc sets a function which is called when the user presses the Escape
|
|
|
|
// key.
|
|
|
|
func (l *List) SetDoneFunc(handler func()) *List {
|
|
|
|
l.done = handler
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
2019-01-13 04:22:58 +08:00
|
|
|
// AddItem calls InsertItem() with an index of -1.
|
|
|
|
func (l *List) AddItem(mainText, secondaryText string, shortcut rune, selected func()) *List {
|
|
|
|
l.InsertItem(-1, mainText, secondaryText, shortcut, selected)
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
|
|
|
// InsertItem adds a new item to the list at the specified index. An index of 0
|
|
|
|
// will insert the item at the beginning, an index of 1 before the second item,
|
|
|
|
// and so on. An index of GetItemCount() or higher will insert the item at the
|
|
|
|
// end of the list. Negative indices are also allowed: An index of -1 will
|
|
|
|
// insert the item at the end of the list, an index of -2 before the last item,
|
|
|
|
// and so on. An index of -GetItemCount()-1 or lower will insert the item at the
|
|
|
|
// beginning.
|
|
|
|
//
|
|
|
|
// An item has a main text which will be highlighted when selected. It also has
|
|
|
|
// a secondary text which is shown underneath the main text (if it is set to
|
|
|
|
// visible) but which may remain empty.
|
2017-12-17 05:48:26 +08:00
|
|
|
//
|
|
|
|
// The shortcut is a key binding. If the specified rune is entered, the item
|
|
|
|
// is selected immediately. Set to 0 for no binding.
|
2017-12-19 03:04:52 +08:00
|
|
|
//
|
|
|
|
// The "selected" callback will be invoked when the user selects the item. You
|
2019-01-13 04:22:58 +08:00
|
|
|
// may provide nil if no such callback is needed or if all events are handled
|
2017-12-19 03:04:52 +08:00
|
|
|
// through the selected callback set with SetSelectedFunc().
|
2019-01-13 04:22:58 +08:00
|
|
|
//
|
|
|
|
// The currently selected item will shift its position accordingly. If the list
|
|
|
|
// was previously empty, a "changed" event is fired because the new item becomes
|
|
|
|
// selected.
|
|
|
|
func (l *List) InsertItem(index int, mainText, secondaryText string, shortcut rune, selected func()) *List {
|
|
|
|
item := &listItem{
|
2017-12-17 05:48:26 +08:00
|
|
|
MainText: mainText,
|
|
|
|
SecondaryText: secondaryText,
|
|
|
|
Shortcut: shortcut,
|
2017-12-19 03:04:52 +08:00
|
|
|
Selected: selected,
|
2019-01-13 04:22:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Shift index to range.
|
|
|
|
if index < 0 {
|
|
|
|
index = len(l.items) + index + 1
|
|
|
|
}
|
|
|
|
if index < 0 {
|
|
|
|
index = 0
|
|
|
|
} else if index > len(l.items) {
|
|
|
|
index = len(l.items)
|
|
|
|
}
|
|
|
|
|
2019-01-24 04:40:01 +08:00
|
|
|
// Shift current item.
|
|
|
|
if l.currentItem < len(l.items) && l.currentItem >= index {
|
|
|
|
l.currentItem++
|
|
|
|
}
|
|
|
|
|
2019-01-13 04:22:58 +08:00
|
|
|
// Insert item (make space for the new item, then shift and insert).
|
|
|
|
l.items = append(l.items, nil)
|
|
|
|
if index < len(l.items)-1 { // -1 because l.items has already grown by one item.
|
|
|
|
copy(l.items[index+1:], l.items[index:])
|
|
|
|
}
|
|
|
|
l.items[index] = item
|
|
|
|
|
|
|
|
// Fire a "change" event for the first item in the list.
|
2018-01-02 00:17:20 +08:00
|
|
|
if len(l.items) == 1 && l.changed != nil {
|
|
|
|
item := l.items[0]
|
|
|
|
l.changed(0, item.MainText, item.SecondaryText, item.Shortcut)
|
|
|
|
}
|
2019-01-13 04:22:58 +08:00
|
|
|
|
2017-12-17 05:48:26 +08:00
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
2018-04-20 03:17:13 +08:00
|
|
|
// GetItemCount returns the number of items in the list.
|
|
|
|
func (l *List) GetItemCount() int {
|
|
|
|
return len(l.items)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetItemText returns an item's texts (main and secondary). Panics if the index
|
|
|
|
// is out of range.
|
|
|
|
func (l *List) GetItemText(index int) (main, secondary string) {
|
|
|
|
return l.items[index].MainText, l.items[index].SecondaryText
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetItemText sets an item's main and secondary text. Panics if the index is
|
|
|
|
// out of range.
|
|
|
|
func (l *List) SetItemText(index int, main, secondary string) *List {
|
|
|
|
item := l.items[index]
|
|
|
|
item.MainText = main
|
|
|
|
item.SecondaryText = secondary
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
2019-01-13 04:22:58 +08:00
|
|
|
// FindItems searches the main and secondary texts for the given strings and
|
|
|
|
// returns a list of item indices in which those strings are found. One of the
|
|
|
|
// two search strings may be empty, it will then be ignored. Indices are always
|
|
|
|
// returned in ascending order.
|
|
|
|
//
|
|
|
|
// If mustContainBoth is set to true, mainSearch must be contained in the main
|
|
|
|
// text AND secondarySearch must be contained in the secondary text. If it is
|
|
|
|
// false, only one of the two search strings must be contained.
|
|
|
|
//
|
|
|
|
// Set ignoreCase to true for case-insensitive search.
|
|
|
|
func (l *List) FindItems(mainSearch, secondarySearch string, mustContainBoth, ignoreCase bool) (indices []int) {
|
|
|
|
if mainSearch == "" && secondarySearch == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if ignoreCase {
|
|
|
|
mainSearch = strings.ToLower(mainSearch)
|
|
|
|
secondarySearch = strings.ToLower(secondarySearch)
|
|
|
|
}
|
|
|
|
|
|
|
|
for index, item := range l.items {
|
|
|
|
mainText := item.MainText
|
|
|
|
secondaryText := item.SecondaryText
|
|
|
|
if ignoreCase {
|
|
|
|
mainText = strings.ToLower(mainText)
|
|
|
|
secondaryText = strings.ToLower(secondaryText)
|
|
|
|
}
|
|
|
|
|
|
|
|
// strings.Contains() always returns true for a "" search.
|
|
|
|
mainContained := strings.Contains(mainText, mainSearch)
|
|
|
|
secondaryContained := strings.Contains(secondaryText, secondarySearch)
|
|
|
|
if mustContainBoth && mainContained && secondaryContained ||
|
|
|
|
!mustContainBoth && (mainText != "" && mainContained || secondaryText != "" && secondaryContained) {
|
|
|
|
indices = append(indices, index)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-02 00:17:20 +08:00
|
|
|
// Clear removes all items from the list.
|
|
|
|
func (l *List) Clear() *List {
|
2017-12-19 03:04:52 +08:00
|
|
|
l.items = nil
|
|
|
|
l.currentItem = 0
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
2017-12-17 05:48:26 +08:00
|
|
|
// Draw draws this primitive onto the screen.
|
|
|
|
func (l *List) Draw(screen tcell.Screen) {
|
2020-11-18 02:33:25 +08:00
|
|
|
l.Box.DrawForSubclass(screen, l)
|
2017-12-17 05:48:26 +08:00
|
|
|
|
|
|
|
// Determine the dimensions.
|
2017-12-22 01:08:53 +08:00
|
|
|
x, y, width, height := l.GetInnerRect()
|
|
|
|
bottomLimit := y + height
|
2020-02-20 01:31:32 +08:00
|
|
|
_, totalHeight := screen.Size()
|
|
|
|
if bottomLimit > totalHeight {
|
|
|
|
bottomLimit = totalHeight
|
|
|
|
}
|
2017-12-17 05:48:26 +08:00
|
|
|
|
|
|
|
// Do we show any shortcuts?
|
|
|
|
var showShortcuts bool
|
|
|
|
for _, item := range l.items {
|
|
|
|
if item.Shortcut != 0 {
|
|
|
|
showShortcuts = true
|
|
|
|
x += 4
|
|
|
|
width -= 4
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-16 01:26:27 +08:00
|
|
|
if l.horizontalOffset < 0 {
|
|
|
|
l.horizontalOffset = 0
|
|
|
|
}
|
2018-03-11 16:51:15 +08:00
|
|
|
|
2017-12-17 05:48:26 +08:00
|
|
|
// Draw the list items.
|
2021-02-16 01:26:27 +08:00
|
|
|
var (
|
|
|
|
maxWidth int // The maximum printed item width.
|
|
|
|
overflowing bool // Whether a text's end exceeds the right border.
|
|
|
|
)
|
2017-12-17 05:48:26 +08:00
|
|
|
for index, item := range l.items {
|
2021-02-16 01:26:27 +08:00
|
|
|
if index < l.itemOffset {
|
2018-03-11 16:51:15 +08:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2017-12-17 05:48:26 +08:00
|
|
|
if y >= bottomLimit {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
// Shortcuts.
|
|
|
|
if showShortcuts && item.Shortcut != 0 {
|
2022-02-16 00:59:36 +08:00
|
|
|
printWithStyle(screen, fmt.Sprintf("(%s)", string(item.Shortcut)), x-5, y, 0, 4, AlignRight, l.shortcutStyle, true)
|
2017-12-17 05:48:26 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Main text.
|
2022-02-16 00:59:36 +08:00
|
|
|
_, printedWidth, _, end := printWithStyle(screen, item.MainText, x, y, l.horizontalOffset, width, AlignLeft, l.mainTextStyle, true)
|
2021-02-16 01:26:27 +08:00
|
|
|
if printedWidth > maxWidth {
|
|
|
|
maxWidth = printedWidth
|
|
|
|
}
|
|
|
|
if end < len(item.MainText) {
|
|
|
|
overflowing = true
|
|
|
|
}
|
2018-01-18 00:13:36 +08:00
|
|
|
|
|
|
|
// Background color of selected text.
|
2018-11-26 18:00:48 +08:00
|
|
|
if index == l.currentItem && (!l.selectedFocusOnly || l.HasFocus()) {
|
2019-03-09 03:13:09 +08:00
|
|
|
textWidth := width
|
2019-02-21 00:58:59 +08:00
|
|
|
if !l.highlightFullLine {
|
2019-03-19 19:12:40 +08:00
|
|
|
if w := TaggedStringWidth(item.MainText); w < textWidth {
|
2019-01-03 14:51:11 +08:00
|
|
|
textWidth = w
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-16 00:59:36 +08:00
|
|
|
mainTextColor, _, _ := l.mainTextStyle.Decompose()
|
2019-01-03 14:51:11 +08:00
|
|
|
for bx := 0; bx < textWidth; bx++ {
|
2018-01-18 00:13:36 +08:00
|
|
|
m, c, style, _ := screen.GetContent(x+bx, y)
|
|
|
|
fg, _, _ := style.Decompose()
|
2022-02-16 00:59:36 +08:00
|
|
|
style = l.selectedStyle
|
|
|
|
if fg != mainTextColor {
|
|
|
|
style = style.Foreground(fg)
|
2018-01-18 00:13:36 +08:00
|
|
|
}
|
|
|
|
screen.SetContent(x+bx, y, m, c, style)
|
2017-12-17 05:48:26 +08:00
|
|
|
}
|
|
|
|
}
|
2018-01-18 00:13:36 +08:00
|
|
|
|
2017-12-17 05:48:26 +08:00
|
|
|
y++
|
|
|
|
|
|
|
|
if y >= bottomLimit {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
// Secondary text.
|
|
|
|
if l.showSecondaryText {
|
2022-02-16 00:59:36 +08:00
|
|
|
_, printedWidth, _, end := printWithStyle(screen, item.SecondaryText, x, y, l.horizontalOffset, width, AlignLeft, l.secondaryTextStyle, true)
|
2021-02-16 01:26:27 +08:00
|
|
|
if printedWidth > maxWidth {
|
|
|
|
maxWidth = printedWidth
|
|
|
|
}
|
|
|
|
if end < len(item.SecondaryText) {
|
|
|
|
overflowing = true
|
|
|
|
}
|
2017-12-17 05:48:26 +08:00
|
|
|
y++
|
|
|
|
}
|
|
|
|
}
|
2021-02-16 01:26:27 +08:00
|
|
|
|
|
|
|
// We don't want the item text to get out of view. If the horizontal offset
|
|
|
|
// is too high, we reset it and redraw. (That should be about as efficient
|
|
|
|
// as calculating everything up front.)
|
|
|
|
if l.horizontalOffset > 0 && maxWidth < width {
|
|
|
|
l.horizontalOffset -= width - maxWidth
|
|
|
|
l.Draw(screen)
|
|
|
|
}
|
|
|
|
l.overflowing = overflowing
|
2017-12-17 05:48:26 +08:00
|
|
|
}
|
|
|
|
|
2022-12-30 02:07:33 +08:00
|
|
|
// adjustOffset adjusts the vertical offset to keep the current selection in
|
|
|
|
// view.
|
|
|
|
func (l *List) adjustOffset() {
|
|
|
|
_, _, _, height := l.GetInnerRect()
|
2023-01-04 23:33:04 +08:00
|
|
|
if height == 0 {
|
|
|
|
return
|
|
|
|
}
|
2022-12-30 02:07:33 +08:00
|
|
|
if l.currentItem < l.itemOffset {
|
|
|
|
l.itemOffset = l.currentItem
|
|
|
|
} else if l.showSecondaryText {
|
|
|
|
if 2*(l.currentItem-l.itemOffset) >= height-1 {
|
|
|
|
l.itemOffset = (2*l.currentItem + 3 - height) / 2
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if l.currentItem-l.itemOffset >= height {
|
|
|
|
l.itemOffset = l.currentItem + 1 - height
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-17 05:48:26 +08:00
|
|
|
// InputHandler returns the handler for this primitive.
|
2017-12-19 03:04:52 +08:00
|
|
|
func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
|
2018-03-20 04:25:30 +08:00
|
|
|
return l.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
|
2020-02-18 00:27:42 +08:00
|
|
|
if event.Key() == tcell.KeyEscape {
|
|
|
|
if l.done != nil {
|
|
|
|
l.done()
|
|
|
|
}
|
|
|
|
return
|
|
|
|
} else if len(l.items) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-02 00:17:20 +08:00
|
|
|
previousItem := l.currentItem
|
|
|
|
|
2017-12-17 05:48:26 +08:00
|
|
|
switch key := event.Key(); key {
|
2021-02-16 01:26:27 +08:00
|
|
|
case tcell.KeyTab, tcell.KeyDown:
|
2017-12-17 05:48:26 +08:00
|
|
|
l.currentItem++
|
2021-02-16 01:26:27 +08:00
|
|
|
case tcell.KeyBacktab, tcell.KeyUp:
|
2017-12-17 05:48:26 +08:00
|
|
|
l.currentItem--
|
2021-02-16 01:26:27 +08:00
|
|
|
case tcell.KeyRight:
|
|
|
|
if l.overflowing {
|
|
|
|
l.horizontalOffset += 2 // We shift by 2 to account for two-cell characters.
|
|
|
|
} else {
|
|
|
|
l.currentItem++
|
|
|
|
}
|
|
|
|
case tcell.KeyLeft:
|
|
|
|
if l.horizontalOffset > 0 {
|
|
|
|
l.horizontalOffset -= 2
|
|
|
|
} else {
|
|
|
|
l.currentItem--
|
|
|
|
}
|
2017-12-17 05:48:26 +08:00
|
|
|
case tcell.KeyHome:
|
|
|
|
l.currentItem = 0
|
|
|
|
case tcell.KeyEnd:
|
|
|
|
l.currentItem = len(l.items) - 1
|
|
|
|
case tcell.KeyPgDn:
|
2020-03-12 03:22:05 +08:00
|
|
|
_, _, _, height := l.GetInnerRect()
|
|
|
|
l.currentItem += height
|
2021-03-16 02:25:18 +08:00
|
|
|
if l.currentItem >= len(l.items) {
|
|
|
|
l.currentItem = len(l.items) - 1
|
|
|
|
}
|
2017-12-17 05:48:26 +08:00
|
|
|
case tcell.KeyPgUp:
|
2020-03-12 03:22:05 +08:00
|
|
|
_, _, _, height := l.GetInnerRect()
|
|
|
|
l.currentItem -= height
|
2021-03-16 02:25:18 +08:00
|
|
|
if l.currentItem < 0 {
|
|
|
|
l.currentItem = 0
|
|
|
|
}
|
2017-12-17 05:48:26 +08:00
|
|
|
case tcell.KeyEnter:
|
2018-05-10 06:13:40 +08:00
|
|
|
if l.currentItem >= 0 && l.currentItem < len(l.items) {
|
|
|
|
item := l.items[l.currentItem]
|
|
|
|
if item.Selected != nil {
|
|
|
|
item.Selected()
|
|
|
|
}
|
|
|
|
if l.selected != nil {
|
|
|
|
l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
|
|
|
|
}
|
2017-12-17 05:48:26 +08:00
|
|
|
}
|
|
|
|
case tcell.KeyRune:
|
|
|
|
ch := event.Rune()
|
|
|
|
if ch != ' ' {
|
|
|
|
// It's not a space bar. Is it a shortcut?
|
|
|
|
var found bool
|
|
|
|
for index, item := range l.items {
|
|
|
|
if item.Shortcut == ch {
|
|
|
|
// We have a shortcut.
|
|
|
|
found = true
|
|
|
|
l.currentItem = index
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !found {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2017-12-19 03:04:52 +08:00
|
|
|
item := l.items[l.currentItem]
|
|
|
|
if item.Selected != nil {
|
|
|
|
item.Selected()
|
|
|
|
}
|
2017-12-17 05:48:26 +08:00
|
|
|
if l.selected != nil {
|
|
|
|
l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if l.currentItem < 0 {
|
2019-12-30 00:47:05 +08:00
|
|
|
if l.wrapAround {
|
|
|
|
l.currentItem = len(l.items) - 1
|
|
|
|
} else {
|
|
|
|
l.currentItem = 0
|
|
|
|
}
|
2017-12-17 05:48:26 +08:00
|
|
|
} else if l.currentItem >= len(l.items) {
|
2019-12-30 00:47:05 +08:00
|
|
|
if l.wrapAround {
|
|
|
|
l.currentItem = 0
|
|
|
|
} else {
|
|
|
|
l.currentItem = len(l.items) - 1
|
|
|
|
}
|
2017-12-17 05:48:26 +08:00
|
|
|
}
|
2018-01-02 00:17:20 +08:00
|
|
|
|
2022-12-30 02:07:33 +08:00
|
|
|
if l.currentItem != previousItem && l.currentItem < len(l.items) {
|
|
|
|
if l.changed != nil {
|
|
|
|
item := l.items[l.currentItem]
|
|
|
|
l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
|
|
|
|
}
|
|
|
|
l.adjustOffset()
|
2018-01-02 00:17:20 +08:00
|
|
|
}
|
2018-01-15 04:29:34 +08:00
|
|
|
})
|
2017-12-17 05:48:26 +08:00
|
|
|
}
|
2019-11-04 14:30:25 +08:00
|
|
|
|
2020-03-28 01:41:44 +08:00
|
|
|
// indexAtPoint returns the index of the list item found at the given position
|
2020-03-30 03:36:06 +08:00
|
|
|
// or a negative value if there is no such list item.
|
2020-03-28 01:41:44 +08:00
|
|
|
func (l *List) indexAtPoint(x, y int) int {
|
|
|
|
rectX, rectY, width, height := l.GetInnerRect()
|
|
|
|
if rectX < 0 || rectX >= rectX+width || y < rectY || y >= rectY+height {
|
2019-11-04 14:30:25 +08:00
|
|
|
return -1
|
|
|
|
}
|
|
|
|
|
2020-03-28 01:41:44 +08:00
|
|
|
index := y - rectY
|
2019-11-04 14:30:25 +08:00
|
|
|
if l.showSecondaryText {
|
2020-03-28 01:41:44 +08:00
|
|
|
index /= 2
|
2019-11-04 14:30:25 +08:00
|
|
|
}
|
2021-02-16 01:26:27 +08:00
|
|
|
index += l.itemOffset
|
2019-11-04 14:30:25 +08:00
|
|
|
|
2020-03-28 01:41:44 +08:00
|
|
|
if index >= len(l.items) {
|
2019-11-04 14:30:25 +08:00
|
|
|
return -1
|
|
|
|
}
|
2020-03-28 01:41:44 +08:00
|
|
|
return index
|
2019-11-04 14:30:25 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// MouseHandler returns the mouse handler for this primitive.
|
2020-02-14 10:09:09 +08:00
|
|
|
func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
|
|
|
return l.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
2020-01-25 04:40:34 +08:00
|
|
|
if !l.InRect(event.Position()) {
|
2020-02-14 10:09:09 +08:00
|
|
|
return false, nil
|
2020-01-25 04:40:34 +08:00
|
|
|
}
|
2020-03-28 01:41:44 +08:00
|
|
|
|
2019-11-04 14:30:25 +08:00
|
|
|
// Process mouse event.
|
2020-03-30 03:36:06 +08:00
|
|
|
switch action {
|
|
|
|
case MouseLeftClick:
|
2022-12-30 02:07:33 +08:00
|
|
|
setFocus(l)
|
2020-03-28 01:41:44 +08:00
|
|
|
index := l.indexAtPoint(event.Position())
|
2019-11-04 14:30:25 +08:00
|
|
|
if index != -1 {
|
2019-11-04 14:55:58 +08:00
|
|
|
item := l.items[index]
|
|
|
|
if item.Selected != nil {
|
|
|
|
item.Selected()
|
|
|
|
}
|
|
|
|
if l.selected != nil {
|
|
|
|
l.selected(index, item.MainText, item.SecondaryText, item.Shortcut)
|
|
|
|
}
|
2022-12-30 02:07:33 +08:00
|
|
|
if index != l.currentItem {
|
|
|
|
if l.changed != nil {
|
|
|
|
l.changed(index, item.MainText, item.SecondaryText, item.Shortcut)
|
|
|
|
}
|
|
|
|
l.adjustOffset()
|
2019-11-04 14:55:58 +08:00
|
|
|
}
|
|
|
|
l.currentItem = index
|
2019-11-04 14:30:25 +08:00
|
|
|
}
|
2020-03-28 01:41:44 +08:00
|
|
|
consumed = true
|
2020-03-30 03:36:06 +08:00
|
|
|
case MouseScrollUp:
|
2021-02-16 01:26:27 +08:00
|
|
|
if l.itemOffset > 0 {
|
|
|
|
l.itemOffset--
|
2020-03-30 03:36:06 +08:00
|
|
|
}
|
|
|
|
consumed = true
|
|
|
|
case MouseScrollDown:
|
2021-02-16 01:26:27 +08:00
|
|
|
lines := len(l.items) - l.itemOffset
|
2020-03-30 03:36:06 +08:00
|
|
|
if l.showSecondaryText {
|
|
|
|
lines *= 2
|
|
|
|
}
|
|
|
|
if _, _, _, height := l.GetInnerRect(); lines > height {
|
2021-02-16 01:26:27 +08:00
|
|
|
l.itemOffset++
|
2020-03-30 03:36:06 +08:00
|
|
|
}
|
|
|
|
consumed = true
|
2019-11-04 14:30:25 +08:00
|
|
|
}
|
2020-03-28 01:41:44 +08:00
|
|
|
|
|
|
|
return
|
2019-11-04 14:30:25 +08:00
|
|
|
})
|
|
|
|
}
|