diff --git a/changelog b/changelog index e1dfaec..f5f4b56 100644 --- a/changelog +++ b/changelog @@ -1,4 +1,13 @@ -2018-01-23 - version 0.7.0 +2018-01-25 - version 0.7.0 +[+] Added new methods to Composer: BeginUpdate and EndUpdate to use in + multithreading application. Call BeginUpdate before creating a new Window or + manipulating Composer Windows if the code runs in separate thread. And + call EndUpdate right after all changes are done. Do not lock for a long + time because while lock is on the screen is not updated. + Another usage: create new Window inside varios handlers (e.g, OnSelectItem + of ListBox) + +2018-01-23 - version 0.7.0 [+] Added feature: Window method to set and read visible buttons in title bar [+] Added feature: Window manual resizing and moving can be disabled [*] Fix TableView scrollbars: both scrollbars did not response mouse clicks diff --git a/composer.go b/composer.go index cf8f599..004e321 100644 --- a/composer.go +++ b/composer.go @@ -2,6 +2,7 @@ package clui import ( term "github.com/nsf/termbox-go" + "sync" ) // Composer is a service object that manages Views and console, processes @@ -22,6 +23,8 @@ type Composer struct { lastX, lastY int // Type of dragging dragType DragType + // For safe Window manipulations + mtx sync.RWMutex } var ( @@ -66,16 +69,21 @@ func termboxEventToLocal(ev term.Event) Event { // Repaints everything on the screen func RefreshScreen() { + comp.BeginUpdate() term.Clear(ColorWhite, ColorBlack) + comp.EndUpdate() - for _, wnd := range comp.windows { - v := comp.topWindow().(*Window) + windows := comp.getWindowList() + for _, wnd := range windows { + v := wnd.(*Window) if v.Visible() { wnd.Draw() } } + comp.BeginUpdate() term.Flush() + comp.EndUpdate() } // AddWindow constucts a new Window, adds it to the composer automatically, @@ -86,7 +94,9 @@ func RefreshScreen() { func AddWindow(posX, posY, width, height int, title string) *Window { window := CreateWindow(posX, posY, width, height, title) + comp.BeginUpdate() comp.windows = append(comp.windows, window) + comp.EndUpdate() window.Draw() comp.activateWindow(window) @@ -96,13 +106,41 @@ func AddWindow(posX, posY, width, height int, title string) *Window { return window } +// BeginUpdate locks any screen update until EndUpdate is called. +// Useful only in multithreading application if you create a new Window in +// some thread that is not main one (e.g, create new Window inside +// OnSelectItem handler of ListBox) +// Note: Do not lock for a long time because while the lock is on the screen is +// not updated +func (c *Composer) BeginUpdate() { + c.mtx.Lock() +} + +// EndUpdate unlocks the screen for any manipulations. +// Useful only in multithreading application if you create a new Window in +// some thread that is not main one (e.g, create new Window inside +// OnSelectItem handler of ListBox) +func (c *Composer) EndUpdate() { + c.mtx.Unlock() +} + +func (c *Composer) getWindowList() []Control { + c.mtx.RLock() + defer c.mtx.RUnlock() + + arr_copy := make([]Control, len(c.windows)) + copy(arr_copy, c.windows) + return arr_copy +} + func (c *Composer) checkWindowUnderMouse(screenX, screenY int) (Control, HitResult) { - if len(c.windows) == 0 { + windows := c.getWindowList() + if len(windows) == 0 { return nil, HitOutside } - for i := len(c.windows) - 1; i >= 0; i-- { - window := c.windows[i] + for i := len(windows) - 1; i >= 0; i-- { + window := windows[i] hit := window.HitTest(screenX, screenY) if hit != HitOutside { return window, hit @@ -113,8 +151,9 @@ func (c *Composer) checkWindowUnderMouse(screenX, screenY int) (Control, HitResu } func (c *Composer) activateWindow(window Control) bool { + windows := c.getWindowList() if c.topWindow() == window { - for _, v := range c.windows { + for _, v := range windows { v.SetActive(false) } window.SetActive(true) @@ -124,7 +163,7 @@ func (c *Composer) activateWindow(window Control) bool { var wList []Control found := false - for _, v := range c.windows { + for _, v := range windows { if v != window { v.SetActive(false) wList = append(wList, v) @@ -138,12 +177,15 @@ func (c *Composer) activateWindow(window Control) bool { } window.SetActive(true) + c.BeginUpdate() + defer c.EndUpdate() c.windows = append(wList, window) return true } func (c *Composer) moveActiveWindowToBottom() bool { - if len(c.windows) < 2 { + windows := c.getWindowList() + if len(windows) < 2 { return false } @@ -152,7 +194,7 @@ func (c *Composer) moveActiveWindowToBottom() bool { } anyVisible := false - for _, w := range c.windows { + for _, w := range windows { v := w.(*Window) if v.Visible() { anyVisible = true @@ -168,10 +210,12 @@ func (c *Composer) moveActiveWindowToBottom() bool { for { last := c.topWindow() + c.BeginUpdate() for i := len(c.windows) - 1; i > 0; i-- { c.windows[i] = c.windows[i-1] } c.windows[0] = last + c.EndUpdate() v := c.topWindow().(*Window) if v.Visible() { @@ -200,11 +244,13 @@ func (c *Composer) sendEventToActiveWindow(ev Event) bool { } func (c *Composer) topWindow() Control { - if len(c.windows) == 0 { + windows := c.getWindowList() + + if len(windows) == 0 { return nil } - return c.windows[len(c.windows)-1] + return windows[len(windows)-1] } func (c *Composer) resizeTopWindow(ev Event) bool { @@ -242,34 +288,32 @@ func (c *Composer) resizeTopWindow(ev Event) bool { } func (c *Composer) moveTopWindow(ev Event) bool { - if len(c.windows) > 0 { - view := c.topWindow() - if view != nil { - topwindow, ok := view.(*Window) - if ok && !topwindow.Movable() { - return false - } + view := c.topWindow() + if view != nil { + topwindow, ok := view.(*Window) + if ok && !topwindow.Movable() { + return false + } - x, y := view.Pos() - w, h := view.Size() - x1, y1 := x, y - cx, cy := term.Size() - if ev.Key == term.KeyArrowUp && y > 0 { - y-- - } else if ev.Key == term.KeyArrowDown && y+h < cy { - y++ - } else if ev.Key == term.KeyArrowLeft && x > 0 { - x-- - } else if ev.Key == term.KeyArrowRight && x+w < cx { - x++ - } + x, y := view.Pos() + w, h := view.Size() + x1, y1 := x, y + cx, cy := term.Size() + if ev.Key == term.KeyArrowUp && y > 0 { + y-- + } else if ev.Key == term.KeyArrowDown && y+h < cy { + y++ + } else if ev.Key == term.KeyArrowLeft && x > 0 { + x-- + } else if ev.Key == term.KeyArrowRight && x+w < cx { + x++ + } - if x1 != x || y1 != y { - view.SetPos(x, y) - event := Event{Type: EventMove, X: x, Y: y} - c.sendEventToActiveWindow(event) - RefreshScreen() - } + if x1 != x || y1 != y { + view.SetPos(x, y) + event := Event{Type: EventMove, X: x, Y: y} + c.sendEventToActiveWindow(event) + RefreshScreen() } return true } @@ -525,13 +569,16 @@ func (c *Composer) DestroyWindow(view Control) { ev := Event{Type: EventClose} c.sendEventToActiveWindow(ev) + windows := c.getWindowList() var newOrder []Control - for i := 0; i < len(c.windows); i++ { - if c.windows[i] != view { - newOrder = append(newOrder, c.windows[i]) + for i := 0; i < len(windows); i++ { + if windows[i] != view { + newOrder = append(newOrder, windows[i]) } } + c.BeginUpdate() c.windows = newOrder + c.EndUpdate() c.activateWindow(c.topWindow()) } diff --git a/dialog.go b/dialog.go index 317acdc..fe3269d 100644 --- a/dialog.go +++ b/dialog.go @@ -59,6 +59,8 @@ func CreateConfirmationDialog(title, question string, buttons []string, defaultB cw, ch := term.Size() dlg.View = AddWindow(cw/2-12, ch/2-8, 30, 3, title) + WindowManager().BeginUpdate() + defer WindowManager().EndUpdate() dlg.View.SetConstraints(30, 3) dlg.View.SetModal(true) dlg.View.SetPack(Vertical) @@ -78,9 +80,13 @@ func CreateConfirmationDialog(title, question string, buttons []string, defaultB btn1 := CreateButton(frm1, AutoSize, AutoSize, bText, Fixed) btn1.OnClick(func(ev Event) { dlg.result = DialogButton1 + WindowManager().DestroyWindow(dlg.View) - if dlg.onClose != nil { - go dlg.onClose() + WindowManager().BeginUpdate() + closeFunc := dlg.onClose + WindowManager().EndUpdate() + if closeFunc != nil { + go closeFunc() } }) var btn2, btn3 *Button @@ -137,6 +143,8 @@ func CreateConfirmationDialog(title, question string, buttons []string, defaultB // OnClose sets the callback that is called when the // dialog is closed func (d *ConfirmationDialog) OnClose(fn func()) { + WindowManager().BeginUpdate() + defer WindowManager().EndUpdate() d.onClose = fn } @@ -170,6 +178,8 @@ func CreateSelectDialog(title string, items []string, selectedItem int, typ Sele dlg.typ = typ dlg.View = AddWindow(cw/2-12, ch/2-8, 20, 10, title) + WindowManager().BeginUpdate() + defer WindowManager().EndUpdate() dlg.View.SetModal(true) dlg.View.SetPack(Vertical) @@ -248,6 +258,8 @@ func CreateSelectDialog(title string, items []string, selectedItem int, typ Sele // OnClose sets the callback that is called when the // dialog is closed func (d *SelectDialog) OnClose(fn func()) { + WindowManager().BeginUpdate() + defer WindowManager().EndUpdate() d.onClose = fn } diff --git a/listbox.go b/listbox.go index 31eaf8e..b205a60 100644 --- a/listbox.go +++ b/listbox.go @@ -253,9 +253,12 @@ func (l *ListBox) processMouseClick(ev Event) bool { } l.SelectItem(l.topLine + dy) - if l.onSelectItem != nil { + WindowManager().BeginUpdate() + onSelFunc := l.onSelectItem + WindowManager().EndUpdate() + if onSelFunc != nil { ev := Event{Y: l.topLine + dy, Msg: l.SelectedItemText()} - go l.onSelectItem(ev) + go onSelFunc(ev) } return true diff --git a/window.go b/window.go index 30f7b34..3c77a9a 100644 --- a/window.go +++ b/window.go @@ -125,6 +125,8 @@ func (wnd *Window) drawButtons() { } func (wnd *Window) Draw() { + WindowManager().BeginUpdate() + defer WindowManager().EndUpdate() PushAttributes() defer PopAttributes()