summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjaseg <git-bigdata-wsl-arch@jaseg.de>2020-08-04 01:04:52 +0200
committerjaseg <git-bigdata-wsl-arch@jaseg.de>2020-08-04 01:04:52 +0200
commit872bb95acf6fabd639da57cd41a4844d7e6dd0f0 (patch)
tree689d8fd95b93e2b54111fd207e93f6789c4b7f28
downloadnumberator-872bb95acf6fabd639da57cd41a4844d7e6dd0f0.tar.gz
numberator-872bb95acf6fabd639da57cd41a4844d7e6dd0f0.tar.bz2
numberator-872bb95acf6fabd639da57cd41a4844d7e6dd0f0.zip
Initial commit
-rw-r--r--.gitignore2
-rw-r--r--TagListDock.ui32
-rw-r--r--aboutdialog.cpp20
-rw-r--r--aboutdialog.h26
-rw-r--r--aboutdialog.ui78
-rw-r--r--main.cpp11
-rw-r--r--numberator.cpp86
-rw-r--r--numberator.h43
-rw-r--r--numberator.pro50
-rw-r--r--numberator.ui157
-rw-r--r--sqlitebackend.cpp322
-rw-r--r--sqlitebackend.h102
-rw-r--r--taglistmodel.cpp76
-rw-r--r--taglistmodel.h30
-rw-r--r--tagproptablemodel.cpp144
-rw-r--r--tagproptablemodel.h36
-rw-r--r--tagscene.cpp6
-rw-r--r--tagscene.h14
-rw-r--r--tagview.cpp96
-rw-r--r--tagview.h38
20 files changed, 1369 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c2463eb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*.pro.user
+Makefile
diff --git a/TagListDock.ui b/TagListDock.ui
new file mode 100644
index 0000000..5d06b5d
--- /dev/null
+++ b/TagListDock.ui
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>TagListDock</class>
+ <widget class="QDockWidget" name="TagListDock">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>300</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Tags</string>
+ </property>
+ <widget class="QWidget" name="dockWidgetContents">
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QSplitter" name="splitter">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <widget class="QListView" name="tagList"/>
+ <widget class="QTableView" name="propertyTable"/>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/aboutdialog.cpp b/aboutdialog.cpp
new file mode 100644
index 0000000..5ba1bf0
--- /dev/null
+++ b/aboutdialog.cpp
@@ -0,0 +1,20 @@
+#include "aboutdialog.h"
+#include "ui_aboutdialog.h"
+
+AboutDialog::AboutDialog(QWidget *parent) :
+ QDialog(parent),
+ ui(new Ui::AboutDialog)
+{
+ ui->setupUi(this);
+}
+
+AboutDialog::~AboutDialog()
+{
+ delete ui;
+}
+
+void AboutDialog::on_buttonBox_clicked(QAbstractButton *button)
+{
+ Q_UNUSED(button); /* There is only one button */
+ this->accept();
+}
diff --git a/aboutdialog.h b/aboutdialog.h
new file mode 100644
index 0000000..5965507
--- /dev/null
+++ b/aboutdialog.h
@@ -0,0 +1,26 @@
+#ifndef ABOUTDIALOG_H
+#define ABOUTDIALOG_H
+
+#include <QAbstractButton>
+#include <QDialog>
+
+namespace Ui {
+class AboutDialog;
+}
+
+class AboutDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit AboutDialog(QWidget *parent = nullptr);
+ ~AboutDialog();
+
+private slots:
+ void on_buttonBox_clicked(QAbstractButton *button);
+
+private:
+ Ui::AboutDialog *ui;
+};
+
+#endif // ABOUTDIALOG_H
diff --git a/aboutdialog.ui b/aboutdialog.ui
new file mode 100644
index 0000000..53b1b88
--- /dev/null
+++ b/aboutdialog.ui
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AboutDialog</class>
+ <widget class="QDialog" name="AboutDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>300</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Dialog</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Numberator v0.1
+©2020 Jan Goette &lt;code@jaseg.de&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Ok</set>
+ </property>
+ <property name="centerButtons">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>AboutDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>AboutDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/main.cpp b/main.cpp
new file mode 100644
index 0000000..11a88de
--- /dev/null
+++ b/main.cpp
@@ -0,0 +1,11 @@
+#include "numberator.h"
+
+#include <QApplication>
+
+int main(int argc, char *argv[])
+{
+ QApplication a(argc, argv);
+ Numberator w;
+ w.show();
+ return a.exec();
+}
diff --git a/numberator.cpp b/numberator.cpp
new file mode 100644
index 0000000..2497403
--- /dev/null
+++ b/numberator.cpp
@@ -0,0 +1,86 @@
+#include "numberator.h"
+#include "ui_numberator.h"
+#include "ui_TagListDock.h"
+
+Numberator::Numberator(QWidget *parent)
+ : QMainWindow(parent)
+ , ui(new Ui::Numberator)
+ , tagsDockUi(new Ui::TagListDock)
+ , settings("jaseg.de", "Numberator")
+ , loadImageDialog(this)
+ , proj()
+ , tagListModel(proj)
+ , tagPropTableModel(proj)
+{
+ ui->setupUi(this);
+
+ QDockWidget *dock = new QDockWidget(this);
+ tagsDockUi->setupUi(dock);
+ addDockWidget(Qt::LeftDockWidgetArea, dock);
+ ui->menuView->addAction(dock->toggleViewAction());
+ connect(ui->actionReload_Image, &QAction::triggered,
+ &proj, &SQLiteSaveFile::reloadImageFromDisk);
+
+ tagsDockUi->tagList->setModel(&tagListModel);
+ tagsDockUi->propertyTable->setModel(&tagPropTableModel);
+
+ loadImageDialog.setWindowModality(Qt::ApplicationModal);
+ loadImageDialog.setWindowTitle("Load Image...");
+ loadImageDialog.setNameFilter("Images (*.png, *.xpm, *.jpg)");
+ loadImageDialog.setFileMode(QFileDialog::ExistingFile);
+ loadImageDialog.restoreState(settings.value("MainWindow/LoadImageFileDialogState").toByteArray());
+ connect(&loadImageDialog, &QFileDialog::accepted, [=]() {
+ settings.setValue("MainWindow/LoadImageFileDialogState", this->loadImageDialog.saveState());
+ });
+ connect(&loadImageDialog, &QFileDialog::fileSelected, &proj, &SQLiteSaveFile::loadImageFromDisk);
+ connect(ui->actionImport_Image, &QAction::triggered, [=](bool checked){
+ Q_UNUSED(checked);
+ this->loadImageDialog.open();
+ });
+
+ saveOpenDialog.setWindowModality(Qt::ApplicationModal);
+ saveOpenDialog.setNameFilter("Project Files (*.npr);;Any File (*)");
+ saveOpenDialog.setFileMode(QFileDialog::AnyFile);
+ saveOpenDialog.restoreState(settings.value("MainWindow/SaveAsFileDialogState").toByteArray());
+ connect(&saveOpenDialog, &QFileDialog::accepted, [=]() {
+ settings.setValue("MainWindow/SaveAsFileDialogState", this->saveOpenDialog.saveState());
+ });
+ connect(ui->actionSave_Project, &QAction::triggered, [=](bool checked){
+ Q_UNUSED(checked);
+ this->saveOpenDialog.setWindowTitle("Save Project as...");
+ disconnect(&this->saveOpenDialog, &QFileDialog::fileSelected, nullptr, nullptr);
+ connect(&this->saveOpenDialog, &QFileDialog::fileSelected,
+ &this->proj, &SQLiteSaveFile::saveAs);
+ this->saveOpenDialog.open();
+ });
+ connect(ui->actionOpen_Project, &QAction::triggered, [=](bool checked){
+ Q_UNUSED(checked);
+ this->saveOpenDialog.setWindowTitle("Open Project...");
+ disconnect(&this->saveOpenDialog, &QFileDialog::fileSelected, nullptr, nullptr);
+ connect(&this->saveOpenDialog, &QFileDialog::fileSelected,
+ this, &Numberator::openFile);
+ this->saveOpenDialog.open();
+ });
+
+ connect(ui->actionNew_Project, &QAction::triggered,
+ &proj, &SQLiteSaveFile::clearNew);
+ connect(ui->actionQuit, &QAction::triggered, &QApplication::quit);
+ connect(ui->actionAbout, &QAction::triggered, &aboutDialog, &AboutDialog::open);
+
+ connect(tagsDockUi->tagList->selectionModel(), &QItemSelectionModel::currentChanged,
+ [=](const QModelIndex &current, const QModelIndex &previous) {
+ Q_UNUSED(previous);
+ tagPropTableModel.showTag(tagListModel.getTag(current));
+ });
+}
+
+Numberator::~Numberator()
+{
+ delete ui;
+}
+
+void Numberator::openFile(const QString &path)
+{
+
+}
+
diff --git a/numberator.h b/numberator.h
new file mode 100644
index 0000000..21203cd
--- /dev/null
+++ b/numberator.h
@@ -0,0 +1,43 @@
+#ifndef NUMBERATOR_H
+#define NUMBERATOR_H
+
+#include "aboutdialog.h"
+#include "sqlitebackend.h"
+#include "taglistmodel.h"
+#include "tagproptablemodel.h"
+
+#include <QFileDialog>
+#include <QMainWindow>
+
+QT_BEGIN_NAMESPACE
+namespace Ui {
+ class Numberator;
+ class TagListDock;
+}
+QT_END_NAMESPACE
+
+class Numberator : public QMainWindow
+{
+ Q_OBJECT
+
+public:
+ Numberator(QWidget *parent = nullptr);
+ ~Numberator();
+
+private slots:
+ void openFile(const QString &path);
+
+private:
+ Ui::Numberator *ui;
+ Ui::TagListDock *tagsDockUi;
+
+ QSettings settings;
+ QFileDialog loadImageDialog,
+ saveOpenDialog;
+ AboutDialog aboutDialog;
+
+ SQLiteSaveFile proj;
+ TagListModel tagListModel;
+ TagPropTableModel tagPropTableModel;
+};
+#endif // NUMBERATOR_H
diff --git a/numberator.pro b/numberator.pro
new file mode 100644
index 0000000..8783811
--- /dev/null
+++ b/numberator.pro
@@ -0,0 +1,50 @@
+QT += core gui sql
+
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
+
+CONFIG += c++2a
+
+
+win32:INCLUDEPATH += $$[QT_INSTALL_PREFIX]/../Src/qtbase/src/3rdparty/sqlite
+win32:SOURCES += $$[QT_INSTALL_PREFIX]/../Src/qtbase/src/3rdparty/sqlite/sqlite3.c
+unix:LIBS += -lsqlite3
+
+# The following define makes your compiler emit warnings if you use
+# any Qt feature that has been marked deprecated (the exact warnings
+# depend on your compiler). Please consult the documentation of the
+# deprecated API in order to know how to port your code away from it.
+DEFINES += QT_DEPRECATED_WARNINGS
+
+# You can also make your code fail to compile if it uses deprecated APIs.
+# In order to do so, uncomment the following line.
+# You can also select to disable deprecated APIs only up to a certain version of Qt.
+#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
+
+SOURCES += \
+ aboutdialog.cpp \
+ main.cpp \
+ numberator.cpp \
+ sqlitebackend.cpp \
+ taglistmodel.cpp \
+ tagproptablemodel.cpp \
+ tagview.cpp \
+ tagscene.cpp
+
+HEADERS += \
+ aboutdialog.h \
+ numberator.h \
+ sqlitebackend.h \
+ taglistmodel.h \
+ tagproptablemodel.h \
+ tagview.h \
+ tagscene.h
+
+FORMS += \
+ TagListDock.ui \
+ aboutdialog.ui \
+ numberator.ui
+
+# Default rules for deployment.
+qnx: target.path = /tmp/$${TARGET}/bin
+else: unix:!android: target.path = /opt/$${TARGET}/bin
+!isEmpty(target.path): INSTALLS += target
diff --git a/numberator.ui b/numberator.ui
new file mode 100644
index 0000000..8bf3371
--- /dev/null
+++ b/numberator.ui
@@ -0,0 +1,157 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Numberator</class>
+ <widget class="QMainWindow" name="Numberator">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>600</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Numberator</string>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QGraphicsView" name="graphicsView"/>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QMenuBar" name="menubar">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>21</height>
+ </rect>
+ </property>
+ <widget class="QMenu" name="menuFile">
+ <property name="title">
+ <string>File</string>
+ </property>
+ <addaction name="actionNew_Project"/>
+ <addaction name="actionOpen_Project"/>
+ <addaction name="separator"/>
+ <addaction name="actionImport_Image"/>
+ <addaction name="actionReload_Image"/>
+ <addaction name="separator"/>
+ <addaction name="actionSave_Project"/>
+ <addaction name="separator"/>
+ <addaction name="actionExport_PDF"/>
+ <addaction name="actionExport_PNG"/>
+ <addaction name="separator"/>
+ <addaction name="actionQuit"/>
+ </widget>
+ <widget class="QMenu" name="menuEdit">
+ <property name="title">
+ <string>Edit</string>
+ </property>
+ <addaction name="actionUndo"/>
+ <addaction name="actionRedo"/>
+ <addaction name="separator"/>
+ <addaction name="actionCopy"/>
+ <addaction name="actionCut"/>
+ <addaction name="actionPaste"/>
+ <addaction name="separator"/>
+ <addaction name="actionNew_Tag"/>
+ </widget>
+ <widget class="QMenu" name="menuHelp">
+ <property name="title">
+ <string>Help</string>
+ </property>
+ <addaction name="actionAbout"/>
+ </widget>
+ <widget class="QMenu" name="menuView">
+ <property name="title">
+ <string>View</string>
+ </property>
+ </widget>
+ <addaction name="menuFile"/>
+ <addaction name="menuEdit"/>
+ <addaction name="menuView"/>
+ <addaction name="menuHelp"/>
+ </widget>
+ <widget class="QStatusBar" name="statusbar"/>
+ <action name="actionAbout">
+ <property name="text">
+ <string>About</string>
+ </property>
+ </action>
+ <action name="actionUndo">
+ <property name="text">
+ <string>Undo</string>
+ </property>
+ </action>
+ <action name="actionRedo">
+ <property name="text">
+ <string>Redo</string>
+ </property>
+ </action>
+ <action name="actionCopy">
+ <property name="text">
+ <string>Copy</string>
+ </property>
+ </action>
+ <action name="actionCut">
+ <property name="text">
+ <string>Cut</string>
+ </property>
+ </action>
+ <action name="actionPaste">
+ <property name="text">
+ <string>Paste</string>
+ </property>
+ </action>
+ <action name="actionNew_Tag">
+ <property name="text">
+ <string>New Tag</string>
+ </property>
+ </action>
+ <action name="actionNew_Project">
+ <property name="text">
+ <string>New Project</string>
+ </property>
+ </action>
+ <action name="actionOpen_Project">
+ <property name="text">
+ <string>Open Project</string>
+ </property>
+ </action>
+ <action name="actionImport_Image">
+ <property name="text">
+ <string>Import Image</string>
+ </property>
+ </action>
+ <action name="actionReload_Image">
+ <property name="text">
+ <string>Reload Image</string>
+ </property>
+ </action>
+ <action name="actionSave_Project">
+ <property name="text">
+ <string>Save Project</string>
+ </property>
+ </action>
+ <action name="actionExport_PDF">
+ <property name="text">
+ <string>Export PDF</string>
+ </property>
+ </action>
+ <action name="actionExport_PNG">
+ <property name="text">
+ <string>Export Image</string>
+ </property>
+ </action>
+ <action name="actionQuit">
+ <property name="text">
+ <string>Exit</string>
+ </property>
+ </action>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/sqlitebackend.cpp b/sqlitebackend.cpp
new file mode 100644
index 0000000..b95e18d
--- /dev/null
+++ b/sqlitebackend.cpp
@@ -0,0 +1,322 @@
+#include <QMessageBox>
+
+#include<sqlite3.h>
+
+#include "sqlitebackend.h"
+
+SQLiteSaveFile::SQLiteSaveFile(QObject *parent, QString filename) :
+ QObject(parent)
+ , lastError(NoError)
+ , lastErrorString(QString())
+ , db(QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString()))
+ , filename(filename)
+ , m_isOpen(false)
+{
+ connect();
+}
+
+bool SQLiteSaveFile::initDb(bool setCreationDate)
+{
+ for (auto const &q: {
+ "CREATE TABLE IF NOT EXISTS metadata (key TEXT, value TEXT)",
+ "CREATE TABLE IF NOT EXISTS tags (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, anchor_x REAL, anchor_y REAL, meta TEXT)",
+ "CREATE TABLE IF NOT EXISTS blobs (name TEXT, data BLOB)"}) {
+ if (!runSql(q))
+ return false;
+ }
+
+ if (setCreationDate) {
+ if (!setMetaLocked("creationTime", QDateTime::currentDateTimeUtc().toMSecsSinceEpoch()))
+ return false;
+ }
+
+ return true;
+}
+
+bool SQLiteSaveFile::connect()
+{
+ QMutexLocker l(&dbMut);
+ resetError();
+ m_isOpen = false;
+ imageData = QByteArray();
+
+ bool newlyCreated = QFile(filename).exists();
+
+ db.setDatabaseName(filename);
+ if (!db.open()) {
+ setDatabaseError(db);
+ db.close();
+ return false;
+ }
+
+ if (!initDb(newlyCreated)) {
+ db.close();
+ return false;
+ }
+
+ m_isOpen = true;
+
+ /* Try to load image, ignore if image is unset */
+ QSqlQuery q("SELECT data FROM blobs WHERE name = 'image'", db);
+ if (!q.next())
+ return setDatabaseError(q);
+
+ imageData = q.value(0).toByteArray();
+ return true;
+}
+
+static sqlite3 *getSqliteHandle(QSqlDatabase &db) {
+ QVariant v = db.driver()->handle();
+ assert (v.isValid());
+ assert (!qstrcmp(v.typeName(), "sqlite3*"));
+ return *static_cast<sqlite3 **>(v.data());
+}
+
+bool SQLiteSaveFile::saveAs(const QString &filename)
+{
+ QMutexLocker l(&dbMut);
+ QFile f(filename);
+ QSqlDatabase old_db = db;
+ QSqlDatabase new_db(QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString()));
+ new_db.setDatabaseName(f.fileName());
+ if (!new_db.open()) {
+ setDatabaseError(new_db);
+ new_db.close();
+ return false;
+ }
+
+ sqlite3 *old_handle = getSqliteHandle(db);
+ sqlite3 *new_handle = getSqliteHandle(new_db);
+ sqlite3_backup *bck = sqlite3_backup_init(new_handle, "main", old_handle, "main");
+ if (!bck)
+ goto err_cleanup;
+
+ if (sqlite3_backup_step(bck, -1) != SQLITE_DONE)
+ goto err_cleanup;
+
+ if (sqlite3_backup_finish(bck) != SQLITE_DONE)
+ goto err_cleanup;
+
+ db = new_db;
+ old_db.close();
+ return true;
+
+err_cleanup:
+ setDatabaseError(new_db);
+ new_db.close();
+ return false;
+}
+
+bool SQLiteSaveFile::clearNew()
+{
+ QMutexLocker l(&dbMut);
+
+ QSqlDatabase new_db(QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString()));
+ new_db.setDatabaseName(":memory:");
+ if (!new_db.open()) {
+ setDatabaseError(new_db);
+ new_db.close();
+ return false;
+ }
+
+ if (!initDb()) {
+ db.close();
+ return false;
+ }
+
+ imageData = QByteArray();
+ return true;
+}
+
+QList<Tag> SQLiteSaveFile::getAllTags()
+{
+ QMutexLocker l(&dbMut);
+ resetError();
+ QList<Tag> rv;
+ QSqlQuery q("SELECT (id, name, anchor_x, anchor_y, meta) FROM tags", db);
+
+ while (q.next()) {
+ rv << Tag {
+ q.value(0).toLongLong(),
+ q.value(1).toString(),
+ q.value(2).toFloat(),
+ q.value(3).toFloat(),
+ q.value(4).toByteArray()
+ };
+ }
+
+ if (!setDatabaseError(q))
+ return QList<Tag>();
+
+ return rv;
+}
+
+bool SQLiteSaveFile::updateTag(Tag tag)
+{
+ QMutexLocker l(&dbMut);
+ if (!runSql("UPDATE tags SET name=?, anchor_x=?, anchor_y=?, meta=? WHERE id=?", {
+ tag.name, tag.anchor.x(), tag.anchor.y(), QJsonDocument::fromVariant(tag.metadata).toJson(), tag.id
+ }))
+ return false;
+
+ tagChange(TagChange::CHANGED, tag);
+ return true;
+}
+
+bool SQLiteSaveFile::deleteTag(Tag tag)
+{
+ QMutexLocker l(&dbMut);
+ if (!runSql("DELETE FROM tags WHERE id=?", {tag.id}))
+ return false;
+
+ tagChange(TagChange::DELETED, tag);
+ return true;
+}
+
+bool SQLiteSaveFile::createTag(Tag tag)
+{
+ QMutexLocker l(&dbMut);
+ resetError();
+ QSqlQuery q("INSERT INTO tags(name, anchor_x, anchor_y, meta) VALUES (?, ?, ?, ?)", db);
+ q.addBindValue(tag.name);
+ q.addBindValue(tag.anchor.x());
+ q.addBindValue(tag.anchor.y());
+ q.addBindValue(QJsonDocument::fromVariant(tag.metadata).toJson());
+ if (!q.exec())
+ return setDatabaseError(q);
+
+ Tag created_tag(q.lastInsertId().toLongLong(), tag);
+ tagChange(TagChange::CREATED, created_tag);
+ return true;
+}
+
+bool SQLiteSaveFile::setMetaLocked(const QString &key, const QVariant &value)
+{
+ return runSql("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)", {key, value});
+}
+
+bool SQLiteSaveFile::setMetaLocked(std::initializer_list<QPair<QString, QVariant>> metas)
+{
+ for (const auto &meta : metas) {
+ if (!setMetaLocked(meta.first, meta.second))
+ return false;
+ }
+ return true;
+}
+
+bool SQLiteSaveFile::setMeta(const QString &key, const QVariant &value) {
+ QMutexLocker l(&dbMut);
+ return setMetaLocked(key, value);
+}
+
+bool SQLiteSaveFile::setMeta(std::initializer_list<QPair<QString, QVariant>> metas) {
+ QMutexLocker l(&dbMut);
+ return setMetaLocked(metas);
+}
+
+QVariant SQLiteSaveFile::getMeta(const QString &key) {
+ QMutexLocker l(&dbMut);
+ return getMetaLocked(key);
+}
+
+QVariant SQLiteSaveFile::getMetaLocked(const QString &key)
+{
+ resetError();
+ QSqlQuery q("SELECT value FROM metadata WHERE key=?", db);
+ q.addBindValue(key);
+ if (!q.next()) {
+ setDatabaseError(q);
+ return QVariant();
+ }
+
+ return q.value(0);
+}
+
+bool SQLiteSaveFile::runSql(QString query, std::initializer_list<QVariant> bindings)
+{
+ resetError();
+ QSqlQuery q(query, db);
+ for (const QVariant &v : bindings)
+ q.addBindValue(v);
+
+ q.exec();
+ return setDatabaseError(q);
+}
+
+bool SQLiteSaveFile::loadImageFromDisk(const QString &filename)
+{
+ QMutexLocker l(&dbMut);
+ QFile f(filename);
+ resetError();
+
+ if (!f.open(QIODevice::ReadOnly)) {
+ setError(ImageOpenError, QString("Failed to open image: %1").arg(f.errorString()));
+ return false;
+ }
+
+ imageData = f.readAll();
+ if (f.error() != QFileDevice::NoError) {
+ setError(ImageReadError, QString("Failed to read image: %1").arg(f.errorString()));
+ return false;
+ }
+
+ if (!setMetaLocked({
+ {"imagePathOriginal", f.fileName()},
+ {"imagePathAbsolute", QFileInfo(f).absoluteFilePath()},
+ {"imageLoadedTime", QDateTime::currentDateTimeUtc().toMSecsSinceEpoch()}}))
+ return false;
+
+ return runSql("INSERT OR REPLACE INTO blobs(name, data) VALUES ('image', ?)", {imageData});
+}
+
+bool SQLiteSaveFile::reloadImageFromDisk()
+{
+ const QString &p = getMeta("imagePathOriginal").toString();
+ if (QFile(p).exists())
+ return loadImageFromDisk(p);
+
+ const QString &q = getMeta("imagePathAbsolute").toString();
+ if (QFile(q).exists())
+ return loadImageFromDisk(q);
+
+ return false;
+}
+
+Tag::Tag(long long id, QString name, qreal anchor_x, qreal anchor_y, QByteArray metadata)
+ : id(id)
+ , name(name)
+ , anchor(QPointF(anchor_x, anchor_y))
+ , metadata(QJsonDocument::fromJson(metadata).object().toVariantMap())
+{
+}
+
+Tag::Tag()
+ : id(-1)
+{
+}
+
+Tag::Tag(long long id, const Tag &other)
+ : id(id)
+ , name(other.name)
+ , anchor(other.anchor)
+ , metadata(other.metadata)
+{
+}
+
+bool SQLiteSaveFile::setDatabaseError(const QSqlQuery &q)
+{
+ if (!q.lastError().isValid())
+ return true;
+
+ setError(SQLiteError, QString("Project file database error: %1").arg(q.lastError().text()));
+ return false;
+}
+
+bool SQLiteSaveFile::setDatabaseError(const QSqlDatabase &db)
+{
+ if (!db.lastError().isValid())
+ return true;
+
+ setError(SQLiteError, QString("Project file database error: %1").arg(db.lastError().text()));
+ return false;
+}
diff --git a/sqlitebackend.h b/sqlitebackend.h
new file mode 100644
index 0000000..4dfd2f9
--- /dev/null
+++ b/sqlitebackend.h
@@ -0,0 +1,102 @@
+#ifndef SQLITEBACKEND_H
+#define SQLITEBACKEND_H
+
+#include <QObject>
+#include <QPointF>
+#include <QHash>
+#include <QtSql>
+#include <QFile>
+
+enum TagChange {
+ CREATED,
+ CHANGED,
+ DELETED
+};
+
+class Tag
+{
+public:
+ Tag(long long int id, QString name, qreal anchor_x, qreal anchor_y, QByteArray metadata);
+ Tag();
+ Tag(long long int id, const Tag &other);
+
+ long long int id;
+ QString name;
+ QPointF anchor;
+ QVariantMap metadata;
+};
+
+class SQLiteSaveFile : public QObject
+{
+ Q_OBJECT
+public:
+ explicit SQLiteSaveFile(QObject *parent = nullptr, QString filename = ":memory:");
+ bool connect();
+ bool isOpen() { return m_isOpen; }
+
+ QList<Tag> getAllTags();
+
+ QByteArray getImage();
+
+ bool updateTag(Tag tag);
+ bool deleteTag(Tag tag);
+ bool createTag(Tag tag);
+
+ bool setMeta(const QString &key, const QVariant &value);
+ bool setMeta(std::initializer_list<QPair<QString, QVariant>> metas);
+ QVariant getMeta(const QString &key);
+
+ QString errorString() { return lastErrorString; }
+ enum Error {
+ NoError = 0,
+ SQLiteError,
+ ImageOpenError,
+ ImageReadError
+ };
+ inline const static QString errorNames[] = {
+ [NoError] = "No Error",
+ [SQLiteError] = "Database Error",
+ [ImageOpenError] = "Error Opening Image",
+ [ImageReadError] = "Error Reading Image"
+ };
+
+ Error error();
+ void resetError() { lastError = NoError; lastErrorString = QString(); }
+
+public slots:
+ /** Save this project file under a new name. This changes the backend database this project file object points to, and copies all data.
+ * Callers can continue to use the same project file object afterwards.
+ */
+ bool saveAs(const QString &filename);
+ bool reloadImageFromDisk();
+ bool loadImageFromDisk(const QString &filename);
+ bool clearNew();
+
+signals:
+ void tagChange(TagChange change, const Tag &tag);
+ void fileReload();
+ void fileIOError(Error e, QString errorName, QString description);
+
+private:
+ bool initDb(bool setCreationDate=true);
+ bool runSql(QString query, std::initializer_list<QVariant> bindings={});
+
+ bool setMetaLocked(const QString &key, const QVariant &value);
+ bool setMetaLocked(std::initializer_list<QPair<QString, QVariant>> metas);
+ QVariant getMetaLocked(const QString &key);
+
+ void setError(Error e, QString desc) { lastError = e; lastErrorString = desc; fileIOError(e, errorNames[e], desc); }
+ bool setDatabaseError(const QSqlQuery &q);
+ bool setDatabaseError(const QSqlDatabase &db);
+
+ Error lastError;
+ QString lastErrorString;
+ QSqlDatabase db;
+ QMutex dbMut;
+ QString filename;
+ QByteArray imageData;
+ bool m_isOpen;
+};
+
+
+#endif // SQLITEBACKEND_H
diff --git a/taglistmodel.cpp b/taglistmodel.cpp
new file mode 100644
index 0000000..e3ba006
--- /dev/null
+++ b/taglistmodel.cpp
@@ -0,0 +1,76 @@
+#include "taglistmodel.h"
+
+TagListModel::TagListModel(SQLiteSaveFile &backend)
+ : backend(backend)
+ , cached_tags(backend.getAllTags())
+{
+ connect(&backend, &SQLiteSaveFile::tagChange,
+ [=](TagChange change, const Tag &tag) { Q_UNUSED(change); Q_UNUSED(tag); reloadTags(); });
+ connect(&backend, &SQLiteSaveFile::fileReload,
+ [=]() { reloadTags(); });
+}
+
+int TagListModel::rowCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent);
+ return cached_tags.size();
+}
+
+void TagListModel::reloadTags()
+{
+ beginResetModel();
+ cached_tags = backend.getAllTags();
+ endResetModel();
+}
+
+QVariant TagListModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ if (role != Qt::DisplayRole)
+ return QVariant();
+
+ return cached_tags.at(index.row()).name;
+}
+
+QVariant TagListModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ assert(section == 0);
+ assert(orientation == Qt::Horizontal);
+
+ if (role != Qt::DisplayRole)
+ return QVariant();
+
+ return QString("Tag");
+}
+
+Qt::ItemFlags TagListModel::flags(const QModelIndex &index) const
+{
+ Q_UNUSED(index);
+ /* TODO: Add drag&drop from tag list to graphics view */
+ return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable;
+}
+
+bool TagListModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ if (!index.isValid())
+ return false;
+
+ if (role != Qt::EditRole)
+ return false;
+
+ Tag t = cached_tags.at(index.row());
+ t.name = value.toString();
+
+ backend.updateTag(t);
+ return true;
+}
+
+Tag TagListModel::getTag(const QModelIndex &index) const
+{
+ if (!index.isValid())
+ return Tag();
+
+ return cached_tags.at(index.row());
+}
diff --git a/taglistmodel.h b/taglistmodel.h
new file mode 100644
index 0000000..41fa520
--- /dev/null
+++ b/taglistmodel.h
@@ -0,0 +1,30 @@
+#ifndef TAGLISTMODEL_H
+#define TAGLISTMODEL_H
+
+#include "sqlitebackend.h"
+
+#include <qabstractitemmodel.h>
+
+class TagListModel : public QAbstractListModel
+{
+public:
+ TagListModel(SQLiteSaveFile &backend);
+
+ int rowCount(const QModelIndex &parent=QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override;
+ QVariant headerData(int section, Qt::Orientation orientation, int role=Qt::DisplayRole) const override;
+
+ Qt::ItemFlags flags(const QModelIndex &index) const override;
+ bool setData(const QModelIndex &index, const QVariant &value, int role=Qt::EditRole) override;
+
+ Tag getTag(const QModelIndex &index) const;
+
+public slots:
+ void reloadTags();
+
+private:
+ SQLiteSaveFile &backend;
+ QList<Tag> cached_tags;
+};
+
+#endif // TAGLISTMODEL_H
diff --git a/tagproptablemodel.cpp b/tagproptablemodel.cpp
new file mode 100644
index 0000000..711494d
--- /dev/null
+++ b/tagproptablemodel.cpp
@@ -0,0 +1,144 @@
+#include "tagproptablemodel.h"
+
+TagPropTableModel::TagPropTableModel(SQLiteSaveFile &backend, const Tag tag)
+ : backend(backend)
+ , tag_cached(tag)
+ , tagIsValid(true)
+{
+ tag_keys = tag_cached.metadata.keys();
+ tag_keys.sort();
+ connect(&backend, &SQLiteSaveFile::tagChange,
+ this, &TagPropTableModel::tagChange);
+ connect(&backend, &SQLiteSaveFile::fileReload,
+ [=]() {
+ beginResetModel();
+ tagIsValid = false;
+ endResetModel();
+ });
+}
+
+TagPropTableModel::TagPropTableModel(SQLiteSaveFile &backend)
+ : backend(backend)
+ , tagIsValid(false)
+{
+}
+
+int TagPropTableModel::rowCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent);
+ if (!tagIsValid)
+ return 0;
+ return 3 + tag_cached.metadata.size();
+}
+
+int TagPropTableModel::columnCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent);
+ return 2;
+}
+
+QVariant TagPropTableModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ if (role != Qt::DisplayRole)
+ return QVariant();
+
+ bool label = index.column() == 0;
+
+ switch (index.row()) {
+ case 0: return label ? QVariant("Database ID") : tag_cached.id; break;
+ case 1: return label ? QVariant("Label") : tag_cached.name; break;
+ case 2: return label ? QVariant("Anchor") : tag_cached.anchor; break;
+ }
+
+ int idx = index.row() - 3;
+ return label ? tag_keys[idx] : tag_cached.metadata[tag_keys[idx]];
+}
+
+QVariant TagPropTableModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if (orientation != Qt::Horizontal)
+ return QVariant();
+
+ if (role != Qt::DisplayRole)
+ return QVariant();
+
+ if (section == 0)
+ return "Property";
+ else
+ return "Value";
+}
+
+Qt::ItemFlags TagPropTableModel::flags(const QModelIndex &index) const
+{
+ if (index.row() == 0)
+ return Qt::NoItemFlags;
+
+ if (index.column() == 0) {
+ if (index.row() < 3)
+ return Qt::ItemIsEnabled;
+
+ return Qt::ItemIsEnabled | Qt::ItemIsEditable;
+ }
+
+ if (index.row() == 1)
+ return Qt::ItemIsEnabled | Qt::ItemIsEditable;
+
+ if (index.row() == 2) /* anchor */
+ return Qt::ItemIsEnabled;
+
+ return Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled;
+}
+
+bool TagPropTableModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ if (!index.isValid())
+ return false;
+
+ if (role != Qt::EditRole)
+ return false;
+
+ if (index.row() == 1)
+ tag_cached.name = value.toString();
+ else if (index.row() < 3)
+ return false;
+
+ int idx = index.row() - 3;
+ if (index.column() == 0) { /* key changed */
+ /* move value to new key and delete old key */
+ tag_cached.metadata[value.toString()] = tag_cached.metadata[tag_keys[idx]];
+ tag_cached.metadata.remove(tag_keys[idx]);
+ } else {
+ tag_cached.metadata[tag_keys[idx]] = value.toString();
+ }
+
+ backend.updateTag(tag_cached);
+ return true;
+}
+
+void TagPropTableModel::tagChange(TagChange change, const Tag &tag)
+{
+ if (tag.id != tag_cached.id)
+ return;
+
+ assert(change != TagChange::CREATED);
+ if (change == TagChange::CHANGED) {
+ showTag(tag);
+ } else if (change == TagChange::DELETED) {
+ beginResetModel();
+ tagIsValid = false;
+ endResetModel();
+ }
+}
+
+
+void TagPropTableModel::showTag(const Tag &tag)
+{
+ beginResetModel();
+ tag_cached = tag;
+ tag_keys = tag_cached.metadata.keys();
+ tag_keys.sort();
+ endResetModel();
+}
diff --git a/tagproptablemodel.h b/tagproptablemodel.h
new file mode 100644
index 0000000..b127f90
--- /dev/null
+++ b/tagproptablemodel.h
@@ -0,0 +1,36 @@
+#ifndef TAGPROPTABLEMODEL_H
+#define TAGPROPTABLEMODEL_H
+
+#include "sqlitebackend.h"
+
+#include <qabstractitemmodel.h>
+
+
+
+class TagPropTableModel : public QAbstractTableModel
+{
+public:
+ TagPropTableModel(SQLiteSaveFile &backend);
+ TagPropTableModel(SQLiteSaveFile &backend, const Tag tag);
+
+ int rowCount(const QModelIndex &parent=QModelIndex()) const override;
+ int columnCount(const QModelIndex &parent=QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override;
+ QVariant headerData(int section, Qt::Orientation orientation, int role=Qt::DisplayRole) const override;
+
+ Qt::ItemFlags flags(const QModelIndex &index) const override;
+ bool setData(const QModelIndex &index, const QVariant &value, int role=Qt::EditRole) override;
+
+ void showTag(const Tag &tag);
+
+private slots:
+ void tagChange(TagChange change, const Tag &tag);
+
+private:
+ SQLiteSaveFile &backend;
+ Tag tag_cached;
+ QStringList tag_keys;
+ bool tagIsValid;
+};
+
+#endif // TAGPROPTABLEMODEL_H
diff --git a/tagscene.cpp b/tagscene.cpp
new file mode 100644
index 0000000..5b028a0
--- /dev/null
+++ b/tagscene.cpp
@@ -0,0 +1,6 @@
+#include "tagscene.h"
+
+TagScene::TagScene()
+{
+
+}
diff --git a/tagscene.h b/tagscene.h
new file mode 100644
index 0000000..823609e
--- /dev/null
+++ b/tagscene.h
@@ -0,0 +1,14 @@
+#ifndef TAGSCENE_H
+#define TAGSCENE_H
+
+#include <QGraphicsScene>
+
+
+
+class TagScene : public QGraphicsScene
+{
+public:
+ TagScene();
+};
+
+#endif // TAGSCENE_H
diff --git a/tagview.cpp b/tagview.cpp
new file mode 100644
index 0000000..abaefcb
--- /dev/null
+++ b/tagview.cpp
@@ -0,0 +1,96 @@
+#include "tagview.h"
+
+#include <QWheelEvent>
+#include <QScrollBar>
+#include <cmath>
+
+TagView::TagView(SQLiteSaveFile &proj)
+ : proj(proj)
+ , saveCenterTimer()
+{
+ setDragMode(QGraphicsView::ScrollHandDrag);
+ setScene(&scene);
+
+ saveCenterTimer.setSingleShot(true);
+ saveCenterTimer.setInterval(500);
+ connect(&saveCenterTimer, &QTimer::timeout,
+ this, &TagView::saveCenter);
+}
+
+void TagView::zoomToFit()
+{
+ QTransform tx = QTransform().rotate(-rotation);
+ QRectF rect = tx.mapRect(scene.itemsBoundingRect());
+ QRectF vp = viewport()->rect();
+
+ setZoom(qMin(vp.width()/rect.width(), vp.height()/rect.height()));
+}
+
+void TagView::setZoom(qreal zoom)
+{
+ this->zoom = zoom;
+ proj.setMeta("view_zoom", zoom);
+ setTransform(QTransform::fromScale(zoom, zoom).rotate(rotation));
+}
+
+void TagView::zoomIn(qreal delta)
+{
+ setZoom(qMax(1.0/16, qMin(4.0, zoom * qPow(1.2, delta/120))));
+}
+
+void TagView::rotate(int angle)
+{
+ QGraphicsView::rotate(angle);
+ int tmp = (rotation + angle) % 360;
+ if (tmp < 0)
+ tmp += 360;
+ rotation = tmp;
+ proj.setMeta("view_rotation", rotation);
+}
+
+void TagView::wheelEvent(QWheelEvent *evt)
+{
+ if (evt->modifiers() == Qt::ControlModifier) {
+ zoomIn(evt->angleDelta().y());
+ } else {
+ if (qAbs(evt->angleDelta().x()) > qAbs(evt->angleDelta().y())) {
+ QCoreApplication::sendEvent(horizontalScrollBar(), evt);
+ } else {
+ QCoreApplication::sendEvent(verticalScrollBar(), evt);
+ }
+ }
+}
+
+void TagView::saveCenter()
+{
+ QPointF p = mapToScene(viewport()->rect().center());
+ proj.setMeta("view_center", QJsonDocument(QJsonArray({p.x(), p.y()})).toJson());
+}
+
+void TagView::restoreViewport()
+{
+ QVariant v_rot = proj.getMeta("view_rotation");
+ if (v_rot.isValid()) {
+ rotation = v_rot.toInt();
+ } else {
+ rotation = 0;
+ }
+
+ QVariant v_zoom = proj.getMeta("view_zoom");
+ if (v_zoom.isValid()) {
+ zoom = v_zoom.toDouble();
+ setTransform(QTransform::fromScale(zoom, zoom).rotate(rotation));
+ } else {
+ zoomToFit();
+ }
+
+ QVariant v_center = proj.getMeta("view_center");
+ if (v_center.isValid()) {
+ QJsonArray arr = QJsonDocument::fromJson(v_center.toByteArray()).toVariant().toJsonArray();
+ assert(arr.size() == 2);
+ assert(arr[0].isDouble() && arr[1].isDouble());
+ centerOn(QPointF(arr[0].toDouble(), arr[1].toDouble()));
+ } else {
+ centerOn(scene.itemsBoundingRect().center());
+ }
+}
diff --git a/tagview.h b/tagview.h
new file mode 100644
index 0000000..eedd019
--- /dev/null
+++ b/tagview.h
@@ -0,0 +1,38 @@
+#ifndef TAGVIEW_H
+#define TAGVIEW_H
+
+#include "sqlitebackend.h"
+#include "tagscene.h"
+
+#include <QGraphicsView>
+#include <QTimer>
+
+
+class TagView : public QGraphicsView
+{
+public:
+ TagView(SQLiteSaveFile &proj);
+
+public slots:
+ void zoomToFit();
+ void setZoom(qreal zoom);
+ void zoomIn(qreal delta);
+ void rotate(int angle);
+
+protected:
+ void wheelEvent(QWheelEvent *evt) override;
+
+private slots:
+ void saveCenter();
+
+private:
+ void restoreViewport();
+
+ TagScene scene;
+ SQLiteSaveFile &proj;
+ QTimer saveCenterTimer;
+ int rotation;
+ double zoom;
+};
+
+#endif // TAGVIEW_H