clui/composer.go
Mark D Horn 7097e0a5f9 Allow for No Border on Window
Keeps current API as-is by defaulting to the new BorderAuto BorderStyle
which keeps the default borders on Windows and defaults on none on Frames.

Allows setting a Window Border to BorderNone for a frameless Window.

Need to allow setting the border default in the Window Manager to avoid
the border from being drawn and then removed.

Signed-off-by: Mark D Horn <mark.d.horn@intel.com>
2018-09-19 18:26:19 -07:00

736 lines
16 KiB
Go

package clui
import (
term "github.com/nsf/termbox-go"
"sync"
)
// Composer is a service object that manages Views and console, processes
// events, and provides service methods. One application must have only
// one object of this type
type Composer struct {
// list of visible Views
windows []Control
windowBorder BorderStyle
consumer Control
// last pressed key - to make repeatable actions simpler, e.g, at first
// one presses Ctrl+S and then just repeatedly presses arrow lest to
// resize Window
lastKey term.Key
// coordinates when the mouse button was down, e.g to detect
// mouse click
mdownX, mdownY int
// last processed coordinates: e.g, for mouse move
lastX, lastY int
// Type of dragging
dragType DragType
// For safe Window manipulations
mtx sync.RWMutex
}
var (
comp *Composer
)
func initComposer() {
comp = new(Composer)
comp.windows = make([]Control, 0)
comp.windowBorder = BorderAuto
comp.consumer = nil
comp.lastKey = term.KeyEsc
}
// WindowManager returns main Window manager (that is Composer). Use it at
// your own risk because it provides an access to some low level Window
// manipulations.
// Note: Now it is not thread safe to call Composer methods from a few threads.
func WindowManager() *Composer {
return comp
}
// GrabEvents makes control c as the exclusive event reciever. After calling
// this function the control will recieve all mouse and keyboard events even
// if it is not active or mouse is outside it. Useful to implement dragging
// or alike stuff
func GrabEvents(c Control) {
comp.consumer = c
}
// ReleaseEvents stops a control being exclusive evetn reciever and backs all
// to normal event processing
func ReleaseEvents() {
comp.consumer = nil
}
func termboxEventToLocal(ev term.Event) Event {
e := Event{Type: EventType(ev.Type), Ch: ev.Ch,
Key: ev.Key, Err: ev.Err, X: ev.MouseX, Y: ev.MouseY,
Mod: ev.Mod, Width: ev.Width, Height: ev.Height}
return e
}
// Repaints everything on the screen
func RefreshScreen() {
comp.BeginUpdate()
term.Clear(ColorWhite, ColorBlack)
comp.EndUpdate()
windows := comp.getWindowList()
for _, wnd := range windows {
v := wnd.(*Window)
if v.Visible() {
wnd.Draw()
WindowManager().BeginUpdate()
PushAttributes()
term.Flush()
PopAttributes()
WindowManager().EndUpdate()
}
}
comp.BeginUpdate()
term.Flush()
comp.EndUpdate()
}
// AddWindow constucts a new Window, adds it to the composer automatically,
// and makes it active
// posX and posY are top left coordinates of the Window
// width and height are Window size
// title is a Window title
func AddWindow(posX, posY, width, height int, title string) *Window {
window := CreateWindow(posX, posY, width, height, title)
window.SetBorder(comp.windowBorder)
comp.BeginUpdate()
comp.windows = append(comp.windows, window)
comp.EndUpdate()
window.Draw()
term.Flush()
comp.activateWindow(window)
RefreshScreen()
return window
}
// Border returns the default window border
func (c *Composer) BorderStyle() BorderStyle {
return c.windowBorder
}
// SetBorder changes the default window border
func (c *Composer) SetBorder(border BorderStyle) {
c.windowBorder = border
}
// 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) {
windows := c.getWindowList()
if len(windows) == 0 {
return nil, HitOutside
}
for i := len(windows) - 1; i >= 0; i-- {
window := windows[i]
hit := window.HitTest(screenX, screenY)
if hit != HitOutside {
return window, hit
}
}
return nil, HitOutside
}
func (c *Composer) activateWindow(window Control) bool {
windows := c.getWindowList()
if c.topWindow() == window {
for _, v := range windows {
v.SetActive(false)
}
window.SetActive(true)
return true
}
var wList []Control
found := false
for _, v := range windows {
if v != window {
v.SetActive(false)
wList = append(wList, v)
} else {
found = true
}
}
if !found {
return false
}
window.SetActive(true)
c.BeginUpdate()
defer c.EndUpdate()
c.windows = append(wList, window)
return true
}
func (c *Composer) moveActiveWindowToBottom() bool {
windows := c.getWindowList()
if len(windows) < 2 {
return false
}
if c.topWindow().Modal() {
return false
}
anyVisible := false
for _, w := range windows {
v := w.(*Window)
if v.Visible() {
anyVisible = true
break
}
}
if !anyVisible {
return false
}
event := Event{Type: EventActivate, X: 0} // send deactivated
c.sendEventToActiveWindow(event)
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() {
if !c.activateWindow(c.topWindow()) {
return false
}
break
}
}
event = Event{Type: EventActivate, X: 1} // send 'activated'
c.sendEventToActiveWindow(event)
RefreshScreen()
return true
}
func (c *Composer) sendEventToActiveWindow(ev Event) bool {
view := c.topWindow()
if view != nil {
return view.ProcessEvent(ev)
}
return false
}
func (c *Composer) topWindow() Control {
windows := c.getWindowList()
if len(windows) == 0 {
return nil
}
return windows[len(windows)-1]
}
func (c *Composer) resizeTopWindow(ev Event) bool {
view := c.topWindow()
if view == nil {
return false
}
topwindow, ok := view.(*Window)
if ok && !topwindow.Sizable() {
return false
}
w, h := view.Size()
w1, h1 := w, h
minW, minH := view.Constraints()
if ev.Key == term.KeyArrowUp && minH < h {
h--
} else if ev.Key == term.KeyArrowLeft && minW < w {
w--
} else if ev.Key == term.KeyArrowDown {
h++
} else if ev.Key == term.KeyArrowRight {
w++
}
if w1 != w || h1 != h {
view.SetSize(w, h)
event := Event{Type: EventResize, X: w, Y: h}
c.sendEventToActiveWindow(event)
RefreshScreen()
}
return true
}
func (c *Composer) moveTopWindow(ev Event) bool {
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++
}
if x1 != x || y1 != y {
view.SetPos(x, y)
event := Event{Type: EventMove, X: x, Y: y}
c.sendEventToActiveWindow(event)
RefreshScreen()
}
return true
}
return false
}
func (c *Composer) closeTopWindow() {
if len(c.windows) > 1 {
view := c.topWindow()
event := Event{Type: EventClose, X: 1}
if c.sendEventToActiveWindow(event) {
c.DestroyWindow(view)
activate := c.topWindow()
c.activateWindow(activate)
event = Event{Type: EventActivate, X: 1} // send 'activated'
c.sendEventToActiveWindow(event)
}
RefreshScreen()
} else {
go Stop()
}
}
func (c *Composer) processWindowDrag(ev Event) {
if ev.Mod != term.ModMotion || c.dragType == DragNone {
return
}
dx := ev.X - c.lastX
dy := ev.Y - c.lastY
if dx == 0 && dy == 0 {
return
}
w := c.topWindow()
newX, newY := w.Pos()
newW, newH := w.Size()
cw, ch := ScreenSize()
switch c.dragType {
case DragMove:
newX = newX + dx
newY = newY + dy
if newX >= 0 && newY >= 0 && newX+newW < cw && newY+newH < ch {
c.lastX = ev.X
c.lastY = ev.Y
w.SetPos(newX, newY)
event := Event{Type: EventMove, X: newX, Y: newY}
c.sendEventToActiveWindow(event)
RefreshScreen()
}
case DragResizeLeft:
newX = newX + dx
newW = newW - dx
if newX >= 0 && newY >= 0 && newX+newW < cw && newY+newH < ch {
c.lastX = ev.X
c.lastY = ev.Y
w.SetPos(newX, newY)
w.SetSize(newW, newH)
event := Event{Type: EventMove, X: newX, Y: newY}
c.sendEventToActiveWindow(event)
event.Type = EventResize
c.sendEventToActiveWindow(event)
RefreshScreen()
}
case DragResizeRight:
newW = newW + dx
if newX >= 0 && newY >= 0 && newX+newW < cw && newY+newH < ch {
c.lastX = ev.X
c.lastY = ev.Y
w.SetSize(newW, newH)
event := Event{Type: EventResize}
c.sendEventToActiveWindow(event)
RefreshScreen()
}
case DragResizeBottom:
newH = newH + dy
if newX >= 0 && newY >= 0 && newX+newW < cw && newY+newH < ch {
c.lastX = ev.X
c.lastY = ev.Y
w.SetSize(newW, newH)
event := Event{Type: EventResize}
c.sendEventToActiveWindow(event)
RefreshScreen()
}
case DragResizeTopLeft:
newX = newX + dx
newW = newW - dx
newY = newY + dy
newH = newH - dy
if newX >= 0 && newY >= 0 && newX+newW < cw && newY+newH < ch {
c.lastX = ev.X
c.lastY = ev.Y
w.SetPos(newX, newY)
w.SetSize(newW, newH)
event := Event{Type: EventMove, X: newX, Y: newY}
c.sendEventToActiveWindow(event)
event.Type = EventResize
c.sendEventToActiveWindow(event)
RefreshScreen()
}
case DragResizeBottomLeft:
newX = newX + dx
newW = newW - dx
newH = newH + dy
if newX >= 0 && newY >= 0 && newX+newW < cw && newY+newH < ch {
c.lastX = ev.X
c.lastY = ev.Y
w.SetPos(newX, newY)
w.SetSize(newW, newH)
event := Event{Type: EventMove, X: newX, Y: newY}
c.sendEventToActiveWindow(event)
event.Type = EventResize
c.sendEventToActiveWindow(event)
RefreshScreen()
}
case DragResizeBottomRight:
newW = newW + dx
newH = newH + dy
if newX >= 0 && newY >= 0 && newX+newW < cw && newY+newH < ch {
c.lastX = ev.X
c.lastY = ev.Y
w.SetSize(newW, newH)
event := Event{Type: EventResize}
c.sendEventToActiveWindow(event)
RefreshScreen()
}
case DragResizeTopRight:
newY = newY + dy
newW = newW + dx
newH = newH - dy
if newX >= 0 && newY >= 0 && newX+newW < cw && newY+newH < ch {
c.lastX = ev.X
c.lastY = ev.Y
w.SetPos(newX, newY)
w.SetSize(newW, newH)
event := Event{Type: EventMove, X: newX, Y: newY}
c.sendEventToActiveWindow(event)
event.Type = EventResize
c.sendEventToActiveWindow(event)
RefreshScreen()
}
}
}
func (c *Composer) processMouse(ev Event) {
if c.consumer != nil {
tmp := c.consumer
tmp.ProcessEvent(ev)
tmp.Draw()
term.Flush()
return
}
view, hit := c.checkWindowUnderMouse(ev.X, ev.Y)
if c.dragType != DragNone {
view = c.topWindow()
}
if c.topWindow() == view {
if ev.Key == term.MouseRelease && c.dragType != DragNone {
c.dragType = DragNone
return
}
if ev.Mod == term.ModMotion && c.dragType != DragNone {
c.processWindowDrag(ev)
return
}
if hit != HitInside && ev.Key == term.MouseLeft {
if hit != HitButtonClose && hit != HitButtonBottom && hit != HitButtonMaximize {
c.lastX = ev.X
c.lastY = ev.Y
c.mdownX = ev.X
c.mdownY = ev.Y
}
switch hit {
case HitButtonClose:
c.closeTopWindow()
case HitButtonBottom:
c.moveActiveWindowToBottom()
case HitButtonMaximize:
v := c.topWindow().(*Window)
maximized := v.Maximized()
v.SetMaximized(!maximized)
case HitTop:
c.dragType = DragMove
case HitBottom:
c.dragType = DragResizeBottom
case HitLeft:
c.dragType = DragResizeLeft
case HitRight:
c.dragType = DragResizeRight
case HitTopLeft:
c.dragType = DragResizeTopLeft
case HitTopRight:
c.dragType = DragResizeTopRight
case HitBottomRight:
c.dragType = DragResizeBottomRight
case HitBottomLeft:
c.dragType = DragResizeBottomLeft
}
return
}
} else if !c.topWindow().Modal() {
c.activateWindow(view)
return
}
if ev.Key == term.MouseLeft {
c.lastX = ev.X
c.lastY = ev.Y
c.mdownX = ev.X
c.mdownY = ev.Y
c.sendEventToActiveWindow(ev)
return
} else if ev.Key == term.MouseRelease {
c.sendEventToActiveWindow(ev)
if c.lastX != ev.X && c.lastY != ev.Y {
return
}
ev.Type = EventClick
c.sendEventToActiveWindow(ev)
return
} else {
c.sendEventToActiveWindow(ev)
return
}
}
// Stop sends termination event to Composer. Composer should stop
// console management and quit application
func Stop() {
ev := Event{Type: EventQuit}
go PutEvent(ev)
}
// DestroyWindow removes the Window from the list of managed Windows
func (c *Composer) DestroyWindow(view Control) {
ev := Event{Type: EventClose}
c.sendEventToActiveWindow(ev)
windows := c.getWindowList()
var newOrder []Control
for i := 0; i < len(windows); i++ {
if windows[i] != view {
newOrder = append(newOrder, windows[i])
}
}
if len(newOrder) == 0 {
go Stop()
return
}
c.BeginUpdate()
c.windows = newOrder
c.EndUpdate()
c.activateWindow(c.topWindow())
}
// IsDeadKey returns true if the pressed key is the first key in
// the key sequence understood by composer. Dead key is never sent to
// any control
func IsDeadKey(key term.Key) bool {
if key == term.KeyCtrlS || key == term.KeyCtrlP ||
key == term.KeyCtrlW || key == term.KeyCtrlQ {
return true
}
return false
}
func (c *Composer) processKey(ev Event) {
if ev.Key == term.KeyEsc {
if IsDeadKey(c.lastKey) {
c.lastKey = term.KeyEsc
return
}
}
if IsDeadKey(ev.Key) && !IsDeadKey(c.lastKey) {
c.lastKey = ev.Key
return
}
if !IsDeadKey(ev.Key) {
if c.consumer != nil {
tmp := c.consumer
tmp.ProcessEvent(ev)
tmp.Draw()
term.Flush()
} else {
c.sendEventToActiveWindow(ev)
c.topWindow().Draw()
term.Flush()
}
}
newKey := term.KeyEsc
switch c.lastKey {
case term.KeyCtrlQ:
switch ev.Key {
case term.KeyCtrlQ:
Stop()
default:
newKey = ev.Key
}
case term.KeyCtrlS:
switch ev.Key {
case term.KeyArrowUp, term.KeyArrowDown, term.KeyArrowLeft, term.KeyArrowRight:
c.resizeTopWindow(ev)
default:
newKey = ev.Key
}
case term.KeyCtrlP:
switch ev.Key {
case term.KeyArrowUp, term.KeyArrowDown, term.KeyArrowLeft, term.KeyArrowRight:
c.moveTopWindow(ev)
default:
newKey = ev.Key
}
case term.KeyCtrlW:
switch ev.Key {
case term.KeyCtrlH:
c.moveActiveWindowToBottom()
case term.KeyCtrlM:
w := c.topWindow().(*Window)
if w.Sizable() && (w.TitleButtons()&ButtonMaximize == ButtonMaximize) {
maxxed := w.Maximized()
w.SetMaximized(!maxxed)
RefreshScreen()
}
case term.KeyCtrlC:
c.closeTopWindow()
default:
newKey = ev.Key
}
}
if newKey != term.KeyEsc {
event := Event{Key: c.lastKey, Type: EventKey}
c.sendEventToActiveWindow(event)
event.Key = newKey
c.sendEventToActiveWindow(event)
c.lastKey = term.KeyEsc
}
}
func ProcessEvent(ev Event) {
switch ev.Type {
case EventCloseWindow:
comp.closeTopWindow()
case EventRedraw:
RefreshScreen()
case EventResize:
SetScreenSize(ev.Width, ev.Height)
for _, c := range comp.windows {
wnd := c.(*Window)
if wnd.Maximized() {
wnd.SetSize(ev.Width, ev.Height)
wnd.ResizeChildren()
wnd.PlaceChildren()
RefreshScreen()
}
if wnd.onScreenResize != nil {
wnd.onScreenResize(ev)
}
}
case EventKey:
comp.processKey(ev)
case EventMouse:
comp.processMouse(ev)
case EventLayout:
for _, c := range comp.windows {
if c == ev.Target {
c.ResizeChildren()
c.PlaceChildren()
break
}
}
}
}