Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6145320858 | |||
| 8296a2fec9 | |||
| 223d13be58 | |||
| eca27dcd4f | |||
| 0f2a3e021a | |||
| 90259fb2b9 | |||
| 7573cf0453 |
24
README.md
24
README.md
@@ -1,13 +1,11 @@
|
|||||||
# yvbolt
|
# yvbolt
|
||||||
|
|
||||||
A graphical browser for multiple databases using [GoVCL](https://z-kit.cc/en/).
|
A graphical interface for multiple databases using [GoVCL](https://z-kit.cc/en/).
|
||||||
|
|
||||||
This is an experimental application and you should generally prefer to use [qbolt](https://code.ivysaur.me/qbolt).
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Native desktop application, running on Linux, Windows, and macOS
|
- Native desktop application, running on Linux, Windows, and macOS
|
||||||
- Connect to multiple databases
|
- Connect to multiple databases at once
|
||||||
- Browse table/bucket content
|
- Browse table/bucket content
|
||||||
- Use context menu to perform special table/bucket actions
|
- Use context menu to perform special table/bucket actions
|
||||||
- Run custom SQL queries
|
- Run custom SQL queries
|
||||||
@@ -17,12 +15,16 @@ This is an experimental application and you should generally prefer to use [qbol
|
|||||||
- Badger v4
|
- Badger v4
|
||||||
- Bolt
|
- Bolt
|
||||||
- Recursive bucket support
|
- Recursive bucket support
|
||||||
- Option to open as readonly
|
- Option to open as readonly for shared access
|
||||||
|
- Supports editing
|
||||||
|
- See also [qbolt](https://code.ivysaur.me/qbolt) for more/different functionality
|
||||||
- Debconf
|
- Debconf
|
||||||
- Pebble
|
- Pebble
|
||||||
- Redis
|
- Redis
|
||||||
- SQLite
|
- SQLite
|
||||||
- Drivers: mattn (CGo), modernc.org (no cgo), experimental command-line driver
|
- Drivers: mattn (CGo), modernc.org (no cgo), experimental command-line driver
|
||||||
|
- Supports editing
|
||||||
|
- Integrated vacuum and export commands
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@@ -32,7 +34,7 @@ This project redistributes images from the famfamfam/silk icon set under the [CC
|
|||||||
|
|
||||||
This project includes trademarked logo images for each supported database type.
|
This project includes trademarked logo images for each supported database type.
|
||||||
|
|
||||||
## Usage
|
## Compiling
|
||||||
|
|
||||||
1. `CGO_ENABLED=1 go build`
|
1. `CGO_ENABLED=1 go build`
|
||||||
2. [Download liblcl](https://github.com/ying32/govcl/releases/download/v2.2.3/liblcl-2.2.3.zip) for your platform, or [compile it yourself](https://github.com/ying32/liblcl) (tested with v2.2.3)
|
2. [Download liblcl](https://github.com/ying32/govcl/releases/download/v2.2.3/liblcl-2.2.3.zip) for your platform, or [compile it yourself](https://github.com/ying32/liblcl) (tested with v2.2.3)
|
||||||
@@ -41,6 +43,16 @@ This project includes trademarked logo images for each supported database type.
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
2024-07-18 v0.7.0
|
||||||
|
|
||||||
|
- SQLite, Bolt: Initial support for editing data (insert, per-cell update, delete)
|
||||||
|
- SQLite: Add context menu actions for compact (vacuum), export, and drop table
|
||||||
|
- App: New grid widget
|
||||||
|
- App: Add refresh button
|
||||||
|
- App: Bigger window size, use icons for toolbars, better UI colours for Windows
|
||||||
|
- App: Prevent submitting blank queries to database
|
||||||
|
- Refactor database interface and error handling
|
||||||
|
|
||||||
2024-06-30 v0.6.0
|
2024-06-30 v0.6.0
|
||||||
|
|
||||||
- Debconf: Add as supported database
|
- Debconf: Add as supported database
|
||||||
|
|||||||
5
TODO
5
TODO
@@ -1,4 +1,3 @@
|
|||||||
- Cast DB to wider interface to check feature support
|
|
||||||
- Syntax highlighting in editor
|
- Syntax highlighting in editor
|
||||||
- Mutation
|
- Mutation
|
||||||
- Get real primary key for mutation instead of string approximation
|
- Get real primary key for mutation instead of string approximation
|
||||||
@@ -6,7 +5,6 @@
|
|||||||
- Pebble: Support insert/update/delete
|
- Pebble: Support insert/update/delete
|
||||||
- Debconf: Support insert/update/delete
|
- Debconf: Support insert/update/delete
|
||||||
- Redis: Support insert/update/delete
|
- Redis: Support insert/update/delete
|
||||||
- SQLite: Support insert/update/delete
|
|
||||||
- Binary data viewer
|
- Binary data viewer
|
||||||
- Detect jpg/png and show as image
|
- Detect jpg/png and show as image
|
||||||
- More DB types
|
- More DB types
|
||||||
@@ -38,3 +36,6 @@
|
|||||||
- Context/interrupt slow queries
|
- Context/interrupt slow queries
|
||||||
- Faster virtual rendering
|
- Faster virtual rendering
|
||||||
- https://github.com/ying32/govcl/blob/master/samples/listviewvirtualdata/main.go
|
- https://github.com/ying32/govcl/blob/master/samples/listviewvirtualdata/main.go
|
||||||
|
- Query history
|
||||||
|
- Test suite
|
||||||
|
- `CREATE TABLE foo (id integer primary key, aaa text not null, bbb text not null);`
|
||||||
|
|||||||
@@ -83,10 +83,6 @@ func (ld *badgerLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *badgerLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error {
|
|
||||||
return ErrNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ld *badgerLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
func (ld *badgerLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
||||||
// In the Badger implementation, there is only one child: "Data"
|
// In the Badger implementation, there is only one child: "Data"
|
||||||
if len(ndata.bucketPath) == 0 {
|
if len(ndata.bucketPath) == 0 {
|
||||||
@@ -102,10 +98,6 @@ 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.TStringGrid) error {
|
|
||||||
return ErrNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ld *badgerLoadedDatabase) Close() {
|
func (ld *badgerLoadedDatabase) Close() {
|
||||||
_ = ld.db.Close()
|
_ = ld.db.Close()
|
||||||
ld.arena = nil
|
ld.arena = nil
|
||||||
|
|||||||
@@ -192,10 +192,6 @@ func (ld *boltLoadedDatabase) DeleteBucket(sender vcl.IComponent, ndata *navData
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *boltLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) error {
|
|
||||||
return ErrNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ld *boltLoadedDatabase) Close() {
|
func (ld *boltLoadedDatabase) Close() {
|
||||||
_ = ld.db.Close()
|
_ = ld.db.Close()
|
||||||
ld.arena = nil
|
ld.arena = nil
|
||||||
|
|||||||
@@ -70,10 +70,6 @@ func (ld *debconfLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *debconfLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error {
|
|
||||||
return ErrNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ld *debconfLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
func (ld *debconfLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
||||||
// In the debconf implementation, there is only one child: "Data"
|
// In the debconf implementation, there is only one child: "Data"
|
||||||
if len(ndata.bucketPath) == 0 {
|
if len(ndata.bucketPath) == 0 {
|
||||||
@@ -89,10 +85,6 @@ 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.TStringGrid) error {
|
|
||||||
return ErrNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ld *debconfLoadedDatabase) Close() {
|
func (ld *debconfLoadedDatabase) Close() {
|
||||||
ld.arena = nil
|
ld.arena = nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,14 +23,6 @@ func (n *noLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error {
|
|
||||||
return ErrNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *noLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) error {
|
|
||||||
return ErrNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *noLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
func (n *noLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
||||||
return []string{}, nil
|
return []string{}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,10 +74,6 @@ func (ld *pebbleLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *pebbleLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error {
|
|
||||||
return ErrNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ld *pebbleLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
func (ld *pebbleLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
||||||
// In the pebble implementation, there is only one child: "Data"
|
// In the pebble implementation, there is only one child: "Data"
|
||||||
if len(ndata.bucketPath) == 0 {
|
if len(ndata.bucketPath) == 0 {
|
||||||
@@ -93,10 +89,6 @@ 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.TStringGrid) error {
|
|
||||||
return ErrNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ld *pebbleLoadedDatabase) Close() {
|
func (ld *pebbleLoadedDatabase) Close() {
|
||||||
_ = ld.db.Close()
|
_ = ld.db.Close()
|
||||||
ld.arena = nil
|
ld.arena = nil
|
||||||
|
|||||||
@@ -132,10 +132,6 @@ func (ld *redisLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *redisLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error {
|
|
||||||
return ErrNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ld *redisLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
func (ld *redisLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
||||||
// ctx := context.Background()
|
// ctx := context.Background()
|
||||||
|
|
||||||
|
|||||||
127
db_sqlite.go
127
db_sqlite.go
@@ -1,10 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
_ "yvbolt/sqliteclidriver"
|
_ "yvbolt/sqliteclidriver"
|
||||||
@@ -100,10 +102,6 @@ func (ld *sqliteLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *sqliteLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error {
|
|
||||||
return ErrNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ld *sqliteLoadedDatabase) sqliteGetColumnNamesForTable(tableName string) ([]string, error) {
|
func (ld *sqliteLoadedDatabase) sqliteGetColumnNamesForTable(tableName string) ([]string, error) {
|
||||||
colr, err := ld.db.Query(`SELECT name FROM pragma_table_info( ? )`, tableName)
|
colr, err := ld.db.Query(`SELECT name FROM pragma_table_info( ? )`, tableName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -191,6 +189,127 @@ func (ld *sqliteLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringG
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *sqliteLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) (retErr error) {
|
||||||
|
|
||||||
|
if len(ndata.bucketPath) != 2 {
|
||||||
|
return errors.New("invalid selection")
|
||||||
|
}
|
||||||
|
tableName := ndata.bucketPath[1]
|
||||||
|
|
||||||
|
// We have rendered row IDs, need to convert back to an SQLite primary key
|
||||||
|
// TODO stash the real key inside f.contentBox.Objects()
|
||||||
|
// FIXME breaks if you try and edit the primary key(!)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
tx, err := n.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var commitOK bool = false
|
||||||
|
defer func() {
|
||||||
|
if !commitOK {
|
||||||
|
err := tx.Rollback()
|
||||||
|
if err != nil {
|
||||||
|
retErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Data grid properties
|
||||||
|
var columnNames []string
|
||||||
|
for i := int32(0); i < f.contentBox.ColCount(); i++ {
|
||||||
|
columnNames = append(columnNames, f.contentBox.Columns().Items(i).Title().Caption())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query sqlite table metadata to determine which of these is the PRIMARY KEY
|
||||||
|
var primaryColumnName string
|
||||||
|
err = tx.QueryRowContext(ctx, `SELECT l.name FROM pragma_table_info(?) as l WHERE l.pk = 1;`, tableName).Scan(&primaryColumnName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Finding primary key for update: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert it to an index
|
||||||
|
var primaryColumnIdx int = -1
|
||||||
|
for i := 0; i < len(columnNames); i++ {
|
||||||
|
if columnNames[i] == primaryColumnName {
|
||||||
|
primaryColumnIdx = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if primaryColumnIdx == -1 {
|
||||||
|
return fmt.Errorf("Primary key %q missing from available columns", primaryColumnName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLite can only LIMIT 1 on update/delete if it was compiled with
|
||||||
|
// SQLITE_ENABLE_UPDATE_DELETE_LIMIT, which isn't the case for the mattn
|
||||||
|
// cgo library
|
||||||
|
// Skip that, and just rely on primary key uniqueness
|
||||||
|
|
||||||
|
// Edit
|
||||||
|
for rowid, editcells := range f.updateRows {
|
||||||
|
stmt := `UPDATE "` + tableName + `" SET `
|
||||||
|
params := []interface{}{} // FIXME reinstate types for the driver (although SQLite doesn't mind)
|
||||||
|
|
||||||
|
for ct, cell := range editcells {
|
||||||
|
if ct > 0 {
|
||||||
|
stmt += `, `
|
||||||
|
}
|
||||||
|
stmt += `"` + columnNames[cell] + `" = ?`
|
||||||
|
params = append(params, f.contentBox.Cells(cell, rowid))
|
||||||
|
}
|
||||||
|
stmt += ` WHERE "` + primaryColumnName + `" = ?`
|
||||||
|
pkVal := f.contentBox.Cells(int32(primaryColumnIdx), rowid)
|
||||||
|
params = append(params, pkVal)
|
||||||
|
|
||||||
|
_, err = tx.ExecContext(ctx, stmt, params...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Updating row %q: %w", pkVal, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete by key (affects rowids after re-render)
|
||||||
|
for rowid, _ := range f.deleteRows {
|
||||||
|
pkVal := f.contentBox.Cells(int32(primaryColumnIdx), rowid)
|
||||||
|
|
||||||
|
stmt := `DELETE FROM "` + tableName + `" WHERE "` + primaryColumnName + `" = ?`
|
||||||
|
|
||||||
|
_, err = tx.ExecContext(ctx, stmt, pkVal)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Deleting row %q: %w", pkVal, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert all new entries
|
||||||
|
for rowid, _ := range f.insertRows {
|
||||||
|
stmt := `INSERT INTO "` + tableName + `" (` + strings.Join(columnNames, `, `) + `) VALUES (`
|
||||||
|
params := []interface{}{} // FIXME reinstate types for the driver (although SQLite doesn't mind)
|
||||||
|
|
||||||
|
for colid := 0; colid < len(columnNames); colid++ {
|
||||||
|
if colid > 0 {
|
||||||
|
stmt += `, `
|
||||||
|
}
|
||||||
|
stmt += "?"
|
||||||
|
params = append(params, f.contentBox.Cells(int32(colid), rowid))
|
||||||
|
}
|
||||||
|
stmt += `)`
|
||||||
|
|
||||||
|
_, err = tx.ExecContext(ctx, stmt, params...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Inserting row: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
commitOK = true // no need for rollback
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ld *sqliteLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
func (ld *sqliteLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
||||||
|
|
||||||
if len(ndata.bucketPath) == 0 {
|
if len(ndata.bucketPath) == 0 {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var ErrNavNotExist error = errors.New("The selected item no longer exists")
|
var ErrNavNotExist error = errors.New("The selected item no longer exists")
|
||||||
var ErrNotSupported error = errors.New("Unsupported action for this database type")
|
|
||||||
|
|
||||||
type contextAction struct {
|
type contextAction struct {
|
||||||
Name string
|
Name string
|
||||||
@@ -20,14 +19,20 @@ type loadedDatabase interface {
|
|||||||
DriverName() string
|
DriverName() string
|
||||||
RootElement() *vcl.TTreeNode
|
RootElement() *vcl.TTreeNode
|
||||||
RenderForNav(f *TMainForm, ndata *navData) error
|
RenderForNav(f *TMainForm, ndata *navData) error
|
||||||
ApplyChanges(f *TMainForm, ndata *navData) error
|
|
||||||
ExecQuery(query string, resultArea *vcl.TStringGrid) error
|
|
||||||
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)
|
||||||
Close()
|
Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type queryableLoadedDatabase interface {
|
||||||
|
ExecQuery(query string, resultArea *vcl.TStringGrid) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type editableLoadedDatabase interface {
|
||||||
|
ApplyChanges(f *TMainForm, ndata *navData) error
|
||||||
|
}
|
||||||
|
|
||||||
// navData is the .Data() pointer for each TTreeNode in the left-hand tree.
|
// navData is the .Data() pointer for each TTreeNode in the left-hand tree.
|
||||||
type navData struct {
|
type navData struct {
|
||||||
ld loadedDatabase
|
ld loadedDatabase
|
||||||
|
|||||||
86
main.go
86
main.go
@@ -37,10 +37,14 @@ type TMainForm struct {
|
|||||||
Tabs *vcl.TPageControl
|
Tabs *vcl.TPageControl
|
||||||
propertiesBox *vcl.TMemo
|
propertiesBox *vcl.TMemo
|
||||||
contentBox *vcl.TStringGrid
|
contentBox *vcl.TStringGrid
|
||||||
|
dataInsertBtn *vcl.TToolButton
|
||||||
|
dataDelRowBtn *vcl.TToolButton
|
||||||
|
dataCommitBtn *vcl.TToolButton
|
||||||
isEditing bool
|
isEditing bool
|
||||||
insertRows map[int32]struct{} // Rows in the StringGrid that are to-be-inserted
|
insertRows map[int32]struct{} // Rows in the StringGrid that are to-be-inserted
|
||||||
deleteRows map[int32]struct{}
|
deleteRows map[int32]struct{}
|
||||||
updateRows map[int32][]int32 // Row->cells that are to-be-updated
|
updateRows map[int32][]int32 // Row->cells that are to-be-updated
|
||||||
|
queryExecBtn *vcl.TToolButton
|
||||||
queryInput *vcl.TRichEdit
|
queryInput *vcl.TRichEdit
|
||||||
queryResult *vcl.TStringGrid
|
queryResult *vcl.TStringGrid
|
||||||
|
|
||||||
@@ -236,26 +240,26 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
|
|||||||
dataRefreshBtn.SetImageIndex(imgArrowRefresh)
|
dataRefreshBtn.SetImageIndex(imgArrowRefresh)
|
||||||
dataRefreshBtn.SetOnClick(func(sender vcl.IObject) { f.RefreshCurrentItem() })
|
dataRefreshBtn.SetOnClick(func(sender vcl.IObject) { f.RefreshCurrentItem() })
|
||||||
|
|
||||||
dataInsertBtn := vcl.NewToolButton(dataButtonBar)
|
f.dataInsertBtn = vcl.NewToolButton(dataButtonBar)
|
||||||
dataInsertBtn.SetParent(dataButtonBar)
|
f.dataInsertBtn.SetParent(dataButtonBar)
|
||||||
dataInsertBtn.SetImageIndex(imgAdd)
|
f.dataInsertBtn.SetImageIndex(imgAdd)
|
||||||
dataInsertBtn.SetHint("Insert")
|
f.dataInsertBtn.SetHint("Insert")
|
||||||
dataInsertBtn.SetShowHint(true)
|
f.dataInsertBtn.SetShowHint(true)
|
||||||
dataInsertBtn.SetOnClick(f.OnDataInsertClick)
|
f.dataInsertBtn.SetOnClick(f.OnDataInsertClick)
|
||||||
|
|
||||||
dataDelRowBtn := vcl.NewToolButton(dataButtonBar)
|
f.dataDelRowBtn = vcl.NewToolButton(dataButtonBar)
|
||||||
dataDelRowBtn.SetParent(dataButtonBar)
|
f.dataDelRowBtn.SetParent(dataButtonBar)
|
||||||
dataDelRowBtn.SetImageIndex(imgDelete)
|
f.dataDelRowBtn.SetImageIndex(imgDelete)
|
||||||
dataDelRowBtn.SetHint("Delete Row")
|
f.dataDelRowBtn.SetHint("Delete Row")
|
||||||
dataDelRowBtn.SetShowHint(true)
|
f.dataDelRowBtn.SetShowHint(true)
|
||||||
dataDelRowBtn.SetOnClick(f.OnDataDeleteRowClick)
|
f.dataDelRowBtn.SetOnClick(f.OnDataDeleteRowClick)
|
||||||
|
|
||||||
dataCommitBtn := vcl.NewToolButton(dataButtonBar)
|
f.dataCommitBtn = vcl.NewToolButton(dataButtonBar)
|
||||||
dataCommitBtn.SetParent(dataButtonBar)
|
f.dataCommitBtn.SetParent(dataButtonBar)
|
||||||
dataCommitBtn.SetImageIndex(imgPencilGo)
|
f.dataCommitBtn.SetImageIndex(imgPencilGo)
|
||||||
dataCommitBtn.SetHint("Commit")
|
f.dataCommitBtn.SetHint("Commit")
|
||||||
dataCommitBtn.SetShowHint(true)
|
f.dataCommitBtn.SetShowHint(true)
|
||||||
dataCommitBtn.SetOnClick(f.OnDataCommitClick)
|
f.dataCommitBtn.SetOnClick(f.OnDataCommitClick)
|
||||||
|
|
||||||
f.contentBox = vcl.NewStringGrid(dataTab)
|
f.contentBox = vcl.NewStringGrid(dataTab)
|
||||||
f.contentBox.SetParent(dataTab)
|
f.contentBox.SetParent(dataTab)
|
||||||
@@ -286,12 +290,12 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
|
|||||||
queryButtonBar.SetImages(f.ImageList)
|
queryButtonBar.SetImages(f.ImageList)
|
||||||
queryButtonBar.SetShowCaptions(true)
|
queryButtonBar.SetShowCaptions(true)
|
||||||
|
|
||||||
queryExecBtn := vcl.NewToolButton(queryButtonBar)
|
f.queryExecBtn = vcl.NewToolButton(queryButtonBar)
|
||||||
queryExecBtn.SetParent(queryButtonBar)
|
f.queryExecBtn.SetParent(queryButtonBar)
|
||||||
queryExecBtn.SetHint("Execute")
|
f.queryExecBtn.SetHint("Execute")
|
||||||
queryExecBtn.SetShowHint(true)
|
f.queryExecBtn.SetShowHint(true)
|
||||||
queryExecBtn.SetImageIndex(imgResultsetNext)
|
f.queryExecBtn.SetImageIndex(imgResultsetNext)
|
||||||
queryExecBtn.SetOnClick(f.OnQueryExecute)
|
f.queryExecBtn.SetOnClick(f.OnQueryExecute)
|
||||||
|
|
||||||
f.queryInput = vcl.NewRichEdit(queryTab)
|
f.queryInput = vcl.NewRichEdit(queryTab)
|
||||||
f.queryInput.SetParent(queryTab)
|
f.queryInput.SetParent(queryTab)
|
||||||
@@ -668,7 +672,14 @@ func (f *TMainForm) OnDataCommitClick(sender vcl.IObject) {
|
|||||||
scrollPos := f.contentBox.TopRow()
|
scrollPos := f.contentBox.TopRow()
|
||||||
|
|
||||||
ndata := (*navData)(node.Data())
|
ndata := (*navData)(node.Data())
|
||||||
err := ndata.ld.ApplyChanges(f, ndata)
|
|
||||||
|
editableLd, ok := ndata.ld.(editableLoadedDatabase)
|
||||||
|
if !ok {
|
||||||
|
vcl.ShowMessage("Unsupported action for this database")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := editableLd.ApplyChanges(f, ndata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vcl.ShowMessage(err.Error())
|
vcl.ShowMessage(err.Error())
|
||||||
}
|
}
|
||||||
@@ -749,6 +760,10 @@ func (f *TMainForm) OnQueryExecute(sender vcl.IObject) {
|
|||||||
queryString = f.queryInput.SelText() // Just the selected text
|
queryString = f.queryInput.SelText() // Just the selected text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(queryString) == "" {
|
||||||
|
return // prevent blank query
|
||||||
|
}
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
node := f.Buckets.Selected()
|
node := f.Buckets.Selected()
|
||||||
if node == nil {
|
if node == nil {
|
||||||
@@ -757,7 +772,14 @@ func (f *TMainForm) OnQueryExecute(sender vcl.IObject) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ndata := (*navData)(node.Data())
|
ndata := (*navData)(node.Data())
|
||||||
err := ndata.ld.ExecQuery(queryString, f.queryResult)
|
|
||||||
|
queryableLd, ok := ndata.ld.(queryableLoadedDatabase)
|
||||||
|
if !ok {
|
||||||
|
vcl.ShowMessage("Unsupported action for this database")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := queryableLd.ExecQuery(queryString, f.queryResult)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vcl.ShowMessage(err.Error())
|
vcl.ShowMessage(err.Error())
|
||||||
return
|
return
|
||||||
@@ -790,6 +812,18 @@ func (f *TMainForm) OnNavChange(sender vcl.IObject, node *vcl.TTreeNode) {
|
|||||||
f.contentBox.SetEnabled(false)
|
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
|
// We're in charge of common status bar text updates
|
||||||
f.StatusBar.SetSimpleText(ld.DisplayName() + " | " + ld.DriverName())
|
f.StatusBar.SetSimpleText(ld.DisplayName() + " | " + ld.DriverName())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,11 @@ func vcl_default_tab_background() types.TColor {
|
|||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
// Assuming that uxtheme is loaded
|
// Assuming that uxtheme is loaded
|
||||||
// @ref https://stackoverflow.com/a/20332712
|
// @ref https://stackoverflow.com/a/20332712
|
||||||
return colors.ClBtnHighlight
|
// return colors.ClBtnHighlight
|
||||||
|
|
||||||
|
// None of the colors.** constants seem to be quite right on a test
|
||||||
|
// Windows 11 machine - should be #f9f9f9
|
||||||
|
return 0x00f9f9f9
|
||||||
} else {
|
} else {
|
||||||
return colors.ClBtnFace // 0x00f0f0f0
|
return colors.ClBtnFace // 0x00f0f0f0
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user