VladimirMarkelov / clui

Command Line User Interface (Console UI inspired by TurboVision)
MIT License
670 stars 50 forks source link

Example of "Add new row"? #130

Closed MostHated closed 5 years ago

MostHated commented 5 years ago

Hey there, I am having a bit of trouble figuring this out as I am still fairly new to Go.

I am currently using the preload table and am able to create test data manually just fine to display it as well as edit it thanks to your first example, but I am having issue figuring out how I would start from scratch with 0 value in it though and making the table add a new entry using:

        case ui.TableActionNew:
            action = "Add new row"

Are you able to perhaps add an extremely simple example? What I was using with test data was:

var tmpAssetData []*AssetDetails

type AssetDetails struct {
    AssetCode     string
    AssetName     string
    AssetApiKey   string
    AssetRole     string
    AssetVersion  string
    AssetReplaced string
    ReplaceDate   string
}

var AssetDetail = []*AssetDetails{{AssetCode: "SCT", AssetName: "SCT - Scriptable Text", AssetApiKey: "123123112312312323123123123", AssetRole: "123123123123123123", AssetReplaced: "No", AssetVersion: "1", ReplaceDate: ""},
    {AssetCode: "UFPS1", AssetName: "UFPS : Ultimate FPS", AssetApiKey: "123123112312312323123123123", AssetRole: "123123123123123123", AssetReplaced: "Yes", AssetVersion: "1", ReplaceDate: "2018-06-06"},
    {AssetCode: "UCC", AssetName: "Ultimate Character Controller", AssetApiKey: "123123112312312323123123123", AssetRole: "123123123123123123", AssetReplaced: "Yes", AssetVersion: "1", ReplaceDate: "2018-06-06"},
    {AssetCode: "TPC", AssetName: "Third Person Controller", AssetApiKey: "123123112312312323123123123", AssetRole: "123123123123123123", AssetReplaced: "Yes", AssetVersion: "1", ReplaceDate: "2018-06-06"},

That worked just fine, at the start of the call of the table, I had:

tmpAssetData = AssetDetail

    d.data = make([][]string, rowCount, rowCount)
    for i := 0; i < rowCount; i++ {
        absIndex := firstRow + i
        d.data[i] = make([]string, columnInTable, columnInTable)
        d.data[i][0] = tmpAssetData[absIndex].AssetCode
        d.data[i][1] = tmpAssetData[absIndex].AssetName
        d.data[i][2] = tmpAssetData[absIndex].AssetApiKey
        d.data[i][3] = tmpAssetData[absIndex].AssetRole
        d.data[i][4] = tmpAssetData[absIndex].AssetVersion
        d.data[i][5] = tmpAssetData[absIndex].AssetReplaced
        d.data[i][6] = tmpAssetData[absIndex].ReplaceDate
    }

I tried getting rid of var AssetDetail = []*AssetDetails{{AssetCode: "SCT", etc, and then at the start just putting either of these:

    if len(tmpAssetData) == 0 {
        tmpAssetData[0].AssetCode = ""
        tmpAssetData[0].AssetName = ""
        tmpAssetData[0].AssetApiKey = ""
        tmpAssetData[0].AssetRole = ""
        tmpAssetData[0].AssetVersion = ""
        tmpAssetData[0].AssetReplaced = ""
        tmpAssetData[0].ReplaceDate = ""
    }

    if tmpAssetData[0] == nil {
        tmpAssetData[0].AssetCode = ""
        tmpAssetData[0].AssetName = ""
        tmpAssetData[0].AssetApiKey = ""
        tmpAssetData[0].AssetRole = ""
        tmpAssetData[0].AssetVersion = ""
        tmpAssetData[0].AssetReplaced = ""
        tmpAssetData[0].ReplaceDate = ""
    }

Neither worked and just gave an out of range error. What might I have to put into:

        case ui.TableActionNew:
            action = "Add new row"

For it to create a new row/entry into var tmpAssetData []*AssetDetails if there was not one in there to begin with?

Not sure if it helps any, but here was how I was editing it currently:

    td.OnAction(func(ev ui.TableEvent) {
        btns := []string{TxtApplyBtn, TxtCancelBtn}
        var action string
        switch ev.Action {
        case ui.TableActionSort:
            action = "Sort table"
        case ui.TableActionEdit:
            c := ev.Col
            r := ev.Row
            var newInfo = ui.ColumnDrawInfo{Row: r, Col: c}
            var editVal = TableEdit{Row: r, Col: c}

            (func(info *ui.ColumnDrawInfo) {
                editVal.OldVal = cache.value(info.Row, info.Col)
            })(&newInfo)

            dlg := CreateEditableDialog(fmt.Sprintf("%s: %s", TxtEditing, editVal.OldVal), editVal.OldVal)
            dlg.View.SetSize(35, 10)
            dlg.View.BaseControl.SetSize(35, 10)
            dlg.OnClose(func() {
                switch dlg.Result() {
                case ui.DialogButton1:
                    editVal.NewVal = dlg.EditResult()
                    cache.NewValue(editVal.Row, editVal.Col, editVal.NewVal)
                    ui.PutEvent(ui.Event{Type: ui.EventRedraw})
                }
            })
VladimirMarkelov commented 5 years ago

Neither worked and just gave an out of range error

What failed? I have no clue what the error was and what line if crashed. Only gueses:

absIndex := firstRow + i
...
    d.data[i] = make([]string, columnInTable, columnInTable)
    d.data[i][0] = tmpAssetData[absIndex].AssetCode`

1) Do you have enough items in tmpAssetData, so len(tmpAssetData) is firstRow+rowCount-1? 2) Have you pre-filled tmpAssetData?

If you set TableView row and col count to the same values(rowCount, colCount), you do not need to write anything for ActionNew

MostHated commented 5 years ago

No, sorry, there is nothing in there at the time. I removed the data I created by hand in code. I am trying to find out how to create a new row if there are 0 rows. Pretend it's a brand new application with 0 data or rows. I am trying to figure out how to use "add new row" action to start fresh and add data to an empty dataset.

VladimirMarkelov commented 5 years ago

I see. I'll try this case myself

MostHated commented 5 years ago

Here is what I have been attempting to come up with:

I feel like I might be close, but I am just not quite sure.


        case ui.TableActionNew:
            //action = "Add new row"
            c := ev.Col
            r := ev.Row
            var editVal = TableEdit{Row: r, Col: c}

            dlg := CreateEditableDialog(fmt.Sprintf("%s:", TxtNewAssetCodeValue), "") // --- A custom popup I made so I can make more adjustments than the default one had
            dlg.View.SetSize(35, 10)
            dlg.OnClose(func() {
                switch dlg.Result() {
                case ui.DialogButton1:
                    editVal.NewVal = dlg.EditResult()
                    if tmpAssetData == nil {
                        var newInfo = ui.ColumnDrawInfo{Row: 0, Col: 0}
                        cache.data = make([][]string, rowCount, rowCount) // ------ I have this because in preload I have at the beginning  if tmpAssetData == nil { return }
                        (func(info *ui.ColumnDrawInfo) {
                            cache.data[newInfo.Row][newInfo.Col] = editVal.NewVal  // ------------------------------195
                            info.Text = cache.data[newInfo.Row][newInfo.Col]
                        })(&newInfo)

                        //cache.NewValue(0, 0, editVal.NewVal)
                    } else {
                        cache.NewValue(len(tmpAssetData), 0, editVal.NewVal)
                    }

                    ui.PutEvent(ui.Event{Type: ui.EventRedraw})
                }
            })
panic: runtime error: index out of range

goroutine 1 [running]:
github.com/instance-id/GoUI/elements.CreateTableDialog.func3.3.1(...)
        /home/mosthated/_dev/programming/go/src/github.com/instance-id/GoUI/elements/CreateTableDialog.go:195
github.com/instance-id/GoUI/elements.CreateTableDialog.func3.3()
        /home/mosthated/_dev/programming/go/src/github.com/instance-id/GoUI/elements/CreateTableDialog.go:197 +0x22a

----------- This was my whole table page, just to give it some context.

package elements

import (
    "fmt"
    . "github.com/instance-id/GoUI/text"
    . "github.com/instance-id/GoUI/utils"
    ui "github.com/instance-id/clui"
    term "github.com/nsf/termbox-go"
)

var tmpAssetData []*AssetDetails

type dbCache struct {
    firstRow int        // previous first visible row
    rowCount int        // previous visible row count
    data     [][]string // cache - contains at least 'rowCount' rows from DB
}

const columnInTable = 7

func (d *dbCache) preload(firstRow, rowCount int) {

    if tmpAssetData == nil {
        return
    }

    if firstRow == d.firstRow && rowCount == d.rowCount {
        // fast path: view area is the same, return immediately
        return
    }

    d.data = make([][]string, rowCount, rowCount)
    for i := 0; i < rowCount; i++ {
        absIndex := firstRow + i
        d.data[i] = make([]string, columnInTable, columnInTable)
        d.data[i][0] = tmpAssetData[absIndex].AssetCode
        d.data[i][1] = tmpAssetData[absIndex].AssetName
        d.data[i][2] = tmpAssetData[absIndex].AssetApiKey
        d.data[i][3] = tmpAssetData[absIndex].AssetRole
        d.data[i][4] = tmpAssetData[absIndex].AssetVersion
        d.data[i][5] = tmpAssetData[absIndex].AssetReplaced
        d.data[i][6] = tmpAssetData[absIndex].ReplaceDate
    }

    // do not forget to save the last values
    d.firstRow = firstRow
    d.rowCount = rowCount
}

// --- Custom function for editing place ---------------------------------
func (d *dbCache) NewValue(row, col int, newText string) {
    d.data[row][col] = newText
}

func (d *dbCache) value(row, col int) string {
    rowId := row - d.firstRow
    if rowId >= len(d.data) {
        return ""
    }
    rowValues := d.data[rowId]
    if col >= len(rowValues) {
        return ""
    }
    return rowValues[col]
}

// --- Window type for data table ----------------------------------------
func CreateTableDialog(btn *ui.ButtonNoShadow, tableTitle string) {
    tableDialog := new(TableDialog)

    // --- Obtain terminal overall size ----------------------------------
    cw, ch := term.Size()

    // --- Save current values to temp value until saved -----------------
    tmpAssetData = Asset

    // --- Create new popup window for table data ------------------------
    tableDialog.View = ui.AddWindow(cw/2-75, ch/2-16, ui.AutoSize, ui.AutoSize, TxtAssetDetails)
    ui.WindowManager().BeginUpdate()
    defer ui.WindowManager().EndUpdate()
    tableDialog.View.SetGaps(1, ui.KeepValue)
    tableDialog.View.SetModal(true)
    tableDialog.View.SetPack(ui.Vertical)

    tableDialog.Frame = NewFramedWindowInput(tableDialog.View, "", nil)

    // --- Create data table ---------------------------------------------
    td := ui.CreateTableView(tableDialog.Frame, 145, 15, 1)
    ui.ActivateControl(tableDialog.Frame, td)

    cache := &dbCache{firstRow: -1}
    rowCount := len(Asset)
    td.SetShowLines(true)
    td.SetShowRowNumber(true)
    td.SetRowCount(rowCount)

    cols := []ui.Column{
        ui.Column{Title: "Asset Code", Width: 5, Alignment: ui.AlignLeft},
        ui.Column{Title: "Asset Name", Width: 50, Alignment: ui.AlignLeft},
        ui.Column{Title: "Asset APIKey", Width: 30, Alignment: ui.AlignLeft},
        ui.Column{Title: "Asset RoleId", Width: 20, Alignment: ui.AlignLeft},
        ui.Column{Title: "Version", Width: 7, Alignment: ui.AlignLeft},
        ui.Column{Title: "Replaced?", Width: 10, Alignment: ui.AlignLeft},
        ui.Column{Title: "Replace Date", Width: 12, Alignment: ui.AlignLeft},
    }
    td.SetColumns(cols)

    td.OnBeforeDraw(func(col, row, colCnt, rowCnt int) {
        cache.preload(row, rowCnt)
        l, t, w, h := td.VisibleArea()
        tableDialog.Frame.SetTitle(fmt.Sprintf("Caching: %d:%d - %dx%d", l, t, w, h))
    })
    td.OnDrawCell(func(info *ui.ColumnDrawInfo) {
        info.Text = cache.value(info.Row, info.Col)
    })

    td.OnAction(func(ev ui.TableEvent) {
        // btns := []string{TxtApplyBtn, TxtCancelBtn}
        //var action string
        switch ev.Action {
        case ui.TableActionSort:
            //action = "Sort table"
        case ui.TableActionEdit:
            c := ev.Col
            r := ev.Row
            var newInfo = ui.ColumnDrawInfo{Row: r, Col: c}
            var editVal = TableEdit{Row: r, Col: c}

            (func(info *ui.ColumnDrawInfo) {
                editVal.OldVal = cache.value(info.Row, info.Col)
            })(&newInfo)

            dlg := CreateEditableDialog(fmt.Sprintf("%s: %s", TxtEditing, editVal.OldVal), editVal.OldVal)
            dlg.View.SetSize(35, 10)
            dlg.View.BaseControl.SetSize(35, 10)
            dlg.OnClose(func() {
                switch dlg.Result() {
                case ui.DialogButton1:
                    editVal.NewVal = dlg.EditResult()
                    cache.NewValue(editVal.Row, editVal.Col, editVal.NewVal)
                    ui.PutEvent(ui.Event{Type: ui.EventRedraw})
                }
            })
            return
        case ui.TableActionNew:
            //action = "Add new row"
            c := ev.Col
            r := ev.Row
            var editVal = TableEdit{Row: r, Col: c}

            dlg := CreateEditableDialog(fmt.Sprintf("%s:", TxtNewAssetCodeValue), "")
            dlg.View.SetSize(35, 10)
            dlg.View.BaseControl.SetSize(35, 10)
            dlg.OnClose(func() {
                switch dlg.Result() {
                case ui.DialogButton1:
                    editVal.NewVal = dlg.EditResult()
                    if tmpAssetData == nil {
                        var newInfo = ui.ColumnDrawInfo{Row: 0, Col: 0}
                        cache.data = make([][]string, rowCount, rowCount)
                        (func(info *ui.ColumnDrawInfo) {
                            cache.data[newInfo.Row][newInfo.Col] = editVal.NewVal
                            info.Text = cache.data[newInfo.Row][newInfo.Col]
                        })(&newInfo)
                    } else {
                        cache.NewValue(len(tmpAssetData), 0, editVal.NewVal)
                    }

                    ui.PutEvent(ui.Event{Type: ui.EventRedraw})
                }
            })
        case ui.TableActionDelete:
            //action = "Delete row"
        default:
            //action = "Unknown action"
        }

        //dlg := ui.CreateConfirmationDialog(
        //  "<c:blue>"+action,
        //  "Click any button or press <c:yellow>SPACE<c:> to close the dialog",
        //  btns, ui.DialogButton1)
        //dlg.OnClose(func() {})
    })

    btnFrame := ui.CreateFrame(tableDialog.Frame, 1, 1, ui.BorderNone, ui.Fixed)
    btnFrame.SetPaddings(1, 1)
    textFrame := ui.CreateFrame(btnFrame, 1, 1, ui.BorderNone, ui.Fixed)
    textFrame.SetPack(ui.Vertical)

    ui.CreateLabel(textFrame, ui.AutoSize, ui.AutoSize, TxtInstructs1, ui.Fixed)
    ui.CreateLabel(textFrame, ui.AutoSize, ui.AutoSize, TxtInstructs2, ui.Fixed)
    ui.CreateLabel(textFrame, ui.AutoSize, ui.AutoSize, TxtInstructs3, ui.Fixed)

    // --- Window Controls -----------------------------------------------
    ui.CreateFrame(btnFrame, 1, 1, ui.BorderNone, 1)
    BtnSave := ui.CreateButton(btnFrame, 15, 1, TxtSaveBtn, ui.Fixed)
    BtnSave.OnClick(func(ev ui.Event) {
        Asset = tmpAssetData
        btn.SetEnabled(true)
    })
    BtnClose := ui.CreateButton(btnFrame, 15, 1, TxtCloseBtn, ui.Fixed)
    BtnClose.OnClick(func(ev ui.Event) {
        ui.WindowManager().DestroyWindow(tableDialog.View)
        btn.SetEnabled(true)
    })

    BtnSave.SetActive(false)
    BtnClose.SetActive(false)
}
VladimirMarkelov commented 5 years ago

Thanks for the sources, but it is still unclear:

  1. It displays error at L195-197. If I copy-paste the source, in my editor it is:
195
196 // --- Window Controls -----------------------------------------------
197 ui.CreateFrame(btnFrame, 1, 1, ui.BorderNone, 1)

That looks incorrect.

  1. How big is Asset? I see rowCount := len(Asset), and if Asset has fewer items than 7, it may fail inside preload at any line that is like d.data[i][N] = tmpAssetData[absIndex].AssetCode (L36-L42)
VladimirMarkelov commented 5 years ago

Oh, sorry. I just opened your message in email and saw that you had marked the line that fails :)

But the question #2 is still unclear: What the size of Asset? since rowcCount=len(Asset). if it is empty than it is clear why L195 panics.

Another point. Quotation from your code:

cache.data = make([][]string, rowCount, rowCount)
 (func(info *ui.ColumnDrawInfo) {
   cache.data[newInfo.Row][newInfo.Col] = editVal.NewVal  // ------------------------------195

you allocated memory for parent slice. But where is allocation for the nested one? I mean something like this:

cache.data = make([][]string, rowCount, rowCount)
cache.data[0] = make([]string, colCount, colCount)
MostHated commented 5 years ago

It is no worries. I think I almost got it. There were a few things I had to change. I think I should get it worked out shortly though!

MostHated commented 5 years ago

I am super close. I have adding a new row working if at least some data exists. I just have to make it so it can add one if none exists. At least I am on the right track. : D

It is definitely not the most clean, I will have to go back through and clean it up. I noticed I forgot to ever actually update the tmpAssetData when I was updating the table. I was only updating d.data, so I had to create a way to update that, as well as the tables data. (It will probably never be more than 7 rows, most of the time it will be 1-2 at most anyways). I had to add another struct as a container for AssetData and create the test data first a new way.

type AssetContainer struct {
    AD []AssetDetails
}

    Asset = &AssetContainer{AD: []AssetDetails{
        {AssetCode: "UFPS1",
            AssetName:     "UFPS : Ultimate FPS",
            AssetApiKey:   "123123112312312323123123123",
            AssetRole:     "123123123123123123",
            AssetReplaced: "Yes",
            AssetVersion:  "1",
            ReplaceDate:   "2018-06-06"},
        {
            AssetCode:     "UFPS2",
            AssetName:     "UFP2 : Ultimate FPS",
            AssetApiKey:   "123123112312312323123123123",
            AssetRole:     "123123123123123123",
            AssetReplaced: "Yes",
            AssetVersion:  "1",
            ReplaceDate:   "2018-06-06"}},
    }

Once I did that, when I updated the data from within the table I had to update both the actual data and the tables display of the data.

// --- This is in the TableActionEdit ----------------------------------------
dlg.OnClose(func() {
                switch dlg.Result() {
                case ui.DialogButton1:
                    editVal.NewVal = dlg.EditResult()
                    cache.UpdateData(editVal.Row, editVal.Col, editVal.NewVal)
                    ui.PutEvent(ui.Event{Type: ui.EventRedraw})
                }
            })

// --- This updates both the table and tmpAssetData ---------------------------
// -- There may be a cleaner / smaller way of doing this ----------------------
func (d *dbCache) UpdateData(row int, col int, data string) []AssetDetails {

    switch col {
    case 0:
        tmpAssetData.AD[row].AssetCode = data
        d.data[row][col] = tmpAssetData.AD[row].AssetCode
    case 1:
        tmpAssetData.AD[row].AssetName = data
        d.data[row][col] = tmpAssetData.AD[row].AssetName
    case 2:
        tmpAssetData.AD[row].AssetApiKey = data
        d.data[row][col] = tmpAssetData.AD[row].AssetApiKey
    case 3:
        tmpAssetData.AD[row].AssetRole = data
        d.data[row][col] = tmpAssetData.AD[row].AssetRole
    case 4:
        tmpAssetData.AD[row].AssetVersion = data
        d.data[row][col] = tmpAssetData.AD[row].AssetVersion
    case 5:
        tmpAssetData.AD[row].AssetReplaced = data
        d.data[row][col] = tmpAssetData.AD[row].AssetReplaced
    case 6:
        tmpAssetData.AD[row].ReplaceDate = data
        d.data[row][col] = tmpAssetData.AD[row].ReplaceDate
    }

    return tmpAssetData.AD
}

Creating the container for the actual AssetData then allowed me to do this:

func (a *AssetContainer) AddNewAsset(asset AssetDetails) []AssetDetails {
    a.AD = append(a.AD, asset)
    return a.AD
}

So now in TableACtionNew, I have this:

            dlg.OnClose(func() {
                switch dlg.Result() {
                case ui.DialogButton1:
                    editVal.NewVal = dlg.EditResult()
                    if tmpAssetData.AD == nil {
                        var newInfo = ui.ColumnDrawInfo{Row: 0, Col: 0}
                        cache.data = make([][]string, rowCount, rowCount)
                        data := cache.UpdateData(newInfo.Row, newInfo.Col, editVal.NewVal)
                        tmpAssetData.AD = data
                    } else {
                        details := AssetDetails{AssetCode: editVal.NewVal}
                        tmpAssetData.AddNewAsset(details)
                        cache.AddNewRow(editVal.NewVal)
                    }
                }

Seems to work pretty well in adding a new row. That 3rd one in the pic is the new one. I used this code to add it to the table:

func (d *dbCache) AddNewRow(newAsset string) {
    data := []string{newAsset, "", "", "", "", "", ""}
    d.data = append(d.data, data)
}

MostHated commented 5 years ago

EDIT -- Nevermind about my question below, I created a work around that ends up doing pretty much the same thing by doing the following:

    var newInfo = ui.ColumnDrawInfo{Row: 0, Col: 0}
    var restrictor = 0

    td.OnActive(func(active bool) {
        if (func(info *ui.ColumnDrawInfo) string {
            return cache.value(info.Row, info.Col)
        })(&newInfo) == "" {
            if restrictor == 0 {
                cache.CreateNewData(tmpAssetData)
                restrictor = 1
            }
        }
    })

Is it possible to force a table event manually?

I tried this:

    if Asset == nil {
        func() ui.TableAction {
            event := ui.TableEvent{Action: ui.TableAction(ui.TableActionNew)}
            return event.Action
        }()
    }

I was hoping that if Asset didn't have data upon opening the table, before the data was populated and what not that I could trigger the TableActionNew event. I tried to use ui.SendEventToChild() but it seems that table actions are not the right kind of action to be able to send that.

MostHated commented 5 years ago

I can't seem to find a way to redraw the table with current data. I read that things like OnBeforeDraw and OnDrawCell are callbacks, but I am not sure what is actually telling the table to draw a new row. I add data to dbCache (as well as my tmpAssetData), but I always have to save (which takes the data from tmpAssetData and writes it to AssetData) close the table and reopen it (which then takes any data that is in AssetData and puts it into tmpAssetData, which then the table uses for its data to draw with initially.

So when I create a new row, I create it in dbCache and tmpAssetData, but nothing triggers for it to redraw with the newly added row. I tried to manually call Draw(), td.OnBeforeDraw(), td.OnDrawCell(), cache.preload() separetly, just to see if it would add the new column after I use ui.TableActionNew but I am stumped on how to do it without closing and reopening the window.

Is there a way somehow in dlg.OnClose(func()) I can force it to happen once I add my new row? I could technically force the entire table window to close and reopen, but that would require saving the new row from tmpAssetData to AssetData automatically so that when the table opens again it can load the new row back into tmpAssetData because that gets wiped out when the table closes. Saving it automatically and opening/closing again quickly would defeat the purpose of having a manual save button, and storing values in tmpAssetData (which is in case the user adds a row, but then decides to cancel the action, they have to save manually with the button)

VladimirMarkelov commented 5 years ago

So when I create a new row, I create it in dbCache and tmpAssetData, but nothing triggers for it to redraw with the newly added row

I have not tested it, but I'd try the following flow on successful row addition: 1) update dbCache and add a new data row to it 2) set new row count with SetRowCount 3) call screen refresh with ui.PutEvent(ui.Event{Type: ui.EventRedraw})

MostHated commented 5 years ago

Looks like that did the job. Thanks!