From 4e81f8f2209958859b8e594494da57c6467e7446 Mon Sep 17 00:00:00 2001 From: Alex Jbanca <47811206+alexjba@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:08:52 +0300 Subject: [PATCH] feat(StatusQ.Models): Adding SingleModelItemData helper component (#14891) SingleModelItemData is a generic component that can provide a live object extract from an arbitrary QAbstractItemModel* --- storybook/pages/ModelEntryPage.qml | 154 ++ storybook/pages/WritableProxyModelPage.qml | 2 + ui/StatusQ/CMakeLists.txt | 6 + ui/StatusQ/include/StatusQ/modelentry.h | 91 + .../StatusQ}/snapshotmodel.h | 13 +- ui/StatusQ/include/StatusQ/snapshotobject.h | 38 + ui/StatusQ/src/modelentry.cpp | 346 ++++ ui/StatusQ/src/plugin.cpp | 4 + ui/StatusQ/src/snapshotmodel.cpp | 100 ++ ui/StatusQ/src/snapshotobject.cpp | 132 ++ ui/StatusQ/tests/CMakeLists.txt | 12 +- .../TestHelpers/persistentindexestester.cpp | 3 +- .../tests/src/TestHelpers/snapshotmodel.cpp | 68 - ui/StatusQ/tests/tst_ModelEntry.cpp | 1531 +++++++++++++++++ ui/StatusQ/tests/tst_MovableModel.cpp | 2 +- ui/StatusQ/tests/tst_SnapshotObject.cpp | 201 +++ ui/StatusQ/tests/tst_WritableProxyModel.cpp | 12 +- 17 files changed, 2632 insertions(+), 83 deletions(-) create mode 100644 storybook/pages/ModelEntryPage.qml create mode 100644 ui/StatusQ/include/StatusQ/modelentry.h rename ui/StatusQ/{tests/src/TestHelpers => include/StatusQ}/snapshotmodel.h (78%) create mode 100644 ui/StatusQ/include/StatusQ/snapshotobject.h create mode 100644 ui/StatusQ/src/modelentry.cpp create mode 100644 ui/StatusQ/src/snapshotmodel.cpp create mode 100644 ui/StatusQ/src/snapshotobject.cpp delete mode 100644 ui/StatusQ/tests/src/TestHelpers/snapshotmodel.cpp create mode 100644 ui/StatusQ/tests/tst_ModelEntry.cpp create mode 100644 ui/StatusQ/tests/tst_SnapshotObject.cpp diff --git a/storybook/pages/ModelEntryPage.qml b/storybook/pages/ModelEntryPage.qml new file mode 100644 index 0000000000..ab850bcbda --- /dev/null +++ b/storybook/pages/ModelEntryPage.qml @@ -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 diff --git a/storybook/pages/WritableProxyModelPage.qml b/storybook/pages/WritableProxyModelPage.qml index e6fbe65b25..76d08c032a 100644 --- a/storybook/pages/WritableProxyModelPage.qml +++ b/storybook/pages/WritableProxyModelPage.qml @@ -207,3 +207,5 @@ Item { } } } + +// category: Models \ No newline at end of file diff --git a/ui/StatusQ/CMakeLists.txt b/ui/StatusQ/CMakeLists.txt index d71e460e89..2c67b720c1 100644 --- a/ui/StatusQ/CMakeLists.txt +++ b/ui/StatusQ/CMakeLists.txt @@ -101,12 +101,15 @@ add_library(StatusQ SHARED include/StatusQ/formatteddoubleproperty.h include/StatusQ/functionaggregator.h include/StatusQ/leftjoinmodel.h + include/StatusQ/modelentry.h include/StatusQ/modelsyncedcontainer.h include/StatusQ/modelutilsinternal.h include/StatusQ/movablemodel.h include/StatusQ/permissionutilsinternal.h include/StatusQ/rolesrenamingmodel.h include/StatusQ/rxvalidator.h + include/StatusQ/snapshotmodel.h + include/StatusQ/snapshotobject.h include/StatusQ/singleroleaggregator.h include/StatusQ/statussyntaxhighlighter.h include/StatusQ/statuswindow.h @@ -124,6 +127,7 @@ add_library(StatusQ SHARED src/formatteddoubleproperty.cpp src/functionaggregator.cpp src/leftjoinmodel.cpp + src/modelentry.cpp src/modelutilsinternal.cpp src/movablemodel.cpp src/permissionutilsinternal.cpp @@ -131,6 +135,8 @@ add_library(StatusQ SHARED src/rolesrenamingmodel.cpp src/rxvalidator.cpp src/singleroleaggregator.cpp + src/snapshotmodel.cpp + src/snapshotobject.cpp src/statussyntaxhighlighter.cpp src/statuswindow.cpp src/stringutilsinternal.cpp diff --git a/ui/StatusQ/include/StatusQ/modelentry.h b/ui/StatusQ/include/StatusQ/modelentry.h new file mode 100644 index 0000000000..357dc0e7c8 --- /dev/null +++ b/ui/StatusQ/include/StatusQ/modelentry.h @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include +#include + +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& roles = {}); + + QModelIndex findIndexInRange(int start, int end, const QList& 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 m_item{nullptr}; + QPointer 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; +}; diff --git a/ui/StatusQ/tests/src/TestHelpers/snapshotmodel.h b/ui/StatusQ/include/StatusQ/snapshotmodel.h similarity index 78% rename from ui/StatusQ/tests/src/TestHelpers/snapshotmodel.h rename to ui/StatusQ/include/StatusQ/snapshotmodel.h index d43c035c0f..ebd43be6b0 100644 --- a/ui/StatusQ/tests/src/TestHelpers/snapshotmodel.h +++ b/ui/StatusQ/include/StatusQ/snapshotmodel.h @@ -2,21 +2,26 @@ #include -class SnapshotModel : public QAbstractListModel { - +class SnapshotModel : public QAbstractListModel +{ + Q_OBJECT public: explicit SnapshotModel(QObject* parent = nullptr); - explicit SnapshotModel(const QAbstractItemModel& model, bool recursive = true, - QObject* parent = nullptr); + explicit SnapshotModel(const QAbstractItemModel& model, bool recursive = true, QObject* parent = nullptr); + + ~SnapshotModel(); int rowCount(const QModelIndex& parent = {}) const override; QHash roleNames() const override; QVariant data(const QModelIndex& index, int role) const override; void grabSnapshot(const QAbstractItemModel& model, bool recursive = true); + void clearSnapshot(); + QVariant data(int row, int role) const; private: + QHash> m_data; QHash m_roles; }; diff --git a/ui/StatusQ/include/StatusQ/snapshotobject.h b/ui/StatusQ/include/StatusQ/snapshotobject.h new file mode 100644 index 0000000000..9fef5c2dc7 --- /dev/null +++ b/ui/StatusQ/include/StatusQ/snapshotobject.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +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}; +}; \ No newline at end of file diff --git a/ui/StatusQ/src/modelentry.cpp b/ui/StatusQ/src/modelentry.cpp new file mode 100644 index 0000000000..0f45566fde --- /dev/null +++ b/ui/StatusQ/src/modelentry.cpp @@ -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& roles = QList()) { + 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& 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& 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()) + { + m_item->insert(role, + QVariant::fromValue(new SnapshotModel(*roleValue.value(), true, m_item.data()))); + } + else if(roleValue.canConvert()) + { + const auto obj = roleValue.value(); + 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); +} diff --git a/ui/StatusQ/src/plugin.cpp b/ui/StatusQ/src/plugin.cpp index 54f395f74d..23aaa7dafa 100644 --- a/ui/StatusQ/src/plugin.cpp +++ b/ui/StatusQ/src/plugin.cpp @@ -16,6 +16,8 @@ #include "StatusQ/permissionutilsinternal.h" #include "StatusQ/rolesrenamingmodel.h" #include "StatusQ/rxvalidator.h" +#include "StatusQ/modelentry.h" +#include "StatusQ/snapshotobject.h" #include "StatusQ/statussyntaxhighlighter.h" #include "StatusQ/statuswindow.h" #include "StatusQ/stringutilsinternal.h" @@ -62,6 +64,8 @@ public: qmlRegisterType("StatusQ", 0, 1, "FormattedDoubleProperty"); qmlRegisterSingletonType("StatusQ", 0, 1, "QClipboardProxy", &QClipboardProxy::qmlInstance); + qmlRegisterType("StatusQ", 0, 1, "ModelEntry"); + qmlRegisterType("StatusQ", 0, 1, "SnapshotObject"); qmlRegisterSingletonType( "StatusQ.Internal", 0, 1, "ModelUtils", &ModelUtilsInternal::qmlInstance); diff --git a/ui/StatusQ/src/snapshotmodel.cpp b/ui/StatusQ/src/snapshotmodel.cpp new file mode 100644 index 0000000000..a9bba99e33 --- /dev/null +++ b/ui/StatusQ/src/snapshotmodel.cpp @@ -0,0 +1,100 @@ +#include "StatusQ/snapshotmodel.h" + +#include "StatusQ/snapshotobject.h" + +#include + +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 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()) + { + const auto submodel = data.value(); + + m_data[role].push_back(QVariant::fromValue(new SnapshotModel(*submodel, true, this))); + } + else if(recursive && data.canConvert()) + { + const auto submodelObject = data.value(); + 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()) + { + item.value()->deleteLater(); + } + } + } + m_data.clear(); +} + +QVariant SnapshotModel::data(int row, int role) const +{ + return data(index(row), role); +} diff --git a/ui/StatusQ/src/snapshotobject.cpp b/ui/StatusQ/src/snapshotobject.cpp new file mode 100644 index 0000000000..ac6124b0c8 --- /dev/null +++ b/ui/StatusQ/src/snapshotobject.cpp @@ -0,0 +1,132 @@ +#include "StatusQ/snapshotobject.h" + +#include "StatusQ/snapshotmodel.h" + +#include +#include + +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(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(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()) + { + map.insert(key, objectToVariant(value.value())); + return; + } + + map.insert(key, value); +} diff --git a/ui/StatusQ/tests/CMakeLists.txt b/ui/StatusQ/tests/CMakeLists.txt index bd31822528..da301f55b3 100644 --- a/ui/StatusQ/tests/CMakeLists.txt +++ b/ui/StatusQ/tests/CMakeLists.txt @@ -23,13 +23,11 @@ add_library(StatusQTestLib src/TestHelpers/modeltestutils.h src/TestHelpers/persistentindexestester.cpp src/TestHelpers/persistentindexestester.h - src/TestHelpers/snapshotmodel.cpp - src/TestHelpers/snapshotmodel.h src/TestHelpers/testmodel.cpp 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) enable_testing() @@ -101,3 +99,11 @@ add_test(NAME MovableModelTest COMMAND MovableModelTest) add_executable(ModelSyncedContainerTest tst_ModelSyncedContainer.cpp) target_link_libraries(ModelSyncedContainerTest PRIVATE StatusQ StatusQTestLib) 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) \ No newline at end of file diff --git a/ui/StatusQ/tests/src/TestHelpers/persistentindexestester.cpp b/ui/StatusQ/tests/src/TestHelpers/persistentindexestester.cpp index a4a372e0d8..c020b6c4dd 100644 --- a/ui/StatusQ/tests/src/TestHelpers/persistentindexestester.cpp +++ b/ui/StatusQ/tests/src/TestHelpers/persistentindexestester.cpp @@ -1,6 +1,5 @@ #include "persistentindexestester.h" - -#include +#include "StatusQ/snapshotmodel.h" #include diff --git a/ui/StatusQ/tests/src/TestHelpers/snapshotmodel.cpp b/ui/StatusQ/tests/src/TestHelpers/snapshotmodel.cpp deleted file mode 100644 index e486a2fc8f..0000000000 --- a/ui/StatusQ/tests/src/TestHelpers/snapshotmodel.cpp +++ /dev/null @@ -1,68 +0,0 @@ -#include "snapshotmodel.h" - -#include - -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 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()) { - const auto submodel = data.value(); - - 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); -} diff --git a/ui/StatusQ/tests/tst_ModelEntry.cpp b/ui/StatusQ/tests/tst_ModelEntry.cpp new file mode 100644 index 0000000000..5be98bf89e --- /dev/null +++ b/ui/StatusQ/tests/tst_ModelEntry.cpp @@ -0,0 +1,1531 @@ +#include "StatusQ/modelentry.h" +#include "StatusQ/snapshotmodel.h" + +#include "TestHelpers/listmodelwrapper.h" +#include "TestHelpers/modelsignalsspy.h" +#include "TestHelpers/testmodel.h" + +#include +#include +#include +#include +#include +#include +#include + +class TestModelEntry : public QObject +{ + Q_OBJECT + ModelEntry* testObject; + QMetaProperty sourceModelProperty; + QMetaProperty keyProperty; + QMetaProperty valueProperty; + QMetaProperty rolesProperty; + QMetaProperty modelItemProperty; + QMetaProperty availableProperty; + QMetaProperty rowProperty; + QMetaProperty cacheOnRemovalProperty; + QMetaProperty itemRemovedFromCacheProperty; + +private slots: + void init() + { + testObject = new ModelEntry(); + sourceModelProperty = + testObject->metaObject()->property(testObject->metaObject()->indexOfProperty("sourceModel")); + keyProperty = testObject->metaObject()->property(testObject->metaObject()->indexOfProperty("key")); + valueProperty = testObject->metaObject()->property(testObject->metaObject()->indexOfProperty("value")); + + modelItemProperty = testObject->metaObject()->property(testObject->metaObject()->indexOfProperty("item")); + rolesProperty = testObject->metaObject()->property(testObject->metaObject()->indexOfProperty("roles")); + availableProperty = testObject->metaObject()->property(testObject->metaObject()->indexOfProperty("available")); + rowProperty = testObject->metaObject()->property(testObject->metaObject()->indexOfProperty("row")); + cacheOnRemovalProperty = + testObject->metaObject()->property(testObject->metaObject()->indexOfProperty("cacheOnRemoval")); + itemRemovedFromCacheProperty = + testObject->metaObject()->property(testObject->metaObject()->indexOfProperty("itemRemovedFromModel")); + } + + void cleanup() + { + delete testObject; + testObject = nullptr; + } + + void initializationTest() + { + // testing default values and properties + QCOMPARE(testObject->sourceModel(), nullptr); + QCOMPARE(sourceModelProperty.isValid(), true); + QCOMPARE(sourceModelProperty.isRequired(), true); + QCOMPARE(sourceModelProperty.isWritable(), true); + QCOMPARE(sourceModelProperty.hasNotifySignal(), true); + QCOMPARE(sourceModelProperty.read(testObject), QVariant::fromValue(nullptr)); + + QCOMPARE(testObject->roles(), {}); + QCOMPARE(rolesProperty.isValid(), true); + QCOMPARE(rolesProperty.isWritable(), false); + QCOMPARE(rolesProperty.hasNotifySignal(), true); + QCOMPARE(rolesProperty.read(testObject), testObject->roles()); + + QVERIFY(testObject->item() != nullptr); + QCOMPARE(modelItemProperty.isValid(), true); + QCOMPARE(modelItemProperty.isWritable(), false); + QCOMPARE(modelItemProperty.hasNotifySignal(), true); + QCOMPARE(modelItemProperty.read(testObject), QVariant::fromValue(testObject->item())); + + QCOMPARE(testObject->available(), false); + QCOMPARE(availableProperty.isValid(), true); + QCOMPARE(availableProperty.isWritable(), false); + QCOMPARE(availableProperty.hasNotifySignal(), true); + QCOMPARE(availableProperty.read(testObject), false); + + QCOMPARE(testObject->row(), -1); + QCOMPARE(rowProperty.isValid(), true); + QCOMPARE(rowProperty.isWritable(), false); + QCOMPARE(rowProperty.hasNotifySignal(), true); + QCOMPARE(rowProperty.read(testObject), -1); + + // testing property setters + QQmlEngine engine; + ListModelWrapper sourceModel( + engine, QJsonArray{QJsonObject{{"key", 1}, {"color", "red"}}, QJsonObject{{"key", 2}, {"color", "blue"}}}); + + QSignalSpy sourceModelChangedSpy(testObject, &ModelEntry::sourceModelChanged); + QSignalSpy keyChangedSpy(testObject, &ModelEntry::keyChanged); + QSignalSpy valueChangedSpy(testObject, &ModelEntry::valueChanged); + QSignalSpy itemChangedSpy(testObject, &ModelEntry::itemChanged); + QSignalSpy availableChangedSpy(testObject, &ModelEntry::availableChanged); + QSignalSpy rolesChangedSpy(testObject, &ModelEntry::rolesChanged); + QSignalSpy rowChangedSpy(testObject, &ModelEntry::rowChanged); + + // testing source model property + QCOMPARE(sourceModelProperty.write(testObject, QVariant::fromValue(sourceModel.model())), + true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 0); + QCOMPARE(valueChangedSpy.count(), 0); + QCOMPARE(itemChangedSpy.count(), 0); + QCOMPARE(availableChangedSpy.count(), 0); + QCOMPARE(rolesChangedSpy.count(), 0); + QCOMPARE(rowChangedSpy.count(), 0); + QCOMPARE(testObject->sourceModel(), static_cast(sourceModel.model())); + QCOMPARE(sourceModelProperty.read(testObject), QVariant::fromValue(sourceModel.model())); + + // testing filter property + QCOMPARE(keyProperty.write(testObject, "key"), true); + QCOMPARE(valueProperty.write(testObject, 1), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rolesChangedSpy.count(), 1); + QCOMPARE(rowChangedSpy.count(), 1); + QCOMPARE(testObject->key(), "key"); + QCOMPARE(keyProperty.read(testObject), "key"); + QCOMPARE(testObject->value(), 1); + QCOMPARE(valueProperty.read(testObject), 1); + + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + + QStringList expectedRoles{"key", "color"}; + auto roles = testObject->roles(); + auto rolesVariant = rolesProperty.read(testObject); + + QCOMPARE(roles, rolesVariant); + QVERIFY(roles.size() == 2); + QCOMPARE(roles.contains("key"), true); + QCOMPARE(roles.contains("color"), true); + + QCOMPARE(testObject->row(), 0); + QCOMPARE(rowProperty.read(testObject), 0); + + QCOMPARE(testObject->item()->value("key"), 1); + QCOMPARE(testObject->item()->value("color"), "red"); + QCOMPARE(modelItemProperty.read(testObject), QVariant::fromValue(testObject->item())); + } + + void reversePropertyInitializationTest() + { + // setting the filter and then the source model + QQmlEngine engine; + ListModelWrapper sourceModel( + engine, QJsonArray{QJsonObject{{"key", 1}, {"color", "red"}}, QJsonObject{{"key", 2}, {"color", "blue"}}}); + + QSignalSpy sourceModelChangedSpy(testObject, &ModelEntry::sourceModelChanged); + QSignalSpy keyChangedSpy(testObject, &ModelEntry::keyChanged); + QSignalSpy valueChangedSpy(testObject, &ModelEntry::valueChanged); + QSignalSpy itemChangedSpy(testObject, &ModelEntry::itemChanged); + QSignalSpy availableChangedSpy(testObject, &ModelEntry::availableChanged); + QSignalSpy rolesChangedSpy(testObject, &ModelEntry::rolesChanged); + QSignalSpy rowChangedSpy(testObject, &ModelEntry::rowChanged); + + // write the filter first + QCOMPARE(keyProperty.write(testObject, "key"), true); + QCOMPARE(valueProperty.write(testObject, 1), true); + + QCOMPARE(sourceModelChangedSpy.count(), 0); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 0); + QCOMPARE(availableChangedSpy.count(), 0); + QCOMPARE(rolesChangedSpy.count(), 0); + QCOMPARE(rowChangedSpy.count(), 0); + + QCOMPARE(testObject->key(), "key"); + QCOMPARE(keyProperty.read(testObject), "key"); + QCOMPARE(testObject->value(), 1); + QCOMPARE(valueProperty.read(testObject), 1); + QCOMPARE(testObject->available(), false); + QCOMPARE(availableProperty.read(testObject), false); + QCOMPARE(testObject->roles(), {}); + QCOMPARE(rolesProperty.read(testObject), testObject->roles()); + QCOMPARE(testObject->row(), -1); + QCOMPARE(rowProperty.read(testObject), -1); + + // write the source model + QCOMPARE(sourceModelProperty.write(testObject, QVariant::fromValue(sourceModel.model())), + true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rolesChangedSpy.count(), 1); + QCOMPARE(rowChangedSpy.count(), 1); + + QCOMPARE(testObject->sourceModel(), static_cast(sourceModel.model())); + QCOMPARE(sourceModelProperty.read(testObject), QVariant::fromValue(sourceModel.model())); + QCOMPARE(testObject->key(), "key"); + QCOMPARE(keyProperty.read(testObject), "key"); + QCOMPARE(testObject->value(), 1); + QCOMPARE(valueProperty.read(testObject), 1); + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + + // testing the output properties + auto roles = testObject->roles(); + auto rolesVariant = rolesProperty.read(testObject); + + QCOMPARE(roles, rolesVariant); + QVERIFY(roles.size() == 2); + QCOMPARE(roles.contains("key"), true); + QCOMPARE(roles.contains("color"), true); + + QCOMPARE(testObject->item()->value("key"), 1); + QCOMPARE(testObject->item()->value("color"), "red"); + QCOMPARE(modelItemProperty.read(testObject), QVariant::fromValue(testObject->item())); + + QCOMPARE(testObject->row(), 0); + QCOMPARE(rowProperty.read(testObject), 0); + } + + // testing source model change + // test cases: + // 1. source model is changed to a new model with the same roles. The item should be updated with the new model's data. + // 2. source model is changed to a new model with the same roles, but not containing the data. The item should be invalidated. + // 3. source model is changed to a new model with different roles. The item should be invalidated. + // 4. source model is changed to a new model with different roles, but contains the right key and value. The item + // should be updated with the new model's data. + // 5. source model is changed to empty model. The item should be cleared. + // 6. source model is changed to the same model. The item should not be updated. + void sourceChangedAfterMatchTest_data() + { + QJsonArray initialSource{QJsonObject{{"key", 1}, {"color", "red"}}, QJsonObject{{"key", 2}, {"color", "blue"}}}; + + QJsonArray similarSourceWithMatch{QJsonObject{{"key", 1}, {"color", "green"}}, + QJsonObject{{"key", 3}, {"color", "yellow"}}}; + + QJsonArray similarSourceNoMatch{QJsonObject{{"key", 3}, {"color", "green"}}, + QJsonObject{{"key", 4}, {"color", "yellow"}}}; + + QJsonArray differentRolesSource{QJsonObject{{"other_key", 1}, {"other_color", "red"}}, + QJsonObject{{"other_key", 2}, {"other_color", "blue"}}}; + + QJsonArray sameKeyDifferentRolesWithMatch{ + QJsonObject{{"key", 1}, {"other_color", "red"}}, + QJsonObject{{"key", 2}, {"other_color", "blue"}}, + QJsonObject{{"key", 3}, {"other_color", "green"}}, + }; + + QTest::addColumn("initialSource"); + QTest::addColumn("secondSource"); + QTest::addColumn("matchingKey"); + QTest::addColumn("matchingRowInSecondSource"); + QTest::addColumn("expectedAvailable"); + QTest::addColumn("expectedItemChange"); + + QTest::newRow("1. source model is changed to a new model with the same roles. The item should be updated with " + "the new model's data.") + << initialSource << similarSourceWithMatch << 1 << 0 << true << false; + + QTest::newRow("2. source model is changed to a new model with the same roles, but not containing the data. The " + "item should be invalidated.") + << initialSource << similarSourceNoMatch << -1 << -1 << false << true; + + QTest::newRow("3. source model is changed to a new model with different roles. The item should be invalidated.") + << initialSource << differentRolesSource << -1 << -1 << false << true; + + QTest::newRow("4. source model is changed to a new model with different roles, but contains the right key and " + "value. The item should be updated with the new model's data.") + << initialSource << sameKeyDifferentRolesWithMatch << 1 << 0 << true << true; + + QTest::newRow("5. source model is changed to empty model. The item should be cleared.") + << initialSource << QJsonArray{} << -1 << -1 << false << true; + + QTest::newRow("6. source model is changed to the same model. The item should not be updated.") + << initialSource << initialSource << 1 << 0 << true << false; + } + + void sourceChangedAfterMatchTest() + { + QFETCH(QJsonArray, initialSource); + QFETCH(QJsonArray, secondSource); + QFETCH(int, matchingKey); + QFETCH(int, matchingRowInSecondSource); + + QFETCH(bool, expectedAvailable); + QFETCH(bool, expectedItemChange); + + QQmlEngine engine; + ListModelWrapper initialSourceModel(engine, initialSource); + ListModelWrapper secondSourceModel(engine, secondSource); + + auto initialRoles = initialSourceModel.model()->roleNames().values(); + auto secondSourceRoles = secondSourceModel.model()->roleNames().values(); + + QSignalSpy sourceModelChangedSpy(testObject, &ModelEntry::sourceModelChanged); + QSignalSpy keyChangedSpy(testObject, &ModelEntry::keyChanged); + QSignalSpy valueChangedSpy(testObject, &ModelEntry::valueChanged); + QSignalSpy itemChangedSpy(testObject, &ModelEntry::itemChanged); + QSignalSpy availableChangedSpy(testObject, &ModelEntry::availableChanged); + QSignalSpy rolesChangedSpy(testObject, &ModelEntry::rolesChanged); + QSignalSpy rowChangedSpy(testObject, &ModelEntry::rowChanged); + + // setting the initial source model + QCOMPARE( + sourceModelProperty.write(testObject, QVariant::fromValue(initialSourceModel.model())), + true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 0); + QCOMPARE(valueChangedSpy.count(), 0); + QCOMPARE(itemChangedSpy.count(), 0); + QCOMPARE(availableChangedSpy.count(), 0); + QCOMPARE(rolesChangedSpy.count(), 0); + QCOMPARE(rowChangedSpy.count(), 0); + + QCOMPARE(testObject->roles(), {}); + QCOMPARE(rolesProperty.read(testObject), testObject->roles()); + + // setting the filter -> initial setup matches the first row + QCOMPARE(keyProperty.write(testObject, "key"), true); + QCOMPARE(valueProperty.write(testObject, 1), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rolesChangedSpy.count(), 1); + + for(const auto& role : initialRoles) + QCOMPARE(testObject->roles().contains(role), true); + + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + + QCOMPARE(testObject->row(), 0); + QCOMPARE(rowProperty.read(testObject), 0); + + QCOMPARE(testObject->item()->value(initialRoles[0]), + initialSource.at(0).toObject().value(initialRoles[0]).toVariant()); + QCOMPARE(testObject->item()->value(initialRoles[1]), + initialSource.at(0).toObject().value(initialRoles[1]).toVariant()); + + QCOMPARE(valueProperty.write(testObject, matchingKey), true); + + QCOMPARE( + sourceModelProperty.write(testObject, QVariant::fromValue(secondSourceModel.model())), + true); + + QCOMPARE(sourceModelChangedSpy.count(), 2); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), matchingKey == 1 ? 1 : 2); + QCOMPARE(itemChangedSpy.count(), expectedItemChange ? 2 : 1); + QCOMPARE(availableChangedSpy.count(), expectedAvailable ? 1 : 2); + + if(expectedAvailable) + { + for(const auto& role : secondSourceRoles) + QCOMPARE(testObject->roles().contains(role), true); + + QCOMPARE(testObject->item()->value(secondSourceRoles[0]), + secondSource.at(0).toObject().value(secondSourceRoles[0]).toVariant()); + QCOMPARE(testObject->item()->value(secondSourceRoles[1]), + secondSource.at(0).toObject().value(secondSourceRoles[1]).toVariant()); + } + + QCOMPARE(testObject->available(), expectedAvailable); + QCOMPARE(availableProperty.read(testObject), expectedAvailable); + + QCOMPARE(testObject->row(), matchingRowInSecondSource); + QCOMPARE(rowProperty.read(testObject), matchingRowInSecondSource); + } + + void filterChangedTest() + { + QQmlEngine engine; + ListModelWrapper sourceModel( + engine, QJsonArray{QJsonObject{{"key", 1}, {"color", "red"}}, QJsonObject{{"key", 2}, {"color", "blue"}}}); + + QSignalSpy sourceModelChangedSpy(testObject, &ModelEntry::sourceModelChanged); + QSignalSpy keyChangedSpy(testObject, &ModelEntry::keyChanged); + QSignalSpy valueChangedSpy(testObject, &ModelEntry::valueChanged); + QSignalSpy itemChangedSpy(testObject, &ModelEntry::itemChanged); + QSignalSpy availableChangedSpy(testObject, &ModelEntry::availableChanged); + QSignalSpy rolesChangedSpy(testObject, &ModelEntry::rolesChanged); + QSignalSpy rowChangedSpy(testObject, &ModelEntry::rowChanged); + + // setting the initial source model + QCOMPARE(sourceModelProperty.write(testObject, QVariant::fromValue(sourceModel.model())), + true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 0); + QCOMPARE(valueChangedSpy.count(), 0); + QCOMPARE(itemChangedSpy.count(), 0); + QCOMPARE(availableChangedSpy.count(), 0); + QCOMPARE(rolesChangedSpy.count(), 0); + QCOMPARE(rowChangedSpy.count(), 0); + + // setting the filter + QCOMPARE(keyProperty.write(testObject, "key"), true); + QCOMPARE(valueProperty.write(testObject, 1), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rolesChangedSpy.count(), 1); + QCOMPARE(rowChangedSpy.count(), 1); + + QCOMPARE(testObject->key(), "key"); + QCOMPARE(keyProperty.read(testObject), "key"); + QCOMPARE(testObject->value(), 1); + QCOMPARE(valueProperty.read(testObject), 1); + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(rolesProperty.read(testObject), testObject->roles()); + QCOMPARE(testObject->row(), 0); + QCOMPARE(rowProperty.read(testObject), 0); + + // changing the filter + QCOMPARE(keyProperty.write(testObject, "color"), true); + QCOMPARE(valueProperty.write(testObject, "blue"), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 2); + QCOMPARE(valueChangedSpy.count(), 2); + QCOMPARE(itemChangedSpy.count(), 3); + QCOMPARE(availableChangedSpy.count(), 3); + QCOMPARE(rolesChangedSpy.count(), 3); + QCOMPARE(rowChangedSpy.count(), 3); + + QCOMPARE(testObject->key(), "color"); + QCOMPARE(keyProperty.read(testObject), "color"); + QCOMPARE(testObject->value(), "blue"); + QCOMPARE(valueProperty.read(testObject), "blue"); + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(rolesProperty.read(testObject), testObject->roles()); + QCOMPARE(testObject->row(), 1); + QCOMPARE(rowProperty.read(testObject), 1); + + // Changing the filter to non-matching filter -> the item should be invalidated + QCOMPARE(keyProperty.write(testObject, "other_key"), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 3); + QCOMPARE(valueChangedSpy.count(), 2); + QCOMPARE(itemChangedSpy.count(), 4); + QCOMPARE(availableChangedSpy.count(), 4); + QCOMPARE(rolesChangedSpy.count(), 4); + QCOMPARE(rowChangedSpy.count(), 4); + + QCOMPARE(testObject->key(), "other_key"); + QCOMPARE(keyProperty.read(testObject), "other_key"); + QCOMPARE(testObject->available(), false); + QCOMPARE(availableProperty.read(testObject), false); + QCOMPARE(testObject->roles(), {}); + QCOMPARE(rolesProperty.read(testObject), testObject->roles()); + QCOMPARE(testObject->row(), -1); + QCOMPARE(rowProperty.read(testObject), -1); + } + + void rolesChangedTest() + { + QQmlEngine engine; + ListModelWrapper sourceModel( + engine, QJsonArray{QJsonObject{{"key", 1}, {"color", "red"}}, QJsonObject{{"key", 2}, {"color", "blue"}}}); + + QSignalSpy sourceModelChangedSpy(testObject, &ModelEntry::sourceModelChanged); + QSignalSpy keyChangedSpy(testObject, &ModelEntry::keyChanged); + QSignalSpy valueChangedSpy(testObject, &ModelEntry::valueChanged); + QSignalSpy itemChangedSpy(testObject, &ModelEntry::itemChanged); + QSignalSpy availableChangedSpy(testObject, &ModelEntry::availableChanged); + QSignalSpy rolesChangedSpy(testObject, &ModelEntry::rolesChanged); + QSignalSpy rowChangedSpy(testObject, &ModelEntry::rowChanged); + + // setting the initial source model + QCOMPARE(sourceModelProperty.write(testObject, QVariant::fromValue(sourceModel.model())), + true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 0); + QCOMPARE(valueChangedSpy.count(), 0); + QCOMPARE(itemChangedSpy.count(), 0); + QCOMPARE(availableChangedSpy.count(), 0); + QCOMPARE(rolesChangedSpy.count(), 0); + QCOMPARE(rowChangedSpy.count(), 0); + + // setting the filter + QCOMPARE(keyProperty.write(testObject, "key"), true); + QCOMPARE(valueProperty.write(testObject, 1), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rolesChangedSpy.count(), 1); + QCOMPARE(rowChangedSpy.count(), 1); + + QCOMPARE(testObject->key(), "key"); + QCOMPARE(keyProperty.read(testObject), "key"); + QCOMPARE(testObject->value(), 1); + QCOMPARE(valueProperty.read(testObject), 1); + + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(rolesProperty.read(testObject), testObject->roles()); + QCOMPARE(testObject->row(), 0); + QCOMPARE(rowProperty.read(testObject), 0); + + QCOMPARE(testObject->item()->value("key"), 1); + QCOMPARE(testObject->item()->value("color"), "red"); + QCOMPARE(testObject->roles().size(), 2); + + // changing the other roles, except for the key + ListModelWrapper secondsourceModel( + engine, + QJsonArray{QJsonObject{{"key", 1}, {"other_color", "red"}, {"other_role", 1}}, + QJsonObject{{"key", 2}, {"other_color", "blue"}, {"other_role", 2}}}); + QCOMPARE( + sourceModelProperty.write(testObject, QVariant::fromValue(secondsourceModel.model())), + true); + + QCOMPARE(sourceModelChangedSpy.count(), 2); + QCOMPARE(itemChangedSpy.count(), 2); + QCOMPARE(rolesChangedSpy.count(), 2); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rowChangedSpy.count(), 1); + + QCOMPARE(testObject->roles().size(), 3); + QCOMPARE(testObject->roles().contains("key"), true); + QCOMPARE(testObject->roles().contains("other_color"), true); + QCOMPARE(testObject->roles().contains("other_role"), true); + QCOMPARE(rolesProperty.read(testObject), testObject->roles()); + + QCOMPARE(testObject->item()->value("key"), 1); + QCOMPARE(testObject->item()->value("other_color"), "red"); + QCOMPARE(testObject->item()->value("other_role"), 1); + + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(testObject->row(), 0); + QCOMPARE(rowProperty.read(testObject), 0); + + // changing the roles including the key + ListModelWrapper thirdsourceModel( + engine, + QJsonArray{QJsonObject{{"other_key", 1}, {"other_color", "red"}, {"other_role", 1}}, + QJsonObject{{"other_key", 2}, {"other_color", "blue"}, {"other_role", 2}}}); + + QCOMPARE( + sourceModelProperty.write(testObject, QVariant::fromValue(thirdsourceModel.model())), + true); + + QCOMPARE(sourceModelChangedSpy.count(), 3); + QCOMPARE(itemChangedSpy.count(), 3); + QCOMPARE(rolesChangedSpy.count(), 3); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 2); + QCOMPARE(rowChangedSpy.count(), 2); + + QCOMPARE(testObject->roles().size(), 0); + QCOMPARE(testObject->row(), -1); + + //try to access previous roles + QCOMPARE(testObject->item()->value("key"), {}); + QCOMPARE(testObject->item()->value("other_color"), {}); + QCOMPARE(testObject->item()->value("other_role"), {}); + + //Update the filter to have a match + QCOMPARE(keyProperty.write(testObject, "other_key"), true); + + QCOMPARE(itemChangedSpy.count(), 4); + QCOMPARE(rolesChangedSpy.count(), 4); + QCOMPARE(sourceModelChangedSpy.count(), 3); + QCOMPARE(rowChangedSpy.count(), 3); + QCOMPARE(availableChangedSpy.count(), 3); + QCOMPARE(keyChangedSpy.count(), 2); + QCOMPARE(valueChangedSpy.count(), 1); + + QCOMPARE(testObject->roles().size(), 3); + QCOMPARE(testObject->roles().contains("other_key"), true); + QCOMPARE(testObject->roles().contains("other_color"), true); + QCOMPARE(testObject->roles().contains("other_role"), true); + + QCOMPARE(testObject->item()->value("other_key"), 1); + QCOMPARE(testObject->item()->value("other_color"), "red"); + QCOMPARE(testObject->item()->value("other_role"), 1); + + QCOMPARE(testObject->item(), modelItemProperty.read(testObject).value()); + + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(testObject->row(), 0); + QCOMPARE(rowProperty.read(testObject), 0); + + // update filter and the item is not found + QCOMPARE(valueProperty.write(testObject, 5), true); + + QCOMPARE(itemChangedSpy.count(), 5); + QCOMPARE(rolesChangedSpy.count(), 5); + QCOMPARE(sourceModelChangedSpy.count(), 3); + QCOMPARE(rowChangedSpy.count(), 4); + QCOMPARE(availableChangedSpy.count(), 4); + QCOMPARE(keyChangedSpy.count(), 2); + QCOMPARE(valueChangedSpy.count(), 2); + + QCOMPARE(testObject->roles().size(), 0); + QCOMPARE(testObject->row(), -1); + + //try to access previous roles + QCOMPARE(testObject->item()->value("other_key"), {}); + QCOMPARE(testObject->item()->value("other_color"), {}); + QCOMPARE(testObject->item()->value("other_role"), {}); + + QCOMPARE(testObject->available(), false); + QCOMPARE(availableProperty.read(testObject), false); + } + + void rowChangedTest() + { + QQmlEngine engine; + ListModelWrapper sourceModel( + engine, QJsonArray{QJsonObject{{"key", 1}, {"color", "red"}}, QJsonObject{{"key", 2}, {"color", "blue"}}}); + + QSignalSpy sourceModelChangedSpy(testObject, &ModelEntry::sourceModelChanged); + QSignalSpy keyChangedSpy(testObject, &ModelEntry::keyChanged); + QSignalSpy valueChangedSpy(testObject, &ModelEntry::valueChanged); + QSignalSpy itemChangedSpy(testObject, &ModelEntry::itemChanged); + QSignalSpy availableChangedSpy(testObject, &ModelEntry::availableChanged); + QSignalSpy rolesChangedSpy(testObject, &ModelEntry::rolesChanged); + QSignalSpy rowChangedSpy(testObject, &ModelEntry::rowChanged); + + // setting the initial source model + QCOMPARE(sourceModelProperty.write(testObject, QVariant::fromValue(sourceModel.model())), + true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 0); + QCOMPARE(valueChangedSpy.count(), 0); + QCOMPARE(itemChangedSpy.count(), 0); + QCOMPARE(availableChangedSpy.count(), 0); + QCOMPARE(rolesChangedSpy.count(), 0); + QCOMPARE(rowChangedSpy.count(), 0); + + // setting the filter + QCOMPARE(keyProperty.write(testObject, "key"), true); + QCOMPARE(valueProperty.write(testObject, 1), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rolesChangedSpy.count(), 1); + QCOMPARE(rowChangedSpy.count(), 1); + + // changing the row -> key = 1 to key = 3 => item is not found + sourceModel.set(0, QJsonObject{{"key", 3}, {"color", "green"}}); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 2); + QCOMPARE(availableChangedSpy.count(), 2); + QCOMPARE(rolesChangedSpy.count(), 2); + QCOMPARE(rowChangedSpy.count(), 2); + + QCOMPARE(testObject->row(), -1); + QCOMPARE(rowProperty.read(testObject), -1); + QCOMPARE(testObject->available(), false); + QCOMPARE(availableProperty.read(testObject), false); + QCOMPARE(testObject->roles().size(), 0); + + QCOMPARE(testObject->item()->value("key"), {}); + + // changing the row to key = 1 => item is found + sourceModel.set(0, QJsonObject{{"key", 1}, {"color", "red"}}); + QCOMPARE(testObject->row(), 0); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 3); + QCOMPARE(availableChangedSpy.count(), 3); + QCOMPARE(rolesChangedSpy.count(), 3); + QCOMPARE(rowChangedSpy.count(), 3); + + QCOMPARE(rowProperty.read(testObject), 0); + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(testObject->roles().size(), 2); + QCOMPARE(testObject->item()->value("key"), 1); + } + + void sourceModelResetTest() + { + QQmlEngine engine; + ListModelWrapper sourceModel( + engine, QJsonArray{QJsonObject{{"key", 1}, {"color", "red"}}, QJsonObject{{"key", 2}, {"color", "blue"}}}); + + QSignalSpy sourceModelChangedSpy(testObject, &ModelEntry::sourceModelChanged); + QSignalSpy keyChangedSpy(testObject, &ModelEntry::keyChanged); + QSignalSpy valueChangedSpy(testObject, &ModelEntry::valueChanged); + QSignalSpy itemChangedSpy(testObject, &ModelEntry::itemChanged); + QSignalSpy availableChangedSpy(testObject, &ModelEntry::availableChanged); + QSignalSpy rolesChangedSpy(testObject, &ModelEntry::rolesChanged); + QSignalSpy rowChangedSpy(testObject, &ModelEntry::rowChanged); + + // setting the initial source model + QCOMPARE(sourceModelProperty.write(testObject, QVariant::fromValue(sourceModel.model())), + true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 0); + QCOMPARE(valueChangedSpy.count(), 0); + QCOMPARE(itemChangedSpy.count(), 0); + QCOMPARE(availableChangedSpy.count(), 0); + QCOMPARE(rolesChangedSpy.count(), 0); + QCOMPARE(rowChangedSpy.count(), 0); + + // setting the filter + QCOMPARE(keyProperty.write(testObject, "key"), true); + QCOMPARE(valueProperty.write(testObject, 1), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rolesChangedSpy.count(), 1); + QCOMPARE(rowChangedSpy.count(), 1); + + QCOMPARE(testObject->row(), 0); + QCOMPARE(rowProperty.read(testObject), 0); + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(testObject->roles().size(), 2); + + QCOMPARE(testObject->item()->value("key"), 1); + QCOMPARE(testObject->item()->value("color"), "red"); + + // changing the source model + QCOMPARE(sourceModelProperty.write(testObject, QVariant::fromValue(nullptr)), true); + + QCOMPARE(sourceModelChangedSpy.count(), 2); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 2); + QCOMPARE(availableChangedSpy.count(), 2); + QCOMPARE(rolesChangedSpy.count(), 2); + QCOMPARE(rowChangedSpy.count(), 2); + + QCOMPARE(testObject->row(), -1); + QCOMPARE(rowProperty.read(testObject), -1); + QCOMPARE(testObject->available(), false); + QCOMPARE(availableProperty.read(testObject), false); + QCOMPARE(testObject->roles().size(), 0); + + QCOMPARE(testObject->item()->value("key"), {}); + } + + void sourceModelRowsInsertedTest() + { + QQmlEngine engine; + ListModelWrapper sourceModel( + engine, QJsonArray{QJsonObject{{"key", 1}, {"color", "red"}}, QJsonObject{{"key", 2}, {"color", "blue"}}}); + + QSignalSpy sourceModelChangedSpy(testObject, &ModelEntry::sourceModelChanged); + QSignalSpy keyChangedSpy(testObject, &ModelEntry::keyChanged); + QSignalSpy valueChangedSpy(testObject, &ModelEntry::valueChanged); + QSignalSpy itemChangedSpy(testObject, &ModelEntry::itemChanged); + QSignalSpy availableChangedSpy(testObject, &ModelEntry::availableChanged); + QSignalSpy rolesChangedSpy(testObject, &ModelEntry::rolesChanged); + QSignalSpy rowChangedSpy(testObject, &ModelEntry::rowChanged); + + // setting the initial source model + QCOMPARE(sourceModelProperty.write(testObject, QVariant::fromValue(sourceModel.model())), + true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 0); + QCOMPARE(valueChangedSpy.count(), 0); + QCOMPARE(itemChangedSpy.count(), 0); + QCOMPARE(availableChangedSpy.count(), 0); + QCOMPARE(rolesChangedSpy.count(), 0); + QCOMPARE(rowChangedSpy.count(), 0); + + // setting the filter + QCOMPARE(keyProperty.write(testObject, "key"), true); + QCOMPARE(valueProperty.write(testObject, 1), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rolesChangedSpy.count(), 1); + QCOMPARE(rowChangedSpy.count(), 1); + + QCOMPARE(testObject->row(), 0); + QCOMPARE(rowProperty.read(testObject), 0); + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(testObject->roles().size(), 2); + + QCOMPARE(testObject->item()->value("key"), 1); + QCOMPARE(testObject->item()->value("color"), "red"); + + // inserting a new row -> nothing should change + sourceModel.insert(0, QJsonObject{{"key", 3}, {"color", "green"}}); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rolesChangedSpy.count(), 1); + QCOMPARE(rowChangedSpy.count(), 1); + + QCOMPARE(testObject->row(), 0); + QCOMPARE(rowProperty.read(testObject), 0); + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(testObject->roles().size(), 2); + + QCOMPARE(testObject->item()->value("key"), 1); + QCOMPARE(testObject->item()->value("color"), "red"); + + // update the filter to invalidate the item and then insert a new row that's a match + QCOMPARE(valueProperty.write(testObject, 4), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 2); + QCOMPARE(itemChangedSpy.count(), 2); + QCOMPARE(availableChangedSpy.count(), 2); + QCOMPARE(rolesChangedSpy.count(), 2); + QCOMPARE(rowChangedSpy.count(), 2); + + QCOMPARE(testObject->row(), -1); + QCOMPARE(rowProperty.read(testObject), -1); + QCOMPARE(testObject->available(), false); + QCOMPARE(availableProperty.read(testObject), false); + QCOMPARE(testObject->roles().size(), 0); + + QCOMPARE(testObject->item()->value("key"), {}); + + sourceModel.insert(0, QJsonObject{{"key", 4}, {"color", "yellow"}}); + QCOMPARE(testObject->row(), 0); + QCOMPARE(rowProperty.read(testObject), 0); + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(testObject->roles().size(), 2); + + QCOMPARE(testObject->item()->value("key"), 4); + QCOMPARE(testObject->item()->value("color"), "yellow"); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 2); + QCOMPARE(itemChangedSpy.count(), 3); + QCOMPARE(availableChangedSpy.count(), 3); + QCOMPARE(rolesChangedSpy.count(), 3); + QCOMPARE(rowChangedSpy.count(), 3); + } + + void sourceModelRowsRemovedTest() + { + QQmlEngine engine; + ListModelWrapper sourceModel(engine, + QJsonArray{QJsonObject{{"key", 1}, {"color", "red"}}, + QJsonObject{{"key", 2}, {"color", "blue"}}, + QJsonObject{{"key", 3}, {"color", "green"}}}); + + QSignalSpy sourceModelChangedSpy(testObject, &ModelEntry::sourceModelChanged); + QSignalSpy keyChangedSpy(testObject, &ModelEntry::keyChanged); + QSignalSpy valueChangedSpy(testObject, &ModelEntry::valueChanged); + QSignalSpy itemChangedSpy(testObject, &ModelEntry::itemChanged); + QSignalSpy availableChangedSpy(testObject, &ModelEntry::availableChanged); + QSignalSpy rolesChangedSpy(testObject, &ModelEntry::rolesChanged); + QSignalSpy rowChangedSpy(testObject, &ModelEntry::rowChanged); + + // setting the initial source model + QCOMPARE(sourceModelProperty.write(testObject, QVariant::fromValue(sourceModel.model())), + true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 0); + QCOMPARE(valueChangedSpy.count(), 0); + QCOMPARE(itemChangedSpy.count(), 0); + QCOMPARE(availableChangedSpy.count(), 0); + QCOMPARE(rolesChangedSpy.count(), 0); + QCOMPARE(rowChangedSpy.count(), 0); + + // setting the filter + QCOMPARE(keyProperty.write(testObject, "key"), true); + QCOMPARE(valueProperty.write(testObject, 1), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rolesChangedSpy.count(), 1); + QCOMPARE(rowChangedSpy.count(), 1); + + QCOMPARE(testObject->row(), 0); + QCOMPARE(rowProperty.read(testObject), 0); + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(testObject->roles().size(), 2); + + QCOMPARE(testObject->item()->value("key"), 1); + + // removing a row that is not a match -> nothing should change + sourceModel.remove(1); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rolesChangedSpy.count(), 1); + QCOMPARE(rowChangedSpy.count(), 1); + + QCOMPARE(testObject->row(), 0); + QCOMPARE(rowProperty.read(testObject), 0); + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(testObject->roles().size(), 2); + + QCOMPARE(testObject->item()->value("key"), 1); + QCOMPARE(testObject->item()->value("color"), "red"); + + // remove the matching row + sourceModel.remove(0); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 2); + QCOMPARE(availableChangedSpy.count(), 2); + QCOMPARE(rolesChangedSpy.count(), 2); + QCOMPARE(rowChangedSpy.count(), 2); + + QCOMPARE(testObject->row(), -1); + QCOMPARE(rowProperty.read(testObject), -1); + QCOMPARE(testObject->available(), false); + QCOMPARE(availableProperty.read(testObject), false); + QCOMPARE(testObject->roles().size(), 0); + + QCOMPARE(testObject->item()->value("key"), {}); + } + + void sourceModelDataChangedTest() + { + QQmlEngine engine; + ListModelWrapper sourceModel(engine, + QJsonArray{QJsonObject{{"key", 1}, {"color", "red"}}, + QJsonObject{{"key", 2}, {"color", "blue"}}, + QJsonObject{{"key", 3}, {"color", "green"}}}); + + QSignalSpy sourceModelChangedSpy(testObject, &ModelEntry::sourceModelChanged); + QSignalSpy keyChangedSpy(testObject, &ModelEntry::keyChanged); + QSignalSpy valueChangedSpy(testObject, &ModelEntry::valueChanged); + QSignalSpy itemChangedSpy(testObject, &ModelEntry::itemChanged); + QSignalSpy availableChangedSpy(testObject, &ModelEntry::availableChanged); + QSignalSpy rolesChangedSpy(testObject, &ModelEntry::rolesChanged); + QSignalSpy rowChangedSpy(testObject, &ModelEntry::rowChanged); + + // setting the initial source model + QCOMPARE(sourceModelProperty.write(testObject, QVariant::fromValue(sourceModel.model())), + true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 0); + QCOMPARE(valueChangedSpy.count(), 0); + QCOMPARE(itemChangedSpy.count(), 0); + QCOMPARE(availableChangedSpy.count(), 0); + QCOMPARE(rolesChangedSpy.count(), 0); + QCOMPARE(rowChangedSpy.count(), 0); + + // setting the filter + QCOMPARE(keyProperty.write(testObject, "key"), true); + QCOMPARE(valueProperty.write(testObject, 1), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rolesChangedSpy.count(), 1); + QCOMPARE(rowChangedSpy.count(), 1); + + QCOMPARE(testObject->row(), 0); + QCOMPARE(rowProperty.read(testObject), 0); + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(testObject->roles().size(), 2); + + QCOMPARE(testObject->item()->value("key"), 1); + QCOMPARE(testObject->item()->value("color"), "red"); + + // update the matching row with new data -> no API signals expected; Only the item should be updated + QSignalSpy itemValueChangedSpy(testObject->item(), &QQmlPropertyMap::valueChanged); + sourceModel.setProperty(0, "color", "yellow"); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rolesChangedSpy.count(), 1); + QCOMPARE(rowChangedSpy.count(), 1); + QCOMPARE(itemValueChangedSpy.count(), 1); + QCOMPARE(itemValueChangedSpy.at(0).at(0).toString(), "color"); + QCOMPARE(itemValueChangedSpy.at(0).at(1).toString(), "yellow"); + + QCOMPARE(testObject->row(), 0); + QCOMPARE(rowProperty.read(testObject), 0); + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(testObject->roles().size(), 2); + + QCOMPARE(testObject->item()->value("key"), 1); + QCOMPARE(testObject->item()->value("color"), "yellow"); + } + + void sourceModelLayoutChangedTest() + { + TestModel sourceModel({{"key", {1, 2, 3}}, {"color", {"red", "blue", "green"}}}); + + QSignalSpy sourceModelChangedSpy(testObject, &ModelEntry::sourceModelChanged); + QSignalSpy keyChangedSpy(testObject, &ModelEntry::keyChanged); + QSignalSpy valueChangedSpy(testObject, &ModelEntry::valueChanged); + QSignalSpy itemChangedSpy(testObject, &ModelEntry::itemChanged); + QSignalSpy availableChangedSpy(testObject, &ModelEntry::availableChanged); + QSignalSpy rolesChangedSpy(testObject, &ModelEntry::rolesChanged); + QSignalSpy rowChangedSpy(testObject, &ModelEntry::rowChanged); + + // setting the initial source model + QCOMPARE(sourceModelProperty.write(testObject, QVariant::fromValue(&sourceModel)), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 0); + QCOMPARE(valueChangedSpy.count(), 0); + QCOMPARE(itemChangedSpy.count(), 0); + QCOMPARE(availableChangedSpy.count(), 0); + QCOMPARE(rolesChangedSpy.count(), 0); + QCOMPARE(rowChangedSpy.count(), 0); + + // setting the filter + QCOMPARE(keyProperty.write(testObject, "key"), true); + QCOMPARE(valueProperty.write(testObject, 1), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rolesChangedSpy.count(), 1); + QCOMPARE(rowChangedSpy.count(), 1); + + QCOMPARE(testObject->row(), 0); + QCOMPARE(rowProperty.read(testObject), 0); + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(testObject->roles().size(), 2); + + QCOMPARE(testObject->item()->value("key"), 1); + QCOMPARE(testObject->item()->value("color"), "red"); + + // update the layout -> only the row should change + sourceModel.invert(); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rolesChangedSpy.count(), 1); + QCOMPARE(rowChangedSpy.count(), 2); + + QCOMPARE(testObject->row(), 2); + QCOMPARE(rowProperty.read(testObject), 2); + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(testObject->roles().size(), 2); + + QCOMPARE(testObject->item()->value("key"), 1); + QCOMPARE(testObject->item()->value("color"), "red"); + } + + void cacheOnSourceRemovalTest() + { + QScopedPointer qObject1 {new QObject()}; + qObject1->setProperty("key", 1); + qObject1->setProperty("color", "red"); + + QScopedPointer qObject2 {new QObject()}; + qObject2->setProperty("key", 2); + qObject2->setProperty("color", "blue"); + + QScopedPointer qObject3 {new QObject()}; + qObject3->setProperty("key", 3); + qObject3->setProperty("color", "green"); + + QScopedPointer subModel1 {new TestModel({{"key", {1}}, {"color", {"red"}}})}; + QScopedPointer subModel2 {new TestModel({{"key", {2}}, {"color", {"blue"}}})}; + QScopedPointer subModel3 {new TestModel({{"key", {3}}, {"color", {"green"}}})}; + + TestModel sourceModel( + {{"key", {1, 2, 3}}, + {"color", {"red", "blue", "green"}}, + {"item", {QVariant::fromValue(qObject1.data()), QVariant::fromValue(qObject2.data()), QVariant::fromValue(qObject3.data())}}, + {"subModel", + {QVariant::fromValue(subModel1.data()), QVariant::fromValue(subModel2.data()), QVariant::fromValue(subModel3.data())}}}); + + QSignalSpy sourceModelChangedSpy(testObject, &ModelEntry::sourceModelChanged); + QSignalSpy keyChangedSpy(testObject, &ModelEntry::keyChanged); + QSignalSpy valueChangedSpy(testObject, &ModelEntry::valueChanged); + QSignalSpy itemChangedSpy(testObject, &ModelEntry::itemChanged); + QSignalSpy availableChangedSpy(testObject, &ModelEntry::availableChanged); + QSignalSpy rolesChangedSpy(testObject, &ModelEntry::rolesChanged); + QSignalSpy rowChangedSpy(testObject, &ModelEntry::rowChanged); + + // setting the initial source model + QCOMPARE(sourceModelProperty.write(testObject, QVariant::fromValue(&sourceModel)), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 0); + QCOMPARE(valueChangedSpy.count(), 0); + QCOMPARE(itemChangedSpy.count(), 0); + QCOMPARE(availableChangedSpy.count(), 0); + QCOMPARE(rolesChangedSpy.count(), 0); + QCOMPARE(rowChangedSpy.count(), 0); + + // setting the filter + QCOMPARE(keyProperty.write(testObject, "key"), true); + QCOMPARE(valueProperty.write(testObject, 1), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rolesChangedSpy.count(), 1); + QCOMPARE(rowChangedSpy.count(), 1); + + QCOMPARE(testObject->row(), 0); + QCOMPARE(rowProperty.read(testObject), 0); + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(testObject->roles().size(), 4); + + QCOMPARE(testObject->item()->value("key"), 1); + QCOMPARE(testObject->item()->value("color"), "red"); + QCOMPARE(testObject->item()->value("item"), QVariant::fromValue(qObject1.data())); + + QSignalSpy cacheOnRemovalChangedSpy(testObject, &ModelEntry::cacheOnRemovalChanged); + QSignalSpy itemRemovedFromModelSpy(testObject, &ModelEntry::itemRemovedFromModelChanged); + + QCOMPARE(cacheOnRemovalProperty.read(testObject), false); + QCOMPARE(cacheOnRemovalProperty.write(testObject, true), true); + QCOMPARE(cacheOnRemovalProperty.read(testObject), true); + QCOMPARE(itemRemovedFromCacheProperty.read(testObject), false); + QCOMPARE(cacheOnRemovalChangedSpy.count(), 1); + QCOMPARE(itemRemovedFromModelSpy.count(), 0); + + // remove the item in the source model -> the item should still be valid + sourceModel.remove(0); + qObject1.reset(); + subModel1.reset(); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rolesChangedSpy.count(), 1); + QCOMPARE(rowChangedSpy.count(), 2); + + QCOMPARE(cacheOnRemovalChangedSpy.count(), 1); + QCOMPARE(itemRemovedFromModelSpy.count(), 1); + QCOMPARE(itemRemovedFromCacheProperty.read(testObject), true); + QCOMPARE(testObject->row(), -1); + + QCOMPARE(testObject->item()->value("key"), 1); + QCOMPARE(testObject->item()->value("color"), "red"); + QCOMPARE(testObject->item()->value("item").toMap().value("key"), 1); + QCOMPARE(testObject->item()->value("item").toMap().value("color"), "red"); + + auto cachedSubModel = testObject->item()->value("subModel").value(); + QVERIFY(cachedSubModel != nullptr); + QCOMPARE(cachedSubModel->data(cachedSubModel->index(0, 0), cachedSubModel->roleNames().key("key")), 1); + QCOMPARE(cachedSubModel->data(cachedSubModel->index(0, 0), cachedSubModel->roleNames().key("color")), "red"); + + // change filter to match an existing row + QCOMPARE(valueProperty.write(testObject, 2), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 2); + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rolesChangedSpy.count(), 1); + QCOMPARE(testObject->row(), 0); + QCOMPARE(rowChangedSpy.count(), 3); + + QCOMPARE(cacheOnRemovalChangedSpy.count(), 1); + QCOMPARE(itemRemovedFromModelSpy.count(), 2); + QCOMPARE(itemRemovedFromCacheProperty.read(testObject), false); + QCOMPARE(testObject->row(), 0); + + QCOMPARE(testObject->item()->value("key"), 2); + QCOMPARE(testObject->item()->value("color"), "blue"); + + // disable cache on removal and delete the row -> the item should be invalidated + sourceModel.remove(0); + QCOMPARE(itemRemovedFromModelSpy.count(), 3); + QCOMPARE(itemRemovedFromCacheProperty.read(testObject), true); + + QCOMPARE(cacheOnRemovalProperty.write(testObject, false), true); + QCOMPARE(cacheOnRemovalProperty.read(testObject), false); + + QCOMPARE(cacheOnRemovalChangedSpy.count(), 2); + QCOMPARE(itemRemovedFromModelSpy.count(), 4); + QCOMPARE(availableChangedSpy.count(), 2); + + QCOMPARE(testObject->available(), false); + QCOMPARE(testObject->itemRemovedFromModel(), false); + QCOMPARE(itemRemovedFromCacheProperty.read(testObject), false); + + QCOMPARE(availableProperty.read(testObject), false); + QCOMPARE(testObject->item()->value("key"), {}); + QCOMPARE(testObject->item()->value("color"), {}); + } + + void cachedItemOnModelReset() + { + auto qObject1 = new QObject(this); + qObject1->setProperty("key", 1); + qObject1->setProperty("color", "red"); + + auto qObject2 = new QObject(this); + qObject2->setProperty("key", 2); + qObject2->setProperty("color", "blue"); + + auto qObject3 = new QObject(this); + qObject3->setProperty("key", 3); + qObject3->setProperty("color", "green"); + + auto subModel1 = new TestModel({{"key", {1}}, {"color", {"red"}}}); + auto subModel2 = new TestModel({{"key", {2}}, {"color", {"blue"}}}); + auto subModel3 = new TestModel({{"key", {3}}, {"color", {"green"}}}); + + TestModel sourceModel( + {{"key", {1, 2, 3}}, + {"color", {"red", "blue", "green"}}, + {"item", {QVariant::fromValue(qObject1), QVariant::fromValue(qObject2), QVariant::fromValue(qObject3)}}, + {"subModel", + {QVariant::fromValue(subModel1), QVariant::fromValue(subModel2), QVariant::fromValue(subModel3)}}}); + + QSignalSpy sourceModelChangedSpy(testObject, &ModelEntry::sourceModelChanged); + QSignalSpy keyChangedSpy(testObject, &ModelEntry::keyChanged); + QSignalSpy valueChangedSpy(testObject, &ModelEntry::valueChanged); + QSignalSpy itemChangedSpy(testObject, &ModelEntry::itemChanged); + QSignalSpy availableChangedSpy(testObject, &ModelEntry::availableChanged); + QSignalSpy rolesChangedSpy(testObject, &ModelEntry::rolesChanged); + QSignalSpy rowChangedSpy(testObject, &ModelEntry::rowChanged); + + // setting the initial source model + QCOMPARE(sourceModelProperty.write(testObject, QVariant::fromValue(&sourceModel)), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 0); + QCOMPARE(valueChangedSpy.count(), 0); + QCOMPARE(itemChangedSpy.count(), 0); + QCOMPARE(availableChangedSpy.count(), 0); + QCOMPARE(rolesChangedSpy.count(), 0); + QCOMPARE(rowChangedSpy.count(), 0); + + // setting the filter + QCOMPARE(keyProperty.write(testObject, "key"), true); + QCOMPARE(valueProperty.write(testObject, 1), true); + + // setting the cache on removal flag + QCOMPARE(cacheOnRemovalProperty.write(testObject, true), true); + + TestModel sourceModel2( + {{"key", {4, 2, 3}}, + {"color", {"red", "blue", "green"}}, + {"item", {QVariant::fromValue(qObject1), QVariant::fromValue(qObject2), QVariant::fromValue(qObject3)}}, + {"subModel", + {QVariant::fromValue(subModel1), QVariant::fromValue(subModel2), QVariant::fromValue(subModel3)}}}); + + // set another model without a matching row + QCOMPARE(sourceModelProperty.write(testObject, QVariant::fromValue(&sourceModel2)), true); + + QCOMPARE(sourceModelChangedSpy.count(), 2); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 2); + QCOMPARE(availableChangedSpy.count(), 2); + + QCOMPARE(testObject->available(), false); + QCOMPARE(availableProperty.read(testObject), false); + QCOMPARE(testObject->itemRemovedFromModel(), false); + QCOMPARE(itemRemovedFromCacheProperty.read(testObject), false); + QCOMPARE(testObject->item()->value("key"), {}); + QCOMPARE(testObject->item()->value("color"), {}); + + // match a row + QCOMPARE(valueProperty.write(testObject, 2), true); + // delete the row + sourceModel2.remove(1); + + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(testObject->itemRemovedFromModel(), true); + QCOMPARE(itemRemovedFromCacheProperty.read(testObject), true); + QCOMPARE(testObject->item()->value("key"), 2); + QCOMPARE(testObject->item()->value("color"), "blue"); + + // insert the row back + sourceModel2.insert(1, {2, "red", QVariant::fromValue(qObject2), QVariant::fromValue(subModel2)}); + + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(testObject->itemRemovedFromModel(), false); + QCOMPARE(itemRemovedFromCacheProperty.read(testObject), false); + QCOMPARE(testObject->item()->value("key"), 2); + QCOMPARE(testObject->item()->value("color"), "red"); + } + + void keyValueFilterTest() + { + TestModel sourceModel({{"key", {1, 2, 3}}, {"color", {"red", "blue", "green"}}}); + + QSignalSpy sourceModelChangedSpy(testObject, &ModelEntry::sourceModelChanged); + QSignalSpy keyChangedSpy(testObject, &ModelEntry::keyChanged); + QSignalSpy valueChangedSpy(testObject, &ModelEntry::valueChanged); + QSignalSpy itemChangedSpy(testObject, &ModelEntry::itemChanged); + QSignalSpy availableChangedSpy(testObject, &ModelEntry::availableChanged); + QSignalSpy rolesChangedSpy(testObject, &ModelEntry::rolesChanged); + QSignalSpy rowChangedSpy(testObject, &ModelEntry::rowChanged); + + // setting the initial source model + QCOMPARE(sourceModelProperty.write(testObject, QVariant::fromValue(&sourceModel)), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 0); + QCOMPARE(valueChangedSpy.count(), 0); + QCOMPARE(itemChangedSpy.count(), 0); + QCOMPARE(availableChangedSpy.count(), 0); + QCOMPARE(rolesChangedSpy.count(), 0); + QCOMPARE(rowChangedSpy.count(), 0); + + // setting the filter + QCOMPARE(keyProperty.write(testObject, {"key"}), true); + QCOMPARE(valueProperty.write(testObject, QVariant::fromValue(1)), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(rolesChangedSpy.count(), 1); + QCOMPARE(rowChangedSpy.count(), 1); + + QCOMPARE(testObject->row(), 0); + QCOMPARE(rowProperty.read(testObject), 0); + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(testObject->roles().size(), 2); + + QCOMPARE(testObject->item()->value("key"), 1); + QCOMPARE(testObject->item()->value("color"), "red"); + + QCOMPARE(testObject->key(), "key"); + QCOMPARE(testObject->value(), 1); + + // update the key -> the item should be invalidated + QCOMPARE(keyProperty.write(testObject, "color"), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 2); + QCOMPARE(valueChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.count(), 2); + QCOMPARE(availableChangedSpy.count(), 2); + QCOMPARE(rolesChangedSpy.count(), 2); + QCOMPARE(rowChangedSpy.count(), 2); + + QCOMPARE(testObject->row(), -1); + QCOMPARE(rowProperty.read(testObject), -1); + QCOMPARE(testObject->available(), false); + QCOMPARE(availableProperty.read(testObject), false); + QCOMPARE(testObject->roles().size(), 0); + + QCOMPARE(testObject->item()->value("key"), {}); + QCOMPARE(testObject->item()->value("color"), {}); + + QCOMPARE(valueProperty.write(testObject, "blue"), true); + + QCOMPARE(sourceModelChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 2); + QCOMPARE(valueChangedSpy.count(), 2); + QCOMPARE(itemChangedSpy.count(), 3); + QCOMPARE(availableChangedSpy.count(), 3); + QCOMPARE(rolesChangedSpy.count(), 3); + QCOMPARE(rowChangedSpy.count(), 3); + + QCOMPARE(testObject->row(), 1); + QCOMPARE(rowProperty.read(testObject), 1); + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + QCOMPARE(testObject->roles().size(), 2); + } + + void signalOrderTest() + { + TestModel sourceModel({{"key", {1, 2, 3}}, {"color", {"red", "blue", "green"}}}); + + QSignalSpy availableChangedSpy(testObject, &ModelEntry::availableChanged); + auto availableChangeConnection = connect(testObject, &ModelEntry::availableChanged, this, [this]() { + // when the available signal is emmitted, the item should still be valid + // inital state -> available = false + // setting the source model -> available = true. In this case the item should be valid before available is emmitted + // setting the filter -> available = false. In this case the item should be invalidated after available is set to false + QCOMPARE(testObject->item()->isEmpty(), false); + QCOMPARE(testObject->roles().size(), 2); + QVERIFY(testObject->item()->value("key") != QVariant{}); + }); + + auto itemChangedConnection = connect(testObject, &ModelEntry::itemChanged, this, [this]() { + // roles should be available after the item is changed + QCOMPARE(testObject->roles().size(), 0); + QVERIFY(testObject->item()->value("key") != QVariant{}); + }); + + auto rolesChangedConnection = connect(testObject, &ModelEntry::rolesChanged, this, [this]() { + // the item should be afailable when the roles are set + QCOMPARE(testObject->roles().size(), 2); + QVERIFY(testObject->item()->value("key") != QVariant{}); + }); + + // setting the initial source model + QCOMPARE(sourceModelProperty.write(testObject, QVariant::fromValue(&sourceModel)), true); + + // setting the filter + QCOMPARE(keyProperty.write(testObject, "key"), true); + QCOMPARE(valueProperty.write(testObject, 1), true); + + QCOMPARE(availableChangedSpy.count(), 1); + QCOMPARE(testObject->available(), true); + QCOMPARE(availableProperty.read(testObject), true); + + QCOMPARE(testObject->item()->value("key"), 1); + QCOMPARE(testObject->item()->value("color"), "red"); + + disconnect(itemChangedConnection); + disconnect(rolesChangedConnection); + + itemChangedConnection = connect(testObject, &ModelEntry::itemChanged, this, [this]() { + // roles should be invalidated before the item is invalidated + QCOMPARE(rolesProperty.read(testObject).toStringList().size(), 0); + QVERIFY(modelItemProperty.read(testObject) != QVariant{}); + }); + + rolesChangedConnection = connect(testObject, &ModelEntry::rolesChanged, this, [this]() { + // the item should be invalid when the roles are invalid + QCOMPARE(testObject->roles().size(), 0); + QVERIFY(testObject->item()->value("key") == QVariant{}); + }); + + QCOMPARE(keyProperty.write(testObject, "color"), true); + + QCOMPARE(availableChangedSpy.count(), 2); + QCOMPARE(testObject->available(), false); + QCOMPARE(availableProperty.read(testObject), false); + + QCOMPARE(testObject->item()->value("key"), {}); + QCOMPARE(testObject->item()->value("color"), {}); + } + + void itemObjectCleanupTest() + { + TestModel sourceModel1({{"key", {1, 2, 3}}, {"color", {"red", "blue", "green"}}}); + TestModel sourceModel2({{"key", {1, 2, 3}}, {"other_color", {"red", "blue", "green"}}}); + + // setting the initial source model + QCOMPARE(sourceModelProperty.write(testObject, QVariant::fromValue(&sourceModel1)), true); + + // setting the filter + QCOMPARE(keyProperty.write(testObject, "key"), true); + QCOMPARE(valueProperty.write(testObject, 1), true); + + QCOMPARE(testObject->item()->value("key"), 1); + QCOMPARE(testObject->item()->value("color"), "red"); + + auto itemObj = testObject->item(); + QSignalSpy deletedSpy(itemObj, &QObject::destroyed); + + // set another source model with different roles + QCOMPARE(sourceModelProperty.write(testObject, QVariant::fromValue(&sourceModel2)), true); + + QCOMPARE(testObject->item()->value("key"), 1); + QCOMPARE(testObject->item()->value("color"), {}); + QCOMPARE(testObject->item()->value("other_color"), "red"); + + // the item object should be destroyed + QCOMPARE(deletedSpy.count(), 1); + } +}; + +QTEST_MAIN(TestModelEntry) +#include "tst_ModelEntry.moc" \ No newline at end of file diff --git a/ui/StatusQ/tests/tst_MovableModel.cpp b/ui/StatusQ/tests/tst_MovableModel.cpp index 4c981b6bb4..143b2442f9 100644 --- a/ui/StatusQ/tests/tst_MovableModel.cpp +++ b/ui/StatusQ/tests/tst_MovableModel.cpp @@ -7,12 +7,12 @@ #include #include +#include #include #include #include #include -#include class TestMovableModel : public QObject { diff --git a/ui/StatusQ/tests/tst_SnapshotObject.cpp b/ui/StatusQ/tests/tst_SnapshotObject.cpp new file mode 100644 index 0000000000..231070910c --- /dev/null +++ b/ui/StatusQ/tests/tst_SnapshotObject.cpp @@ -0,0 +1,201 @@ +#include "StatusQ/snapshotobject.h" + +#include +#include + +#include +#include +#include +#include + +#include + +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 {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 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 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(); + 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 {new SnapshotObject()}; + + { + QScopedPointer 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(); + + 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" diff --git a/ui/StatusQ/tests/tst_WritableProxyModel.cpp b/ui/StatusQ/tests/tst_WritableProxyModel.cpp index 6a880d76e9..2b79616334 100644 --- a/ui/StatusQ/tests/tst_WritableProxyModel.cpp +++ b/ui/StatusQ/tests/tst_WritableProxyModel.cpp @@ -1,17 +1,19 @@ #include #include -#include #include +#include +#include "StatusQ/snapshotmodel.h" #include "StatusQ/writableproxymodel.h" -#include -#include #include +#include -namespace { +namespace +{ -class TestSourceModel : public QAbstractListModel { +class TestSourceModel : public QAbstractListModel +{ public: explicit TestSourceModel(QList> data)