mirror of
https://github.com/VladimirMarkelov/clui.git
synced 2025-04-26 13:49:01 +08:00
396 lines
8.1 KiB
Go
396 lines
8.1 KiB
Go
package clui
|
|
|
|
import (
|
|
term "github.com/nsf/termbox-go"
|
|
"strings"
|
|
)
|
|
|
|
/*
|
|
Control to display a list of items and allow to user to select any of them.
|
|
Content is scrollable with arrow keys or by clicking up and bottom buttons
|
|
on the scroll(now content is scrollable with mouse dragging only on Windows).
|
|
|
|
ListBox calls onSelectItem item function after a user changes currently
|
|
selected item with mouse or using keyboard (extra case: the event is emitted
|
|
when a user presses Enter - the case is used in ComboBox to select an item
|
|
from drop down list). Event structure has 2 fields filled: Y - selected
|
|
item number in list(-1 if nothing is selected), Msg - text of the selected item.
|
|
|
|
ListBox provides a few own methods to manipulate its items:
|
|
AddItem, SelectItem, FindItem, GetSelectedItem, RemoveItem, Clear
|
|
*/
|
|
type ListBox struct {
|
|
ControlBase
|
|
// own listbox members
|
|
items []string
|
|
currSelection int
|
|
topLine int
|
|
maxItems int
|
|
buttonPos int
|
|
|
|
onSelectItem func(Event)
|
|
}
|
|
|
|
func NewListBox(view View, parent Control, width, height int, scale int) *ListBox {
|
|
l := new(ListBox)
|
|
|
|
if height == AutoSize {
|
|
height = 3
|
|
}
|
|
if width == AutoSize {
|
|
width = 5
|
|
}
|
|
|
|
l.SetSize(width, height)
|
|
l.SetConstraints(width, height)
|
|
l.currSelection = -1
|
|
l.items = make([]string, 0)
|
|
l.topLine = 0
|
|
l.parent = parent
|
|
l.view = view
|
|
l.maxItems = 0
|
|
l.buttonPos = -1
|
|
|
|
l.SetTabStop(true)
|
|
|
|
l.onSelectItem = nil
|
|
|
|
if parent != nil {
|
|
parent.AddChild(l, scale)
|
|
}
|
|
|
|
return l
|
|
}
|
|
|
|
func (l *ListBox) redrawScroll(canvas Canvas, tm Theme) {
|
|
parts := []rune(tm.SysObject(ObjScrollBar))
|
|
|
|
chLine, chCursor, chUp, chDown := parts[0], parts[1], parts[2], parts[3]
|
|
|
|
fg, bg := RealColor(tm, l.fg, ColorScrollText), RealColor(tm, l.bg, ColorScrollBack)
|
|
fgThumb, bgThumb := RealColor(tm, l.fg, ColorThumbText), RealColor(tm, l.bg, ColorThumbBack)
|
|
|
|
canvas.PutSymbol(l.x+l.width-1, l.y, term.Cell{Ch: chUp, Fg: fg, Bg: bg})
|
|
canvas.PutSymbol(l.x+l.width-1, l.y+l.height-1, term.Cell{Ch: chDown, Fg: fg, Bg: bg})
|
|
|
|
if l.height > 2 {
|
|
for yy := 1; yy < l.height-1; yy++ {
|
|
canvas.PutSymbol(l.x+l.width-1, l.y+yy, term.Cell{Ch: chLine, Fg: fg, Bg: bg})
|
|
}
|
|
}
|
|
|
|
if l.currSelection == -1 {
|
|
return
|
|
}
|
|
|
|
if l.height == 3 || l.currSelection <= 0 {
|
|
canvas.PutSymbol(l.x+l.width-1, l.y+1, term.Cell{Ch: chCursor, Fg: fgThumb, Bg: bgThumb})
|
|
return
|
|
}
|
|
|
|
// if l.pressY == -1 {
|
|
ydiff := int(float32(l.currSelection) / float32(len(l.items)-1.0) * float32(l.height-3))
|
|
l.buttonPos = ydiff + 1
|
|
// }
|
|
canvas.PutSymbol(l.x+l.width-1, l.y+l.buttonPos, term.Cell{Ch: chCursor, Fg: fgThumb, Bg: bgThumb})
|
|
}
|
|
|
|
func (l *ListBox) redrawItems(canvas Canvas, tm Theme) {
|
|
maxCurr := len(l.items) - 1
|
|
curr := l.topLine
|
|
dy := 0
|
|
maxDy := l.height - 1
|
|
maxWidth := l.width - 1
|
|
|
|
fg, bg := RealColor(tm, l.fg, ColorEditText), RealColor(tm, l.bg, ColorEditBack)
|
|
if l.Active() {
|
|
fg, bg = RealColor(tm, l.fg, ColorEditActiveText), RealColor(tm, l.bg, ColorEditActiveBack)
|
|
}
|
|
fgSel, bgSel := RealColor(tm, l.fgActive, ColorSelectionText), RealColor(tm, l.bgActive, ColorSelectionBack)
|
|
|
|
for curr <= maxCurr && dy <= maxDy {
|
|
f, b := fg, bg
|
|
if curr == l.currSelection {
|
|
f, b = fgSel, bgSel
|
|
}
|
|
|
|
canvas.FillRect(l.x, l.y+dy, l.width-1, 1, term.Cell{Bg: b, Ch: ' ', Fg: f})
|
|
_, text := AlignText(l.items[curr], maxWidth, AlignLeft)
|
|
canvas.PutText(l.x, l.y+dy, text, f, b)
|
|
|
|
curr++
|
|
dy++
|
|
}
|
|
}
|
|
|
|
func (l *ListBox) Repaint() {
|
|
canvas := l.view.Canvas()
|
|
tm := l.view.Screen().Theme()
|
|
|
|
x, y := l.Pos()
|
|
w, h := l.Size()
|
|
|
|
bg := RealColor(tm, l.bg, ColorEditBack)
|
|
if l.Active() {
|
|
bg = RealColor(tm, l.bg, ColorEditActiveBack)
|
|
}
|
|
canvas.FillRect(x, y, w, h, term.Cell{Bg: bg, Ch: ' '})
|
|
l.redrawItems(canvas, tm)
|
|
l.redrawScroll(canvas, tm)
|
|
}
|
|
|
|
func (l *ListBox) home() {
|
|
if len(l.items) > 0 {
|
|
l.currSelection = 0
|
|
}
|
|
l.topLine = 0
|
|
}
|
|
|
|
func (l *ListBox) end() {
|
|
length := len(l.items)
|
|
|
|
if length == 0 {
|
|
return
|
|
}
|
|
|
|
l.currSelection = length - 1
|
|
if length > l.height {
|
|
l.topLine = length - l.height
|
|
}
|
|
}
|
|
|
|
func (l *ListBox) moveUp() {
|
|
if l.topLine == 0 && l.currSelection == 0 {
|
|
return
|
|
}
|
|
|
|
if l.currSelection == -1 {
|
|
if len(l.items) != 0 {
|
|
l.currSelection = 0
|
|
}
|
|
return
|
|
}
|
|
|
|
l.currSelection--
|
|
l.EnsureVisible()
|
|
}
|
|
|
|
func (l *ListBox) moveDown() {
|
|
length := len(l.items)
|
|
|
|
if length == 0 || l.currSelection == length-1 {
|
|
return
|
|
}
|
|
|
|
l.currSelection++
|
|
l.EnsureVisible()
|
|
}
|
|
|
|
func (l *ListBox) EnsureVisible() {
|
|
length := len(l.items)
|
|
|
|
if length <= l.height {
|
|
return
|
|
}
|
|
|
|
diff := l.currSelection - l.topLine
|
|
if diff >= 0 && diff < l.height {
|
|
return
|
|
}
|
|
|
|
if diff < 0 {
|
|
l.topLine = l.currSelection
|
|
} else {
|
|
top := l.currSelection - l.height + 1
|
|
if length-top > l.height {
|
|
l.topLine = top
|
|
} else {
|
|
l.topLine = length - l.height
|
|
}
|
|
}
|
|
}
|
|
|
|
// Deletes all ListBox items
|
|
func (l *ListBox) Clear() {
|
|
l.items = make([]string, 0)
|
|
l.currSelection = -1
|
|
l.topLine = 0
|
|
}
|
|
|
|
func (l *ListBox) processMouseClick(ev Event) bool {
|
|
if ev.Key != term.MouseLeft {
|
|
return false
|
|
}
|
|
|
|
dx := ev.X - l.x
|
|
dy := ev.Y - l.y
|
|
|
|
if dx == l.width-1 {
|
|
if dy < 0 || dy >= l.height || len(l.items) < 2 {
|
|
return true
|
|
}
|
|
|
|
if dy == 0 {
|
|
l.moveUp()
|
|
return true
|
|
}
|
|
if dy == l.height-1 {
|
|
l.moveDown()
|
|
return true
|
|
}
|
|
|
|
l.buttonPos = dy
|
|
l.recalcPositionByScroll()
|
|
return true
|
|
}
|
|
|
|
if dx < 0 || dx >= l.width || dy < 0 || dy >= l.height {
|
|
return true
|
|
}
|
|
|
|
if dy >= len(l.items) {
|
|
return true
|
|
}
|
|
|
|
l.SelectItem(l.topLine + dy)
|
|
if l.onSelectItem != nil {
|
|
ev := Event{Y: l.topLine + dy, Msg: l.SelectedItemText()}
|
|
go l.onSelectItem(ev)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (l *ListBox) recalcPositionByScroll() {
|
|
if len(l.items) < 2 {
|
|
return
|
|
}
|
|
|
|
newPos := int(float32(len(l.items)-1)*float32(l.buttonPos-1)/float32(l.height-3) + 0.9)
|
|
|
|
if newPos < 0 {
|
|
newPos = 0
|
|
} else if newPos >= len(l.items) {
|
|
newPos = len(l.items) - 1
|
|
}
|
|
|
|
l.currSelection = newPos
|
|
l.EnsureVisible()
|
|
}
|
|
|
|
func (l *ListBox) ProcessEvent(event Event) bool {
|
|
if !l.Active() || !l.Enabled() {
|
|
return false
|
|
}
|
|
|
|
switch event.Type {
|
|
case EventKey:
|
|
switch event.Key {
|
|
case term.KeyHome:
|
|
l.home()
|
|
return true
|
|
case term.KeyEnd:
|
|
l.end()
|
|
return true
|
|
case term.KeyArrowUp:
|
|
l.moveUp()
|
|
return true
|
|
case term.KeyArrowDown:
|
|
l.moveDown()
|
|
return true
|
|
case term.KeyCtrlM:
|
|
if l.currSelection != -1 && l.onSelectItem != nil {
|
|
ev := Event{Y: l.currSelection, Msg: l.SelectedItemText()}
|
|
go l.onSelectItem(ev)
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
case EventMouse:
|
|
return l.processMouseClick(event)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// own methods
|
|
|
|
// Adds a new item to item list
|
|
// Returns true if the operation is successful
|
|
func (l *ListBox) AddItem(item string) bool {
|
|
if l.maxItems > 0 && len(l.items) > l.maxItems {
|
|
l.RemoveItem(0)
|
|
}
|
|
|
|
l.items = append(l.items, item)
|
|
return true
|
|
}
|
|
|
|
// Selects item which number in the list equals id. If the item exists the
|
|
// ListBox scrolls the list to make the item visible.
|
|
// Returns true if the item is selected successfully
|
|
func (l *ListBox) SelectItem(id int) bool {
|
|
if len(l.items) <= id || id < 0 {
|
|
return false
|
|
}
|
|
|
|
l.currSelection = id
|
|
l.EnsureVisible()
|
|
return true
|
|
}
|
|
|
|
// Finds an item in list which text equals to text, by default the search
|
|
// is casesensitive.
|
|
// Returns item number in item list or -1 if nothing is found.
|
|
func (l *ListBox) FindItem(text string, caseSensitive bool) int {
|
|
for idx, itm := range l.items {
|
|
if itm == text || (caseSensitive && strings.EqualFold(itm, text)) {
|
|
return idx
|
|
}
|
|
}
|
|
|
|
return -1
|
|
}
|
|
|
|
// Returns currently selected item id
|
|
func (l *ListBox) SelectedItem() int {
|
|
return l.currSelection
|
|
}
|
|
|
|
// Returns text of currently selected item or empty sting if nothing is
|
|
// selected or ListBox is empty
|
|
func (l *ListBox) SelectedItemText() string {
|
|
if l.currSelection == -1 {
|
|
return ""
|
|
}
|
|
|
|
return l.items[l.currSelection]
|
|
}
|
|
|
|
// Deletes an item which number is id in item list
|
|
// Returns true if item is deleted
|
|
func (l *ListBox) RemoveItem(id int) bool {
|
|
if id < 0 || id >= len(l.items) {
|
|
return false
|
|
}
|
|
|
|
l.items = append(l.items[:id], l.items[id+1:]...)
|
|
return true
|
|
}
|
|
|
|
func (l *ListBox) OnSelectItem(fn func(Event)) {
|
|
l.onSelectItem = fn
|
|
}
|
|
|
|
func (l *ListBox) MaxItems() int {
|
|
return l.maxItems
|
|
}
|
|
|
|
func (l *ListBox) SetMaxItems(max int) {
|
|
l.maxItems = max
|
|
}
|
|
|
|
func (l *ListBox) ItemCount() int {
|
|
return len(l.items)
|
|
}
|