mirror of
https://github.com/rivo/tview.git
synced 2025-04-26 13:49:06 +08:00
Add multi-select mode for lists
This adds a multi-select mode for lists in which the current selection changes to be a cursor and pressing enter marks the item under the cursor as actually selected. DropDown is based on list so this should unblock adding a multi-select mode to DropDown's as well. Updates #330 Signed-off-by: Sam Whited <sam@samwhited.com>
This commit is contained in:
parent
c76f7879f5
commit
699bc05d47
162
list.go
162
list.go
@ -2,6 +2,7 @@ package tview
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
@ -62,6 +63,9 @@ type List struct {
|
||||
// The style for selected items.
|
||||
selectedStyle tcell.Style
|
||||
|
||||
// The style for the item under the cursor.
|
||||
cursorStyle tcell.Style
|
||||
|
||||
// If true, the selection is only shown when the list has focus.
|
||||
selectedFocusOnly bool
|
||||
|
||||
@ -95,6 +99,12 @@ type List struct {
|
||||
|
||||
// An optional function which is called when the user presses the Escape key.
|
||||
done func()
|
||||
|
||||
// True if multiple items may be selected.
|
||||
multiSelect bool
|
||||
|
||||
// The indices of the currently selected items in multi-select mode.
|
||||
selectedItems []int
|
||||
}
|
||||
|
||||
// NewList returns a new list.
|
||||
@ -107,17 +117,27 @@ func NewList() *List {
|
||||
secondaryTextStyle: tcell.StyleDefault.Foreground(Styles.TertiaryTextColor).Background(Styles.PrimitiveBackgroundColor),
|
||||
shortcutStyle: tcell.StyleDefault.Foreground(Styles.SecondaryTextColor).Background(Styles.PrimitiveBackgroundColor),
|
||||
selectedStyle: tcell.StyleDefault.Foreground(Styles.PrimitiveBackgroundColor).Background(Styles.PrimaryTextColor),
|
||||
cursorStyle: tcell.StyleDefault.Foreground(Styles.ContrastBackgroundColor).Background(Styles.PrimaryTextColor),
|
||||
mainStyleTags: true,
|
||||
secondaryStyleTags: true,
|
||||
}
|
||||
}
|
||||
|
||||
// MultiSelect enables or disables the selection of multiple list items.
|
||||
func (l *List) MultiSelect(enable bool) *List {
|
||||
l.multiSelect = enable
|
||||
return l
|
||||
}
|
||||
|
||||
// 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.
|
||||
// If multi-select mode is enabled the changed event is never triggered as this
|
||||
// changes the locatin of the cursor but does not select or deselect the list
|
||||
// option.
|
||||
func (l *List) SetCurrentItem(index int) *List {
|
||||
if index < 0 {
|
||||
index = len(l.items) + index
|
||||
@ -143,6 +163,8 @@ func (l *List) SetCurrentItem(index int) *List {
|
||||
|
||||
// GetCurrentItem returns the index of the currently selected list item,
|
||||
// starting at 0 for the first item.
|
||||
// If multi-select mode is enabled this returns the location of the cursor
|
||||
// whether the item is selected or not.
|
||||
func (l *List) GetCurrentItem() int {
|
||||
return l.currentItem
|
||||
}
|
||||
@ -194,22 +216,42 @@ func (l *List) RemoveItem(index int) *List {
|
||||
|
||||
// Remove item.
|
||||
l.items = append(l.items[:index], l.items[index+1:]...)
|
||||
idx := slices.Index(l.selectedItems, index)
|
||||
if idx > -1 {
|
||||
l.selectedItems = append(l.selectedItems[:idx], l.selectedItems[idx+1:]...)
|
||||
}
|
||||
|
||||
// If there is nothing left, we're done.
|
||||
if len(l.items) == 0 {
|
||||
return l
|
||||
}
|
||||
|
||||
// Shift current item.
|
||||
// Shift cursor.
|
||||
previousCurrentItem := l.currentItem
|
||||
if l.currentItem > index || l.currentItem == len(l.items) {
|
||||
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)
|
||||
if l.multiSelect {
|
||||
// Shift selected items.
|
||||
for i := 0; i < len(l.selectedItems); i++ {
|
||||
previousSelectedItem := l.selectedItems[i]
|
||||
if l.selectedItems[i] > index || l.selectedItems[i] == len(l.items) {
|
||||
l.selectedItems[i]--
|
||||
}
|
||||
|
||||
// Fire "changed" event for removed items.
|
||||
if previousSelectedItem == index && l.changed != nil {
|
||||
item := l.items[l.selectedItems[i]]
|
||||
l.changed(l.selectedItems[i], item.MainText, item.SecondaryText, item.Shortcut)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fire "changed" event for removed items in single-select mode.
|
||||
if previousCurrentItem == index && l.changed != nil {
|
||||
item := l.items[l.currentItem]
|
||||
l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
|
||||
}
|
||||
}
|
||||
|
||||
return l
|
||||
@ -279,6 +321,14 @@ func (l *List) SetSelectedStyle(style tcell.Style) *List {
|
||||
return l
|
||||
}
|
||||
|
||||
// SetCursorStyle sets the style of the item under the cursor in multi-select
|
||||
// mode. 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) SetCursorStyle(style tcell.Style) *List {
|
||||
l.cursorStyle = style
|
||||
return l
|
||||
}
|
||||
|
||||
// SetUseStyleTags sets a flag which determines whether style tags are used in
|
||||
// the main and secondary texts. The default is true.
|
||||
func (l *List) SetUseStyleTags(mainStyleTags, secondaryStyleTags bool) *List {
|
||||
@ -399,10 +449,16 @@ func (l *List) InsertItem(index int, mainText, secondaryText string, shortcut ru
|
||||
index = len(l.items)
|
||||
}
|
||||
|
||||
// Shift current item.
|
||||
// Shift cursor.
|
||||
if l.currentItem < len(l.items) && l.currentItem >= index {
|
||||
l.currentItem++
|
||||
}
|
||||
// Shift selected items.
|
||||
for i := 0; i < len(l.selectedItems); i++ {
|
||||
if l.selectedItems[i] < len(l.items) && l.selectedItems[i] >= index {
|
||||
l.selectedItems[i]++
|
||||
}
|
||||
}
|
||||
|
||||
// Insert item (make space for the new item, then shift and insert).
|
||||
l.items = append(l.items, nil)
|
||||
@ -491,6 +547,7 @@ func (l *List) FindItems(mainSearch, secondarySearch string, mustContainBoth, ig
|
||||
func (l *List) Clear() *List {
|
||||
l.items = nil
|
||||
l.currentItem = 0
|
||||
l.selectedItems = nil
|
||||
return l
|
||||
}
|
||||
|
||||
@ -538,11 +595,23 @@ func (l *List) Draw(screen tcell.Screen) {
|
||||
}
|
||||
|
||||
// Main text.
|
||||
selected := index == l.currentItem && (!l.selectedFocusOnly || l.HasFocus())
|
||||
var (
|
||||
selected bool
|
||||
underCursor bool
|
||||
)
|
||||
if l.multiSelect {
|
||||
selected = slices.Contains(l.selectedItems, index) && (!l.selectedFocusOnly || l.HasFocus())
|
||||
underCursor = index == l.currentItem && (!l.selectedFocusOnly || l.HasFocus())
|
||||
} else {
|
||||
selected = index == l.currentItem && (!l.selectedFocusOnly || l.HasFocus())
|
||||
}
|
||||
style := l.mainTextStyle
|
||||
if selected {
|
||||
style = l.selectedStyle
|
||||
}
|
||||
if underCursor {
|
||||
style = l.cursorStyle
|
||||
}
|
||||
mainText := item.MainText
|
||||
if !l.mainStyleTags {
|
||||
mainText = Escape(mainText)
|
||||
@ -648,19 +717,34 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
|
||||
}
|
||||
case tcell.KeyEnter:
|
||||
if l.currentItem >= 0 && l.currentItem < len(l.items) {
|
||||
item := l.items[l.currentItem]
|
||||
if item.Selected != nil {
|
||||
item.Selected()
|
||||
idx := -1
|
||||
if l.multiSelect {
|
||||
idx = slices.Index(l.selectedItems, l.currentItem)
|
||||
}
|
||||
if l.selected != nil {
|
||||
l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
|
||||
if idx == -1 {
|
||||
// Toggle the item to be selected.
|
||||
if l.multiSelect {
|
||||
// Only add the item to the selectedItems list if we're in
|
||||
// multi-select mode.
|
||||
l.selectedItems = append(l.selectedItems, l.currentItem)
|
||||
}
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
// Remove the item from the selection if we're in multi-select mode.
|
||||
l.selectedItems = append(l.selectedItems[:idx], l.selectedItems[idx+1:]...)
|
||||
}
|
||||
}
|
||||
case tcell.KeyRune:
|
||||
ch := event.Rune()
|
||||
var found bool
|
||||
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.
|
||||
@ -669,16 +753,28 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
break
|
||||
}
|
||||
// If it's a space bar or a shortcut, select or toggle the item.
|
||||
var removed bool
|
||||
if found || ch == ' ' {
|
||||
if l.multiSelect {
|
||||
idx := slices.Index(l.selectedItems, l.currentItem)
|
||||
if idx == -1 {
|
||||
l.selectedItems = append(l.selectedItems, l.currentItem)
|
||||
} else {
|
||||
l.selectedItems = append(l.selectedItems[:idx], l.selectedItems[idx+1:]...)
|
||||
removed = true
|
||||
}
|
||||
}
|
||||
if !removed {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -738,13 +834,25 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse,
|
||||
case MouseLeftClick:
|
||||
setFocus(l)
|
||||
index := l.indexAtPoint(event.Position())
|
||||
var removed bool
|
||||
if index != -1 {
|
||||
item := l.items[index]
|
||||
if item.Selected != nil {
|
||||
item.Selected()
|
||||
if l.multiSelect {
|
||||
idx := slices.Index(l.selectedItems, index)
|
||||
if idx == -1 {
|
||||
l.selectedItems = append(l.selectedItems, index)
|
||||
} else {
|
||||
removed = true
|
||||
l.selectedItems = append(l.selectedItems[:idx], l.selectedItems[idx+1:]...)
|
||||
}
|
||||
}
|
||||
if l.selected != nil {
|
||||
l.selected(index, item.MainText, item.SecondaryText, item.Shortcut)
|
||||
item := l.items[index]
|
||||
if !removed {
|
||||
if item.Selected != nil {
|
||||
item.Selected()
|
||||
}
|
||||
if l.selected != nil {
|
||||
l.selected(index, item.MainText, item.SecondaryText, item.Shortcut)
|
||||
}
|
||||
}
|
||||
if index != l.currentItem {
|
||||
if l.changed != nil {
|
||||
|
Loading…
x
Reference in New Issue
Block a user