StatusQ/ConcatModel: flag added changing behavior on source model's reset

Closes: #15891
This commit is contained in:
Michał Cieślak 2024-07-30 16:31:56 +02:00 committed by Michał
parent e94fb9c6f6
commit baa1baa17f
3 changed files with 335 additions and 88 deletions

View File

@ -49,6 +49,8 @@ class ConcatModel : public QAbstractListModel, public QQmlParserStatus
Q_PROPERTY(QStringList expectedRoles READ expectedRoles Q_PROPERTY(QStringList expectedRoles READ expectedRoles
WRITE setExpectedRoles NOTIFY expectedRolesChanged) WRITE setExpectedRoles NOTIFY expectedRolesChanged)
Q_PROPERTY(bool propagateResets READ propagateResets
WRITE setPropagateResets NOTIFY propagateResetsChanged)
public: public:
explicit ConcatModel(QObject *parent = nullptr); explicit ConcatModel(QObject *parent = nullptr);
@ -60,6 +62,9 @@ public:
void setExpectedRoles(const QStringList& expectedRoles); void setExpectedRoles(const QStringList& expectedRoles);
const QStringList& expectedRoles() const; const QStringList& expectedRoles() const;
void setPropagateResets(bool propagateResets);
bool propagateResets() const;
Q_INVOKABLE int sourceModelRow(int row) const; Q_INVOKABLE int sourceModelRow(int row) const;
Q_INVOKABLE QAbstractItemModel* sourceModel(int row) const; Q_INVOKABLE QAbstractItemModel* sourceModel(int row) const;
Q_INVOKABLE int fromSourceRow(const QAbstractItemModel* model, int row) const; Q_INVOKABLE int fromSourceRow(const QAbstractItemModel* model, int row) const;
@ -77,6 +82,7 @@ public:
signals: signals:
void markerRoleNameChanged(); void markerRoleNameChanged();
void expectedRolesChanged(); void expectedRolesChanged();
void propagateResetsChanged();
private: private:
static constexpr auto s_defaultMarkerRoleName = "whichModel"; static constexpr auto s_defaultMarkerRoleName = "whichModel";
@ -101,6 +107,7 @@ private:
QList<SourceModel*> m_sources; QList<SourceModel*> m_sources;
QStringList m_expectedRoles; QStringList m_expectedRoles;
bool m_propagateResets = false;
QString m_markerRoleName = s_defaultMarkerRoleName; QString m_markerRoleName = s_defaultMarkerRoleName;
int m_markerRole = 0; int m_markerRole = 0;

View File

@ -216,6 +216,27 @@ const QStringList& ConcatModel::expectedRoles() const
return m_expectedRoles; return m_expectedRoles;
} }
void ConcatModel::setPropagateResets(bool propagateResets)
{
if (m_propagateResets == propagateResets)
return;
m_propagateResets = propagateResets;
emit propagateResetsChanged();
}
/*!
\qmlproperty list<string> 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) \qmlmethod int StatusQ::ConcatModel::sourceModelRow(row)
@ -674,12 +695,18 @@ void ConcatModel::connectModelSlots(int index, QAbstractItemModel *model)
if (!m_initialized) if (!m_initialized)
return; return;
if (m_propagateResets) {
this->beginResetModel();
return;
}
auto currentCount = m_rowCounts[index]; auto currentCount = m_rowCounts[index];
if (currentCount) { if (currentCount == 0)
auto prefix = this->countPrefix(index); return;
this->beginRemoveRows({}, prefix, prefix + currentCount - 1);
} auto prefix = this->countPrefix(index);
this->beginRemoveRows({}, prefix, prefix + currentCount - 1);
}); });
connect(model, &QAbstractItemModel::modelReset, this, [this, model, index] connect(model, &QAbstractItemModel::modelReset, this, [this, model, index]
@ -687,8 +714,11 @@ void ConcatModel::connectModelSlots(int index, QAbstractItemModel *model)
auto count = model->rowCount(); auto count = model->rowCount();
if (!m_initialized) { if (!m_initialized) {
if (count) { if (count != 0) {
this->beginInsertRows({}, 0, count - 1); if (m_propagateResets)
this->beginResetModel();
else
this->beginInsertRows({}, 0, count - 1);
initRoles(); initRoles();
initRolesMapping(); initRolesMapping();
@ -696,25 +726,34 @@ void ConcatModel::connectModelSlots(int index, QAbstractItemModel *model)
m_rowCounts[index] = count; m_rowCounts[index] = count;
this->endInsertRows(); if (m_propagateResets)
this->endResetModel();
else
this->endInsertRows();
} }
} else { } else {
auto previousCount = m_rowCounts[index]; if (m_propagateResets) {
initRolesMapping(index, model);
if (previousCount) {
m_rowCounts[index] = 0;
this->endRemoveRows();
}
initRolesMapping(index, model);
if (count) {
auto prefix = this->countPrefix(index);
this->beginInsertRows({}, prefix, prefix + count - 1);
m_rowCounts[index] = count; 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();
}
} }
} }
}); });

View File

@ -185,6 +185,24 @@ private slots:
QCOMPARE(model.fromSourceRow(sourceModel3, 4), -1); 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() void dataChangeTest()
{ {
QQmlEngine engine; QQmlEngine engine;
@ -1539,29 +1557,15 @@ private slots:
QCOMPARE(model.roleNames(), {}); QCOMPARE(model.roleNames(), {});
{ {
QSignalSpy modelAboutToBeResetSpy(&model, &ConcatModel::modelAboutToBeReset); ModelSignalsSpy signalsSpy(&model);
QSignalSpy modelResetSpy(&model, &ConcatModel::modelReset);
QSignalSpy rowsAboutToBeInsertedSpy(&model, &ConcatModel::rowsAboutToBeInserted);
QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted);
proxy2.setSourceModel(sourceModel4); proxy2.setSourceModel(sourceModel4);
QCOMPARE(modelAboutToBeResetSpy.count(), 0); QCOMPARE(signalsSpy.count(), 0);
QCOMPARE(modelResetSpy.count(), 0);
QCOMPARE(rowsAboutToBeInsertedSpy.count(), 0);
QCOMPARE(rowsInsertedSpy.count(), 0);
QCOMPARE(model.rowCount(), 0); QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.roleNames(), {}); QCOMPARE(model.roleNames(), {});
} }
{ {
QSignalSpy modelAboutToBeResetSpy(&model, &ConcatModel::modelAboutToBeReset); ModelSignalsSpy signalsSpy(&model);
QSignalSpy modelResetSpy(&model, &ConcatModel::modelReset);
QSignalSpy rowsAboutToBeInsertedSpy(&model, &ConcatModel::rowsAboutToBeInserted);
QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted);
// checking validity inside rowsAboutToBeInserted signal // checking validity inside rowsAboutToBeInserted signal
{ {
@ -1572,18 +1576,96 @@ private slots:
proxy2.setSourceModel(sourceModel5); proxy2.setSourceModel(sourceModel5);
} }
QCOMPARE(modelAboutToBeResetSpy.count(), 0); QCOMPARE(signalsSpy.count(), 2);
QCOMPARE(modelResetSpy.count(), 0);
QCOMPARE(rowsAboutToBeInsertedSpy.count(), 1); QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.count(), 1);
QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(0), QModelIndex{});
QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(1), 0); QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(1), 0);
QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(2), 1); QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(2), 1);
QCOMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(signalsSpy.rowsInsertedSpy.count(), 1);
QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(0), QModelIndex{});
QCOMPARE(rowsInsertedSpy.at(0).at(1), 0); QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(1), 0);
QCOMPARE(rowsInsertedSpy.at(0).at(2), 1); 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<SourceModel> 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(); auto roles = model.roleNames();
@ -1651,14 +1733,7 @@ private slots:
// reset to empty model // reset to empty model
{ {
QSignalSpy modelAboutToBeResetSpy(&model, &ConcatModel::modelAboutToBeReset); ModelSignalsSpy signalsSpy(&model);
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);
// checking validity inside rowsAboutToBeRemoved signal // checking validity inside rowsAboutToBeRemoved signal
{ {
@ -1674,17 +1749,13 @@ private slots:
proxy2.setSourceModel(sourceModel4); proxy2.setSourceModel(sourceModel4);
} }
QCOMPARE(modelAboutToBeResetSpy.count(), 0); QCOMPARE(signalsSpy.count(), 2);
QCOMPARE(modelResetSpy.count(), 0);
QCOMPARE(rowsAboutToBeInsertedSpy.count(), 0); QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.count(), 1);
QCOMPARE(rowsInsertedSpy.count(), 0); QCOMPARE(signalsSpy.rowsRemovedSpy.count(), 1);
QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(0), QModelIndex{});
QCOMPARE(rowsAboutToBeRemovedSpy.count(), 1); QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(1), 2);
QCOMPARE(rowsRemovedSpy.count(), 1); QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(2), 4);
QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(0), QModelIndex{});
QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(1), 2);
QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(2), 4);
QCOMPARE(model.rowCount(), 2); QCOMPARE(model.rowCount(), 2);
@ -1714,14 +1785,7 @@ private slots:
} }
// reset to not empty model // reset to not empty model
{ {
QSignalSpy modelAboutToBeResetSpy(&model, &ConcatModel::modelAboutToBeReset); ModelSignalsSpy signalsSpy(&model);
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);
// checking validity inside rowsAboutToBeRemoved, rowsRemoved and // checking validity inside rowsAboutToBeRemoved, rowsRemoved and
// rowsAboutToBeInserted signals // rowsAboutToBeInserted signals
@ -1750,22 +1814,159 @@ private slots:
proxy1.setSourceModel(sourceModel5); proxy1.setSourceModel(sourceModel5);
} }
QCOMPARE(modelAboutToBeResetSpy.count(), 0); QCOMPARE(signalsSpy.count(), 4);
QCOMPARE(modelResetSpy.count(), 0);
QCOMPARE(rowsAboutToBeRemovedSpy.count(), 1); QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.count(), 1);
QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(0), QModelIndex{});
QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(1), 0); QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(1), 0);
QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(2), 1); QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(2), 1);
QCOMPARE(rowsRemovedSpy.count(), 1); QCOMPARE(signalsSpy.rowsRemovedSpy.count(), 1);
QCOMPARE(rowsAboutToBeInsertedSpy.count(), 1); QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.count(), 1);
QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(0), QModelIndex{});
QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(1), 0); QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(1), 0);
QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(2), 2); 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<SourceModel> 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); QCOMPARE(model.rowCount(), 3);
@ -2180,10 +2381,10 @@ private slots:
// //
// import QtQuick 2.15 // import QtQuick 2.15
// import QtQuick.Controls 2.15 // import QtQuick.Controls 2.15
//
// import StatusQ 0.1 // import StatusQ 0.1
// import SortFilterProxyModel 0.2 // import SortFilterProxyModel 0.2
//
// Item { // Item {
// ListModel { // ListModel {
// id: src // id: src