Files
esdi/tui/internal/controllers/cdashdisplay_layout.go
T

586 lines
16 KiB
Go

package controllers
import (
"fmt"
"os"
"strconv"
"esdi/devices/cdashdisplay"
"esdi/services"
"esdi/tui/internal/views"
helper "esdi/helpers"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
type LayoutController struct {
*Controller
OnExit func()
LayoutToolView *views.LayoutToolView
Messages chan string
DevService *services.DeviceService
MoveToolState *windowManipState
SelectedLayout string // NOTE: This should be a struct to handle its own things
// TODO: create a filter to select the layout
// filter telemetry provider, then filter vehicle in use and so on
}
func NewLayoutController(base *Controller, service *services.DeviceService) *LayoutController {
lc := &LayoutController{
Controller: base,
LayoutToolView: views.NewLayoutToolView(),
Messages: make(chan string, 10),
DevService: service,
MoveToolState: &windowManipState{Mode: moveMode},
// SelectedLayout: "beamng.yaml",
SelectedLayout: "layout.yaml",
}
lc.registerHooks()
return lc
}
func (lc *LayoutController) registerHooks() {
// Set the input capture behaviour
lc.LayoutToolView.LayoutTree.Tree.SetInputCapture(
func(ev *tcell.EventKey) *tcell.EventKey {
switch ev.Key() {
case tcell.KeyEsc:
lc.OnExit()
}
switch ev.Rune() {
case 'n':
// Create a new window
lc.Messages <- "calling new window form\n"
lc.newWindowAction()
case 'x':
lc.Messages <- "calling delete window\n"
lc.deleteWindow()
case 'm':
lc.Messages <- "calling window manipulation tool\n"
lc.moveWindow()
case 'c':
lc.Messages <- "calling change selected layout tool\n"
lc.changeSelectedLoadout()
case 's':
// Save the current layout
lc.Messages <- "calling save layout\n"
lc.saveLayout()
case 'l':
// Load the layout
lc.Messages <- "calling load layout\n"
lc.loadLayout()
case 'u':
// Unload the layout
lc.Messages <- "calling unload layout\n"
lc.unloadLayout()
case 'g':
// Go -> launches the current set up source
// streamingWindow(bus, doc)
}
return ev
},
)
// Set the tree input actions
lc.LayoutToolView.LayoutTree.Tree.SetChangedFunc(func(node *tview.TreeNode) {
// Here we want to change the current existing form on the action pages
nodeRef := node.GetReference()
if nodeRef == nil {
// its probably root I guess, thats what I'll believe
return
}
nodeWinRef := nodeRef.(int16)
lc.Messages <- fmt.Sprintf("changing to -> %d\n", nodeWinRef)
lc.LayoutToolView.ShowWindowFormByID(nodeWinRef)
})
lc.LayoutToolView.LayoutTree.Tree.SetSelectedFunc(func(node *tview.TreeNode) {
// Change focus to the selected window
nodeRef := node.GetReference()
if nodeRef == nil {
// its probably root I guess, thats what I'll believe
return
}
nodeWinRef := nodeRef.(int16)
lc.App.SetFocus(lc.LayoutToolView.FormQuickAccess[nodeWinRef].Form.Form)
})
}
func (lc *LayoutController) parseWindowFormData(
form views.CDashDisplayWindowFormView,
) (*cdashdisplay.DesktopUIWindow, error) {
xValue, err := strconv.ParseUint(form.X.GetText(), 10, 64)
if err != nil {
return nil, err
}
yValue, err := strconv.ParseUint(form.Y.GetText(), 10, 64)
if err != nil {
return nil, err
}
widthValue, err := strconv.ParseUint(form.Width.GetText(), 10, 64)
if err != nil {
return nil, err
}
heightValue, err := strconv.ParseUint(form.Height.GetText(), 10, 64)
if err != nil {
return nil, err
}
winTypeInputID, _ := form.WinType.GetCurrentOption()
if winTypeInputID == -1 {
return nil, fmt.Errorf("no option selected for win type")
}
titleSizeInputID, titleSizeInput := form.TitleSize.GetCurrentOption()
if titleSizeInputID == -1 {
return nil, fmt.Errorf("no option selected for title size")
}
titleSizeValue, err := strconv.ParseUint(titleSizeInput, 10, 64)
if err != nil {
return nil, err
}
textSizeInputID, textSizeInput := form.TextSize.GetCurrentOption()
if textSizeInputID == -1 {
return nil, fmt.Errorf("no option selected for text size")
}
textSizeValue, err := strconv.ParseUint(textSizeInput, 10, 64)
if err != nil {
return nil, err
}
telemFieldInputID, telemField := form.TelemetryField.GetCurrentOption()
if telemFieldInputID == -1 {
return nil, fmt.Errorf("no option selected for telemetry field")
}
showIDValue := cdashdisplay.ShowIDFalse
if form.ShowID.IsChecked() {
showIDValue = cdashdisplay.ShowIDTrue
}
winDecor := cdashdisplay.DefaultDecorations
winDecor.TextSize = uint8(textSizeValue - 1)
winDecor.TitleSize = uint8(titleSizeValue - 1)
uiWindow := cdashdisplay.UIWindow{
Dims: cdashdisplay.UIDimensions{
X0: uint16(xValue),
Y0: uint16(yValue),
Width: uint16(widthValue),
Height: uint16(heightValue),
},
Opts: cdashdisplay.UIWindowOpts{
WinType: uint8(winTypeInputID),
ShowID: showIDValue,
PreviewValue: helper.B32(form.PreviewValue.GetText()),
},
Decor: winDecor,
Title: helper.B32(form.Title.GetText()),
}
uiData := cdashdisplay.DesktopUIData{
TelemetryField: telemField,
}
return &cdashdisplay.DesktopUIWindow{
UIWindow: uiWindow,
UIData: uiData,
}, nil
}
// createWindow is the function the callback for the "Create" button on the new window
// form
// - parses the contents of the new window form
// - sends that data to the correct device service
func (lc *LayoutController) createWindow() {
window, err := lc.parseWindowFormData(*lc.LayoutToolView.LayoutActions.CreateWindowView.Form)
if err != nil {
lc.Messages <- "failed to parse form inputs: " + err.Error()
return
}
// Acquire the cdashdisplay
displayIF, err := lc.DevService.GetDevice(cdashdisplay.Name)
if err != nil {
lc.Messages <- "failed to get " + cdashdisplay.Name
return
}
display, ok := displayIF.(*cdashdisplay.CDashDisplay)
if !ok {
lc.Messages <- "failed to acquire " + cdashdisplay.Name
return
}
// ---
window, err = display.CreateWindow(window)
if err != nil {
lc.Messages <- "failed to create window\n"
return
}
err = lc.updateFormView(window)
if err != nil {
// NOTE: if we fail to append the window to the views we must delete it
// altogether given we won't be able to manipulate it any further
lc.Messages <- "failed to append window to views " + err.Error()
}
}
// updateFormView updates the new window form to be an existing window form and we change
// the buttons for that purpuse
func (lc *LayoutController) updateFormView(win *cdashdisplay.DesktopUIWindow) error {
// OnSuccess we update our form to be an existing window form
lc.Logger.Debug(fmt.Sprintf("updating form view for: %+v", win.Title))
err := lc.LayoutToolView.WindowCreatedSuccessfuly(lc.Logger, win)
if err != nil {
return err
}
// Set the update window button behaviour
lc.Logger.Debug(fmt.Sprintf("setting the update window behaviour: %+v", win.Title))
formView := lc.LayoutToolView.FormQuickAccess[win.UIData.IDX]
lc.Logger.Debug(fmt.Sprintf("setting form button callback: %+v", win.Title))
err = SetFormButtonCallback(formView.Form.Form, "Update", func() {
window, err := lc.parseWindowFormData(*formView.Form)
if err != nil {
return
}
window.UIData.IDX = formView.WinID
lc.updateWindowAction(window)
})
if err != nil {
return err
}
lc.Logger.Debug(fmt.Sprintf("setting input catpure: %+v", win.Title))
formView.Form.Form.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey {
switch ev.Key() {
case tcell.KeyEscape:
lc.App.SetFocus(lc.LayoutToolView.LayoutTree.Tree)
}
return ev
})
lc.Logger.Debug(fmt.Sprintf("giving focus to layouttree"))
lc.App.SetFocus(lc.LayoutToolView.LayoutTree.Tree)
return nil
}
func (lc *LayoutController) newWindowAction() {
if lc.LayoutToolView.LayoutActions.CreateWindowView != nil {
// already exists, just change focus and leave
lc.App.SetFocus(lc.LayoutToolView.LayoutActions.CreateWindowView.Form.Form)
lc.Messages <- "already exists"
return
}
// Get the new window form view
lc.LayoutToolView.LayoutActions.CreateWindowView = views.NewCreateWindowFormView()
newWindowForm := lc.LayoutToolView.LayoutActions.CreateWindowView
// Set the event capture for this form
newWindowForm.Form.Form.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey {
switch ev.Key() {
case tcell.KeyEscape:
lc.App.SetFocus(lc.LayoutToolView.LayoutTree.Tree)
}
return ev
})
// Set the behaviour for when we press the create button
err := SetFormButtonCallback(newWindowForm.Form.Form, "Create", func() {
lc.createWindow()
})
if err != nil {
lc.Messages <- "Failed to set the callback for the create window button"
}
// Set this on the action page and change to it
lc.LayoutToolView.ShowCreateWindowForm()
// Set its focus
lc.App.SetFocus(newWindowForm.Form.Form)
}
func (lc *LayoutController) updateWindowAction(win *cdashdisplay.DesktopUIWindow) {
// Acquire the cdashdisplay
displayIF, err := lc.DevService.GetDevice(cdashdisplay.Name)
if err != nil {
lc.Messages <- "failed to get " + cdashdisplay.Name
return
}
display, ok := displayIF.(*cdashdisplay.CDashDisplay)
if !ok {
lc.Messages <- "failed to acquire " + cdashdisplay.Name
return
}
// ---
err = display.UpdateWindow(win)
lc.Messages <- fmt.Sprintf("Window: %v\n", win)
if err != nil {
lc.Messages <- "failed to update window due to " + err.Error() + "\n"
return
}
}
func (lc *LayoutController) displayLoadedLayouts() {
lc.Logger.Debug("We want to view our layout!")
// Acquire the cdashdisplay
displayIF, err := lc.DevService.GetDevice(cdashdisplay.Name)
if err != nil {
lc.Messages <- "failed to get " + cdashdisplay.Name
return
}
display, ok := displayIF.(*cdashdisplay.CDashDisplay)
if !ok {
lc.Messages <- "failed to acquire " + cdashdisplay.Name
return
}
// ---
for _, w := range display.State.Layout.Windows {
lc.Logger.Debug("==========================================================================")
lc.Logger.Debug(fmt.Sprintf("updating form view for a layout: %+v", w.UIData.TelemetryField))
err := lc.updateFormView(w)
lc.Logger.Debug(fmt.Sprintf("form view updated: %+v", err))
if err != nil {
lc.Logger.Debug("an error happened viewing our layout")
lc.Messages <- "failed to add window to list"
}
}
lc.Logger.Debug("If we don't get here it means we somehow hanged the program earlier")
}
func (lc *LayoutController) getCurrentTreeNodeModel() (*tview.TreeNode, int16, error) {
// Grab the form for the currently selected window
node := lc.LayoutToolView.LayoutTree.Tree.GetCurrentNode()
if node == nil {
return nil, -1, fmt.Errorf("failed to grab current tree node")
}
// From this node get its references
ref, ok := node.GetReference().(int16)
if !ok {
return nil, -1, fmt.Errorf("failed to get data on currently selected window")
}
return node, ref, nil
}
func (lc *LayoutController) loadLayout() {
// Acquire the cdashdisplay
displayIF, err := lc.DevService.GetDevice(cdashdisplay.Name)
if err != nil {
lc.Messages <- "failed to get " + cdashdisplay.Name
return
}
display, ok := displayIF.(*cdashdisplay.CDashDisplay)
if !ok {
lc.Messages <- "failed to acquire " + cdashdisplay.Name
return
}
// ---
// We would get the layout path from somewhere but for nots its layout.yaml
err = display.LoadLayout(lc.SelectedLayout)
if err != nil {
lc.Messages <- "failed to load layout: " + err.Error()
return
}
lc.displayLoadedLayouts()
}
func (lc *LayoutController) unloadLayout() {
// Acquire the cdashdisplay
displayIF, err := lc.DevService.GetDevice(cdashdisplay.Name)
if err != nil {
lc.Messages <- "failed to get " + cdashdisplay.Name
return
}
display, ok := displayIF.(*cdashdisplay.CDashDisplay)
if !ok {
lc.Messages <- "failed to acquire " + cdashdisplay.Name
return
}
// ---
err = display.UnloadLayout()
if err != nil {
lc.Logger.Error(fmt.Sprintf("Failed to unload layout: %+v", err))
return
}
}
func (lc *LayoutController) saveLayout() {
// Acquire the cdashdisplay
displayIF, err := lc.DevService.GetDevice(cdashdisplay.Name)
if err != nil {
lc.Messages <- "failed to get " + cdashdisplay.Name
return
}
display, ok := displayIF.(*cdashdisplay.CDashDisplay)
if !ok {
lc.Messages <- "failed to acquire " + cdashdisplay.Name
return
}
// ---
err = display.SaveLayout(lc.SelectedLayout)
if err != nil {
lc.Messages <- "failed to save layout: " + err.Error()
return
}
}
// deleteWindow will delete the currently highlighted window in the layoutTree
// - get the currently highlighted element
// - tell the LayoutToolView too delete it
func (lc *LayoutController) deleteWindow() {
// Delete the window first
// Get the selected node info
curNode := lc.LayoutToolView.LayoutTree.Tree.GetCurrentNode()
if curNode == nil {
lc.Messages <- "couldn't get a hold of currently selected node\n"
return
}
wID := curNode.GetReference().(int16)
// Acquire the cdashdisplay
displayIF, err := lc.DevService.GetDevice(cdashdisplay.Name)
if err != nil {
lc.Messages <- "failed to get " + cdashdisplay.Name
return
}
display, ok := displayIF.(*cdashdisplay.CDashDisplay)
if !ok {
lc.Messages <- "failed to acquire " + cdashdisplay.Name
return
}
// ---
// Delete it
err = display.DestroyWindow(wID)
if err != nil {
lc.Messages <- "failed to delete window: " + err.Error() + "\n"
return
}
// Reflect the deletion in the view
lc.LayoutToolView.DeleteWindowByNode(curNode)
}
// moveWindow is a utility to interactively move the windows on the display
func (lc *LayoutController) moveWindow() {
lc.Messages <- "Entering move mode (ESC to exit)\n"
_, idx, err := lc.getCurrentTreeNodeModel()
if err != nil {
lc.Messages <- "failed to enter move mode: " + err.Error() + "\n"
return
}
formView := lc.LayoutToolView.FormQuickAccess[idx].Form
oldCapture := formView.Form.GetInputCapture()
formView.Form.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey {
switch ev.Key() {
case tcell.KeyESC:
formView.Form.SetInputCapture(oldCapture)
lc.App.SetFocus(lc.LayoutToolView.LayoutTree.Tree)
}
// Mode switching
switch ev.Rune() {
case 'r':
lc.MoveToolState.Mode = resizeMode
lc.Messages <- "Switching to resizeMode"
return nil
case 'm':
lc.MoveToolState.Mode = moveMode
lc.Messages <- "Switching to moveMode"
return nil
}
// Delegate the current handler input
handler := lc.MoveToolState.CurrentHandler(lc, idx)
if handler != nil {
return handler(ev)
}
return nil
})
lc.App.SetFocus(formView.Form)
}
// changeSelectedLoadout is used to change the currently selected loadout
// its a very bare bones utility to aid development
// NOTE: FOR THE LOVE OF WHATS HOLY ACTUALLY MAKE THIS GOOD BEFORE ADDING MORE FEATURES
func (lc *LayoutController) changeSelectedLoadout() {
// First we get the files in the layouts directory
entries, err := os.ReadDir("./layouts")
if err != nil {
lc.Logger.Error(fmt.Sprintf("Failed to read directory: %v", err))
return
}
// Then we open a modal and pass that list there
lc.Logger.Debug(fmt.Sprintf("Found layouts: %+v", entries))
layoutSelection := views.NewLayoutToolLayoutsList()
layoutSelection.AddEntries(entries)
layoutSelection.Tree.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey {
switch ev.Key() {
case tcell.KeyESC:
// We have to destroy this window, but first we tell the pages to show something else
// NOTE: will this be left hanging and then given to GC? Can we improve that?
lc.LayoutToolView.LayoutActions.LayoutSelection = nil
lc.LayoutToolView.DeleteLayoutList()
lc.App.SetFocus(lc.LayoutToolView.LayoutTree.Tree)
}
return ev
})
layoutSelection.Tree.SetSelectedFunc(func(node *tview.TreeNode) {
ref := node.GetReference()
if ref == nil {
return
}
lc.Messages <- "User selected " + fmt.Sprintf("%+v", ref) + "\n"
lc.SelectedLayout = ref.(string)
lc.LayoutToolView.LayoutTree.Tree.SetTitle(fmt.Sprintf("%s [%s]",
views.LayoutToolTreeViewTitle, ref.(string)))
})
lc.LayoutToolView.LayoutActions.LayoutSelection = layoutSelection
lc.LayoutToolView.ShowLayoutList()
lc.App.SetFocus(lc.LayoutToolView.LayoutActions.LayoutSelection.Tree)
}