diff --git a/VERSION b/VERSION index 7b83801..3b1fc8e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.0 \ No newline at end of file +0.6.2 \ No newline at end of file diff --git a/barchart.go b/barchart.go index bd6aa65..8a5f426 100644 --- a/barchart.go +++ b/barchart.go @@ -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 } diff --git a/base_control.go b/base_control.go index b23e3f2..1671b81 100644 --- a/base_control.go +++ b/base_control.go @@ -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 } diff --git a/button.go b/button.go index 4e3b7b2..02870c1 100644 --- a/button.go +++ b/button.go @@ -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 } } diff --git a/changelog b/changelog index 3fb5404..e5feeeb 100644 --- a/changelog +++ b/changelog @@ -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 diff --git a/checkbox.go b/checkbox.go index 240ddf0..26618be 100644 --- a/checkbox.go +++ b/checkbox.go @@ -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 } diff --git a/progressbar.go b/progressbar.go index 16414b2..8b9f0db 100644 --- a/progressbar.go +++ b/progressbar.go @@ -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 { diff --git a/sparkchart.go b/sparkchart.go index 4d1a6f4..670ca73 100644 --- a/sparkchart.go +++ b/sparkchart.go @@ -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 } diff --git a/tableview.go b/tableview.go index 431db81..eec3a0d 100644 --- a/tableview.go +++ b/tableview.go @@ -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 } diff --git a/theme.go b/theme.go index 4d80014..51ab3e3 100644 --- a/theme.go +++ b/theme.go @@ -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