2024-10-03 06:34:28 +00:00
package main
import (
"fmt"
2024-10-05 02:58:56 +00:00
"path/filepath"
2024-10-03 06:34:28 +00:00
"strconv"
2024-10-05 02:58:56 +00:00
"strings"
2024-10-03 06:34:28 +00:00
"github.com/mappu/miqt/qt"
2024-10-05 03:36:41 +00:00
bolt "go.etcd.io/bbolt"
2024-10-03 06:34:28 +00:00
)
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 ( )
2024-10-05 02:58:56 +00:00
top . SetText ( 0 , filepath . Base ( file ) )
2024-10-03 06:34:28 +00:00
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 {
2024-10-05 02:58:56 +00:00
if qba == "" {
return "<empty>"
}
2024-10-03 06:34:28 +00:00
ret := strconv . Quote ( qba )
2024-10-05 02:58:56 +00:00
return ret [ 1 : len ( ret ) - 1 ]
2024-10-03 06:34:28 +00:00
}
func ( this * MainWindow ) refreshBucketTree ( itm * qt . QTreeWidgetItem ) {
2024-10-05 02:58:56 +00:00
ws := this . getSelection ( itm )
2024-10-03 06:34:28 +00:00
// Remove existing children
i := itm . ChildCount ( )
for i > 0 {
2024-10-05 02:58:56 +00:00
itm . TakeChild ( i - 1 ) . Delete ( )
2024-10-03 06:34:28 +00:00
i -= 1
}
2024-10-05 02:58:56 +00:00
err := Bolt_ListBuckets ( ws . bdb , ws . browse , func ( qba string ) {
2024-10-03 06:34:28 +00:00
2024-10-05 02:58:56 +00:00
child := qt . NewQTreeWidgetItem6 ( itm ) // NewQTreeWidgetItem()
2024-10-03 06:34:28 +00:00
child . SetText ( 0 , getDisplayName ( qba ) )
2024-10-05 02:58:56 +00:00
child . SetData ( 0 , BinaryDataRole , qt . NewQVariant15 ( MakeQByteArray ( qba ) ) )
2024-10-03 06:34:28 +00:00
child . SetIcon ( 0 , qt . NewQIcon4 ( ":/rsrc/table.png" ) )
itm . AddChild ( child )
this . refreshBucketTree ( child )
} )
if err != nil {
2024-10-05 02:58:56 +00:00
this . alert ( fmt . Sprintf ( "Error listing buckets under %s: %s" , strings . Join ( ws . browse , ` / ` ) , err . Error ( ) ) )
2024-10-03 06:34:28 +00:00
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 ( ) ,
2024-10-05 03:37:15 +00:00
"<b>QBolt " + Version + "</b><br>Graphical interface for managing Bolt databases<br><br>" +
2024-10-03 06:34:28 +00:00
"- <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
2024-10-05 02:58:56 +00:00
this . bucketContext . Popup ( this . ui . bucketTree . Viewport ( ) . MapToGlobal ( pos ) )
2024-10-03 06:34:28 +00:00
} else {
// Top-level item, show the database menu
2024-10-05 02:58:56 +00:00
this . databaseContext . Popup ( this . ui . bucketTree . Viewport ( ) . MapToGlobal ( pos ) )
2024-10-03 06:34:28 +00:00
}
}
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 ( )
2024-10-05 02:58:56 +00:00
ws := this . getSelection ( current )
2024-10-03 06:34:28 +00:00
2024-10-05 02:58:56 +00:00
stats , err := Bolt_BucketStats ( ws . bdb , ws . browse )
2024-10-03 06:34:28 +00:00
if err != nil {
2024-10-05 02:58:56 +00:00
this . ui . bucketPropertiesArea . SetPlainText ( fmt . Sprintf ( "Error retrieving bucket statistics: %s" , err . Error ( ) ) )
2024-10-03 06:34:28 +00:00
} else {
2024-10-05 02:58:56 +00:00
this . ui . bucketPropertiesArea . SetPlainText ( stats )
2024-10-03 06:34:28 +00:00
}
// Load the data tab
2024-10-05 02:58:56 +00:00
this . refreshData ( ws . bdb , ws . browse )
2024-10-03 06:34:28 +00:00
// 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 ) )
2024-10-05 02:58:56 +00:00
itm . SetData ( 0 , BinaryDataRole , qt . NewQVariant15 ( MakeQByteArray ( lii . Name ) ) )
2024-10-03 06:34:28 +00:00
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
}
2024-10-05 02:58:56 +00:00
return this . getSelection ( itm ) , true
}
func ( this * MainWindow ) getSelection ( itm * qt . QTreeWidgetItem ) windowSelection {
2024-10-03 06:34:28 +00:00
top := itm
var browse [ ] string
for {
if top . Parent ( ) == nil {
break
} else {
2024-10-05 02:58:56 +00:00
browse = append ( browse , FromQByteArray ( top . Data ( 0 , BinaryDataRole ) . ToByteArray ( ) ) )
2024-10-03 06:34:28 +00:00
top = top . Parent ( )
}
}
ReverseSlice ( browse )
bdb := GET_BDB ( top )
2024-10-05 02:58:56 +00:00
return windowSelection { itm : itm , top : top , browse : browse , bdb : bdb }
2024-10-03 06:34:28 +00:00
}
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 ( )
2024-10-05 02:58:56 +00:00
key := FromQByteArray ( model . Data2 ( model . Index ( index . Row ( ) , 0 ) , BinaryDataRole ) . ToByteArray ( ) )
2024-10-03 06:34:28 +00:00
// 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
2024-10-05 02:58:56 +00:00
bucketToDelete := FromQByteArray ( ws . itm . Data ( 0 , BinaryDataRole ) . ToByteArray ( ) )
2024-10-03 06:34:28 +00:00
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 {
2024-10-05 02:58:56 +00:00
err := Bolt_DeleteItem ( ws . bdb , ws . browse , FromQByteArray ( selection [ i - 1 ] . Data ( 0 , BinaryDataRole ) . ToByteArray ( ) ) )
2024-10-03 06:34:28 +00:00
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 )
}