feat(StatusQ): ModelSyncedContainer utility

This commit is contained in:
Michał Cieślak 2024-05-20 15:42:09 +02:00 committed by Michał
parent e4dffc60aa
commit d9707091d3
4 changed files with 405 additions and 0 deletions

View File

@ -101,6 +101,7 @@ add_library(StatusQ SHARED
include/StatusQ/formatteddoubleproperty.h
include/StatusQ/functionaggregator.h
include/StatusQ/leftjoinmodel.h
include/StatusQ/modelsyncedcontainer.h
include/StatusQ/modelutilsinternal.h
include/StatusQ/movablemodel.h
include/StatusQ/permissionutilsinternal.h

View File

@ -0,0 +1,148 @@
#pragma once
#include <QAbstractItemModel>
#include <QObject>
#include <QVariant>
template<typename T>
class ModelSyncedContainer
{
public:
void setModel(QAbstractItemModel* model)
{
m_container.clear();
if (model == nullptr)
return;
m_container.resize(model->rowCount());
QObject::connect(model, &QAbstractItemModel::rowsRemoved, &m_ctx,
[this] (const QModelIndex& parent, int first, int last)
{
if (parent.isValid())
return;
m_container.erase(m_container.cbegin() + first,
m_container.cbegin() + last + 1);
});
QObject::connect(model, &QAbstractItemModel::rowsInserted, &m_ctx,
[this] (const QModelIndex& parent, int first, int last)
{
if (parent.isValid())
return;
auto count = last - first + 1;
if (count <= 0)
return;
std::vector<T> toBeInserted(count);
m_container.insert(m_container.cbegin() + first,
std::make_move_iterator(toBeInserted.begin()),
std::make_move_iterator(toBeInserted.end()));
});
QObject::connect(model, &QAbstractItemModel::rowsAboutToBeMoved, &m_ctx,
[this, model] (const QModelIndex& parent)
{
if (parent.isValid())
return;
storePersistentIndexes(model);
});
QObject::connect(model, &QAbstractItemModel::rowsMoved,
&m_ctx, [this] (const QModelIndex& parent)
{
if (parent.isValid())
return;
// This implementation is simplified. Can be replaced by faster
// implementation not using persistent indexes but reordering
// the container directly
updateFromPersistentIndexes();
});
QObject::connect(model, &QAbstractItemModel::layoutAboutToBeChanged,
&m_ctx, [this, model]
{
storePersistentIndexes(model);
});
QObject::connect(model, &QAbstractItemModel::layoutChanged,
&m_ctx, [this]
{
updateFromPersistentIndexes();
});
QObject::connect(model, &QAbstractItemModel::modelReset,
&m_ctx, [this, model]
{
m_container.clear();
m_container.resize(model->rowCount());
});
QObject::connect(model, &QAbstractItemModel::destroyed,
&m_ctx, [this, model]
{
m_container.clear();
});
}
const T& operator[](std::size_t i) const
{
return m_container[i];
}
T& operator[](std::size_t i)
{
return m_container[i];
}
std::size_t size() const {
return m_container.size();
}
const std::vector<T>& data() const {
return m_container;
}
private:
void storePersistentIndexes(QAbstractItemModel* model)
{
auto count = model->rowCount();
m_persistentIndexes.clear();
m_persistentIndexes.reserve(count);
for (decltype(count) i = 0; i < count; i++)
m_persistentIndexes.push_back(model->index(i, 0));
}
void updateFromPersistentIndexes()
{
auto newCount = std::count_if(
m_persistentIndexes.cbegin(), m_persistentIndexes.cend(),
[] (auto& idx) { return idx.isValid(); });
std::vector<T> newContainer(newCount);
for (std::size_t i = 0; i < m_persistentIndexes.size(); i++) {
QModelIndex idx = m_persistentIndexes[i];
if (!idx.isValid())
continue;
newContainer[idx.row()] = std::move(m_container[i]);
}
std::swap(m_container, newContainer);
m_persistentIndexes.clear();
}
QList<QPersistentModelIndex> m_persistentIndexes;
std::vector<T> m_container;
QObject m_ctx;
};

View File

@ -97,3 +97,7 @@ add_test(NAME WritableProxyModelTest COMMAND WritableProxyModelTest)
add_executable(MovableModelTest tst_MovableModel.cpp)
target_link_libraries(MovableModelTest PRIVATE StatusQ StatusQTestLib)
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)

View File

@ -0,0 +1,252 @@
#include <QtTest>
#include <QQmlEngine>
#include <StatusQ/modelsyncedcontainer.h>
#include <TestHelpers/listmodelwrapper.h>
#include <TestHelpers/testmodel.h>
class TestModelSyncedContainer : public QObject
{
Q_OBJECT
private slots:
void basicTest()
{
QQmlEngine engine;
ListModelWrapper model(engine, R"([
{ "name": "A" }, { "name": "A" }, { "name": "B" },
{ "name": "C" }, { "name": "C" }, { "name": "C" }
])");
ModelSyncedContainer<int> container;
QCOMPARE(container.size(), 0);
container.setModel(model);
QCOMPARE(container.size(), 6);
QCOMPARE(container.data(), std::vector<int>({0, 0, 0, 0, 0, 0}));
}
void modelChangeTest()
{
QQmlEngine engine;
ListModelWrapper model1(engine, R"([
{ "name": "A" }, { "name": "A" }, { "name": "B" },
{ "name": "C" }, { "name": "C" }, { "name": "C" }
])");
ListModelWrapper model2(engine, R"([ { "name": "A" } ])");
ModelSyncedContainer<int> container;
QCOMPARE(container.size(), 0);
container.setModel(model1);
QCOMPARE(container.size(), 6);
QCOMPARE(container.data(), std::vector<int>({0, 0, 0, 0, 0, 0}));
container[0] = 3;
container.setModel(model2);
QCOMPARE(container.size(), 1);
QCOMPARE(container.data(), std::vector<int>({0}));
container.setModel(nullptr);
QCOMPARE(container.size(), 0);
}
void appendTest()
{
QQmlEngine engine;
ListModelWrapper model(engine, R"([
{ "name": "A" }, { "name": "A" }, { "name": "B" },
{ "name": "C" }, { "name": "C" }, { "name": "C" }
])");
ModelSyncedContainer<int> container;
container.setModel(model);
for (auto i = 0; i < container.size(); i++)
container[i] = i;
model.append(QJsonArray {
QJsonObject {{ "key", 205}, { "balance", 305 }, { "name", "n205" }},
QJsonObject {{ "key", 206},{ "balance", 306 }, { "name", "n206" }}
});
QCOMPARE(container.data(), std::vector<int>({0, 1, 2, 3, 4, 5, 0, 0}));
}
void insertTest()
{
QQmlEngine engine;
ListModelWrapper model(engine, R"([
{ "name": "A" }, { "name": "A" }, { "name": "B" },
{ "name": "C" }, { "name": "C" }, { "name": "C" }
])");
ModelSyncedContainer<int> container;
container.setModel(model);
for (auto i = 0; i < container.size(); i++)
container[i] = i;
model.insert(2, QJsonArray {
QJsonObject {{ "key", 205}, { "balance", 305 }, { "name", "n205" }},
QJsonObject {{ "key", 206},{ "balance", 306 }, { "name", "n206" }}
});
QCOMPARE(container.data(), std::vector<int>({0, 1, 0, 0, 2, 3, 4, 5}));
}
void moveSingleItemTest()
{
QQmlEngine engine;
ListModelWrapper model(engine, R"([
{ "name": "A" }, { "name": "B" }, { "name": "C" },
{ "name": "D" }, { "name": "E" }, { "name": "F" }
])");
ModelSyncedContainer<int> container;
container.setModel(model);
for (auto i = 0; i < container.size(); i++)
container[i] = i;
model.move(1, 0);
QCOMPARE(container.data(), std::vector<int>({1, 0, 2, 3, 4, 5}));
model.move(5, 0);
QCOMPARE(container.data(), std::vector<int>({5, 1, 0, 2, 3, 4}));
model.move(0, 5);
QCOMPARE(container.data(), std::vector<int>({1, 0, 2, 3, 4, 5}));
}
void moveMultipleItemTest()
{
QQmlEngine engine;
ListModelWrapper model(engine, R"([
{ "name": "A" }, { "name": "B" }, { "name": "C" },
{ "name": "D" }, { "name": "E" }, { "name": "F" }
])");
ModelSyncedContainer<int> container;
container.setModel(model);
for (auto i = 0; i < container.size(); i++)
container[i] = i;
model.move(0, 3, 3);
QCOMPARE(container.data(), std::vector<int>({3, 4, 5, 0, 1, 2}));
model.move(3, 0, 3);
QCOMPARE(container.data(), std::vector<int>({0, 1, 2, 3, 4, 5}));
}
void removeTest()
{
QQmlEngine engine;
ListModelWrapper model(engine, R"([
{ "name": "A" }, { "name": "B" }, { "name": "C" },
{ "name": "D" }, { "name": "E" }, { "name": "F" }
])");
ModelSyncedContainer<int> container;
container.setModel(model);
for (auto i = 0; i < container.size(); i++)
container[i] = i;
model.remove(1, 2);
QCOMPARE(container.data(), std::vector<int>({0, 3, 4, 5}));
}
void layoutChangeTest()
{
TestModel model({
{ "name", { "A", "B", "C", "D", "E", "F" }}
});
ModelSyncedContainer<int> container;
container.setModel(&model);
for (auto i = 0; i < container.size(); i++)
container[i] = i;
model.invert();
QCOMPARE(container.data(), std::vector<int>({5, 4, 3, 2, 1, 0}));
}
void layoutChangeWithRomovalTest()
{
TestModel model({
{ "name", { "A", "B", "C", "D", "E", "F" }}
});
ModelSyncedContainer<int> container;
container.setModel(&model);
for (auto i = 0; i < container.size(); i++)
container[i] = i;
model.removeEverySecond();
QCOMPARE(container.data(), std::vector<int>({1, 3, 5}));
}
void modelResetTest()
{
TestModel model({
{ "name", { "A", "B", "C", "D", "E", "F" }}
});
ModelSyncedContainer<int> container;
container.setModel(&model);
for (auto i = 0; i < container.size(); i++)
container[i] = i;
model.reset();
QCOMPARE(container.data(), std::vector<int>({0, 0, 0, 0, 0, 0}));
}
void modelDestroyedTest()
{
ModelSyncedContainer<int> container;
{
TestModel model({
{ "name", { "A", "B", "C", "D", "E", "F" }}
});
container.setModel(&model);
QCOMPARE(container.size(), model.rowCount());
}
QCOMPARE(container.size(), 0);
}
// This test verifies if `ModelSyncedContainer` can be parametrized with
// non-copyable type, like std::unique_ptr
void nonCopyableTypeTest()
{
TestModel model({
{ "name", { "A", "B", "C", "D", "E", "F" }}
});
ModelSyncedContainer<std::unique_ptr<int>> container;
container.setModel(&model);
}
};
QTEST_MAIN(TestModelSyncedContainer)
#include "tst_ModelSyncedContainer.moc"