commit 872bb95acf6fabd639da57cd41a4844d7e6dd0f0 Author: jaseg Date: Tue Aug 4 01:04:52 2020 +0200 Initial commit 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