clui/composer.go
2015-09-21 20:54:39 -07:00

687 lines
16 KiB
Go

package clui
import (
"github.com/VladimirMarkelov/termbox-go"
"log"
"os"
)
// An service object that manages Windows and console, processes events, and provides service methods
// One application must have only one object of this type
type Composer struct {
// list of visible Windows
windows map[WinId]Window
// console width and height
width, height int
// console canvas
screen *FrameBuffer
// Window draw order. The last Window is active Window
windowOrder []WinId
// ID assigned to the last created Window
lastWindowId WinId
// a channel to communicate with Windows(e.g, Windows send redraw event to this channel)
channel chan InternalEvent
// What kind of dragging is active: nothing is dragged, something is being moved, something is being resized
dragging DragAction
// Window ID that is currently dragged. NullWindow if no Window is dragged
dragObject WinId
// Console position where mouse button was pressed and drag action started
dragStartX, dragStartY int
// which part of dragged Window is active, it determines whether Window is resizing or dragging and what size is changing
dragHit HitResult
// current color scheme
themeManager *ThemeManager
// multi key sequences support. The flag below are true if the last keyboard combination was Ctrl+S or Ctrl+W respectively
ctrlSpressed bool
ctrlWpressed bool
// 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 termbox.Key
//debug
logger *log.Logger
}
func (c *Composer) initBuffer(w, h int) *FrameBuffer {
bf := NewFrameBuffer(w, h)
bf.Clear(ColorBlack)
return bf
}
// Initialize library and starts console management
func (c *Composer) Init() {
err := termbox.Init()
if err != nil {
panic(err)
}
termbox.HideCursor()
file, _ := os.OpenFile("debug.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
c.logger = log.New(file, "", log.Ldate|log.Ltime|log.Lshortfile)
c.logger.Printf("----------------------------------")
c.lastWindowId = 0
c.channel = make(chan InternalEvent)
c.windows = make(map[WinId]Window)
c.windowOrder = make([]WinId, 0)
c.width, c.height = termbox.Size()
c.screen = c.initBuffer(c.width, c.height)
c.themeManager = NewThemeManager()
c.ctrlSpressed = false
c.ctrlWpressed = false
termbox.SetInputMode(termbox.InputEsc | termbox.InputMouse)
c.redrawAll()
}
func (c *Composer) redrawAll() {
for j := 0; j < c.height; j++ {
for i := 0; i < c.width; i++ {
sym, ok := c.screen.GetSymbol(i, j)
if ok {
termbox.SetCell(i, j, sym.ch, termbox.Attribute(sym.fg), termbox.Attribute(sym.bg))
}
}
}
termbox.Flush()
}
// Repaints all Windows on the screen. Now the method is not efficient: at first clears a console and then draws all Windows starting from the bottom
func (c *Composer) RefreshScreen() {
// TODO: make more intelligent (for each Cell ask starting from the top most if it is its Cell and write it)
for yy := 0; yy < c.height; yy++ {
for xx := 0; xx < c.width; xx++ {
c.screen.rawRuneOut(xx, yy, ' ', ColorWhite, ColorBlack)
}
}
for _, winId := range c.windowOrder {
wnd, ok := c.windows[winId]
if !ok {
continue
}
wnd.Redraw()
posx, posy := wnd.GetPos()
w, h := wnd.GetSize()
for y := posy; y < posy+h; y++ {
for x := posx; x < posx+w; x++ {
if x < c.width && y < c.height {
sym, ok := wnd.GetScreenSymbol(x, y)
if ok {
c.screen.rawRuneOut(x, y, sym.ch, sym.fg, sym.bg)
}
}
}
}
}
c.redrawAll()
}
func (c *Composer) makeTopViewActive() {
for i := 0; i < len(c.windowOrder)-1; i++ {
window, ok := c.windows[c.windowOrder[i]]
if ok {
window.SetActive(false)
}
}
window, ok := c.windows[c.windowOrder[len(c.windowOrder)-1]]
if ok {
window.SetActive(true)
}
}
func (c *Composer) CreateWindow(posX, posY, width, height int, title string, props Props) Window {
c.lastWindowId++
id := c.lastWindowId
view := NewView(c, id, posX, posY, width, height, title)
view.SetBorderIcons(IconClose | IconBottom)
c.windows[id] = view
view.Redraw()
c.windowOrder = append(c.windowOrder, id)
c.makeTopViewActive()
c.RefreshScreen()
return view
}
func (c *Composer) DestroyControl(parent Window, control Control) {
parent.RemoveControl(control)
}
func (c *Composer) checkWindowUnderMouse(screenX, screenY int) (WinId, HitResult) {
if len(c.windowOrder) == 0 {
return NullWindow, HitOutside
}
for i := len(c.windowOrder) - 1; i >= 0; i-- {
window, ok := c.windows[c.windowOrder[i]]
if ok {
hit := window.HitTest(screenX, screenY)
if hit != HitOutside {
return c.windowOrder[i], hit
}
}
}
return NullWindow, HitOutside
}
func (c *Composer) getWindow(id WinId) Window {
wnd, ok := c.windows[id]
if !ok {
return nil
}
return wnd
}
func (c *Composer) getTopWindow() WinId {
if len(c.windowOrder) == 0 {
return NullWindow
}
return c.windowOrder[len(c.windowOrder)-1]
}
func (c *Composer) makeWindowTop(wid WinId) {
for i := 0; i < len(c.windowOrder); i++ {
if c.windowOrder[i] == wid {
c.windowOrder = append(c.windowOrder[:i], c.windowOrder[i+1:]...)
break
}
}
c.windowOrder = append(c.windowOrder, wid)
c.makeTopViewActive()
}
func (c *Composer) moveActiveWindowToBottom() bool {
if len(c.windowOrder) < 2 {
return false
}
event := Event{Type: EventActivate, X: 0} // send deactivated
c.sendEventToActiveView(event)
last := c.windowOrder[len(c.windowOrder)-1]
for i := len(c.windowOrder) - 1; i > 0; i-- {
c.windowOrder[i] = c.windowOrder[i-1]
}
c.windowOrder[0] = last
c.makeTopViewActive()
event = Event{Type: EventActivate, X: 1} // send 'activated'
c.sendEventToActiveView(event)
c.RefreshScreen()
return true
}
func (c *Composer) termboxEventToLocal(ev termbox.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}
return e
}
func (c *Composer) sendEventToActiveView(ev Event) bool {
winid := c.getTopWindow()
if winid != NullWindow {
window := c.getWindow(winid)
if window != nil {
return window.ProcessEvent(ev)
}
}
return false
}
func (c *Composer) resizeTopWindow(ev termbox.Event) bool {
if len(c.windowOrder) > 0 {
if ev.Mod&termbox.ModControl != 0 {
window := c.getWindow(c.getTopWindow())
if window != nil {
w, h := window.GetSize()
w1, h1 := w, h
minW, minH := window.GetConstraints()
if ev.Key == termbox.KeyArrowUp && (minH == -1 || minH < h) {
h--
} else if ev.Key == termbox.KeyArrowLeft && (minW < w || minW == -1) {
w--
}
if w1 != w || h1 != h {
window.SetSize(w, h)
event := Event{Type: EventResize, X: w, Y: h}
c.sendEventToActiveView(event)
c.RefreshScreen()
}
}
}
return true
} else {
return false
}
}
func (c *Composer) moveTopWindow(ev termbox.Event) bool {
if len(c.windowOrder) > 0 {
window := c.getWindow(c.getTopWindow())
if window != nil {
x, y := window.GetPos()
w, h := window.GetSize()
x1, y1 := x, y
cx, cy := termbox.Size()
if ev.Key == termbox.KeyArrowUp && y > 0 {
y--
} else if ev.Key == termbox.KeyArrowDown && y+h < cy {
y++
} else if ev.Key == termbox.KeyArrowLeft && x > 0 {
x--
} else if ev.Key == termbox.KeyArrowRight && x+w < cx {
x++
}
if x1 != x || y1 != y {
window.SetPos(x, y)
event := Event{Type: EventMove, X: x, Y: y}
c.sendEventToActiveView(event)
c.RefreshScreen()
}
}
return true
} else {
return false
}
}
func (c *Composer) isDeadKey(ev termbox.Event) bool {
if ev.Key == termbox.KeyCtrlS {
c.ctrlSpressed = true
c.lastKey = termbox.KeyEsc
return true
}
if ev.Key == termbox.KeyCtrlW {
c.ctrlWpressed = true
c.lastKey = termbox.KeyEsc
return true
}
c.ctrlSpressed = false
c.ctrlWpressed = false
return false
}
func (c *Composer) processKeySeq(ev termbox.Event) bool {
if !c.ctrlSpressed && !c.ctrlWpressed {
return false
}
if c.ctrlSpressed {
c.ctrlWpressed = false
if c.lastKey == termbox.KeyEsc {
c.lastKey = ev.Key
} else if c.lastKey != ev.Key {
c.ctrlSpressed = false
return false
}
switch ev.Key {
case termbox.KeyArrowUp, termbox.KeyArrowDown, termbox.KeyArrowLeft, termbox.KeyArrowRight:
evCopy := ev
evCopy.Mod = termbox.ModControl
c.resizeTopWindow(evCopy)
return true
}
return false
}
if c.ctrlWpressed {
c.ctrlSpressed = false
if c.lastKey == termbox.KeyEsc {
c.lastKey = ev.Key
} else if c.lastKey != ev.Key {
c.ctrlWpressed = false
return false
}
switch ev.Key {
case termbox.KeyArrowUp, termbox.KeyArrowDown, termbox.KeyArrowLeft, termbox.KeyArrowRight:
evCopy := ev
evCopy.Mod = termbox.ModAlt
c.moveTopWindow(evCopy)
return true
case termbox.KeyCtrlH:
if len(c.windowOrder) > 1 && ev.Mod&termbox.ModControl != 0 {
c.moveActiveWindowToBottom()
}
return true
}
return false
}
c.ctrlSpressed = false
c.ctrlWpressed = false
return true
}
func (c *Composer) processKey(ev termbox.Event) bool {
processed := false
if c.processKeySeq(ev) {
return false
}
if c.isDeadKey(ev) {
return false
}
switch ev.Key {
case termbox.KeyCtrlQ:
return true
case termbox.KeyArrowUp, termbox.KeyArrowDown, termbox.KeyArrowLeft, termbox.KeyArrowRight:
if ev.Mod&termbox.ModControl != 0 {
processed = processed || c.resizeTopWindow(ev)
}
if ev.Mod&termbox.ModAlt != 0 {
processed = processed || c.moveTopWindow(ev)
}
if !processed {
if c.sendEventToActiveView(c.termboxEventToLocal(ev)) {
c.RefreshScreen()
}
}
case termbox.KeyEnd:
if len(c.windowOrder) > 1 && ev.Mod&termbox.ModControl != 0 {
processed = c.moveActiveWindowToBottom()
}
if !processed {
if c.sendEventToActiveView(c.termboxEventToLocal(ev)) {
c.RefreshScreen()
}
}
default:
if c.sendEventToActiveView(c.termboxEventToLocal(ev)) {
c.RefreshScreen()
}
}
return false
}
func (c *Composer) processMouseClick(ev termbox.Event) {
winId, hit := c.checkWindowUnderMouse(ev.MouseX, ev.MouseY)
if c.getTopWindow() != winId {
event := Event{Type: EventActivate, X: 0} // send 'deactivated'
c.sendEventToActiveView(event)
c.makeWindowTop(winId)
event = Event{Type: EventActivate, X: 1} // send 'activated'
c.sendEventToActiveView(event)
c.RefreshScreen()
} else if hit == HitInside {
// c.logger.Printf("Clicked inside window %v", int(winId))
c.sendEventToActiveView(c.termboxEventToLocal(ev))
c.RefreshScreen()
} else if hit == HitButtonClose {
if len(c.windowOrder) > 1 {
event := Event{Type: EventClose}
c.sendEventToActiveView(event)
c.DestroyWindow(winId)
activate := c.windowOrder[len(c.windowOrder)-1]
c.makeWindowTop(activate)
event = Event{Type: EventActivate, X: 1} // send 'activated'
c.sendEventToActiveView(event)
c.RefreshScreen()
}
} else if hit == HitButtonBottom {
c.moveActiveWindowToBottom()
}
}
func (c *Composer) stopDragging() {
c.dragging = DragNone
c.dragStartX, c.dragStartY = -1, -1
c.dragObject = NullWindow
}
func (c *Composer) processMouseRelease(ev termbox.Event) {
if c.dragging == DragNone {
c.sendEventToActiveView(c.termboxEventToLocal(ev))
}
c.stopDragging()
}
func (c *Composer) processMousePress(ev termbox.Event) {
winId, hit := c.checkWindowUnderMouse(ev.MouseX, ev.MouseY)
if c.getTopWindow() != winId {
event := Event{Type: EventActivate, X: 0} // send 'deactivated'
c.sendEventToActiveView(event)
c.makeWindowTop(winId)
event = Event{Type: EventActivate, X: 1} // send 'activated'
c.sendEventToActiveView(event)
c.RefreshScreen()
} else {
if hit == HitHeader {
c.dragging = DragMove
c.dragObject = winId
c.dragStartX, c.dragStartY = ev.MouseX, ev.MouseY
} else if hit == HitLeftBorder || hit == HitRightBorder || hit == HitBottomBorder || hit == HitBottomLeft || hit == HitBottomRight || hit == HitTopLeft || hit == HitTopRight {
c.dragging = DragResize
c.dragObject = winId
c.dragStartX, c.dragStartY = ev.MouseX, ev.MouseY
c.dragHit = hit
} else if hit == HitInside {
c.sendEventToActiveView(c.termboxEventToLocal(ev))
}
}
}
func (c *Composer) moveAndResize(dx, dy int, wnd Window) bool {
if dx == 0 && dy == 0 {
return false
}
posX, posY := wnd.GetPos()
w, h := wnd.GetSize()
mnX, mnY := wnd.GetConstraints()
newW, newH := w, h
newX, newY := posX, posY
if c.dragHit == HitLeftBorder {
newX = posX + dx
newW = w - dx
} else if c.dragHit == HitRightBorder {
newW = w + dx
} else if c.dragHit == HitBottomBorder {
newH = h + dy
} else if c.dragHit == HitTopLeft {
newX, newY = posX+dx, posY+dy
newW, newH = w-dx, h-dy
} else if c.dragHit == HitBottomLeft {
newX = posX + dx
newW, newH = w-dx, h+dy
} else if c.dragHit == HitTopRight {
newY = posY + dy
newW, newH = w+dx, h-dy
} else if c.dragHit == HitBottomRight {
newW, newH = w+dx, h+dy
}
if (mnX != -1 && newW < mnX) || (mnY != -1 && newH < mnY) {
return false
}
wnd.SetPos(newX, newY)
wnd.SetSize(newW, newH)
if posX != newX || posY != newY {
event := Event{Type: EventMove, X: newX, Y: newY}
c.sendEventToActiveView(event)
}
if newW != w || newH != h {
event := Event{Type: EventResize, X: w, Y: h}
c.sendEventToActiveView(event)
}
wnd.Redraw()
return true
}
func (c *Composer) processMouseMove(ev termbox.Event) {
if c.dragging == DragNone {
c.sendEventToActiveView(c.termboxEventToLocal(ev))
c.RefreshScreen()
return
}
wnd, ok := c.windows[c.dragObject]
if !ok || c.dragObject == NullWindow {
c.stopDragging()
return
}
if c.dragging == DragMove {
dx, dy := ev.MouseX-c.dragStartX, ev.MouseY-c.dragStartY
if dx != 0 || dy != 0 {
c.dragStartY = ev.MouseY
c.dragStartX = ev.MouseX
posX, posY := wnd.GetPos()
wnd.SetPos(posX+dx, posY+dy)
event := Event{Type: EventMove, X: posX + dx, Y: posY + dy}
c.sendEventToActiveView(event)
c.RefreshScreen()
}
} else if c.dragging == DragResize && c.dragHit != HitOutside {
dx, dy := 0, 0
if c.dragHit == HitLeftBorder || c.dragHit == HitRightBorder {
dx = ev.MouseX - c.dragStartX
} else if c.dragHit == HitBottomBorder {
dy = ev.MouseY - c.dragStartY
} else if c.dragHit == HitTopLeft || c.dragHit == HitTopRight || c.dragHit == HitBottomLeft || c.dragHit == HitBottomRight {
dx = ev.MouseX - c.dragStartX
dy = ev.MouseY - c.dragStartY
}
if c.moveAndResize(dx, dy, wnd) {
c.dragStartX = ev.MouseX
c.dragStartY = ev.MouseY
c.RefreshScreen()
}
}
}
// Asks a Composer to stops console management and quit application
func (c *Composer) Stop() {
ev := InternalEvent{act: EventQuit}
go c.SendEvent(ev)
}
// Main event loop
func (c *Composer) MainLoop() {
c.redrawAll()
eventQueue := make(chan termbox.Event)
go func() {
for {
eventQueue <- termbox.PollEvent()
}
}()
for {
select {
case ev := <-eventQueue:
switch ev.Type {
case termbox.EventMouseScroll:
if c.sendEventToActiveView(c.termboxEventToLocal(ev)) {
c.RefreshScreen()
}
case termbox.EventKey:
if c.processKey(ev) {
return
}
case termbox.EventMouse, termbox.EventMouseClick:
c.processMouseClick(ev)
case termbox.EventMouseRelease:
c.processMouseRelease(ev)
case termbox.EventMousePress:
c.processMousePress(ev)
case termbox.EventMouseMove:
c.processMouseMove(ev)
case termbox.EventError:
panic(ev.Err)
case termbox.EventResize:
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
c.width, c.height = termbox.Size()
c.screen = c.initBuffer(c.width, c.height)
c.RefreshScreen()
}
case cmd := <-c.channel:
if cmd.act == EventRedraw {
c.RefreshScreen()
} else if cmd.act == EventQuit {
return
}
}
}
}
// Send event to a Composer. Used by Windows to ask for repainting or for quitting the application
func (c *Composer) SendEvent(ev InternalEvent) {
c.channel <- ev
}
// Closes console management and makes a console cursor visible
func (c *Composer) Close() {
termbox.SetCursor(3, 3)
termbox.Close()
}
// Shows consolse cursor at given position. Setting cursor to -1,-1 hides cursor
func (c *Composer) SetCursorPos(x, y int) {
termbox.SetCursor(x, y)
}
// Returns thememanager to get current theme colors etc
func (c *Composer) GetThemeManager() *ThemeManager {
return c.themeManager
}
func (c *Composer) DestroyWindow(winId WinId) {
ev := Event{Type: EventClose}
c.sendEventToActiveView(ev)
newOrder := make([]WinId, 0)
for i := 0; i < len(c.windowOrder); i++ {
if c.windowOrder[i] != winId {
newOrder = append(newOrder, c.windowOrder[i])
}
}
c.windowOrder = newOrder
delete(c.windows, winId)
}