fix(StatusQ/ConcatModel): Handle source submodel reset properly, tests amended

Closes: #12974
This commit is contained in:
Michał Cieślak 2023-12-08 17:33:08 +01:00 committed by Michał
parent 9a0ce98d1b
commit fd638de880
2 changed files with 337 additions and 13 deletions

View File

@ -303,7 +303,7 @@ QVariant ConcatModel::data(const QModelIndex &index, int role) const
if (model == nullptr) if (model == nullptr)
return {}; return {};
auto& mapping = m_rolesMappingToSource.at(i); auto& mapping = m_rolesMappingToSource[i];
auto it = mapping.find(role); auto it = mapping.find(role);
if (it == mapping.end()) if (it == mapping.end())
@ -517,7 +517,7 @@ void ConcatModel::initRolesMapping()
m_rolesMappingInitializationFlags.resize(m_sources.size(), false); m_rolesMappingInitializationFlags.resize(m_sources.size(), false);
for (auto i = 0; i < m_sources.size(); ++i) { for (auto i = 0; i < m_sources.size(); ++i) {
auto sourceModelWrapper = m_sources.at(i); auto sourceModelWrapper = m_sources[i];
auto sourceModel = sourceModelWrapper->model(); auto sourceModel = sourceModelWrapper->model();
if (sourceModel == nullptr) if (sourceModel == nullptr)
@ -530,7 +530,7 @@ void ConcatModel::initRolesMapping()
void ConcatModel::initAllModelsSlots() void ConcatModel::initAllModelsSlots()
{ {
for (auto sourceIndex = 0; sourceIndex < m_sources.size(); ++sourceIndex) { for (auto sourceIndex = 0; sourceIndex < m_sources.size(); ++sourceIndex) {
auto sourceModelWrapper = m_sources.at(sourceIndex); auto sourceModelWrapper = m_sources[sourceIndex];
auto sourceModel = sourceModelWrapper->model(); auto sourceModel = sourceModelWrapper->model();
if (sourceModel) if (sourceModel)
@ -556,7 +556,7 @@ void ConcatModel::connectModelSlots(int index, QAbstractItemModel *model)
initRoles(); initRoles();
initRolesMapping(); initRolesMapping();
m_initialized = true; m_initialized = true;
} else if (!m_rolesMappingInitializationFlags.at(index)) { } else if (!m_rolesMappingInitializationFlags[index]) {
initRolesMapping(index, model); initRolesMapping(index, model);
} }
@ -603,17 +603,54 @@ void ConcatModel::connectModelSlots(int index, QAbstractItemModel *model)
emit this->layoutChanged(); emit this->layoutChanged();
}); });
connect(model, &QAbstractItemModel::modelAboutToBeReset, this, [this] connect(model, &QAbstractItemModel::modelAboutToBeReset, this, [this, index]
{ {
this->beginResetModel(); if (!m_initialized)
return;
auto currentCount = m_rowCounts[index];
if (currentCount) {
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]
{ {
m_rowCounts[index] = model->rowCount(); auto count = model->rowCount();
m_rolesMappingInitializationFlags[index] = false;
this->endResetModel(); if (!m_initialized) {
if (count) {
this->beginInsertRows({}, 0, count - 1);
initRoles();
initRolesMapping();
m_initialized = true;
m_rowCounts[index] = count;
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);
m_rowCounts[index] = count;
this->endInsertRows();
}
}
}); });
connect(model, &QAbstractItemModel::dataChanged, this, connect(model, &QAbstractItemModel::dataChanged, this,
@ -655,7 +692,7 @@ void ConcatModel::fetchRowCounts()
m_rowCounts.resize(m_sources.size()); m_rowCounts.resize(m_sources.size());
for (auto i = 0; i < m_sources.size(); ++i) { for (auto i = 0; i < m_sources.size(); ++i) {
auto sourceModelWrapper = m_sources.at(i); auto sourceModelWrapper = m_sources[i];
auto sourceModel = sourceModelWrapper->model(); auto sourceModel = sourceModelWrapper->model();
m_rowCounts[i] = (sourceModel == nullptr) ? 0 : sourceModel->rowCount(); m_rowCounts[i] = (sourceModel == nullptr) ? 0 : sourceModel->rowCount();

View File

@ -1,3 +1,4 @@
#include <QIdentityProxyModel>
#include <QSignalSpy> #include <QSignalSpy>
#include <QTest> #include <QTest>
@ -17,6 +18,16 @@
namespace { namespace {
// Workaround for a bug in QIdentityProxyModel (returning roleNames improperly)
class IdentityModel : public QIdentityProxyModel {
public:
QHash<int,QByteArray> roleNames() const override {
if (sourceModel())
return sourceModel()->roleNames();
return {};
}
};
class ListModelWrapper { class ListModelWrapper {
public: public:
@ -103,7 +114,7 @@ public:
} }
void clear() { void clear() {
runExpression(QString("append()")); runExpression(QString("clear()"));
} }
void remove(int index, int count = 1) { void remove(int index, int count = 1) {
@ -1609,9 +1620,285 @@ private slots:
QCOMPARE(layoutChangedSpy.count(), 1); QCOMPARE(layoutChangedSpy.count(), 1);
} }
void layoutResetTest() void modelResetWhenEmptyTest()
{ {
// TODO QQmlEngine engine;
ConcatModel model;
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.model());
proxy2.setSourceModel(sourceModel2.model());
proxy3.setSourceModel(sourceModel3.model());
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(), {});
{
QSignalSpy modelAboutToBeResetSpy(&model, &ConcatModel::modelAboutToBeReset);
QSignalSpy modelResetSpy(&model, &ConcatModel::modelReset);
QSignalSpy rowsAboutToBeInsertedSpy(&model, &ConcatModel::rowsAboutToBeInserted);
QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted);
proxy2.setSourceModel(sourceModel4.model());
QCOMPARE(modelAboutToBeResetSpy.count(), 0);
QCOMPARE(modelResetSpy.count(), 0);
QCOMPARE(rowsAboutToBeInsertedSpy.count(), 0);
QCOMPARE(rowsInsertedSpy.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);
// checking validity inside rowsAboutToBeInserted signal
{
QObject context;
connect(&model, &ConcatModel::rowsAboutToBeInserted, &context,
[&model] { QCOMPARE(model.rowCount(), 0); });
proxy2.setSourceModel(sourceModel5.model());
}
QCOMPARE(modelAboutToBeResetSpy.count(), 0);
QCOMPARE(modelResetSpy.count(), 0);
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(rowsInsertedSpy.count(), 1);
QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{});
QCOMPARE(rowsInsertedSpy.at(0).at(1), 0);
QCOMPARE(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 modelResetWhenNotEmptyTest()
{
QQmlEngine engine;
ConcatModel model;
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.model());
proxy2.setSourceModel(sourceModel2.model());
proxy3.setSourceModel(sourceModel3.model());
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
{
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);
// checking validity inside rowsAboutToBeRemoved signal
{
QObject context;
connect(&model, &ConcatModel::rowsAboutToBeRemoved, &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.model());
}
QCOMPARE(modelAboutToBeResetSpy.count(), 0);
QCOMPARE(modelResetSpy.count(), 0);
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(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
{
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);
// checking validity inside rowsAboutToBeRemoved, rowsRemoved and
// rowsAboutToBeInserted signals
{
QObject context;
connect(&model, &ConcatModel::rowsAboutToBeRemoved, &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");
});
connect(&model, &ConcatModel::rowsRemoved, &context,
[this, &model, &roles] {
QCOMPARE(model.rowCount(), 0);
});
connect(&model, &ConcatModel::rowsAboutToBeInserted, &context,
[this, &model, &roles] {
QCOMPARE(model.rowCount(), 0);
});
proxy1.setSourceModel(sourceModel5.model());
}
QCOMPARE(modelAboutToBeResetSpy.count(), 0);
QCOMPARE(modelResetSpy.count(), 0);
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(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(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 sameModelUsedMultipleTimesTest() void sameModelUsedMultipleTimesTest()