Basic model/view action works

This commit is contained in:
jaseg 2020-08-16 17:04:32 +02:00
parent 752f270cf8
commit 2deadc6cfb
15 changed files with 439 additions and 133 deletions

View file

@ -29,6 +29,8 @@ Numberator::Numberator(QWidget *parent)
connect(ui->actionReload_Image, &QAction::triggered, connect(ui->actionReload_Image, &QAction::triggered,
&proj, &SQLiteSaveFile::reloadImageFromDisk); &proj, &SQLiteSaveFile::reloadImageFromDisk);
ui->graphicsView->setProject(&proj);
tagsDockUi->tagList->setModel(&tagListModel); tagsDockUi->tagList->setModel(&tagListModel);
tagsDockUi->propertyTable->setModel(&tagPropTableModel); tagsDockUi->propertyTable->setModel(&tagPropTableModel);
@ -78,11 +80,15 @@ Numberator::Numberator(QWidget *parent)
}); });
connect(ui->actionAbout, &QAction::triggered, &aboutDialog, &AboutDialog::open); connect(ui->actionAbout, &QAction::triggered, &aboutDialog, &AboutDialog::open);
connect(tagsDockUi->tagList->selectionModel(), &QItemSelectionModel::currentChanged, connect(tagsDockUi->tagList->selectionModel(), &QItemSelectionModel::selectionChanged,
[=](const QModelIndex &current, const QModelIndex &previous) { [=](const QItemSelection &selected, const QItemSelection &deselected) {
Q_UNUSED(previous); Q_UNUSED(deselected);
tagPropTableModel.showTag(tagListModel.getTag(current)); tagPropTableModel.showTag(tagListModel.getTag(selected.indexes().first()));
}); });
connect(ui->actionZoom_to_fit, &QAction::triggered, ui->graphicsView, &TagView::zoomToFit);
connect(ui->actionZoom_in, &QAction::triggered, ui->graphicsView, &TagView::zoomIn);
connect(ui->actionZoom_out, &QAction::triggered, ui->graphicsView, &TagView::zoomOut);
} }
Numberator::~Numberator() Numberator::~Numberator()

View file

@ -5,6 +5,7 @@
#include "sqlitebackend.h" #include "sqlitebackend.h"
#include "taglistmodel.h" #include "taglistmodel.h"
#include "tagproptablemodel.h" #include "tagproptablemodel.h"
#include "tagview.h"
#include <QFileDialog> #include <QFileDialog>
#include <QMainWindow> #include <QMainWindow>
@ -41,5 +42,6 @@ private:
SQLiteSaveFile proj; SQLiteSaveFile proj;
TagListModel tagListModel; TagListModel tagListModel;
TagPropTableModel tagPropTableModel; TagPropTableModel tagPropTableModel;
TagView m_tagView;
}; };
#endif // NUMBERATOR_H #endif // NUMBERATOR_H

View file

@ -16,7 +16,7 @@
<widget class="QWidget" name="centralwidget"> <widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<item> <item>
<widget class="QGraphicsView" name="graphicsView"/> <widget class="TagView" name="graphicsView"/>
</item> </item>
</layout> </layout>
</widget> </widget>
@ -69,6 +69,9 @@
<property name="title"> <property name="title">
<string>View</string> <string>View</string>
</property> </property>
<addaction name="actionZoom_to_fit"/>
<addaction name="actionZoom_in"/>
<addaction name="actionZoom_out"/>
</widget> </widget>
<addaction name="menuFile"/> <addaction name="menuFile"/>
<addaction name="menuEdit"/> <addaction name="menuEdit"/>
@ -85,11 +88,17 @@
<property name="text"> <property name="text">
<string>Undo</string> <string>Undo</string>
</property> </property>
<property name="shortcut">
<string>Ctrl+Z</string>
</property>
</action> </action>
<action name="actionRedo"> <action name="actionRedo">
<property name="text"> <property name="text">
<string>Redo</string> <string>Redo</string>
</property> </property>
<property name="shortcut">
<string>Ctrl+R</string>
</property>
</action> </action>
<action name="actionCopy"> <action name="actionCopy">
<property name="text"> <property name="text">
@ -100,46 +109,73 @@
<property name="text"> <property name="text">
<string>Cut</string> <string>Cut</string>
</property> </property>
<property name="shortcut">
<string>Ctrl+X</string>
</property>
</action> </action>
<action name="actionPaste"> <action name="actionPaste">
<property name="text"> <property name="text">
<string>Paste</string> <string>Paste</string>
</property> </property>
<property name="shortcut">
<string>Ctrl+V</string>
</property>
</action> </action>
<action name="actionNew_Tag"> <action name="actionNew_Tag">
<property name="text"> <property name="text">
<string>New Tag</string> <string>New Tag</string>
</property> </property>
<property name="shortcut">
<string>Ctrl+A</string>
</property>
</action> </action>
<action name="actionNew_Project"> <action name="actionNew_Project">
<property name="text"> <property name="text">
<string>New Project</string> <string>New Project</string>
</property> </property>
<property name="shortcut">
<string>Ctrl+N</string>
</property>
</action> </action>
<action name="actionOpen_Project"> <action name="actionOpen_Project">
<property name="text"> <property name="text">
<string>Open Project</string> <string>Open Project</string>
</property> </property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
</action> </action>
<action name="actionImport_Image"> <action name="actionImport_Image">
<property name="text"> <property name="text">
<string>Import Image</string> <string>Import Image</string>
</property> </property>
<property name="shortcut">
<string>Ctrl+I</string>
</property>
</action> </action>
<action name="actionReload_Image"> <action name="actionReload_Image">
<property name="text"> <property name="text">
<string>Reload Image</string> <string>Reload Image</string>
</property> </property>
<property name="shortcut">
<string>F5</string>
</property>
</action> </action>
<action name="actionSave_Project"> <action name="actionSave_Project">
<property name="text"> <property name="text">
<string>Save Project</string> <string>Save Project as...</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+S</string>
</property> </property>
</action> </action>
<action name="actionExport_PDF"> <action name="actionExport_PDF">
<property name="text"> <property name="text">
<string>Export PDF</string> <string>Export PDF</string>
</property> </property>
<property name="shortcut">
<string>Ctrl+E</string>
</property>
</action> </action>
<action name="actionExport_PNG"> <action name="actionExport_PNG">
<property name="text"> <property name="text">
@ -150,8 +186,36 @@
<property name="text"> <property name="text">
<string>Exit</string> <string>Exit</string>
</property> </property>
<property name="shortcut">
<string>Ctrl+Q</string>
</property>
</action>
<action name="actionZoom_to_fit">
<property name="text">
<string>Zoom to fit</string>
</property>
<property name="shortcut">
<string>Ctrl+0</string>
</property>
</action>
<action name="actionZoom_in">
<property name="text">
<string>Zoom in</string>
</property>
</action>
<action name="actionZoom_out">
<property name="text">
<string>Zoom out</string>
</property>
</action> </action>
</widget> </widget>
<customwidgets>
<customwidget>
<class>TagView</class>
<extends>QGraphicsView</extends>
<header>tagview.h</header>
</customwidget>
</customwidgets>
<resources/> <resources/>
<connections/> <connections/>
</ui> </ui>

View file

@ -14,8 +14,8 @@ SQLiteSaveFile::SQLiteSaveFile(QObject *parent) :
bool SQLiteSaveFile::open(const QString &filename) bool SQLiteSaveFile::open(const QString &filename)
{ {
qDebug() << "open"; auto dbg = qDebug() << "open";
{ { /* Emit signals only after unlocking mutex to allow accesses by receivers */
QMutexLocker l(&m_dbMut); QMutexLocker l(&m_dbMut);
QFile f(filename); QFile f(filename);
if (!f.exists()) { if (!f.exists()) {
@ -44,15 +44,16 @@ bool SQLiteSaveFile::open(const QString &filename)
m_dirty = false; m_dirty = false;
m_open = true; m_open = true;
m_image = q.value(0).toByteArray(); m_image = q.value(0).toByteArray();
dbg << QString("Loaded %1 byte image").arg(m_image.size());
} }
fileReload(); /* Call after unlocking mutex to allow accesses by receivers */ fileReload();
return true; return true;
} }
bool SQLiteSaveFile::clearNew() bool SQLiteSaveFile::clearNew()
{ {
qDebug() << "clearNew"; qDebug() << "clearNew";
{ { /* Emit signals only after unlocking mutex to allow accesses by receivers */
QMutexLocker l(&m_dbMut); QMutexLocker l(&m_dbMut);
QSqlDatabase new_db(QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString())); QSqlDatabase new_db(QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString()));
@ -148,7 +149,7 @@ bool SQLiteSaveFile::saveAs(const QString &filename)
QList<Tag> SQLiteSaveFile::getAllTags() QList<Tag> SQLiteSaveFile::getAllTags()
{ {
qDebug() << "getAllTags"; auto dbg = qDebug() << "getAllTags()";
QMutexLocker l(&m_dbMut); QMutexLocker l(&m_dbMut);
resetError(); resetError();
QList<Tag> rv; QList<Tag> rv;
@ -170,56 +171,134 @@ QList<Tag> SQLiteSaveFile::getAllTags()
if (!setDatabaseError(q)) if (!setDatabaseError(q))
return QList<Tag>(); return QList<Tag>();
dbg << QString("%1 tags").arg(rv.size());
return rv; return rv;
} }
bool SQLiteSaveFile::updateTag(Tag tag) bool SQLiteSaveFile::updateTag(Tag tag)
{ {
QMutexLocker l(&m_dbMut); auto dbg = qDebug() << "updating tag" << tag.id << tag.name;
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 QMutexLocker l(&m_dbMut);
})) QSqlQuery q(m_db);
return false; q.prepare("SELECT 1 FROM tags WHERE id=? AND name=?");
q.addBindValue(tag.id);
q.addBindValue(tag.name);
if (!q.exec()) {
setDatabaseError(q);
return false;
}
bool nameChanged = !q.next();
if (!setDatabaseError(q))
return false;
m_dirty = true; if (nameChanged)
setMetaLocked("lastTagName", tag.name);
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;
}
m_dirty = true;
}
dbg << "calling handlers";
tagChange(TagChange::CHANGED, tag); tagChange(TagChange::CHANGED, tag);
return true; return true;
} }
bool SQLiteSaveFile::deleteTag(Tag tag) bool SQLiteSaveFile::deleteTag(Tag tag)
{ {
QMutexLocker l(&m_dbMut); {
if (!runSql("DELETE FROM tags WHERE id=?", {tag.id})) QMutexLocker l(&m_dbMut);
return false; if (!runSql("DELETE FROM tags WHERE id=?", {tag.id}))
return false;
m_dirty = true; m_dirty = true;
}
tagChange(TagChange::DELETED, tag); tagChange(TagChange::DELETED, tag);
return true; return true;
} }
bool SQLiteSaveFile::createTag(Tag tag) bool SQLiteSaveFile::createTag(Tag tag)
{ {
qDebug() << "createTag";
QMutexLocker l(&m_dbMut);
resetError();
QSqlQuery q(m_db); QSqlQuery q(m_db);
q.prepare("INSERT INTO tags(name, anchor_x, anchor_y, meta) VALUES (?, ?, ?, ?)"); auto dbg = qDebug() << "createTag";
q.addBindValue(tag.name); {
q.addBindValue(tag.anchor.x()); QMutexLocker l(&m_dbMut);
q.addBindValue(tag.anchor.y()); resetError();
q.addBindValue(QJsonDocument::fromVariant(tag.metadata).toJson()); if (!runSql("INSERT INTO tags(name, anchor_x, anchor_y, meta) VALUES (?, ?, ?, ?)", {
tag.name,
tag.anchor.x(),
tag.anchor.y(),
QJsonDocument::fromVariant(tag.metadata).toJson()
})) {
return false;
}
m_dirty = true;
}
auto id = q.lastInsertId().toLongLong();
dbg << "id" << id;
tagChange(TagChange::CREATED, Tag(id, tag));
return true;
}
if (!setDatabaseError(q)) QString SQLiteSaveFile::getNextAutoTagName()
{
QVariant lookupResult = getMeta("lastTagName");
QString lastTagName = "U0";
if (lookupResult.isValid() && !lookupResult.toString().isNull())
lastTagName = lookupResult.toString();
QString newName = "U1";
QRegularExpression name_re("^(.*?)(\\d+)$");
auto res = name_re.match(lastTagName);
if (res.hasMatch()) {
bool ok = false;
int numericSuffix = res.captured(2).toInt(&ok);
QString stringPrefix = res.captured(1);
qDebug() << "Name has match" << stringPrefix << numericSuffix;
if (ok) {
do {
numericSuffix ++;
newName = QString("%1%2").arg(stringPrefix).arg(numericSuffix);
} while (numericSuffix<10000 && !tagNameIsFree(newName));
}
}
return newName;
}
bool SQLiteSaveFile::createTagAt(const QPointF &anchor)
{
QString newName = getNextAutoTagName();
if (!setMeta("lastTagName", newName))
return false;
return createTag(Tag(newName, anchor));
}
bool SQLiteSaveFile::tagNameIsFree(const QString &name)
{
QMutexLocker l(&m_dbMut);
QSqlQuery q(m_db);
q.prepare("SELECT name FROM tags WHERE name=?");
q.addBindValue(name);
if (!q.exec()) {
setDatabaseError(q);
return true;
}
if (q.next())
return false; return false;
Tag created_tag(q.lastInsertId().toLongLong(), tag); setDatabaseError(q);
m_dirty = true;
tagChange(TagChange::CREATED, created_tag);
return true; return true;
} }
bool SQLiteSaveFile::setMetaLocked(const QString &key, const QVariant &value) bool SQLiteSaveFile::setMetaLocked(const QString &key, const QVariant &value)
{ {
qDebug() << QString("setMeta: %1=%2").arg(key).arg(value.toString());
return runSql("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)", {key, value}); return runSql("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)", {key, value});
} }
@ -232,15 +311,15 @@ bool SQLiteSaveFile::setMetaLocked(std::initializer_list<QPair<QString, QVariant
return true; return true;
} }
bool SQLiteSaveFile::setMeta(const QString &key, const QVariant &value) { bool SQLiteSaveFile::setMeta(const QString &key, const QVariant &value, bool setDirty) {
QMutexLocker l(&m_dbMut); QMutexLocker l(&m_dbMut);
m_dirty = true; m_dirty = m_dirty || setDirty;
return setMetaLocked(key, value); return setMetaLocked(key, value);
} }
bool SQLiteSaveFile::setMeta(std::initializer_list<QPair<QString, QVariant>> metas) { bool SQLiteSaveFile::setMeta(std::initializer_list<QPair<QString, QVariant>> metas, bool setDirty) {
QMutexLocker l(&m_dbMut); QMutexLocker l(&m_dbMut);
m_dirty = true; m_dirty = m_dirty || setDirty;
return setMetaLocked(metas); return setMetaLocked(metas);
} }
@ -251,7 +330,6 @@ const QVariant SQLiteSaveFile::getMeta(const QString &key) const {
const QVariant SQLiteSaveFile::getMetaLocked(const QString &key) const const QVariant SQLiteSaveFile::getMetaLocked(const QString &key) const
{ {
qDebug() << "getMeta " << key;
resetError(); resetError();
QSqlQuery q(m_db); QSqlQuery q(m_db);
q.prepare("SELECT value FROM metadata WHERE key=?"); q.prepare("SELECT value FROM metadata WHERE key=?");
@ -267,6 +345,7 @@ const QVariant SQLiteSaveFile::getMetaLocked(const QString &key) const
return QVariant(); return QVariant();
} }
qDebug() << QString("getMeta: %1=%2").arg(key).arg(q.value(0).toString());
return q.value(0); return q.value(0);
} }
@ -288,29 +367,35 @@ bool SQLiteSaveFile::runSql(QString query, std::initializer_list<QVariant> bindi
bool SQLiteSaveFile::loadImageFromDisk(const QString &filename) bool SQLiteSaveFile::loadImageFromDisk(const QString &filename)
{ {
QMutexLocker l(&m_dbMut); {
QFile f(filename); QMutexLocker l(&m_dbMut);
resetError(); QFile f(filename);
resetError();
if (!f.open(QIODevice::ReadOnly)) {
setError(ImageOpenError, QString("Failed to open image: %1").arg(f.errorString())); if (!f.open(QIODevice::ReadOnly)) {
return false; setError(ImageOpenError, QString("Failed to open image: %1").arg(f.errorString()));
return false;
}
m_image = 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;
m_dirty = true;
if (!runSql("INSERT OR REPLACE INTO blobs(name, data) VALUES ('image', ?)", {m_image}))
return false;
} }
/* Emit signal with mutex unlocked */
m_image = f.readAll(); imageLoaded(m_image);
if (f.error() != QFileDevice::NoError) { return true;
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;
m_dirty = true;
return runSql("INSERT OR REPLACE INTO blobs(name, data) VALUES ('image', ?)", {m_image});
} }
bool SQLiteSaveFile::reloadImageFromDisk() bool SQLiteSaveFile::reloadImageFromDisk()
@ -344,6 +429,15 @@ Tag::Tag(long long id, const Tag &other)
{ {
} }
Tag::Tag(QString name, const QPointF &anchor, const QVariantMap metadata)
: id(-1)
, name(name)
, anchor(anchor)
, metadata(metadata)
, valid(false)
{
}
bool SQLiteSaveFile::setDatabaseError(const QSqlQuery &q) const bool SQLiteSaveFile::setDatabaseError(const QSqlQuery &q) const
{ {
if (!q.lastError().isValid()) if (!q.lastError().isValid())

View file

@ -19,6 +19,7 @@ public:
Tag() : valid(false) {} Tag() : valid(false) {}
Tag(long long int id, QString name, qreal anchor_x, qreal anchor_y, QByteArray metadata); Tag(long long int id, QString name, qreal anchor_x, qreal anchor_y, QByteArray metadata);
Tag(long long int id, const Tag &other); Tag(long long int id, const Tag &other);
Tag(QString name, const QPointF &anchor, const QVariantMap metadata=QVariantMap());
bool isValid() { return valid; } bool isValid() { return valid; }
@ -44,13 +45,17 @@ public:
bool updateTag(Tag tag); bool updateTag(Tag tag);
bool deleteTag(Tag tag); bool deleteTag(Tag tag);
bool createTag(Tag tag); bool createTag(Tag tag);
bool createTagAt(const QPointF &anchor);
QString getNextAutoTagName();
bool tagNameIsFree(const QString &name);
bool isMemory() { return m_memory; } /* backend db points to temporary memory db */ bool isMemory() { return m_memory; } /* backend db points to temporary memory db */
bool isDirty() { return m_dirty; } /* backend db was changed since opening */ bool isDirty() { return m_dirty; } /* backend db was changed since opening */
bool isOpen() { return m_open; } /* backend db is open */ bool isOpen() { return m_open; } /* backend db is open */
bool setMeta(const QString &key, const QVariant &value); bool setMeta(const QString &key, const QVariant &value, bool setDirty=false);
bool setMeta(std::initializer_list<QPair<QString, QVariant>> metas); bool setMeta(std::initializer_list<QPair<QString, QVariant>> metas, bool setDirty=false);
const QVariant getMeta(const QString &key) const; const QVariant getMeta(const QString &key) const;
const QString &errorString() const { return m_lastErrorString; } const QString &errorString() const { return m_lastErrorString; }
@ -87,6 +92,7 @@ signals:
void tagChange(TagChange change, const Tag &tag); void tagChange(TagChange change, const Tag &tag);
void fileReload(); void fileReload();
void fileIOError(Error e, QString errorName, QString description) const; void fileIOError(Error e, QString errorName, QString description) const;
void imageLoaded(const QByteArray &image);
private: private:
bool connect(); bool connect();

View file

@ -1,25 +1,86 @@
#include "tagitem.h" #include "tagitem.h"
#include <QPainter>
#include <QGuiApplication>
TagItem::TagItem(const Tag &tag) TagItem::TagItem(const Tag &tag)
: valid(true) : valid(true)
{ {
setFlags(QGraphicsItem::ItemIsMovable setFlags(QGraphicsItem::ItemIsMovable
| QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemIsSelectable
| QGraphicsItem::ItemIsFocusable); | QGraphicsItem::ItemIsFocusable
/* TODO text_it.setFlags(QGraphicsItem::ItemIgnoresTransformations); | QGraphicsItem::ItemIgnoresTransformations
*/ | QGraphicsItem::ItemSendsGeometryChanges);
QFont font(QGuiApplication::font());
font.setPointSize(18);
setFont(font);
tagUpdated(tag); tagUpdated(tag);
} }
void TagItem::tagUpdated(const Tag &tag) void TagItem::tagUpdated(const Tag &tag)
{ {
m_tag = tag; m_tag = tag;
qDebug() << "TagItem updated" << tag.name << tag.anchor;
setText(tag.name); setText(tag.name);
setPos(tag.anchor); setPos(tag.anchor);
} }
QRectF TagItem::boundingRect() const
{
QRectF parentRect(QGraphicsSimpleTextItem::boundingRect());
parentRect.translate(-(parentRect.bottomRight() - parentRect.topLeft()) * 0.5);
return parentRect.marginsAdded(QMargins(5, 5, 5, 5));
}
/* For some reason this is not exposed through the public API so we have to copy-paste it here m( */
static void paintSelectionHighlightBorder(QPainter *painter, const QStyleOptionGraphicsItem *option, TagItem *item)
{
const QRectF mbrect = painter->transform().mapRect(item->boundingRect());
if (qMin(mbrect.width(), mbrect.height()) < qreal(1.0))
return;
const qreal pad = item->pen().widthF() / 2;
const qreal penWidth = 0; // cosmetic pen
const QColor fgcolor = option->palette.windowText().color();
const QColor bgcolor( // ensure good contrast against fgcolor
fgcolor.red() > 127 ? 0 : 255,
fgcolor.green() > 127 ? 0 : 255,
fgcolor.blue() > 127 ? 0 : 255);
painter->setPen(QPen(bgcolor, penWidth, Qt::SolidLine));
painter->setBrush(Qt::NoBrush);
painter->drawRect(item->boundingRect().adjusted(pad, pad, -pad, -pad));
painter->setPen(QPen(option->palette.windowText(), 0, Qt::DashLine));
painter->setBrush(Qt::NoBrush);
painter->drawRect(item->boundingRect().adjusted(pad, pad, -pad, -pad));
}
void TagItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
painter->setBrush(Qt::white);
painter->drawRect(boundingRect());
QRectF parentRect(QGraphicsSimpleTextItem::boundingRect());
QPointF pos = (parentRect.bottomRight() - parentRect.topLeft()) * 0.5;
painter->translate(-pos);
QStyleOptionGraphicsItem newOption(*option);
newOption.state = ~(QStyle::State_Selected |QStyle::State_HasFocus);
QGraphicsSimpleTextItem::paint(painter, &newOption, widget);
painter->translate(pos);
if (option->state & (QStyle::State_Selected | QStyle::State_HasFocus))
paintSelectionHighlightBorder(painter, option, this);
}
QPainterPath TagItem::shape() const
{
QPainterPath path;
return QGraphicsItem::shape();
}
QVariant TagItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) QVariant TagItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
{ {
qDebug() << "itemChange" << m_tag.name << this->boundingRect() << change;
if (change == ItemPositionChange) { if (change == ItemPositionChange) {
/* https://gist.github.com/csukuangfj/c2a06416062bec9ed99eddd705c21275#file-qgraphicsscenetest-cpp-L90 /* https://gist.github.com/csukuangfj/c2a06416062bec9ed99eddd705c21275#file-qgraphicsscenetest-cpp-L90
* *

View file

@ -14,13 +14,14 @@ public:
enum { TagItemType = UserType + 1 }; enum { TagItemType = UserType + 1 };
int type() const override { return TagItemType; } int type() const override { return TagItemType; }
bool isValid() { return valid; } bool isValid() { return valid; }
void tagUpdated(const Tag &tag); void tagUpdated(const Tag &tag);
Tag tag() { return m_tag; } Tag tag() { return m_tag; }
virtual QRectF boundingRect() const override;
virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
virtual QPainterPath shape() const override;
protected: protected:
QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override; QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override;

View file

@ -4,6 +4,7 @@ TagListModel::TagListModel(SQLiteSaveFile &backend)
: backend(backend) : backend(backend)
, cached_tags(backend.getAllTags()) , cached_tags(backend.getAllTags())
{ {
qDebug() << "connecting TagListModel" << &backend;
connect(&backend, &SQLiteSaveFile::tagChange, connect(&backend, &SQLiteSaveFile::tagChange,
[=](TagChange change, const Tag &tag) { Q_UNUSED(change); Q_UNUSED(tag); reloadTags(); }); [=](TagChange change, const Tag &tag) { Q_UNUSED(change); Q_UNUSED(tag); reloadTags(); });
connect(&backend, &SQLiteSaveFile::fileReload, connect(&backend, &SQLiteSaveFile::fileReload,
@ -28,10 +29,11 @@ QVariant TagListModel::data(const QModelIndex &index, int role) const
if (!index.isValid()) if (!index.isValid())
return QVariant(); return QVariant();
if (role != Qt::DisplayRole) if (role != Qt::DisplayRole && role != Qt::EditRole)
return QVariant(); return QVariant();
return cached_tags.at(index.row()).name; auto rv = cached_tags.at(index.row()).name;
return rv;
} }
QVariant TagListModel::headerData(int section, Qt::Orientation orientation, int role) const QVariant TagListModel::headerData(int section, Qt::Orientation orientation, int role) const

View file

@ -7,6 +7,8 @@
class TagListModel : public QAbstractListModel class TagListModel : public QAbstractListModel
{ {
Q_OBJECT
public: public:
TagListModel(SQLiteSaveFile &backend); TagListModel(SQLiteSaveFile &backend);

View file

@ -1,26 +1,24 @@
#include "tagproptablemodel.h" #include "tagproptablemodel.h"
TagPropTableModel::TagPropTableModel(SQLiteSaveFile &backend, const Tag tag) TagPropTableModel::TagPropTableModel(SQLiteSaveFile &backend, const Tag tag)
: backend(backend) : TagPropTableModel(backend, true)
, tag_cached(tag)
, tagIsValid(true)
{ {
tag_cached = tag;
tag_keys = tag_cached.metadata.keys(); tag_keys = tag_cached.metadata.keys();
tag_keys.sort(); tag_keys.sort();
connect(&backend, &SQLiteSaveFile::tagChange, qDebug() << "connecting TagPropTableModel" << &backend;
this, &TagPropTableModel::tagChange);
connect(&backend, &SQLiteSaveFile::fileReload,
[=]() {
beginResetModel();
tagIsValid = false;
endResetModel();
});
} }
TagPropTableModel::TagPropTableModel(SQLiteSaveFile &backend) TagPropTableModel::TagPropTableModel(SQLiteSaveFile &backend, bool tagIsValid)
: backend(backend) : backend(backend)
, tagIsValid(false) , tagIsValid(tagIsValid)
{ {
connect(&backend, &SQLiteSaveFile::tagChange, this, &TagPropTableModel::tagChange);
connect(&backend, &SQLiteSaveFile::fileReload, [=]() {
beginResetModel();
this->tagIsValid = false;
endResetModel();
});
} }
int TagPropTableModel::rowCount(const QModelIndex &parent) const int TagPropTableModel::rowCount(const QModelIndex &parent) const
@ -42,7 +40,7 @@ QVariant TagPropTableModel::data(const QModelIndex &index, int role) const
if (!index.isValid()) if (!index.isValid())
return QVariant(); return QVariant();
if (role != Qt::DisplayRole) if (role != Qt::DisplayRole && role != Qt::EditRole)
return QVariant(); return QVariant();
bool label = index.column() == 0; bool label = index.column() == 0;
@ -50,7 +48,7 @@ QVariant TagPropTableModel::data(const QModelIndex &index, int role) const
switch (index.row()) { switch (index.row()) {
case 0: return label ? QVariant("Database ID") : tag_cached.id; break; case 0: return label ? QVariant("Database ID") : tag_cached.id; break;
case 1: return label ? QVariant("Label") : tag_cached.name; break; case 1: return label ? QVariant("Label") : tag_cached.name; break;
case 2: return label ? QVariant("Anchor") : tag_cached.anchor; break; case 2: return label ? QVariant("Anchor") : QString("%1, %2").arg(tag_cached.anchor.x()).arg(tag_cached.anchor.y()); break;
} }
int idx = index.row() - 3; int idx = index.row() - 3;
@ -100,18 +98,19 @@ bool TagPropTableModel::setData(const QModelIndex &index, const QVariant &value,
if (role != Qt::EditRole) if (role != Qt::EditRole)
return false; return false;
if (index.row() == 1) if (index.row() == 1) {
tag_cached.name = value.toString(); tag_cached.name = value.toString();
else if (index.row() < 3) } else if (index.row() < 3) {
return false; 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 { } else {
tag_cached.metadata[tag_keys[idx]] = value.toString(); 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); backend.updateTag(tag_cached);
@ -120,13 +119,17 @@ bool TagPropTableModel::setData(const QModelIndex &index, const QVariant &value,
void TagPropTableModel::tagChange(TagChange change, const Tag &tag) void TagPropTableModel::tagChange(TagChange change, const Tag &tag)
{ {
auto dbg = qDebug() << QString("TagPropTableModel::tagChange(%1, %2 \"%3\")").arg(change).arg(tag.id).arg(tag.name);
if (tag.id != tag_cached.id) if (tag.id != tag_cached.id)
return; return;
assert(change != TagChange::CREATED); assert(change != TagChange::CREATED);
if (change == TagChange::CHANGED) { if (change == TagChange::CHANGED) {
dbg << "changed";
showTag(tag); showTag(tag);
} else if (change == TagChange::DELETED) { } else if (change == TagChange::DELETED) {
dbg << "deleted";
beginResetModel(); beginResetModel();
tagIsValid = false; tagIsValid = false;
endResetModel(); endResetModel();
@ -140,5 +143,6 @@ void TagPropTableModel::showTag(const Tag &tag)
tag_cached = tag; tag_cached = tag;
tag_keys = tag_cached.metadata.keys(); tag_keys = tag_cached.metadata.keys();
tag_keys.sort(); tag_keys.sort();
tagIsValid = true;
endResetModel(); endResetModel();
} }

View file

@ -9,8 +9,10 @@
class TagPropTableModel : public QAbstractTableModel class TagPropTableModel : public QAbstractTableModel
{ {
Q_OBJECT
public: public:
TagPropTableModel(SQLiteSaveFile &backend); TagPropTableModel(SQLiteSaveFile &backend) : TagPropTableModel(backend, false) {};
TagPropTableModel(SQLiteSaveFile &backend, const Tag tag); TagPropTableModel(SQLiteSaveFile &backend, const Tag tag);
int rowCount(const QModelIndex &parent=QModelIndex()) const override; int rowCount(const QModelIndex &parent=QModelIndex()) const override;
@ -27,6 +29,8 @@ private slots:
void tagChange(TagChange change, const Tag &tag); void tagChange(TagChange change, const Tag &tag);
private: private:
TagPropTableModel(SQLiteSaveFile &backend, bool tagIsValid);
SQLiteSaveFile &backend; SQLiteSaveFile &backend;
Tag tag_cached; Tag tag_cached;
QStringList tag_keys; QStringList tag_keys;

View file

@ -2,15 +2,16 @@
#include <QGraphicsSceneMouseEvent> #include <QGraphicsSceneMouseEvent>
TagScene::TagScene(SQLiteSaveFile &proj) void TagScene::setProject(SQLiteSaveFile *proj) {
: proj(proj) if (m_proj)
{ disconnect(m_proj, nullptr, this, nullptr);
reloadPicture(); m_proj = proj;
reloadTags();
connect(&proj, &SQLiteSaveFile::tagChange, this, &TagScene::tagChanged); reloadScene();
connect(&proj, &SQLiteSaveFile::fileReload,
[=]() { reloadTags(); }); connect(m_proj, &SQLiteSaveFile::tagChange, this, &TagScene::tagChanged);
connect(m_proj, &SQLiteSaveFile::fileReload, this, &TagScene::reloadScene);
connect(m_proj, &SQLiteSaveFile::imageLoaded, this, &TagScene::reloadPicture);
} }
void TagScene::tagChanged(TagChange change, const Tag &tag) void TagScene::tagChanged(TagChange change, const Tag &tag)
@ -40,11 +41,22 @@ void TagScene::tagChanged(TagChange change, const Tag &tag)
void TagScene::reloadPicture() void TagScene::reloadPicture()
{ {
pix.loadFromData(proj.getImage()); if (!m_proj)
return;
pix.loadFromData(m_proj->getImage());
if (pix_it) if (pix_it)
removeItem(pix_it); removeItem(pix_it);
pix_it = new QGraphicsPixmapItem(pix); pix_it = new QGraphicsPixmapItem(pix);
pix_it->setZValue(-1);
addItem(pix_it); addItem(pix_it);
QRectF imgBounds = pix_it->boundingRect();
imgBounds = imgBounds.marginsAdded(QMargins(imgBounds.width() * 0.5, imgBounds.height() * 0.5, imgBounds.width() * 0.5, imgBounds.height() * 0.5));
setSceneRect(itemsBoundingRect().united(imgBounds));
qDebug() << "TagScene: reloadPicture()" << pix_it->boundingRect() << pix_it << pix << pix_it->isActive();
invalidate();
imageLoaded();
} }
void TagScene::addTag(const Tag tag) { void TagScene::addTag(const Tag tag) {
@ -53,16 +65,20 @@ void TagScene::addTag(const Tag tag) {
tags[tag.id] = it; tags[tag.id] = it;
} }
void TagScene::reloadTags() void TagScene::reloadScene()
{ {
clear(); clear();
for (auto *it : tags.values()) { for (auto *it : tags.values())
delete it; delete it;
} pix_it = nullptr;
for (const Tag &tag : proj.getAllTags()) { if (!m_proj)
return;
for (const Tag &tag : m_proj->getAllTags())
addTag(tag); addTag(tag);
}
reloadPicture(); /* calls invalidate() for us */
} }
void TagScene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) void TagScene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
@ -70,11 +86,18 @@ void TagScene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
QGraphicsItem *it = itemAt(event->scenePos(), QTransform()); QGraphicsItem *it = itemAt(event->scenePos(), QTransform());
if (!it) { if (!it) {
QGraphicsScene::mouseDoubleClickEvent(event); QGraphicsScene::mouseDoubleClickEvent(event);
return;
}
if (it == pix_it) {
m_proj->createTagAt(event->scenePos());
return;
} }
TagItem *tagitem = qgraphicsitem_cast<TagItem *>(it); TagItem *tagitem = qgraphicsitem_cast<TagItem *>(it);
if (!tagitem) { if (!tagitem) {
QGraphicsScene::mouseDoubleClickEvent(event); QGraphicsScene::mouseDoubleClickEvent(event);
return;
} }
tagDoubleClicked(tagitem->tag()); tagDoubleClicked(tagitem->tag());

View file

@ -14,14 +14,18 @@ class TagScene : public QGraphicsScene
Q_OBJECT Q_OBJECT
public: public:
TagScene(SQLiteSaveFile &proj); TagScene() {}
const QGraphicsPixmapItem *backgroundPixmapItem() const { return pix_it; }
public slots: public slots:
void reloadPicture(); void reloadPicture();
void reloadTags(); void reloadScene();
void setProject(SQLiteSaveFile *proj);
signals: signals:
void tagDoubleClicked(const Tag &tag); void tagDoubleClicked(const Tag &tag);
void imageLoaded();
protected: protected:
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
@ -32,9 +36,9 @@ private slots:
private: private:
void addTag(const Tag tag); void addTag(const Tag tag);
SQLiteSaveFile &proj; SQLiteSaveFile *m_proj = nullptr;
QGraphicsPixmapItem *pix_it; QGraphicsPixmapItem *pix_it = nullptr;
QPixmap pix; QPixmap pix;
QMap<long long int, TagItem*> tags; QMap<long long int, TagItem*> tags;
}; };

View file

@ -4,34 +4,41 @@
#include <QScrollBar> #include <QScrollBar>
#include <cmath> #include <cmath>
TagView::TagView(SQLiteSaveFile &proj) TagView::TagView(QWidget *parent)
: scene(proj) : QGraphicsView(parent)
, proj(proj) , saveCenterTimer(this)
, saveCenterTimer()
{ {
setDragMode(QGraphicsView::ScrollHandDrag); setDragMode(QGraphicsView::ScrollHandDrag);
setScene(&scene); setScene(&m_scene);
connect(&scene, &TagScene::tagDoubleClicked, this, &TagView::tagDoubleClicked); connect(&m_scene, &TagScene::tagDoubleClicked, this, &TagView::tagDoubleClicked);
connect(&m_scene, &TagScene::imageLoaded, this, &TagView::zoomToFit);
saveCenterTimer.setSingleShot(true); saveCenterTimer.setSingleShot(true);
saveCenterTimer.setInterval(500); saveCenterTimer.setInterval(500);
connect(&saveCenterTimer, &QTimer::timeout, connect(&saveCenterTimer, &QTimer::timeout, this, &TagView::saveCenter);
this, &TagView::saveCenter); }
TagView::~TagView()
{
were_done = true;
} }
void TagView::zoomToFit() void TagView::zoomToFit()
{ {
QTransform tx = QTransform().rotate(-rotation); QTransform tx = QTransform().rotate(-rotation);
QRectF rect = tx.mapRect(scene.itemsBoundingRect()); QRectF rect = tx.mapRect(m_scene.itemsBoundingRect());
QRectF vp = viewport()->rect(); QRectF vp = viewport()->rect();
setZoom(qMin(vp.width()/rect.width(), vp.height()/rect.height())); setZoom(qMin(vp.width()/rect.width(), vp.height()/rect.height()));
centerOn(m_scene.backgroundPixmapItem());
qDebug() << "TagView::zoomToFit():" << sceneRect() << rect << viewport()->rect();
} }
void TagView::setZoom(qreal zoom) void TagView::setZoom(qreal zoom)
{ {
this->zoom = zoom; this->zoom = zoom;
proj.setMeta("view_zoom", zoom); if (m_proj)
m_proj->setMeta("view_zoom", zoom);
setTransform(QTransform::fromScale(zoom, zoom).rotate(rotation)); setTransform(QTransform::fromScale(zoom, zoom).rotate(rotation));
} }
@ -47,7 +54,15 @@ void TagView::rotate(int angle)
if (tmp < 0) if (tmp < 0)
tmp += 360; tmp += 360;
rotation = tmp; rotation = tmp;
proj.setMeta("view_rotation", rotation); if (m_proj)
m_proj->setMeta("view_rotation", rotation);
}
void TagView::setProject(SQLiteSaveFile *proj)
{
m_proj = proj;
m_scene.setProject(m_proj);
restoreViewport();
} }
void TagView::wheelEvent(QWheelEvent *evt) void TagView::wheelEvent(QWheelEvent *evt)
@ -63,22 +78,34 @@ void TagView::wheelEvent(QWheelEvent *evt)
} }
} }
void TagView::scrollContentsBy(int dx, int dy)
{
QGraphicsView::scrollContentsBy(dx, dy);
/* Hackety hack: On object destruction this method is called downstream in the destructor chain. Prevent segfaults from calling an uninitialized timer. */
if (!were_done)
saveCenterTimer.start();
}
void TagView::saveCenter() void TagView::saveCenter()
{ {
QPointF p = mapToScene(viewport()->rect().center()); QPointF p = mapToScene(viewport()->rect().center());
proj.setMeta("view_center", QJsonDocument(QJsonArray({p.x(), p.y()})).toJson()); if (m_proj)
m_proj->setMeta("view_center", QJsonDocument(QJsonArray({p.x(), p.y()})).toJson());
} }
void TagView::restoreViewport() void TagView::restoreViewport()
{ {
QVariant v_rot = proj.getMeta("view_rotation"); if (!m_proj)
return;
QVariant v_rot = m_proj->getMeta("view_rotation");
if (v_rot.isValid()) { if (v_rot.isValid()) {
rotation = v_rot.toInt(); rotation = v_rot.toInt();
} else { } else {
rotation = 0; rotation = 0;
} }
QVariant v_zoom = proj.getMeta("view_zoom"); QVariant v_zoom = m_proj->getMeta("view_zoom");
if (v_zoom.isValid()) { if (v_zoom.isValid()) {
zoom = v_zoom.toDouble(); zoom = v_zoom.toDouble();
setTransform(QTransform::fromScale(zoom, zoom).rotate(rotation)); setTransform(QTransform::fromScale(zoom, zoom).rotate(rotation));
@ -86,13 +113,13 @@ void TagView::restoreViewport()
zoomToFit(); zoomToFit();
} }
QVariant v_center = proj.getMeta("view_center"); QVariant v_center = m_proj->getMeta("view_center");
if (v_center.isValid()) { if (v_center.isValid()) {
QJsonArray arr = QJsonDocument::fromJson(v_center.toByteArray()).toVariant().toJsonArray(); QJsonArray arr = QJsonDocument::fromJson(v_center.toByteArray()).toVariant().toJsonArray();
assert(arr.size() == 2); assert(arr.size() == 2);
assert(arr[0].isDouble() && arr[1].isDouble()); assert(arr[0].isDouble() && arr[1].isDouble());
centerOn(QPointF(arr[0].toDouble(), arr[1].toDouble())); centerOn(QPointF(arr[0].toDouble(), arr[1].toDouble()));
} else { } else {
centerOn(scene.itemsBoundingRect().center()); centerOn(m_scene.itemsBoundingRect().center());
} }
} }

View file

@ -13,19 +13,24 @@ class TagView : public QGraphicsView
Q_OBJECT Q_OBJECT
public: public:
TagView(SQLiteSaveFile &proj); TagView(QWidget *parent=nullptr);
virtual ~TagView();
public slots: public slots:
void zoomToFit(); void zoomToFit();
void setZoom(qreal zoom); void setZoom(qreal zoom);
void zoomIn(qreal delta); void zoomIn(qreal delta=120);
void zoomOut(qreal delta=120) { zoomIn(-delta); }
void rotate(int angle); void rotate(int angle);
void setProject(SQLiteSaveFile *proj);
signals: signals:
void tagDoubleClicked(const Tag &tag); void tagDoubleClicked(const Tag &tag);
protected: protected:
void wheelEvent(QWheelEvent *evt) override; void wheelEvent(QWheelEvent *evt) override;
void scrollContentsBy(int dx, int dy) override;
private slots: private slots:
void saveCenter(); void saveCenter();
@ -33,9 +38,10 @@ private slots:
private: private:
void restoreViewport(); void restoreViewport();
TagScene scene;
SQLiteSaveFile &proj;
QTimer saveCenterTimer; QTimer saveCenterTimer;
bool were_done = false;
TagScene m_scene;
SQLiteSaveFile *m_proj = nullptr;
int rotation; int rotation;
double zoom; double zoom;
}; };