qbolt/mainwindow.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 &quot;Silk&quot; 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)
}