From 872bb95acf6fabd639da57cd41a4844d7e6dd0f0 Mon Sep 17 00:00:00 2001 From: jaseg Date: Tue, 4 Aug 2020 01:04:52 +0200 Subject: Initial commit --- .gitignore | 2 + TagListDock.ui | 32 +++++ aboutdialog.cpp | 20 ++++ aboutdialog.h | 26 ++++ aboutdialog.ui | 78 ++++++++++++ main.cpp | 11 ++ numberator.cpp | 86 ++++++++++++++ numberator.h | 43 +++++++ numberator.pro | 50 ++++++++ numberator.ui | 157 ++++++++++++++++++++++++ sqlitebackend.cpp | 322 ++++++++++++++++++++++++++++++++++++++++++++++++++ sqlitebackend.h | 102 ++++++++++++++++ taglistmodel.cpp | 76 ++++++++++++ taglistmodel.h | 30 +++++ tagproptablemodel.cpp | 144 ++++++++++++++++++++++ tagproptablemodel.h | 36 ++++++ tagscene.cpp | 6 + tagscene.h | 14 +++ tagview.cpp | 96 +++++++++++++++ tagview.h | 38 ++++++ 20 files changed, 1369 insertions(+) create mode 100644 .gitignore create mode 100644 TagListDock.ui create mode 100644 aboutdialog.cpp create mode 100644 aboutdialog.h create mode 100644 aboutdialog.ui create mode 100644 main.cpp create mode 100644 numberator.cpp create mode 100644 numberator.h create mode 100644 numberator.pro create mode 100644 numberator.ui create mode 100644 sqlitebackend.cpp create mode 100644 sqlitebackend.h create mode 100644 taglistmodel.cpp create mode 100644 taglistmodel.h create mode 100644 tagproptablemodel.cpp create mode 100644 tagproptablemodel.h create mode 100644 tagscene.cpp create mode 100644 tagscene.h create mode 100644 tagview.cpp create mode 100644 tagview.h 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 @@ + + + TagListDock + + + + 0 + 0 + 400 + 300 + + + + Tags + + + + + + + Qt::Vertical + + + + + + + + + + + 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 +#include + +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 @@ + + + AboutDialog + + + + 0 + 0 + 400 + 300 + + + + Dialog + + + + + + Numberator v0.1 +©2020 Jan Goette <code@jaseg.de> + + + Qt::AlignCenter + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Ok + + + true + + + + + + + + + buttonBox + accepted() + AboutDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + AboutDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + 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 + +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 +#include + +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 @@ + + + Numberator + + + + 0 + 0 + 800 + 600 + + + + Numberator + + + + + + + + + + + + 0 + 0 + 800 + 21 + + + + + File + + + + + + + + + + + + + + + + + Edit + + + + + + + + + + + + + Help + + + + + + View + + + + + + + + + + + About + + + + + Undo + + + + + Redo + + + + + Copy + + + + + Cut + + + + + Paste + + + + + New Tag + + + + + New Project + + + + + Open Project + + + + + Import Image + + + + + Reload Image + + + + + Save Project + + + + + Export PDF + + + + + Export Image + + + + + Exit + + + + + + 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 + +#include + +#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(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 SQLiteSaveFile::getAllTags() +{ + QMutexLocker l(&dbMut); + resetError(); + QList 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(); + + 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> 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> 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 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 +#include +#include +#include +#include + +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 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> 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 bindings={}); + + bool setMetaLocked(const QString &key, const QVariant &value); + bool setMetaLocked(std::initializer_list> 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 + +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 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 + + + +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 + + + +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 +#include +#include + +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 +#include + + +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 -- cgit