497 lines
13 KiB
Go
497 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/mappu/miqt/qt"
|
|
bolt "go.etcd.io/bbolt"
|
|
)
|
|
|
|
type MainWindow struct {
|
|
ui *MainWindowUi
|
|
|
|
databaseContext *qt.QMenu
|
|
bucketContext *qt.QMenu
|
|
lastContextSelection *qt.QTreeWidgetItem
|
|
}
|
|
|
|
func NewMainWindow() *MainWindow {
|
|
this := &MainWindow{}
|
|
this.ui = NewMainWindowUi()
|
|
|
|
this.on_bucketTree_currentItemChanged(nil, nil)
|
|
|
|
this.databaseContext = qt.NewQMenu()
|
|
this.databaseContext.QWidget.AddAction(this.ui.actionRefresh_buckets)
|
|
this.databaseContext.QWidget.AddAction(this.ui.actionAdd_bucket)
|
|
this.databaseContext.AddSeparator()
|
|
this.databaseContext.QWidget.AddAction(this.ui.actionDisconnect)
|
|
|
|
this.bucketContext = qt.NewQMenu()
|
|
this.bucketContext.QWidget.AddAction(this.ui.actionRefresh_buckets)
|
|
this.bucketContext.QWidget.AddAction(this.ui.actionAdd_bucket)
|
|
this.bucketContext.AddSeparator()
|
|
this.bucketContext.QWidget.AddAction(this.ui.actionDelete_bucket)
|
|
|
|
// Connections
|
|
this.ui.actionNew_database.OnTriggered(this.on_actionNew_database_triggered)
|
|
this.ui.actionOpen_database.OnTriggered(this.on_actionOpen_database_triggered)
|
|
this.ui.actionOpen_database_as_read_only.OnTriggered(this.on_actionOpen_database_as_read_only_triggered)
|
|
this.ui.actionExit.OnTriggered(this.on_actionExit_triggered)
|
|
this.ui.actionAbout_Qt.OnTriggered(this.on_actionAbout_Qt_triggered)
|
|
this.ui.actionAbout_qbolt.OnTriggered(this.on_actionAbout_qbolt_triggered)
|
|
this.ui.actionDisconnect.OnTriggered(this.on_actionDisconnect_triggered)
|
|
this.ui.bucketTree.OnCustomContextMenuRequested(this.on_bucketTree_customContextMenuRequested)
|
|
this.ui.actionRefresh_buckets.OnTriggered(this.on_actionRefresh_buckets_triggered)
|
|
this.ui.bucketTree.OnCurrentItemChanged(this.on_bucketTree_currentItemChanged)
|
|
this.ui.actionClear_selection.OnTriggered(this.on_actionClear_selection_triggered)
|
|
this.ui.bucketData.OnDoubleClicked(this.on_bucketData_doubleClicked)
|
|
this.ui.actionAdd_bucket.OnTriggered(this.on_actionAdd_bucket_triggered)
|
|
this.ui.actionDelete_bucket.OnTriggered(this.on_actionDelete_bucket_triggered)
|
|
this.ui.AddDataButton.OnClicked(this.on_AddDataButton_clicked)
|
|
this.ui.DeleteDataButton.OnClicked(this.on_DeleteDataButton_clicked)
|
|
this.ui.bucketData.OnItemSelectionChanged(this.on_bucketData_itemSelectionChanged)
|
|
|
|
return this
|
|
}
|
|
|
|
const (
|
|
BdbPointerRole = int(qt.UserRole + 1)
|
|
BinaryDataRole = int(qt.UserRole + 2)
|
|
)
|
|
|
|
var bdbs []*bolt.DB = nil
|
|
|
|
func SET_BDB(top *qt.QTreeWidgetItem, bdb *bolt.DB) {
|
|
idx := len(bdbs)
|
|
bdbs = append(bdbs, bdb)
|
|
top.SetData(0, BdbPointerRole, qt.NewQVariant7(idx)) // Don't store a Go pointer in Qt memory
|
|
}
|
|
|
|
func GET_BDB(top *qt.QTreeWidgetItem) *bolt.DB {
|
|
if top == nil {
|
|
panic("Passed a nil QTreeWidgetItem")
|
|
}
|
|
|
|
dataVariant := top.Data(0, BdbPointerRole)
|
|
if dataVariant == nil {
|
|
panic("Selected item has no bdb")
|
|
}
|
|
|
|
return bdbs[dataVariant.ToInt()]
|
|
}
|
|
|
|
func (this *MainWindow) Widget() *qt.QWidget {
|
|
return this.ui.centralWidget
|
|
}
|
|
|
|
func (this *MainWindow) on_actionNew_database_triggered() {
|
|
file := qt.QFileDialog_GetSaveFileName2(this.Widget(), "Save new bolt database as...")
|
|
if len(file) > 0 {
|
|
this.openDatabase(file, false)
|
|
}
|
|
}
|
|
|
|
func (this *MainWindow) on_actionOpen_database_triggered() {
|
|
file := qt.QFileDialog_GetOpenFileName2(this.Widget(), "Select bolt database...")
|
|
if len(file) > 0 {
|
|
this.openDatabase(file, false)
|
|
}
|
|
}
|
|
|
|
func (this *MainWindow) on_actionOpen_database_as_read_only_triggered() {
|
|
file := qt.QFileDialog_GetOpenFileName2(this.Widget(), "Select bolt database...")
|
|
if len(file) > 0 {
|
|
this.openDatabase(file, true)
|
|
}
|
|
}
|
|
|
|
func (this *MainWindow) alert(message string) {
|
|
qt.QMessageBox_Critical(this.Widget(), "qbolt", message)
|
|
}
|
|
|
|
func (this *MainWindow) openDatabase(file string, readOnly bool) {
|
|
|
|
// Open
|
|
bdb, err := Bolt_Open(readOnly, file)
|
|
if err != nil {
|
|
this.alert(fmt.Sprintf("Error opening database: %s", err.Error()))
|
|
return
|
|
}
|
|
|
|
top := qt.NewQTreeWidgetItem()
|
|
top.SetText(0, filepath.Base(file))
|
|
top.SetIcon(0, qt.NewQIcon4(":/rsrc/database.png"))
|
|
SET_BDB(top, bdb)
|
|
this.ui.bucketTree.AddTopLevelItem(top)
|
|
|
|
this.refreshBucketTree(top)
|
|
this.ui.bucketTree.SetCurrentItem(top)
|
|
|
|
this.ui.bucketTree.ExpandItem(top)
|
|
}
|
|
|
|
func getDisplayName(qba string) string {
|
|
if qba == "" {
|
|
return "<empty>"
|
|
}
|
|
|
|
ret := strconv.Quote(qba)
|
|
return ret[1 : len(ret)-1]
|
|
}
|
|
|
|
func (this *MainWindow) refreshBucketTree(itm *qt.QTreeWidgetItem) {
|
|
ws := this.getSelection(itm)
|
|
|
|
// Remove existing children
|
|
i := itm.ChildCount()
|
|
for i > 0 {
|
|
itm.TakeChild(i - 1).Delete()
|
|
i -= 1
|
|
}
|
|
|
|
err := Bolt_ListBuckets(ws.bdb, ws.browse, func(qba string) {
|
|
|
|
child := qt.NewQTreeWidgetItem6(itm) // NewQTreeWidgetItem()
|
|
child.SetText(0, getDisplayName(qba))
|
|
child.SetData(0, BinaryDataRole, qt.NewQVariant15(MakeQByteArray(qba)))
|
|
child.SetIcon(0, qt.NewQIcon4(":/rsrc/table.png"))
|
|
|
|
itm.AddChild(child)
|
|
this.refreshBucketTree(child)
|
|
})
|
|
if err != nil {
|
|
this.alert(fmt.Sprintf("Error listing buckets under %s: %s", strings.Join(ws.browse, `/`), err.Error()))
|
|
return
|
|
}
|
|
}
|
|
|
|
func (this *MainWindow) on_actionExit_triggered() {
|
|
this.ui.MainWindow.Close()
|
|
}
|
|
|
|
func (this *MainWindow) on_actionAbout_Qt_triggered() {
|
|
qt.QApplication_AboutQt()
|
|
}
|
|
|
|
func (this *MainWindow) on_actionAbout_qbolt_triggered() {
|
|
qt.QMessageBox_About(
|
|
this.Widget(),
|
|
qt.QGuiApplication_ApplicationDisplayName(),
|
|
"<b>QBolt "+Version+"</b><br>Graphical interface for managing Bolt databases<br><br>"+
|
|
"- <a href='https://github.com/boltdb/bolt'>About BoltDB</a><br>"+
|
|
"- <a href='http://www.famfamfam.com/lab/icons/silk/'>FamFamFam "Silk" icon set</a><br>"+
|
|
"- <a href='https://code.ivysaur.me/qbolt'>QBolt homepage</a><br>",
|
|
)
|
|
}
|
|
|
|
func (this *MainWindow) on_actionDisconnect_triggered() {
|
|
top := this.lastContextSelection
|
|
if top.Parent() != nil {
|
|
return // somehow we didn't select a top-level item
|
|
}
|
|
|
|
bdb := GET_BDB(top)
|
|
|
|
// Remove UI
|
|
this.ui.bucketTree.ClearSelection()
|
|
top.Delete()
|
|
|
|
// Disconnect from DB
|
|
bdb.Close()
|
|
}
|
|
|
|
func (this *MainWindow) on_bucketTree_customContextMenuRequested(pos *qt.QPoint) {
|
|
|
|
itm := this.ui.bucketTree.ItemAt(pos)
|
|
if itm == nil {
|
|
return
|
|
}
|
|
|
|
this.lastContextSelection = itm
|
|
|
|
if itm.Parent() != nil {
|
|
// Child item, show the bucket menu
|
|
this.bucketContext.Popup(this.ui.bucketTree.Viewport().MapToGlobal(pos))
|
|
|
|
} else {
|
|
// Top-level item, show the database menu
|
|
this.databaseContext.Popup(this.ui.bucketTree.Viewport().MapToGlobal(pos))
|
|
}
|
|
}
|
|
|
|
func (this *MainWindow) on_actionRefresh_buckets_triggered() {
|
|
this.refreshBucketTree(this.lastContextSelection)
|
|
}
|
|
|
|
func (this *MainWindow) on_bucketTree_currentItemChanged(current, previous *qt.QTreeWidgetItem) {
|
|
_ = previous // Q_UNUSED
|
|
if current == nil {
|
|
this.ui.stackedWidget.SetVisible(false)
|
|
return
|
|
}
|
|
|
|
this.ui.stackedWidget.SetVisible(true)
|
|
|
|
if current.Parent() == nil {
|
|
// Selected a database
|
|
this.ui.stackedWidget.SetCurrentWidget(this.ui.databasePage)
|
|
this.ui.databasePropertiesArea.Clear()
|
|
|
|
bdb := GET_BDB(current)
|
|
stats, err := Bolt_DBStats(bdb)
|
|
if err != nil {
|
|
this.ui.databasePropertiesArea.SetPlainText(fmt.Sprintf("Error retrieving database statistics: %s", err.Error()))
|
|
} else {
|
|
this.ui.databasePropertiesArea.SetPlainText(stats)
|
|
}
|
|
|
|
// Clean up foreign areas
|
|
this.ui.bucketPropertiesArea.Clear()
|
|
this.ui.bucketData.Clear()
|
|
|
|
} else {
|
|
// Selected a bucket
|
|
|
|
this.ui.stackedWidget.SetCurrentWidget(this.ui.bucketPage)
|
|
this.ui.bucketPropertiesArea.Clear()
|
|
|
|
ws := this.getSelection(current)
|
|
|
|
stats, err := Bolt_BucketStats(ws.bdb, ws.browse)
|
|
if err != nil {
|
|
this.ui.bucketPropertiesArea.SetPlainText(fmt.Sprintf("Error retrieving bucket statistics: %s", err.Error()))
|
|
} else {
|
|
this.ui.bucketPropertiesArea.SetPlainText(stats)
|
|
}
|
|
|
|
// Load the data tab
|
|
this.refreshData(ws.bdb, ws.browse)
|
|
|
|
// Clean up foreign areas
|
|
this.ui.databasePropertiesArea.Clear()
|
|
}
|
|
}
|
|
|
|
func (this *MainWindow) refreshData(bdb *bolt.DB, browse []string) {
|
|
// Load the data tab
|
|
this.ui.bucketData.Clear()
|
|
|
|
err := Bolt_ListItems(bdb, browse, func(lii ListItemInfo) error {
|
|
itm := qt.NewQTreeWidgetItem()
|
|
itm.SetText(0, getDisplayName(lii.Name))
|
|
itm.SetData(0, BinaryDataRole, qt.NewQVariant15(MakeQByteArray(lii.Name)))
|
|
itm.SetText(1, fmt.Sprintf("%d", lii.DataLen))
|
|
this.ui.bucketData.AddTopLevelItem(itm)
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
this.alert(fmt.Sprintf("Error listing bucket content: %s", err.Error()))
|
|
return
|
|
}
|
|
|
|
this.ui.bucketData.ResizeColumnToContents(0)
|
|
this.on_bucketData_itemSelectionChanged()
|
|
}
|
|
|
|
func (this *MainWindow) on_actionClear_selection_triggered() {
|
|
this.ui.bucketTree.SetCurrentItem(nil)
|
|
}
|
|
|
|
type windowSelection struct {
|
|
itm *qt.QTreeWidgetItem
|
|
top *qt.QTreeWidgetItem
|
|
browse []string
|
|
bdb *bolt.DB
|
|
}
|
|
|
|
func (this *MainWindow) getWindowSelection() (windowSelection, bool) {
|
|
|
|
itm := this.ui.bucketTree.CurrentItem()
|
|
if itm == nil {
|
|
return windowSelection{}, false // No selection
|
|
}
|
|
|
|
return this.getSelection(itm), true
|
|
}
|
|
|
|
func (this *MainWindow) getSelection(itm *qt.QTreeWidgetItem) windowSelection {
|
|
|
|
top := itm
|
|
|
|
var browse []string
|
|
for {
|
|
if top.Parent() == nil {
|
|
break
|
|
} else {
|
|
browse = append(browse, FromQByteArray(top.Data(0, BinaryDataRole).ToByteArray()))
|
|
top = top.Parent()
|
|
}
|
|
}
|
|
|
|
ReverseSlice(browse)
|
|
|
|
bdb := GET_BDB(top)
|
|
|
|
return windowSelection{itm: itm, top: top, browse: browse, bdb: bdb}
|
|
}
|
|
|
|
func (this *MainWindow) openEditor(bdb *bolt.DB, saveAs []string, saveAsKey string, currentContent []byte) {
|
|
iw := NewItemWindowUi()
|
|
|
|
iw.contentArea.SetPlainText(string(currentContent))
|
|
iw.ItemWindow.SetWindowTitle(getDisplayName(saveAsKey))
|
|
iw.ItemWindow.SetWindowModality(qt.ApplicationModal) // we need this - otherwise we'll refresh a possibly-changed area after saving
|
|
iw.ItemWindow.OnFinished(func(exitCode int) {
|
|
if exitCode == int(qt.QDialog__Accepted) {
|
|
|
|
err := Bolt_SetItem(bdb, saveAs, saveAsKey, iw.contentArea.ToPlainText())
|
|
if err != nil {
|
|
this.alert(fmt.Sprintf("Error saving item content: %s", err.Error()))
|
|
}
|
|
|
|
this.refreshData(bdb, saveAs)
|
|
}
|
|
iw.ItemWindow.DeleteLater()
|
|
})
|
|
|
|
iw.ItemWindow.Show()
|
|
}
|
|
|
|
func (this *MainWindow) on_bucketData_doubleClicked(index *qt.QModelIndex) {
|
|
ws, ok := this.getWindowSelection()
|
|
if !ok {
|
|
return // no selection
|
|
}
|
|
|
|
// Get item key
|
|
|
|
model := index.Model()
|
|
key := FromQByteArray(model.Data2(model.Index(index.Row(), 0), BinaryDataRole).ToByteArray())
|
|
|
|
// DB lookup
|
|
content, err := Bolt_GetItem(ws.bdb, ws.browse, key)
|
|
if err != nil {
|
|
this.alert(fmt.Sprintf("Error loading item content: %s", err.Error()))
|
|
return
|
|
}
|
|
|
|
this.openEditor(ws.bdb, ws.browse, key, []byte(content))
|
|
}
|
|
|
|
func (this *MainWindow) on_actionAdd_bucket_triggered() {
|
|
ws, ok := this.getWindowSelection()
|
|
if !ok {
|
|
return // no selection
|
|
}
|
|
|
|
// Prompt for bucket name
|
|
|
|
name := qt.QInputDialog_GetText(this.Widget(), "New bucket", "Enter a key for the new bucket:")
|
|
if len(name) == 0 {
|
|
return
|
|
}
|
|
|
|
// Create
|
|
err := Bolt_CreateBucket(ws.bdb, ws.browse, name)
|
|
if err != nil {
|
|
this.alert(fmt.Sprintf("Error creating bucket: %s", err.Error()))
|
|
return
|
|
}
|
|
|
|
// Refresh bucket list
|
|
this.refreshBucketTree(ws.itm) // sub-tree only
|
|
this.ui.bucketTree.ExpandItem(ws.itm)
|
|
}
|
|
|
|
func (this *MainWindow) on_actionDelete_bucket_triggered() {
|
|
ws, ok := this.getWindowSelection()
|
|
if !ok {
|
|
return // no selection
|
|
}
|
|
|
|
// Prompt for confirmation
|
|
bucketToDelete := FromQByteArray(ws.itm.Data(0, BinaryDataRole).ToByteArray())
|
|
if qt.QMessageBox_Question4(
|
|
this.Widget(),
|
|
"Delete bucket",
|
|
fmt.Sprintf("Are you sure you want to remove the bucket '%s'?", getDisplayName(bucketToDelete)),
|
|
qt.QMessageBox__Yes,
|
|
qt.QMessageBox__Cancel,
|
|
) != int(qt.QMessageBox__Yes) {
|
|
return
|
|
}
|
|
|
|
parent := ws.itm.Parent()
|
|
|
|
// One level down
|
|
|
|
if len(ws.browse) > 0 {
|
|
ws.browse = ws.browse[0 : len(ws.browse)-1]
|
|
}
|
|
|
|
err := Bolt_DeleteBucket(ws.bdb, ws.browse, bucketToDelete)
|
|
if err != nil {
|
|
this.alert(fmt.Sprintf("Error removing bucket: %s", err.Error()))
|
|
return
|
|
}
|
|
|
|
// Refresh bucket list
|
|
this.refreshBucketTree(parent) // sub-tree only
|
|
this.ui.bucketTree.ExpandItem(parent)
|
|
this.ui.bucketTree.SetCurrentItem(parent)
|
|
}
|
|
|
|
func (this *MainWindow) on_AddDataButton_clicked() {
|
|
ws, ok := this.getWindowSelection()
|
|
if !ok {
|
|
return // no selection
|
|
}
|
|
|
|
// Prompt for bucket name
|
|
|
|
name := qt.QInputDialog_GetText(this.Widget(), "New item", "Enter a key for the new item:")
|
|
if len(name) == 0 {
|
|
return
|
|
}
|
|
|
|
this.openEditor(ws.bdb, ws.browse, name, []byte(""))
|
|
}
|
|
|
|
func (this *MainWindow) on_DeleteDataButton_clicked() {
|
|
ws, ok := this.getWindowSelection()
|
|
if !ok {
|
|
return // no selection
|
|
}
|
|
|
|
selection := this.ui.bucketData.SelectedItems()
|
|
if len(selection) == 0 {
|
|
return // nothing to do
|
|
}
|
|
|
|
// Prompt for confirmation
|
|
if qt.QMessageBox_Question4(this.Widget(), "Delete items", fmt.Sprintf("Are you sure you want to remove %d item(s)?", len(selection)), qt.QMessageBox__Yes, qt.QMessageBox__Cancel) != int(qt.QMessageBox__Yes) {
|
|
return
|
|
}
|
|
|
|
var i int = len(selection)
|
|
for i > 0 {
|
|
err := Bolt_DeleteItem(ws.bdb, ws.browse, FromQByteArray(selection[i-1].Data(0, BinaryDataRole).ToByteArray()))
|
|
if err != nil {
|
|
this.alert(fmt.Sprintf("Error removing item: %s", err.Error()))
|
|
return
|
|
}
|
|
i -= 1
|
|
}
|
|
|
|
this.refreshData(ws.bdb, ws.browse)
|
|
}
|
|
|
|
func (this *MainWindow) on_bucketData_itemSelectionChanged() {
|
|
this.ui.DeleteDataButton.SetEnabled(len(this.ui.bucketData.SelectedItems()) > 0)
|
|
}
|