479 lines
13 KiB
C++
479 lines
13 KiB
C++
#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) );
|
|
}
|