gui: convert data tables from TListView to TStringGrid

This commit is contained in:
mappu 2024-07-05 19:21:08 +12:00
parent cecfc338d4
commit d7e3363173
10 changed files with 78 additions and 105 deletions

View File

@ -42,25 +42,17 @@ func (ld *badgerLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
f.propertiesBox.SetText(content) f.propertiesBox.SetText(content)
// Load data // Load data
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
// Badger always uses Key + Value as the columns // Badger always uses Key + Value as the columns
f.contentBox.Columns().Clear()
colKey := f.contentBox.Columns().Add() colKey := f.contentBox.Columns().Add()
colKey.SetCaption("Key") colKey.Title().SetCaption("Key")
colKey.SetWidth(MY_WIDTH) colKey.SetWidth(MY_WIDTH)
colKey.SetAlignment(types.TaLeftJustify) colKey.SetAlignment(types.TaLeftJustify)
colVal := f.contentBox.Columns().Add() colVal := f.contentBox.Columns().Add()
colVal.SetCaption("Value") colVal.Title().SetCaption("Value")
err := ld.db.View(func(txn *badger.Txn) error { err := ld.db.View(func(txn *badger.Txn) error {
// Valid
f.contentBox.Clear()
// Create iterator // Create iterator
opts := badger.DefaultIteratorOptions opts := badger.DefaultIteratorOptions
opts.PrefetchSize = 64 opts.PrefetchSize = 64
@ -71,9 +63,10 @@ func (ld *badgerLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
item := it.Item() item := it.Item()
k := item.Key() k := item.Key()
err := item.Value(func(v []byte) error { err := item.Value(func(v []byte) error {
dataEntry := f.contentBox.Items().Add() rpos := f.contentBox.RowCount()
dataEntry.SetCaption(formatUtf8(k)) f.contentBox.SetRowCount(rpos + 1)
dataEntry.SubItems().Add(formatUtf8(v)) f.contentBox.SetCells(0, rpos, formatUtf8(k))
f.contentBox.SetCells(1, rpos, formatUtf8(v))
return nil return nil
}) })
@ -107,7 +100,7 @@ func (ld *badgerLoadedDatabase) NavContext(ndata *navData) ([]contextAction, err
return nil, nil // No special actions are supported return nil, nil // No special actions are supported
} }
func (ld *badgerLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) { func (ld *badgerLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) {
vcl.ShowMessage("Badger doesn't support querying") vcl.ShowMessage("Badger doesn't support querying")
} }

View File

@ -48,19 +48,15 @@ func (ld *boltLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
f.propertiesBox.SetText(content) f.propertiesBox.SetText(content)
// Load data // Load data
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
// Bolt always uses Key + Value as the columns // Bolt always uses Key + Value as the columns
f.contentBox.Columns().Clear() f.contentBox.Columns().Clear()
colKey := f.contentBox.Columns().Add() colKey := f.contentBox.Columns().Add()
colKey.SetCaption("Key") colKey.Title().SetCaption("Key")
colKey.SetWidth(MY_WIDTH) colKey.SetWidth(MY_WIDTH)
colKey.SetAlignment(types.TaLeftJustify) colKey.SetAlignment(types.TaLeftJustify)
colVal := f.contentBox.Columns().Add() colVal := f.contentBox.Columns().Add()
colVal.SetCaption("Value") colVal.Title().SetCaption("Value")
err := ld.db.View(func(tx *bbolt.Tx) error { err := ld.db.View(func(tx *bbolt.Tx) error {
b := boltTargetBucket(tx, ndata.bucketPath) b := boltTargetBucket(tx, ndata.bucketPath)
@ -70,13 +66,12 @@ func (ld *boltLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
} }
// Valid // Valid
f.contentBox.Clear()
c := b.Cursor() c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() { for k, v := c.First(); k != nil; k, v = c.Next() {
dataEntry := f.contentBox.Items().Add() rpos := f.contentBox.RowCount()
dataEntry.SetCaption(formatUtf8(k)) f.contentBox.SetRowCount(rpos + 1)
dataEntry.SubItems().Add(formatUtf8(v)) f.contentBox.SetCells(0, rpos, formatUtf8(k))
f.contentBox.SetCells(1, rpos, formatUtf8(v))
} }
return nil return nil
}) })
@ -142,7 +137,7 @@ func (ld *boltLoadedDatabase) DeleteBucket(ndata *navData) {
} }
} }
func (ld *boltLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) { func (ld *boltLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) {
vcl.ShowMessage("Bolt doesn't support querying") vcl.ShowMessage("Bolt doesn't support querying")
} }

View File

@ -44,32 +44,25 @@ func (ld *debconfLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
// Load data // Load data
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
// debconf always uses Key + Value as the columns
indexes := make(map[string]int) indexes := make(map[string]int)
f.contentBox.Columns().Clear()
for i, cname := range ld.db.AllColumnNames { for i, cname := range ld.db.AllColumnNames {
indexes[cname] = i indexes[cname] = i
col := f.contentBox.Columns().Add() col := f.contentBox.Columns().Add()
col.SetCaption(cname) col.Title().SetCaption(cname)
col.SetWidth(MY_WIDTH) col.SetWidth(MY_WIDTH)
} }
for _, entry := range ld.db.Entries { for _, entry := range ld.db.Entries {
cell := f.contentBox.Items().Add() rpos := f.contentBox.RowCount()
cell.SetCaption(entry.Name) f.contentBox.SetRowCount(rpos + 1)
f.contentBox.SetCells(0, rpos, entry.Name)
texts := make([]string, len(ld.db.AllColumnNames))
for _, proppair := range entry.Properties { for _, proppair := range entry.Properties {
texts[indexes[proppair[0]]-1 /* compensate for 'Name' always being first */] = proppair[1] f.contentBox.SetCells(int32(indexes[proppair[0]]), rpos, proppair[1])
} }
cell.SubItems().AddStrings2(texts)
} }
// Valid // Valid
@ -91,7 +84,7 @@ func (ld *debconfLoadedDatabase) NavContext(ndata *navData) ([]contextAction, er
return nil, nil // No special actions are supported return nil, nil // No special actions are supported
} }
func (ld *debconfLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) { func (ld *debconfLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) {
vcl.ShowMessage("debconf doesn't support querying") vcl.ShowMessage("debconf doesn't support querying")
} }

View File

@ -20,11 +20,9 @@ func (n *noLoadedDatabase) RootElement() *vcl.TTreeNode {
func (n *noLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) { func (n *noLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
f.propertiesBox.SetText("Open a database to get started...") f.propertiesBox.SetText("Open a database to get started...")
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
} }
func (n *noLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) { func (n *noLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) {
} }
func (n *noLoadedDatabase) NavChildren(ndata *navData) ([]string, error) { func (n *noLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {

View File

@ -46,19 +46,14 @@ func (ld *pebbleLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
f.propertiesBox.SetText(content) f.propertiesBox.SetText(content)
// Load data // Load data
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
// pebble always uses Key + Value as the columns // pebble always uses Key + Value as the columns
f.contentBox.Columns().Clear()
colKey := f.contentBox.Columns().Add() colKey := f.contentBox.Columns().Add()
colKey.SetCaption("Key") colKey.Title().SetCaption("Key")
colKey.SetWidth(MY_WIDTH) colKey.SetWidth(MY_WIDTH)
colKey.SetAlignment(types.TaLeftJustify) colKey.SetAlignment(types.TaLeftJustify)
colVal := f.contentBox.Columns().Add() colVal := f.contentBox.Columns().Add()
colVal.SetCaption("Value") colVal.Title().SetCaption("Value")
itr := ld.db.NewIterWithContext(ctx, nil) itr := ld.db.NewIterWithContext(ctx, nil)
defer itr.Close() defer itr.Close()
@ -71,9 +66,10 @@ func (ld *pebbleLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
return return
} }
dataEntry := f.contentBox.Items().Add() rpos := f.contentBox.RowCount()
dataEntry.SetCaption(formatUtf8(k)) f.contentBox.SetRowCount(rpos + 1)
dataEntry.SubItems().Add(formatUtf8(v)) f.contentBox.SetCells(0, rpos, formatUtf8(k))
f.contentBox.SetCells(0, rpos, formatUtf8(v))
} }
// Valid // Valid
@ -95,7 +91,7 @@ func (ld *pebbleLoadedDatabase) NavContext(ndata *navData) ([]contextAction, err
return nil, nil // No special actions are supported return nil, nil // No special actions are supported
} }
func (ld *pebbleLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) { func (ld *pebbleLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) {
vcl.ShowMessage("pebble doesn't support querying") vcl.ShowMessage("pebble doesn't support querying")
} }

View File

@ -54,9 +54,7 @@ func (ld *redisLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
f.propertiesBox.SetText(infostr) f.propertiesBox.SetText(infostr)
// Disable data tab // Leave data tab disabled (default behaviour)
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
} else if len(ndata.bucketPath) == 1 { } else if len(ndata.bucketPath) == 1 {
// One selected database // One selected database
@ -77,18 +75,14 @@ func (ld *redisLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
// Redis always uses Key + Value as the columns // Redis always uses Key + Value as the columns
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
f.contentBox.Columns().Clear()
colKey := f.contentBox.Columns().Add() colKey := f.contentBox.Columns().Add()
colKey.SetCaption("Key") colKey.Title().SetCaption("Key")
colKey.SetWidth(MY_WIDTH) colKey.SetWidth(MY_WIDTH)
colKey.SetAlignment(types.TaLeftJustify) colKey.SetAlignment(types.TaLeftJustify)
colType := f.contentBox.Columns().Add() colType := f.contentBox.Columns().Add()
colType.SetCaption("Type") colType.Title().SetCaption("Type")
colVal := f.contentBox.Columns().Add() colVal := f.contentBox.Columns().Add()
colVal.SetCaption("Value") colVal.Title().SetCaption("Value")
for _, key := range allKeys { for _, key := range allKeys {
typeName, err := ld.db.Type(ctx, key).Result() typeName, err := ld.db.Type(ctx, key).Result()
@ -97,9 +91,10 @@ func (ld *redisLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
return return
} }
dataEntry := f.contentBox.Items().Add() rpos := f.contentBox.RowCount()
dataEntry.SetCaption(key) // formatUtf8 f.contentBox.SetRowCount(rpos + 1)
dataEntry.SubItems().Add(typeName) f.contentBox.SetCells(0, rpos, formatUtf8([]byte(key)))
f.contentBox.SetCells(1, rpos, typeName)
switch typeName { switch typeName {
case "string": case "string":
@ -108,7 +103,7 @@ func (ld *redisLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
vcl.ShowMessage(fmt.Sprintf("Loading %q/%q: %v", ndata.bucketPath[0], key, err)) vcl.ShowMessage(fmt.Sprintf("Loading %q/%q: %v", ndata.bucketPath[0], key, err))
return return
} }
dataEntry.SubItems().Add(val) f.contentBox.SetCells(2, rpos, val)
case "hash": case "hash":
val, err := ld.db.HGetAll(ctx, key).Result() val, err := ld.db.HGetAll(ctx, key).Result()
@ -117,7 +112,7 @@ func (ld *redisLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
return return
} }
// It's a map[string]string // It's a map[string]string
dataEntry.SubItems().Add(formatAny(val)) f.contentBox.SetCells(2, rpos, formatAny(val))
case "lists": case "lists":
fallthrough fallthrough
@ -129,7 +124,7 @@ func (ld *redisLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
fallthrough fallthrough
default: default:
dataEntry.SubItems().Add("<<<other object type>>>") f.contentBox.SetCells(2, rpos, "<<<other object type>>>")
} }
} }
@ -170,7 +165,7 @@ func (ld *redisLoadedDatabase) NavContext(ndata *navData) ([]contextAction, erro
return nil, nil // No special actions are supported return nil, nil // No special actions are supported
} }
func (ld *redisLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) { func (ld *redisLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) {
ctx := context.Background() ctx := context.Background()
// Need to parse the query into separate string+args fields for the protocol // Need to parse the query into separate string+args fields for the protocol
@ -193,21 +188,24 @@ func (ld *redisLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView
resultArea.Columns().Clear() resultArea.Columns().Clear()
colVal := resultArea.Columns().Add() colVal := resultArea.Columns().Add()
colVal.SetCaption("Result") colVal.Title().SetCaption("Result")
// The result is probably a single value or a string slice // The result is probably a single value or a string slice
switch ret := ret.(type) { switch ret := ret.(type) {
case []string: case []string:
// Multiple values // Multiple values
for _, single := range ret { for _, single := range ret {
cell := resultArea.Items().Add()
cell.SetCaption(single) // formatUtf8 rpos := resultArea.RowCount()
resultArea.SetRowCount(rpos + 1)
resultArea.SetCells(0, rpos, formatUtf8([]byte(single)))
} }
default: default:
// Single value // Single value
dataEntry := resultArea.Items().Add() rpos := resultArea.RowCount()
dataEntry.SetCaption(formatAny(ret)) // formatUtf8 resultArea.SetRowCount(rpos + 1)
resultArea.SetCells(0, rpos, formatAny(ret)) // formatUtf8
} }

View File

@ -42,14 +42,10 @@ func (ld *sqliteLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
if len(ndata.bucketPath) == 0 { if len(ndata.bucketPath) == 0 {
// Top-level // Top-level
f.propertiesBox.SetText("Please select...") f.propertiesBox.SetText("Please select...")
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
} else if len(ndata.bucketPath) == 1 { } else if len(ndata.bucketPath) == 1 {
// Category (tables, ...) // Category (tables, ...)
f.propertiesBox.SetText("Please select...") f.propertiesBox.SetText("Please select...")
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
} else if len(ndata.bucketPath) == 2 && ndata.bucketPath[0] == sqliteTablesCaption { } else if len(ndata.bucketPath) == 2 && ndata.bucketPath[0] == sqliteTablesCaption {
// Render for specific table // Render for specific table
@ -66,9 +62,6 @@ func (ld *sqliteLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
// Display table properties // Display table properties
f.propertiesBox.SetText(fmt.Sprintf("Selected table %q\n\nSchema:\n\n%s", tableName, schemaStmt)) f.propertiesBox.SetText(fmt.Sprintf("Selected table %q\n\nSchema:\n\n%s", tableName, schemaStmt))
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
// Load column details // Load column details
// Use SELECT form instead of common PRAGMA table_info so we can just get names // Use SELECT form instead of common PRAGMA table_info so we can just get names
// We could possibly get this from the main data select, but this will // We could possibly get this from the main data select, but this will
@ -129,17 +122,17 @@ func (ld *sqliteLoadedDatabase) sqliteGetColumnNamesForTable(tableName string) (
return ret, nil return ret, nil
} }
func populateColumns(names []string, dest *vcl.TListView) { func populateColumns(names []string, dest *vcl.TStringGrid) {
dest.Columns().Clear() dest.Columns().Clear()
for _, columnName := range names { for _, columnName := range names {
col := dest.Columns().Add() col := dest.Columns().Add()
col.SetCaption(columnName) col.Title().SetCaption(columnName)
col.SetWidth(MY_WIDTH) col.SetWidth(MY_WIDTH)
col.SetAlignment(types.TaLeftJustify) col.SetAlignment(types.TaLeftJustify)
} }
} }
func populateRows(rr *sql.Rows, dest *vcl.TListView) { func populateRows(rr *sql.Rows, dest *vcl.TStringGrid) {
numColumns := int(dest.Columns().Count()) numColumns := int(dest.Columns().Count())
@ -156,10 +149,10 @@ func populateRows(rr *sql.Rows, dest *vcl.TListView) {
return return
} }
dataEntry := dest.Items().Add() rpos := dest.RowCount()
dataEntry.SetCaption(formatAny(fields[0])) dest.SetRowCount(rpos + 1)
for i := 1; i < len(fields); i += 1 { for i := 0; i < len(fields); i += 1 {
dataEntry.SubItems().Add(formatAny(fields[i])) dest.SetCells(int32(i), rpos, formatAny(fields[i]))
} }
} }
@ -169,7 +162,7 @@ func populateRows(rr *sql.Rows, dest *vcl.TListView) {
} }
} }
func (ld *sqliteLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) { func (ld *sqliteLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) {
rr, err := ld.db.Query(query) rr, err := ld.db.Query(query)
if err != nil { if err != nil {
vcl.ShowMessage(err.Error()) vcl.ShowMessage(err.Error())

View File

@ -19,7 +19,7 @@ type loadedDatabase interface {
DriverName() string DriverName() string
RootElement() *vcl.TTreeNode RootElement() *vcl.TTreeNode
RenderForNav(f *TMainForm, ndata *navData) RenderForNav(f *TMainForm, ndata *navData)
ExecQuery(query string, resultArea *vcl.TListView) ExecQuery(query string, resultArea *vcl.TStringGrid)
NavChildren(ndata *navData) ([]string, error) NavChildren(ndata *navData) ([]string, error)
NavContext(ndata *navData) ([]contextAction, error) NavContext(ndata *navData) ([]contextAction, error)
Keepalive(ndata *navData) Keepalive(ndata *navData)

24
main.go
View File

@ -30,9 +30,9 @@ type TMainForm struct {
Buckets *vcl.TTreeView Buckets *vcl.TTreeView
Tabs *vcl.TPageControl Tabs *vcl.TPageControl
propertiesBox *vcl.TMemo propertiesBox *vcl.TMemo
contentBox *vcl.TListView contentBox *vcl.TStringGrid
queryInput *vcl.TMemo queryInput *vcl.TMemo
queryResult *vcl.TListView queryResult *vcl.TStringGrid
none *noLoadedDatabase none *noLoadedDatabase
dbs []loadedDatabase dbs []loadedDatabase
@ -209,14 +209,12 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
dataTab.SetCaption("Data") dataTab.SetCaption("Data")
dataTab.SetImageIndex(imgTable) dataTab.SetImageIndex(imgTable)
f.contentBox = vcl.NewListView(dataTab) f.contentBox = vcl.NewStringGrid(dataTab)
f.contentBox.SetParent(dataTab) f.contentBox.SetParent(dataTab)
f.contentBox.BorderSpacing().SetAround(MY_SPACING) f.contentBox.BorderSpacing().SetAround(MY_SPACING)
f.contentBox.SetAlign(types.AlClient) // fill remaining space f.contentBox.SetAlign(types.AlClient) // fill remaining space
f.contentBox.SetViewStyle(types.VsReport) // "Report style" i.e. has columns f.contentBox.SetOptions(f.contentBox.Options().Include(types.GoThumbTracking))
f.contentBox.SetAutoWidthLastColumn(true) vcl_stringgrid_clear(f.contentBox)
f.contentBox.SetReadOnly(true)
f.contentBox.Columns().Clear()
queryTab := vcl.NewTabSheet(f.Tabs) queryTab := vcl.NewTabSheet(f.Tabs)
queryTab.SetParent(f.Tabs) queryTab.SetParent(f.Tabs)
@ -260,16 +258,14 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
vsplit.SetAlign(types.AlTop) vsplit.SetAlign(types.AlTop)
vsplit.SetTop(2) vsplit.SetTop(2)
f.queryResult = vcl.NewListView(queryTab) f.queryResult = vcl.NewStringGrid(queryTab)
f.queryResult.SetParent(queryTab) f.queryResult.SetParent(queryTab)
f.queryResult.SetAlign(types.AlClient) // fill remaining space f.queryResult.SetAlign(types.AlClient) // fill remaining space
f.queryResult.SetViewStyle(types.VsReport) // "Report style" i.e. has columns
f.queryResult.SetAutoWidthLastColumn(true)
f.queryResult.SetReadOnly(true)
f.queryResult.Columns().Clear()
f.queryResult.BorderSpacing().SetLeft(MY_SPACING) f.queryResult.BorderSpacing().SetLeft(MY_SPACING)
f.queryResult.BorderSpacing().SetRight(MY_SPACING) f.queryResult.BorderSpacing().SetRight(MY_SPACING)
f.queryResult.BorderSpacing().SetBottom(MY_SPACING) f.queryResult.BorderSpacing().SetBottom(MY_SPACING)
f.queryResult.SetOptions(f.queryResult.Options().Include(types.GoThumbTracking))
vcl_stringgrid_clear(f.queryResult)
f.none = &noLoadedDatabase{} f.none = &noLoadedDatabase{}
f.OnNavChange(f, nil) // calls f.none.RenderForNav and sets up status bar content f.OnNavChange(f, nil) // calls f.none.RenderForNav and sets up status bar content
@ -543,6 +539,10 @@ func (f *TMainForm) OnNavChange(sender vcl.IObject, node *vcl.TTreeNode) {
ld = ndata.ld ld = ndata.ld
} }
// Reset some controls that the render function is expected to populate
f.propertiesBox.Clear()
vcl_stringgrid_clear(f.contentBox)
ld.RenderForNav(f, ndata) // Handover to the database type's own renderer function ld.RenderForNav(f, ndata) // Handover to the database type's own renderer function
// We're in charge of common status bar text updates // We're in charge of common status bar text updates

View File

@ -62,3 +62,10 @@ func vcl_default_tab_background() types.TColor {
return colors.ClBtnFace // 0x00f0f0f0 return colors.ClBtnFace // 0x00f0f0f0
} }
} }
func vcl_stringgrid_clear(d *vcl.TStringGrid) {
d.SetFixedCols(0) // No left-hand cols
d.SetRowCount(1) // The 0th row is the column headers
d.SetEnabled(false)
d.Columns().Clear()
}