It is not complete patch because there are more places to fix. At least all demos and full-featured application termfb2 works without race condition warnings.

This commit is contained in:
Vladimir Markelov 2017-11-28 19:27:32 -08:00
parent bc8d838e86
commit 2c06a01186
10 changed files with 201 additions and 65 deletions

View File

@ -1 +1 @@
0.5.0
0.6.2

View File

@ -4,6 +4,7 @@ import (
"fmt"
xs "github.com/huandu/xstrings"
term "github.com/nsf/termbox-go"
"sync/atomic"
)
// BarData is info about one bar in the chart. Every
@ -59,10 +60,10 @@ type BarChart struct {
BaseControl
data []BarData
autosize bool
gap int
barWidth int
legendWidth int
valueWidth int
gap int32
barWidth int32
legendWidth int32
valueWidth int32
showMarks bool
showTitles bool
onDrawCell func(*BarDataCell)
@ -105,6 +106,9 @@ func CreateBarChart(parent Control, w, h int, scale int) *BarChart {
// Repaint draws the control on its View surface
func (b *BarChart) Draw() {
b.mtx.RLock()
defer b.mtx.RUnlock()
PushAttributes()
defer PopAttributes()
@ -214,7 +218,7 @@ func (b *BarChart) drawBars() {
DrawRawText(b.x+pos+shift, b.y+h+1, s)
}
pos += barW + b.gap
pos += barW + int(b.BarGap())
}
}
@ -242,7 +246,7 @@ func (b *BarChart) drawLegend() {
SetTextColor(d.Fg)
SetBackColor(d.Bg)
PutChar(b.x+pos+width, b.y+idx, c)
s := CutText(fmt.Sprintf(" - %v", d.Title), b.legendWidth)
s := CutText(fmt.Sprintf(" - %v", d.Title), int(b.LegendWidth()))
SetTextColor(fg)
SetBackColor(bg)
DrawRawText(b.x+pos+width+1, b.y+idx, s)
@ -250,7 +254,8 @@ func (b *BarChart) drawLegend() {
}
func (b *BarChart) drawValues() {
if b.valueWidth <= 0 {
valVal := int(b.ValueWidth())
if valVal <= 0 {
return
}
@ -266,11 +271,11 @@ func (b *BarChart) drawValues() {
}
dy := 0
format := fmt.Sprintf("%%%v.2f", b.valueWidth)
format := fmt.Sprintf("%%%v.2f", valVal)
for dy < h-1 {
v := float64(h-dy) / float64(h) * max
s := fmt.Sprintf(format, v)
s = CutText(s, b.valueWidth)
s = CutText(s, valVal)
DrawRawText(b.x, b.y+dy, s)
dy += 2
@ -278,7 +283,7 @@ func (b *BarChart) drawValues() {
}
func (b *BarChart) drawRulers() {
if b.valueWidth <= 0 && b.legendWidth <= 0 && !b.showTitles {
if int(b.ValueWidth()) <= 0 && int(b.LegendWidth()) <= 0 && !b.showTitles {
return
}
@ -314,13 +319,15 @@ func (b *BarChart) calculateBarArea() (int, int) {
w := b.width
pos := 0
if b.valueWidth < w/2 {
w = w - b.valueWidth - 1
pos = b.valueWidth + 1
valVal := int(b.ValueWidth())
if valVal < w/2 {
w = w - valVal - 1
pos = valVal + 1
}
if b.legendWidth < w/2 {
w -= b.legendWidth
legVal := int(b.LegendWidth())
if legVal < w/2 {
w -= legVal
}
return pos, w
@ -332,24 +339,28 @@ func (b *BarChart) calculateBarWidth() int {
}
if !b.autosize {
return b.barWidth
return int(b.MinBarWidth())
}
w := b.width
if b.valueWidth < w/2 {
w = w - b.valueWidth - 1
legVal := int(b.LegendWidth())
valVal := int(b.ValueWidth())
if valVal < w/2 {
w = w - valVal - 1
}
if b.legendWidth < w/2 {
w -= b.legendWidth
if legVal < w/2 {
w -= legVal
}
dataCount := len(b.data)
minSize := dataCount*b.barWidth + (dataCount-1)*b.gap
gapVal := int(b.BarGap())
barVal := int(b.MinBarWidth())
minSize := dataCount*barVal + (dataCount-1)*gapVal
if minSize >= w {
return b.barWidth
return barVal
}
sz := (w - (dataCount-1)*b.gap) / dataCount
sz := (w - (dataCount-1)*gapVal) / dataCount
if sz == 0 {
sz = 1
}
@ -383,16 +394,25 @@ func (b *BarChart) calculateMultiplier() (float64, float64) {
// AddData appends a new bar to a chart
func (b *BarChart) AddData(val BarData) {
b.mtx.Lock()
defer b.mtx.Unlock()
b.data = append(b.data, val)
}
// ClearData removes all bar from chart
func (b *BarChart) ClearData() {
b.mtx.Lock()
defer b.mtx.Unlock()
b.data = make([]BarData, 0)
}
// SetData assign a new bar list to a chart
func (b *BarChart) SetData(data []BarData) {
b.mtx.Lock()
defer b.mtx.Unlock()
b.data = make([]BarData, len(data))
copy(b.data, data)
}
@ -410,39 +430,42 @@ func (b *BarChart) AutoSize() bool {
// SetAutoSize enables or disables automatic bar
// width calculation
func (b *BarChart) SetAutoSize(auto bool) {
b.mtx.Lock()
defer b.mtx.Unlock()
b.autosize = auto
}
// Gap returns width of visual gap between two adjacent bars
func (b *BarChart) BarGap() int {
return b.gap
func (b *BarChart) BarGap() int32 {
return atomic.LoadInt32(&b.gap)
}
// SetGap sets the space width between two adjacent bars
func (b *BarChart) SetBarGap(gap int) {
b.gap = gap
func (b *BarChart) SetBarGap(gap int32) {
atomic.StoreInt32(&b.gap, gap)
}
// MinBarWidth returns current minimal bar width
func (b *BarChart) MinBarWidth() int {
return b.barWidth
func (b *BarChart) MinBarWidth() int32 {
return atomic.LoadInt32(&b.barWidth)
}
// SetMinBarWidth changes the minimal bar width
func (b *BarChart) SetMinBarWidth(size int) {
b.barWidth = size
func (b *BarChart) SetMinBarWidth(size int32) {
atomic.StoreInt32(&b.barWidth, size)
}
// ValueWidth returns the width of the area at the left of
// chart used to draw values. Set it to 0 to turn off the
// value panel
func (b *BarChart) ValueWidth() int {
return b.valueWidth
func (b *BarChart) ValueWidth() int32 {
return atomic.LoadInt32(&b.valueWidth)
}
// SetValueWidth changes width of the value panel on the left
func (b *BarChart) SetValueWidth(width int) {
b.valueWidth = width
func (b *BarChart) SetValueWidth(width int32) {
atomic.StoreInt32(&b.valueWidth, width)
}
// ShowTitles returns if chart displays horizontal axis and
@ -453,18 +476,21 @@ func (b *BarChart) ShowTitles() bool {
// SetShowTitles turns on and off horizontal axis and bar titles
func (b *BarChart) SetShowTitles(show bool) {
b.mtx.Lock()
defer b.mtx.Unlock()
b.showTitles = show
}
// LegendWidth returns width of chart legend displayed at the
// right side of the chart. Set it to 0 to disable legend
func (b *BarChart) LegendWidth() int {
return b.legendWidth
func (b *BarChart) LegendWidth() int32 {
return atomic.LoadInt32(&b.legendWidth)
}
// SetLegendWidth sets new legend panel width
func (b *BarChart) SetLegendWidth(width int) {
b.legendWidth = width
func (b *BarChart) SetLegendWidth(width int32) {
atomic.StoreInt32(&b.legendWidth, width)
}
// OnDrawCell sets callback that allows to draw multicolored
@ -475,6 +501,9 @@ func (b *BarChart) SetLegendWidth(width int) {
// changing colors and rune makes sense. Changing anything else
// does not affect the chart
func (b *BarChart) OnDrawCell(fn func(*BarDataCell)) {
b.mtx.Lock()
defer b.mtx.Unlock()
b.onDrawCell = fn
}
@ -486,5 +515,8 @@ func (b *BarChart) ShowMarks() bool {
// SetShowMarks turns on and off marks under horizontal axis
func (b *BarChart) SetShowMarks(show bool) {
b.mtx.Lock()
defer b.mtx.Unlock()
b.showMarks = show
}

View File

@ -2,6 +2,7 @@ package clui
import (
term "github.com/nsf/termbox-go"
"sync"
)
// BaseControl is a base for all visible controls.
@ -26,6 +27,7 @@ type BaseControl struct {
gapX, gapY int
pack PackType
children []Control
mtx sync.RWMutex
}
func (c *BaseControl) Title() string {
@ -103,10 +105,16 @@ func (c *BaseControl) SetTabStop(tabstop bool) {
}
func (c *BaseControl) Enabled() bool {
c.mtx.RLock()
defer c.mtx.RUnlock()
return !c.disabled
}
func (c *BaseControl) SetEnabled(enabled bool) {
c.mtx.Lock()
defer c.mtx.Unlock()
c.disabled = !enabled
}

View File

@ -4,6 +4,7 @@ import (
xs "github.com/huandu/xstrings"
term "github.com/nsf/termbox-go"
"time"
"sync/atomic"
)
/*
@ -15,7 +16,7 @@ type Button struct {
BaseControl
shadowColor term.Attribute
bgActive term.Attribute
pressed bool
pressed int32
onClick func(Event)
}
@ -62,6 +63,8 @@ func CreateButton(parent Control, width, height int, title string, scale int) *B
// Repaint draws the control on its View surface
func (b *Button) Draw() {
b.mtx.RLock()
defer b.mtx.RUnlock()
PushAttributes()
defer PopAttributes()
@ -70,7 +73,7 @@ func (b *Button) Draw() {
fg, bg := b.fg, b.bg
shadow := RealColor(b.shadowColor, ColorButtonShadow)
if !b.Enabled() {
if b.disabled {
fg, bg = RealColor(fg, ColorButtonDisabledText), RealColor(bg, ColorButtonDisabledBack)
} else if b.Active() {
fg, bg = RealColor(b.fgActive, ColorButtonActiveText), RealColor(b.bgActive, ColorButtonActiveBack)
@ -81,7 +84,7 @@ func (b *Button) Draw() {
dy := int((h - 1) / 2)
SetTextColor(fg)
shift, text := AlignColorizedText(b.title, w-1, b.align)
if !b.pressed {
if b.isPressed() == 0 {
SetBackColor(shadow)
FillRect(x+1, y+1, w-1, h-1, ' ')
SetBackColor(bg)
@ -94,6 +97,14 @@ func (b *Button) Draw() {
}
}
func (b *Button) isPressed() int32 {
return atomic.LoadInt32(&b.pressed)
}
func (b *Button) setPressed(pressed int32) {
atomic.StoreInt32(&b.pressed, pressed)
}
/*
ProcessEvent processes all events come from the control parent. If a control
processes an event it should return true. If the method returns false it means
@ -106,39 +117,39 @@ func (b *Button) ProcessEvent(event Event) bool {
}
if event.Type == EventKey {
if event.Key == term.KeySpace && !b.pressed {
b.pressed = true
if event.Key == term.KeySpace && b.isPressed() == 0 {
b.setPressed(1)
ev := Event{Type: EventRedraw}
go func() {
b.Draw()
PutEvent(ev)
PutEvent(ev)
time.Sleep(100 * time.Millisecond)
b.pressed = false
b.Draw()
b.setPressed(0)
PutEvent(ev)
}()
if b.onClick != nil {
b.onClick(event)
}
return true
} else if event.Key == term.KeyEsc && b.pressed {
b.pressed = false
} else if event.Key == term.KeyEsc && b.isPressed() != 0 {
b.setPressed(0)
ReleaseEvents()
return true
}
} else if event.Type == EventMouse {
if event.Key == term.MouseLeft {
b.pressed = true
b.setPressed(1)
GrabEvents(b)
return true
} else if event.Key == term.MouseRelease && b.pressed {
} else if event.Key == term.MouseRelease && b.isPressed() != 0 {
ReleaseEvents()
if event.X >= b.x && event.Y >= b.y && event.X < b.x+b.width && event.Y < b.y+b.height {
if b.onClick != nil {
b.onClick(event)
}
}
b.pressed = false
b.setPressed(0)
return true
}
}

View File

@ -1,4 +1,8 @@
2017-09-07 - version 0.6.2
[*] Fix races (that racy tool shows). The work is not completed but all demos
and termfb2 application works without warnings
2017-09-07 - version 0.6.2
[*] Setting the first button of confirmation dialog as default one did not work
[*] TableView does not use 'go' to fire events that allows to create on the
fly any required dialog. That is useful, e.g., to create a simple

View File

@ -52,6 +52,9 @@ func CreateCheckBox(parent Control, width int, title string, scale int) *CheckBo
// Repaint draws the control on its View surface
func (c *CheckBox) Draw() {
c.mtx.RLock()
defer c.mtx.RUnlock()
PushAttributes()
defer PopAttributes()
@ -120,6 +123,9 @@ func (c *CheckBox) ProcessEvent(event Event) bool {
// Value must be 0 or 1 if Allow3State is off,
// and 0, 1, or 2 if Allow3State is on
func (c *CheckBox) SetState(val int) {
c.mtx.Lock()
defer c.mtx.Unlock()
if val == c.state {
return
}
@ -143,6 +149,9 @@ func (c *CheckBox) SetState(val int) {
// State returns current state of CheckBox
func (c *CheckBox) State() int {
c.mtx.RLock()
defer c.mtx.RUnlock()
return c.state
}
@ -185,5 +194,8 @@ func (c *CheckBox) SetSize(width, height int) {
// of the CheckBox is changed. Argument of callback is the current
// CheckBox state: 0 - off, 1 - on, 2 - third state
func (c *CheckBox) OnChange(fn func(int)) {
c.mtx.Lock()
defer c.mtx.Unlock()
c.onChange = fn
}

View File

@ -71,6 +71,8 @@ func CreateProgressBar(parent Control, width, height int, scale int) *ProgressBa
// pb.SetTitle("{{value}} of {{max}}")
// pb.SetTitle("{{percent}}%")
func (b *ProgressBar) Draw() {
b.mtx.RLock()
defer b.mtx.RUnlock()
if b.max <= b.min {
return
}
@ -162,6 +164,8 @@ func (b *ProgressBar) Draw() {
// SetValue sets new progress value. If value exceeds ProgressBar
// limits then the limit value is used
func (b *ProgressBar) SetValue(pos int) {
b.mtx.Lock()
defer b.mtx.Unlock()
if pos < b.min {
b.value = b.min
} else if pos > b.max {
@ -173,6 +177,8 @@ func (b *ProgressBar) SetValue(pos int) {
// Value returns the current ProgressBar value
func (b *ProgressBar) Value() int {
b.mtx.RLock()
defer b.mtx.RUnlock()
return b.value
}
@ -198,6 +204,8 @@ func (b *ProgressBar) SetLimits(min, max int) {
// Step increases ProgressBar value by 1 if the value is less
// than ProgressBar high limit
func (b *ProgressBar) Step() int {
b.mtx.Lock()
defer b.mtx.Unlock()
b.value++
if b.value > b.max {

View File

@ -72,6 +72,9 @@ func CreateSparkChart(parent Control, w, h int, scale int) *SparkChart {
// Repaint draws the control on its View surface
func (b *SparkChart) Draw() {
b.mtx.RLock()
defer b.mtx.RUnlock()
PushAttributes()
defer PopAttributes()
@ -212,6 +215,9 @@ func (b *SparkChart) calculateMultiplier() (float64, float64) {
// AddData appends a new bar to a chart
func (b *SparkChart) AddData(val float64) {
b.mtx.Lock()
defer b.mtx.Unlock()
b.data = append(b.data, val)
_, width := b.calculateBarArea()
@ -222,11 +228,17 @@ func (b *SparkChart) AddData(val float64) {
// ClearData removes all bar from chart
func (b *SparkChart) ClearData() {
b.mtx.Lock()
defer b.mtx.Unlock()
b.data = make([]float64, 0)
}
// SetData assign a new bar list to a chart
func (b *SparkChart) SetData(data []float64) {
b.mtx.Lock()
defer b.mtx.Unlock()
b.data = make([]float64, len(data))
copy(b.data, data)
@ -245,6 +257,9 @@ func (b *SparkChart) ValueWidth() int {
// SetValueWidth changes width of the value panel on the left
func (b *SparkChart) SetValueWidth(width int) {
b.mtx.Lock()
defer b.mtx.Unlock()
b.valueWidth = width
}
@ -257,6 +272,9 @@ func (b *SparkChart) Top() float64 {
// SetTop sets the theoretical highest value of data flow
// to scale the chart
func (b *SparkChart) SetTop(top float64) {
b.mtx.Lock()
defer b.mtx.Unlock()
b.topValue = top
}
@ -268,6 +286,9 @@ func (b *SparkChart) AutoScale() bool {
// SetAutoScale changes the way of scaling the data flow
func (b *SparkChart) SetAutoScale(auto bool) {
b.mtx.Lock()
defer b.mtx.Unlock()
b.autosize = auto
}
@ -280,5 +301,8 @@ func (b *SparkChart) HilitePeaks() bool {
// SetHilitePeaks enables or disables hiliting maximum
// values with different colors
func (b *SparkChart) SetHilitePeaks(hilite bool) {
b.mtx.Lock()
defer b.mtx.Unlock()
b.hiliteMax = hilite
}

View File

@ -348,6 +348,8 @@ func (l *TableView) drawCells() {
// Repaint draws the control on its View surface
func (l *TableView) Draw() {
l.mtx.RLock()
defer l.mtx.RUnlock()
PushAttributes()
defer PopAttributes()
@ -924,6 +926,9 @@ func (l *TableView) OnKeyPress(fn func(term.Key) bool) {
// OnDrawCell is called every time the table is going to display
// a cell
func (l *TableView) OnDrawCell(fn func(*ColumnDrawInfo)) {
l.mtx.Lock()
defer l.mtx.Unlock()
l.onDrawCell = fn
}

View File

@ -7,6 +7,7 @@ import (
"io/ioutil"
"os"
"strings"
"sync"
"unicode/utf8"
)
@ -87,6 +88,7 @@ const themeSuffix = ".theme"
var (
themeManager *ThemeManager
thememtx sync.RWMutex
)
// ThemeDesc is a detailed information about theme:
@ -122,6 +124,9 @@ func initThemeManager() {
// Reset removes all loaded themes from cache and reinitialize
// the default theme
func ThemeReset() {
thememtx.Lock()
defer thememtx.Unlock()
themeManager.current = defaultTheme
themeManager.themes = make(map[string]theme, 0)
@ -209,10 +214,12 @@ func ThemeReset() {
// The method panics if theme loop is detected - check if
// parent attribute is correct
func SysColor(color string) term.Attribute {
thememtx.RLock()
sch, ok := themeManager.themes[themeManager.current]
if !ok {
sch = themeManager.themes[defaultTheme]
}
thememtx.RUnlock()
clr, okclr := sch.colors[color]
if !okclr {
@ -226,9 +233,11 @@ func SysColor(color string) term.Attribute {
if sch.parent == "" {
break
}
themeManager.LoadTheme(sch.parent)
themeManager.loadTheme(sch.parent)
thememtx.RLock()
sch = themeManager.themes[sch.parent]
clr, okclr = sch.colors[color]
thememtx.RUnlock()
if ok {
break
@ -250,10 +259,12 @@ func SysColor(color string) term.Attribute {
// The method panics if theme loop is detected - check if
// parent attribute is correct
func SysObject(object string) string {
thememtx.RLock()
sch, ok := themeManager.themes[themeManager.current]
if !ok {
sch = themeManager.themes[defaultTheme]
}
thememtx.RUnlock()
obj, okobj := sch.objects[object]
if !okobj {
@ -268,9 +279,11 @@ func SysObject(object string) string {
break
}
themeManager.LoadTheme(sch.parent)
themeManager.loadTheme(sch.parent)
thememtx.RLock()
sch = themeManager.themes[sch.parent]
obj, okobj = sch.objects[object]
thememtx.RUnlock()
if ok {
break
@ -313,22 +326,32 @@ func ThemeNames() []string {
// CurrentTheme returns name of the current theme
func CurrentTheme() string {
thememtx.RLock()
defer thememtx.RUnlock()
return themeManager.current
}
// SetCurrentTheme changes the current theme.
// Returns false if changing failed - e.g, theme does not exist
func SetCurrentTheme(name string) bool {
if _, ok := themeManager.themes[name]; !ok {
thememtx.RLock()
_, ok := themeManager.themes[name]
thememtx.RUnlock()
if !ok {
tnames := ThemeNames()
for _, theme := range tnames {
if theme == name {
themeManager.LoadTheme(theme)
themeManager.loadTheme(theme)
break
}
}
}
thememtx.Lock()
defer thememtx.Unlock()
if _, ok := themeManager.themes[name]; ok {
themeManager.current = name
return true
@ -352,9 +375,12 @@ func SetThemePath(path string) {
ThemeReset()
}
// LoadTheme loads the theme if it is not in the cache already.
// If theme is in the cache LoadTheme does nothing
func (s *ThemeManager) LoadTheme(name string) {
// loadTheme loads the theme if it is not in the cache already.
// If theme is in the cache loadTheme does nothing
func (s *ThemeManager) loadTheme(name string) {
thememtx.Lock()
defer thememtx.Unlock()
if _, ok := s.themes[name]; ok {
return
}
@ -442,25 +468,31 @@ func (s *ThemeManager) LoadTheme(name string) {
s.themes[name] = theme
}
// ReLoadTheme refresh cache entry for the theme with new
// ReloadTheme refresh cache entry for the theme with new
// data loaded from file. Use it to apply theme changes on
// the fly without resetting manager or restarting application
func ReLoadTheme(name string) {
func ReloadTheme(name string) {
if name == defaultTheme {
// default theme cannot be reloaded
return
}
thememtx.Lock()
if _, ok := themeManager.themes[name]; ok {
delete(themeManager.themes, name)
}
thememtx.Unlock()
themeManager.LoadTheme(name)
themeManager.loadTheme(name)
}
// ThemeInfo returns detailed info about theme
func ThemeInfo(name string) ThemeDesc {
themeManager.LoadTheme(name)
themeManager.loadTheme(name)
thememtx.RLock()
defer thememtx.RUnlock()
var theme ThemeDesc
if t, ok := themeManager.themes[name]; !ok {
theme.parent = t.parent