Make cview thread-safe

This commit is contained in:
Trevor Slocum 2020-03-25 14:32:57 +00:00
parent d045073571
commit e29d4b73b9
22 changed files with 1291 additions and 87 deletions

View File

@ -1,3 +1,6 @@
v1.4.5 (WIP)
- Add multithreading support
v1.4.4 (2020-02-24)
- Fix panic when navigating empty list
- Fix resize event dimensions on Windows

View File

@ -26,6 +26,10 @@ maintainers and allowing code changes which may be outside of tview's scope.
# Differences
## cview is thread-safe
tview [is not thread-safe](https://godoc.org/github.com/rivo/tview#hdr-Concurrency).
## Application.QueueUpdate and Application.QueueUpdateDraw do not block
tview [blocks until the queued function returns](https://github.com/rivo/tview/blob/fe3052019536251fd145835dbaa225b33b7d3088/application.go#L510).

View File

@ -67,9 +67,6 @@ the program in the "demos/presentation" subdirectory.
Package documentation is available via [godoc](https://docs.rocketnine.space/gitlab.com/tslocum/cview).
**This package is not thread-safe.** Most functions may only be called from the
main thread, as documented in [Concurrency](https://docs.rocketnine.space/gitlab.com/tslocum/cview/#hdr-Concurrency).
An [introduction tutorial](https://rocketnine.space/post/tview-and-you/) is also available.
## Dependencies

View File

@ -27,8 +27,6 @@ const resizeEventThrottle = 200 * time.Millisecond
// panic(err)
// }
type Application struct {
sync.RWMutex
// The application's screen. Apart from Run(), this variable should never be
// set directly. Always use the screenReplacement channel after calling
// Fini(), to set a new screen (or nil to stop the application).
@ -94,6 +92,8 @@ type Application struct {
lastMouseX, lastMouseY int
lastMouseBtn tcell.ButtonMask
lastMouseTarget Primitive // nil if none
sync.RWMutex
}
// NewApplication creates and returns a new application.
@ -115,6 +115,9 @@ func NewApplication() *Application {
// itself: Such a handler can intercept the Ctrl-C event which closes the
// application.
func (a *Application) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) *Application {
a.Lock()
defer a.Unlock()
a.inputCapture = capture
return a
}
@ -122,6 +125,9 @@ func (a *Application) SetInputCapture(capture func(event *tcell.EventKey) *tcell
// GetInputCapture returns the function installed with SetInputCapture() or nil
// if no such function has been installed.
func (a *Application) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey {
a.RLock()
defer a.RUnlock()
return a.inputCapture
}
@ -139,6 +145,9 @@ func (a *Application) SetMouseCapture(capture func(event *EventMouse) *EventMous
// GetMouseCapture returns the function installed with SetMouseCapture() or nil
// if no such function has been installed.
func (a *Application) GetMouseCapture() func(event *EventMouse) *EventMouse {
a.RLock()
defer a.RUnlock()
return a.mouseCapture
}
@ -155,6 +164,9 @@ func (a *Application) SetTemporaryMouseCapture(capture func(event *EventMouse) *
// GetTemporaryMouseCapture returns the function installed with
// SetTemporaryMouseCapture() or nil if no such function has been installed.
func (a *Application) GetTemporaryMouseCapture() func(event *EventMouse) *EventMouse {
a.RLock()
defer a.RUnlock()
return a.tempMouseCapture
}
@ -478,6 +490,7 @@ func findAtPoint(atX, atY int, p Primitive, capture func(p Primitive)) Primitive
func (a *Application) GetPrimitiveAtPoint(atX, atY int) Primitive {
a.RLock()
defer a.RUnlock()
return findAtPoint(atX, atY, a.root, nil)
}
@ -486,6 +499,7 @@ func (a *Application) GetPrimitiveAtPoint(atX, atY int) Primitive {
func (a *Application) appendStackAtPoint(buf []Primitive, atX, atY int) []Primitive {
a.RLock()
defer a.RUnlock()
findAtPoint(atX, atY, a.root, func(p Primitive) {
buf = append(buf, p)
})
@ -496,6 +510,7 @@ func (a *Application) appendStackAtPoint(buf []Primitive, atX, atY int) []Primit
func (a *Application) Stop() {
a.Lock()
defer a.Unlock()
screen := a.screen
if screen == nil {
return
@ -566,7 +581,6 @@ func (a *Application) ForceDraw() *Application {
// draw actually does what Draw() promises to do.
func (a *Application) draw() *Application {
a.Lock()
defer a.Unlock()
screen := a.screen
root := a.root
@ -576,6 +590,7 @@ func (a *Application) draw() *Application {
// Maybe we're not ready yet or not anymore.
if screen == nil || root == nil {
a.Unlock()
return a
}
@ -587,10 +602,13 @@ func (a *Application) draw() *Application {
// Call before handler if there is one.
if before != nil {
a.Unlock()
if before(screen) {
screen.Show()
return a
}
} else {
a.Unlock()
}
// Draw all primitives.
@ -617,6 +635,9 @@ func (a *Application) draw() *Application {
//
// Provide nil to uninstall the callback function.
func (a *Application) SetBeforeDrawFunc(handler func(screen tcell.Screen) bool) *Application {
a.Lock()
defer a.Unlock()
a.beforeDraw = handler
return a
}
@ -624,6 +645,9 @@ func (a *Application) SetBeforeDrawFunc(handler func(screen tcell.Screen) bool)
// GetBeforeDrawFunc returns the callback function installed with
// SetBeforeDrawFunc() or nil if none has been installed.
func (a *Application) GetBeforeDrawFunc() func(screen tcell.Screen) bool {
a.RLock()
defer a.RUnlock()
return a.beforeDraw
}
@ -632,6 +656,9 @@ func (a *Application) GetBeforeDrawFunc() func(screen tcell.Screen) bool {
//
// Provide nil to uninstall the callback function.
func (a *Application) SetAfterDrawFunc(handler func(screen tcell.Screen)) *Application {
a.Lock()
defer a.Unlock()
a.afterDraw = handler
return a
}
@ -639,6 +666,9 @@ func (a *Application) SetAfterDrawFunc(handler func(screen tcell.Screen)) *Appli
// GetAfterDrawFunc returns the callback function installed with
// SetAfterDrawFunc() or nil if none has been installed.
func (a *Application) GetAfterDrawFunc() func(screen tcell.Screen) {
a.RLock()
defer a.RUnlock()
return a.afterDraw
}
@ -680,6 +710,9 @@ func (a *Application) ResizeToFullScreen(p Primitive) *Application {
//
// Provide nil to uninstall the callback function.
func (a *Application) SetAfterResizeFunc(handler func(width int, height int)) *Application {
a.Lock()
defer a.Unlock()
a.afterResize = handler
return a
}
@ -687,6 +720,9 @@ func (a *Application) SetAfterResizeFunc(handler func(width int, height int)) *A
// GetAfterResizeFunc returns the callback function installed with
// SetAfterResizeFunc() or nil if none has been installed.
func (a *Application) GetAfterResizeFunc() func(width int, height int) {
a.RLock()
defer a.RUnlock()
return a.afterResize
}
@ -720,13 +756,11 @@ func (a *Application) SetFocus(p Primitive) *Application {
func (a *Application) GetFocus() Primitive {
a.RLock()
defer a.RUnlock()
return a.focus
}
// QueueUpdate is used to synchronize access to primitives from non-main
// goroutines. The provided function will be executed as part of the event loop
// and thus will not cause race conditions with other such update functions or
// the Draw() function.
// QueueUpdate queues a function to be executed as part of the event loop.
//
// Note that Draw() is not implicitly called after the execution of f as that
// may not be desirable. You can call Draw() from f if the screen should be

103
box.go
View File

@ -1,6 +1,8 @@
package cview
import (
"sync"
"github.com/gdamore/tcell"
)
@ -60,6 +62,8 @@ type Box struct {
// event to be forwarded to the primitive's default mouse event handler (nil if
// nothing should be forwarded).
mouseCapture func(event *EventMouse) *EventMouse
l sync.RWMutex
}
// NewBox returns a Box without a border.
@ -79,6 +83,9 @@ func NewBox() *Box {
// SetBorderPadding sets the size of the borders around the box content.
func (b *Box) SetBorderPadding(top, bottom, left, right int) *Box {
b.l.Lock()
defer b.l.Unlock()
b.paddingTop, b.paddingBottom, b.paddingLeft, b.paddingRight = top, bottom, left, right
return b
}
@ -86,6 +93,9 @@ func (b *Box) SetBorderPadding(top, bottom, left, right int) *Box {
// GetRect returns the current position of the rectangle, x, y, width, and
// height.
func (b *Box) GetRect() (int, int, int, int) {
b.l.RLock()
defer b.l.RUnlock()
return b.x, b.y, b.width, b.height
}
@ -93,10 +103,15 @@ func (b *Box) GetRect() (int, int, int, int) {
// height), without the border and without any padding. Width and height values
// will clamp to 0 and thus never be negative.
func (b *Box) GetInnerRect() (int, int, int, int) {
b.l.RLock()
if b.innerX >= 0 {
defer b.l.RUnlock()
return b.innerX, b.innerY, b.innerWidth, b.innerHeight
}
b.l.RUnlock()
x, y, width, height := b.GetRect()
b.l.RLock()
if b.border {
x++
y++
@ -113,6 +128,7 @@ func (b *Box) GetInnerRect() (int, int, int, int) {
if height < 0 {
height = 0
}
b.l.RUnlock()
return x, y, width, height
}
@ -122,6 +138,9 @@ func (b *Box) GetInnerRect() (int, int, int, int) {
//
// application.SetRoot(b, true)
func (b *Box) SetRect(x, y, width, height int) {
b.l.Lock()
defer b.l.Unlock()
b.x = x
b.y = y
b.width = width
@ -138,6 +157,9 @@ func (b *Box) SetRect(x, y, width, height int) {
// returned by GetInnerRect(), used by descendent primitives to draw their own
// content.
func (b *Box) SetDrawFunc(handler func(screen tcell.Screen, x, y, width, height int) (int, int, int, int)) *Box {
b.l.Lock()
defer b.l.Unlock()
b.draw = handler
return b
}
@ -145,6 +167,9 @@ func (b *Box) SetDrawFunc(handler func(screen tcell.Screen, x, y, width, height
// GetDrawFunc returns the callback function which was installed with
// SetDrawFunc() or nil if no such function has been installed.
func (b *Box) GetDrawFunc() func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) {
b.l.RLock()
defer b.l.RUnlock()
return b.draw
}
@ -166,6 +191,9 @@ func (b *Box) WrapInputHandler(inputHandler func(*tcell.EventKey, func(p Primiti
// InputHandler returns nil.
func (b *Box) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
b.l.RLock()
defer b.l.RUnlock()
return b.WrapInputHandler(nil)
}
@ -184,6 +212,9 @@ func (b *Box) InputHandler() func(event *tcell.EventKey, setFocus func(p Primiti
// to their contained primitives and thus never receive any key events
// themselves. Therefore, they cannot intercept key events.
func (b *Box) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) *Box {
b.l.Lock()
defer b.l.Unlock()
b.inputCapture = capture
return b
}
@ -191,6 +222,9 @@ func (b *Box) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKe
// GetInputCapture returns the function installed with SetInputCapture() or nil
// if no such function has been installed.
func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey {
b.l.RLock()
defer b.l.RUnlock()
return b.inputCapture
}
@ -212,6 +246,9 @@ func (b *Box) WrapMouseHandler(mouseHandler func(*EventMouse)) func(*EventMouse)
// MouseHandler returns nil.
func (b *Box) MouseHandler() func(event *EventMouse) {
b.l.RLock()
defer b.l.RUnlock()
return b.WrapMouseHandler(nil)
}
@ -223,6 +260,9 @@ func (b *Box) MouseHandler() func(event *EventMouse) {
//
// Providing a nil handler will remove a previously existing handler.
func (b *Box) SetMouseCapture(capture func(*EventMouse) *EventMouse) *Box {
b.l.Lock()
defer b.l.Unlock()
b.mouseCapture = capture
return b
}
@ -230,11 +270,17 @@ func (b *Box) SetMouseCapture(capture func(*EventMouse) *EventMouse) *Box {
// GetMouseCapture returns the function installed with SetMouseCapture() or nil
// if no such function has been installed.
func (b *Box) GetMouseCapture() func(*EventMouse) *EventMouse {
b.l.RLock()
defer b.l.RUnlock()
return b.mouseCapture
}
// SetBackgroundColor sets the box's background color.
func (b *Box) SetBackgroundColor(color tcell.Color) *Box {
b.l.Lock()
defer b.l.Unlock()
b.backgroundColor = color
return b
}
@ -242,12 +288,18 @@ func (b *Box) SetBackgroundColor(color tcell.Color) *Box {
// SetBorder sets the flag indicating whether or not the box should have a
// border.
func (b *Box) SetBorder(show bool) *Box {
b.l.Lock()
defer b.l.Unlock()
b.border = show
return b
}
// SetBorderColor sets the box's border color.
func (b *Box) SetBorderColor(color tcell.Color) *Box {
b.l.Lock()
defer b.l.Unlock()
b.borderColor = color
return b
}
@ -257,23 +309,35 @@ func (b *Box) SetBorderColor(color tcell.Color) *Box {
//
// box.SetBorderAttributes(tcell.AttrUnderline | tcell.AttrBold)
func (b *Box) SetBorderAttributes(attr tcell.AttrMask) *Box {
b.l.Lock()
defer b.l.Unlock()
b.borderAttributes = attr
return b
}
// SetTitle sets the box's title.
func (b *Box) SetTitle(title string) *Box {
b.l.Lock()
defer b.l.Unlock()
b.title = title
return b
}
// GetTitle returns the box's current title.
func (b *Box) GetTitle() string {
b.l.RLock()
defer b.l.RUnlock()
return b.title
}
// SetTitleColor sets the box's title color.
func (b *Box) SetTitleColor(color tcell.Color) *Box {
b.l.Lock()
defer b.l.Unlock()
b.titleColor = color
return b
}
@ -281,14 +345,20 @@ func (b *Box) SetTitleColor(color tcell.Color) *Box {
// SetTitleAlign sets the alignment of the title, one of AlignLeft, AlignCenter,
// or AlignRight.
func (b *Box) SetTitleAlign(align int) *Box {
b.l.Lock()
defer b.l.Unlock()
b.titleAlign = align
return b
}
// Draw draws this primitive onto the screen.
func (b *Box) Draw(screen tcell.Screen) {
b.l.Lock()
// Don't draw anything if there is no space.
if b.width <= 0 || b.height <= 0 {
b.l.Unlock()
return
}
@ -306,7 +376,14 @@ func (b *Box) Draw(screen tcell.Screen) {
if b.border && b.width >= 2 && b.height >= 2 {
border := background.Foreground(b.borderColor) | tcell.Style(b.borderAttributes)
var vertical, horizontal, topLeft, topRight, bottomLeft, bottomRight rune
if b.focus.HasFocus() {
var hasFocus bool
if b.focus == b {
hasFocus = b.hasFocus
} else {
hasFocus = b.focus.HasFocus()
}
if hasFocus {
horizontal = Borders.HorizontalFocus
vertical = Borders.VerticalFocus
topLeft = Borders.TopLeftFocus
@ -347,11 +424,17 @@ func (b *Box) Draw(screen tcell.Screen) {
// Call custom draw function.
if b.draw != nil {
b.innerX, b.innerY, b.innerWidth, b.innerHeight = b.draw(screen, b.x, b.y, b.width, b.height)
b.l.Unlock()
newX, newY, newWidth, newHeight := b.draw(screen, b.x, b.y, b.width, b.height)
b.l.Lock()
b.innerX, b.innerY, b.innerWidth, b.innerHeight = newX, newY, newWidth, newHeight
} else {
// Remember the inner rect.
b.innerX = -1
b.innerX, b.innerY, b.innerWidth, b.innerHeight = b.GetInnerRect()
b.l.Unlock()
newX, newY, newWidth, newHeight := b.GetInnerRect()
b.l.Lock()
b.innerX, b.innerY, b.innerWidth, b.innerHeight = newX, newY, newWidth, newHeight
}
// Clamp inner rect to screen.
@ -376,25 +459,39 @@ func (b *Box) Draw(screen tcell.Screen) {
if b.innerHeight < 0 {
b.innerHeight = 0
}
b.l.Unlock()
}
// Focus is called when this primitive receives focus.
func (b *Box) Focus(delegate func(p Primitive)) {
b.l.Lock()
defer b.l.Unlock()
b.hasFocus = true
}
// Blur is called when this primitive loses focus.
func (b *Box) Blur() {
b.l.Lock()
defer b.l.Unlock()
b.hasFocus = false
}
// HasFocus returns whether or not this primitive has focus.
func (b *Box) HasFocus() bool {
b.l.RLock()
defer b.l.RUnlock()
return b.hasFocus
}
// GetFocusable returns the item's Focusable.
func (b *Box) GetFocusable() Focusable {
b.l.RLock()
defer b.l.RUnlock()
return b.focus
}

View File

@ -1,6 +1,8 @@
package cview
import (
"sync"
"github.com/gdamore/tcell"
)
@ -28,6 +30,8 @@ type Button struct {
// An optional function which is called when the user leaves the button. A
// key is provided indicating which key was pressed to leave (tab or backtab).
blur func(tcell.Key)
sync.Mutex
}
// NewButton returns a new input field.
@ -45,17 +49,26 @@ func NewButton(label string) *Button {
// SetLabel sets the button text.
func (b *Button) SetLabel(label string) *Button {
b.Lock()
defer b.Unlock()
b.label = label
return b
}
// GetLabel returns the button text.
func (b *Button) GetLabel() string {
b.Lock()
defer b.Unlock()
return b.label
}
// SetLabelColor sets the color of the button text.
func (b *Button) SetLabelColor(color tcell.Color) *Button {
b.Lock()
defer b.Unlock()
b.labelColor = color
return b
}
@ -63,6 +76,9 @@ func (b *Button) SetLabelColor(color tcell.Color) *Button {
// SetLabelColorActivated sets the color of the button text when the button is
// in focus.
func (b *Button) SetLabelColorActivated(color tcell.Color) *Button {
b.Lock()
defer b.Unlock()
b.labelColorActivated = color
return b
}
@ -70,12 +86,18 @@ func (b *Button) SetLabelColorActivated(color tcell.Color) *Button {
// SetBackgroundColorActivated sets the background color of the button text when
// the button is in focus.
func (b *Button) SetBackgroundColorActivated(color tcell.Color) *Button {
b.Lock()
defer b.Unlock()
b.backgroundColorActivated = color
return b
}
// SetSelectedFunc sets a handler which is called when the button was selected.
func (b *Button) SetSelectedFunc(handler func()) *Button {
b.Lock()
defer b.Unlock()
b.selected = handler
return b
}
@ -88,12 +110,18 @@ func (b *Button) SetSelectedFunc(handler func()) *Button {
// - KeyTab: Move to the next field.
// - KeyBacktab: Move to the previous field.
func (b *Button) SetBlurFunc(handler func(key tcell.Key)) *Button {
b.Lock()
defer b.Unlock()
b.blur = handler
return b
}
// Draw draws this primitive onto the screen.
func (b *Button) Draw(screen tcell.Screen) {
b.Lock()
defer b.Unlock()
// Draw the box.
borderColor := b.borderColor
backgroundColor := b.backgroundColor
@ -104,7 +132,9 @@ func (b *Button) Draw(screen tcell.Screen) {
b.borderColor = borderColor
}()
}
b.Unlock()
b.Box.Draw(screen)
b.Lock()
b.backgroundColor = backgroundColor
// Draw label.

View File

@ -1,6 +1,8 @@
package cview
import (
"sync"
"github.com/gdamore/tcell"
)
@ -45,6 +47,8 @@ type Checkbox struct {
// A callback function set by the Form class and called when the user leaves
// this form item.
finished func(tcell.Key)
sync.Mutex
}
// NewCheckbox returns a new input field.
@ -59,64 +63,97 @@ func NewCheckbox() *Checkbox {
// SetChecked sets the state of the checkbox.
func (c *Checkbox) SetChecked(checked bool) *Checkbox {
c.Lock()
defer c.Unlock()
c.checked = checked
return c
}
// IsChecked returns whether or not the box is checked.
func (c *Checkbox) IsChecked() bool {
c.Lock()
defer c.Unlock()
return c.checked
}
// SetLabel sets the text to be displayed before the input area.
func (c *Checkbox) SetLabel(label string) *Checkbox {
c.Lock()
defer c.Unlock()
c.label = label
return c
}
// GetLabel returns the text to be displayed before the input area.
func (c *Checkbox) GetLabel() string {
c.Lock()
defer c.Unlock()
return c.label
}
// SetMessage sets the text to be displayed after the checkbox
func (c *Checkbox) SetMessage(message string) *Checkbox {
c.Lock()
defer c.Unlock()
c.message = message
return c
}
// GetMessage returns the text to be displayed after the checkbox
func (c *Checkbox) GetMessage() string {
c.Lock()
defer c.Unlock()
return c.message
}
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
// primitive to use the width of the label string.
func (c *Checkbox) SetLabelWidth(width int) *Checkbox {
c.Lock()
defer c.Unlock()
c.labelWidth = width
return c
}
// SetLabelColor sets the color of the label.
func (c *Checkbox) SetLabelColor(color tcell.Color) *Checkbox {
c.Lock()
defer c.Unlock()
c.labelColor = color
return c
}
// SetFieldBackgroundColor sets the background color of the input area.
func (c *Checkbox) SetFieldBackgroundColor(color tcell.Color) *Checkbox {
c.Lock()
defer c.Unlock()
c.fieldBackgroundColor = color
return c
}
// SetFieldTextColor sets the text color of the input area.
func (c *Checkbox) SetFieldTextColor(color tcell.Color) *Checkbox {
c.Lock()
defer c.Unlock()
c.fieldTextColor = color
return c
}
// SetFormAttributes sets attributes shared by all form items.
func (c *Checkbox) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
c.Lock()
defer c.Unlock()
c.labelWidth = labelWidth
c.labelColor = labelColor
c.backgroundColor = bgColor
@ -127,6 +164,9 @@ func (c *Checkbox) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldT
// GetFieldWidth returns this primitive's field width.
func (c *Checkbox) GetFieldWidth() int {
c.Lock()
defer c.Unlock()
if c.message == "" {
return 1
}
@ -138,6 +178,9 @@ func (c *Checkbox) GetFieldWidth() int {
// checkbox was changed by the user. The handler function receives the new
// state.
func (c *Checkbox) SetChangedFunc(handler func(checked bool)) *Checkbox {
c.Lock()
defer c.Unlock()
c.changed = handler
return c
}
@ -150,12 +193,18 @@ func (c *Checkbox) SetChangedFunc(handler func(checked bool)) *Checkbox {
// - KeyTab: Move to the next field.
// - KeyBacktab: Move to the previous field.
func (c *Checkbox) SetDoneFunc(handler func(key tcell.Key)) *Checkbox {
c.Lock()
defer c.Unlock()
c.done = handler
return c
}
// SetFinishedFunc sets a callback invoked when the user leaves this form item.
func (c *Checkbox) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
c.Lock()
defer c.Unlock()
c.finished = handler
return c
}
@ -164,6 +213,9 @@ func (c *Checkbox) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
func (c *Checkbox) Draw(screen tcell.Screen) {
c.Box.Draw(screen)
c.Lock()
defer c.Unlock()
// Prepare
x, y, width, height := c.GetInnerRect()
rightLimit := x + width
@ -209,7 +261,9 @@ func (c *Checkbox) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
if key == tcell.KeyRune && event.Rune() != ' ' {
break
}
c.Lock()
c.checked = !c.checked
c.Unlock()
if c.changed != nil {
c.changed(c.checked)
}
@ -229,7 +283,9 @@ func (c *Checkbox) MouseHandler() func(event *EventMouse) {
return c.WrapMouseHandler(func(event *EventMouse) {
// Process mouse event.
if event.Action()&MouseClick != 0 {
c.Lock()
c.checked = !c.checked
c.Unlock()
if c.changed != nil {
c.changed(c.checked)
}

View File

@ -13,7 +13,11 @@ the following shortcuts can be used:
package main
import (
"flag"
"fmt"
"log"
"net/http"
_ "net/http/pprof"
"strconv"
"github.com/gdamore/tcell"
@ -30,6 +34,16 @@ var app = cview.NewApplication()
// Starting point for the presentation.
func main() {
var debugPort int
flag.IntVar(&debugPort, "debug", 0, "port to serve debug info")
flag.Parse()
if debugPort > 0 {
go func() {
log.Println(http.ListenAndServe(fmt.Sprintf("localhost:%d", debugPort), nil))
}()
}
// The presentation slides.
slides := []Slide{
Cover,

34
doc.go
View File

@ -34,26 +34,18 @@ primitive, Box, and thus inherit its functions. This isn't necessarily
required, but it makes more sense than reimplementing Box's functionality in
each widget.
Types
This package is a fork of https://github.com/rivo/tview which is based on
https://github.com/gdamore/tcell. It uses types and constants from tcell
(e.g. colors and keyboard values).
Concurrency
Most of cview's functions are not thread-safe. You must synchronize execution
via Application.QueueUpdate or Application.QueueUpdateDraw (see function
documentation for more information):
go func() {
// Queue a UI change from a goroutine.
app.QueueUpdateDraw(func() {
// This function will execute on the main thread.
table.SetCellSimple(0, 0, "Foo bar")
})
}()
One exception to this is the io.Writer interface implemented by TextView; you
may safely write to a TextView from any goroutine. You may also call
Application.Draw from any goroutine.
Event handlers execute on the main goroutine and thus do not require
synchronization.
All functions may be called concurrently (they are thread-safe). When called
from multiple threads, functions will block until the application or widget
becomes available. Function calls may be queued with Application.QueueUpdate to
avoid blocking.
Unicode Support
@ -80,12 +72,6 @@ developers to permanently intercept mouse events.
Event handlers may return nil to stop propagation.
Types
This package is a fork of https://github.com/rivo/tview which is based on
https://github.com/gdamore/tcell. It uses types and constants from tcell
(e.g. colors and keyboard values).
Colors
Throughout this package, colors are specified using the tcell.Color type.

View File

@ -2,6 +2,7 @@ package cview
import (
"strings"
"sync"
"github.com/gdamore/tcell"
)
@ -79,6 +80,8 @@ type DropDown struct {
// A callback function which is called when the user changes the drop-down's
// selection.
selected func(text string, index int)
sync.RWMutex
}
// NewDropDown returns a new drop-down.
@ -110,20 +113,29 @@ func NewDropDown() *DropDown {
// be a negative value to indicate that no option is currently selected. Calling
// this function will also trigger the "selected" callback (if there is one).
func (d *DropDown) SetCurrentOption(index int) *DropDown {
d.Lock()
defer d.Unlock()
if index >= 0 && index < len(d.options) {
d.currentOption = index
d.list.SetCurrentItem(index)
if d.selected != nil {
d.Unlock()
d.selected(d.options[index].Text, index)
d.Lock()
}
if d.options[index].Selected != nil {
d.Unlock()
d.options[index].Selected()
d.Lock()
}
} else {
d.currentOption = -1
d.list.SetCurrentItem(0) // Set to 0 because -1 means "last item".
if d.selected != nil {
d.Unlock()
d.selected("", -1)
d.Lock()
}
}
return d
@ -132,6 +144,9 @@ func (d *DropDown) SetCurrentOption(index int) *DropDown {
// GetCurrentOption returns the index of the currently selected option as well
// as its text. If no option was selected, -1 and an empty string is returned.
func (d *DropDown) GetCurrentOption() (int, string) {
d.RLock()
defer d.RUnlock()
var text string
if d.currentOption >= 0 && d.currentOption < len(d.options) {
text = d.options[d.currentOption].Text
@ -145,6 +160,9 @@ func (d *DropDown) GetCurrentOption() (int, string) {
// displayed when no option is currently selected. Per default, all of these
// strings are empty.
func (d *DropDown) SetTextOptions(prefix, suffix, currentPrefix, currentSuffix, noSelection string) *DropDown {
d.Lock()
defer d.Unlock()
d.currentOptionPrefix = currentPrefix
d.currentOptionSuffix = currentSuffix
d.noSelection = noSelection
@ -158,36 +176,54 @@ func (d *DropDown) SetTextOptions(prefix, suffix, currentPrefix, currentSuffix,
// SetLabel sets the text to be displayed before the input area.
func (d *DropDown) SetLabel(label string) *DropDown {
d.Lock()
defer d.Unlock()
d.label = label
return d
}
// GetLabel returns the text to be displayed before the input area.
func (d *DropDown) GetLabel() string {
d.RLock()
defer d.RUnlock()
return d.label
}
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
// primitive to use the width of the label string.
func (d *DropDown) SetLabelWidth(width int) *DropDown {
d.Lock()
defer d.Unlock()
d.labelWidth = width
return d
}
// SetLabelColor sets the color of the label.
func (d *DropDown) SetLabelColor(color tcell.Color) *DropDown {
d.Lock()
defer d.Unlock()
d.labelColor = color
return d
}
// SetFieldBackgroundColor sets the background color of the options area.
func (d *DropDown) SetFieldBackgroundColor(color tcell.Color) *DropDown {
d.Lock()
defer d.Unlock()
d.fieldBackgroundColor = color
return d
}
// SetFieldTextColor sets the text color of the options area.
func (d *DropDown) SetFieldTextColor(color tcell.Color) *DropDown {
d.Lock()
defer d.Unlock()
d.fieldTextColor = color
return d
}
@ -196,12 +232,18 @@ func (d *DropDown) SetFieldTextColor(color tcell.Color) *DropDown {
// shown when the user starts typing text, which directly selects the first
// option that starts with the typed string.
func (d *DropDown) SetPrefixTextColor(color tcell.Color) *DropDown {
d.Lock()
defer d.Unlock()
d.prefixTextColor = color
return d
}
// SetFormAttributes sets attributes shared by all form items.
func (d *DropDown) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
d.Lock()
defer d.Unlock()
d.labelWidth = labelWidth
d.labelColor = labelColor
d.backgroundColor = bgColor
@ -213,12 +255,18 @@ func (d *DropDown) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldT
// SetFieldWidth sets the screen width of the options area. A value of 0 means
// extend to as long as the longest option text.
func (d *DropDown) SetFieldWidth(width int) *DropDown {
d.Lock()
defer d.Unlock()
d.fieldWidth = width
return d
}
// GetFieldWidth returns this primitive's field screen width.
func (d *DropDown) GetFieldWidth() int {
d.RLock()
defer d.RUnlock()
if d.fieldWidth > 0 {
return d.fieldWidth
}
@ -235,6 +283,13 @@ func (d *DropDown) GetFieldWidth() int {
// AddOption adds a new selectable option to this drop-down. The "selected"
// callback is called when this option was selected. It may be nil.
func (d *DropDown) AddOption(text string, selected func()) *DropDown {
d.Lock()
defer d.Unlock()
return d.addOption(text, selected)
}
func (d *DropDown) addOption(text string, selected func()) *DropDown {
d.options = append(d.options, &dropDownOption{Text: text, Selected: selected})
d.list.AddItem(d.optionPrefix+text+d.optionSuffix, "", 0, nil)
return d
@ -245,11 +300,14 @@ func (d *DropDown) AddOption(text string, selected func()) *DropDown {
// It will be called with the option's text and its index into the options
// slice. The "selected" parameter may be nil.
func (d *DropDown) SetOptions(texts []string, selected func(text string, index int)) *DropDown {
d.Lock()
defer d.Unlock()
d.list.Clear()
d.options = nil
for index, text := range texts {
func(t string, i int) {
d.AddOption(text, nil)
d.addOption(text, nil)
}(text, index)
}
d.selected = selected
@ -262,6 +320,9 @@ func (d *DropDown) SetOptions(texts []string, selected func(text string, index i
// selected option's text and index. If "no option" was selected, these values
// are an empty string and -1.
func (d *DropDown) SetSelectedFunc(handler func(text string, index int)) *DropDown {
d.Lock()
defer d.Unlock()
d.selected = handler
return d
}
@ -274,12 +335,18 @@ func (d *DropDown) SetSelectedFunc(handler func(text string, index int)) *DropDo
// - KeyTab: Move to the next field.
// - KeyBacktab: Move to the previous field.
func (d *DropDown) SetDoneFunc(handler func(key tcell.Key)) *DropDown {
d.Lock()
defer d.Unlock()
d.done = handler
return d
}
// SetFinishedFunc sets a callback invoked when the user leaves this form item.
func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
d.Lock()
defer d.Unlock()
d.finished = handler
return d
}
@ -287,6 +354,10 @@ func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
// Draw draws this primitive onto the screen.
func (d *DropDown) Draw(screen tcell.Screen) {
d.Box.Draw(screen)
hasFocus := d.GetFocusable().HasFocus()
d.Lock()
defer d.Unlock()
// Prepare.
x, y, width, height := d.GetInnerRect()
@ -338,7 +409,7 @@ func (d *DropDown) Draw(screen tcell.Screen) {
fieldWidth = rightLimit - x
}
fieldStyle := tcell.StyleDefault.Background(d.fieldBackgroundColor)
if d.GetFocusable().HasFocus() && !d.open {
if hasFocus && !d.open {
fieldStyle = fieldStyle.Background(d.fieldTextColor)
}
for index := 0; index < fieldWidth; index++ {
@ -363,14 +434,14 @@ func (d *DropDown) Draw(screen tcell.Screen) {
text = d.currentOptionPrefix + d.options[d.currentOption].Text + d.currentOptionSuffix
}
// Just show the current selection.
if d.GetFocusable().HasFocus() && !d.open {
if hasFocus && !d.open {
color = d.fieldBackgroundColor
}
Print(screen, text, x, y, fieldWidth, AlignLeft, color)
}
// Draw options list.
if d.HasFocus() && d.open {
if hasFocus && d.open {
// We prefer to drop down but if there is no space, maybe drop up?
lx := x
ly := y + 1
@ -400,6 +471,9 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
// Process key event.
switch key := event.Key(); key {
case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown:
d.Lock()
defer d.Unlock()
d.prefix = ""
// If the first key was a letter already, it becomes part of the prefix.
@ -447,10 +521,14 @@ func (d *DropDown) openList(setFocus func(Primitive), app *Application) {
// Trigger "selected" event.
if d.selected != nil {
d.Unlock()
d.selected(d.options[d.currentOption].Text, d.currentOption)
d.Lock()
}
if d.options[d.currentOption].Selected != nil {
d.Unlock()
d.options[d.currentOption].Selected()
d.Lock()
}
}).SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyRune {
@ -524,6 +602,9 @@ func (d *DropDown) Focus(delegate func(p Primitive)) {
// HasFocus returns whether or not this primitive has focus.
func (d *DropDown) HasFocus() bool {
d.RLock()
defer d.RUnlock()
if d.open {
return d.list.HasFocus()
}
@ -535,6 +616,9 @@ func (d *DropDown) MouseHandler() func(event *EventMouse) {
return d.WrapMouseHandler(func(event *EventMouse) {
// Process mouse event.
if event.Action()&MouseDown != 0 && event.Buttons()&tcell.Button1 != 0 {
d.Lock()
defer d.Unlock()
//d.open = !d.open
//event.SetFocus(d)
if d.open {

33
flex.go
View File

@ -1,6 +1,8 @@
package cview
import (
"sync"
"github.com/gdamore/tcell"
)
@ -36,6 +38,8 @@ type Flex struct {
// If set to true, Flex will use the entire screen as its available space
// instead its box dimensions.
fullScreen bool
sync.Mutex
}
// NewFlex returns a new flexbox layout container with no primitives and its
@ -60,6 +64,9 @@ func NewFlex() *Flex {
// SetDirection sets the direction in which the contained primitives are
// distributed. This can be either FlexColumn (default) or FlexRow.
func (f *Flex) SetDirection(direction int) *Flex {
f.Lock()
defer f.Unlock()
f.direction = direction
return f
}
@ -67,6 +74,9 @@ func (f *Flex) SetDirection(direction int) *Flex {
// SetFullScreen sets the flag which, when true, causes the flex layout to use
// the entire screen space instead of whatever size it is currently assigned to.
func (f *Flex) SetFullScreen(fullScreen bool) *Flex {
f.Lock()
defer f.Unlock()
f.fullScreen = fullScreen
return f
}
@ -86,6 +96,9 @@ func (f *Flex) SetFullScreen(fullScreen bool) *Flex {
// You can provide a nil value for the primitive. This will still consume screen
// space but nothing will be drawn.
func (f *Flex) AddItem(item Primitive, fixedSize, proportion int, focus bool) *Flex {
f.Lock()
defer f.Unlock()
f.items = append(f.items, &flexItem{Item: item, FixedSize: fixedSize, Proportion: proportion, Focus: focus})
return f
}
@ -93,6 +106,9 @@ func (f *Flex) AddItem(item Primitive, fixedSize, proportion int, focus bool) *F
// RemoveItem removes all items for the given primitive from the container,
// keeping the order of the remaining items intact.
func (f *Flex) RemoveItem(p Primitive) *Flex {
f.Lock()
defer f.Unlock()
for index := len(f.items) - 1; index >= 0; index-- {
if f.items[index].Item == p {
f.items = append(f.items[:index], f.items[index+1:]...)
@ -105,6 +121,9 @@ func (f *Flex) RemoveItem(p Primitive) *Flex {
// are multiple Flex items with the same primitive, they will all receive the
// same size. For details regarding the size parameters, see AddItem().
func (f *Flex) ResizeItem(p Primitive, fixedSize, proportion int) *Flex {
f.Lock()
defer f.Unlock()
for _, item := range f.items {
if item.Item == p {
item.FixedSize = fixedSize
@ -118,6 +137,9 @@ func (f *Flex) ResizeItem(p Primitive, fixedSize, proportion int) *Flex {
func (f *Flex) Draw(screen tcell.Screen) {
f.Box.Draw(screen)
f.Lock()
defer f.Unlock()
// Calculate size and position of the items.
// Do we use the entire screen?
@ -178,16 +200,24 @@ func (f *Flex) Draw(screen tcell.Screen) {
// Focus is called when this primitive receives focus.
func (f *Flex) Focus(delegate func(p Primitive)) {
f.Lock()
for _, item := range f.items {
if item.Item != nil && item.Focus {
f.Unlock()
delegate(item.Item)
return
}
}
f.Unlock()
}
// HasFocus returns whether or not this primitive has focus.
func (f *Flex) HasFocus() bool {
f.Lock()
defer f.Unlock()
for _, item := range f.items {
if item.Item != nil && item.Item.GetFocusable().HasFocus() {
return true
@ -198,6 +228,9 @@ func (f *Flex) HasFocus() bool {
// GetChildren returns all primitives that have been added.
func (f *Flex) GetChildren() []Primitive {
f.Lock()
defer f.Unlock()
children := make([]Primitive, len(f.items))
for i, item := range f.items {
children[i] = item.Item

116
form.go
View File

@ -1,6 +1,8 @@
package cview
import (
"sync"
"github.com/gdamore/tcell"
)
@ -83,6 +85,8 @@ type Form struct {
// An optional function which is called when the user hits Escape.
cancel func()
sync.Mutex
}
// NewForm returns a new form.
@ -108,6 +112,9 @@ func NewForm() *Form {
// layouts and the number of empty cells between form items for horizontal
// layouts.
func (f *Form) SetItemPadding(padding int) *Form {
f.Lock()
defer f.Unlock()
f.itemPadding = padding
return f
}
@ -117,24 +124,36 @@ func (f *Form) SetItemPadding(padding int) *Form {
// positioned from left to right, moving into the next row if there is not
// enough space.
func (f *Form) SetHorizontal(horizontal bool) *Form {
f.Lock()
defer f.Unlock()
f.horizontal = horizontal
return f
}
// SetLabelColor sets the color of the labels.
func (f *Form) SetLabelColor(color tcell.Color) *Form {
f.Lock()
defer f.Unlock()
f.labelColor = color
return f
}
// SetFieldBackgroundColor sets the background color of the input areas.
func (f *Form) SetFieldBackgroundColor(color tcell.Color) *Form {
f.Lock()
defer f.Unlock()
f.fieldBackgroundColor = color
return f
}
// SetFieldTextColor sets the text color of the input areas.
func (f *Form) SetFieldTextColor(color tcell.Color) *Form {
f.Lock()
defer f.Unlock()
f.fieldTextColor = color
return f
}
@ -142,18 +161,27 @@ func (f *Form) SetFieldTextColor(color tcell.Color) *Form {
// SetButtonsAlign sets how the buttons align horizontally, one of AlignLeft
// (the default), AlignCenter, and AlignRight. This is only
func (f *Form) SetButtonsAlign(align int) *Form {
f.Lock()
defer f.Unlock()
f.buttonsAlign = align
return f
}
// SetButtonBackgroundColor sets the background color of the buttons.
func (f *Form) SetButtonBackgroundColor(color tcell.Color) *Form {
f.Lock()
defer f.Unlock()
f.buttonBackgroundColor = color
return f
}
// SetButtonTextColor sets the color of the button texts.
func (f *Form) SetButtonTextColor(color tcell.Color) *Form {
f.Lock()
defer f.Unlock()
f.buttonTextColor = color
return f
}
@ -162,6 +190,9 @@ func (f *Form) SetButtonTextColor(color tcell.Color) *Form {
// non-button items first and buttons last. Note that this index is only used
// when the form itself receives focus.
func (f *Form) SetFocus(index int) *Form {
f.Lock()
defer f.Unlock()
if index < 0 {
f.focusedElement = 0
} else if index >= len(f.items)+len(f.buttons) {
@ -178,6 +209,9 @@ func (f *Form) SetFocus(index int) *Form {
// accept any text), and an (optional) callback function which is invoked when
// the input field's text has changed.
func (f *Form) AddInputField(label, value string, fieldWidth int, accept func(textToCheck string, lastChar rune) bool, changed func(text string)) *Form {
f.Lock()
defer f.Unlock()
f.items = append(f.items, NewInputField().
SetLabel(label).
SetText(value).
@ -194,6 +228,9 @@ func (f *Form) AddInputField(label, value string, fieldWidth int, accept func(te
// (optional) callback function which is invoked when the input field's text has
// changed.
func (f *Form) AddPasswordField(label, value string, fieldWidth int, mask rune, changed func(text string)) *Form {
f.Lock()
defer f.Unlock()
if mask == 0 {
mask = '*'
}
@ -211,6 +248,9 @@ func (f *Form) AddPasswordField(label, value string, fieldWidth int, mask rune,
// selected. The initial option may be a negative value to indicate that no
// option is currently selected.
func (f *Form) AddDropDown(label string, options []string, initialOption int, selected func(option string, optionIndex int)) *Form {
f.Lock()
defer f.Unlock()
f.items = append(f.items, NewDropDown().
SetLabel(label).
SetOptions(options, selected).
@ -222,6 +262,9 @@ func (f *Form) AddDropDown(label string, options []string, initialOption int, se
// initial state, and an (optional) callback function which is invoked when the
// state of the checkbox was changed by the user.
func (f *Form) AddCheckbox(label string, message string, checked bool, changed func(checked bool)) *Form {
f.Lock()
defer f.Unlock()
f.items = append(f.items, NewCheckbox().
SetLabel(label).
SetMessage(message).
@ -233,6 +276,9 @@ func (f *Form) AddCheckbox(label string, message string, checked bool, changed f
// AddButton adds a new button to the form. The "selected" function is called
// when the user selects this button. It may be nil.
func (f *Form) AddButton(label string, selected func()) *Form {
f.Lock()
defer f.Unlock()
f.buttons = append(f.buttons, NewButton(label).SetSelectedFunc(selected))
return f
}
@ -241,18 +287,27 @@ func (f *Form) AddButton(label string, selected func()) *Form {
// buttons have been specially prepared for this form and modifying some of
// their attributes may have unintended side effects.
func (f *Form) GetButton(index int) *Button {
f.Lock()
defer f.Unlock()
return f.buttons[index]
}
// RemoveButton removes the button at the specified position, starting with 0
// for the button that was added first.
func (f *Form) RemoveButton(index int) *Form {
f.Lock()
defer f.Unlock()
f.buttons = append(f.buttons[:index], f.buttons[index+1:]...)
return f
}
// GetButtonCount returns the number of buttons in this form.
func (f *Form) GetButtonCount() int {
f.Lock()
defer f.Unlock()
return len(f.buttons)
}
@ -260,6 +315,9 @@ func (f *Form) GetButtonCount() int {
// with 0 for the button that was added first. If no such label was found, -1
// is returned.
func (f *Form) GetButtonIndex(label string) int {
f.Lock()
defer f.Unlock()
for index, button := range f.buttons {
if button.GetLabel() == label {
return index
@ -271,6 +329,9 @@ func (f *Form) GetButtonIndex(label string) int {
// Clear removes all input elements from the form, including the buttons if
// specified.
func (f *Form) Clear(includeButtons bool) *Form {
f.Lock()
defer f.Unlock()
f.items = nil
if includeButtons {
f.ClearButtons()
@ -281,6 +342,9 @@ func (f *Form) Clear(includeButtons bool) *Form {
// ClearButtons removes all buttons from the form.
func (f *Form) ClearButtons() *Form {
f.Lock()
defer f.Unlock()
f.buttons = nil
return f
}
@ -296,6 +360,9 @@ func (f *Form) ClearButtons() *Form {
// - The field text color
// - The field background color
func (f *Form) AddFormItem(item FormItem) *Form {
f.Lock()
defer f.Unlock()
f.items = append(f.items, item)
return f
}
@ -303,6 +370,9 @@ func (f *Form) AddFormItem(item FormItem) *Form {
// GetFormItemCount returns the number of items in the form (not including the
// buttons).
func (f *Form) GetFormItemCount() int {
f.Lock()
defer f.Unlock()
return len(f.items)
}
@ -310,6 +380,9 @@ func (f *Form) GetFormItemCount() int {
// 0. Elements are referenced in the order they were added. Buttons are not
// included.
func (f *Form) GetFormItem(index int) FormItem {
f.Lock()
defer f.Unlock()
return f.items[index]
}
@ -317,6 +390,9 @@ func (f *Form) GetFormItem(index int) FormItem {
// index 0. Elements are referenced in the order they were added. Buttons are
// not included.
func (f *Form) RemoveFormItem(index int) *Form {
f.Lock()
defer f.Unlock()
f.items = append(f.items[:index], f.items[index+1:]...)
return f
}
@ -325,6 +401,9 @@ func (f *Form) RemoveFormItem(index int) *Form {
// no such element is found, nil is returned. Buttons are not searched and will
// therefore not be returned.
func (f *Form) GetFormItemByLabel(label string) FormItem {
f.Lock()
defer f.Unlock()
for _, item := range f.items {
if item.GetLabel() == label {
return item
@ -337,6 +416,9 @@ func (f *Form) GetFormItemByLabel(label string) FormItem {
// label. If no such element is found, -1 is returned. Buttons are not searched
// and will therefore not be returned.
func (f *Form) GetFormItemIndex(label string) int {
f.Lock()
defer f.Unlock()
for index, item := range f.items {
if item.GetLabel() == label {
return index
@ -348,6 +430,9 @@ func (f *Form) GetFormItemIndex(label string) int {
// GetFocusedItemIndex returns the indices of the form element or button which
// currently has focus. If they don't, -1 is returned resepectively.
func (f *Form) GetFocusedItemIndex() (formItem, button int) {
f.Lock()
defer f.Unlock()
index := f.focusIndex()
if index < 0 {
return -1, -1
@ -364,6 +449,9 @@ func (f *Form) GetFocusedItemIndex() (formItem, button int) {
// false, the selection won't change when navigating downwards on the last item
// or navigating upwards on the first item.
func (f *Form) SetWrapAround(wrapAround bool) *Form {
f.Lock()
defer f.Unlock()
f.wrapAround = wrapAround
return f
}
@ -371,6 +459,9 @@ func (f *Form) SetWrapAround(wrapAround bool) *Form {
// SetCancelFunc sets a handler which is called when the user hits the Escape
// key.
func (f *Form) SetCancelFunc(callback func()) *Form {
f.Lock()
defer f.Unlock()
f.cancel = callback
return f
}
@ -379,6 +470,9 @@ func (f *Form) SetCancelFunc(callback func()) *Form {
func (f *Form) Draw(screen tcell.Screen) {
f.Box.Draw(screen)
f.Lock()
defer f.Unlock()
// Determine the actual item that has focus.
if index := f.focusIndex(); index >= 0 {
f.focusedElement = index
@ -565,8 +659,10 @@ func (f *Form) Draw(screen tcell.Screen) {
// Focus is called by the application when the primitive receives focus.
func (f *Form) Focus(delegate func(p Primitive)) {
f.Lock()
if len(f.items)+len(f.buttons) == 0 {
f.hasFocus = true
f.Unlock()
return
}
f.hasFocus = false
@ -576,13 +672,17 @@ func (f *Form) Focus(delegate func(p Primitive)) {
f.focusedElement = 0
}
handler := func(key tcell.Key) {
f.Lock()
switch key {
case tcell.KeyTab, tcell.KeyEnter:
f.focusedElement++
if !f.wrapAround && f.focusedElement >= len(f.items)+len(f.buttons) {
f.focusedElement = (len(f.items) + len(f.buttons)) - 1
}
f.Unlock()
f.Focus(delegate)
f.Lock()
case tcell.KeyBacktab:
f.focusedElement--
if f.focusedElement < 0 {
@ -592,32 +692,45 @@ func (f *Form) Focus(delegate func(p Primitive)) {
f.focusedElement = 0
}
}
f.Unlock()
f.Focus(delegate)
f.Lock()
case tcell.KeyEscape:
if f.cancel != nil {
f.Unlock()
f.cancel()
f.Lock()
} else {
f.focusedElement = 0
f.Unlock()
f.Focus(delegate)
f.Lock()
}
}
f.Unlock()
}
if f.focusedElement < len(f.items) {
// We're selecting an item.
item := f.items[f.focusedElement]
item.SetFinishedFunc(handler)
f.Unlock()
delegate(item)
} else {
// We're selecting a button.
button := f.buttons[f.focusedElement-len(f.items)]
button.SetBlurFunc(handler)
f.Unlock()
delegate(button)
}
}
// HasFocus returns whether or not this primitive has focus.
func (f *Form) HasFocus() bool {
f.Lock()
defer f.Unlock()
if f.hasFocus {
return true
}
@ -643,6 +756,9 @@ func (f *Form) focusIndex() int {
// GetChildren returns all primitives that have been added.
func (f *Form) GetChildren() []Primitive {
f.Lock()
defer f.Unlock()
children := make([]Primitive, len(f.items)+len(f.buttons))
i := 0
for _, item := range f.items {

View File

@ -1,6 +1,8 @@
package cview
import (
"sync"
"github.com/gdamore/tcell"
)
@ -27,6 +29,8 @@ type Frame struct {
// Border spacing.
top, bottom, header, footer, left, right int
sync.Mutex
}
// NewFrame returns a new frame around the given primitive. The primitive's
@ -57,6 +61,9 @@ func NewFrame(primitive Primitive) *Frame {
// the footer are printed bottom to top. Note that long text can overlap as
// different alignments will be placed on the same row.
func (f *Frame) AddText(text string, header bool, align int, color tcell.Color) *Frame {
f.Lock()
defer f.Unlock()
f.text = append(f.text, &frameText{
Text: text,
Header: header,
@ -68,6 +75,9 @@ func (f *Frame) AddText(text string, header bool, align int, color tcell.Color)
// Clear removes all text from the frame.
func (f *Frame) Clear() *Frame {
f.Lock()
defer f.Unlock()
f.text = nil
return f
}
@ -76,6 +86,9 @@ func (f *Frame) Clear() *Frame {
// "footer", the vertical space between the header and footer text and the
// contained primitive (does not apply if there is no text).
func (f *Frame) SetBorders(top, bottom, header, footer, left, right int) *Frame {
f.Lock()
defer f.Unlock()
f.top, f.bottom, f.header, f.footer, f.left, f.right = top, bottom, header, footer, left, right
return f
}
@ -84,6 +97,9 @@ func (f *Frame) SetBorders(top, bottom, header, footer, left, right int) *Frame
func (f *Frame) Draw(screen tcell.Screen) {
f.Box.Draw(screen)
f.Lock()
defer f.Unlock()
// Calculate start positions.
x, top, width, height := f.GetInnerRect()
bottom := top + height - 1
@ -144,11 +160,18 @@ func (f *Frame) Draw(screen tcell.Screen) {
// Focus is called when this primitive receives focus.
func (f *Frame) Focus(delegate func(p Primitive)) {
delegate(f.primitive)
f.Lock()
primitive := f.primitive
defer f.Unlock()
delegate(primitive)
}
// HasFocus returns whether or not this primitive has focus.
func (f *Frame) HasFocus() bool {
f.Lock()
defer f.Unlock()
focusable, ok := f.primitive.(Focusable)
if ok {
return focusable.HasFocus()
@ -158,5 +181,8 @@ func (f *Frame) HasFocus() bool {
// GetChildren returns all primitives that have been added.
func (f *Frame) GetChildren() []Primitive {
f.Lock()
defer f.Unlock()
return []Primitive{f.primitive}
}

64
grid.go
View File

@ -2,6 +2,7 @@ package cview
import (
"math"
"sync"
"github.com/gdamore/tcell"
)
@ -56,6 +57,8 @@ type Grid struct {
// The color of the borders around grid items.
bordersColor tcell.Color
sync.Mutex
}
// NewGrid returns a new grid-based layout container with no initial primitives.
@ -105,6 +108,9 @@ func NewGrid() *Grid {
// The resulting widths would be: 30, 15, 15, 15, 20, 15, and 15 cells, a total
// of 125 cells, 25 cells wider than the available grid width.
func (g *Grid) SetColumns(columns ...int) *Grid {
g.Lock()
defer g.Unlock()
g.columns = columns
return g
}
@ -116,6 +122,9 @@ func (g *Grid) SetColumns(columns ...int) *Grid {
// The provided values correspond to row heights, the first value defining
// the height of the topmost row.
func (g *Grid) SetRows(rows ...int) *Grid {
g.Lock()
defer g.Unlock()
g.rows = rows
return g
}
@ -123,6 +132,9 @@ func (g *Grid) SetRows(rows ...int) *Grid {
// SetSize is a shortcut for SetRows() and SetColumns() where all row and column
// values are set to the given size values. See SetColumns() for details on sizes.
func (g *Grid) SetSize(numRows, numColumns, rowSize, columnSize int) *Grid {
g.Lock()
defer g.Unlock()
g.rows = make([]int, numRows)
for index := range g.rows {
g.rows[index] = rowSize
@ -137,6 +149,9 @@ func (g *Grid) SetSize(numRows, numColumns, rowSize, columnSize int) *Grid {
// SetMinSize sets an absolute minimum width for rows and an absolute minimum
// height for columns. Panics if negative values are provided.
func (g *Grid) SetMinSize(row, column int) *Grid {
g.Lock()
defer g.Unlock()
if row < 0 || column < 0 {
panic("Invalid minimum row/column size")
}
@ -148,6 +163,9 @@ func (g *Grid) SetMinSize(row, column int) *Grid {
// If borders are drawn (see SetBorders()), these values are ignored and a gap
// of 1 is assumed. Panics if negative values are provided.
func (g *Grid) SetGap(row, column int) *Grid {
g.Lock()
defer g.Unlock()
if row < 0 || column < 0 {
panic("Invalid gap size")
}
@ -159,12 +177,18 @@ func (g *Grid) SetGap(row, column int) *Grid {
// this value to true will cause the gap values (see SetGap()) to be ignored and
// automatically assumed to be 1 where the border graphics are drawn.
func (g *Grid) SetBorders(borders bool) *Grid {
g.Lock()
defer g.Unlock()
g.borders = borders
return g
}
// SetBordersColor sets the color of the item borders.
func (g *Grid) SetBordersColor(color tcell.Color) *Grid {
g.Lock()
defer g.Unlock()
g.bordersColor = color
return g
}
@ -196,6 +220,9 @@ func (g *Grid) SetBordersColor(color tcell.Color) *Grid {
// receives focus. If there are multiple items with a true focus flag, the last
// visible one that was added will receive focus.
func (g *Grid) AddItem(p Primitive, row, column, rowSpan, colSpan, minGridHeight, minGridWidth int, focus bool) *Grid {
g.Lock()
defer g.Unlock()
g.items = append(g.items, &gridItem{
Item: p,
Row: row,
@ -212,6 +239,9 @@ func (g *Grid) AddItem(p Primitive, row, column, rowSpan, colSpan, minGridHeight
// RemoveItem removes all items for the given primitive from the grid, keeping
// the order of the remaining items intact.
func (g *Grid) RemoveItem(p Primitive) *Grid {
g.Lock()
defer g.Unlock()
for index := len(g.items) - 1; index >= 0; index-- {
if g.items[index].Item == p {
g.items = append(g.items[:index], g.items[index+1:]...)
@ -222,6 +252,9 @@ func (g *Grid) RemoveItem(p Primitive) *Grid {
// Clear removes all items from the grid.
func (g *Grid) Clear() *Grid {
g.Lock()
defer g.Unlock()
g.items = nil
return g
}
@ -232,6 +265,9 @@ func (g *Grid) Clear() *Grid {
// the grid is drawn. The actual position of the grid may also be adjusted such
// that contained primitives that have focus remain visible.
func (g *Grid) SetOffset(rows, columns int) *Grid {
g.Lock()
defer g.Unlock()
g.rowOffset, g.columnOffset = rows, columns
return g
}
@ -239,27 +275,43 @@ func (g *Grid) SetOffset(rows, columns int) *Grid {
// GetOffset returns the current row and column offset (see SetOffset() for
// details).
func (g *Grid) GetOffset() (rows, columns int) {
g.Lock()
defer g.Unlock()
return g.rowOffset, g.columnOffset
}
// Focus is called when this primitive receives focus.
func (g *Grid) Focus(delegate func(p Primitive)) {
for _, item := range g.items {
g.Lock()
items := g.items
g.Unlock()
for _, item := range items {
if item.Focus {
delegate(item.Item)
return
}
}
g.Lock()
g.hasFocus = true
g.Unlock()
}
// Blur is called when this primitive loses focus.
func (g *Grid) Blur() {
g.Lock()
defer g.Unlock()
g.hasFocus = false
}
// HasFocus returns whether or not this primitive has focus.
func (g *Grid) HasFocus() bool {
g.Lock()
defer g.Unlock()
for _, item := range g.items {
if item.visible && item.Item.GetFocusable().HasFocus() {
return true
@ -271,6 +323,9 @@ func (g *Grid) HasFocus() bool {
// InputHandler returns the handler for this primitive.
func (g *Grid) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return g.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
g.Lock()
defer g.Unlock()
switch event.Key() {
case tcell.KeyRune:
switch event.Rune() {
@ -306,6 +361,10 @@ func (g *Grid) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
// Draw draws this primitive onto the screen.
func (g *Grid) Draw(screen tcell.Screen) {
g.Box.Draw(screen)
g.Lock()
defer g.Unlock()
x, y, width, height := g.GetInnerRect()
screenWidth, screenHeight := screen.Size()
@ -663,6 +722,9 @@ func (g *Grid) Draw(screen tcell.Screen) {
// GetChildren returns all primitives that have been added.
func (g *Grid) GetChildren() []Primitive {
g.Lock()
defer g.Unlock()
children := make([]Primitive, len(g.items))
for i, item := range g.items {
children[i] = item.Item

View File

@ -79,8 +79,7 @@ type InputField struct {
// The List object which shows the selectable autocomplete entries. If not
// nil, the list's main texts represent the current autocomplete entries.
autocompleteList *List
autocompleteListMutex sync.Mutex
autocompleteList *List
// An optional function which may reject the last character that was entered.
accept func(text string, ch rune) bool
@ -96,6 +95,8 @@ type InputField struct {
// A callback function set by the Form class and called when the user leaves
// this form item.
finished func(tcell.Key)
sync.RWMutex
}
// NewInputField returns a new input field.
@ -111,69 +112,105 @@ func NewInputField() *InputField {
// SetText sets the current text of the input field.
func (i *InputField) SetText(text string) *InputField {
i.Lock()
i.text = text
i.cursorPos = len(text)
if i.changed != nil {
i.Unlock()
i.changed(text)
} else {
i.Unlock()
}
return i
}
// GetText returns the current text of the input field.
func (i *InputField) GetText() string {
i.RLock()
defer i.RUnlock()
return i.text
}
// SetLabel sets the text to be displayed before the input area.
func (i *InputField) SetLabel(label string) *InputField {
i.Lock()
defer i.Unlock()
i.label = label
return i
}
// GetLabel returns the text to be displayed before the input area.
func (i *InputField) GetLabel() string {
i.RLock()
defer i.RUnlock()
return i.label
}
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
// primitive to use the width of the label string.
func (i *InputField) SetLabelWidth(width int) *InputField {
i.Lock()
defer i.Unlock()
i.labelWidth = width
return i
}
// SetPlaceholder sets the text to be displayed when the input text is empty.
func (i *InputField) SetPlaceholder(text string) *InputField {
i.Lock()
defer i.Unlock()
i.placeholder = text
return i
}
// SetLabelColor sets the color of the label.
func (i *InputField) SetLabelColor(color tcell.Color) *InputField {
i.Lock()
defer i.Unlock()
i.labelColor = color
return i
}
// SetFieldBackgroundColor sets the background color of the input area.
func (i *InputField) SetFieldBackgroundColor(color tcell.Color) *InputField {
i.Lock()
defer i.Unlock()
i.fieldBackgroundColor = color
return i
}
// SetFieldTextColor sets the text color of the input area.
func (i *InputField) SetFieldTextColor(color tcell.Color) *InputField {
i.Lock()
defer i.Unlock()
i.fieldTextColor = color
return i
}
// SetPlaceholderTextColor sets the text color of placeholder text.
func (i *InputField) SetPlaceholderTextColor(color tcell.Color) *InputField {
i.Lock()
defer i.Unlock()
i.placeholderTextColor = color
return i
}
// SetFormAttributes sets attributes shared by all form items.
func (i *InputField) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
i.Lock()
defer i.Unlock()
i.labelWidth = labelWidth
i.labelColor = labelColor
i.backgroundColor = bgColor
@ -185,18 +222,27 @@ func (i *InputField) SetFormAttributes(labelWidth int, labelColor, bgColor, fiel
// SetFieldWidth sets the screen width of the input area. A value of 0 means
// extend as much as possible.
func (i *InputField) SetFieldWidth(width int) *InputField {
i.Lock()
defer i.Unlock()
i.fieldWidth = width
return i
}
// GetFieldWidth returns this primitive's field width.
func (i *InputField) GetFieldWidth() int {
i.RLock()
defer i.RUnlock()
return i.fieldWidth
}
// SetMaskCharacter sets a character that masks user input on a screen. A value
// of 0 disables masking.
func (i *InputField) SetMaskCharacter(mask rune) *InputField {
i.Lock()
defer i.Unlock()
i.maskCharacter = mask
return i
}
@ -208,7 +254,10 @@ func (i *InputField) SetMaskCharacter(mask rune) *InputField {
// Autocomplete() is called. Entries are cleared when the user selects an entry
// or presses Escape.
func (i *InputField) SetAutocompleteFunc(callback func(currentText string) (entries []string)) *InputField {
i.Lock()
i.autocomplete = callback
i.Unlock()
i.Autocomplete()
return i
}
@ -222,20 +271,25 @@ func (i *InputField) SetAutocompleteFunc(callback func(currentText string) (entr
// field is not redrawn automatically unless called from the main goroutine
// (e.g. in response to events).
func (i *InputField) Autocomplete() *InputField {
i.autocompleteListMutex.Lock()
defer i.autocompleteListMutex.Unlock()
i.Lock()
if i.autocomplete == nil {
i.Unlock()
return i
}
i.Unlock()
// Do we have any autocomplete entries?
entries := i.autocomplete(i.text)
if len(entries) == 0 {
// No entries, no list.
i.Lock()
i.autocompleteList = nil
i.Unlock()
return i
}
i.Lock()
// Make a list if we have none.
if i.autocompleteList == nil {
i.autocompleteList = NewList()
@ -262,6 +316,7 @@ func (i *InputField) Autocomplete() *InputField {
i.autocompleteList.SetCurrentItem(currentEntry)
}
i.Unlock()
return i
}
@ -271,6 +326,9 @@ func (i *InputField) Autocomplete() *InputField {
// This package defines a number of variables prefixed with InputField which may
// be used for common input (e.g. numbers, maximum text length).
func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) *InputField {
i.Lock()
defer i.Unlock()
i.accept = handler
return i
}
@ -278,6 +336,9 @@ func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar
// SetChangedFunc sets a handler which is called whenever the text of the input
// field has changed. It receives the current text (after the change).
func (i *InputField) SetChangedFunc(handler func(text string)) *InputField {
i.Lock()
defer i.Unlock()
i.changed = handler
return i
}
@ -291,12 +352,18 @@ func (i *InputField) SetChangedFunc(handler func(text string)) *InputField {
// - KeyTab: Move to the next field.
// - KeyBacktab: Move to the previous field.
func (i *InputField) SetDoneFunc(handler func(key tcell.Key)) *InputField {
i.Lock()
defer i.Unlock()
i.done = handler
return i
}
// SetFinishedFunc sets a callback invoked when the user leaves this form item.
func (i *InputField) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
i.Lock()
defer i.Unlock()
i.finished = handler
return i
}
@ -305,6 +372,9 @@ func (i *InputField) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
func (i *InputField) Draw(screen tcell.Screen) {
i.Box.Draw(screen)
i.Lock()
defer i.Unlock()
// Prepare
x, y, width, height := i.GetInnerRect()
rightLimit := x + width
@ -395,8 +465,6 @@ func (i *InputField) Draw(screen tcell.Screen) {
}
// Draw autocomplete list.
i.autocompleteListMutex.Lock()
defer i.autocompleteListMutex.Unlock()
if i.autocompleteList != nil {
// How much space do we need?
lheight := i.autocompleteList.GetItemCount()
@ -438,10 +506,16 @@ func (i *InputField) Draw(screen tcell.Screen) {
// InputHandler returns the handler for this primitive.
func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return i.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
i.Lock()
// Trigger changed events.
currentText := i.text
defer func() {
if i.text != currentText {
i.Lock()
newText := i.text
i.Unlock()
if newText != currentText {
i.Autocomplete()
if i.changed != nil {
i.changed(i.text)
@ -494,8 +568,6 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
}
// Process key event.
i.autocompleteListMutex.Lock()
defer i.autocompleteListMutex.Unlock()
switch key := event.Key(); key {
case tcell.KeyRune: // Regular character.
if event.Modifiers()&tcell.ModAlt > 0 {
@ -511,12 +583,14 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
moveWordRight()
default:
if !add(event.Rune()) {
i.Unlock()
return
}
}
} else {
// Other keys are simply accepted as regular characters.
if !add(event.Rune()) {
i.Unlock()
return
}
}
@ -563,9 +637,12 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
case tcell.KeyEnter, tcell.KeyEscape: // We might be done.
if i.autocompleteList != nil {
i.autocompleteList = nil
i.Unlock()
} else {
i.Unlock()
finish(key)
}
return
case tcell.KeyDown, tcell.KeyTab: // Autocomplete selection.
if i.autocompleteList != nil {
count := i.autocompleteList.GetItemCount()
@ -575,10 +652,13 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
}
i.autocompleteList.SetCurrentItem(newEntry)
currentText, _ = i.autocompleteList.GetItemText(newEntry) // Don't trigger changed function twice.
i.Unlock()
i.SetText(currentText)
} else {
i.Unlock()
finish(key)
}
return
case tcell.KeyUp, tcell.KeyBacktab: // Autocomplete selection.
if i.autocompleteList != nil {
newEntry := i.autocompleteList.GetCurrentItem() - 1
@ -587,11 +667,16 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
}
i.autocompleteList.SetCurrentItem(newEntry)
currentText, _ = i.autocompleteList.GetItemText(newEntry) // Don't trigger changed function twice.
i.Unlock()
i.SetText(currentText)
} else {
i.Unlock()
finish(key)
}
return
}
i.Unlock()
})
}

108
list.go
View File

@ -3,6 +3,7 @@ package cview
import (
"fmt"
"strings"
"sync"
"github.com/gdamore/tcell"
)
@ -73,6 +74,8 @@ type List struct {
// An optional function which is called when the user presses the Escape key.
done func()
sync.RWMutex
}
// NewList returns a new form.
@ -97,6 +100,8 @@ func NewList() *List {
//
// Calling this function triggers a "changed" event if the selection changes.
func (l *List) SetCurrentItem(index int) *List {
l.Lock()
if index < 0 {
index = len(l.items) + index
}
@ -109,17 +114,23 @@ func (l *List) SetCurrentItem(index int) *List {
if index != l.currentItem && l.changed != nil {
item := l.items[index]
l.Unlock()
l.changed(index, item.MainText, item.SecondaryText, item.Shortcut)
l.Lock()
}
l.currentItem = index
l.Unlock()
return l
}
// GetCurrentItem returns the index of the currently selected list item,
// starting at 0 for the first item.
func (l *List) GetCurrentItem() int {
l.RLock()
defer l.RUnlock()
return l.currentItem
}
@ -132,7 +143,10 @@ func (l *List) GetCurrentItem() int {
// The currently selected item is shifted accordingly. If it is the one that is
// removed, a "changed" event is fired.
func (l *List) RemoveItem(index int) *List {
l.Lock()
if len(l.items) == 0 {
l.Unlock()
return l
}
@ -152,6 +166,7 @@ func (l *List) RemoveItem(index int) *List {
// If there is nothing left, we're done.
if len(l.items) == 0 {
l.Unlock()
return l
}
@ -164,7 +179,10 @@ func (l *List) RemoveItem(index int) *List {
// Fire "changed" event for removed items.
if previousCurrentItem == index && l.changed != nil {
item := l.items[l.currentItem]
l.Unlock()
l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
} else {
l.Unlock()
}
return l
@ -172,30 +190,45 @@ func (l *List) RemoveItem(index int) *List {
// SetMainTextColor sets the color of the items' main text.
func (l *List) SetMainTextColor(color tcell.Color) *List {
l.Lock()
defer l.Unlock()
l.mainTextColor = color
return l
}
// SetSecondaryTextColor sets the color of the items' secondary text.
func (l *List) SetSecondaryTextColor(color tcell.Color) *List {
l.Lock()
defer l.Unlock()
l.secondaryTextColor = color
return l
}
// SetShortcutColor sets the color of the items' shortcut.
func (l *List) SetShortcutColor(color tcell.Color) *List {
l.Lock()
defer l.Unlock()
l.shortcutColor = color
return l
}
// SetSelectedTextColor sets the text color of selected items.
func (l *List) SetSelectedTextColor(color tcell.Color) *List {
l.Lock()
defer l.Unlock()
l.selectedTextColor = color
return l
}
// SetSelectedBackgroundColor sets the background color of selected items.
func (l *List) SetSelectedBackgroundColor(color tcell.Color) *List {
l.Lock()
defer l.Unlock()
l.selectedBackgroundColor = color
return l
}
@ -204,6 +237,9 @@ func (l *List) SetSelectedBackgroundColor(color tcell.Color) *List {
// 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.Lock()
defer l.Unlock()
l.selectedFocusOnly = focusOnly
return l
}
@ -213,24 +249,36 @@ func (l *List) SetSelectedFocusOnly(focusOnly bool) *List {
// 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.Lock()
defer l.Unlock()
l.highlightFullLine = highlight
return l
}
// ShowSecondaryText determines whether or not to show secondary item texts.
func (l *List) ShowSecondaryText(show bool) *List {
l.Lock()
defer l.Unlock()
l.showSecondaryText = show
return l
}
// SetScrollBarVisibility specifies the display of the scroll bar.
func (l *List) SetScrollBarVisibility(visibility ScrollBarVisibility) *List {
l.Lock()
defer l.Unlock()
l.scrollBarVisibility = visibility
return l
}
// SetScrollBarColor sets the color of the scroll bar.
func (l *List) SetScrollBarColor(color tcell.Color) *List {
l.Lock()
defer l.Unlock()
l.scrollBarColor = color
return l
}
@ -241,6 +289,9 @@ func (l *List) SetScrollBarColor(color tcell.Color) *List {
// 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.Lock()
defer l.Unlock()
l.wrapAround = wrapAround
return l
}
@ -252,6 +303,9 @@ func (l *List) SetWrapAround(wrapAround bool) *List {
// This function is also called when the first item is added or when
// SetCurrentItem() is called.
func (l *List) SetChangedFunc(handler func(index int, mainText string, secondaryText string, shortcut rune)) *List {
l.Lock()
defer l.Unlock()
l.changed = handler
return l
}
@ -261,6 +315,9 @@ func (l *List) SetChangedFunc(handler func(index int, mainText string, secondary
// the item's index in the list of items (starting with 0), its main text,
// secondary text, and its shortcut rune.
func (l *List) SetSelectedFunc(handler func(int, string, string, rune)) *List {
l.Lock()
defer l.Unlock()
l.selected = handler
return l
}
@ -268,6 +325,9 @@ func (l *List) SetSelectedFunc(handler func(int, string, string, rune)) *List {
// SetDoneFunc sets a function which is called when the user presses the Escape
// key.
func (l *List) SetDoneFunc(handler func()) *List {
l.Lock()
defer l.Unlock()
l.done = handler
return l
}
@ -301,6 +361,8 @@ func (l *List) AddItem(mainText, secondaryText string, shortcut rune, selected f
// 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 {
l.Lock()
item := &listItem{
MainText: mainText,
SecondaryText: secondaryText,
@ -333,7 +395,10 @@ func (l *List) InsertItem(index int, mainText, secondaryText string, shortcut ru
// Fire a "change" event for the first item in the list.
if len(l.items) == 1 && l.changed != nil {
item := l.items[0]
l.Unlock()
l.changed(0, item.MainText, item.SecondaryText, item.Shortcut)
} else {
l.Unlock()
}
return l
@ -341,18 +406,27 @@ func (l *List) InsertItem(index int, mainText, secondaryText string, shortcut ru
// GetItemCount returns the number of items in the list.
func (l *List) GetItemCount() int {
l.RLock()
defer l.RUnlock()
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) {
l.RLock()
defer l.RUnlock()
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 {
l.Lock()
defer l.Unlock()
item := l.items[index]
item.MainText = main
item.SecondaryText = secondary
@ -370,6 +444,9 @@ func (l *List) SetItemText(index int, main, secondary string) *List {
//
// Set ignoreCase to true for case-insensitive search.
func (l *List) FindItems(mainSearch, secondarySearch string, mustContainBoth, ignoreCase bool) (indices []int) {
l.RLock()
defer l.RUnlock()
if mainSearch == "" && secondarySearch == "" {
return
}
@ -401,6 +478,9 @@ func (l *List) FindItems(mainSearch, secondarySearch string, mustContainBoth, ig
// Clear removes all items from the list.
func (l *List) Clear() *List {
l.Lock()
defer l.Unlock()
l.items = nil
l.currentItem = 0
return l
@ -410,6 +490,9 @@ func (l *List) Clear() *List {
func (l *List) Draw(screen tcell.Screen) {
l.Box.Draw(screen)
l.Lock()
defer l.Unlock()
// Determine the dimensions.
x, y, width, height := l.GetInnerRect()
bottomLimit := y + height
@ -517,12 +600,16 @@ func (l *List) Draw(screen tcell.Screen) {
// InputHandler returns the handler for this primitive.
func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return l.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
l.Lock()
if event.Key() == tcell.KeyEscape {
if l.done != nil {
l.done()
}
l.Unlock()
return
} else if len(l.items) == 0 {
l.Unlock()
return
}
@ -545,10 +632,14 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
if l.currentItem >= 0 && l.currentItem < len(l.items) {
item := l.items[l.currentItem]
if item.Selected != nil {
l.Unlock()
item.Selected()
l.Lock()
}
if l.selected != nil {
l.Unlock()
l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
l.Lock()
}
}
case tcell.KeyRune:
@ -570,10 +661,14 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
}
item := l.items[l.currentItem]
if item.Selected != nil {
l.Unlock()
item.Selected()
l.Lock()
}
if l.selected != nil {
l.Unlock()
l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
l.Lock()
}
}
@ -593,7 +688,10 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
if l.currentItem != previousItem && l.currentItem < len(l.items) && l.changed != nil {
item := l.items[l.currentItem]
l.Unlock()
l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
} else {
l.Unlock()
}
})
}
@ -621,21 +719,31 @@ func (l *List) MouseHandler() func(event *EventMouse) {
return l.WrapMouseHandler(func(event *EventMouse) {
// Process mouse event.
if event.Action()&MouseClick != 0 {
l.Lock()
atX, atY := event.Position()
index := l.indexAtPoint(atX, atY)
if index != -1 {
item := l.items[index]
if item.Selected != nil {
l.Unlock()
item.Selected()
l.Lock()
}
if l.selected != nil {
l.Unlock()
l.selected(index, item.MainText, item.SecondaryText, item.Shortcut)
l.Lock()
}
if index != l.currentItem && l.changed != nil {
l.Unlock()
l.changed(index, item.MainText, item.SecondaryText, item.Shortcut)
l.Lock()
}
l.currentItem = index
}
l.Unlock()
}
})
}

View File

@ -1,6 +1,8 @@
package cview
import (
"sync"
"github.com/gdamore/tcell"
)
@ -27,6 +29,8 @@ type Modal struct {
// The optional callback for when the user clicked one of the buttons. It
// receives the index of the clicked button and the button's label.
done func(buttonIndex int, buttonLabel string)
sync.Mutex
}
// NewModal returns a new modal message window.
@ -55,6 +59,9 @@ func NewModal() *Modal {
// SetBackgroundColor sets the color of the modal frame background.
func (m *Modal) SetBackgroundColor(color tcell.Color) *Modal {
m.Lock()
defer m.Unlock()
m.form.SetBackgroundColor(color)
m.frame.SetBackgroundColor(color)
return m
@ -62,18 +69,27 @@ func (m *Modal) SetBackgroundColor(color tcell.Color) *Modal {
// SetTextColor sets the color of the message text.
func (m *Modal) SetTextColor(color tcell.Color) *Modal {
m.Lock()
defer m.Unlock()
m.textColor = color
return m
}
// SetButtonBackgroundColor sets the background color of the buttons.
func (m *Modal) SetButtonBackgroundColor(color tcell.Color) *Modal {
m.Lock()
defer m.Unlock()
m.form.SetButtonBackgroundColor(color)
return m
}
// SetButtonTextColor sets the color of the button texts.
func (m *Modal) SetButtonTextColor(color tcell.Color) *Modal {
m.Lock()
defer m.Unlock()
m.form.SetButtonTextColor(color)
return m
}
@ -83,6 +99,9 @@ func (m *Modal) SetButtonTextColor(color tcell.Color) *Modal {
// handler is also called when the user presses the Escape key. The index will
// then be negative and the label text an emptry string.
func (m *Modal) SetDoneFunc(handler func(buttonIndex int, buttonLabel string)) *Modal {
m.Lock()
defer m.Unlock()
m.done = handler
return m
}
@ -91,6 +110,9 @@ func (m *Modal) SetDoneFunc(handler func(buttonIndex int, buttonLabel string)) *
// breaks. Note that words are wrapped, too, based on the final size of the
// window.
func (m *Modal) SetText(text string) *Modal {
m.Lock()
defer m.Unlock()
m.text = text
return m
}
@ -98,6 +120,9 @@ func (m *Modal) SetText(text string) *Modal {
// AddButtons adds buttons to the window. There must be at least one button and
// a "done" handler so the window can be closed again.
func (m *Modal) AddButtons(labels []string) *Modal {
m.Lock()
defer m.Unlock()
for index, label := range labels {
func(i int, l string) {
m.form.AddButton(label, func() {
@ -122,12 +147,18 @@ func (m *Modal) AddButtons(labels []string) *Modal {
// ClearButtons removes all buttons from the window.
func (m *Modal) ClearButtons() *Modal {
m.Lock()
defer m.Unlock()
m.form.ClearButtons()
return m
}
// SetFocus shifts the focus to the button with the given index.
func (m *Modal) SetFocus(index int) *Modal {
m.Lock()
defer m.Unlock()
m.form.SetFocus(index)
return m
}
@ -139,11 +170,17 @@ func (m *Modal) Focus(delegate func(p Primitive)) {
// HasFocus returns whether or not this primitive has focus.
func (m *Modal) HasFocus() bool {
m.Lock()
defer m.Unlock()
return m.form.HasFocus()
}
// Draw draws this primitive onto the screen.
func (m *Modal) Draw(screen tcell.Screen) {
m.Lock()
defer m.Unlock()
// Calculate the width of this modal.
buttonsWidth := 0
for _, button := range m.form.buttons {
@ -178,5 +215,8 @@ func (m *Modal) Draw(screen tcell.Screen) {
// GetChildren returns all primitives that have been added.
func (m *Modal) GetChildren() []Primitive {
m.Lock()
defer m.Unlock()
return []Primitive{m.frame}
}

104
pages.go
View File

@ -1,6 +1,8 @@
package cview
import (
"sync"
"github.com/gdamore/tcell"
)
@ -30,6 +32,8 @@ type Pages struct {
// An optional handler which is called whenever the visibility or the order of
// pages changes.
changed func()
sync.Mutex
}
// NewPages returns a new Pages object.
@ -44,12 +48,18 @@ func NewPages() *Pages {
// SetChangedFunc sets a handler which is called whenever the visibility or the
// order of any visible pages changes. This can be used to redraw the pages.
func (p *Pages) SetChangedFunc(handler func()) *Pages {
p.Lock()
defer p.Unlock()
p.changed = handler
return p
}
// GetPageCount returns the number of pages currently stored in this object.
func (p *Pages) GetPageCount() int {
p.Lock()
defer p.Unlock()
return len(p.pages)
}
@ -64,6 +74,10 @@ func (p *Pages) GetPageCount() int {
// the pages are drawn.
func (p *Pages) AddPage(name string, item Primitive, resize, visible bool) *Pages {
hasFocus := p.HasFocus()
p.Lock()
defer p.Unlock()
for index, pg := range p.pages {
if pg.Name == name {
p.pages = append(p.pages[:index], p.pages[index+1:]...)
@ -72,10 +86,14 @@ func (p *Pages) AddPage(name string, item Primitive, resize, visible bool) *Page
}
p.pages = append(p.pages, &page{Item: item, Name: name, Resize: resize, Visible: visible})
if p.changed != nil {
p.Unlock()
p.changed()
p.Lock()
}
if hasFocus {
p.Unlock()
p.Focus(p.setFocus)
p.Lock()
}
return p
}
@ -91,14 +109,20 @@ func (p *Pages) AddAndSwitchToPage(name string, item Primitive, resize bool) *Pa
// RemovePage removes the page with the given name. If that page was the only
// visible page, visibility is assigned to the last page.
func (p *Pages) RemovePage(name string) *Pages {
var isVisible bool
hasFocus := p.HasFocus()
p.Lock()
defer p.Unlock()
var isVisible bool
for index, page := range p.pages {
if page.Name == name {
isVisible = page.Visible
p.pages = append(p.pages[:index], p.pages[index+1:]...)
if page.Visible && p.changed != nil {
p.Unlock()
p.changed()
p.Lock()
}
break
}
@ -115,13 +139,18 @@ func (p *Pages) RemovePage(name string) *Pages {
}
}
if hasFocus {
p.Unlock()
p.Focus(p.setFocus)
p.Lock()
}
return p
}
// HasPage returns true if a page with the given name exists in this object.
func (p *Pages) HasPage(name string) bool {
p.Lock()
defer p.Unlock()
for _, page := range p.pages {
if page.Name == name {
return true
@ -133,34 +162,52 @@ func (p *Pages) HasPage(name string) bool {
// ShowPage sets a page's visibility to "true" (in addition to any other pages
// which are already visible).
func (p *Pages) ShowPage(name string) *Pages {
hasFocus := p.HasFocus()
p.Lock()
defer p.Unlock()
for _, page := range p.pages {
if page.Name == name {
page.Visible = true
if p.changed != nil {
p.Unlock()
p.changed()
p.Lock()
}
break
}
}
if p.HasFocus() {
if hasFocus {
p.Unlock()
p.Focus(p.setFocus)
p.Lock()
}
return p
}
// HidePage sets a page's visibility to "false".
func (p *Pages) HidePage(name string) *Pages {
hasFocus := p.HasFocus()
p.Lock()
defer p.Unlock()
for _, page := range p.pages {
if page.Name == name {
page.Visible = false
if p.changed != nil {
p.Unlock()
p.changed()
p.Lock()
}
break
}
}
if p.HasFocus() {
if hasFocus {
p.Unlock()
p.Focus(p.setFocus)
p.Lock()
}
return p
}
@ -168,6 +215,11 @@ func (p *Pages) HidePage(name string) *Pages {
// SwitchToPage sets a page's visibility to "true" and all other pages'
// visibility to "false".
func (p *Pages) SwitchToPage(name string) *Pages {
hasFocus := p.HasFocus()
p.Lock()
defer p.Unlock()
for _, page := range p.pages {
if page.Name == name {
page.Visible = true
@ -176,10 +228,14 @@ func (p *Pages) SwitchToPage(name string) *Pages {
}
}
if p.changed != nil {
p.Unlock()
p.changed()
p.Lock()
}
if p.HasFocus() {
if hasFocus {
p.Unlock()
p.Focus(p.setFocus)
p.Lock()
}
return p
}
@ -188,19 +244,28 @@ func (p *Pages) SwitchToPage(name string) *Pages {
// name comes last, causing it to be drawn last with the next update (if
// visible).
func (p *Pages) SendToFront(name string) *Pages {
hasFocus := p.HasFocus()
p.Lock()
defer p.Unlock()
for index, page := range p.pages {
if page.Name == name {
if index < len(p.pages)-1 {
p.pages = append(append(p.pages[:index], p.pages[index+1:]...), page)
}
if page.Visible && p.changed != nil {
p.Lock()
p.changed()
p.Unlock()
}
break
}
}
if p.HasFocus() {
if hasFocus {
p.Lock()
p.Focus(p.setFocus)
p.Unlock()
}
return p
}
@ -209,19 +274,28 @@ func (p *Pages) SendToFront(name string) *Pages {
// name comes first, causing it to be drawn first with the next update (if
// visible).
func (p *Pages) SendToBack(name string) *Pages {
hasFocus := p.HasFocus()
p.Lock()
defer p.Unlock()
for index, pg := range p.pages {
if pg.Name == name {
if index > 0 {
p.pages = append(append([]*page{pg}, p.pages[:index]...), p.pages[index+1:]...)
}
if pg.Visible && p.changed != nil {
p.Unlock()
p.changed()
p.Lock()
}
break
}
}
if p.HasFocus() {
if hasFocus {
p.Unlock()
p.Focus(p.setFocus)
p.Lock()
}
return p
}
@ -229,6 +303,9 @@ func (p *Pages) SendToBack(name string) *Pages {
// GetFrontPage returns the front-most visible page. If there are no visible
// pages, ("", nil) is returned.
func (p *Pages) GetFrontPage() (name string, item Primitive) {
p.Lock()
defer p.Unlock()
for index := len(p.pages) - 1; index >= 0; index-- {
if p.pages[index].Visible {
return p.pages[index].Name, p.pages[index].Item
@ -239,6 +316,9 @@ func (p *Pages) GetFrontPage() (name string, item Primitive) {
// HasFocus returns whether or not this primitive has focus.
func (p *Pages) HasFocus() bool {
p.Lock()
defer p.Unlock()
for _, page := range p.pages {
if page.Item.GetFocusable().HasFocus() {
return true
@ -249,6 +329,9 @@ func (p *Pages) HasFocus() bool {
// Focus is called by the application when the primitive receives focus.
func (p *Pages) Focus(delegate func(p Primitive)) {
p.Lock()
defer p.Unlock()
if delegate == nil {
return // We cannot delegate so we cannot focus.
}
@ -260,13 +343,19 @@ func (p *Pages) Focus(delegate func(p Primitive)) {
}
}
if topItem != nil {
p.Unlock()
delegate(topItem)
p.Lock()
}
}
// Draw draws this primitive onto the screen.
func (p *Pages) Draw(screen tcell.Screen) {
p.Box.Draw(screen)
p.Lock()
defer p.Unlock()
for _, page := range p.pages {
if !page.Visible {
continue
@ -281,6 +370,9 @@ func (p *Pages) Draw(screen tcell.Screen) {
// GetChildren returns all primitives that have been added.
func (p *Pages) GetChildren() []Primitive {
p.Lock()
defer p.Unlock()
var children []Primitive
for _, page := range p.pages {
// Considering invisible pages as not children.

View File

@ -29,7 +29,8 @@ type ProgressBar struct {
max int
progress int
*sync.Mutex
sync.Mutex
}
// NewProgressBar returns a new progress bar.
@ -41,7 +42,6 @@ func NewProgressBar() *ProgressBar {
FilledRune: tcell.RuneBlock,
FilledColor: Styles.PrimaryTextColor,
max: 100,
Mutex: new(sync.Mutex),
}
}
@ -95,11 +95,11 @@ func (p *ProgressBar) Complete() bool {
// Draw draws this primitive onto the screen.
func (p *ProgressBar) Draw(screen tcell.Screen) {
p.Box.Draw(screen)
p.Lock()
defer p.Unlock()
p.Box.Draw(screen)
x, y, width, height := p.GetInnerRect()
barSize := height

142
table.go
View File

@ -2,6 +2,7 @@ package cview
import (
"sort"
"sync"
"github.com/gdamore/tcell"
colorful "github.com/lucasb-eyer/go-colorful"
@ -44,6 +45,8 @@ type TableCell struct {
// The position and width of the cell the last time table was drawn.
x, y, width int
sync.Mutex
}
// NewTableCell returns a new table cell with sensible defaults. That is, left
@ -60,6 +63,9 @@ func NewTableCell(text string) *TableCell {
// SetText sets the cell's text.
func (c *TableCell) SetText(text string) *TableCell {
c.Lock()
defer c.Unlock()
c.Text = text
return c
}
@ -67,6 +73,9 @@ func (c *TableCell) SetText(text string) *TableCell {
// SetAlign sets the cell's text alignment, one of AlignLeft, AlignCenter, or
// AlignRight.
func (c *TableCell) SetAlign(align int) *TableCell {
c.Lock()
defer c.Unlock()
c.Align = align
return c
}
@ -75,6 +84,9 @@ func (c *TableCell) SetAlign(align int) *TableCell {
// give a column a maximum width. Any cell text whose screen width exceeds this
// width is cut off. Set to 0 if there is no maximum width.
func (c *TableCell) SetMaxWidth(maxWidth int) *TableCell {
c.Lock()
defer c.Unlock()
c.MaxWidth = maxWidth
return c
}
@ -93,6 +105,9 @@ func (c *TableCell) SetMaxWidth(maxWidth int) *TableCell {
//
// This function panics if a negative value is provided.
func (c *TableCell) SetExpansion(expansion int) *TableCell {
c.Lock()
defer c.Unlock()
if expansion < 0 {
panic("Table cell expansion values may not be negative")
}
@ -102,6 +117,9 @@ func (c *TableCell) SetExpansion(expansion int) *TableCell {
// SetTextColor sets the cell's text color.
func (c *TableCell) SetTextColor(color tcell.Color) *TableCell {
c.Lock()
defer c.Unlock()
c.Color = color
return c
}
@ -109,6 +127,9 @@ func (c *TableCell) SetTextColor(color tcell.Color) *TableCell {
// SetBackgroundColor sets the cell's background color. Set to
// tcell.ColorDefault to use the table's background color.
func (c *TableCell) SetBackgroundColor(color tcell.Color) *TableCell {
c.Lock()
defer c.Unlock()
c.BackgroundColor = color
return c
}
@ -118,6 +139,9 @@ func (c *TableCell) SetBackgroundColor(color tcell.Color) *TableCell {
//
// cell.SetAttributes(tcell.AttrUnderline | tcell.AttrBold)
func (c *TableCell) SetAttributes(attr tcell.AttrMask) *TableCell {
c.Lock()
defer c.Unlock()
c.Attributes = attr
return c
}
@ -125,12 +149,18 @@ func (c *TableCell) SetAttributes(attr tcell.AttrMask) *TableCell {
// SetStyle sets the cell's style (foreground color, background color, and
// attributes) all at once.
func (c *TableCell) SetStyle(style tcell.Style) *TableCell {
c.Lock()
defer c.Unlock()
c.Color, c.BackgroundColor, c.Attributes = style.Decompose()
return c
}
// SetSelectable sets whether or not this cell can be selected by the user.
func (c *TableCell) SetSelectable(selectable bool) *TableCell {
c.Lock()
defer c.Unlock()
c.NotSelectable = !selectable
return c
}
@ -139,12 +169,18 @@ func (c *TableCell) SetSelectable(selectable bool) *TableCell {
// will allow you to establish a mapping between the cell and your
// actual data.
func (c *TableCell) SetReference(reference interface{}) *TableCell {
c.Lock()
defer c.Unlock()
c.Reference = reference
return c
}
// GetReference returns this cell's reference object.
func (c *TableCell) GetReference() interface{} {
c.Lock()
defer c.Unlock()
return c.Reference
}
@ -157,6 +193,9 @@ func (c *TableCell) GetReference() interface{} {
// SetSelectedFunc()) or a "selectionChanged" event (see
// SetSelectionChangedFunc()).
func (c *TableCell) GetLastPosition() (x, y, width int) {
c.Lock()
defer c.Unlock()
return c.x, c.y, c.width
}
@ -274,6 +313,8 @@ type Table struct {
// An optional function which gets called when the user presses Escape, Tab,
// or Backtab. Also when the user presses Enter if nothing is selectable.
done func(key tcell.Key)
sync.Mutex
}
// NewTable returns a new table.
@ -290,6 +331,9 @@ func NewTable() *Table {
// Clear removes all table data.
func (t *Table) Clear() *Table {
t.Lock()
defer t.Unlock()
t.cells = nil
t.lastColumn = -1
return t
@ -298,24 +342,36 @@ func (t *Table) Clear() *Table {
// SetBorders sets whether or not each cell in the table is surrounded by a
// border.
func (t *Table) SetBorders(show bool) *Table {
t.Lock()
defer t.Unlock()
t.borders = show
return t
}
// SetBordersColor sets the color of the cell borders.
func (t *Table) SetBordersColor(color tcell.Color) *Table {
t.Lock()
defer t.Unlock()
t.bordersColor = color
return t
}
// SetScrollBarVisibility specifies the display of the scroll bar.
func (t *Table) SetScrollBarVisibility(visibility ScrollBarVisibility) *Table {
t.Lock()
defer t.Unlock()
t.scrollBarVisibility = visibility
return t
}
// SetScrollBarColor sets the color of the scroll bar.
func (t *Table) SetScrollBarColor(color tcell.Color) *Table {
t.Lock()
defer t.Unlock()
t.scrollBarColor = color
return t
}
@ -328,6 +384,9 @@ func (t *Table) SetScrollBarColor(color tcell.Color) *Table {
//
// table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorDefault, 0)
func (t *Table) SetSelectedStyle(foregroundColor, backgroundColor tcell.Color, attributes tcell.AttrMask) *Table {
t.Lock()
defer t.Unlock()
t.selectedStyle = tcell.StyleDefault.Foreground(foregroundColor).Background(backgroundColor) | tcell.Style(attributes)
return t
}
@ -340,6 +399,9 @@ func (t *Table) SetSelectedStyle(foregroundColor, backgroundColor tcell.Color, a
//
// Separators have the same color as borders.
func (t *Table) SetSeparator(separator rune) *Table {
t.Lock()
defer t.Unlock()
t.separator = separator
return t
}
@ -348,6 +410,9 @@ func (t *Table) SetSeparator(separator rune) *Table {
// even when the rest of the cells are scrolled out of view. Rows are always the
// top-most ones. Columns are always the left-most ones.
func (t *Table) SetFixed(rows, columns int) *Table {
t.Lock()
defer t.Unlock()
t.fixedRows, t.fixedColumns = rows, columns
return t
}
@ -360,6 +425,9 @@ func (t *Table) SetFixed(rows, columns int) *Table {
// - rows = false, columns = true: Columns can be selected.
// - rows = true, columns = true: Individual cells can be selected.
func (t *Table) SetSelectable(rows, columns bool) *Table {
t.Lock()
defer t.Unlock()
t.rowsSelectable, t.columnsSelectable = rows, columns
return t
}
@ -367,6 +435,9 @@ func (t *Table) SetSelectable(rows, columns bool) *Table {
// GetSelectable returns what can be selected in a table. Refer to
// SetSelectable() for details.
func (t *Table) GetSelectable() (rows, columns bool) {
t.Lock()
defer t.Unlock()
return t.rowsSelectable, t.columnsSelectable
}
@ -374,6 +445,9 @@ func (t *Table) GetSelectable() (rows, columns bool) {
// If entire rows are selected, the column index is undefined.
// Likewise for entire columns.
func (t *Table) GetSelection() (row, column int) {
t.Lock()
defer t.Unlock()
return t.selectedRow, t.selectedColumn
}
@ -383,9 +457,14 @@ func (t *Table) GetSelection() (row, column int) {
// is available (even if the selection ends up being the same as before, even if
// cells are not selectable).
func (t *Table) Select(row, column int) *Table {
t.Lock()
defer t.Unlock()
t.selectedRow, t.selectedColumn = row, column
if t.selectionChanged != nil {
t.Unlock()
t.selectionChanged(row, column)
t.Lock()
}
return t
}
@ -396,6 +475,9 @@ func (t *Table) Select(row, column int) *Table {
//
// Fixed rows and columns are never skipped.
func (t *Table) SetOffset(row, column int) *Table {
t.Lock()
defer t.Unlock()
t.rowOffset, t.columnOffset = row, column
t.trackEnd = false
return t
@ -404,6 +486,9 @@ func (t *Table) SetOffset(row, column int) *Table {
// GetOffset returns the current row and column offset. This indicates how many
// rows and columns the table is scrolled down and to the right.
func (t *Table) GetOffset() (row, column int) {
t.Lock()
defer t.Unlock()
return t.rowOffset, t.columnOffset
}
@ -414,6 +499,9 @@ func (t *Table) GetOffset() (row, column int) {
// Set this flag to true to avoid shifting column widths when the table is
// scrolled. (May be slower for large tables.)
func (t *Table) SetEvaluateAllRows(all bool) *Table {
t.Lock()
defer t.Unlock()
t.evaluateAllRows = all
return t
}
@ -423,6 +511,9 @@ func (t *Table) SetEvaluateAllRows(all bool) *Table {
// the selection and its cell contents. If entire rows are selected, the column
// index is undefined. Likewise for entire columns.
func (t *Table) SetSelectedFunc(handler func(row, column int)) *Table {
t.Lock()
defer t.Unlock()
t.selected = handler
return t
}
@ -432,6 +523,9 @@ func (t *Table) SetSelectedFunc(handler func(row, column int)) *Table {
// If entire rows are selected, the column index is undefined. Likewise for
// entire columns.
func (t *Table) SetSelectionChangedFunc(handler func(row, column int)) *Table {
t.Lock()
defer t.Unlock()
t.selectionChanged = handler
return t
}
@ -441,6 +535,9 @@ func (t *Table) SetSelectionChangedFunc(handler func(row, column int)) *Table {
// user presses the Enter key (because pressing Enter on a selection triggers
// the "selected" handler set via SetSelectedFunc()).
func (t *Table) SetDoneFunc(handler func(key tcell.Key)) *Table {
t.Lock()
defer t.Unlock()
t.done = handler
return t
}
@ -455,6 +552,9 @@ func (t *Table) SetDoneFunc(handler func(key tcell.Key)) *Table {
//
// To avoid unnecessary garbage collection, fill columns from left to right.
func (t *Table) SetCell(row, column int, cell *TableCell) *Table {
t.Lock()
defer t.Unlock()
if row >= len(t.cells) {
t.cells = append(t.cells, make([][]*TableCell, row-len(t.cells)+1)...)
}
@ -474,8 +574,7 @@ func (t *Table) SetCell(row, column int, cell *TableCell) *Table {
// SetCellSimple calls SetCell() with the given text, left-aligned, in white.
func (t *Table) SetCellSimple(row, column int, text string) *Table {
t.SetCell(row, column, NewTableCell(text))
return t
return t.SetCell(row, column, NewTableCell(text))
}
// GetCell returns the contents of the cell at the specified position. A valid
@ -484,6 +583,9 @@ func (t *Table) SetCellSimple(row, column int, text string) *Table {
// be inserted. Therefore, repeated calls to this function may return different
// pointers for uninitialized cells.
func (t *Table) GetCell(row, column int) *TableCell {
t.Lock()
defer t.Unlock()
if row >= len(t.cells) || column >= len(t.cells[row]) {
return &TableCell{}
}
@ -493,6 +595,9 @@ func (t *Table) GetCell(row, column int) *TableCell {
// RemoveRow removes the row at the given position from the table. If there is
// no such row, this has no effect.
func (t *Table) RemoveRow(row int) *Table {
t.Lock()
defer t.Unlock()
if row < 0 || row >= len(t.cells) {
return t
}
@ -505,6 +610,9 @@ func (t *Table) RemoveRow(row int) *Table {
// RemoveColumn removes the column at the given position from the table. If
// there is no such column, this has no effect.
func (t *Table) RemoveColumn(column int) *Table {
t.Lock()
defer t.Unlock()
for row := range t.cells {
if column < 0 || column >= len(t.cells[row]) {
continue
@ -519,6 +627,9 @@ func (t *Table) RemoveColumn(column int) *Table {
// given row and below will be shifted to the bottom by one row. If "row" is
// equal or larger than the current number of rows, this function has no effect.
func (t *Table) InsertRow(row int) *Table {
t.Lock()
defer t.Unlock()
if row >= len(t.cells) {
return t
}
@ -533,6 +644,9 @@ func (t *Table) InsertRow(row int) *Table {
// column. Rows that have fewer initialized cells than "column" will remain
// unchanged.
func (t *Table) InsertColumn(column int) *Table {
t.Lock()
defer t.Unlock()
for row := range t.cells {
if column >= len(t.cells[row]) {
continue
@ -546,11 +660,17 @@ func (t *Table) InsertColumn(column int) *Table {
// GetRowCount returns the number of rows in the table.
func (t *Table) GetRowCount() int {
t.Lock()
defer t.Unlock()
return len(t.cells)
}
// GetColumnCount returns the (maximum) number of columns in the table.
func (t *Table) GetColumnCount() int {
t.Lock()
defer t.Unlock()
if len(t.cells) == 0 {
return 0
}
@ -561,6 +681,9 @@ func (t *Table) GetColumnCount() int {
// corner of the table is shown. Note that this position may be corrected if
// there is a selection.
func (t *Table) ScrollToBeginning() *Table {
t.Lock()
defer t.Unlock()
t.trackEnd = false
t.columnOffset = 0
t.rowOffset = 0
@ -572,6 +695,9 @@ func (t *Table) ScrollToBeginning() *Table {
// automatically scroll with the new data. Note that this position may be
// corrected if there is a selection.
func (t *Table) ScrollToEnd() *Table {
t.Lock()
defer t.Unlock()
t.trackEnd = true
t.columnOffset = 0
t.rowOffset = len(t.cells)
@ -582,6 +708,9 @@ func (t *Table) ScrollToEnd() *Table {
func (t *Table) Draw(screen tcell.Screen) {
t.Box.Draw(screen)
t.Lock()
defer t.Unlock()
// What's our available screen space?
x, y, width, height := t.GetInnerRect()
if t.borders {
@ -1026,6 +1155,9 @@ ColumnLoop:
// InputHandler returns the handler for this primitive.
func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
t.Lock()
defer t.Unlock()
key := event.Key()
if (!t.rowsSelectable && !t.columnsSelectable && key == tcell.KeyEnter) ||
@ -1033,7 +1165,9 @@ func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primi
key == tcell.KeyTab ||
key == tcell.KeyBacktab {
if t.done != nil {
t.Unlock()
t.done(key)
t.Lock()
}
return
}
@ -1228,7 +1362,9 @@ func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primi
pageUp()
case tcell.KeyEnter:
if (t.rowsSelectable || t.columnsSelectable) && t.selected != nil {
t.Unlock()
t.selected(t.selectedRow, t.selectedColumn)
t.Lock()
}
}
@ -1236,7 +1372,9 @@ func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primi
if t.selectionChanged != nil &&
(t.rowsSelectable && previouslySelectedRow != t.selectedRow ||
t.columnsSelectable && previouslySelectedColumn != t.selectedColumn) {
t.Unlock()
t.selectionChanged(t.selectedRow, t.selectedColumn)
t.Lock()
}
})
}

View File

@ -9,8 +9,8 @@ import (
"unicode/utf8"
"github.com/gdamore/tcell"
colorful "github.com/lucasb-eyer/go-colorful"
runewidth "github.com/mattn/go-runewidth"
"github.com/lucasb-eyer/go-colorful"
"github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
)
@ -90,7 +90,7 @@ type textViewIndex struct {
//
// See https://gitlab.com/tslocum/cview/wiki/TextView for an example.
type TextView struct {
sync.Mutex
sync.RWMutex
*Box
// The text buffer.
@ -191,6 +191,9 @@ func NewTextView() *TextView {
// scrollable. If true, text is kept in a buffer and can be navigated. If false,
// the last line will always be visible.
func (t *TextView) SetScrollable(scrollable bool) *TextView {
t.Lock()
defer t.Unlock()
t.scrollable = scrollable
if !scrollable {
t.trackEnd = true
@ -202,6 +205,9 @@ func (t *TextView) SetScrollable(scrollable bool) *TextView {
// available width being wrapped onto the next line. If false, any characters
// beyond the available width are not displayed.
func (t *TextView) SetWrap(wrap bool) *TextView {
t.Lock()
defer t.Unlock()
if t.wrap != wrap {
t.index = nil
}
@ -215,6 +221,9 @@ func (t *TextView) SetWrap(wrap bool) *TextView {
//
// This flag is ignored if the "wrap" flag is false.
func (t *TextView) SetWordWrap(wrapOnWords bool) *TextView {
t.Lock()
defer t.Unlock()
if t.wordWrap != wrapOnWords {
t.index = nil
}
@ -225,6 +234,9 @@ func (t *TextView) SetWordWrap(wrapOnWords bool) *TextView {
// SetTextAlign sets the text alignment within the text view. This must be
// either AlignLeft, AlignCenter, or AlignRight.
func (t *TextView) SetTextAlign(align int) *TextView {
t.Lock()
defer t.Unlock()
if t.align != align {
t.index = nil
}
@ -236,6 +248,9 @@ func (t *TextView) SetTextAlign(align int) *TextView {
// dynamically by sending color strings in square brackets to the text view if
// dynamic colors are enabled).
func (t *TextView) SetTextColor(color tcell.Color) *TextView {
t.Lock()
defer t.Unlock()
t.textColor = color
return t
}
@ -251,6 +266,9 @@ func (t *TextView) SetText(text string) *TextView {
// GetText returns the current text of this text view. If "stripTags" is set
// to true, any region/color tags are stripped from the text.
func (t *TextView) GetText(stripTags bool) string {
t.RLock()
defer t.RUnlock()
// Get the buffer.
buffer := t.buffer
if !stripTags {
@ -279,6 +297,9 @@ func (t *TextView) GetText(stripTags bool) string {
// SetDynamicColors sets the flag that allows the text color to be changed
// dynamically. See class description for details.
func (t *TextView) SetDynamicColors(dynamic bool) *TextView {
t.Lock()
defer t.Unlock()
if t.dynamicColors != dynamic {
t.index = nil
}
@ -289,6 +310,9 @@ func (t *TextView) SetDynamicColors(dynamic bool) *TextView {
// SetRegions sets the flag that allows to define regions in the text. See class
// description for details.
func (t *TextView) SetRegions(regions bool) *TextView {
t.Lock()
defer t.Unlock()
if t.regions != regions {
t.index = nil
}
@ -313,6 +337,9 @@ func (t *TextView) SetRegions(regions bool) *TextView {
//
// See package description for details on dealing with concurrency.
func (t *TextView) SetChangedFunc(handler func()) *TextView {
t.Lock()
defer t.Unlock()
t.changed = handler
return t
}
@ -321,12 +348,18 @@ func (t *TextView) SetChangedFunc(handler func()) *TextView {
// following keys: Escape, Enter, Tab, Backtab. The key is passed to the
// handler.
func (t *TextView) SetDoneFunc(handler func(key tcell.Key)) *TextView {
t.Lock()
defer t.Unlock()
t.done = handler
return t
}
// ScrollTo scrolls to the specified row and column (both starting with 0).
func (t *TextView) ScrollTo(row, column int) *TextView {
t.Lock()
defer t.Unlock()
if !t.scrollable {
return t
}
@ -339,6 +372,9 @@ func (t *TextView) ScrollTo(row, column int) *TextView {
// ScrollToBeginning scrolls to the top left corner of the text if the text view
// is scrollable.
func (t *TextView) ScrollToBeginning() *TextView {
t.Lock()
defer t.Unlock()
if !t.scrollable {
return t
}
@ -352,6 +388,9 @@ func (t *TextView) ScrollToBeginning() *TextView {
// is scrollable. Adding new rows to the end of the text view will cause it to
// scroll with the new data.
func (t *TextView) ScrollToEnd() *TextView {
t.Lock()
defer t.Unlock()
if !t.scrollable {
return t
}
@ -363,11 +402,17 @@ func (t *TextView) ScrollToEnd() *TextView {
// GetScrollOffset returns the number of rows and columns that are skipped at
// the top left corner when the text view has been scrolled.
func (t *TextView) GetScrollOffset() (row, column int) {
t.RLock()
defer t.RUnlock()
return t.lineOffset, t.columnOffset
}
// Clear removes all text from the buffer.
func (t *TextView) Clear() *TextView {
t.Lock()
defer t.Unlock()
t.buffer = nil
t.recentBytes = nil
t.index = nil
@ -383,6 +428,9 @@ func (t *TextView) Clear() *TextView {
// Calling this function will remove any previous highlights. To remove all
// highlights, call this function without any arguments.
func (t *TextView) Highlight(regionIDs ...string) *TextView {
t.Lock()
defer t.Unlock()
t.highlights = make(map[string]struct{})
for _, id := range regionIDs {
if id == "" {
@ -396,6 +444,9 @@ func (t *TextView) Highlight(regionIDs ...string) *TextView {
// GetHighlights returns the IDs of all currently highlighted regions.
func (t *TextView) GetHighlights() (regionIDs []string) {
t.RLock()
defer t.RUnlock()
for id := range t.highlights {
regionIDs = append(regionIDs, id)
}
@ -411,6 +462,9 @@ func (t *TextView) GetHighlights() (regionIDs []string) {
// Nothing happens if there are no highlighted regions or if the text view is
// not scrollable.
func (t *TextView) ScrollToHighlight() *TextView {
t.Lock()
defer t.Unlock()
if len(t.highlights) == 0 || !t.scrollable || !t.regions {
return t
}
@ -427,6 +481,9 @@ func (t *TextView) ScrollToHighlight() *TextView {
// If the region does not exist or if regions are turned off, an empty string
// is returned.
func (t *TextView) GetRegionText(regionID string) string {
t.RLock()
defer t.RUnlock()
if !t.regions || regionID == "" {
return ""
}
@ -494,18 +551,20 @@ func (t *TextView) GetRegionText(regionID string) string {
// Focus is called when this primitive receives focus.
func (t *TextView) Focus(delegate func(p Primitive)) {
// Implemented here with locking because this is used by layout primitives.
t.Lock()
defer t.Unlock()
// Implemented here with locking because this is used by layout primitives.
t.hasFocus = true
}
// HasFocus returns whether or not this primitive has focus.
func (t *TextView) HasFocus() bool {
t.RLock()
defer t.RUnlock()
// Implemented here with locking because this may be used in the "changed"
// callback.
t.Lock()
defer t.Unlock()
return t.hasFocus
}
@ -513,15 +572,12 @@ func (t *TextView) HasFocus() bool {
// replaced with TabSize space characters. A "\n" or "\r\n" will be interpreted
// as a new line.
func (t *TextView) Write(p []byte) (n int, err error) {
// Notify at the end.
t.Lock()
changed := t.changed
t.Unlock()
if changed != nil {
defer changed() // Deadlocks may occur if we lock here.
// Notify at the end.
defer changed()
}
t.Lock()
defer t.Unlock()
// Copy data over.
@ -749,9 +805,10 @@ func (t *TextView) reindexBuffer(width int) {
// Draw draws this primitive onto the screen.
func (t *TextView) Draw(screen tcell.Screen) {
t.Box.Draw(screen)
t.Lock()
defer t.Unlock()
t.Box.Draw(screen)
// Get the available size.
x, y, width, height := t.GetInnerRect()
@ -971,6 +1028,9 @@ func (t *TextView) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
return
}
t.Lock()
defer t.Unlock()
if !t.scrollable {
return
}

View File

@ -1,6 +1,8 @@
package cview
import (
"sync"
"github.com/gdamore/tcell"
)
@ -49,6 +51,8 @@ type TreeNode struct {
level int // The hierarchy level (0 for the root, 1 for its children, and so on).
graphicsX int // The x-coordinate of the left-most graphics rune.
textX int // The x-coordinate of the first rune of the text.
sync.Mutex
}
// NewTreeNode returns a new tree node.
@ -68,6 +72,13 @@ func NewTreeNode(text string) *TreeNode {
// The callback returns whether traversal should continue with the traversed
// node's child nodes (true) or not recurse any deeper (false).
func (n *TreeNode) Walk(callback func(node, parent *TreeNode) bool) *TreeNode {
n.Lock()
defer n.Unlock()
return n.walk(callback)
}
func (n *TreeNode) walk(callback func(node, parent *TreeNode) bool) *TreeNode {
n.parent = nil
nodes := []*TreeNode{n}
for len(nodes) > 0 {
@ -93,39 +104,60 @@ func (n *TreeNode) Walk(callback func(node, parent *TreeNode) bool) *TreeNode {
// will allow you to establish a mapping between the TreeView hierarchy and your
// internal tree structure.
func (n *TreeNode) SetReference(reference interface{}) *TreeNode {
n.Lock()
defer n.Unlock()
n.reference = reference
return n
}
// GetReference returns this node's reference object.
func (n *TreeNode) GetReference() interface{} {
n.Lock()
defer n.Unlock()
return n.reference
}
// SetChildren sets this node's child nodes.
func (n *TreeNode) SetChildren(childNodes []*TreeNode) *TreeNode {
n.Lock()
defer n.Unlock()
n.children = childNodes
return n
}
// GetText returns this node's text.
func (n *TreeNode) GetText() string {
n.Lock()
defer n.Unlock()
return n.text
}
// GetChildren returns this node's children.
func (n *TreeNode) GetChildren() []*TreeNode {
n.Lock()
defer n.Unlock()
return n.children
}
// ClearChildren removes all child nodes from this node.
func (n *TreeNode) ClearChildren() *TreeNode {
n.Lock()
defer n.Unlock()
n.children = nil
return n
}
// AddChild adds a new child node to this node.
func (n *TreeNode) AddChild(node *TreeNode) *TreeNode {
n.Lock()
defer n.Unlock()
n.children = append(n.children, node)
return n
}
@ -133,6 +165,9 @@ func (n *TreeNode) AddChild(node *TreeNode) *TreeNode {
// SetSelectable sets a flag indicating whether this node can be focused and
// selected by the user.
func (n *TreeNode) SetSelectable(selectable bool) *TreeNode {
n.Lock()
defer n.Unlock()
n.selectable = selectable
return n
}
@ -142,6 +177,9 @@ func (n *TreeNode) SetSelectable(selectable bool) *TreeNode {
//
// This function is also called when the user selects this node.
func (n *TreeNode) SetFocusedFunc(handler func()) *TreeNode {
n.Lock()
defer n.Unlock()
n.focused = handler
return n
}
@ -149,24 +187,36 @@ func (n *TreeNode) SetFocusedFunc(handler func()) *TreeNode {
// SetSelectedFunc sets a function which is called when the user selects this
// node by hitting Enter when it is focused.
func (n *TreeNode) SetSelectedFunc(handler func()) *TreeNode {
n.Lock()
defer n.Unlock()
n.selected = handler
return n
}
// SetExpanded sets whether or not this node's child nodes should be displayed.
func (n *TreeNode) SetExpanded(expanded bool) *TreeNode {
n.Lock()
defer n.Unlock()
n.expanded = expanded
return n
}
// Expand makes the child nodes of this node appear.
func (n *TreeNode) Expand() *TreeNode {
n.Lock()
defer n.Unlock()
n.expanded = true
return n
}
// Collapse makes the child nodes of this node disappear.
func (n *TreeNode) Collapse() *TreeNode {
n.Lock()
defer n.Unlock()
n.expanded = false
return n
}
@ -191,22 +241,34 @@ func (n *TreeNode) CollapseAll() *TreeNode {
// IsExpanded returns whether the child nodes of this node are visible.
func (n *TreeNode) IsExpanded() bool {
n.Lock()
defer n.Unlock()
return n.expanded
}
// SetText sets the node's text which is displayed.
func (n *TreeNode) SetText(text string) *TreeNode {
n.Lock()
defer n.Unlock()
n.text = text
return n
}
// GetColor returns the node's color.
func (n *TreeNode) GetColor() tcell.Color {
n.Lock()
defer n.Unlock()
return n.color
}
// SetColor sets the node's text color.
func (n *TreeNode) SetColor(color tcell.Color) *TreeNode {
n.Lock()
defer n.Unlock()
n.color = color
return n
}
@ -215,6 +277,9 @@ func (n *TreeNode) SetColor(color tcell.Color) *TreeNode {
// keeps the text as far left as possible with a minimum of line graphics. Any
// value greater than that moves the text to the right.
func (n *TreeNode) SetIndent(indent int) *TreeNode {
n.Lock()
defer n.Unlock()
n.indent = indent
return n
}
@ -299,6 +364,8 @@ type TreeView struct {
// The visible nodes, top-down, as set by process().
nodes []*TreeNode
sync.Mutex
}
// NewTreeView returns a new tree view.
@ -314,6 +381,9 @@ func NewTreeView() *TreeView {
// SetRoot sets the root node of the tree.
func (t *TreeView) SetRoot(root *TreeNode) *TreeView {
t.Lock()
defer t.Unlock()
t.root = root
return t
}
@ -321,6 +391,9 @@ func (t *TreeView) SetRoot(root *TreeNode) *TreeView {
// GetRoot returns the root node of the tree. If no such node was previously
// set, nil is returned.
func (t *TreeView) GetRoot() *TreeNode {
t.Lock()
defer t.Unlock()
return t.root
}
@ -330,9 +403,14 @@ func (t *TreeView) GetRoot() *TreeNode {
//
// This function does NOT trigger the "changed" callback.
func (t *TreeView) SetCurrentNode(node *TreeNode) *TreeView {
t.Lock()
defer t.Unlock()
t.currentNode = node
if t.currentNode.focused != nil {
t.Unlock()
t.currentNode.focused()
t.Lock()
}
return t
}
@ -340,6 +418,9 @@ func (t *TreeView) SetCurrentNode(node *TreeNode) *TreeView {
// GetCurrentNode returns the currently selected node or nil of no node is
// currently selected.
func (t *TreeView) GetCurrentNode() *TreeNode {
t.Lock()
defer t.Unlock()
return t.currentNode
}
@ -347,6 +428,9 @@ func (t *TreeView) GetCurrentNode() *TreeNode {
// root, 1 to the root's child nodes, and so on. Nodes above the top level are
// not displayed.
func (t *TreeView) SetTopLevel(topLevel int) *TreeView {
t.Lock()
defer t.Unlock()
t.topLevel = topLevel
return t
}
@ -361,6 +445,9 @@ func (t *TreeView) SetTopLevel(topLevel int) *TreeView {
// treeView.SetGraphics(false).
// SetPrefixes([]string{"* ", "- ", "x "})
func (t *TreeView) SetPrefixes(prefixes []string) *TreeView {
t.Lock()
defer t.Unlock()
t.prefixes = prefixes
return t
}
@ -369,6 +456,9 @@ func (t *TreeView) SetPrefixes(prefixes []string) *TreeView {
// all texts except that of top-level nodes will be placed in the same column.
// If set to false, they will indent with the hierarchy.
func (t *TreeView) SetAlign(align bool) *TreeView {
t.Lock()
defer t.Unlock()
t.align = align
return t
}
@ -376,24 +466,36 @@ func (t *TreeView) SetAlign(align bool) *TreeView {
// SetGraphics sets a flag which determines whether or not line graphics are
// drawn to illustrate the tree's hierarchy.
func (t *TreeView) SetGraphics(showGraphics bool) *TreeView {
t.Lock()
defer t.Unlock()
t.graphics = showGraphics
return t
}
// SetGraphicsColor sets the colors of the lines used to draw the tree structure.
func (t *TreeView) SetGraphicsColor(color tcell.Color) *TreeView {
t.Lock()
defer t.Unlock()
t.graphicsColor = color
return t
}
// SetScrollBarVisibility specifies the display of the scroll bar.
func (t *TreeView) SetScrollBarVisibility(visibility ScrollBarVisibility) *TreeView {
t.Lock()
defer t.Unlock()
t.scrollBarVisibility = visibility
return t
}
// SetScrollBarColor sets the color of the scroll bar.
func (t *TreeView) SetScrollBarColor(color tcell.Color) *TreeView {
t.Lock()
defer t.Unlock()
t.scrollBarColor = color
return t
}
@ -401,6 +503,9 @@ func (t *TreeView) SetScrollBarColor(color tcell.Color) *TreeView {
// SetChangedFunc sets the function which is called when the user navigates to
// a new tree node.
func (t *TreeView) SetChangedFunc(handler func(node *TreeNode)) *TreeView {
t.Lock()
defer t.Unlock()
t.changed = handler
return t
}
@ -408,6 +513,9 @@ func (t *TreeView) SetChangedFunc(handler func(node *TreeNode)) *TreeView {
// SetSelectedFunc sets the function which is called when the user selects a
// node by pressing Enter on the current selection.
func (t *TreeView) SetSelectedFunc(handler func(node *TreeNode)) *TreeView {
t.Lock()
defer t.Unlock()
t.selected = handler
return t
}
@ -415,6 +523,9 @@ func (t *TreeView) SetSelectedFunc(handler func(node *TreeNode)) *TreeView {
// SetDoneFunc sets a handler which is called whenever the user presses the
// Escape, Tab, or Backtab key.
func (t *TreeView) SetDoneFunc(handler func(key tcell.Key)) *TreeView {
t.Lock()
defer t.Unlock()
t.done = handler
return t
}
@ -423,6 +534,9 @@ func (t *TreeView) SetDoneFunc(handler func(key tcell.Key)) *TreeView {
// of the tree view. Note that when the user navigates the tree view, this value
// is only updated after the tree view has been redrawn.
func (t *TreeView) GetScrollOffset() int {
t.Lock()
defer t.Unlock()
return t.offsetY
}
@ -431,6 +545,9 @@ func (t *TreeView) GetScrollOffset() int {
// of collapsed nodes. Note that this value is only up to date after the tree
// view has been drawn.
func (t *TreeView) GetRowCount() int {
t.Lock()
defer t.Unlock()
return len(t.nodes)
}
@ -447,7 +564,7 @@ func (t *TreeView) process() {
if t.graphics {
graphicsOffset = 1
}
t.root.Walk(func(node, parent *TreeNode) bool {
t.root.walk(func(node, parent *TreeNode) bool {
// Set node attributes.
node.parent = parent
if parent == nil {
@ -570,10 +687,14 @@ func (t *TreeView) process() {
if newSelectedIndex != selectedIndex {
t.movement = treeNone
if t.changed != nil {
t.Unlock()
t.changed(t.currentNode)
t.Lock()
}
if t.currentNode.focused != nil {
t.Unlock()
t.currentNode.focused()
t.Lock()
}
}
selectedIndex = newSelectedIndex
@ -605,6 +726,10 @@ func (t *TreeView) process() {
// Draw draws this primitive onto the screen.
func (t *TreeView) Draw(screen tcell.Screen) {
t.Box.Draw(screen)
t.Lock()
defer t.Unlock()
if t.root == nil {
return
}
@ -720,25 +845,35 @@ func (t *TreeView) Draw(screen tcell.Screen) {
func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
selectNode := func() {
if t.currentNode != nil {
if t.selected != nil {
t.selected(t.currentNode)
}
if t.currentNode.focused != nil {
t.currentNode.focused()
}
if t.currentNode.selected != nil {
t.currentNode.selected()
}
t.Lock()
currentNode := t.currentNode
t.Unlock()
if currentNode == nil {
return
}
if t.selected != nil {
t.selected(currentNode)
}
if currentNode.focused != nil {
currentNode.focused()
}
if currentNode.selected != nil {
currentNode.selected()
}
}
t.Lock()
defer t.Unlock()
// Because the tree is flattened into a list only at drawing time, we also
// postpone the (selection) movement to drawing time.
switch key := event.Key(); key {
case tcell.KeyTab, tcell.KeyBacktab, tcell.KeyEscape:
if t.done != nil {
t.Unlock()
t.done(key)
t.Lock()
}
case tcell.KeyDown, tcell.KeyRight:
t.movement = treeDown
@ -763,10 +898,14 @@ func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
case 'k':
t.movement = treeUp
case ' ':
t.Unlock()
selectNode()
t.Lock()
}
case tcell.KeyEnter:
t.Unlock()
selectNode()
t.Lock()
}
t.process()