623 lines
13 KiB
Go
623 lines
13 KiB
Go
package clui
|
|
|
|
import (
|
|
term "github.com/nsf/termbox-go"
|
|
)
|
|
|
|
// 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
|
|
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
|
|
}
|
|
|
|
var (
|
|
comp *Composer
|
|
)
|
|
|
|
func initComposer() {
|
|
comp = new(Composer)
|
|
comp.windows = make([]Control, 0)
|
|
comp.consumer = nil
|
|
comp.lastKey = term.KeyEsc
|
|
}
|
|
|
|
func composer() *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() {
|
|
term.Clear(ColorWhite, ColorBlack)
|
|
|
|
for _, wnd := range comp.windows {
|
|
wnd.Draw()
|
|
}
|
|
|
|
term.Flush()
|
|
}
|
|
|
|
// 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)
|
|
|
|
comp.windows = append(comp.windows, window)
|
|
window.Draw()
|
|
|
|
comp.activateWindow(window)
|
|
|
|
RefreshScreen()
|
|
|
|
return window
|
|
}
|
|
|
|
func (c *Composer) checkWindowUnderMouse(screenX, screenY int) (Control, HitResult) {
|
|
if len(c.windows) == 0 {
|
|
return nil, HitOutside
|
|
}
|
|
|
|
for i := len(c.windows) - 1; i >= 0; i-- {
|
|
window := c.windows[i]
|
|
hit := window.HitTest(screenX, screenY)
|
|
if hit != HitOutside {
|
|
return window, hit
|
|
}
|
|
}
|
|
|
|
return nil, HitOutside
|
|
}
|
|
|
|
func (c *Composer) activateWindow(window Control) bool {
|
|
if c.topWindow() == window {
|
|
for _, v := range c.windows {
|
|
v.SetActive(false)
|
|
}
|
|
window.SetActive(true)
|
|
return true
|
|
}
|
|
|
|
var wList []Control
|
|
found := false
|
|
|
|
for _, v := range c.windows {
|
|
if v != window {
|
|
v.SetActive(false)
|
|
wList = append(wList, v)
|
|
} else {
|
|
found = true
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return false
|
|
}
|
|
|
|
window.SetActive(true)
|
|
c.windows = append(wList, window)
|
|
return true
|
|
}
|
|
|
|
func (c *Composer) moveActiveWindowToBottom() bool {
|
|
if len(c.windows) < 2 {
|
|
return false
|
|
}
|
|
|
|
if c.topWindow().Modal() {
|
|
return false
|
|
}
|
|
|
|
event := Event{Type: EventActivate, X: 0} // send deactivated
|
|
c.sendEventToActiveWindow(event)
|
|
|
|
last := c.topWindow()
|
|
|
|
for i := len(c.windows) - 1; i > 0; i-- {
|
|
c.windows[i] = c.windows[i-1]
|
|
}
|
|
|
|
c.windows[0] = last
|
|
if !c.activateWindow(c.topWindow()) {
|
|
return false
|
|
}
|
|
|
|
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 {
|
|
if len(c.windows) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return c.windows[len(c.windows)-1]
|
|
}
|
|
|
|
func (c *Composer) resizeTopWindow(ev Event) bool {
|
|
view := c.topWindow()
|
|
if view == nil {
|
|
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 {
|
|
if len(c.windows) > 0 {
|
|
view := c.topWindow()
|
|
if view != nil {
|
|
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}
|
|
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
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if view == nil {
|
|
return
|
|
}
|
|
|
|
if c.topWindow() != view {
|
|
if c.topWindow().Modal() {
|
|
return
|
|
}
|
|
event := Event{Type: EventActivate, X: 0} // send 'deactivated'
|
|
c.sendEventToActiveWindow(event)
|
|
c.activateWindow(view)
|
|
event = Event{Type: EventActivate, X: 1} // send 'activated'
|
|
c.sendEventToActiveWindow(event)
|
|
} else if hit == HitInside {
|
|
c.sendEventToActiveWindow(ev)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
|
|
var newOrder []Control
|
|
for i := 0; i < len(c.windows); i++ {
|
|
if c.windows[i] != view {
|
|
newOrder = append(newOrder, c.windows[i])
|
|
}
|
|
}
|
|
c.windows = newOrder
|
|
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)
|
|
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 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()
|
|
}
|
|
}
|
|
case EventKey:
|
|
comp.processKey(ev)
|
|
case EventMouse:
|
|
comp.processMouse(ev)
|
|
}
|
|
}
|