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
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<SourceModel*> m_sources;
QStringList m_expectedRoles;
bool m_propagateResets = false;
QString m_markerRoleName = s_defaultMarkerRoleName;
int m_markerRole = 0;

View File

@ -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<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)
@ -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) {
if (currentCount == 0)
return;
auto prefix = this->countPrefix(index);
this->beginRemoveRows({}, prefix, prefix + currentCount - 1);
}
});
connect(model, &QAbstractItemModel::modelReset, this, [this, model, index]
@ -687,7 +714,10 @@ void ConcatModel::connectModelSlots(int index, QAbstractItemModel *model)
auto count = model->rowCount();
if (!m_initialized) {
if (count) {
if (count != 0) {
if (m_propagateResets)
this->beginResetModel();
else
this->beginInsertRows({}, 0, count - 1);
initRoles();
@ -696,19 +726,27 @@ void ConcatModel::connectModelSlots(int index, QAbstractItemModel *model)
m_rowCounts[index] = count;
if (m_propagateResets)
this->endResetModel();
else
this->endInsertRows();
}
} else {
if (m_propagateResets) {
initRolesMapping(index, model);
m_rowCounts[index] = count;
this->endResetModel();
} else {
auto previousCount = m_rowCounts[index];
if (previousCount) {
if (previousCount != 0) {
m_rowCounts[index] = 0;
this->endRemoveRows();
}
initRolesMapping(index, model);
if (count) {
if (count != 0) {
auto prefix = this->countPrefix(index);
this->beginInsertRows({}, prefix, prefix + count - 1);
@ -717,6 +755,7 @@ void ConcatModel::connectModelSlots(int index, QAbstractItemModel *model)
this->endInsertRows();
}
}
}
});
connect(model, &QAbstractItemModel::dataChanged, this,

View File

@ -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<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();
@ -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<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);
@ -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