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/formatteddoubleproperty.h
|
||||||
include/StatusQ/functionaggregator.h
|
include/StatusQ/functionaggregator.h
|
||||||
include/StatusQ/leftjoinmodel.h
|
include/StatusQ/leftjoinmodel.h
|
||||||
|
include/StatusQ/modelsyncedcontainer.h
|
||||||
include/StatusQ/modelutilsinternal.h
|
include/StatusQ/modelutilsinternal.h
|
||||||
include/StatusQ/movablemodel.h
|
include/StatusQ/movablemodel.h
|
||||||
include/StatusQ/permissionutilsinternal.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)
|
add_executable(MovableModelTest tst_MovableModel.cpp)
|
||||||
target_link_libraries(MovableModelTest PRIVATE StatusQ StatusQTestLib)
|
target_link_libraries(MovableModelTest PRIVATE StatusQ StatusQTestLib)
|
||||||
add_test(NAME MovableModelTest COMMAND MovableModelTest)
|
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