Compare commits
	
		
			26 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 18331ae007 | |||
| f08242e93c | |||
| 23965230e2 | |||
| e43b261752 | |||
| f9b4cb71a5 | |||
| 96c05641bd | |||
| 2c5d1946ec | |||
| a6cbc5a9ed | |||
| 6008ae44a2 | |||
| 99096b2360 | |||
| 0e92459779 | |||
| a13eaaf523 | |||
| dca8d277d3 | |||
| 15c739c0b9 | |||
| fc2da972ef | |||
| e45eea7111 | |||
| 1e62d79c07 | |||
| c74c5ae5c0 | |||
| b0092c4a6e | |||
| 5ce6368c4a | |||
| 3e7e54da4b | |||
| 9f80d687b2 | |||
| 96411e877f | |||
| ac7e078c02 | |||
| 6fb8d1ba0c | |||
| 8d8374be24 | 
							
								
								
									
										16
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,16 @@
 | 
			
		||||
# development
 | 
			
		||||
qbolt
 | 
			
		||||
dummy-data/dummy-data*
 | 
			
		||||
 | 
			
		||||
# 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-*
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
mode:regexp
 | 
			
		||||
^build-qbolt-
 | 
			
		||||
^dummy-data/dummy-data$
 | 
			
		||||
\.pro\.user$
 | 
			
		||||
^build/
 | 
			
		||||
							
								
								
									
										94
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						@@ -1,68 +1,60 @@
 | 
			
		||||
 | 
			
		||||
export PATH := /usr/lib/mxe/usr/bin:$(PATH)
 | 
			
		||||
GOFLAGS := -ldflags='-s -w' -gcflags='-trimpath=$(CURDIR)' -asmflags='-trimpath=$(CURDIR)'
 | 
			
		||||
VERSION := 1.0.1
 | 
			
		||||
VERSION := 1.0.3
 | 
			
		||||
GOFLAGS_L := -ldflags='-s -w -X main.Version=v$(VERSION)' -buildvcs=false -gcflags='-trimpath=$(CURDIR)' -asmflags='-trimpath=$(CURDIR)'
 | 
			
		||||
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: \
 | 
			
		||||
	build/dist/qbolt-${VERSION}-win32.zip \
 | 
			
		||||
	build/dist/qbolt-${VERSION}-src.tar.gz \
 | 
			
		||||
	build/dist/qbolt-${VERSION}-linux_amd64.tar.xz
 | 
			
		||||
.PHONY: dist
 | 
			
		||||
dist: build/qbolt-${VERSION}-windows-x86_64.zip build/qbolt-${VERSION}-debian12-x86_64.tar.xz
 | 
			
		||||
 | 
			
		||||
.PHONY: clean
 | 
			
		||||
clean:
 | 
			
		||||
	if [ -f qbolt/qbolt.a ] ; then rm qbolt/qbolt.a ; fi
 | 
			
		||||
	if [ -f qbolt ] ; then rm qbolt ; fi
 | 
			
		||||
	if [ -d build ] ; then rm -r build ; fi
 | 
			
		||||
	rm -f qbolt || true
 | 
			
		||||
	rm -rf build || true
 | 
			
		||||
	rm -f windows-manifest.json || true
 | 
			
		||||
	
 | 
			
		||||
# Build core golang shared library (linux)
 | 
			
		||||
# Generated files
 | 
			
		||||
	
 | 
			
		||||
build/linux/qbolt.a: *.go
 | 
			
		||||
	mkdir -p build/linux
 | 
			
		||||
	go build ${GOFLAGS} -buildmode=c-archive -o build/linux/qbolt.a
 | 
			
		||||
resources.rcc resources.go: resources.qrc
 | 
			
		||||
	$(MIQT_RCC) resources.qrc
 | 
			
		||||
	
 | 
			
		||||
# Build core golang shared library (win32)
 | 
			
		||||
mainwindow_ui.go: mainwindow.ui
 | 
			
		||||
	$(MIQT_UIC) -InFile mainwindow.ui -OutFile mainwindow_ui.go
 | 
			
		||||
 | 
			
		||||
build/win32/qbolt.a: *.go
 | 
			
		||||
	mkdir -p build/win32
 | 
			
		||||
	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
 | 
			
		||||
itemwindow_ui.go: itemwindow.ui
 | 
			
		||||
	$(MIQT_UIC) -InFile itemwindow.ui -OutFile itemwindow_ui.go
 | 
			
		||||
	
 | 
			
		||||
# Linux binaries
 | 
			
		||||
windows-manifest.json: windows-manifest.template.json Makefile
 | 
			
		||||
	cat windows-manifest.template.json | sed -re 's_%VERSION%_$(VERSION)_' > windows-manifest.json
 | 
			
		||||
	
 | 
			
		||||
build/linux/qbolt: build/linux/qbolt.a qbolt/*
 | 
			
		||||
	cd build/linux && qmake ../../qbolt/qbolt.pro && make
 | 
			
		||||
rsrc_windows_amd64.syso: windows-manifest.json
 | 
			
		||||
	$(GO_WINRES) make --in windows-manifest.json
 | 
			
		||||
	rm rsrc_windows_386.syso || true # we do not build x86_32
 | 
			
		||||
	
 | 
			
		||||
# Linux distribution
 | 
			
		||||
# Linux release
 | 
			
		||||
	
 | 
			
		||||
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
 | 
			
		||||
build/qbolt: $(SOURCES)
 | 
			
		||||
	go build $(GOFLAGS_L) -o build/qbolt
 | 
			
		||||
	upx build/qbolt
 | 
			
		||||
 | 
			
		||||
# Windows binaries
 | 
			
		||||
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
 | 
			
		||||
	
 | 
			
		||||
build/win32/release/qbolt.exe: build/win32/qbolt.a qbolt/*
 | 
			
		||||
	cd build/win32 && i686-w64-mingw32.static-qmake-qt5 ../../qbolt/qbolt.pro && make
 | 
			
		||||
# Windows release (docker)
 | 
			
		||||
 | 
			
		||||
# Windows distribution
 | 
			
		||||
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/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
 | 
			
		||||
	mkdir -p build/dist
 | 
			
		||||
	zip -0 -j build/dist/qbolt-${VERSION}-win32.zip build/win32/dist/qbolt.exe
 | 
			
		||||
 | 
			
		||||
# Source code archives
 | 
			
		||||
 | 
			
		||||
build/dist/qbolt-${VERSION}-src.tar.gz:
 | 
			
		||||
	hg archive build/dist/qbolt-${VERSION}-src.tar.gz
 | 
			
		||||
build/qbolt-${VERSION}-windows-x86_64.zip: build/qbolt.exe
 | 
			
		||||
	zip -9 -j build/qbolt-${VERSION}-windows-x86_64.zip build/qbolt.exe
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
}
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
# qbolt
 | 
			
		||||
 | 
			
		||||
A graphical database manager for BoltDB.
 | 
			
		||||
 | 
			
		||||
QBolt allows you to graphically view and edit the content of Bolt databases.
 | 
			
		||||
 | 
			
		||||
The project consists of two parts; a C binding (CGo) for the embeddable Bolt database engine, and a graphical interface built in C++/Qt that links to it.
 | 
			
		||||
Written in Golang (Qt)
 | 
			
		||||
 | 
			
		||||
Written in C++ (Qt), Golang (CGo)
 | 
			
		||||
 | 
			
		||||
=FEATURES=
 | 
			
		||||
## Features
 | 
			
		||||
 | 
			
		||||
- Open existing database or create new database
 | 
			
		||||
- Option to open database as readonly for concurrent use
 | 
			
		||||
@@ -16,23 +16,39 @@ Written in C++ (Qt), Golang (CGo)
 | 
			
		||||
- 100% Bolt compatibility via the real codebase
 | 
			
		||||
- Tested working on both Windows and Linux
 | 
			
		||||
 | 
			
		||||
=LICENSE=
 | 
			
		||||
## License
 | 
			
		||||
 | 
			
		||||
Source code content of `qbolt-x.x.x-src.tar.gz` is released under the ISC license.
 | 
			
		||||
BoltDB is released under the MIT license.
 | 
			
		||||
The Windows binary is released under LGPL-3+ owing to the static copy of Qt.
 | 
			
		||||
 | 
			
		||||
=SEE ALSO=
 | 
			
		||||
## See also
 | 
			
		||||
 | 
			
		||||
- BoltDB https://github.com/boltdb/bolt
 | 
			
		||||
 | 
			
		||||
=CHANGELOG=
 | 
			
		||||
## Changelog
 | 
			
		||||
 | 
			
		||||
2024-10-05 1.0.3
 | 
			
		||||
- Port from hybrid Go/C++ to now using [MIQT](https://github.com/mappu/miqt)
 | 
			
		||||
- Switch Windows build to win64
 | 
			
		||||
- Rebuild artefacts with miqt v0.5.0, etcd-io/bbolt v1.3.11, go 1.19 (deb12), go 1.23 (win64)
 | 
			
		||||
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.3)
 | 
			
		||||
 | 
			
		||||
2020-04-12 1.0.2
 | 
			
		||||
- Rebuild artefacts with etcd-io/bbolt v1.3.5, go 1.15, Qt 5.15, and new GCC versions
 | 
			
		||||
- Switch from hg to Git
 | 
			
		||||
- Use Go modules
 | 
			
		||||
- Add support for building Windows binary in Docker
 | 
			
		||||
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.2)
 | 
			
		||||
 | 
			
		||||
2017-06-19 1.0.1
 | 
			
		||||
- Feature: Option to open database as read-only
 | 
			
		||||
- Fix an issue with support for bucket names and keys not surviving UTF-8 roundtrips (now binary-clean)
 | 
			
		||||
- Fix an issue with crashing when deleting a bucket other than the selected one
 | 
			
		||||
- Fix a cosmetic issue with application icon on Windows
 | 
			
		||||
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.1)
 | 
			
		||||
 | 
			
		||||
2017-05-21 1.0.0
 | 
			
		||||
- Initial public release
 | 
			
		||||
- The project consists of two parts; a C binding (CGo) for the embeddable Bolt database engine, and a graphical interface built in C++/Qt that links to it.
 | 
			
		||||
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.0)
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
 | 
			
		||||
- Reduce unnecessary memory copies between QString/QByteArray
 | 
			
		||||
 | 
			
		||||
- Convert from item-based to model-based Qt widgets - needs deep integration with Bolt cursors...
 | 
			
		||||
 | 
			
		||||
- Rename buckets
 | 
			
		||||
 | 
			
		||||
- Delete multiple buckets
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										219
									
								
								bolt.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| 
		 Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB  | 
| 
		 Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB  | 
| 
		 Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB  | 
| 
		 Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB  | 
							
								
								
									
										20
									
								
								docker/win64-cross.Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,20 @@
 | 
			
		||||
FROM golang:1.23-bookworm
 | 
			
		||||
 | 
			
		||||
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
 | 
			
		||||
    apt-get install -qyy gnupg2 ca-certificates
 | 
			
		||||
    
 | 
			
		||||
RUN DEBIAN_FRONTEND=noninteractive \
 | 
			
		||||
    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-get update && \
 | 
			
		||||
    apt-get install -qyy mxe-x86-64-w64-mingw32.static-qt5 && \
 | 
			
		||||
    apt-get clean
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,10 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	//"fmt"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"github.com/boltdb/bolt"
 | 
			
		||||
	bolt "go.etcd.io/bbolt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func random_name() string {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,10 @@
 | 
			
		||||
module code.ivysaur.me/qbolt
 | 
			
		||||
 | 
			
		||||
go 1.23
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/mappu/miqt v0.5.0
 | 
			
		||||
	go.etcd.io/bbolt v1.3.11
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require golang.org/x/sys v0.4.0 // indirect
 | 
			
		||||
							
								
								
									
										11
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 | 
			
		||||
github.com/mappu/miqt v0.5.0 h1:BWajkNI9PWlWN6ZDgWKwv1gieBGEImRqlWS8ZqDmDfA=
 | 
			
		||||
github.com/mappu/miqt v0.5.0/go.mod h1:xFg7ADaO1QSkmXPsPODoKe/bydJpRG9fgCYyIDl/h1U=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
			
		||||
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=
 | 
			
		||||
@@ -42,12 +42,6 @@
 | 
			
		||||
   </item>
 | 
			
		||||
   <item row="1" column="0">
 | 
			
		||||
    <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">
 | 
			
		||||
      <item row="0" column="0">
 | 
			
		||||
       <widget class="QDialogButtonBox" name="buttonBox">
 | 
			
		||||
							
								
								
									
										71
									
								
								itemwindow_ui.go
									
									
									
									
									
										Normal 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
									
									
									
									
									
								
							
							
						
						@@ -1,398 +1,22 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import "C"
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/binary"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"os"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/boltdb/bolt"
 | 
			
		||||
	"github.com/mappu/miqt/qt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
var Version string = "v0.0.0-devel"
 | 
			
		||||
 | 
			
		||||
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
									
								
							
							
						
						@@ -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 "Silk" icon set</a><br>"+
 | 
			
		||||
			"- <a href='https://code.ivysaur.me/qbolt'>QBolt homepage</a><br>",
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *MainWindow) on_actionDisconnect_triggered() {
 | 
			
		||||
	top := this.lastContextSelection
 | 
			
		||||
	if top.Parent() != nil {
 | 
			
		||||
		return // somehow we didn't select a top-level item
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	bdb := GET_BDB(top)
 | 
			
		||||
 | 
			
		||||
	// Remove UI
 | 
			
		||||
	this.ui.bucketTree.ClearSelection()
 | 
			
		||||
	top.Delete()
 | 
			
		||||
 | 
			
		||||
	// Disconnect from DB
 | 
			
		||||
	bdb.Close()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *MainWindow) on_bucketTree_customContextMenuRequested(pos *qt.QPoint) {
 | 
			
		||||
 | 
			
		||||
	itm := this.ui.bucketTree.ItemAt(pos)
 | 
			
		||||
	if itm == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this.lastContextSelection = itm
 | 
			
		||||
 | 
			
		||||
	if itm.Parent() != nil {
 | 
			
		||||
		// Child item, show the bucket menu
 | 
			
		||||
		this.bucketContext.Popup(this.ui.bucketTree.Viewport().MapToGlobal(pos))
 | 
			
		||||
 | 
			
		||||
	} else {
 | 
			
		||||
		// Top-level item, show the database menu
 | 
			
		||||
		this.databaseContext.Popup(this.ui.bucketTree.Viewport().MapToGlobal(pos))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *MainWindow) on_actionRefresh_buckets_triggered() {
 | 
			
		||||
	this.refreshBucketTree(this.lastContextSelection)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *MainWindow) on_bucketTree_currentItemChanged(current, previous *qt.QTreeWidgetItem) {
 | 
			
		||||
	_ = previous // Q_UNUSED
 | 
			
		||||
	if current == nil {
 | 
			
		||||
		this.ui.stackedWidget.SetVisible(false)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this.ui.stackedWidget.SetVisible(true)
 | 
			
		||||
 | 
			
		||||
	if current.Parent() == nil {
 | 
			
		||||
		// Selected a database
 | 
			
		||||
		this.ui.stackedWidget.SetCurrentWidget(this.ui.databasePage)
 | 
			
		||||
		this.ui.databasePropertiesArea.Clear()
 | 
			
		||||
 | 
			
		||||
		bdb := GET_BDB(current)
 | 
			
		||||
		stats, err := Bolt_DBStats(bdb)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			this.ui.databasePropertiesArea.SetPlainText(fmt.Sprintf("Error retrieving database statistics: %s", err.Error()))
 | 
			
		||||
		} else {
 | 
			
		||||
			this.ui.databasePropertiesArea.SetPlainText(stats)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Clean up foreign areas
 | 
			
		||||
		this.ui.bucketPropertiesArea.Clear()
 | 
			
		||||
		this.ui.bucketData.Clear()
 | 
			
		||||
 | 
			
		||||
	} else {
 | 
			
		||||
		// Selected a bucket
 | 
			
		||||
 | 
			
		||||
		this.ui.stackedWidget.SetCurrentWidget(this.ui.bucketPage)
 | 
			
		||||
		this.ui.bucketPropertiesArea.Clear()
 | 
			
		||||
 | 
			
		||||
		ws := this.getSelection(current)
 | 
			
		||||
 | 
			
		||||
		stats, err := Bolt_BucketStats(ws.bdb, ws.browse)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			this.ui.bucketPropertiesArea.SetPlainText(fmt.Sprintf("Error retrieving bucket statistics: %s", err.Error()))
 | 
			
		||||
		} else {
 | 
			
		||||
			this.ui.bucketPropertiesArea.SetPlainText(stats)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Load the data tab
 | 
			
		||||
		this.refreshData(ws.bdb, ws.browse)
 | 
			
		||||
 | 
			
		||||
		// Clean up foreign areas
 | 
			
		||||
		this.ui.databasePropertiesArea.Clear()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *MainWindow) refreshData(bdb *bolt.DB, browse []string) {
 | 
			
		||||
	// Load the data tab
 | 
			
		||||
	this.ui.bucketData.Clear()
 | 
			
		||||
 | 
			
		||||
	err := Bolt_ListItems(bdb, browse, func(lii ListItemInfo) error {
 | 
			
		||||
		itm := qt.NewQTreeWidgetItem()
 | 
			
		||||
		itm.SetText(0, getDisplayName(lii.Name))
 | 
			
		||||
		itm.SetData(0, BinaryDataRole, qt.NewQVariant15(MakeQByteArray(lii.Name)))
 | 
			
		||||
		itm.SetText(1, fmt.Sprintf("%d", lii.DataLen))
 | 
			
		||||
		this.ui.bucketData.AddTopLevelItem(itm)
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		this.alert(fmt.Sprintf("Error listing bucket content: %s", err.Error()))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this.ui.bucketData.ResizeColumnToContents(0)
 | 
			
		||||
	this.on_bucketData_itemSelectionChanged()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *MainWindow) on_actionClear_selection_triggered() {
 | 
			
		||||
	this.ui.bucketTree.SetCurrentItem(nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type windowSelection struct {
 | 
			
		||||
	itm    *qt.QTreeWidgetItem
 | 
			
		||||
	top    *qt.QTreeWidgetItem
 | 
			
		||||
	browse []string
 | 
			
		||||
	bdb    *bolt.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *MainWindow) getWindowSelection() (windowSelection, bool) {
 | 
			
		||||
 | 
			
		||||
	itm := this.ui.bucketTree.CurrentItem()
 | 
			
		||||
	if itm == nil {
 | 
			
		||||
		return windowSelection{}, false // No selection
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return this.getSelection(itm), true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *MainWindow) getSelection(itm *qt.QTreeWidgetItem) windowSelection {
 | 
			
		||||
 | 
			
		||||
	top := itm
 | 
			
		||||
 | 
			
		||||
	var browse []string
 | 
			
		||||
	for {
 | 
			
		||||
		if top.Parent() == nil {
 | 
			
		||||
			break
 | 
			
		||||
		} else {
 | 
			
		||||
			browse = append(browse, FromQByteArray(top.Data(0, BinaryDataRole).ToByteArray()))
 | 
			
		||||
			top = top.Parent()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ReverseSlice(browse)
 | 
			
		||||
 | 
			
		||||
	bdb := GET_BDB(top)
 | 
			
		||||
 | 
			
		||||
	return windowSelection{itm: itm, top: top, browse: browse, bdb: bdb}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *MainWindow) openEditor(bdb *bolt.DB, saveAs []string, saveAsKey string, currentContent []byte) {
 | 
			
		||||
	iw := NewItemWindowUi()
 | 
			
		||||
 | 
			
		||||
	iw.contentArea.SetPlainText(string(currentContent))
 | 
			
		||||
	iw.ItemWindow.SetWindowTitle(getDisplayName(saveAsKey))
 | 
			
		||||
	iw.ItemWindow.SetWindowModality(qt.ApplicationModal) // we need this - otherwise we'll refresh a possibly-changed area after saving
 | 
			
		||||
	iw.ItemWindow.OnFinished(func(exitCode int) {
 | 
			
		||||
		if exitCode == int(qt.QDialog__Accepted) {
 | 
			
		||||
 | 
			
		||||
			err := Bolt_SetItem(bdb, saveAs, saveAsKey, iw.contentArea.ToPlainText())
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				this.alert(fmt.Sprintf("Error saving item content: %s", err.Error()))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			this.refreshData(bdb, saveAs)
 | 
			
		||||
		}
 | 
			
		||||
		iw.ItemWindow.DeleteLater()
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	iw.ItemWindow.Show()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *MainWindow) on_bucketData_doubleClicked(index *qt.QModelIndex) {
 | 
			
		||||
	ws, ok := this.getWindowSelection()
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return // no selection
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get item key
 | 
			
		||||
 | 
			
		||||
	model := index.Model()
 | 
			
		||||
	key := FromQByteArray(model.Data2(model.Index(index.Row(), 0), BinaryDataRole).ToByteArray())
 | 
			
		||||
 | 
			
		||||
	// DB lookup
 | 
			
		||||
	content, err := Bolt_GetItem(ws.bdb, ws.browse, key)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		this.alert(fmt.Sprintf("Error loading item content: %s", err.Error()))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this.openEditor(ws.bdb, ws.browse, key, []byte(content))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *MainWindow) on_actionAdd_bucket_triggered() {
 | 
			
		||||
	ws, ok := this.getWindowSelection()
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return // no selection
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Prompt for bucket name
 | 
			
		||||
 | 
			
		||||
	name := qt.QInputDialog_GetText(this.Widget(), "New bucket", "Enter a key for the new bucket:")
 | 
			
		||||
	if len(name) == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Create
 | 
			
		||||
	err := Bolt_CreateBucket(ws.bdb, ws.browse, name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		this.alert(fmt.Sprintf("Error creating bucket: %s", err.Error()))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Refresh bucket list
 | 
			
		||||
	this.refreshBucketTree(ws.itm) // sub-tree only
 | 
			
		||||
	this.ui.bucketTree.ExpandItem(ws.itm)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *MainWindow) on_actionDelete_bucket_triggered() {
 | 
			
		||||
	ws, ok := this.getWindowSelection()
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return // no selection
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Prompt for confirmation
 | 
			
		||||
	bucketToDelete := FromQByteArray(ws.itm.Data(0, BinaryDataRole).ToByteArray())
 | 
			
		||||
	if qt.QMessageBox_Question4(
 | 
			
		||||
		this.Widget(),
 | 
			
		||||
		"Delete bucket",
 | 
			
		||||
		fmt.Sprintf("Are you sure you want to remove the bucket '%s'?", getDisplayName(bucketToDelete)),
 | 
			
		||||
		qt.QMessageBox__Yes,
 | 
			
		||||
		qt.QMessageBox__Cancel,
 | 
			
		||||
	) != int(qt.QMessageBox__Yes) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parent := ws.itm.Parent()
 | 
			
		||||
 | 
			
		||||
	// One level down
 | 
			
		||||
 | 
			
		||||
	if len(ws.browse) > 0 {
 | 
			
		||||
		ws.browse = ws.browse[0 : len(ws.browse)-1]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := Bolt_DeleteBucket(ws.bdb, ws.browse, bucketToDelete)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		this.alert(fmt.Sprintf("Error removing bucket: %s", err.Error()))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Refresh bucket list
 | 
			
		||||
	this.refreshBucketTree(parent) // sub-tree only
 | 
			
		||||
	this.ui.bucketTree.ExpandItem(parent)
 | 
			
		||||
	this.ui.bucketTree.SetCurrentItem(parent)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *MainWindow) on_AddDataButton_clicked() {
 | 
			
		||||
	ws, ok := this.getWindowSelection()
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return // no selection
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Prompt for bucket name
 | 
			
		||||
 | 
			
		||||
	name := qt.QInputDialog_GetText(this.Widget(), "New item", "Enter a key for the new item:")
 | 
			
		||||
	if len(name) == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this.openEditor(ws.bdb, ws.browse, name, []byte(""))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *MainWindow) on_DeleteDataButton_clicked() {
 | 
			
		||||
	ws, ok := this.getWindowSelection()
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return // no selection
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	selection := this.ui.bucketData.SelectedItems()
 | 
			
		||||
	if len(selection) == 0 {
 | 
			
		||||
		return // nothing to do
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Prompt for confirmation
 | 
			
		||||
	if qt.QMessageBox_Question4(this.Widget(), "Delete items", fmt.Sprintf("Are you sure you want to remove %d item(s)?", len(selection)), qt.QMessageBox__Yes, qt.QMessageBox__Cancel) != int(qt.QMessageBox__Yes) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var i int = len(selection)
 | 
			
		||||
	for i > 0 {
 | 
			
		||||
		err := Bolt_DeleteItem(ws.bdb, ws.browse, FromQByteArray(selection[i-1].Data(0, BinaryDataRole).ToByteArray()))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			this.alert(fmt.Sprintf("Error removing item: %s", err.Error()))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		i -= 1
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this.refreshData(ws.bdb, ws.browse)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *MainWindow) on_bucketData_itemSelectionChanged() {
 | 
			
		||||
	this.ui.DeleteDataButton.SetEnabled(len(this.ui.bucketData.SelectedItems()) > 0)
 | 
			
		||||
}
 | 
			
		||||
@@ -269,12 +269,12 @@
 | 
			
		||||
     <x>0</x>
 | 
			
		||||
     <y>0</y>
 | 
			
		||||
     <width>668</width>
 | 
			
		||||
     <height>21</height>
 | 
			
		||||
     <height>29</height>
 | 
			
		||||
    </rect>
 | 
			
		||||
   </property>
 | 
			
		||||
   <widget class="QMenu" name="menuFile">
 | 
			
		||||
    <property name="title">
 | 
			
		||||
     <string>Fi&le</string>
 | 
			
		||||
     <string>&File</string>
 | 
			
		||||
    </property>
 | 
			
		||||
    <addaction name="actionNew_database"/>
 | 
			
		||||
    <addaction name="actionOpen_database"/>
 | 
			
		||||
							
								
								
									
										325
									
								
								mainwindow_ui.go
									
									
									
									
									
										Normal 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"))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										219
									
								
								qbolt/boltdb.cpp
									
									
									
									
									
								
							
							
						
						@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -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();
 | 
			
		||||
}
 | 
			
		||||
@@ -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 "Silk" 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) );
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -1,47 +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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
@@ -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
									
								
							
							
						
						@@ -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
									
								
							
							
						
						| 
		 Before Width: | Height: | Size: 733 B After Width: | Height: | Size: 733 B  | 
| 
		 Before Width: | Height: | Size: 685 B After Width: | Height: | Size: 685 B  | 
| 
		 Before Width: | Height: | Size: 541 B After Width: | Height: | Size: 541 B  | 
| 
		 Before Width: | Height: | Size: 390 B After Width: | Height: | Size: 390 B  | 
| 
		 Before Width: | Height: | Size: 658 B After Width: | Height: | Size: 658 B  | 
| 
		 Before Width: | Height: | Size: 763 B After Width: | Height: | Size: 763 B  | 
| 
		 Before Width: | Height: | Size: 775 B After Width: | Height: | Size: 775 B  | 
| 
		 Before Width: | Height: | Size: 715 B After Width: | Height: | Size: 715 B  | 
| 
		 Before Width: | Height: | Size: 796 B After Width: | Height: | Size: 796 B  | 
| 
		 Before Width: | Height: | Size: 688 B After Width: | Height: | Size: 688 B  | 
| 
		 Before Width: | Height: | Size: 778 B After Width: | Height: | Size: 778 B  | 
| 
		 Before Width: | Height: | Size: 635 B After Width: | Height: | Size: 635 B  | 
| 
		 Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB  | 
| 
		 Before Width: | Height: | Size: 566 B After Width: | Height: | Size: 566 B  | 
| 
		 Before Width: | Height: | Size: 663 B After Width: | Height: | Size: 663 B  | 
| 
		 Before Width: | Height: | Size: 660 B After Width: | Height: | Size: 660 B  | 
							
								
								
									
										24
									
								
								util.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						@@ -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")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										50
									
								
								windows-manifest.template.json
									
									
									
									
									
										Normal 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
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||