package controllers import ( "fmt" "os" "strconv" "esdi/cdashdisplay" helper "esdi/helpers" "esdi/services" "esdi/tui/internal/views" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) type LayoutController struct { *Controller OnExit func() LayoutToolView *views.LayoutToolView Messages chan string DevService *services.CDashService MoveToolState *windowManipState SelectedLayout string // NOTE: This should be a struct to handle its own things } func NewLayoutController(base *Controller, service *services.CDashService) *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 } window, err = lc.DevService.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) { err := lc.DevService.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!") for _, w := range lc.DevService.CDash.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() { // We would get the layout path from somewhere but for nots its layout.yaml err := lc.DevService.LoadLayout(lc.SelectedLayout) if err != nil { lc.Messages <- "failed to load layout: " + err.Error() return } lc.displayLoadedLayouts() } func (lc *LayoutController) unloadLayout() { err := lc.DevService.UnloadLayout() if err != nil { lc.Logger.Error(fmt.Sprintf("Failed to unload layout: %+v", err)) return } } func (lc *LayoutController) saveLayout() { err := lc.DevService.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) // Delete it err := lc.DevService.DeleteWindow(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) }