diff --git a/ui/StatusQ/include/StatusQ/concatmodel.h b/ui/StatusQ/include/StatusQ/concatmodel.h index e0b39ee6ef..8d8a3c506f 100644 --- a/ui/StatusQ/include/StatusQ/concatmodel.h +++ b/ui/StatusQ/include/StatusQ/concatmodel.h @@ -49,6 +49,8 @@ class ConcatModel : public QAbstractListModel, public QQmlParserStatus Q_PROPERTY(QStringList expectedRoles READ expectedRoles WRITE setExpectedRoles NOTIFY expectedRolesChanged) + Q_PROPERTY(bool propagateResets READ propagateResets + WRITE setPropagateResets NOTIFY propagateResetsChanged) public: explicit ConcatModel(QObject *parent = nullptr); @@ -60,6 +62,9 @@ public: void setExpectedRoles(const QStringList& expectedRoles); const QStringList& expectedRoles() const; + void setPropagateResets(bool propagateResets); + bool propagateResets() const; + Q_INVOKABLE int sourceModelRow(int row) const; Q_INVOKABLE QAbstractItemModel* sourceModel(int row) const; Q_INVOKABLE int fromSourceRow(const QAbstractItemModel* model, int row) const; @@ -77,6 +82,7 @@ public: signals: void markerRoleNameChanged(); void expectedRolesChanged(); + void propagateResetsChanged(); private: static constexpr auto s_defaultMarkerRoleName = "whichModel"; @@ -101,6 +107,7 @@ private: QList m_sources; QStringList m_expectedRoles; + bool m_propagateResets = false; QString m_markerRoleName = s_defaultMarkerRoleName; int m_markerRole = 0; diff --git a/ui/StatusQ/src/concatmodel.cpp b/ui/StatusQ/src/concatmodel.cpp index 67d8dc7a37..e48777ea73 100644 --- a/ui/StatusQ/src/concatmodel.cpp +++ b/ui/StatusQ/src/concatmodel.cpp @@ -216,6 +216,27 @@ const QStringList& ConcatModel::expectedRoles() const return m_expectedRoles; } +void ConcatModel::setPropagateResets(bool propagateResets) +{ + if (m_propagateResets == propagateResets) + return; + + m_propagateResets = propagateResets; + emit propagateResetsChanged(); +} + +/*! + \qmlproperty list StatusQ::ConcatModel::propagateResets + + When set to true, model resets on source models result with model reset of + the ConcatModel. Otherwise model resets of sources are handled as removals + and insertions. Default is false. +*/ +bool ConcatModel::propagateResets() const +{ + return m_propagateResets; +} + /*! \qmlmethod int StatusQ::ConcatModel::sourceModelRow(row) @@ -674,12 +695,18 @@ void ConcatModel::connectModelSlots(int index, QAbstractItemModel *model) if (!m_initialized) return; + if (m_propagateResets) { + this->beginResetModel(); + return; + } + auto currentCount = m_rowCounts[index]; - if (currentCount) { - auto prefix = this->countPrefix(index); - this->beginRemoveRows({}, prefix, prefix + currentCount - 1); - } + if (currentCount == 0) + return; + + auto prefix = this->countPrefix(index); + this->beginRemoveRows({}, prefix, prefix + currentCount - 1); }); connect(model, &QAbstractItemModel::modelReset, this, [this, model, index] @@ -687,8 +714,11 @@ void ConcatModel::connectModelSlots(int index, QAbstractItemModel *model) auto count = model->rowCount(); if (!m_initialized) { - if (count) { - this->beginInsertRows({}, 0, count - 1); + if (count != 0) { + if (m_propagateResets) + this->beginResetModel(); + else + this->beginInsertRows({}, 0, count - 1); initRoles(); initRolesMapping(); @@ -696,25 +726,34 @@ void ConcatModel::connectModelSlots(int index, QAbstractItemModel *model) m_rowCounts[index] = count; - this->endInsertRows(); + if (m_propagateResets) + this->endResetModel(); + else + this->endInsertRows(); } } else { - auto previousCount = m_rowCounts[index]; - - if (previousCount) { - m_rowCounts[index] = 0; - this->endRemoveRows(); - } - - initRolesMapping(index, model); - - if (count) { - auto prefix = this->countPrefix(index); - this->beginInsertRows({}, prefix, prefix + count - 1); - + if (m_propagateResets) { + initRolesMapping(index, model); m_rowCounts[index] = count; + this->endResetModel(); + } else { + auto previousCount = m_rowCounts[index]; - this->endInsertRows(); + if (previousCount != 0) { + m_rowCounts[index] = 0; + this->endRemoveRows(); + } + + initRolesMapping(index, model); + + if (count != 0) { + auto prefix = this->countPrefix(index); + this->beginInsertRows({}, prefix, prefix + count - 1); + + m_rowCounts[index] = count; + + this->endInsertRows(); + } } } }); diff --git a/ui/StatusQ/tests/tst_ConcatModel.cpp b/ui/StatusQ/tests/tst_ConcatModel.cpp index 27a93f6cb5..84ed21fb92 100644 --- a/ui/StatusQ/tests/tst_ConcatModel.cpp +++ b/ui/StatusQ/tests/tst_ConcatModel.cpp @@ -185,6 +185,24 @@ private slots: QCOMPARE(model.fromSourceRow(sourceModel3, 4), -1); } + void settingPropagateResetTest() + { + ConcatModel model; + QSignalSpy spy(&model, &ConcatModel::propagateResetsChanged); + + QCOMPARE(model.propagateResets(), false); + model.setPropagateResets(false); + QCOMPARE(spy.count(), 0); + + model.setPropagateResets(true); + QCOMPARE(spy.count(), 1); + model.setPropagateResets(true); + QCOMPARE(spy.count(), 1); + + model.setPropagateResets(false); + QCOMPARE(spy.count(), 2); + } + void dataChangeTest() { QQmlEngine engine; @@ -1539,29 +1557,15 @@ private slots: QCOMPARE(model.roleNames(), {}); { - QSignalSpy modelAboutToBeResetSpy(&model, &ConcatModel::modelAboutToBeReset); - QSignalSpy modelResetSpy(&model, &ConcatModel::modelReset); - - QSignalSpy rowsAboutToBeInsertedSpy(&model, &ConcatModel::rowsAboutToBeInserted); - QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); - + ModelSignalsSpy signalsSpy(&model); proxy2.setSourceModel(sourceModel4); - QCOMPARE(modelAboutToBeResetSpy.count(), 0); - QCOMPARE(modelResetSpy.count(), 0); - - QCOMPARE(rowsAboutToBeInsertedSpy.count(), 0); - QCOMPARE(rowsInsertedSpy.count(), 0); - + QCOMPARE(signalsSpy.count(), 0); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); } { - QSignalSpy modelAboutToBeResetSpy(&model, &ConcatModel::modelAboutToBeReset); - QSignalSpy modelResetSpy(&model, &ConcatModel::modelReset); - - QSignalSpy rowsAboutToBeInsertedSpy(&model, &ConcatModel::rowsAboutToBeInserted); - QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); + ModelSignalsSpy signalsSpy(&model); // checking validity inside rowsAboutToBeInserted signal { @@ -1572,18 +1576,96 @@ private slots: proxy2.setSourceModel(sourceModel5); } - QCOMPARE(modelAboutToBeResetSpy.count(), 0); - QCOMPARE(modelResetSpy.count(), 0); + QCOMPARE(signalsSpy.count(), 2); - QCOMPARE(rowsAboutToBeInsertedSpy.count(), 1); - QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(0), QModelIndex{}); - QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(1), 0); - QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(2), 1); + QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.count(), 1); + QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(0), QModelIndex{}); + QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(1), 0); + QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(2), 1); - QCOMPARE(rowsInsertedSpy.count(), 1); - QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{}); - QCOMPARE(rowsInsertedSpy.at(0).at(1), 0); - QCOMPARE(rowsInsertedSpy.at(0).at(2), 1); + QCOMPARE(signalsSpy.rowsInsertedSpy.count(), 1); + QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(0), QModelIndex{}); + QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(1), 0); + QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(2), 1); + + auto roles = model.roleNames(); + + QCOMPARE(model.rowCount(), 2); + QCOMPARE(roles.count(), 3); + + QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 1); + QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "key")), 2); + QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "color")), "red"); + QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "color")), "blue"); + } + } + + void modelResetWhenEmptyWithPropagateResetsTest() + { + QQmlEngine engine; + ConcatModel model; + model.setPropagateResets(true); + + ListModelWrapper sourceModel1(engine); + ListModelWrapper sourceModel2(engine); + ListModelWrapper sourceModel3(engine); + ListModelWrapper sourceModel4(engine); + ListModelWrapper sourceModel5(engine, QJsonArray { + QJsonObject {{ "key", 1}, { "color", "red" }}, + QJsonObject {{ "key", 2}, { "color", "blue" }} + }); + + QQmlListProperty sources = model.sources(); + + SourceModel source1, source2, source3; + + IdentityModel proxy1, proxy2, proxy3; + + proxy1.setSourceModel(sourceModel1); + proxy2.setSourceModel(sourceModel2); + proxy3.setSourceModel(sourceModel3); + + source1.setModel(&proxy1); + source2.setModel(&proxy2); + source3.setModel(&proxy3); + + sources.append(&sources, &source1); + sources.append(&sources, &source2); + sources.append(&sources, &source3); + + QCOMPARE(model.rowCount(), 0); + QCOMPARE(model.roleNames(), {}); + QCOMPARE(model.index(0, 0).isValid(), false); + + model.componentComplete(); + + QCOMPARE(model.rowCount(), 0); + QCOMPARE(model.roleNames(), {}); + + { + ModelSignalsSpy signalsSpy(&model); + proxy2.setSourceModel(sourceModel4); + + QCOMPARE(signalsSpy.count(), 0); + QCOMPARE(model.rowCount(), 0); + QCOMPARE(model.roleNames(), {}); + } + { + ModelSignalsSpy signalsSpy(&model); + + // checking validity inside rowsAboutToBeInserted signal + { + QObject context; + connect(&model, &ConcatModel::rowsAboutToBeInserted, &context, + [&model] { QCOMPARE(model.rowCount(), 0); }); + + proxy2.setSourceModel(sourceModel5); + } + + QCOMPARE(signalsSpy.count(), 2); + + QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1); + QCOMPARE(signalsSpy.modelResetSpy.count(), 1); auto roles = model.roleNames(); @@ -1651,14 +1733,7 @@ private slots: // reset to empty model { - QSignalSpy modelAboutToBeResetSpy(&model, &ConcatModel::modelAboutToBeReset); - QSignalSpy modelResetSpy(&model, &ConcatModel::modelReset); - - QSignalSpy rowsAboutToBeInsertedSpy(&model, &ConcatModel::rowsAboutToBeInserted); - QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); - - QSignalSpy rowsAboutToBeRemovedSpy(&model, &ConcatModel::rowsAboutToBeRemoved); - QSignalSpy rowsRemovedSpy(&model, &ConcatModel::rowsRemoved); + ModelSignalsSpy signalsSpy(&model); // checking validity inside rowsAboutToBeRemoved signal { @@ -1674,17 +1749,13 @@ private slots: proxy2.setSourceModel(sourceModel4); } - QCOMPARE(modelAboutToBeResetSpy.count(), 0); - QCOMPARE(modelResetSpy.count(), 0); + QCOMPARE(signalsSpy.count(), 2); - QCOMPARE(rowsAboutToBeInsertedSpy.count(), 0); - QCOMPARE(rowsInsertedSpy.count(), 0); - - QCOMPARE(rowsAboutToBeRemovedSpy.count(), 1); - QCOMPARE(rowsRemovedSpy.count(), 1); - QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(0), QModelIndex{}); - QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(1), 2); - QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(2), 4); + QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.count(), 1); + QCOMPARE(signalsSpy.rowsRemovedSpy.count(), 1); + QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(0), QModelIndex{}); + QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(1), 2); + QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(2), 4); QCOMPARE(model.rowCount(), 2); @@ -1714,14 +1785,7 @@ private slots: } // reset to not empty model { - QSignalSpy modelAboutToBeResetSpy(&model, &ConcatModel::modelAboutToBeReset); - QSignalSpy modelResetSpy(&model, &ConcatModel::modelReset); - - QSignalSpy rowsAboutToBeRemovedSpy(&model, &ConcatModel::rowsAboutToBeRemoved); - QSignalSpy rowsRemovedSpy(&model, &ConcatModel::rowsRemoved); - - QSignalSpy rowsAboutToBeInsertedSpy(&model, &ConcatModel::rowsAboutToBeInserted); - QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); + ModelSignalsSpy signalsSpy(&model); // checking validity inside rowsAboutToBeRemoved, rowsRemoved and // rowsAboutToBeInserted signals @@ -1750,22 +1814,159 @@ private slots: proxy1.setSourceModel(sourceModel5); } - QCOMPARE(modelAboutToBeResetSpy.count(), 0); - QCOMPARE(modelResetSpy.count(), 0); + QCOMPARE(signalsSpy.count(), 4); - QCOMPARE(rowsAboutToBeRemovedSpy.count(), 1); - QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(0), QModelIndex{}); - QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(1), 0); - QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(2), 1); + QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.count(), 1); + QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(0), QModelIndex{}); + QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(1), 0); + QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(2), 1); - QCOMPARE(rowsRemovedSpy.count(), 1); + QCOMPARE(signalsSpy.rowsRemovedSpy.count(), 1); - QCOMPARE(rowsAboutToBeInsertedSpy.count(), 1); - QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(0), QModelIndex{}); - QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(1), 0); - QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(2), 2); + QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.count(), 1); + QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(0), QModelIndex{}); + QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(1), 0); + QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(2), 2); - QCOMPARE(rowsInsertedSpy.count(), 1); + QCOMPARE(signalsSpy.rowsInsertedSpy.count(), 1); + + QCOMPARE(model.rowCount(), 3); + + QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 11); + QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "key")), 12); + QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "key")), 13); + QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "color")), "red"); + QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "color")), "blue"); + QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "color")), "pink"); + } + } + + void modelResetWhenNotEmptyWithPropagateResetsTest() + { + QQmlEngine engine; + ConcatModel model; + model.setPropagateResets(true); + + ListModelWrapper sourceModel1(engine, QJsonArray { + QJsonObject {{ "key", 1}, { "color", "red" }}, + QJsonObject {{ "key", 2}, { "color", "blue" }} + }); + ListModelWrapper sourceModel2(engine, QJsonArray { + QJsonObject {{ "key", 3}}, + QJsonObject {{ "key", 4}}, + QJsonObject {{ "key", 5}} + }); + ListModelWrapper sourceModel3(engine); + + ListModelWrapper sourceModel4(engine); + ListModelWrapper sourceModel5(engine, QJsonArray { + QJsonObject {{ "color", "red" }, { "name", "a" }, { "key", 11}}, + QJsonObject {{ "color", "blue" }, { "name", "b" }, { "key", 12}}, + QJsonObject {{ "color", "pink" }, { "name", "c" }, { "key", 13}} + }); + + QQmlListProperty sources = model.sources(); + + SourceModel source1, source2, source3; + + IdentityModel proxy1, proxy2, proxy3; + + proxy1.setSourceModel(sourceModel1); + proxy2.setSourceModel(sourceModel2); + proxy3.setSourceModel(sourceModel3); + + source1.setModel(&proxy1); + source2.setModel(&proxy2); + source3.setModel(&proxy3); + + sources.append(&sources, &source1); + sources.append(&sources, &source2); + sources.append(&sources, &source3); + + QCOMPARE(model.rowCount(), 0); + QCOMPARE(model.roleNames(), {}); + QCOMPARE(model.index(0, 0).isValid(), false); + + model.componentComplete(); + + auto roles = model.roleNames(); + + QCOMPARE(model.rowCount(), 5); + QCOMPARE(roles.count(), 3); + + // reset to empty model + { + ModelSignalsSpy signalsSpy(&model); + + // checking validity inside modelAboutToBeReset signal + { + QObject context; + connect(&model, &ConcatModel::modelAboutToBeReset, &context, + [this, &model, &roles] { + QCOMPARE(model.rowCount(), 5); + + QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "key")), 4); + QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "color")), {}); + }); + + proxy2.setSourceModel(sourceModel4); + } + + QCOMPARE(signalsSpy.count(), 2); + + QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1); + QCOMPARE(signalsSpy.modelResetSpy.count(), 1); + + QCOMPARE(model.rowCount(), 2); + + QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 1); + QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "key")), 2); + QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "color")), "red"); + QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "color")), "blue"); + + // insert some data to check if roles are re-initialized properly + sourceModel4.append(QJsonArray { + QJsonObject {{ "color", "purple"}, { "key", 3} }, + QJsonObject {{ "color", "green" }, { "key", 4}} + }); + + QCOMPARE(model.rowCount(), 4); + + QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 1); + QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "key")), 2); + QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "key")), 3); + QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "key")), 4); + QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "color")), "red"); + QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "color")), "blue"); + QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "color")), "purple"); + QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "color")), "green"); + + sourceModel4.clear(); + } + // reset to not empty model + { + ModelSignalsSpy signalsSpy(&model); + + // checking validity inside modelAboutToBeReset signal + { + QObject context; + connect(&model, &ConcatModel::modelAboutToBeReset, &context, + [this, &model, &roles] { + QCOMPARE(model.rowCount(), 2); + + QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 1); + QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "key")), 2); + QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "color")), "red"); + QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "color")), "blue"); + }); + + proxy1.setSourceModel(sourceModel5); + } + + QCOMPARE(signalsSpy.count(), 2); + + QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1); + QCOMPARE(signalsSpy.modelResetSpy.count(), 1); QCOMPARE(model.rowCount(), 3); @@ -2180,10 +2381,10 @@ private slots: // // import QtQuick 2.15 // import QtQuick.Controls 2.15 - + // // import StatusQ 0.1 // import SortFilterProxyModel 0.2 - + // // Item { // ListModel { // id: src