mirror of
https://github.com/VladimirMarkelov/clui.git
synced 2025-04-26 13:49:01 +08:00
411 lines
8.6 KiB
Go
411 lines
8.6 KiB
Go
package clui
|
|
|
|
import (
|
|
term "github.com/nsf/termbox-go"
|
|
"strings"
|
|
)
|
|
|
|
/*
|
|
ListBox is 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.
|
|
*/
|
|
type ListBox struct {
|
|
BaseControl
|
|
// own listbox members
|
|
items []string
|
|
currSelection int
|
|
topLine int
|
|
buttonPos int
|
|
|
|
onSelectItem func(Event)
|
|
onKeyPress func(term.Key) bool
|
|
}
|
|
|
|
/*
|
|
NewListBox creates a new frame.
|
|
view - is a View that manages the control
|
|
parent - is container that keeps the control. The same View can be a view and a parent at the same time.
|
|
width and heigth - are minimal size of the control.
|
|
scale - the way of scaling the control when the parent is resized. Use DoNotScale constant if the
|
|
control should keep its original size.
|
|
*/
|
|
func CreateListBox(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.buttonPos = -1
|
|
|
|
l.SetTabStop(true)
|
|
l.SetScale(scale)
|
|
|
|
l.onSelectItem = nil
|
|
|
|
if parent != nil {
|
|
parent.AddChild(l)
|
|
}
|
|
|
|
return l
|
|
}
|
|
|
|
func (l *ListBox) drawScroll() {
|
|
PushAttributes()
|
|
defer PopAttributes()
|
|
|
|
pos := ThumbPosition(l.currSelection, len(l.items), l.height)
|
|
l.buttonPos = pos
|
|
|
|
DrawScrollBar(l.x+l.width-1, l.y, 1, l.height, pos)
|
|
}
|
|
|
|
func (l *ListBox) drawItems() {
|
|
PushAttributes()
|
|
defer PopAttributes()
|
|
|
|
maxCurr := len(l.items) - 1
|
|
curr := l.topLine
|
|
dy := 0
|
|
maxDy := l.height - 1
|
|
maxWidth := l.width - 1
|
|
|
|
fg, bg := RealColor(l.fg, ColorEditText), RealColor(l.bg, ColorEditBack)
|
|
if l.Active() {
|
|
fg, bg = RealColor(l.fg, ColorEditActiveText), RealColor(l.bg, ColorEditActiveBack)
|
|
}
|
|
fgSel, bgSel := RealColor(l.fgActive, ColorSelectionText), RealColor(l.bgActive, ColorSelectionBack)
|
|
|
|
for curr <= maxCurr && dy <= maxDy {
|
|
f, b := fg, bg
|
|
if curr == l.currSelection {
|
|
f, b = fgSel, bgSel
|
|
}
|
|
|
|
SetTextColor(f)
|
|
SetBackColor(b)
|
|
FillRect(l.x, l.y+dy, l.width-1, 1, ' ')
|
|
str := SliceColorized(l.items[curr], 0, maxWidth)
|
|
DrawText(l.x, l.y+dy, str)
|
|
|
|
curr++
|
|
dy++
|
|
}
|
|
}
|
|
|
|
// Repaint draws the control on its View surface
|
|
func (l *ListBox) Draw() {
|
|
PushAttributes()
|
|
defer PopAttributes()
|
|
|
|
x, y := l.Pos()
|
|
w, h := l.Size()
|
|
|
|
fg, bg := RealColor(l.fg, ColorEditText), RealColor(l.bg, ColorEditBack)
|
|
if l.Active() {
|
|
fg, bg = RealColor(l.fg, ColorEditActiveText), RealColor(l.bg, ColorEditActiveBack)
|
|
}
|
|
SetTextColor(fg)
|
|
SetBackColor(bg)
|
|
FillRect(x, y, w, h, ' ')
|
|
l.drawItems()
|
|
l.drawScroll()
|
|
}
|
|
|
|
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(dy int) {
|
|
if l.topLine == 0 && l.currSelection == 0 {
|
|
return
|
|
}
|
|
|
|
if l.currSelection == -1 {
|
|
if len(l.items) != 0 {
|
|
l.currSelection = 0
|
|
}
|
|
return
|
|
}
|
|
|
|
if l.currSelection < dy {
|
|
l.currSelection = 0
|
|
} else {
|
|
l.currSelection -= dy
|
|
}
|
|
|
|
l.EnsureVisible()
|
|
}
|
|
|
|
func (l *ListBox) moveDown(dy int) {
|
|
length := len(l.items)
|
|
|
|
if length == 0 || l.currSelection == length-1 {
|
|
return
|
|
}
|
|
|
|
if l.currSelection+dy >= length {
|
|
l.currSelection = length - 1
|
|
} else {
|
|
l.currSelection += dy
|
|
}
|
|
|
|
l.EnsureVisible()
|
|
}
|
|
|
|
// EnsureVisible makes the currently selected item visible and scrolls the item list if it is required
|
|
func (l *ListBox) EnsureVisible() {
|
|
length := len(l.items)
|
|
|
|
if length <= l.height || l.currSelection == -1 {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clear 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(1)
|
|
return true
|
|
}
|
|
if dy == l.height-1 {
|
|
l.moveDown(1)
|
|
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)
|
|
WindowManager().BeginUpdate()
|
|
onSelFunc := l.onSelectItem
|
|
WindowManager().EndUpdate()
|
|
if onSelFunc != nil {
|
|
ev := Event{Y: l.topLine + dy, Msg: l.SelectedItemText()}
|
|
go onSelFunc(ev)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (l *ListBox) recalcPositionByScroll() {
|
|
newPos := ItemByThumbPosition(l.buttonPos, len(l.items), l.height)
|
|
if newPos < 1 {
|
|
return
|
|
}
|
|
|
|
l.currSelection = newPos
|
|
l.EnsureVisible()
|
|
}
|
|
|
|
/*
|
|
ProcessEvent processes all events come from the control parent. If a control
|
|
processes an event it should return true. If the method returns false it means
|
|
that the control do not want or cannot process the event and the caller sends
|
|
the event to the control parent
|
|
*/
|
|
func (l *ListBox) ProcessEvent(event Event) bool {
|
|
if !l.Active() || !l.Enabled() {
|
|
return false
|
|
}
|
|
|
|
switch event.Type {
|
|
case EventKey:
|
|
if l.onKeyPress != nil {
|
|
res := l.onKeyPress(event.Key)
|
|
if res {
|
|
return true
|
|
}
|
|
}
|
|
|
|
switch event.Key {
|
|
case term.KeyHome:
|
|
l.home()
|
|
return true
|
|
case term.KeyEnd:
|
|
l.end()
|
|
return true
|
|
case term.KeyArrowUp:
|
|
l.moveUp(1)
|
|
return true
|
|
case term.KeyArrowDown:
|
|
l.moveDown(1)
|
|
return true
|
|
case term.KeyPgdn:
|
|
l.moveDown(l.height)
|
|
return true
|
|
case term.KeyPgup:
|
|
l.moveUp(l.height)
|
|
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
|
|
|
|
// AddItem adds a new item to item list.
|
|
// Returns true if the operation is successful
|
|
func (l *ListBox) AddItem(item string) bool {
|
|
l.items = append(l.items, item)
|
|
return true
|
|
}
|
|
|
|
// SelectItem slects 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
|
|
}
|
|
|
|
// FindItem looks for 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
|
|
}
|
|
|
|
// SelectedItem returns currently selected item id
|
|
func (l *ListBox) SelectedItem() int {
|
|
return l.currSelection
|
|
}
|
|
|
|
// SelectedItemText 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]
|
|
}
|
|
|
|
// RemoveItem 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
|
|
}
|
|
|
|
// OnSelectItem sets a callback that is called every time
|
|
// the selected item is changed
|
|
func (l *ListBox) OnSelectItem(fn func(Event)) {
|
|
l.onSelectItem = fn
|
|
}
|
|
|
|
// OnKeyPress sets the callback that is called when a user presses a Key while
|
|
// the controls is active. If a handler processes the key it should return
|
|
// true. If handler returns false it means that the default handler will
|
|
// process the key
|
|
func (l *ListBox) OnKeyPress(fn func(term.Key) bool) {
|
|
l.onKeyPress = fn
|
|
}
|
|
|
|
// ItemCount returns the number of items in the ListBox
|
|
func (l *ListBox) ItemCount() int {
|
|
return len(l.items)
|
|
}
|