yvbolt/main.go

925 lines
25 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"
"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
dataInsertBtn *vcl.TToolButton
dataDelRowBtn *vcl.TToolButton
dataCommitBtn *vcl.TToolButton
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
queryExecBtn *vcl.TToolButton
queryInput *vcl.TRichEdit
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(imgResultsetNext)
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)
dataRefreshBtn := vcl.NewToolButton(dataButtonBar)
dataRefreshBtn.SetParent(dataButtonBar)
dataRefreshBtn.SetHint("Refresh")
dataRefreshBtn.SetShowHint(true)
dataRefreshBtn.SetImageIndex(imgArrowRefresh)
dataRefreshBtn.SetOnClick(func(sender vcl.IObject) { f.RefreshCurrentItem() })
f.dataInsertBtn = vcl.NewToolButton(dataButtonBar)
f.dataInsertBtn.SetParent(dataButtonBar)
f.dataInsertBtn.SetImageIndex(imgAdd)
f.dataInsertBtn.SetHint("Insert")
f.dataInsertBtn.SetShowHint(true)
f.dataInsertBtn.SetOnClick(f.OnDataInsertClick)
f.dataDelRowBtn = vcl.NewToolButton(dataButtonBar)
f.dataDelRowBtn.SetParent(dataButtonBar)
f.dataDelRowBtn.SetImageIndex(imgDelete)
f.dataDelRowBtn.SetHint("Delete Row")
f.dataDelRowBtn.SetShowHint(true)
f.dataDelRowBtn.SetOnClick(f.OnDataDeleteRowClick)
f.dataCommitBtn = vcl.NewToolButton(dataButtonBar)
f.dataCommitBtn.SetParent(dataButtonBar)
f.dataCommitBtn.SetImageIndex(imgPencilGo)
f.dataCommitBtn.SetHint("Commit")
f.dataCommitBtn.SetShowHint(true)
f.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)
f.queryExecBtn = vcl.NewToolButton(queryButtonBar)
f.queryExecBtn.SetParent(queryButtonBar)
f.queryExecBtn.SetHint("Execute")
f.queryExecBtn.SetShowHint(true)
f.queryExecBtn.SetImageIndex(imgResultsetNext)
f.queryExecBtn.SetOnClick(f.OnQueryExecute)
f.queryInput = vcl.NewRichEdit(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.SetCursor(types.CrIBeam) // Use text cursor instead of default pointer cursor
f.queryInput.BorderSpacing().SetLeft(MY_SPACING)
//f.queryInput.BorderSpacing().SetTop(1)
f.queryInput.BorderSpacing().SetRight(MY_SPACING)
f.queryInput.SetBorderStyle(types.BsFrame)
// f.queryInput.SetOnKeyUp(f.OnQueryTextChanged) // Performs extremely slowly
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(boltFilter)
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(boltFilter)
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(boltFilter)
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(sqliteFilter)
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) {
err = cb(f, ndata)
if err != nil {
vcl.ShowMessage(err.Error())
}
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
}
node := f.Buckets.Selected()
if node == nil {
vcl.ShowMessage("No database selected")
return
}
scrollPos := f.contentBox.TopRow()
ndata := (*navData)(node.Data())
editableLd, ok := ndata.ld.(editableLoadedDatabase)
if !ok {
vcl.ShowMessage("Unsupported action for this database")
return
}
err := editableLd.ApplyChanges(f, ndata)
if err != nil {
vcl.ShowMessage(err.Error())
}
// Refresh content
f.OnNavChange(f.Buckets, node) // Refresh RHS pane/data content
// Preserve scroll position
f.contentBox.SetTopRow(scrollPos)
}
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) OnQueryTextChanged(sender vcl.IObject) {
func (f *TMainForm) OnQueryTextChanged(sender vcl.IObject, key *types.Char, shift types.TShiftState) {
// FIXME changing the text colour calls the onchange handler recursively
// FIXME changing the text colour pushes into the undo stack
// Preserve
f.queryInput.Lines().BeginUpdate()
origPos := f.queryInput.SelStart()
origLen := f.queryInput.SelLength()
defer func() {
f.queryInput.SetSelStart(origPos)
f.queryInput.SetSelLength(origLen)
f.queryInput.Lines().EndUpdate()
}()
tx := strings.ToLower(f.queryInput.Text())
// Reset all existing colors
f.queryInput.SetSelStart(0)
f.queryInput.SetSelLength(int32(len(tx)))
f.queryInput.SelAttributes().SetColor(colors.ClBlack)
searchPos := 0
for {
matchPos := strings.Index(tx[searchPos:], "select")
if matchPos == -1 {
break
}
matchPos += searchPos // compensate for slicing
f.queryInput.SetSelStart(int32(matchPos))
f.queryInput.SetSelLength(6)
f.queryInput.SelAttributes().SetColor(colors.ClRed)
searchPos = matchPos + 6 // strlen(SELECT)
}
}
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
}
if strings.TrimSpace(queryString) == "" {
return // prevent blank query
}
// Execute
node := f.Buckets.Selected()
if node == nil {
vcl.ShowMessage("No database selected")
return
}
ndata := (*navData)(node.Data())
queryableLd, ok := ndata.ld.(queryableLoadedDatabase)
if !ok {
vcl.ShowMessage("Unsupported action for this database")
return
}
err := queryableLd.ExecQuery(queryString, f.queryResult)
if err != nil {
vcl.ShowMessage(err.Error())
return
}
}
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)
err := ld.RenderForNav(f, ndata) // Handover to the database type's own renderer function
if err != nil {
vcl.ShowMessage(err.Error())
// Ensure elements are disabled
f.contentBox.SetEnabled(false)
}
// Toggle the Edit functionality
_, ok := ld.(editableLoadedDatabase)
f.dataCommitBtn.SetEnabled(ok)
f.dataDelRowBtn.SetEnabled(ok)
f.dataInsertBtn.SetEnabled(ok)
// Toggle the Query functionality
_, ok = ld.(queryableLoadedDatabase)
f.queryInput.SetEnabled(ok)
f.queryResult.SetEnabled(ok)
f.queryExecBtn.SetEnabled(ok)
// 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
}