feat(StatusQ.Models): Adding SingleModelItemData helper component (#14891)

SingleModelItemData is a generic component that can provide a live object extract from an arbitrary QAbstractItemModel*
This commit is contained in:
Alex Jbanca 2024-06-04 13:08:52 +03:00 committed by GitHub
parent f1308f3b28
commit 4e81f8f220
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 2632 additions and 83 deletions

View File

@ -0,0 +1,154 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ 0.1
import StatusQ.Core.Utils 0.1
import Models 1.0
import Storybook 1.0
Control {
id: root
UsersModel {
id: usersModel
}
ModelEntry {
id: itemData
sourceModel: usersModel
key: "pubKey"
value: pubKeySelector.currentText
}
contentItem: ColumnLayout {
anchors.fill: parent
Pane {
Layout.fillWidth: true
background: Rectangle {
border.width: 1
border.color: "lightgray"
}
contentItem: ColumnLayout {
Label {
text: "User with pubKey " + itemData.value
font.bold: true
}
Label {
text: "Data available: " + itemData.available
font.bold: true
}
Label {
text: "Keys: " + itemData.roles
font.bold: true
}
Label {
text: "Item removed from model: " + itemData.itemRemovedFromModel
font.bold: true
}
}
}
Loader {
Layout.fillWidth: true
active: itemData.available
sourceComponent: Pane {
background: Rectangle {
border.width: 1
border.color: "lightgray"
}
contentItem: ColumnLayout {
Repeater {
model: itemData.roles
delegate: Label {
text: modelData + ": " + itemData.item[modelData]
}
}
}
}
}
GenericListView {
Layout.fillWidth: true
Layout.fillHeight: true
model: usersModel
insetComponent: RowLayout {
Button {
height: 20
font.pixelSize: 11
text: "remove"
highlighted: model.index === itemData.row
onClicked: {
usersModel.remove(model.index)
}
}
Button {
height: 20
font.pixelSize: 11
text: "edit"
highlighted: model.index === itemData.row
onClicked: {
menu.row = model.index
menu.popup()
}
}
}
}
Pane {
contentItem: RowLayout {
ComboBox {
id: pubKeySelector
model: [...ModelUtils.modelToFlatArray(usersModel, "pubKey"), "none"]
}
CheckBox {
text: "Cache item on removal"
checked: itemData.cacheOnRemoval
onCheckedChanged: {
itemData.cacheOnRemoval = checked
}
}
}
}
}
Menu {
id: menu
property int row: -1
readonly property var modelItem: usersModel.get(row)
contentItem: ColumnLayout {
Label {
text: "Edit user"
font.bold: true
}
TextField {
id: pubKeyField
placeholderText: "pubKey"
enabled: !!menu.modelItem
text: !!menu.modelItem ? menu.modelItem.pubKey : ""
onAccepted: usersModel.setProperty(menu.row, "pubKey", pubKeyField.text)
}
TextField {
id: displayNameField
placeholderText: "displayName"
enabled: !!menu.modelItem
text: !!menu.modelItem ? menu.modelItem.displayName : ""
onAccepted: usersModel.setProperty(menu.row, "displayName", displayNameField.text)
}
TextField {
id: ensNameField
placeholderText: "ensName"
enabled: !!menu.modelItem
text: !!menu.modelItem ? menu.modelItem.ensName : ""
onAccepted: usersModel.setProperty(menu.row, "ensName", ensNameField.text)
}
}
}
}
// category: Models

View File

@ -207,3 +207,5 @@ Item {
} }
} }
} }
// category: Models

View File

@ -101,12 +101,15 @@ add_library(StatusQ SHARED
include/StatusQ/formatteddoubleproperty.h include/StatusQ/formatteddoubleproperty.h
include/StatusQ/functionaggregator.h include/StatusQ/functionaggregator.h
include/StatusQ/leftjoinmodel.h include/StatusQ/leftjoinmodel.h
include/StatusQ/modelentry.h
include/StatusQ/modelsyncedcontainer.h include/StatusQ/modelsyncedcontainer.h
include/StatusQ/modelutilsinternal.h include/StatusQ/modelutilsinternal.h
include/StatusQ/movablemodel.h include/StatusQ/movablemodel.h
include/StatusQ/permissionutilsinternal.h include/StatusQ/permissionutilsinternal.h
include/StatusQ/rolesrenamingmodel.h include/StatusQ/rolesrenamingmodel.h
include/StatusQ/rxvalidator.h include/StatusQ/rxvalidator.h
include/StatusQ/snapshotmodel.h
include/StatusQ/snapshotobject.h
include/StatusQ/singleroleaggregator.h include/StatusQ/singleroleaggregator.h
include/StatusQ/statussyntaxhighlighter.h include/StatusQ/statussyntaxhighlighter.h
include/StatusQ/statuswindow.h include/StatusQ/statuswindow.h
@ -124,6 +127,7 @@ add_library(StatusQ SHARED
src/formatteddoubleproperty.cpp src/formatteddoubleproperty.cpp
src/functionaggregator.cpp src/functionaggregator.cpp
src/leftjoinmodel.cpp src/leftjoinmodel.cpp
src/modelentry.cpp
src/modelutilsinternal.cpp src/modelutilsinternal.cpp
src/movablemodel.cpp src/movablemodel.cpp
src/permissionutilsinternal.cpp src/permissionutilsinternal.cpp
@ -131,6 +135,8 @@ add_library(StatusQ SHARED
src/rolesrenamingmodel.cpp src/rolesrenamingmodel.cpp
src/rxvalidator.cpp src/rxvalidator.cpp
src/singleroleaggregator.cpp src/singleroleaggregator.cpp
src/snapshotmodel.cpp
src/snapshotobject.cpp
src/statussyntaxhighlighter.cpp src/statussyntaxhighlighter.cpp
src/statuswindow.cpp src/statuswindow.cpp
src/stringutilsinternal.cpp src/stringutilsinternal.cpp

View File

@ -0,0 +1,91 @@
#pragma once
#include <QAbstractItemModel>
#include <QPointer>
#include <QQmlEngine>
#include <QQmlPropertyMap>
class ModelEntry : public QObject
{
Q_OBJECT
////////////// input
// the source model to get the item from
Q_PROPERTY(QAbstractItemModel* sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged REQUIRED)
// the key role used to search for the item
Q_PROPERTY(QString key READ key WRITE setKey NOTIFY keyChanged REQUIRED)
// the value role used to cache the item
Q_PROPERTY(QVariant value READ value WRITE setValue NOTIFY valueChanged REQUIRED)
// whether to cache the item when it is removed from the model
// if true, the item will be cached and available until another source model is used or the cacheOnRemoval is set to false
Q_PROPERTY(bool cacheOnRemoval READ cacheOnRemoval WRITE setCacheOnRemoval NOTIFY cacheOnRemovalChanged)
///////////// output
// the item found in the source model
Q_PROPERTY(QQmlPropertyMap* item READ item NOTIFY itemChanged)
// whether the item is available
Q_PROPERTY(bool available READ available NOTIFY availableChanged)
// the roles of the item
Q_PROPERTY(QStringList roles READ roles NOTIFY rolesChanged)
// the row of the item in the source model, -1 if not available or removed
Q_PROPERTY(int row READ row NOTIFY rowChanged)
// whether the item was removed from the model. This flag is only set when cacheOnRemoval is true
Q_PROPERTY(bool itemRemovedFromModel READ itemRemovedFromModel NOTIFY itemRemovedFromModelChanged)
public:
explicit ModelEntry(QObject* parent = nullptr);
QAbstractItemModel* sourceModel() const;
QString key() const;
QVariant value() const;
bool cacheOnRemoval() const;
QQmlPropertyMap* item() const;
bool available() const;
const QStringList& roles() const;
int row() const;
bool itemRemovedFromModel() const;
protected:
void setSourceModel(QAbstractItemModel* sourceModel);
void setKey(const QString& key);
void setValue(const QVariant& value);
void setIndex(const QModelIndex& index);
void setAvailable(bool available);
void setRoles(const QStringList& roles);
void setRow(int row);
void setCacheOnRemoval(bool cacheOnRemoval);
void setItemRemovedFromModel(bool itemRemovedFromModel);
void resetIndex();
void tryItemResetOrUpdate();
void resetItem();
void updateItem(const QList<int>& roles = {});
QModelIndex findIndexInRange(int start, int end, const QList<int>& roles = {}) const;
bool itemHasCorrectRoles() const;
void cacheItem();
void resetCachedItem();
signals:
void sourceModelChanged();
void keyChanged();
void valueChanged();
void itemChanged();
void availableChanged();
void rolesChanged();
void rowChanged();
void cacheOnRemovalChanged();
void itemRemovedFromModelChanged();
private:
QScopedPointer<QQmlPropertyMap> m_item{nullptr};
QPointer<QAbstractItemModel> m_sourceModel{nullptr};
QPersistentModelIndex m_index;
bool m_available{false};
QStringList m_roles;
int m_row{-1};
bool m_cacheOnRemoval{false};
bool m_itemRemovedFromModel{false};
QVariant m_value;
QString m_key;
};

View File

@ -2,21 +2,26 @@
#include <QAbstractListModel> #include <QAbstractListModel>
class SnapshotModel : public QAbstractListModel { class SnapshotModel : public QAbstractListModel
{
Q_OBJECT
public: public:
explicit SnapshotModel(QObject* parent = nullptr); explicit SnapshotModel(QObject* parent = nullptr);
explicit SnapshotModel(const QAbstractItemModel& model, bool recursive = true, explicit SnapshotModel(const QAbstractItemModel& model, bool recursive = true, QObject* parent = nullptr);
QObject* parent = nullptr);
~SnapshotModel();
int rowCount(const QModelIndex& parent = {}) const override; int rowCount(const QModelIndex& parent = {}) const override;
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
QVariant data(const QModelIndex& index, int role) const override; QVariant data(const QModelIndex& index, int role) const override;
void grabSnapshot(const QAbstractItemModel& model, bool recursive = true); void grabSnapshot(const QAbstractItemModel& model, bool recursive = true);
void clearSnapshot();
QVariant data(int row, int role) const; QVariant data(int row, int role) const;
private: private:
QHash<int, QList<QVariant>> m_data; QHash<int, QList<QVariant>> m_data;
QHash<int, QByteArray> m_roles; QHash<int, QByteArray> m_roles;
}; };

View File

@ -0,0 +1,38 @@
#pragma once
#include <QObject>
#include <QVariantMap>
class QAbstractItemModel;
class SnapshotObject : public QObject
{
Q_OBJECT
Q_PROPERTY(QVariant snapshot READ snapshot NOTIFY snapshotChanged)
Q_PROPERTY(bool available READ available NOTIFY availableChanged)
public:
explicit SnapshotObject(QObject* parent = nullptr);
explicit SnapshotObject(const QObject* object, QObject* parent);
QVariant snapshot() const;
bool available() const;
Q_INVOKABLE void grabSnapshot(const QObject* object);
signals:
void snapshotChanged();
void availableChanged();
private:
void setAvailable(bool available);
void setSnapshot(const QVariant& snapshot);
QVariantMap objectToVariantMap(const QObject* object);
QVariant objectToVariant(const QObject* object);
QVariant modelToVariant(const QAbstractItemModel* model);
void insertIntoVariantMap(QVariantMap& map, const QString& key, const QVariant& value);
QVariant m_snapshot;
bool m_available{false};
};

View File

@ -0,0 +1,346 @@
#include "StatusQ/modelentry.h"
#include "StatusQ/snapshotmodel.h"
#include "StatusQ/snapshotobject.h"
ModelEntry::ModelEntry(QObject* parent)
: QObject(parent)
, m_item(new QQmlPropertyMap(this))
{ }
QQmlPropertyMap* ModelEntry::item() const
{
return m_item.data();
}
QAbstractItemModel* ModelEntry::sourceModel() const
{
return m_sourceModel.data();
}
QString ModelEntry::key() const
{
return m_key;
}
QVariant ModelEntry::value() const
{
return m_value;
}
bool ModelEntry::available() const
{
return m_available;
}
const QStringList& ModelEntry::roles() const
{
return m_roles;
}
int ModelEntry::row() const
{
return m_row;
}
bool ModelEntry::cacheOnRemoval() const
{
return m_cacheOnRemoval;
}
bool ModelEntry::itemRemovedFromModel() const
{
return m_itemRemovedFromModel;
}
void ModelEntry::setSourceModel(QAbstractItemModel* sourceModel)
{
if(m_sourceModel == sourceModel) return;
if(m_sourceModel)
{
disconnect(m_sourceModel, nullptr, this, nullptr);
}
m_sourceModel = sourceModel;
resetCachedItem();
resetIndex();
if(!m_sourceModel)
{
emit sourceModelChanged();
return;
}
connect(m_sourceModel, &QAbstractItemModel::modelReset, this, [this]() { resetIndex(); });
connect(m_sourceModel, &QAbstractItemModel::rowsInserted, this, [this]() {
if(!m_index.isValid())
{
resetIndex();
}
});
connect(m_sourceModel,
&QAbstractItemModel::rowsMoved,
this,
[this](const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row) {
if(!m_index.isValid()) return;
if(m_index.row() >= destination.row() && m_index.row() <= destination.row() + (end - start))
{
emit rowChanged();
}
});
connect(m_sourceModel,
&QAbstractItemModel::rowsAboutToBeRemoved,
this,
[this](const QModelIndex& parent, int first, int last) {
if(!m_index.isValid()) return;
if(m_index.row() < first || m_index.row() > last) return;
if(m_cacheOnRemoval)
{
cacheItem();
setItemRemovedFromModel(true);
setRow(-1);
return;
}
setIndex({});
});
connect(m_sourceModel,
&QAbstractItemModel::dataChanged,
this,
[this](const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList<int>& roles = QList<int>()) {
if(!m_index.isValid())
{
auto index = findIndexInRange(topLeft.row(), bottomRight.row() + 1);
setIndex(index);
return;
}
// Check if the index is still valid
auto index = findIndexInRange(m_index.row(), m_index.row() + 1, roles);
if(index != m_index)
{
setIndex(index);
return;
}
if(topLeft.row() <= m_index.row() && m_index.row() <= bottomRight.row())
{
updateItem(roles);
}
});
connect(m_sourceModel, &QAbstractItemModel::layoutChanged, this, [this]() {
if(!m_index.isValid())
{
// Resetting just to cover cases where the rows are removed after the layout change
resetItem();
}
setRow(m_index.row());
});
emit sourceModelChanged();
}
void ModelEntry::setKey(const QString& key)
{
if(m_key == key) return;
m_key = key;
resetIndex();
emit keyChanged();
}
void ModelEntry::setValue(const QVariant& value)
{
if(m_value == value) return;
m_value = value;
resetIndex();
emit valueChanged();
}
void ModelEntry::setIndex(const QModelIndex& index)
{
if(m_index == index) return;
m_index = index;
tryItemResetOrUpdate();
setRow(m_index.row());
}
void ModelEntry::setAvailable(bool available)
{
if(available == m_available) return;
m_available = available;
emit availableChanged();
}
void ModelEntry::setRoles(const QStringList& roles)
{
if(m_roles.size() == roles.size() && !m_roles.empty() &&
std::all_of(roles.begin(), roles.end(), [this](const QString& role) { return m_roles.contains(role); }))
return;
m_roles = roles;
emit rolesChanged();
}
void ModelEntry::setRow(int row)
{
if(m_row == row) return;
m_row = row;
emit rowChanged();
}
void ModelEntry::setCacheOnRemoval(bool cacheOnRemoval)
{
if(m_cacheOnRemoval == cacheOnRemoval) return;
resetCachedItem();
m_cacheOnRemoval = cacheOnRemoval;
emit cacheOnRemovalChanged();
}
void ModelEntry::setItemRemovedFromModel(bool itemRemovedFromModel)
{
if(m_itemRemovedFromModel == itemRemovedFromModel) return;
m_itemRemovedFromModel = itemRemovedFromModel;
emit itemRemovedFromModelChanged();
}
QModelIndex ModelEntry::findIndexInRange(int start, int end, const QList<int>& roles) const
{
if(!m_sourceModel || m_key.isEmpty()) return {};
auto keysForRole = m_sourceModel->roleNames().keys(m_key.toUtf8());
// no matching roles found
if(keysForRole.isEmpty() || (!roles.isEmpty() && !roles.contains(keysForRole.first()))) return {};
for(int i = start; i < end; i++)
{
auto index = m_sourceModel->index(i, 0);
auto data = index.data(keysForRole.first());
if(data == m_value) return index;
}
return {};
}
void ModelEntry::resetIndex()
{
auto index = QModelIndex();
if(m_sourceModel) index = findIndexInRange(0, m_sourceModel->rowCount());
setIndex(index);
}
void ModelEntry::tryItemResetOrUpdate()
{
if(!m_index.isValid() || !itemHasCorrectRoles())
{
resetItem();
return;
}
updateItem();
setAvailable(true);
}
void ModelEntry::resetItem()
{
// Signal order is important here
if(!m_index.isValid())
{
setAvailable(false);
}
m_item.reset(new QQmlPropertyMap());
updateItem();
if(!m_index.isValid())
{
setRoles(m_item->keys());
}
emit itemChanged();
if(m_index.isValid())
{
setRoles(m_item->keys());
setAvailable(true);
}
}
void ModelEntry::updateItem(const QList<int>& roles /*{}*/)
{
if(!m_index.isValid() || !m_sourceModel) return;
const auto& rolesRef = roles.isEmpty() ? m_sourceModel->roleNames().keys() : roles;
for(auto role : rolesRef)
{
auto roleName = m_sourceModel->roleNames().value(role);
auto roleValue = m_index.data(role);
if(roleValue == m_item->value(roleName)) continue;
m_item->insert(roleName, roleValue);
emit m_item->valueChanged(roleName, roleValue);
}
setItemRemovedFromModel(false);
}
bool ModelEntry::itemHasCorrectRoles() const
{
if(!m_sourceModel || !m_item) return false;
auto itemKeys = m_item->keys();
auto modelRoles = m_sourceModel->roleNames().values();
return std::all_of(modelRoles.cbegin(),
modelRoles.cend(),
[itemKeys](const QByteArray& role) { return itemKeys.contains(role); }) &&
itemKeys.size() == modelRoles.size();
}
void ModelEntry::cacheItem()
{
if(!m_cacheOnRemoval) return;
for(const auto& role : qAsConst(m_roles))
{
auto roleName = m_sourceModel->roleNames().key(role.toUtf8());
auto roleValue = m_index.data(roleName);
if(roleValue.canConvert<QAbstractItemModel*>())
{
m_item->insert(role,
QVariant::fromValue(new SnapshotModel(*roleValue.value<QAbstractItemModel*>(), true, m_item.data())));
}
else if(roleValue.canConvert<QObject*>())
{
const auto obj = roleValue.value<QObject*>();
const auto snapshot = new SnapshotObject(obj, m_item.data());
m_item->insert(role, QVariant::fromValue(snapshot->snapshot()));
}
}
}
void ModelEntry::resetCachedItem()
{
if(!m_cacheOnRemoval || !m_itemRemovedFromModel) return;
resetIndex();
tryItemResetOrUpdate();
setItemRemovedFromModel(false);
}

View File

@ -16,6 +16,8 @@
#include "StatusQ/permissionutilsinternal.h" #include "StatusQ/permissionutilsinternal.h"
#include "StatusQ/rolesrenamingmodel.h" #include "StatusQ/rolesrenamingmodel.h"
#include "StatusQ/rxvalidator.h" #include "StatusQ/rxvalidator.h"
#include "StatusQ/modelentry.h"
#include "StatusQ/snapshotobject.h"
#include "StatusQ/statussyntaxhighlighter.h" #include "StatusQ/statussyntaxhighlighter.h"
#include "StatusQ/statuswindow.h" #include "StatusQ/statuswindow.h"
#include "StatusQ/stringutilsinternal.h" #include "StatusQ/stringutilsinternal.h"
@ -62,6 +64,8 @@ public:
qmlRegisterType<FormattedDoubleProperty>("StatusQ", 0, 1, "FormattedDoubleProperty"); qmlRegisterType<FormattedDoubleProperty>("StatusQ", 0, 1, "FormattedDoubleProperty");
qmlRegisterSingletonType<QClipboardProxy>("StatusQ", 0, 1, "QClipboardProxy", &QClipboardProxy::qmlInstance); qmlRegisterSingletonType<QClipboardProxy>("StatusQ", 0, 1, "QClipboardProxy", &QClipboardProxy::qmlInstance);
qmlRegisterType<ModelEntry>("StatusQ", 0, 1, "ModelEntry");
qmlRegisterType<SnapshotObject>("StatusQ", 0, 1, "SnapshotObject");
qmlRegisterSingletonType<ModelUtilsInternal>( qmlRegisterSingletonType<ModelUtilsInternal>(
"StatusQ.Internal", 0, 1, "ModelUtils", &ModelUtilsInternal::qmlInstance); "StatusQ.Internal", 0, 1, "ModelUtils", &ModelUtilsInternal::qmlInstance);

View File

@ -0,0 +1,100 @@
#include "StatusQ/snapshotmodel.h"
#include "StatusQ/snapshotobject.h"
#include <QDebug>
SnapshotModel::SnapshotModel(QObject* parent)
: QAbstractListModel(parent)
{ }
SnapshotModel::SnapshotModel(const QAbstractItemModel& model, bool recursive, QObject* parent)
: QAbstractListModel(parent)
{
grabSnapshot(model, recursive);
}
SnapshotModel::~SnapshotModel()
{
clearSnapshot();
}
int SnapshotModel::rowCount(const QModelIndex& parent) const
{
if(parent.isValid()) return 0;
return m_data.size() ? m_data.begin()->size() : 0;
}
QHash<int, QByteArray> SnapshotModel::roleNames() const
{
return m_roles;
}
QVariant SnapshotModel::data(const QModelIndex& index, int role) const
{
if(!index.isValid() || !m_roles.contains(role) || index.row() >= rowCount())
{
return {};
}
return m_data[role][index.row()];
}
void SnapshotModel::grabSnapshot(const QAbstractItemModel& model, bool recursive)
{
beginResetModel();
clearSnapshot();
m_roles = model.roleNames();
auto roles = m_roles.keys();
auto count = model.rowCount();
for(auto role : roles)
{
for(int i = 0; i < count; i++)
{
QVariant data = model.data(model.index(i, 0), role);
if(recursive && data.canConvert<QAbstractItemModel*>())
{
const auto submodel = data.value<QAbstractItemModel*>();
m_data[role].push_back(QVariant::fromValue(new SnapshotModel(*submodel, true, this)));
}
else if(recursive && data.canConvert<QObject*>())
{
const auto submodelObject = data.value<QObject*>();
const auto snapshot = new SnapshotObject(submodelObject, this);
connect(this, &SnapshotModel::modelAboutToBeReset, snapshot, &SnapshotObject::deleteLater);
m_data[role].push_back(snapshot->snapshot());
}
else
{
m_data[role].push_back(data);
}
}
}
endResetModel();
}
void SnapshotModel::clearSnapshot()
{
for (auto& data : m_data.values())
{
for (auto& item : data)
{
if (item.canConvert<SnapshotModel*>())
{
item.value<SnapshotModel*>()->deleteLater();
}
}
}
m_data.clear();
}
QVariant SnapshotModel::data(int row, int role) const
{
return data(index(row), role);
}

View File

@ -0,0 +1,132 @@
#include "StatusQ/snapshotobject.h"
#include "StatusQ/snapshotmodel.h"
#include <QDebug>
#include <QMetaProperty>
SnapshotObject::SnapshotObject(QObject* parent)
: QObject(parent)
{ }
SnapshotObject::SnapshotObject(const QObject* object, QObject* parent)
: QObject(parent)
{
grabSnapshot(object);
}
QVariant SnapshotObject::snapshot() const
{
return m_snapshot;
}
bool SnapshotObject::available() const
{
return m_available;
}
void SnapshotObject::setAvailable(bool available)
{
if(m_available == available) return;
m_available = available;
emit availableChanged();
}
void SnapshotObject::setSnapshot(const QVariant& snapshot)
{
if(m_snapshot == snapshot) return;
m_snapshot = snapshot;
// available emit order is important
if (!m_snapshot.isValid()) setAvailable(false);
emit snapshotChanged();
if (m_snapshot.isValid()) setAvailable(true);
}
void SnapshotObject::grabSnapshot(const QObject* object)
{
if(!object)
{
setSnapshot({});
return;
}
// try cast to QAbstractItemModel
if(const auto model = qobject_cast<const QAbstractItemModel*>(object))
{
setSnapshot(modelToVariant(model));
return;
}
setSnapshot(QVariant::fromValue(objectToVariantMap(object)));
}
QVariantMap SnapshotObject::objectToVariantMap(const QObject* object)
{
if(!object)
{
return {};
}
QVariantMap item;
const auto metaObject = object->metaObject();
const auto count = metaObject->propertyCount();
const auto propertyOffset = metaObject->propertyOffset();
for(int i = propertyOffset; i < propertyOffset + count; i++)
{
const auto property = metaObject->property(i);
const auto name = property.name();
const auto value = property.read(object);
insertIntoVariantMap(item, name, value);
}
const auto dynamicPropertyNames = object->dynamicPropertyNames();
for(const auto& name : dynamicPropertyNames)
{
const auto value = object->property(name);
insertIntoVariantMap(item, name, value);
}
return item;
}
QVariant SnapshotObject::objectToVariant(const QObject* object)
{
if(auto model = qobject_cast<const QAbstractItemModel*>(object))
{
return modelToVariant(model);
}
return {objectToVariantMap(object)};
}
QVariant SnapshotObject::modelToVariant(const QAbstractItemModel* model)
{
if(!model)
{
return {};
}
auto modelSnapshot = new SnapshotModel(*model, true, this);
connect(this, &SnapshotObject::snapshotChanged, modelSnapshot, [modelSnapshot]() { modelSnapshot->deleteLater(); });
return QVariant::fromValue(modelSnapshot);
}
void SnapshotObject::insertIntoVariantMap(QVariantMap& map, const QString& key, const QVariant& value)
{
if(value.canConvert<QObject*>())
{
map.insert(key, objectToVariant(value.value<QObject*>()));
return;
}
map.insert(key, value);
}

View File

@ -23,13 +23,11 @@ add_library(StatusQTestLib
src/TestHelpers/modeltestutils.h src/TestHelpers/modeltestutils.h
src/TestHelpers/persistentindexestester.cpp src/TestHelpers/persistentindexestester.cpp
src/TestHelpers/persistentindexestester.h src/TestHelpers/persistentindexestester.h
src/TestHelpers/snapshotmodel.cpp
src/TestHelpers/snapshotmodel.h
src/TestHelpers/testmodel.cpp src/TestHelpers/testmodel.cpp
src/TestHelpers/testmodel.h src/TestHelpers/testmodel.h
) )
target_link_libraries(StatusQTestLib PUBLIC Qt5::Core Qt5::Quick Qt5::Test) target_link_libraries(StatusQTestLib PUBLIC Qt5::Core Qt5::Quick Qt5::Test StatusQ)
target_include_directories(StatusQTestLib PUBLIC src) target_include_directories(StatusQTestLib PUBLIC src)
enable_testing() enable_testing()
@ -101,3 +99,11 @@ add_test(NAME MovableModelTest COMMAND MovableModelTest)
add_executable(ModelSyncedContainerTest tst_ModelSyncedContainer.cpp) add_executable(ModelSyncedContainerTest tst_ModelSyncedContainer.cpp)
target_link_libraries(ModelSyncedContainerTest PRIVATE StatusQ StatusQTestLib) target_link_libraries(ModelSyncedContainerTest PRIVATE StatusQ StatusQTestLib)
add_test(NAME ModelSyncedContainerTest COMMAND ModelSyncedContainerTest) add_test(NAME ModelSyncedContainerTest COMMAND ModelSyncedContainerTest)
add_executable(ModelEntryTest tst_ModelEntry.cpp)
target_link_libraries(ModelEntryTest PRIVATE StatusQ StatusQTestLib)
add_test(NAME ModelEntryTest COMMAND ModelEntryTest)
add_executable(SnapshotObjectTest tst_SnapshotObject.cpp)
target_link_libraries(SnapshotObjectTest PRIVATE StatusQ StatusQTestLib)
add_test(NAME SnapshotObjectTest COMMAND SnapshotObjectTest)

View File

@ -1,6 +1,5 @@
#include "persistentindexestester.h" #include "persistentindexestester.h"
#include "StatusQ/snapshotmodel.h"
#include <TestHelpers/snapshotmodel.h>
#include <QDebug> #include <QDebug>

View File

@ -1,68 +0,0 @@
#include "snapshotmodel.h"
#include <QDebug>
SnapshotModel::SnapshotModel(QObject* parent)
: QAbstractListModel(parent)
{
}
SnapshotModel::SnapshotModel(const QAbstractItemModel& model, bool recursive,
QObject* parent)
: QAbstractListModel(parent)
{
grabSnapshot(model, recursive);
}
int SnapshotModel::rowCount(const QModelIndex& parent) const
{
if(parent.isValid())
return 0;
return m_data.size() ? m_data.begin()->size() : 0;
}
QHash<int, QByteArray> SnapshotModel::roleNames() const
{
return m_roles;
}
QVariant SnapshotModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid() || !m_roles.contains(role)
|| index.row() >= rowCount()) {
return {};
}
return m_data[role][index.row()];
}
void SnapshotModel::grabSnapshot(const QAbstractItemModel& model, bool recursive)
{
m_roles = model.roleNames();
m_data.clear();
auto roles = m_roles.keys();
auto count = model.rowCount();
for (auto role : roles) {
for (int i = 0; i < count; i++) {
QVariant data = model.data(model.index(i, 0), role);
if (recursive && data.canConvert<QAbstractItemModel*>()) {
const auto submodel = data.value<QAbstractItemModel*>();
m_data[role].push_back(
QVariant::fromValue(
new SnapshotModel(*submodel, true, this)));
} else {
m_data[role].push_back(data);
}
}
}
}
QVariant SnapshotModel::data(int row, int role) const
{
return data(index(row), role);
}

File diff suppressed because it is too large Load Diff

View File

@ -7,12 +7,12 @@
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
#include <StatusQ/movablemodel.h> #include <StatusQ/movablemodel.h>
#include <StatusQ/snapshotmodel.h>
#include <TestHelpers/listmodelwrapper.h> #include <TestHelpers/listmodelwrapper.h>
#include <TestHelpers/modelsignalsspy.h> #include <TestHelpers/modelsignalsspy.h>
#include <TestHelpers/modeltestutils.h> #include <TestHelpers/modeltestutils.h>
#include <TestHelpers/persistentindexestester.h> #include <TestHelpers/persistentindexestester.h>
#include <TestHelpers/snapshotmodel.h>
class TestMovableModel : public QObject class TestMovableModel : public QObject
{ {

View File

@ -0,0 +1,201 @@
#include "StatusQ/snapshotobject.h"
#include <QSignalSpy>
#include <QTest>
#include <QQmlListProperty>
#include <QQmlPropertyMap>
#include <QStandardItemModel>
#include <QScopedPointer>
#include <QDebug>
class SimpleObject : public QObject
{
Q_OBJECT
Q_PROPERTY(bool boolProperty MEMBER m_boolProperty)
Q_PROPERTY(int intProperty MEMBER m_intProperty)
Q_PROPERTY(QString stringProperty MEMBER m_stringProperty)
Q_PROPERTY(QVariant variantProperty MEMBER m_variantProperty)
public:
SimpleObject(bool boolProperty = true,
int intProperty = 5,
const QString& stringProperty = "string",
const QVariant& variantProperty = "variant",
QObject* parent = nullptr)
: QObject(parent)
, m_boolProperty(boolProperty)
, m_intProperty(intProperty)
, m_stringProperty(stringProperty)
, m_variantProperty(variantProperty)
{ }
bool m_boolProperty;
int m_intProperty;
QString m_stringProperty;
QVariant m_variantProperty;
};
class QObjectTest : public QObject
{
Q_OBJECT
Q_PROPERTY(bool boolProperty MEMBER m_boolProperty)
Q_PROPERTY(int intProperty MEMBER m_intProperty)
Q_PROPERTY(QString stringProperty MEMBER m_stringProperty)
Q_PROPERTY(QVariant variantProperty MEMBER m_variantProperty)
Q_PROPERTY(QVariantList variantListProperty MEMBER m_variantListProperty)
Q_PROPERTY(QVariantMap variantMapProperty MEMBER m_variantMapProperty)
Q_PROPERTY(QObject* objectProperty MEMBER m_objectProperty)
Q_PROPERTY(QStandardItemModel* standardItemModel MEMBER m_standardItemModel)
public:
bool m_boolProperty{true};
int m_intProperty{5};
QString m_stringProperty{"string"};
QVariant m_variantProperty{"variant"};
QVariantList m_variantListProperty{"variant1", "variant2"};
QVariantMap m_variantMapProperty{{"key1", "value1"}, {"key2", "value2"}};
QObject* m_objectProperty{new SimpleObject(true, 45, "stringVal", "variantVal", this)};
QStandardItemModel* m_standardItemModel = nullptr;
};
class SnapshotObjectTest : public QObject
{
Q_OBJECT
private slots:
void snapshotQObjectTest()
{
QScopedPointer<SnapshotObject> snapshotObject {new SnapshotObject()};
QSignalSpy snapshotChangedSpy(snapshotObject.data(), &SnapshotObject::snapshotChanged);
QSignalSpy availableChangedSpy(snapshotObject.data(), &SnapshotObject::availableChanged);
QVERIFY(snapshotObject->snapshot().isNull());
QVERIFY(!snapshotObject->available());
// grabSnapshot(nullptr) should clear the snapshot and set available to false
snapshotObject->grabSnapshot(nullptr);
QCOMPARE(snapshotChangedSpy.count(), 0);
QCOMPARE(availableChangedSpy.count(), 0);
QVERIFY(snapshotObject->snapshot().isNull());
QVERIFY(!snapshotObject->available());
{
// grabSnapshot(new SimpleObject) should set the snapshot and set available to true
QScopedPointer<SimpleObject> testObject {new SimpleObject(true, 45, "stringVal", "variantVal")};
const auto snapshotObjPtr = snapshotObject.data();
auto connection = connect(snapshotObjPtr, &SnapshotObject::availableChanged, [snapshotObjPtr]() {
// the snapshot object must change after the available property
QVERIFY(snapshotObjPtr->snapshot().isValid());
});
snapshotObject->grabSnapshot(testObject.data());
QCOMPARE(snapshotChangedSpy.count(), 1);
QCOMPARE(availableChangedSpy.count(), 1);
QCOMPARE(snapshotObject->snapshot().toMap()["boolProperty"].toBool(), true);
QCOMPARE(snapshotObject->snapshot().toMap()["intProperty"].toInt(), 45);
QCOMPARE(snapshotObject->snapshot().toMap()["stringProperty"].toString(), "stringVal");
QCOMPARE(snapshotObject->snapshot().toMap()["variantProperty"].toString(), "variantVal");
QVERIFY(snapshotObject->available());
disconnect(connection);
// delete the test object and check that the snapshot is still available
}
QCOMPARE(snapshotChangedSpy.count(), 1);
QCOMPARE(availableChangedSpy.count(), 1);
QCOMPARE(snapshotObject->snapshot().toMap()["boolProperty"].toBool(), true);
QCOMPARE(snapshotObject->snapshot().toMap()["intProperty"].toInt(), 45);
QCOMPARE(snapshotObject->snapshot().toMap()["stringProperty"].toString(), "stringVal");
QCOMPARE(snapshotObject->snapshot().toMap()["variantProperty"].toString(), "variantVal");
QVERIFY(snapshotObject->available());
{
// grabshapshot(new QObjectTest) should set the snapshot and set available to true
auto snapshotObjPtr = snapshotObject.data();
auto connection = connect(snapshotObject.data(), &SnapshotObject::availableChanged, [snapshotObjPtr]() {
// the snapshot object must change after the available property
QVERIFY(snapshotObjPtr->snapshot().isValid());
});
QScopedPointer<QObjectTest> testObject {new QObjectTest()};
testObject->m_standardItemModel = new QStandardItemModel(this);
testObject->m_standardItemModel->insertRow(0, new QStandardItem("item1"));
testObject->m_standardItemModel->insertRow(1, new QStandardItem("item2"));
snapshotObject->grabSnapshot(testObject.data());
disconnect(connection);
// the testObject and the model is destroyed here
}
QCOMPARE(snapshotChangedSpy.count(), 2);
QCOMPARE(availableChangedSpy.count(), 1);
QCOMPARE(snapshotObject->snapshot().toMap()["boolProperty"].toBool(), true);
QCOMPARE(snapshotObject->snapshot().toMap()["intProperty"].toInt(), 5);
QCOMPARE(snapshotObject->snapshot().toMap()["stringProperty"].toString(), "string");
QCOMPARE(snapshotObject->snapshot().toMap()["variantProperty"].toString(), "variant");
QCOMPARE(snapshotObject->snapshot().toMap()["variantListProperty"].toList().size(), 2);
QCOMPARE(snapshotObject->snapshot().toMap()["variantListProperty"].toList().at(0).toString(), "variant1");
QCOMPARE(snapshotObject->snapshot().toMap()["variantListProperty"].toList().at(1).toString(), "variant2");
QCOMPARE(snapshotObject->snapshot().toMap()["variantMapProperty"].toMap().size(), 2);
QCOMPARE(snapshotObject->snapshot().toMap()["variantMapProperty"].toMap()["key1"].toString(), "value1");
QCOMPARE(snapshotObject->snapshot().toMap()["variantMapProperty"].toMap()["key2"].toString(), "value2");
QCOMPARE(snapshotObject->snapshot().toMap()["objectProperty"].toMap()["boolProperty"].toBool(), true);
QCOMPARE(snapshotObject->snapshot().toMap()["objectProperty"].toMap()["intProperty"].toInt(), 45);
QCOMPARE(snapshotObject->snapshot().toMap()["objectProperty"].toMap()["stringProperty"].toString(),
"stringVal");
QCOMPARE(snapshotObject->snapshot().toMap()["objectProperty"].toMap()["variantProperty"].toString(),
"variantVal");
auto standardItemModel = snapshotObject->snapshot().toMap()["standardItemModel"].value<QAbstractItemModel*>();
QSignalSpy modelDestroyedSpy(standardItemModel, &QObject::destroyed);
QCOMPARE(standardItemModel->rowCount(), 2);
QCOMPARE(standardItemModel->data(standardItemModel->index(0, 0)), "item1");
QCOMPARE(standardItemModel->data(standardItemModel->index(1, 0)).toString(), "item2");
QVERIFY(snapshotObject->available());
snapshotObject->grabSnapshot(nullptr);
QCOMPARE(snapshotChangedSpy.count(), 3);
QCOMPARE(availableChangedSpy.count(), 2);
QVERIFY(snapshotObject->snapshot().isNull());
QVERIFY(!snapshotObject->available());
// check if the memory is released after grabbing another snapshot
QTRY_COMPARE(modelDestroyedSpy.count(), 1);
}
void snapshotModelTest()
{
QScopedPointer<SnapshotObject> snapshotObject {new SnapshotObject()};
{
QScopedPointer<QStandardItemModel> model {new QStandardItemModel()};
model->insertRow(0, new QStandardItem("item1"));
model->insertRow(1, new QStandardItem("item2"));
snapshotObject->grabSnapshot(model.data());
}
auto snapshot = snapshotObject->snapshot();
auto snapshotModel = snapshot.value<QAbstractItemModel*>();
QSignalSpy modelDestroyedSpy(snapshotModel, &QObject::destroyed);
QVERIFY(snapshotModel);
QCOMPARE(snapshotModel->rowCount(), 2);
QCOMPARE(snapshotModel->data(snapshotModel->index(0, 0)).toString(), "item1");
QCOMPARE(snapshotModel->data(snapshotModel->index(1, 0)).toString(), "item2");
QVERIFY(snapshotObject->available());
snapshotObject->grabSnapshot(nullptr);
QTRY_COMPARE(modelDestroyedSpy.count(), 1);
}
};
QTEST_MAIN(SnapshotObjectTest)
#include "tst_SnapshotObject.moc"

View File

@ -1,17 +1,19 @@
#include <QAbstractItemModelTester> #include <QAbstractItemModelTester>
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QTest>
#include <QSignalSpy> #include <QSignalSpy>
#include <QTest>
#include "StatusQ/snapshotmodel.h"
#include "StatusQ/writableproxymodel.h" #include "StatusQ/writableproxymodel.h"
#include <TestHelpers/persistentindexestester.h>
#include <TestHelpers/snapshotmodel.h>
#include <TestHelpers/modeltestutils.h> #include <TestHelpers/modeltestutils.h>
#include <TestHelpers/persistentindexestester.h>
namespace { namespace
{
class TestSourceModel : public QAbstractListModel { class TestSourceModel : public QAbstractListModel
{
public: public:
explicit TestSourceModel(QList<QPair<QString, QVariantList>> data) explicit TestSourceModel(QList<QPair<QString, QVariantList>> data)