package main import ( "errors" "fmt" "runtime" "runtime/debug" "strings" "unsafe" "github.com/pkg/browser" "github.com/ying32/govcl/vcl" "github.com/ying32/govcl/vcl/types" "github.com/ying32/govcl/vcl/types/colors" ) const ( APPNAME = "yvbolt" HOMEPAGE_URL = "https://code.ivysaur.me/yvbolt" CO_INSERT = colors.ClYellow CO_EDIT_IMPLICIT = colors.ClLightgreen CO_EDIT_EXPLICIT = colors.ClGreen CO_DELETE = colors.ClRed ) type TMainForm struct { *vcl.TForm ImageList *vcl.TImageList Menu *vcl.TMainMenu SQLiteUseCliDriver *vcl.TMenuItem StatusBar *vcl.TStatusBar Buckets *vcl.TTreeView Tabs *vcl.TPageControl propertiesBox *vcl.TMemo contentBox *vcl.TStringGrid isEditing bool insertRows map[int32]struct{} // Rows in the StringGrid that are to-be-inserted deleteRows map[int32]struct{} updateRows map[int32][]int32 // Row->cells that are to-be-updated queryInput *vcl.TMemo queryResult *vcl.TStringGrid none *noLoadedDatabase dbs []loadedDatabase } var ( mainForm *TMainForm ) func main() { vcl.RunApp(&mainForm) } func (f *TMainForm) OnFormCreate(sender vcl.IObject) { f.ImageList = loadImages(f) f.SetCaption(APPNAME) f.ScreenCenter() f.SetWidth(1280) f.SetHeight(640) f.ImageList.GetIcon(imgDatabaseLightning, f.Icon()) mnuFile := vcl.NewMenuItem(f) mnuFile.SetCaption("File") // mnuFileBadger := vcl.NewMenuItem(mnuFile) mnuFileBadger.SetCaption("Badger") mnuFileBadger.SetImageIndex(imgVendorDgraph) mnuFile.Add(mnuFileBadger) vcl_menuitem(mnuFileBadger, "Open database...", imgDatabaseAdd, f.OnMnuFileBadgerOpenClick) vcl_menuitem(mnuFileBadger, "New in-memory database", imgDatabaseAdd, f.OnMnuFileBadgerMemoryClick) // mnuFileBolt := vcl.NewMenuItem(mnuFile) mnuFileBolt.SetCaption("Bolt") mnuFileBolt.SetImageIndex(imgVendorGithub) mnuFile.Add(mnuFileBolt) vcl_menuitem(mnuFileBolt, "New database...", imgDatabaseAdd, f.OnMnuFileBoltNewClick) vcl_menuitem(mnuFileBolt, "Open database...", imgDatabaseAdd, f.OnMnuFileBoltOpenClick) vcl_menuitem(mnuFileBolt, "Open database (read-only)...", imgDatabaseAdd, f.OnMnuFileBoltOpenReadonlyClick) // mnuFileDebconf := vcl.NewMenuItem(mnuFile) mnuFileDebconf.SetCaption("Debconf") mnuFileDebconf.SetImageIndex(imgVendorDebian) mnuFile.Add(mnuFileDebconf) vcl_menuitem(mnuFileDebconf, "Open database...", imgDatabaseAdd, f.OnMnuFileDebianOpenClick) // mnuFilePebble := vcl.NewMenuItem(mnuFile) mnuFilePebble.SetCaption("Pebble") mnuFilePebble.SetImageIndex(imgVendorCockroach) mnuFile.Add(mnuFilePebble) vcl_menuitem(mnuFilePebble, "Open database...", imgDatabaseAdd, f.OnMnuFilePebbleOpenClick) vcl_menuitem(mnuFilePebble, "New in-memory database", imgDatabaseAdd, f.OnMnuFilePebbleMemoryClick) // mnuFileRedis := vcl.NewMenuItem(mnuFile) mnuFileRedis.SetCaption("Redis") mnuFileRedis.SetImageIndex(imgVendorRedis) mnuFile.Add(mnuFileRedis) vcl_menuitem(mnuFileRedis, "Connect...", imgDatabaseAdd, f.OnMnuFileRedisConnectClick) // mnuFileSqlite := vcl.NewMenuItem(mnuFile) mnuFileSqlite.SetCaption("SQLite") mnuFileSqlite.SetImageIndex(imgVendorSqlite) mnuFile.Add(mnuFileSqlite) vcl_menuitem(mnuFileSqlite, "Open database...", imgDatabaseAdd, f.OnMnuFileSqliteOpenClick) vcl_menuitem(mnuFileSqlite, "New in-memory database", imgDatabaseAdd, f.OnMnuFileSqliteMemoryClick) vcl_menuseparator(mnuFileSqlite) f.SQLiteUseCliDriver = vcl_menuitem(mnuFileSqlite, "Connect using CLI driver (experimental)", -1, nil) f.SQLiteUseCliDriver.SetAutoCheck(true) // vcl_menuseparator(mnuFile) mnuFileExit := vcl.NewMenuItem(mnuFile) mnuFileExit.SetCaption("Exit") mnuFileExit.SetOnClick(f.OnMnuFileExitClick) mnuFile.Add(mnuFileExit) mnuQuery := vcl.NewMenuItem(f) mnuQuery.SetCaption("Query") mnuQueryExecute := vcl.NewMenuItem(mnuQuery) mnuQueryExecute.SetCaption("Execute") mnuQueryExecute.SetShortCutFromString("F5") mnuQueryExecute.SetOnClick(f.OnQueryExecute) mnuQueryExecute.SetImageIndex(imgLightning) mnuQuery.Add(mnuQueryExecute) mnuHelp := vcl.NewMenuItem(f) mnuHelp.SetCaption("Help") mnuHelpVersion := vcl.NewMenuItem(mnuHelp) mnuHelpVersion.SetCaption("Driver versions...") mnuHelpVersion.SetOnClick(f.OnMenuHelpVersion) mnuHelp.Add(mnuHelpVersion) mnuHelpHomepage := vcl.NewMenuItem(mnuHelp) mnuHelpHomepage.SetCaption("About " + APPNAME) mnuHelpHomepage.SetShortCutFromString("F1") mnuHelpHomepage.SetOnClick(f.OnMnuHelpHomepage) mnuHelp.Add(mnuHelpHomepage) f.Menu = vcl.NewMainMenu(f) f.Menu.SetImages(f.ImageList) f.Menu.Items().Add(mnuFile) f.Menu.Items().Add(mnuQuery) f.Menu.Items().Add(mnuHelp) // f.StatusBar = vcl.NewStatusBar(f) f.StatusBar.SetParent(f) f.StatusBar.SetSimpleText("") // f.Buckets = vcl.NewTreeView(f) f.Buckets.SetParent(f) f.Buckets.SetImages(f.ImageList) f.Buckets.SetAlign(types.AlLeft) f.Buckets.SetWidth(MY_WIDTH) f.Buckets.SetReadOnly(true) // prevent click to rename on nodes f.Buckets.SetOnExpanding(f.OnNavExpanding) f.Buckets.SetOnChange(f.OnNavChange) f.Buckets.SetOnContextPopup(f.OnNavContextPopup) hsplit := vcl.NewSplitter(f) hsplit.SetParent(f) hsplit.SetAlign(types.AlLeft) hsplit.SetLeft(1) // Just needs to be further "over" than f.Buckets for auto-alignment f.Tabs = vcl.NewPageControl(f) f.Tabs.SetParent(f) f.Tabs.SetAlign(types.AlClient) // fill remaining space f.Tabs.SetImages(f.ImageList) propertiesTab := vcl.NewTabSheet(f.Tabs) propertiesTab.SetParent(f.Tabs) propertiesTab.SetCaption("Properties") propertiesTab.SetImageIndex(imgChartBar) f.propertiesBox = vcl.NewMemo(propertiesTab) f.propertiesBox.SetParent(propertiesTab) f.propertiesBox.BorderSpacing().SetAround(MY_SPACING) f.propertiesBox.SetAlign(types.AlClient) // fill remaining space f.propertiesBox.SetReadOnly(true) f.propertiesBox.SetEnabled(true) // Need to leave it enabled so scrolling works f.propertiesBox.SetColor(vcl_default_tab_background()) f.propertiesBox.SetBorderStyle(types.BsNone) f.propertiesBox.SetScrollBars(types.SsAutoVertical) dataTab := vcl.NewTabSheet(f.Tabs) dataTab.SetParent(f.Tabs) dataTab.SetCaption("Data") dataTab.SetImageIndex(imgTable) dataButtonBar := vcl.NewToolBar(dataTab) dataButtonBar.SetParent(dataTab) dataButtonBar.SetAlign(types.AlTop) dataButtonBar.BorderSpacing().SetLeft(MY_SPACING) dataButtonBar.BorderSpacing().SetTop(MY_SPACING) dataButtonBar.BorderSpacing().SetBottom(1) dataButtonBar.BorderSpacing().SetRight(MY_SPACING) dataButtonBar.SetEdgeBorders(0) dataButtonBar.SetImages(f.ImageList) dataButtonBar.SetShowCaptions(true) dataRefreshBtn := vcl.NewToolButton(dataButtonBar) dataRefreshBtn.SetParent(dataButtonBar) dataRefreshBtn.SetCaption("Refresh") // dataRefreshBtn.SetImageIndex(imgLightning) dataRefreshBtn.SetOnClick(func(sender vcl.IObject) { f.RefreshCurrentItem() }) dataInsertBtn := vcl.NewToolButton(dataButtonBar) dataInsertBtn.SetParent(dataButtonBar) dataInsertBtn.SetCaption("Insert") dataInsertBtn.SetOnClick(f.OnDataInsertClick) dataDelRowBtn := vcl.NewToolButton(dataButtonBar) dataDelRowBtn.SetParent(dataButtonBar) dataDelRowBtn.SetCaption("Delete Row") dataDelRowBtn.SetOnClick(f.OnDataDeleteRowClick) dataCommitBtn := vcl.NewToolButton(dataButtonBar) dataCommitBtn.SetParent(dataButtonBar) dataCommitBtn.SetCaption("Commit") dataCommitBtn.SetOnClick(f.OnDataCommitClick) f.contentBox = vcl.NewStringGrid(dataTab) f.contentBox.SetParent(dataTab) f.contentBox.BorderSpacing().SetLeft(MY_SPACING) f.contentBox.BorderSpacing().SetRight(MY_SPACING) f.contentBox.BorderSpacing().SetBottom(MY_SPACING) f.contentBox.SetAlign(types.AlClient) // fill remaining space f.contentBox.SetOptions(f.contentBox.Options().Include(types.GoThumbTracking, types.GoColSizing, types.GoDblClickAutoSize, types.GoEditing)) f.contentBox.SetOnPrepareCanvas(f.OnDataPrepareCanvas) f.contentBox.SetOnEditingDone(f.OnDataCellEdited) f.contentBox.SetOnGetEditText(f.OnDataCellEditStarting) f.contentBox.SetDefaultColWidth(MY_WIDTH) vcl_stringgrid_clear(f.contentBox) queryTab := vcl.NewTabSheet(f.Tabs) queryTab.SetParent(f.Tabs) queryTab.SetCaption("Query") queryTab.SetImageIndex(imgLightning) queryButtonBar := vcl.NewToolBar(queryTab) queryButtonBar.SetParent(queryTab) queryButtonBar.SetAlign(types.AlTop) queryButtonBar.BorderSpacing().SetLeft(MY_SPACING) queryButtonBar.BorderSpacing().SetTop(MY_SPACING) //queryButtonBar.BorderSpacing().SetBottom(1) queryButtonBar.BorderSpacing().SetRight(MY_SPACING) queryButtonBar.SetEdgeBorders(0) queryButtonBar.SetImages(f.ImageList) queryButtonBar.SetShowCaptions(true) queryExecBtn := vcl.NewToolButton(queryButtonBar) queryExecBtn.SetParent(queryButtonBar) queryExecBtn.SetCaption("Execute") // queryExecBtn.SetImageIndex(imgLightning) queryExecBtn.SetOnClick(f.OnQueryExecute) f.queryInput = vcl.NewMemo(queryTab) f.queryInput.SetParent(queryTab) f.queryInput.SetHeight(MY_HEIGHT) f.queryInput.SetAlign(types.AlTop) f.queryInput.SetTop(1) if runtime.GOOS == "windows" { f.queryInput.Font().SetName("Consolas") } else { f.queryInput.Font().SetName("monospace") } f.queryInput.BorderSpacing().SetLeft(MY_SPACING) //f.queryInput.BorderSpacing().SetTop(1) f.queryInput.BorderSpacing().SetRight(MY_SPACING) f.queryInput.SetBorderStyle(types.BsFrame) vsplit := vcl.NewSplitter(queryTab) vsplit.SetParent(queryTab) vsplit.SetAlign(types.AlTop) vsplit.SetTop(2) f.queryResult = vcl.NewStringGrid(queryTab) f.queryResult.SetParent(queryTab) f.queryResult.SetAlign(types.AlClient) // fill remaining space f.queryResult.BorderSpacing().SetLeft(MY_SPACING) f.queryResult.BorderSpacing().SetRight(MY_SPACING) f.queryResult.BorderSpacing().SetBottom(MY_SPACING) f.queryResult.SetOptions(f.queryResult.Options().Include(types.GoThumbTracking)) f.queryResult.SetDefaultColWidth(MY_WIDTH) vcl_stringgrid_clear(f.queryResult) f.none = &noLoadedDatabase{} f.OnNavChange(f, nil) // calls f.none.RenderForNav and sets up status bar content } func (f *TMainForm) OnMnuFileBoltNewClick(sender vcl.IObject) { dlg := vcl.NewSaveDialog(f) dlg.SetTitle("Save database as...") dlg.SetFilter("Bolt database|*.db|All files|*.*") ret := dlg.Execute() // Fake blocking if ret { f.boltAddDatabaseFromFile(dlg.FileName(), false) } } func (f *TMainForm) OnMnuFileBoltOpenClick(sender vcl.IObject) { dlg := vcl.NewOpenDialog(f) dlg.SetTitle("Select a database file...") dlg.SetFilter("Bolt database|*.db|All files|*.*") ret := dlg.Execute() // Fake blocking if ret { f.boltAddDatabaseFromFile(dlg.FileName(), false) } } func (f *TMainForm) OnMnuFileBoltOpenReadonlyClick(sender vcl.IObject) { dlg := vcl.NewOpenDialog(f) dlg.SetTitle("Select a database file...") dlg.SetFilter("Bolt database|*.db|All files|*.*") ret := dlg.Execute() // Fake blocking if ret { f.boltAddDatabaseFromFile(dlg.FileName(), true) } } func (f *TMainForm) OnMnuFileSqliteOpenClick(sender vcl.IObject) { cliDriver := f.SQLiteUseCliDriver.Checked() dlg := vcl.NewOpenDialog(f) dlg.SetTitle("Select a database file...") dlg.SetFilter("SQLite database|*.db;*.db3;*.sqlite;*.sqlite3|All files|*.*") ret := dlg.Execute() // Fake blocking if ret { f.sqliteAddDatabaseFromFile(dlg.FileName(), cliDriver) } } func (f *TMainForm) OnMnuFileBadgerOpenClick(sender vcl.IObject) { dlg := vcl.NewSelectDirectoryDialog(f) dlg.SetTitle("Select a database directory...") ret := dlg.Execute() // Fake blocking if ret { f.badgerAddDatabaseFromDirectory(dlg.FileName()) } } func (f *TMainForm) OnMnuFileBadgerMemoryClick(sender vcl.IObject) { f.badgerAddDatabaseFromMemory() } func (f *TMainForm) OnMnuFilePebbleOpenClick(sender vcl.IObject) { dlg := vcl.NewSelectDirectoryDialog(f) dlg.SetTitle("Select a database directory...") ret := dlg.Execute() // Fake blocking if ret { f.pebbleAddDatabaseFrom(dlg.FileName(), nil) } } func (f *TMainForm) OnMnuFileDebianOpenClick(sender vcl.IObject) { dlg := vcl.NewOpenDialog(f) dlg.SetTitle("Select a database file...") dlg.SetFilter("Debconf database|*.dat|All files|*.*") if runtime.GOOS == "linux" { dlg.SetInitialDir(`/var/cache/debconf/`) } ret := dlg.Execute() // Fake blocking if ret { f.debconfAddDatabaseFrom(dlg.FileName()) } } func (f *TMainForm) OnMnuFilePebbleMemoryClick(sender vcl.IObject) { f.pebbleAddDatabaseFromMemory() } func (f *TMainForm) OnMnuFileSqliteMemoryClick(sender vcl.IObject) { cliDriver := f.SQLiteUseCliDriver.Checked() f.sqliteAddDatabaseFromFile(`:memory:`, cliDriver) } func (f *TMainForm) OnMnuFileRedisConnectClick(sender vcl.IObject) { var child *TRedisConnectionDialog vcl.Application.CreateForm(&child) defer child.Free() var ret TRedisConnectionDialogResult child.CustomReturn = &ret mr := child.ShowModal() // Fake blocking if mr != types.MrOk || ret == nil { return // Cancelled } // Connect f.redisConnect(ret) } func (f *TMainForm) OnMnuFileExitClick(sender vcl.IObject) { f.Close() } func (f *TMainForm) OnMnuHelpHomepage(sender vcl.IObject) { err := browser.OpenURL(HOMEPAGE_URL) if err != nil { vcl.ShowMessage("Opening browser: " + err.Error()) } } func (f *TMainForm) OnMenuHelpVersion(sender vcl.IObject) { bi, ok := debug.ReadBuildInfo() if !ok { return } info := "This version of " + APPNAME + " was compiled with:\n\n" for _, dep := range bi.Deps { // Filter to only interesting things switch dep.Path { case `github.com/cockroachdb/pebble`, `github.com/dgraph-io/badger/v4`, `github.com/mattn/go-sqlite3`, `github.com/redis/go-redis/v9`, `go.etcd.io/bbolt`, `modernc.org/sqlite`: info += fmt.Sprintf("- %s %s\n", dep.Path, dep.Version) } } vcl.ShowMessage(info) } func (f *TMainForm) OnNavContextPopup(sender vcl.IObject, mousePos types.TPoint, handled *bool) { *handled = true curItem := f.Buckets.Selected() if curItem == nil { // Nothing is selected at all return } ndata := (*navData)(curItem.Data()) mnu := vcl.NewPopupMenu(f.Buckets) mnu.SetImages(f.ImageList) mnuRefresh := vcl.NewMenuItem(mnu) mnuRefresh.SetCaption("Refresh") mnuRefresh.SetImageIndex(imgArrowRefresh) mnuRefresh.SetOnClick(func(sender vcl.IObject) { f.OnNavContextRefresh(curItem, ndata) }) mnu.Items().Add(mnuRefresh) // Check what custom actions the ndata->db itself wants to add actions, err := ndata.ld.NavContext(ndata) if err != nil { vcl.ShowMessage(err.Error()) return } if len(actions) > 0 { mnuSep := vcl.NewMenuItem(mnu) mnuSep.SetCaption("-") mnu.Items().Add(mnuSep) for _, action := range actions { mnuAction := vcl.NewMenuItem(mnu) mnuAction.SetCaption(action.Name) cb := action.Callback // Copy to avoid reuse of loop variable mnuAction.SetOnClick(func(sender vcl.IObject) { cb(ndata) f.OnNavContextRefresh(curItem, ndata) }) mnu.Items().Add(mnuAction) } } if curItem.Parent() == nil { // Top-level item (database connection). Allow closing by right-click. mnuSep := vcl.NewMenuItem(mnu) mnuSep.SetCaption("-") mnu.Items().Add(mnuSep) mnuClose := vcl.NewMenuItem(mnu) mnuClose.SetCaption("Close") mnuClose.SetOnClick(f.OnNavContextClose) mnuClose.SetImageIndex(imgDatabaseDelete) mnu.Items().Add(mnuClose) } // Show popup mnu.Popup2() } func (f *TMainForm) RefreshCurrentItem() { item := f.Buckets.Selected() if item == nil { return // nothing to do } ndata := (*navData)(item.Data()) f.OnNavContextRefresh(item, ndata) // Refresh LHS pane/children f.OnNavChange(f.Buckets, item) // Refresh RHS pane/data content } func (f *TMainForm) OnNavContextRefresh(item *vcl.TTreeNode, ndata *navData) { isExpanded := item.Expanded() // Reset nav node to 'unloaded' state item.Collapse(true) item.DeleteChildren() item.SetHasChildren(true) ndata.childrenLoaded = false // Trigger a virtual reload item.Expand(false) // Calls OnNavExpanding to dynamically detect children // Restore previous gui state item.SetExpanded(isExpanded) } func (f *TMainForm) OnDataPrepareCanvas(sender vcl.IObject, aCol, aRow int32, aState types.TGridDrawState) { if _, ok := f.deleteRows[aRow]; ok { f.contentBox.Canvas().Brush().SetColor(CO_DELETE) return } if _, ok := f.insertRows[aRow]; ok { f.contentBox.Canvas().Brush().SetColor(CO_INSERT) return } if er, ok := f.updateRows[aRow]; ok { // This row is being edited if int32slice_contains(er, aCol) { f.contentBox.Canvas().Brush().SetColor(CO_EDIT_EXPLICIT) } else { f.contentBox.Canvas().Brush().SetColor(CO_EDIT_IMPLICIT) } return } } // func (f *TMainForm) OnDataCellEditStarting(sender vcl.IObject, aCol, aRow int32, editor **vcl.TWinControl) { func (f *TMainForm) OnDataCellEditStarting(sender vcl.IObject, aCol, aRow int32, value *string) { f.isEditing = true } func (f *TMainForm) OnDataCellEdited(sender vcl.IObject) { // The OnEditingDone event fires whenever the TStringGrid loses focus, even // if editing was not currently taking place // To detect real edits, set a flag in the OnSelectEditor event if !f.isEditing { return } f.isEditing = false aRow := f.contentBox.Row() aCol := f.contentBox.Col() // If this is an insert row, no need to patch updateRows if _, ok := f.insertRows[aRow]; ok { return // nothing to do } if chk, ok := f.updateRows[aRow]; ok { if int32slice_contains(chk, aCol) { // nothing to do } else { chk = append(chk, aCol) f.updateRows[aRow] = chk } } else { f.updateRows[aRow] = []int32{aCol} } // If this row was marked for deletion, this new event takes priority delete(f.deleteRows, aRow) // Signal repaint f.contentBox.InvalidateRow(aRow) } func (f *TMainForm) OnDataInsertClick(sender vcl.IObject) { if !f.contentBox.Enabled() { return // Not an active data view } rpos := f.contentBox.RowCount() f.contentBox.SetRowCount(rpos + 1) f.insertRows[rpos] = struct{}{} // Scroll to bottom f.contentBox.SetTopRow(rpos) } func (f *TMainForm) OnDataDeleteRowClick(sender vcl.IObject) { if !f.contentBox.Enabled() { return // Not an active data view } rpos := f.contentBox.Row() f.deleteRows[rpos] = struct{}{} // If this row was marked for edit, this takes priority delete(f.updateRows, rpos) // Repaint f.contentBox.InvalidateRow(rpos) } func (f *TMainForm) OnDataCommitClick(sender vcl.IObject) { if !f.contentBox.Enabled() { return // Not an active data view } // TODO } func (f *TMainForm) OnNavContextClose(sender vcl.IObject) { curItem := f.Buckets.Selected() if curItem == nil { return // Nothing selected (shouldn't happen) } if curItem.Parent() != nil { return // Selection is not top-level DB connection (shouldn't happen) } ndata := (*navData)(curItem.Data()) ndata.ld.Close() curItem.Delete() // n.b. This triggers OnNavChange, which will then re-render from noLoadedDatabase{} } func (f *TMainForm) OnQueryExecute(sender vcl.IObject) { // If query tab is not selected, switch to it, but do not exec if f.Tabs.ActivePageIndex() != 2 { f.Tabs.SetActivePageIndex(2) return } queryString := f.queryInput.Text() if f.queryInput.SelLength() > 0 { queryString = f.queryInput.SelText() // Just the selected text } // Execute node := f.Buckets.Selected() if node == nil { vcl.ShowMessage("No database selected") return } ndata := (*navData)(node.Data()) ndata.ld.ExecQuery(queryString, f.queryResult) } func (f *TMainForm) OnNavChange(sender vcl.IObject, node *vcl.TTreeNode) { var ld loadedDatabase = f.none var ndata *navData = nil if node != nil && node.Data() != nil { ndata = (*navData)(node.Data()) ld = ndata.ld } // Reset some controls that the render function is expected to populate f.insertRows = make(map[int32]struct{}) f.updateRows = make(map[int32][]int32) f.deleteRows = make(map[int32]struct{}) f.isEditing = false f.propertiesBox.Clear() vcl_stringgrid_clear(f.contentBox) ld.RenderForNav(f, ndata) // Handover to the database type's own renderer function // We're in charge of common status bar text updates f.StatusBar.SetSimpleText(ld.DisplayName() + " | " + ld.DriverName()) } func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allowExpansion *bool) { if node.Data() == nil { vcl.ShowMessage("unexpected nil data") *allowExpansion = false return } ndata := (*navData)(node.Data()) err := f.NavLoadChildren(node, ndata) if err != nil { if errors.Is(err, ErrNavNotExist) { // The nav entry has been deleted. // This is a normal thing to happen when e.g. deleting a table // f.StatusBar.SetSimpleText(err.Error()) // Just gets overridden when the selection changes node.Delete() return } vcl.ShowMessage(err.Error()) *allowExpansion = false // Permanently block return } *allowExpansion = node.HasChildren() // While we're here - preload one single level deep (not any deeper) if node.HasChildren() { // This node has children that we haven't processed. Process them now cc := node.GetFirstChild() for { ndataChild := (*navData)(cc.Data()) if ndataChild.childrenLoaded { break // We always do them together, so if one's done, no need to keep looking } f.NavLoadChildren(cc, ndataChild) cc = cc.GetNextSibling() if cc == nil { break } } } } func (f *TMainForm) NavLoadChildren(node *vcl.TTreeNode, ndata *navData) error { if ndata.childrenLoaded { return nil // Nothing to do } // Find the child buckets from this point under the element nextBucketNames, err := ndata.ld.NavChildren(ndata) if err != nil { return fmt.Errorf("Failed to find child buckets under %q: %w", strings.Join(ndata.bucketPath, `/`), err) } ndata.childrenLoaded = true // don't repeat this work if len(nextBucketNames) == 0 { node.SetHasChildren(false) } else { node.SetHasChildren(true) // Populate LCL child nodes for _, bucketName := range nextBucketNames { node := f.Buckets.Items().AddChild(node, formatUtf8([]byte(bucketName))) node.SetHasChildren(true) // dynamically populate in OnNavExpanding node.SetImageIndex(imgTable) node.SetSelectedIndex(imgTable) navData := &navData{ ld: ndata.ld, childrenLoaded: false, // will be loaded dynamically bucketPath: []string{}, // empty = root } navData.bucketPath = append(navData.bucketPath, ndata.bucketPath...) navData.bucketPath = append(navData.bucketPath, bucketName) node.SetData(unsafe.Pointer(navData)) ndata.ld.Keepalive(navData) } } return nil }