feat(StatusQ): ModelSyncedContainer utility
This commit is contained in:
parent
e4dffc60aa
commit
d9707091d3
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
Loading…
Reference in New Issue