diff --git a/ui/StatusQ/CMakeLists.txt b/ui/StatusQ/CMakeLists.txt index c7d1b651c5..d71e460e89 100644 --- a/ui/StatusQ/CMakeLists.txt +++ b/ui/StatusQ/CMakeLists.txt @@ -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 diff --git a/ui/StatusQ/include/StatusQ/modelsyncedcontainer.h b/ui/StatusQ/include/StatusQ/modelsyncedcontainer.h new file mode 100644 index 0000000000..0d6bd4e58d --- /dev/null +++ b/ui/StatusQ/include/StatusQ/modelsyncedcontainer.h @@ -0,0 +1,148 @@ +#pragma once + +#include +#include +#include + +template +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 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& 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 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 m_persistentIndexes; + std::vector m_container; + QObject m_ctx; +}; diff --git a/ui/StatusQ/tests/CMakeLists.txt b/ui/StatusQ/tests/CMakeLists.txt index bf84b38e73..bd31822528 100644 --- a/ui/StatusQ/tests/CMakeLists.txt +++ b/ui/StatusQ/tests/CMakeLists.txt @@ -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) diff --git a/ui/StatusQ/tests/tst_ModelSyncedContainer.cpp b/ui/StatusQ/tests/tst_ModelSyncedContainer.cpp new file mode 100644 index 0000000000..f564665de8 --- /dev/null +++ b/ui/StatusQ/tests/tst_ModelSyncedContainer.cpp @@ -0,0 +1,252 @@ +#include +#include + +#include + +#include +#include + +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 container; + QCOMPARE(container.size(), 0); + + container.setModel(model); + QCOMPARE(container.size(), 6); + + QCOMPARE(container.data(), std::vector({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 container; + QCOMPARE(container.size(), 0); + + container.setModel(model1); + QCOMPARE(container.size(), 6); + + QCOMPARE(container.data(), std::vector({0, 0, 0, 0, 0, 0})); + + container[0] = 3; + + container.setModel(model2); + QCOMPARE(container.size(), 1); + + QCOMPARE(container.data(), std::vector({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 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({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 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({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 container; + container.setModel(model); + + for (auto i = 0; i < container.size(); i++) + container[i] = i; + + model.move(1, 0); + QCOMPARE(container.data(), std::vector({1, 0, 2, 3, 4, 5})); + + model.move(5, 0); + QCOMPARE(container.data(), std::vector({5, 1, 0, 2, 3, 4})); + + model.move(0, 5); + QCOMPARE(container.data(), std::vector({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 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({3, 4, 5, 0, 1, 2})); + + model.move(3, 0, 3); + QCOMPARE(container.data(), std::vector({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 container; + container.setModel(model); + + for (auto i = 0; i < container.size(); i++) + container[i] = i; + + model.remove(1, 2); + QCOMPARE(container.data(), std::vector({0, 3, 4, 5})); + } + + void layoutChangeTest() + { + TestModel model({ + { "name", { "A", "B", "C", "D", "E", "F" }} + }); + + ModelSyncedContainer container; + container.setModel(&model); + + for (auto i = 0; i < container.size(); i++) + container[i] = i; + + model.invert(); + QCOMPARE(container.data(), std::vector({5, 4, 3, 2, 1, 0})); + } + + void layoutChangeWithRomovalTest() + { + TestModel model({ + { "name", { "A", "B", "C", "D", "E", "F" }} + }); + + ModelSyncedContainer container; + container.setModel(&model); + + for (auto i = 0; i < container.size(); i++) + container[i] = i; + + model.removeEverySecond(); + QCOMPARE(container.data(), std::vector({1, 3, 5})); + } + + void modelResetTest() + { + TestModel model({ + { "name", { "A", "B", "C", "D", "E", "F" }} + }); + + ModelSyncedContainer container; + container.setModel(&model); + + for (auto i = 0; i < container.size(); i++) + container[i] = i; + + model.reset(); + QCOMPARE(container.data(), std::vector({0, 0, 0, 0, 0, 0})); + } + + void modelDestroyedTest() + { + ModelSyncedContainer 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> container; + container.setModel(&model); + } +}; + +QTEST_MAIN(TestModelSyncedContainer) +#include "tst_ModelSyncedContainer.moc"