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 { ControlBase // own listbox members items []string currSelection int topLine int maxItems int buttonPos int onSelectItem func(Event) } /* 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 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++ } } // Repaint draws the control on its View surface 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() } // 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() 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() } /* 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: 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 // AddItem adds a new item to item list. If the maximun item // is greater than 0 and the number of item greater maximum // then the first item is deleted. // 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 } // 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 } // MaxItems returns the maximum number of items that the // ListBox can keep. 0 means unlimited. It makes a ListBox // work like a FIFO queue: the oldest(the first) items are // deleted if one adds an item to a full ListBox func (l *ListBox) MaxItems() int { return l.maxItems } // SetMaxItems sets the maximum items that ListBox keeps func (l *ListBox) SetMaxItems(max int) { l.maxItems = max } // ItemCount returns the number of items in the ListBox func (l *ListBox) ItemCount() int { return len(l.items) }