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 "" } 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(), "QBolt "+Version+"
Graphical interface for managing Bolt databases

"+ "- About BoltDB
"+ "- FamFamFam "Silk" icon set
"+ "- QBolt homepage
", ) } 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) }