644 lines
17 KiB
Go
644 lines
17 KiB
Go
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"
|
|
)
|
|
|
|
const (
|
|
APPNAME = "yvbolt"
|
|
HOMEPAGE_URL = "https://code.ivysaur.me/yvbolt"
|
|
)
|
|
|
|
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.TListView
|
|
queryInput *vcl.TMemo
|
|
queryResult *vcl.TListView
|
|
|
|
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.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)
|
|
|
|
f.contentBox = vcl.NewListView(dataTab)
|
|
f.contentBox.SetParent(dataTab)
|
|
f.contentBox.BorderSpacing().SetAround(MY_SPACING)
|
|
f.contentBox.SetAlign(types.AlClient) // fill remaining space
|
|
f.contentBox.SetViewStyle(types.VsReport) // "Report style" i.e. has columns
|
|
f.contentBox.SetAutoWidthLastColumn(true)
|
|
f.contentBox.SetReadOnly(true)
|
|
f.contentBox.Columns().Clear()
|
|
|
|
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.NewListView(queryTab)
|
|
f.queryResult.SetParent(queryTab)
|
|
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().SetRight(MY_SPACING)
|
|
f.queryResult.BorderSpacing().SetBottom(MY_SPACING)
|
|
|
|
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"
|
|
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) 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) 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
|
|
}
|
|
|
|
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
|
|
}
|