Initial commit

This commit is contained in:
jaseg 2020-08-04 01:04:52 +02:00
commit 872bb95acf
20 changed files with 1369 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.pro.user
Makefile

32
TagListDock.ui Normal file
View file

@ -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>

20
aboutdialog.cpp Normal file
View file

@ -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();
}

26
aboutdialog.h Normal file
View file

@ -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

78
aboutdialog.ui Normal file
View file

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AboutDialog</class>
<widget class="QDialog" name="AboutDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Numberator v0.1
©2020 Jan Goette &lt;code@jaseg.de&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>AboutDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>AboutDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

11
main.cpp Normal file
View file

@ -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();
}

86
numberator.cpp Normal file
View file

@ -0,0 +1,86 @@
#include "numberator.h"
#include "ui_numberator.h"
#include "ui_TagListDock.h"
Numberator::Numberator(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::Numberator)
, tagsDockUi(new Ui::TagListDock)
, settings("jaseg.de", "Numberator")
, loadImageDialog(this)
, proj()
, tagListModel(proj)
, tagPropTableModel(proj)
{
ui->setupUi(this);
QDockWidget *dock = new QDockWidget(this);
tagsDockUi->setupUi(dock);
addDockWidget(Qt::LeftDockWidgetArea, dock);
ui->menuView->addAction(dock->toggleViewAction());
connect(ui->actionReload_Image, &QAction::triggered,
&proj, &SQLiteSaveFile::reloadImageFromDisk);
tagsDockUi->tagList->setModel(&tagListModel);
tagsDockUi->propertyTable->setModel(&tagPropTableModel);
loadImageDialog.setWindowModality(Qt::ApplicationModal);
loadImageDialog.setWindowTitle("Load Image...");
loadImageDialog.setNameFilter("Images (*.png, *.xpm, *.jpg)");
loadImageDialog.setFileMode(QFileDialog::ExistingFile);
loadImageDialog.restoreState(settings.value("MainWindow/LoadImageFileDialogState").toByteArray());
connect(&loadImageDialog, &QFileDialog::accepted, [=]() {
settings.setValue("MainWindow/LoadImageFileDialogState", this->loadImageDialog.saveState());
});
connect(&loadImageDialog, &QFileDialog::fileSelected, &proj, &SQLiteSaveFile::loadImageFromDisk);
connect(ui->actionImport_Image, &QAction::triggered, [=](bool checked){
Q_UNUSED(checked);
this->loadImageDialog.open();
});
saveOpenDialog.setWindowModality(Qt::ApplicationModal);
saveOpenDialog.setNameFilter("Project Files (*.npr);;Any File (*)");
saveOpenDialog.setFileMode(QFileDialog::AnyFile);
saveOpenDialog.restoreState(settings.value("MainWindow/SaveAsFileDialogState").toByteArray());
connect(&saveOpenDialog, &QFileDialog::accepted, [=]() {
settings.setValue("MainWindow/SaveAsFileDialogState", this->saveOpenDialog.saveState());
});
connect(ui->actionSave_Project, &QAction::triggered, [=](bool checked){
Q_UNUSED(checked);
this->saveOpenDialog.setWindowTitle("Save Project as...");
disconnect(&this->saveOpenDialog, &QFileDialog::fileSelected, nullptr, nullptr);
connect(&this->saveOpenDialog, &QFileDialog::fileSelected,
&this->proj, &SQLiteSaveFile::saveAs);
this->saveOpenDialog.open();
});
connect(ui->actionOpen_Project, &QAction::triggered, [=](bool checked){
Q_UNUSED(checked);
this->saveOpenDialog.setWindowTitle("Open Project...");
disconnect(&this->saveOpenDialog, &QFileDialog::fileSelected, nullptr, nullptr);
connect(&this->saveOpenDialog, &QFileDialog::fileSelected,
this, &Numberator::openFile);
this->saveOpenDialog.open();
});
connect(ui->actionNew_Project, &QAction::triggered,
&proj, &SQLiteSaveFile::clearNew);
connect(ui->actionQuit, &QAction::triggered, &QApplication::quit);
connect(ui->actionAbout, &QAction::triggered, &aboutDialog, &AboutDialog::open);
connect(tagsDockUi->tagList->selectionModel(), &QItemSelectionModel::currentChanged,
[=](const QModelIndex &current, const QModelIndex &previous) {
Q_UNUSED(previous);
tagPropTableModel.showTag(tagListModel.getTag(current));
});
}
Numberator::~Numberator()
{
delete ui;
}
void Numberator::openFile(const QString &path)
{
}

43
numberator.h Normal file
View file

@ -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

50
numberator.pro Normal file
View file

@ -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

157
numberator.ui Normal file
View file

@ -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>

322
sqlitebackend.cpp Normal file
View file

@ -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;
}

102
sqlitebackend.h Normal file
View file

@ -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

76
taglistmodel.cpp Normal file
View file

@ -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());
}

30
taglistmodel.h Normal file
View file

@ -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

144
tagproptablemodel.cpp Normal file
View file

@ -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();
}

36
tagproptablemodel.h Normal file
View file

@ -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

6
tagscene.cpp Normal file
View file

@ -0,0 +1,6 @@
#include "tagscene.h"
TagScene::TagScene()
{
}

14
tagscene.h Normal file
View file

@ -0,0 +1,14 @@
#ifndef TAGSCENE_H
#define TAGSCENE_H
#include <QGraphicsScene>
class TagScene : public QGraphicsScene
{
public:
TagScene();
};
#endif // TAGSCENE_H

96
tagview.cpp Normal file
View file

@ -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());
}
}

38
tagview.h Normal file
View file

@ -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