diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | TagListDock.ui | 32 | ||||
-rw-r--r-- | aboutdialog.cpp | 20 | ||||
-rw-r--r-- | aboutdialog.h | 26 | ||||
-rw-r--r-- | aboutdialog.ui | 78 | ||||
-rw-r--r-- | main.cpp | 11 | ||||
-rw-r--r-- | numberator.cpp | 86 | ||||
-rw-r--r-- | numberator.h | 43 | ||||
-rw-r--r-- | numberator.pro | 50 | ||||
-rw-r--r-- | numberator.ui | 157 | ||||
-rw-r--r-- | sqlitebackend.cpp | 322 | ||||
-rw-r--r-- | sqlitebackend.h | 102 | ||||
-rw-r--r-- | taglistmodel.cpp | 76 | ||||
-rw-r--r-- | taglistmodel.h | 30 | ||||
-rw-r--r-- | tagproptablemodel.cpp | 144 | ||||
-rw-r--r-- | tagproptablemodel.h | 36 | ||||
-rw-r--r-- | tagscene.cpp | 6 | ||||
-rw-r--r-- | tagscene.h | 14 | ||||
-rw-r--r-- | tagview.cpp | 96 | ||||
-rw-r--r-- | tagview.h | 38 |
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 <code@jaseg.de></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 ¤t, 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 |