Merge branch 'qbolt-go'

This commit is contained in:
mappu 2024-10-05 17:42:14 +13:00
commit f08242e93c
48 changed files with 1325 additions and 1539 deletions

18
.gitignore vendored
View File

@ -1,4 +1,16 @@
build-qbolt-* # development
qbolt
dummy-data/dummy-data* dummy-data/dummy-data*
*.pro.user$
build/ # temporary build files
rsrc_windows_amd64.syso
windows-manifest.json
# release build files
build/qbolt
build/qbolt.exe
build/*.xz
build/*.zip
# local makefile definition scripts
make-*

108
Makefile
View File

@ -1,72 +1,60 @@
export PATH := /usr/lib/mxe/usr/bin:$(PATH) VERSION := 1.0.3
GOFLAGS := -ldflags='-s -w' -gcflags='-trimpath=$(CURDIR)' -asmflags='-trimpath=$(CURDIR)' GOFLAGS_L := -ldflags='-s -w -X main.Version=v$(VERSION)' -buildvcs=false -gcflags='-trimpath=$(CURDIR)' -asmflags='-trimpath=$(CURDIR)'
VERSION := 1.0.2 GOFLAGS_W := -ldflags='-s -w -X main.Version=v$(VERSION) -H windowsgui' -buildvcs=false --tags=windowsqtstatic -gcflags='-trimpath=$(CURDIR)' -asmflags='-trimpath=$(CURDIR)'
SHELL := /bin/bash
# Allow overriding DOCKER with e.g. sudo docker
DOCKER := docker
MIQT_UIC := miqt-uic
MIQT_RCC := miqt-rcc
GO_WINRES := go-winres
SOURCES := $(wildcard *.go *.ui *.qrc) resources.go mainwindow_ui.go itemwindow_ui.go rsrc_windows_amd64.syso
.PHONY: all libs dist clean .PHONY: all
all: build/qbolt build/qbolt.exe
all: \
build/linux/qbolt \
build/win32/release/qbolt.exe
libs: \
build/linux/qbolt.a \
build/win32/qbolt.a
dist: \ .PHONY: dist
build/dist/qbolt-${VERSION}-win32.zip \ dist: build/qbolt-${VERSION}-windows-x86_64.zip build/qbolt-${VERSION}-debian12-x86_64.tar.xz
build/dist/qbolt-${VERSION}-linux_amd64.tar.xz
.PHONY: clean
clean: clean:
if [ -f qbolt/qbolt.a ] ; then rm qbolt/qbolt.a ; fi rm -f qbolt || true
if [ -f qbolt ] ; then rm qbolt ; fi rm -rf build || true
if [ -d build ] ; then rm -r build ; fi rm -f windows-manifest.json || true
# Build core golang shared library (linux) # Generated files
build/linux/qbolt.a: *.go resources.rcc resources.go: resources.qrc
mkdir -p build/linux $(MIQT_RCC) resources.qrc
go build ${GOFLAGS} -buildmode=c-archive -o build/linux/qbolt.a
# Build core golang shared library (win32) mainwindow_ui.go: mainwindow.ui
$(MIQT_UIC) -InFile mainwindow.ui -OutFile mainwindow_ui.go
itemwindow_ui.go: itemwindow.ui
$(MIQT_UIC) -InFile itemwindow.ui -OutFile itemwindow_ui.go
build/win32/qbolt.a: *.go windows-manifest.json: windows-manifest.template.json Makefile
mkdir -p build/win32 cat windows-manifest.template.json | sed -re 's_%VERSION%_$(VERSION)_' > windows-manifest.json
CC=/usr/lib/mxe/usr/bin/i686-w64-mingw32.static-gcc CGO_ENABLED=1 GOARCH=386 GOOS=windows \
go build ${GOFLAGS} -buildmode=c-archive -o build/win32/qbolt.a
# Linux binaries
build/linux/qbolt: build/linux/qbolt.a qbolt/* rsrc_windows_amd64.syso: windows-manifest.json
cd build/linux && qmake ../../qbolt/qbolt.pro && make $(GO_WINRES) make --in windows-manifest.json
rm rsrc_windows_386.syso || true # we do not build x86_32
# Linux distribution
build/dist/qbolt-${VERSION}-linux_amd64.tar.xz: build/linux/qbolt
XZ_OPTS=-9 tar caf build/dist/qbolt-${VERSION}-linux_amd64.tar.xz -C build/linux qbolt --owner=0 --group=0
# Windows binaries # Linux release
build/win32/release/qbolt.exe: build/win32/qbolt.a qbolt/*
cd build/win32 && i686-w64-mingw32.static-qmake-qt5 ../../qbolt/qbolt.pro && make
# Dockerized Windows build
.PHONY: build-docker-build-environment
build-docker-build-environment:
cd docker && docker build -t win32-cross-qt-mxe:latest -f win32-cross-qt-mxe.Dockerfile
.PHONY: build-windows-in-docker
build-windows-in-docker:
docker run --rm -v $(CURDIR):/qbolt win32-cross-qt-mxe:latest /bin/sh -c 'cd /qbolt && make build/win32/release/qbolt.exe'
# Windows distribution
build/win32/dist/qbolt.exe: build/win32/release/qbolt.exe
mkdir -p build/win32/dist
cp build/win32/release/qbolt.exe build/win32/dist/qbolt.exe
# upx --lzma build/win32/dist/qbolt.exe
build/dist/qbolt-${VERSION}-win32.zip: build/win32/dist/qbolt.exe build/qbolt: $(SOURCES)
mkdir -p build/dist go build $(GOFLAGS_L) -o build/qbolt
zip -0 -j build/dist/qbolt-${VERSION}-win32.zip build/win32/dist/qbolt.exe upx build/qbolt
build/qbolt-${VERSION}-debian12-x86_64.tar.xz: build/qbolt
XZ_OPTS=-9e tar caf build/qbolt-${VERSION}-debian12-x86_64.tar.xz -C build qbolt --owner=0 --group=0
# Windows release (docker)
build/qbolt.exe: $(SOURCES)
( $(DOCKER) image ls | fgrep qbolt-win64-cross ) || ( cd docker && $(DOCKER) build -t qbolt-win64-cross:latest -f win64-cross.Dockerfile . )
$(DOCKER) run --rm -v $(CURDIR):/qbolt -w /qbolt qbolt-win64-cross:latest /bin/sh -c "go build $(GOFLAGS_W) -o build/qbolt.exe"
upx --force build/qbolt.exe
build/qbolt-${VERSION}-windows-x86_64.zip: build/qbolt.exe
zip -9 -j build/qbolt-${VERSION}-windows-x86_64.zip build/qbolt.exe

View File

@ -1,56 +0,0 @@
package main
import "C"
import (
"errors"
"sync"
)
type ObjectReference int64
var NullObjectReference error = errors.New("Null object reference")
// GoMemoryStore is a int->interface storage structure so that Go pointers are
// never exposed to C code.
type GoMemoryStore struct {
mtx sync.RWMutex
items map[int64]interface{}
next int64
}
func NewGoMemoryStore() *GoMemoryStore {
ret := GoMemoryStore{}
ret.items = make(map[int64]interface{})
return &ret
}
func (this *GoMemoryStore) Put(itm interface{}) ObjectReference {
this.mtx.Lock()
defer this.mtx.Unlock()
key := this.next
this.items[key] = itm
this.next++
return ObjectReference(key)
}
func (this *GoMemoryStore) Get(i ObjectReference) (interface{}, bool) {
this.mtx.RLock()
defer this.mtx.RUnlock()
ret, ok := this.items[int64(i)]
return ret, ok
}
func (this *GoMemoryStore) Delete(i ObjectReference) {
this.mtx.Lock()
defer this.mtx.Unlock()
delete(this.items, int64(i))
}
var gms *GoMemoryStore = nil
func init() {
gms = NewGoMemoryStore()
}

219
bolt.go Normal file
View File

@ -0,0 +1,219 @@
package main
import (
"encoding/json"
"errors"
"os"
"time"
bolt "go.etcd.io/bbolt"
)
func Bolt_Open(readOnly bool, path string) (*bolt.DB, error) {
opts := *bolt.DefaultOptions
opts.Timeout = 10 * time.Second
opts.ReadOnly = readOnly
return bolt.Open(path, os.FileMode(0644), &opts)
}
func walkBuckets(tx *bolt.Tx, browse []string) (*bolt.Bucket, error) {
bucket := tx.Bucket([]byte(browse[0]))
if bucket == nil {
return nil, errors.New("Unknown bucket")
}
for i := 1; i < len(browse); i += 1 {
bucket = bucket.Bucket([]byte(browse[i]))
if bucket == nil {
return nil, errors.New("Unknown bucket")
}
}
return bucket, nil
}
func withBrowse_ReadOnly(db *bolt.DB, browse []string, fn func(tx *bolt.Tx, bucket *bolt.Bucket) error) error {
if len(browse) == 0 {
// not a bucket
return errors.New("No bucket selected")
}
return db.View(func(tx *bolt.Tx) error {
bucket, err := walkBuckets(tx, browse)
if err != nil {
return err
}
// Walked the bucket chain, now run the user callback
return fn(tx, bucket)
})
}
func Bolt_CreateBucket(db *bolt.DB, browse []string, newBucket string) error {
return db.Update(func(tx *bolt.Tx) error {
if len(browse) == 0 {
// Top-level bucket
_, err := tx.CreateBucket([]byte(newBucket))
return err
} else {
// Deeper bucket
bucket, err := walkBuckets(tx, browse)
if err != nil {
return err
}
// Walked the bucket chain, now create the new bucket
_, err = bucket.CreateBucket([]byte(newBucket))
return err
}
})
}
func Bolt_DeleteBucket(db *bolt.DB, browse []string, delBucket string) error {
return db.Update(func(tx *bolt.Tx) error {
if len(browse) == 0 {
// Top-level bucket
return tx.DeleteBucket([]byte(delBucket))
} else {
// Deeper bucket
bucket, err := walkBuckets(tx, browse)
if err != nil {
return err
}
// Walked the bucket chain, now delete the selected bucket
return bucket.DeleteBucket([]byte(delBucket))
}
})
}
func Bolt_SetItem(db *bolt.DB, browse []string, key, val string) error {
if len(browse) == 0 {
return errors.New("Can't create top-level items")
}
return db.Update(func(tx *bolt.Tx) error {
bucket, err := walkBuckets(tx, browse)
if err != nil {
return err
}
return bucket.Put([]byte(key), []byte(val))
})
}
func Bolt_DeleteItem(db *bolt.DB, browse []string, key string) error {
if len(browse) == 0 {
return errors.New("Can't create top-level items")
}
return db.Update(func(tx *bolt.Tx) error {
bucket, err := walkBuckets(tx, browse)
if err != nil {
return err
}
return bucket.Delete([]byte(key))
})
}
func Bolt_DBStats(db *bolt.DB) (string, error) {
jBytes, err := json.MarshalIndent(db.Stats(), "", " ")
if err != nil {
return "", err
}
return string(jBytes), nil
}
func Bolt_BucketStats(db *bolt.DB, browse []string) (string, error) {
var stats bolt.BucketStats
err := withBrowse_ReadOnly(db, browse, func(tx *bolt.Tx, bucket *bolt.Bucket) error {
stats = bucket.Stats()
return nil
})
if err != nil {
return "", err
}
jBytes, err := json.MarshalIndent(stats, "", " ")
if err != nil {
return "", err
}
return string(jBytes), err
}
func Bolt_ListBuckets(db *bolt.DB, browse []string, cb func(b string)) error {
if len(browse) == 0 {
// root mode
return db.View(func(tx *bolt.Tx) error {
return tx.ForEach(func(k []byte, _ *bolt.Bucket) error {
cb(string(k))
return nil
})
})
}
// Nested-mode
return withBrowse_ReadOnly(db, browse, func(tx *bolt.Tx, bucket *bolt.Bucket) error {
return bucket.ForEach(func(k, v []byte) error {
// non-nil v means it's a data item
if v == nil {
cb(string(k))
}
return nil
})
})
}
type ListItemInfo struct {
Name string
DataLen int64
}
func Bolt_ListItems(db *bolt.DB, browse []string, cb func(ListItemInfo) error) error {
if len(browse) == 0 {
return errors.New("No bucket specified")
}
// Nested-mode
return withBrowse_ReadOnly(db, browse, func(tx *bolt.Tx, bucket *bolt.Bucket) error {
return bucket.ForEach(func(k, v []byte) error {
if v == nil {
return nil // nil v means it's a bucket, skip
}
return cb(ListItemInfo{string(k), int64(len(v))})
})
})
}
func Bolt_GetItem(db *bolt.DB, browse []string, key string) (string, error) {
var ret string
err := withBrowse_ReadOnly(db, browse, func(tx *bolt.Tx, bucket *bolt.Bucket) error {
d := bucket.Get([]byte(key))
ret = string(d)
return nil
})
return ret, err
}
func Bolt_Close(db *bolt.DB) error {
return db.Close()
}

0
build/.create_dir Normal file
View File

View File

@ -1,13 +1,20 @@
FROM debian:bullseye FROM golang:1.23-bookworm
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
apt-get install -qyy gnupg2 golang-go ca-certificates apt-get install -qyy gnupg2 ca-certificates
RUN DEBIAN_FRONTEND=noninteractive \ RUN DEBIAN_FRONTEND=noninteractive \
echo "deb https://pkg.mxe.cc/repos/apt buster main" >/etc/apt/sources.list.d/mxeapt.list && \ echo "deb https://pkg.mxe.cc/repos/apt buster main" >/etc/apt/sources.list.d/mxeapt.list && \
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 86B72ED9 && \ apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 86B72ED9 && \
apt-get update && \ apt-get update && \
apt-get install -qyy mxe-i686-w64-mingw32.static-qt5 && \ apt-get install -qyy mxe-x86-64-w64-mingw32.static-qt5 && \
apt-get clean apt-get clean
ENV PATH=/usr/lib/mxe/usr/bin:$PATH ENV PATH=/usr/lib/mxe/usr/bin:$PATH
ENV CXX=x86_64-w64-mingw32.static-g++
ENV CC=x86_64-w64-mingw32.static-gcc
ENV PKG_CONFIG=x86_64-w64-mingw32.static-pkg-config
ENV GOOS=windows
ENV CGO_ENABLED=1

View File

@ -1,11 +1,10 @@
package main package main
import ( import (
//"fmt"
"math/rand" "math/rand"
"os" "os"
bolt "github.com/boltdb/bolt" bolt "go.etcd.io/bbolt"
) )
func random_name() string { func random_name() string {

9
go.mod
View File

@ -1,7 +1,10 @@
module code.ivysaur.me/qbolt module code.ivysaur.me/qbolt
go 1.15 go 1.23
require github.com/boltdb/bolt v1.3.1 require (
github.com/mappu/miqt v0.5.0
go.etcd.io/bbolt v1.3.11
)
replace github.com/boltdb/bolt => go.etcd.io/bbolt v1.3.5 require golang.org/x/sys v0.4.0 // indirect

16
go.sum
View File

@ -1,5 +1,11 @@
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/mappu/miqt v0.5.0 h1:BWajkNI9PWlWN6ZDgWKwv1gieBGEImRqlWS8ZqDmDfA=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= github.com/mappu/miqt v0.5.0/go.mod h1:xFg7ADaO1QSkmXPsPODoKe/bydJpRG9fgCYyIDl/h1U=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -42,12 +42,6 @@
</item> </item>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QFrame" name="frame"> <widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="0" column="0"> <item row="0" column="0">
<widget class="QDialogButtonBox" name="buttonBox"> <widget class="QDialogButtonBox" name="buttonBox">

71
itemwindow_ui.go Normal file
View File

@ -0,0 +1,71 @@
// Generated by miqt-uic. To update this file, edit the .ui file in
// Qt Designer, and then run 'go generate'.
//
//go:generate miqt-uic -InFile itemwindow.ui -OutFile itemwindow_ui.go
package main
import (
"github.com/mappu/miqt/qt"
)
type ItemWindowUi struct {
ItemWindow *qt.QDialog
gridLayout_2 *qt.QGridLayout
contentArea *qt.QPlainTextEdit
frame *qt.QFrame
gridLayout *qt.QGridLayout
buttonBox *qt.QDialogButtonBox
}
// NewItemWindowUi creates all Qt widget classes for ItemWindow.
func NewItemWindowUi() *ItemWindowUi {
ui := &ItemWindowUi{}
ui.ItemWindow = qt.NewQDialog2(nil)
ui.ItemWindow.SetObjectName("ItemWindow")
ui.ItemWindow.Resize(370, 353)
ui.ItemWindow.SetWindowTitle("")
icon0 := qt.NewQIcon()
icon0.AddFile4(":/rsrc/database_lightning.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.ItemWindow.SetWindowIcon(icon0)
ui.gridLayout_2 = qt.NewQGridLayout(ui.ItemWindow.QWidget)
ui.gridLayout_2.SetObjectName("gridLayout_2")
ui.gridLayout_2.SetVerticalSpacing(0)
ui.gridLayout_2.SetContentsMargins(0, 0, 0, 0)
ui.gridLayout_2.SetSpacing(6)
ui.contentArea = qt.NewQPlainTextEdit3(ui.ItemWindow.QWidget)
ui.contentArea.SetObjectName("contentArea")
ui.contentArea.SetFrameShape(qt.QFrame__NoFrame)
ui.gridLayout_2.AddWidget2(ui.contentArea.QWidget, 0, 0)
ui.frame = qt.NewQFrame2(ui.ItemWindow.QWidget)
ui.frame.SetObjectName("frame")
ui.gridLayout = qt.NewQGridLayout(ui.frame.QWidget)
ui.gridLayout.SetObjectName("gridLayout")
ui.gridLayout.SetContentsMargins(11, 11, 11, 11)
ui.gridLayout.SetSpacing(6)
ui.buttonBox = qt.NewQDialogButtonBox5(ui.frame.QWidget)
ui.buttonBox.SetObjectName("buttonBox")
ui.buttonBox.SetStandardButtons(qt.QDialogButtonBox__Cancel | qt.QDialogButtonBox__Save)
ui.buttonBox.OnAccepted(ui.ItemWindow.Accept)
ui.buttonBox.OnRejected(ui.ItemWindow.Reject)
ui.gridLayout.AddWidget2(ui.buttonBox.QWidget, 0, 0)
ui.gridLayout_2.AddWidget2(ui.frame.QWidget, 1, 0)
ui.Retranslate()
return ui
}
// Retranslate reapplies all text translations.
func (ui *ItemWindowUi) Retranslate() {
}

400
main.go
View File

@ -1,398 +1,22 @@
package main package main
import "C"
import ( import (
"encoding/binary"
"encoding/json"
"errors"
"os" "os"
"time"
bolt "github.com/boltdb/bolt" "github.com/mappu/miqt/qt"
) )
const ( var Version string = "v0.0.0-devel"
ERROR_AND_STOP_CALLING int64 = 100
ERROR_AND_KEEP_CALLING = 101
FINISHED_OK = 102
REAL_MESSAGE = 103
)
const Magic int64 = 0x10203040
//export GetMagic
func GetMagic() int64 {
return Magic
}
//export Bolt_Open
func Bolt_Open(readOnly bool, path string) (ObjectReference, *C.char, int) {
opts := *bolt.DefaultOptions
opts.Timeout = 10 * time.Second
opts.ReadOnly = readOnly
ptrDB, err := bolt.Open(path, os.FileMode(0644), &opts)
if err != nil {
errMsg := err.Error()
return 0, C.CString(errMsg), len(errMsg)
}
dbRef := gms.Put(ptrDB)
return dbRef, nil, 0
}
func withBoltDBReference(b ObjectReference, fn func(db *bolt.DB) error) error {
dbIFC, ok := gms.Get(b)
if !ok {
return NullObjectReference
}
ptrDB, ok := dbIFC.(*bolt.DB)
if !ok {
return NullObjectReference
}
return fn(ptrDB)
}
func walkBuckets(tx *bolt.Tx, browse []string) (*bolt.Bucket, error) {
bucket := tx.Bucket([]byte(browse[0]))
if bucket == nil {
return nil, errors.New("Unknown bucket")
}
for i := 1; i < len(browse); i += 1 {
bucket = bucket.Bucket([]byte(browse[i]))
if bucket == nil {
return nil, errors.New("Unknown bucket")
}
}
return bucket, nil
}
func withBrowse_ReadOnly(b_ref ObjectReference, browse []string, fn func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error) error {
if len(browse) == 0 {
// not a bucket
return errors.New("No bucket selected")
}
return withBoltDBReference(b_ref, func(db *bolt.DB) error {
return db.View(func(tx *bolt.Tx) error {
bucket, err := walkBuckets(tx, browse)
if err != nil {
return err
}
// Walked the bucket chain, now run the user callback
return fn(db, tx, bucket)
})
})
}
func err2triple(err error) (int64, *C.char, int) {
if err != nil {
msg := err.Error()
return ERROR_AND_STOP_CALLING, C.CString(msg), len(msg)
}
return FINISHED_OK, nil, 0
}
//export Bolt_CreateBucket
func Bolt_CreateBucket(b_ref ObjectReference, browse []string, newBucket string) (int64, *C.char, int) {
err := withBoltDBReference(b_ref, func(db *bolt.DB) error {
return db.Update(func(tx *bolt.Tx) error {
if len(browse) == 0 {
// Top-level bucket
_, err := tx.CreateBucket([]byte(newBucket))
return err
} else {
// Deeper bucket
bucket, err := walkBuckets(tx, browse)
if err != nil {
return err
}
// Walked the bucket chain, now create the new bucket
_, err = bucket.CreateBucket([]byte(newBucket))
return err
}
})
})
return err2triple(err)
}
//export Bolt_DeleteBucket
func Bolt_DeleteBucket(b_ref ObjectReference, browse []string, delBucket string) (int64, *C.char, int) {
err := withBoltDBReference(b_ref, func(db *bolt.DB) error {
return db.Update(func(tx *bolt.Tx) error {
if len(browse) == 0 {
// Top-level bucket
return tx.DeleteBucket([]byte(delBucket))
} else {
// Deeper bucket
bucket, err := walkBuckets(tx, browse)
if err != nil {
return err
}
// Walked the bucket chain, now delete the selected bucket
return bucket.DeleteBucket([]byte(delBucket))
}
})
})
return err2triple(err)
}
//export Bolt_SetItem
func Bolt_SetItem(b_ref ObjectReference, browse []string, key, val string) (int64, *C.char, int) {
if len(browse) == 0 {
return err2triple(errors.New("Can't create top-level items"))
}
err := withBoltDBReference(b_ref, func(db *bolt.DB) error {
return db.Update(func(tx *bolt.Tx) error {
bucket, err := walkBuckets(tx, browse)
if err != nil {
return err
}
return bucket.Put([]byte(key), []byte(val))
})
})
return err2triple(err)
}
//export Bolt_DeleteItem
func Bolt_DeleteItem(b_ref ObjectReference, browse []string, key string) (int64, *C.char, int) {
if len(browse) == 0 {
return err2triple(errors.New("Can't create top-level items"))
}
err := withBoltDBReference(b_ref, func(db *bolt.DB) error {
return db.Update(func(tx *bolt.Tx) error {
bucket, err := walkBuckets(tx, browse)
if err != nil {
return err
}
return bucket.Delete([]byte(key))
})
})
return err2triple(err)
}
type CallResponse struct {
s string
e error
}
//export Bolt_DBStats
func Bolt_DBStats(b ObjectReference) (int64, *C.char, int) {
var stats bolt.Stats
err := withBoltDBReference(b, func(db *bolt.DB) error {
stats = db.Stats()
return nil
})
jBytes, err := json.Marshal(stats)
if err != nil {
return err2triple(err)
}
return REAL_MESSAGE, C.CString(string(jBytes)), len(jBytes)
}
//export Bolt_BucketStats
func Bolt_BucketStats(b ObjectReference, browse []string) (int64, *C.char, int) {
var stats bolt.BucketStats
err := withBrowse_ReadOnly(b, browse, func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error {
stats = bucket.Stats()
return nil
})
if err != nil {
return err2triple(err)
}
jBytes, err := json.Marshal(stats)
if err != nil {
return err2triple(err)
}
return REAL_MESSAGE, C.CString(string(jBytes)), len(jBytes)
}
type NextCall struct {
content chan CallResponse
}
//export Bolt_ListBuckets
func Bolt_ListBuckets(b ObjectReference, browse []string) ObjectReference {
pNC := &NextCall{
content: make(chan CallResponse, 0),
}
pNC_Ref := gms.Put(pNC)
go func() {
var err error
if len(browse) == 0 {
// root mode
err = withBoltDBReference(b, func(db *bolt.DB) error {
return db.View(func(tx *bolt.Tx) error {
return tx.ForEach(func(k []byte, _ *bolt.Bucket) error {
pNC.content <- CallResponse{s: string(k)}
return nil
})
})
})
} else {
// Nested-mode
err = withBrowse_ReadOnly(b, browse, func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error {
return bucket.ForEach(func(k, v []byte) error {
// non-nil v means it's a data item
if v == nil {
pNC.content <- CallResponse{s: string(k)}
}
return nil
})
})
}
if err != nil {
pNC.content <- CallResponse{e: err}
}
close(pNC.content)
}()
return pNC_Ref
}
//export Bolt_ListItems
func Bolt_ListItems(b ObjectReference, browse []string) ObjectReference {
pNC := &NextCall{
content: make(chan CallResponse, 0),
}
pNC_Ref := gms.Put(pNC)
go func() {
var err error
if len(browse) == 0 {
err = errors.New("No bucket specified")
} else {
// Nested-mode
err = withBrowse_ReadOnly(b, browse, func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error {
return bucket.ForEach(func(k, v []byte) error {
if v == nil {
return nil // nil v means it's a bucket, skip
}
itemLength := make([]byte, 8)
binary.LittleEndian.PutUint64(itemLength, uint64(len(v)))
pNC.content <- CallResponse{s: string(itemLength) + string(k)}
return nil
})
})
}
if err != nil {
pNC.content <- CallResponse{e: err}
}
close(pNC.content)
}()
return pNC_Ref
}
//export Bolt_GetItem
func Bolt_GetItem(b ObjectReference, browse []string, key string) (int64, *C.char, int) {
var ret *C.char = nil
var ret_len = 0
err := withBrowse_ReadOnly(b, browse, func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error {
d := bucket.Get([]byte(key))
ret = C.CString(string(d))
ret_len = len(d)
return nil
})
if err != nil {
return err2triple(err)
}
return REAL_MESSAGE, ret, ret_len
}
//export GetNext
func GetNext(oRef ObjectReference) (int64, *C.char, int) {
pNC_Iface, ok := gms.Get(oRef)
if !ok {
return err2triple(NullObjectReference)
}
pNC, ok := pNC_Iface.(*NextCall)
if !ok {
return err2triple(NullObjectReference)
}
cr, ok := <-pNC.content
if !ok {
gms.Delete(oRef)
return err2triple(nil)
}
if cr.e != nil {
msg := cr.e.Error()
return ERROR_AND_KEEP_CALLING, C.CString(msg), len(msg)
}
return REAL_MESSAGE, C.CString(cr.s), len(cr.s)
}
//export Bolt_ListBucketsAtRoot
func Bolt_ListBucketsAtRoot(b ObjectReference) ObjectReference {
return Bolt_ListBuckets(b, nil)
}
//export Bolt_Close
func Bolt_Close(b ObjectReference) (*C.char, int) {
err := withBoltDBReference(b, func(db *bolt.DB) error {
return db.Close()
})
if err != nil {
msg := err.Error()
return C.CString(msg), len(msg)
}
gms.Delete(b)
return nil, 0
}
func main() { func main() {
// virtual
_ = qt.NewQApplication(os.Args)
qt.QGuiApplication_SetApplicationDisplayName("QBolt")
qt.QGuiApplication_SetWindowIcon(qt.NewQIcon4(":/rsrc/database_lightning.png"))
w := NewMainWindow()
w.ui.MainWindow.Show()
qt.QApplication_Exec()
} }

496
mainwindow.go Normal file
View File

@ -0,0 +1,496 @@
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)
}

View File

@ -269,12 +269,12 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>668</width> <width>668</width>
<height>21</height> <height>29</height>
</rect> </rect>
</property> </property>
<widget class="QMenu" name="menuFile"> <widget class="QMenu" name="menuFile">
<property name="title"> <property name="title">
<string>Fi&amp;le</string> <string>&amp;File</string>
</property> </property>
<addaction name="actionNew_database"/> <addaction name="actionNew_database"/>
<addaction name="actionOpen_database"/> <addaction name="actionOpen_database"/>

325
mainwindow_ui.go Normal file
View File

@ -0,0 +1,325 @@
// Generated by miqt-uic. To update this file, edit the .ui file in
// Qt Designer, and then run 'go generate'.
//
//go:generate miqt-uic -InFile mainwindow.ui -OutFile mainwindow_ui.go
package main
import (
"github.com/mappu/miqt/qt"
)
type MainWindowUi struct {
MainWindow *qt.QMainWindow
centralWidget *qt.QWidget
gridLayout *qt.QGridLayout
splitter *qt.QSplitter
bucketTree *qt.QTreeWidget
stackedWidget *qt.QStackedWidget
databasePage *qt.QWidget
gridLayout_4 *qt.QGridLayout
databaseTabWidget *qt.QTabWidget
databasePropertiesTab *qt.QWidget
gridLayout_2 *qt.QGridLayout
databasePropertiesArea *qt.QPlainTextEdit
bucketPage *qt.QWidget
gridLayout_3 *qt.QGridLayout
bucketTabWidget *qt.QTabWidget
bucketPropertiesTab *qt.QWidget
gridLayout_5 *qt.QGridLayout
bucketPropertiesArea *qt.QPlainTextEdit
bucketDataTab *qt.QWidget
gridLayout_6 *qt.QGridLayout
bucketData *qt.QTreeWidget
AddDataButton *qt.QPushButton
horizontalSpacer *qt.QSpacerItem
DeleteDataButton *qt.QPushButton
menuBar *qt.QMenuBar
menuFile *qt.QMenu
menuHelp *qt.QMenu
menuView *qt.QMenu
mainToolBar *qt.QToolBar
statusBar *qt.QStatusBar
actionAbout_qbolt *qt.QAction
actionAbout_Qt *qt.QAction
actionOpen_database *qt.QAction
actionExit *qt.QAction
actionDisconnect *qt.QAction
actionDelete_bucket *qt.QAction
actionRefresh_buckets *qt.QAction
actionClear_selection *qt.QAction
actionNew_database *qt.QAction
actionAdd_bucket *qt.QAction
actionOpen_database_as_read_only *qt.QAction
}
// NewMainWindowUi creates all Qt widget classes for MainWindow.
func NewMainWindowUi() *MainWindowUi {
ui := &MainWindowUi{}
ui.MainWindow = qt.NewQMainWindow2(nil)
ui.MainWindow.SetObjectName("MainWindow")
ui.MainWindow.Resize(668, 405)
ui.MainWindow.SetWindowTitle("QBolt")
icon0 := qt.NewQIcon()
icon0.AddFile4(":/rsrc/database_lightning.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.MainWindow.SetWindowIcon(icon0)
ui.actionAbout_qbolt = qt.NewQAction()
ui.actionAbout_qbolt.SetObjectName("actionAbout_qbolt")
icon1 := qt.NewQIcon()
icon1.AddFile4(":/rsrc/information.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionAbout_qbolt.SetIcon(icon1)
ui.actionAbout_Qt = qt.NewQAction()
ui.actionAbout_Qt.SetObjectName("actionAbout_Qt")
ui.actionOpen_database = qt.NewQAction()
ui.actionOpen_database.SetObjectName("actionOpen_database")
icon2 := qt.NewQIcon()
icon2.AddFile4(":/rsrc/database.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionOpen_database.SetIcon(icon2)
ui.actionExit = qt.NewQAction()
ui.actionExit.SetObjectName("actionExit")
icon3 := qt.NewQIcon()
icon3.AddFile4(":/rsrc/door_out.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionExit.SetIcon(icon3)
ui.actionDisconnect = qt.NewQAction()
ui.actionDisconnect.SetObjectName("actionDisconnect")
icon4 := qt.NewQIcon()
icon4.AddFile4(":/rsrc/disconnect.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionDisconnect.SetIcon(icon4)
ui.actionDelete_bucket = qt.NewQAction()
ui.actionDelete_bucket.SetObjectName("actionDelete_bucket")
icon5 := qt.NewQIcon()
icon5.AddFile4(":/rsrc/table_delete.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionDelete_bucket.SetIcon(icon5)
ui.actionRefresh_buckets = qt.NewQAction()
ui.actionRefresh_buckets.SetObjectName("actionRefresh_buckets")
icon6 := qt.NewQIcon()
icon6.AddFile4(":/rsrc/arrow_refresh.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionRefresh_buckets.SetIcon(icon6)
ui.actionClear_selection = qt.NewQAction()
ui.actionClear_selection.SetObjectName("actionClear_selection")
ui.actionNew_database = qt.NewQAction()
ui.actionNew_database.SetObjectName("actionNew_database")
icon7 := qt.NewQIcon()
icon7.AddFile4(":/rsrc/database_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionNew_database.SetIcon(icon7)
ui.actionAdd_bucket = qt.NewQAction()
ui.actionAdd_bucket.SetObjectName("actionAdd_bucket")
icon8 := qt.NewQIcon()
icon8.AddFile4(":/rsrc/table_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionAdd_bucket.SetIcon(icon8)
ui.actionOpen_database_as_read_only = qt.NewQAction()
ui.actionOpen_database_as_read_only.SetObjectName("actionOpen_database_as_read_only")
ui.centralWidget = qt.NewQWidget2(ui.MainWindow.QWidget)
ui.centralWidget.SetObjectName("centralWidget")
ui.gridLayout = qt.NewQGridLayout(ui.centralWidget)
ui.gridLayout.SetObjectName("gridLayout")
ui.gridLayout.SetContentsMargins(0, 0, 0, 0)
ui.gridLayout.SetSpacing(6)
ui.splitter = qt.NewQSplitter3(ui.centralWidget)
ui.splitter.SetObjectName("splitter")
ui.splitter.SetOrientation(qt.Horizontal)
ui.splitter.SetChildrenCollapsible(false)
ui.bucketTree = qt.NewQTreeWidget2(ui.splitter.QWidget)
ui.bucketTree.SetObjectName("bucketTree")
ui.bucketTree.SetContextMenuPolicy(qt.CustomContextMenu)
ui.bucketTree.SetUniformRowHeights(true)
ui.splitter.AddWidget(ui.bucketTree.QWidget)
ui.stackedWidget = qt.NewQStackedWidget2(ui.splitter.QWidget)
ui.stackedWidget.SetObjectName("stackedWidget")
ui.databasePage = qt.NewQWidget2(ui.stackedWidget.QWidget)
ui.databasePage.SetObjectName("databasePage")
ui.gridLayout_4 = qt.NewQGridLayout(ui.databasePage)
ui.gridLayout_4.SetObjectName("gridLayout_4")
ui.gridLayout_4.SetContentsMargins(0, 0, 0, 0)
ui.gridLayout_4.SetSpacing(6)
ui.databaseTabWidget = qt.NewQTabWidget2(ui.databasePage)
ui.databaseTabWidget.SetObjectName("databaseTabWidget")
ui.databasePropertiesTab = qt.NewQWidget2(ui.databaseTabWidget.QWidget)
ui.databasePropertiesTab.SetObjectName("databasePropertiesTab")
ui.gridLayout_2 = qt.NewQGridLayout(ui.databasePropertiesTab)
ui.gridLayout_2.SetObjectName("gridLayout_2")
ui.gridLayout_2.SetContentsMargins(3, 3, 3, 3)
ui.gridLayout_2.SetSpacing(6)
ui.databasePropertiesArea = qt.NewQPlainTextEdit3(ui.databasePropertiesTab)
ui.databasePropertiesArea.SetObjectName("databasePropertiesArea")
ui.databasePropertiesArea.SetFrameShape(qt.QFrame__NoFrame)
ui.databasePropertiesArea.SetReadOnly(true)
ui.gridLayout_2.AddWidget2(ui.databasePropertiesArea.QWidget, 0, 0)
icon9 := qt.NewQIcon()
icon9.AddFile4(":/rsrc/chart_bar.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.databaseTabWidget.AddTab2(ui.databasePropertiesTab, icon9, "")
ui.gridLayout_4.AddWidget2(ui.databaseTabWidget.QWidget, 0, 0)
ui.stackedWidget.AddWidget(ui.databasePage)
ui.bucketPage = qt.NewQWidget2(ui.stackedWidget.QWidget)
ui.bucketPage.SetObjectName("bucketPage")
ui.gridLayout_3 = qt.NewQGridLayout(ui.bucketPage)
ui.gridLayout_3.SetObjectName("gridLayout_3")
ui.gridLayout_3.SetContentsMargins(0, 0, 0, 0)
ui.gridLayout_3.SetSpacing(6)
ui.bucketTabWidget = qt.NewQTabWidget2(ui.bucketPage)
ui.bucketTabWidget.SetObjectName("bucketTabWidget")
ui.bucketPropertiesTab = qt.NewQWidget2(ui.bucketTabWidget.QWidget)
ui.bucketPropertiesTab.SetObjectName("bucketPropertiesTab")
ui.gridLayout_5 = qt.NewQGridLayout(ui.bucketPropertiesTab)
ui.gridLayout_5.SetObjectName("gridLayout_5")
ui.gridLayout_5.SetContentsMargins(3, 3, 3, 3)
ui.gridLayout_5.SetSpacing(6)
ui.bucketPropertiesArea = qt.NewQPlainTextEdit3(ui.bucketPropertiesTab)
ui.bucketPropertiesArea.SetObjectName("bucketPropertiesArea")
ui.bucketPropertiesArea.SetFrameShape(qt.QFrame__NoFrame)
ui.bucketPropertiesArea.SetReadOnly(true)
ui.gridLayout_5.AddWidget2(ui.bucketPropertiesArea.QWidget, 0, 0)
icon10 := qt.NewQIcon()
icon10.AddFile4(":/rsrc/chart_bar.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.bucketTabWidget.AddTab2(ui.bucketPropertiesTab, icon10, "")
ui.bucketDataTab = qt.NewQWidget2(ui.bucketTabWidget.QWidget)
ui.bucketDataTab.SetObjectName("bucketDataTab")
ui.gridLayout_6 = qt.NewQGridLayout(ui.bucketDataTab)
ui.gridLayout_6.SetObjectName("gridLayout_6")
ui.gridLayout_6.SetContentsMargins(3, 3, 3, 3)
ui.gridLayout_6.SetSpacing(6)
ui.bucketData = qt.NewQTreeWidget2(ui.bucketDataTab)
ui.bucketData.SetObjectName("bucketData")
ui.bucketData.SetSelectionMode(qt.QAbstractItemView__ExtendedSelection)
ui.bucketData.SetIndentation(0)
ui.bucketData.SetRootIsDecorated(false)
ui.bucketData.SetUniformRowHeights(true)
ui.bucketData.SetItemsExpandable(false)
ui.gridLayout_6.AddWidget3(ui.bucketData.QWidget, 0, 0, 1, 3)
ui.AddDataButton = qt.NewQPushButton4(ui.bucketDataTab)
ui.AddDataButton.SetObjectName("AddDataButton")
icon11 := qt.NewQIcon()
icon11.AddFile4(":/rsrc/add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.AddDataButton.SetIcon(icon11)
ui.gridLayout_6.AddWidget2(ui.AddDataButton.QWidget, 1, 0)
/* miqt-uic: no handler for spacer */
ui.DeleteDataButton = qt.NewQPushButton4(ui.bucketDataTab)
ui.DeleteDataButton.SetObjectName("DeleteDataButton")
icon12 := qt.NewQIcon()
icon12.AddFile4(":/rsrc/delete.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.DeleteDataButton.SetIcon(icon12)
ui.gridLayout_6.AddWidget2(ui.DeleteDataButton.QWidget, 1, 1)
icon13 := qt.NewQIcon()
icon13.AddFile4(":/rsrc/table.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.bucketTabWidget.AddTab2(ui.bucketDataTab, icon13, "")
ui.gridLayout_3.AddWidget2(ui.bucketTabWidget.QWidget, 0, 0)
ui.stackedWidget.AddWidget(ui.bucketPage)
ui.splitter.AddWidget(ui.stackedWidget.QWidget)
ui.gridLayout.AddWidget2(ui.splitter.QWidget, 0, 0)
ui.MainWindow.SetCentralWidget(ui.centralWidget) // Set central widget
ui.menuBar = qt.NewQMenuBar2(ui.MainWindow.QWidget)
ui.menuBar.SetObjectName("menuBar")
ui.menuBar.Resize(668, 29)
ui.menuFile = qt.NewQMenu3(ui.menuBar.QWidget)
ui.menuFile.SetObjectName("menuFile")
ui.menuFile.QWidget.AddAction(ui.actionNew_database)
ui.menuFile.QWidget.AddAction(ui.actionOpen_database)
ui.menuFile.QWidget.AddAction(ui.actionOpen_database_as_read_only)
ui.menuFile.AddSeparator()
ui.menuFile.QWidget.AddAction(ui.actionExit)
ui.menuHelp = qt.NewQMenu3(ui.menuBar.QWidget)
ui.menuHelp.SetObjectName("menuHelp")
ui.menuHelp.QWidget.AddAction(ui.actionAbout_qbolt)
ui.menuHelp.QWidget.AddAction(ui.actionAbout_Qt)
ui.menuView = qt.NewQMenu3(ui.menuBar.QWidget)
ui.menuView.SetObjectName("menuView")
ui.menuView.QWidget.AddAction(ui.actionClear_selection)
ui.menuBar.AddMenu(ui.menuFile)
ui.menuBar.AddMenu(ui.menuView)
ui.menuBar.AddMenu(ui.menuHelp)
ui.MainWindow.SetMenuBar(ui.menuBar)
ui.mainToolBar = qt.NewQToolBar4(ui.MainWindow.QWidget)
ui.mainToolBar.SetObjectName("mainToolBar")
ui.MainWindow.AddToolBar(qt.TopToolBarArea, ui.mainToolBar)
/* miqt-uic: no handler for mainToolBar attribute 'toolBarBreak' */
ui.mainToolBar.QWidget.AddAction(ui.actionNew_database)
ui.mainToolBar.QWidget.AddAction(ui.actionOpen_database)
ui.mainToolBar.AddSeparator()
ui.statusBar = qt.NewQStatusBar2(ui.MainWindow.QWidget)
ui.statusBar.SetObjectName("statusBar")
ui.MainWindow.SetStatusBar(ui.statusBar)
ui.Retranslate()
ui.stackedWidget.SetCurrentIndex(0)
ui.databaseTabWidget.SetCurrentIndex(0)
ui.bucketTabWidget.SetCurrentIndex(0)
return ui
}
// Retranslate reapplies all text translations.
func (ui *MainWindowUi) Retranslate() {
ui.actionAbout_qbolt.SetText(qt.QMainWindow_Tr("&About QBolt"))
ui.actionAbout_Qt.SetText(qt.QMainWindow_Tr("About &Qt"))
ui.actionOpen_database.SetText(qt.QMainWindow_Tr("&Open database..."))
ui.actionOpen_database.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("Ctrl+O")))
ui.actionExit.SetText(qt.QMainWindow_Tr("&Exit"))
ui.actionDisconnect.SetText(qt.QMainWindow_Tr("Disconnect"))
ui.actionDelete_bucket.SetText(qt.QMainWindow_Tr("Delete bucket"))
ui.actionRefresh_buckets.SetText(qt.QMainWindow_Tr("Refresh buckets"))
ui.actionClear_selection.SetText(qt.QMainWindow_Tr("&Clear selection"))
ui.actionNew_database.SetText(qt.QMainWindow_Tr("&New database..."))
ui.actionAdd_bucket.SetText(qt.QMainWindow_Tr("Add bucket..."))
ui.actionOpen_database_as_read_only.SetText(qt.QMainWindow_Tr("Open database as read-only..."))
ui.bucketTree.HeaderItem().SetText(0, qt.QTreeWidget_Tr("Bucket"))
ui.databaseTabWidget.SetTabText(ui.databaseTabWidget.IndexOf(ui.databasePropertiesTab), qt.QTabWidget_Tr("Database"))
ui.databasePropertiesArea.SetPlainText(qt.QWidget_Tr("No selection"))
ui.bucketTabWidget.SetTabText(ui.bucketTabWidget.IndexOf(ui.bucketPropertiesTab), qt.QTabWidget_Tr("Bucket"))
ui.bucketTabWidget.SetTabText(ui.bucketTabWidget.IndexOf(ui.bucketDataTab), qt.QTabWidget_Tr("Data"))
ui.bucketData.HeaderItem().SetText(0, qt.QTreeWidget_Tr("Key"))
ui.bucketData.HeaderItem().SetText(1, qt.QTreeWidget_Tr("Data length"))
ui.AddDataButton.SetText(qt.QWidget_Tr("Add..."))
ui.DeleteDataButton.SetText(qt.QWidget_Tr("Delete..."))
ui.menuFile.SetTitle(qt.QMenuBar_Tr("&File"))
ui.menuHelp.SetTitle(qt.QMenuBar_Tr("Help"))
ui.menuView.SetTitle(qt.QMenuBar_Tr("&View"))
}

View File

@ -1,219 +0,0 @@
#include "boltdb.h"
#include <QtEndian>
BoltDB::BoltDB()
{
}
BoltDB* BoltDB::createFrom(QString filePath, bool readOnly, QString &errorOut)
{
QByteArray filePathBytes(filePath.toUtf8());
GoString filePathGS = Interop::toGoString_WeakRef(&filePathBytes);
auto open_ret = ::Bolt_Open(readOnly, filePathGS);
if (open_ret.r2 != 0) {
errorOut = QString::fromUtf8(open_ret.r1, open_ret.r2);
free(open_ret.r1);
return nullptr;
}
BoltDB *ret = new BoltDB();
ret->gmsDbRef = open_ret.r0;
return ret;
}
static const int ERROR_AND_STOP_CALLING = 100;
static const int ERROR_AND_KEEP_CALLING = 101;
static const int FINISHED_OK = 102;
static const int REAL_MESSAGE = 103;
static bool handleTriple(int r0, char* r1, int64_t r2, QString& errorOut) {
if (r0 == ERROR_AND_STOP_CALLING) {
errorOut = QString::fromUtf8(r1, r2);
free(r1);
return false;
} else if (r0 == FINISHED_OK) {
return true;
} else {
// ?? unreachable
return false;
}
}
bool BoltDB::addBucket(const QList<QByteArray>& bucketPath, QByteArray bucketName, QString& errorOut)
{
GoSliceManagedWrapper browse(bucketPath);
GoString bucketNameGS = Interop::toGoString_WeakRef(&bucketName);
auto resp = ::Bolt_CreateBucket(this->gmsDbRef, browse.slice, bucketNameGS);
return handleTriple(resp.r0, resp.r1, resp.r2, errorOut);
}
bool BoltDB::deleteBucket(const QList<QByteArray>& bucketPath, QByteArray bucketName, QString& errorOut)
{
GoSliceManagedWrapper browse(bucketPath);
GoString bucketNameGS = Interop::toGoString_WeakRef(&bucketName);
auto resp = ::Bolt_DeleteBucket(this->gmsDbRef, browse.slice, bucketNameGS);
return handleTriple(resp.r0, resp.r1, resp.r2, errorOut);
}
bool BoltDB::setItem(const QList<QByteArray>& bucketPath, QByteArray keyName, QByteArray value, QString& errorOut)
{
GoSliceManagedWrapper browse(bucketPath);
GoString keyNameGS = Interop::toGoString_WeakRef(&keyName);
GoString valueGS = Interop::toGoString_WeakRef(&value);
auto resp = ::Bolt_SetItem(this->gmsDbRef, browse.slice, keyNameGS, valueGS);
return handleTriple(resp.r0, resp.r1, resp.r2, errorOut);
}
bool BoltDB::deleteItem(const QList<QByteArray>& bucketPath, QByteArray keyName, QString& errorOut)
{
GoSliceManagedWrapper browse(bucketPath);
GoString keyNameGS = Interop::toGoString_WeakRef(&keyName);
auto resp = ::Bolt_DeleteItem(this->gmsDbRef, browse.slice, keyNameGS);
return handleTriple(resp.r0, resp.r1, resp.r2, errorOut);
}
bool BoltDB::listBucketsAtRoot(QString& errorOut, NameReciever cb)
{
auto listJob = ::Bolt_ListBucketsAtRoot(this->gmsDbRef);
return pumpNext(listJob, errorOut, cb);
}
bool BoltDB::listBuckets(const QList<QByteArray>& bucketPath, QString& errorOut, NameReciever cb)
{
if (bucketPath.size() == 0) {
return listBucketsAtRoot(errorOut, cb);
}
GoSliceManagedWrapper browse(bucketPath);
auto listJob = ::Bolt_ListBuckets(this->gmsDbRef, browse.slice);
return pumpNext(listJob, errorOut, cb);
}
bool BoltDB::listKeys(const QList<QByteArray>& bucketPath, QString& errorOut, std::function<void(QByteArray, int64_t)> cb)
{
GoSliceManagedWrapper browse(bucketPath);
auto listJob = ::Bolt_ListItems(this->gmsDbRef, browse.slice);
return pumpNext(listJob, errorOut, [=](QByteArray b) {
// First 8 bytes are little-endian uint64 len
int64_t dataLen = qFromLittleEndian<qint64>(b.mid(0, 8));
cb(b.mid(8), dataLen);
});
}
bool BoltDB::getData(const QList<QByteArray>& bucketPath, QByteArray key, std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError)
{
GoSliceManagedWrapper browse(bucketPath);
GoString keyGS = Interop::toGoString_WeakRef(&key);
auto resp = ::Bolt_GetItem(this->gmsDbRef, browse.slice, keyGS);
if (resp.r0 == ERROR_AND_STOP_CALLING) {
onError(QString::fromUtf8(resp.r1, resp.r2));
free(resp.r1);
return false;
} else if (resp.r0 == REAL_MESSAGE) {
onSuccess(QByteArray(resp.r1, resp.r2));
free(resp.r1);
return true;
} else {
// ?? unreachable
return false;
}
}
bool BoltDB::pumpNext(GoInt64 jobRef, QString& errorOut, NameReciever cb)
{
errorOut.clear();
for(;;) {
auto gnr = ::GetNext(jobRef);
if (gnr.r0 == ERROR_AND_STOP_CALLING) {
errorOut.append(QString::fromUtf8(gnr.r1, gnr.r2)); // log error
free(gnr.r1);
break; // done
} else if (gnr.r0 == ERROR_AND_KEEP_CALLING) {
errorOut.append(QString::fromUtf8(gnr.r1, gnr.r2)); // log error
free(gnr.r1);
continue;
} else if (gnr.r0 == FINISHED_OK) {
// Once we hit this, the go-side will clean up the channel / associated goroutines
break;
} else if (gnr.r0 == REAL_MESSAGE) {
cb(QByteArray(gnr.r1, gnr.r2));
free(gnr.r1);
continue;
}
}
return (errorOut.length() == 0);
}
bool BoltDB::getStatsJSON(std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError)
{
auto statresp = Bolt_DBStats(this->gmsDbRef);
if (statresp.r0 == ERROR_AND_STOP_CALLING) {
onError(QString::fromUtf8(statresp.r1, statresp.r2));
free(statresp.r1);
return false;
} else if (statresp.r0 == REAL_MESSAGE) {
onSuccess(QByteArray(statresp.r1, statresp.r2));
free(statresp.r1);
return true;
} else {
// ?? shouldn't be reachable
return false;
}
}
bool BoltDB::getBucketStatsJSON(const QList<QByteArray>& bucketPath, std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError)
{
GoSliceManagedWrapper sliceWrapper(bucketPath);
auto statresp = Bolt_BucketStats(this->gmsDbRef, sliceWrapper.slice);
if (statresp.r0 == ERROR_AND_STOP_CALLING) {
QString err = QString::fromUtf8(statresp.r1, statresp.r2);
free(statresp.r1);
onError(err);
return false;
} else if (statresp.r0 == REAL_MESSAGE) {
onSuccess(QByteArray(statresp.r1, statresp.r2));
free(statresp.r1);
return true;
} else {
// ?? shouldn't be reachable
return false;
}
}
BoltDB::~BoltDB()
{
auto err = ::Bolt_Close(this->gmsDbRef);
if (err.r1 != 0) {
// Error closing database!
// Need to display an alert... somewhere
free(err.r0);
}
}

View File

@ -1,46 +0,0 @@
#ifndef BOLTDB_H
#define BOLTDB_H
#include "interop.h"
#include <functional>
typedef std::function<void(QByteArray)> NameReciever;
class BoltDB
{
protected:
BoltDB();
GoInt64 gmsDbRef;
public:
static BoltDB* createFrom(QString filePath, bool readOnly, QString &errorOut);
bool listBucketsAtRoot(QString& errorOut, NameReciever cb);
bool listBuckets(const QList<QByteArray>& bucketPath, QString& errorOut, NameReciever cb);
bool addBucket(const QList<QByteArray>& bucketPath, QByteArray bucketName, QString& errorOut);
bool deleteBucket(const QList<QByteArray>& bucketPath, QByteArray bucketName, QString& errorOut);
bool setItem(const QList<QByteArray>& bucketPath, QByteArray keyName, QByteArray value, QString& errorOut);
bool deleteItem(const QList<QByteArray>& bucketPath, QByteArray keyName, QString& errorOut);
bool listKeys(const QList<QByteArray>& bucketPath, QString& errorOut, std::function<void(QByteArray, int64_t)> cb);
bool getData(const QList<QByteArray>& bucketPath, QByteArray key, std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError);
bool getStatsJSON(std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError);
bool getBucketStatsJSON(const QList<QByteArray>& bucketPath, std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError);
~BoltDB();
protected:
bool pumpNext(GoInt64 jobRef, QString& errorOut, NameReciever cb);
};
#endif // BOLTDB_H

View File

@ -1,42 +0,0 @@
#include "interop.h"
#include <QStringList>
Interop::Interop()
{
}
GoString Interop::toGoString_WeakRef(QByteArray *qba) {
return GoString{qba->data(), qba->length()};
}
int64_t Interop::GetMagic() {
return ::GetMagic();
}
//
GoSliceManagedWrapper::GoSliceManagedWrapper(const QList<QByteArray>& qsl) :
rawStrings(),
slice(),
strings(nullptr)
{
rawStrings.reserve(qsl.size());
strings = new GoString[qsl.size()];
for (int i = 0; i < qsl.size(); ++i) {
rawStrings.push_back( qsl.at(i) );
strings[i].p = rawStrings[i].data();
strings[i].n = rawStrings[i].size();
}
slice.data = static_cast<void*>(strings);
slice.len = qsl.size(); // * sizeof(GoString);
slice.cap = slice.len;
}
GoSliceManagedWrapper::~GoSliceManagedWrapper()
{
delete[] strings;
}

View File

@ -1,31 +0,0 @@
#ifndef INTEROP_H
#define INTEROP_H
#include "qbolt_cgo.h"
#include <QString>
#include <QList>
class GoSliceManagedWrapper {
Q_DISABLE_COPY(GoSliceManagedWrapper)
public:
GoSliceManagedWrapper(const QList<QByteArray>& qsl);
~GoSliceManagedWrapper();
protected:
QList<QByteArray> rawStrings;
public:
GoSlice slice;
GoString *strings;
};
class Interop
{
public:
Interop();
static GoString toGoString_WeakRef(QByteArray *qba);
static int64_t GetMagic();
};
#endif // INTEROP_H

View File

@ -1,18 +0,0 @@
#include "itemwindow.h"
#include "ui_itemwindow.h"
ItemWindow::ItemWindow(QWidget *parent) :
QDialog(parent),
ui(new Ui::ItemWindow)
{
ui->setupUi(this);
}
ItemWindow::~ItemWindow()
{
delete ui;
}
QPlainTextEdit* ItemWindow::ContentArea() const {
return ui->contentArea;
}

View File

@ -1,25 +0,0 @@
#ifndef ITEMWINDOW_H
#define ITEMWINDOW_H
#include <QDialog>
#include <QPlainTextEdit>
namespace Ui {
class ItemWindow;
}
class ItemWindow : public QDialog
{
Q_OBJECT
public:
explicit ItemWindow(QWidget *parent = 0);
~ItemWindow();
QPlainTextEdit* ContentArea() const;
private:
Ui::ItemWindow *ui;
};
#endif // ITEMWINDOW_H

View File

@ -1,23 +0,0 @@
#include "mainwindow.h"
#include <QApplication>
#include "interop.h"
#include <QDebug>
int main(int argc, char *argv[])
{
int64_t magic = Interop::GetMagic();
if (magic != 0x10203040) {
qDebug() << "bad magic " << magic;
return 1;
}
QApplication a(argc, argv);
QApplication::setApplicationDisplayName("QBolt");
QApplication::setWindowIcon(QIcon(":/rsrc/database_lightning.png"));
MainWindow w;
w.show();
return a.exec();
}

View File

@ -1,478 +0,0 @@
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "itemwindow.h"
#include "boltdb.h"
#include <QFileDialog>
#include <QMessageBox>
#include <QJsonDocument>
#include <QInputDialog>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
on_bucketTree_currentItemChanged(nullptr, nullptr);
databaseContext = new QMenu();
databaseContext->addAction(ui->actionRefresh_buckets);
databaseContext->addAction(ui->actionAdd_bucket);
databaseContext->addSeparator();
databaseContext->addAction(ui->actionDisconnect);
bucketContext = new QMenu();
bucketContext->addAction(ui->actionRefresh_buckets);
bucketContext->addAction(ui->actionAdd_bucket);
bucketContext->addSeparator();
bucketContext->addAction(ui->actionDelete_bucket);
}
MainWindow::~MainWindow()
{
delete ui;
}
static const int BdbPointerRole = Qt::UserRole + 1;
static const int BinaryDataRole = Qt::UserRole + 2;
#define SET_BDB(top, bdb) top->setData(0, BdbPointerRole, QVariant::fromValue<void*>(static_cast<void*>(bdb)))
#define GET_BDB(top) static_cast<BoltDB*>( top->data(0, BdbPointerRole).value<void*>() )
void MainWindow::on_actionNew_database_triggered()
{
QString file = QFileDialog::getSaveFileName(this, tr("Save new bolt database as..."));
if (file.length()) {
openDatabase(file, false);
}
}
void MainWindow::on_actionOpen_database_triggered()
{
QString file = QFileDialog::getOpenFileName(this, tr("Select bolt database..."));
if (file.length()) {
openDatabase(file, false);
}
}
void MainWindow::on_actionOpen_database_as_read_only_triggered()
{
QString file = QFileDialog::getOpenFileName(this, tr("Select bolt database..."));
if (file.length()) {
openDatabase(file, true);
}
}
void MainWindow::openDatabase(QString file, bool readOnly)
{
// Open
QString error;
auto *bdb = BoltDB::createFrom(file, readOnly, error);
if (bdb == nullptr) {
QMessageBox qmb;
qmb.setText(tr("Error opening database: %1").arg(error));
qmb.exec();
return;
}
QTreeWidgetItem *top = new QTreeWidgetItem();
top->setText(0, QFileInfo(file).fileName());
top->setIcon(0, QIcon(":/rsrc/database.png"));
SET_BDB(top, bdb);
ui->bucketTree->addTopLevelItem(top);
refreshBucketTree(top);
ui->bucketTree->setCurrentItem(top);
ui->bucketTree->expandItem(top);
}
static const QString getDisplayName(const QByteArray &qba) {
// FIXME the formatting isn't so great when control characters, etc. are used
// A C-style escape display, or the unicode-replacement-character would be preferable
QString ret(QString::fromUtf8(qba));
bool allPrintable = true;
for (auto i = ret.begin(), e = ret.end(); i != e; ++i) {
if (! i->isPrint()) {
allPrintable = false;
break;
}
}
if (allPrintable) {
return ret; // fine
}
// Some of the characters weren't printable.
// Build up a replacement string
QString replacement;
for (auto i = ret.begin(), e = ret.end(); i != e; ++i) {
replacement += i->isPrint() ? *i : QStringLiteral("\\u{%1}").arg(i->unicode());
}
return replacement;
}
void MainWindow::refreshBucketTree(QTreeWidgetItem* itm)
{
QTreeWidgetItem *top = itm;
QList<QByteArray> browsePath;
while(top->parent() != nullptr) {
browsePath.push_front(top->data(0, BinaryDataRole).toByteArray());
top = top->parent();
}
// Remove existing children
for (int i = itm->childCount(); i --> 0;) {
delete itm->takeChild(i);
}
auto *bdb = GET_BDB(top);
QString error;
bool ok = bdb->listBuckets(
browsePath,
error,
[=](QByteArray qba){
QTreeWidgetItem *child = new QTreeWidgetItem();
child->setText(0, getDisplayName(qba));
child->setData(0, BinaryDataRole, qba);
child->setIcon(0, QIcon(":/rsrc/table.png"));
itm->addChild(child);
refreshBucketTree(child);
}
);
if (!ok) {
QMessageBox qmb;
qmb.setText(tr("Error listing buckets: %1").arg(error));
qmb.exec();
// (continue)
}
}
void MainWindow::on_actionExit_triggered()
{
close();
}
void MainWindow::on_actionAbout_Qt_triggered()
{
QApplication::aboutQt();
}
void MainWindow::on_actionAbout_qbolt_triggered()
{
QMessageBox::about(
this,
QApplication::applicationDisplayName(),
"<b>QBolt</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>"
);
}
void MainWindow::on_actionDisconnect_triggered()
{
QTreeWidgetItem *top = lastContextSelection;
if (top->parent()) {
return; // somehow we didn't select a top-level item
}
auto *bdb = GET_BDB(top);
// Remove UI
ui->bucketTree->clearSelection();
delete top;
// Disconnect from DB
delete bdb;
}
void MainWindow::on_bucketTree_customContextMenuRequested(const QPoint &pos)
{
auto *itm = ui->bucketTree->itemAt(pos);
if (itm == nullptr) {
return;
}
lastContextSelection = itm;
if (itm->parent() != nullptr) {
// Child item, show the bucket menu
bucketContext->popup(ui->bucketTree->mapToGlobal(pos));
} else {
// Top-level item, show the database menu
databaseContext->popup(ui->bucketTree->mapToGlobal(pos));
}
}
void MainWindow::on_actionRefresh_buckets_triggered()
{
refreshBucketTree(lastContextSelection);
}
void MainWindow::on_bucketTree_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous)
{
Q_UNUSED(previous);
if (current == nullptr) {
ui->stackedWidget->setVisible(false);
return;
}
ui->stackedWidget->setVisible(true);
if (current->parent() == nullptr) {
// Selected a database
ui->stackedWidget->setCurrentWidget(ui->databasePage);
ui->databasePropertiesArea->clear();
auto *bdb = GET_BDB(current);
bdb->getStatsJSON(
[=](QByteArray j) {
auto doc = QJsonDocument::fromJson(j);
ui->databasePropertiesArea->setPlainText(QString::fromUtf8(doc.toJson(QJsonDocument::Indented)));
},
[=](QString error) {
ui->databasePropertiesArea->setPlainText(tr("Error retrieving database statistics: %1").arg(error));
}
);
// Clean up foreign areas
ui->bucketPropertiesArea->clear();
ui->bucketData->clear();
} else {
// Selected a bucket
ui->stackedWidget->setCurrentWidget(ui->bucketPage);
ui->bucketPropertiesArea->clear();
QList<QByteArray> browse;
QTreeWidgetItem *top = current;
while (top->parent() != nullptr) {
browse.push_front(top->data(0, BinaryDataRole).toByteArray());
top = top->parent();
}
auto *bdb = GET_BDB(top);
bdb->getBucketStatsJSON(
browse,
[=](QByteArray j) {
auto doc = QJsonDocument::fromJson(j);
ui->bucketPropertiesArea->setPlainText(QString::fromUtf8(doc.toJson(QJsonDocument::Indented)));
},
[=](QString error) {
ui->bucketPropertiesArea->setPlainText(tr("Error retrieving bucket statistics: %1").arg(error));
}
);
// Load the data tab
refreshData(bdb, browse);
// Clean up foreign areas
ui->databasePropertiesArea->clear();
}
}
void MainWindow::refreshData(BoltDB *bdb, const QList<QByteArray>& browse)
{
// Load the data tab
ui->bucketData->clear();
QString err;
bool ok = bdb->listKeys(browse, err, [=](QByteArray name, int64_t dataLen) {
auto *itm = new QTreeWidgetItem();
itm->setText(0, getDisplayName(name));
itm->setData(0, BinaryDataRole, name);
itm->setText(1, QString("%1").arg(dataLen));
ui->bucketData->addTopLevelItem(itm);
});
if (! ok) {
QMessageBox qmb;
qmb.setText(tr("Error listing bucket content: %1").arg(err));
qmb.exec();
}
ui->bucketData->resizeColumnToContents(0);
on_bucketData_itemSelectionChanged();
}
void MainWindow::on_actionClear_selection_triggered()
{
ui->bucketTree->setCurrentItem(nullptr);
}
#define GET_ITM_TOP_BROWSE_BDB \
QTreeWidgetItem* itm = ui->bucketTree->currentItem(); \
if (itm == nullptr) { \
return; \
} \
QTreeWidgetItem* top = itm; \
QList<QByteArray> browse; \
while(top->parent() != nullptr) { \
browse.push_front(top->data(0, BinaryDataRole).toByteArray()); \
top = top->parent(); \
} \
auto *bdb = GET_BDB(top);
void MainWindow::openEditor(BoltDB *bdb, const QList<QByteArray>& saveAs, QByteArray saveAsKey, QByteArray currentContent)
{
auto iw = new ItemWindow();
iw->ContentArea()->setPlainText(QString::fromUtf8(currentContent));
iw->setWindowTitle(QString::fromUtf8(saveAsKey));
iw->setWindowModality(Qt::ApplicationModal); // we need this - otherwise we'll refresh a possibly-changed area after saving
connect(iw, &ItemWindow::finished, iw, [=](int exitCode){
if (exitCode == ItemWindow::Accepted) {
QString err;
if (! bdb->setItem(saveAs, saveAsKey, iw->ContentArea()->toPlainText().toUtf8(), err)) {
QMessageBox qmb;
qmb.setText(tr("Error saving item content: %1").arg(err));
qmb.exec();
}
refreshData(bdb, saveAs);
}
iw->deleteLater();
});
iw->show();
}
void MainWindow::on_bucketData_doubleClicked(const QModelIndex &index)
{
GET_ITM_TOP_BROWSE_BDB;
// Get item key
auto model = index.model();
const QByteArray& key = model->data(model->index(index.row(), 0), BinaryDataRole).toByteArray();
// DB lookup
bdb->getData(
browse,
key,
[=](QByteArray content) {
openEditor(bdb, browse, key, content);
},
[=](QString error) {
QMessageBox qmb;
qmb.setText(tr("Error loading item content: %1").arg(error));
qmb.exec();
}
);
}
void MainWindow::on_actionAdd_bucket_triggered()
{
GET_ITM_TOP_BROWSE_BDB;
// Prompt for bucket name
QString name = QInputDialog::getText(this, tr("New bucket"), tr("Enter a key for the new bucket:"));
if (name.length() == 0) {
return;
}
// Create
QString err;
if (! bdb->addBucket(browse, name.toUtf8(), err)) {
QMessageBox qmb;
qmb.setText(tr("Error creating bucket: %1").arg(err));
qmb.exec();
return;
}
// Refresh bucket list
refreshBucketTree(itm); // sub-tree only
ui->bucketTree->expandItem(itm);
}
void MainWindow::on_actionDelete_bucket_triggered()
{
GET_ITM_TOP_BROWSE_BDB;
// Prompt for confirmation
const QByteArray& bucketToDelete = itm->data(0, BinaryDataRole).toByteArray();
if (
QMessageBox::question(
this,
tr("Delete bucket"),
tr("Are you sure you want to remove the bucket '%1'?").arg(getDisplayName(bucketToDelete)),
QMessageBox::Yes,
QMessageBox::Cancel
) != QMessageBox::Yes
) {
return;
}
QTreeWidgetItem* parent = itm->parent();
// One level down
browse.pop_back();
QString err;
if (! bdb->deleteBucket(browse, bucketToDelete, err)) {
QMessageBox qmb;
qmb.setText(tr("Error removing bucket: %1").arg(err));
qmb.exec();
return;
}
// Refresh bucket list
refreshBucketTree(parent); // sub-tree only
ui->bucketTree->expandItem(parent);
ui->bucketTree->setCurrentItem(parent);
}
void MainWindow::on_AddDataButton_clicked()
{
GET_ITM_TOP_BROWSE_BDB;
// Prompt for bucket name
QString name = QInputDialog::getText(this, tr("New item"), tr("Enter a key for the new item:"));
if (name.length() == 0) {
return;
}
openEditor(bdb, browse, name.toUtf8(), QByteArray());
}
void MainWindow::on_DeleteDataButton_clicked()
{
GET_ITM_TOP_BROWSE_BDB;
auto selection = ui->bucketData->selectedItems();
if (selection.length() == 0) {
return; // nothing to do
}
// Prompt for confirmation
if (QMessageBox::question(this, tr("Delete items"), tr("Are you sure you want to remove %1 item(s)?").arg(selection.length()), QMessageBox::Yes, QMessageBox::Cancel) != QMessageBox::Yes) {
return;
}
QString err;
for (int i = selection.length(); i-->0;) {
if (! bdb->deleteItem(browse, selection[i]->data(0, BinaryDataRole).toByteArray(), err)) {
QMessageBox qmb;
qmb.setText(tr("Error removing item: %1").arg(err));
qmb.exec();
}
}
refreshData(bdb, browse);
}
void MainWindow::on_bucketData_itemSelectionChanged()
{
ui->DeleteDataButton->setEnabled( (ui->bucketData->selectedItems().size() > 0) );
}

View File

@ -1,70 +0,0 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include "boltdb.h"
#include <QMainWindow>
#include <QMenu>
#include <QTreeWidgetItem>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
void on_actionOpen_database_triggered();
void on_actionExit_triggered();
void on_actionAbout_Qt_triggered();
void on_actionAbout_qbolt_triggered();
void on_actionDisconnect_triggered();
void on_bucketTree_customContextMenuRequested(const QPoint &pos);
void on_actionRefresh_buckets_triggered();
void on_bucketTree_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous);
void on_actionClear_selection_triggered();
void on_bucketData_doubleClicked(const QModelIndex &index);
void on_actionNew_database_triggered();
void on_actionAdd_bucket_triggered();
void on_actionDelete_bucket_triggered();
void on_AddDataButton_clicked();
void on_DeleteDataButton_clicked();
void on_bucketData_itemSelectionChanged();
void on_actionOpen_database_as_read_only_triggered();
protected:
void openDatabase(QString file, bool readOnly);
void refreshBucketTree(QTreeWidgetItem* top);
void refreshData(BoltDB *bdb, const QList<QByteArray>& browse);
void openEditor(BoltDB *bdb, const QList<QByteArray>& saveAs, QByteArray saveAsKey, QByteArray currentContent);
private:
Ui::MainWindow *ui;
QMenu *databaseContext;
QMenu *bucketContext;
QTreeWidgetItem* lastContextSelection;
};
#endif // MAINWINDOW_H

View File

@ -1,48 +0,0 @@
#-------------------------------------------------
#
# Project created by QtCreator 2017-05-15T19:38:33
#
#-------------------------------------------------
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = qbolt
TEMPLATE = app
# Enforce Qt deprecations
DEFINES += QT_DEPRECATED_WARNINGS
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000
win32: {
# for some reason, qbolt_cgo.h never realises that Q_OS_WIN is defined for win32 builds... weird
DEFINES += CGO_WINDOWS
QMAKE_LIBS += $$_PRO_FILE_PWD_/../build/win32/qbolt.a
QMAKE_LIBS += -lntdll
RC_ICONS = rsrc/qbolt.ico
}
linux: {
QMAKE_LIBS += $$_PRO_FILE_PWD_/../build/linux/qbolt.a
QMAKE_LIBS += -lpthread
}
SOURCES += main.cpp\
mainwindow.cpp \
interop.cpp \
boltdb.cpp \
itemwindow.cpp
HEADERS += mainwindow.h \
interop.h \
boltdb.h \
qbolt_cgo.h \
itemwindow.h
FORMS += mainwindow.ui \
itemwindow.ui
RESOURCES += \
resources.qrc

View File

@ -1,10 +0,0 @@
#ifndef QBOLT_CGO_H
#define QBOLT_CGO_H
#if defined(Q_OS_WIN) || defined(CGO_WINDOWS)
#include "../build/win32/qbolt.h"
#else
#include "../build/linux/qbolt.h"
#endif
#endif // QBOLT_CGO_H

17
resources.go Normal file
View File

@ -0,0 +1,17 @@
package main
//go:generate miqt-rcc.sh resources.qrc
import (
"embed"
"github.com/mappu/miqt/qt"
)
//go:embed resources.rcc
var _resourceRcc []byte
func init() {
_ = embed.FS{}
qt.QResource_RegisterResourceWithRccData(&_resourceRcc[0])
}

BIN
resources.rcc Normal file

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 733 B

After

Width:  |  Height:  |  Size: 733 B

View File

Before

Width:  |  Height:  |  Size: 685 B

After

Width:  |  Height:  |  Size: 685 B

View File

Before

Width:  |  Height:  |  Size: 541 B

After

Width:  |  Height:  |  Size: 541 B

View File

Before

Width:  |  Height:  |  Size: 390 B

After

Width:  |  Height:  |  Size: 390 B

View File

Before

Width:  |  Height:  |  Size: 658 B

After

Width:  |  Height:  |  Size: 658 B

View File

Before

Width:  |  Height:  |  Size: 763 B

After

Width:  |  Height:  |  Size: 763 B

View File

Before

Width:  |  Height:  |  Size: 775 B

After

Width:  |  Height:  |  Size: 775 B

View File

Before

Width:  |  Height:  |  Size: 715 B

After

Width:  |  Height:  |  Size: 715 B

View File

Before

Width:  |  Height:  |  Size: 796 B

After

Width:  |  Height:  |  Size: 796 B

View File

Before

Width:  |  Height:  |  Size: 688 B

After

Width:  |  Height:  |  Size: 688 B

View File

Before

Width:  |  Height:  |  Size: 778 B

After

Width:  |  Height:  |  Size: 778 B

View File

Before

Width:  |  Height:  |  Size: 635 B

After

Width:  |  Height:  |  Size: 635 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 566 B

After

Width:  |  Height:  |  Size: 566 B

View File

Before

Width:  |  Height:  |  Size: 663 B

After

Width:  |  Height:  |  Size: 663 B

View File

Before

Width:  |  Height:  |  Size: 660 B

After

Width:  |  Height:  |  Size: 660 B

24
util.go Normal file
View File

@ -0,0 +1,24 @@
package main
import (
"unsafe"
"github.com/mappu/miqt/qt"
)
// ReverseSlice reverses a slice.
// @ref https://stackoverflow.com/a/28058324
func ReverseSlice[S ~[]E, E any](s S) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
func MakeQByteArray(s string) *qt.QByteArray {
return qt.NewQByteArray7(s, len(s))
}
func FromQByteArray(qba *qt.QByteArray) string {
var rawData []byte = unsafe.Slice((*byte)(qba.Data()), qba.Length())
return string(rawData) // copy
}

17
util_test.go Normal file
View File

@ -0,0 +1,17 @@
package main
import (
"testing"
)
func TestQByteArray(t *testing.T) {
foo := "the \x00 quick \x01 brown \x02 fox \x03 jumps \x04 over \x05 the \x06 lazy \x07 dog"
qb := MakeQByteArray(foo)
out := FromQByteArray(qb)
if foo != out {
t.Errorf("Expected equal")
}
}

View File

@ -0,0 +1,50 @@
{
"RT_GROUP_ICON": {
"APP": {
"0000": "rsrc/database_lightning.png"
}
},
"RT_VERSION": {
"#1": {
"0000": {
"fixed": {
"file_version": "%VERSION%.0",
"product_version": "%VERSION%.0"
},
"info": {
"0409": {
"FileDescription": "QBolt Database Viewer",
"FileVersion": "%VERSION%.0",
"ProductName": "QBolt Database Viewer",
"ProductVersion": "%VERSION%.0"
}
}
}
}
},
"RT_MANIFEST": {
"#1": {
"0409": {
"identity": {
"name": "",
"version": ""
},
"description": "",
"minimum-os": "win7",
"execution-level": "as invoker",
"ui-access": false,
"auto-elevate": false,
"dpi-awareness": "per monitor v2",
"disable-theming": false,
"disable-window-filtering": false,
"high-resolution-scrolling-aware": false,
"ultra-high-resolution-scrolling-aware": false,
"long-path-aware": false,
"printer-driver-isolation": false,
"gdi-scaling": false,
"segment-heap": false,
"use-common-controls-v6": true
}
}
}
}