diff --git a/storybook/pages/GroupingModelPage.qml b/storybook/pages/GroupingModelPage.qml
new file mode 100644
index 0000000000..bcb9ead0ca
--- /dev/null
+++ b/storybook/pages/GroupingModelPage.qml
@@ -0,0 +1,143 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+import StatusQ 0.1
+
+import SortFilterProxyModel 0.2
+
+Pane {
+ id: root
+
+ ListModel {
+ id: collectiblesModel
+
+ Component.onCompleted: {
+ const randomInt = max => Math.floor(Math.random() * max)
+ const data = []
+
+ for (let i = 0; i < 1000; i++) {
+ const collectionName = "collection_" + i
+ const tokensCount = randomInt(10) + 1
+
+ for (let j = 0; j < tokensCount; j++) {
+ data.push({
+ collectionName,
+ tokenName: "token_" + j,
+ tokenValue: randomInt(200)
+ })
+ }
+ }
+
+ append(data)
+ }
+ }
+
+ SortFilterProxyModel {
+ id: sfpm
+
+ sourceModel: collectiblesModel
+
+ filters: RangeFilter {
+ roleName: "tokenValue"
+ minimumValue: slider.value
+ }
+ }
+
+ GroupingModel {
+ id: groupingModel
+
+ sourceModel: sfpm
+ groupingRoleName: "collectionName"
+ submodelRoleName: "collectibles"
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+
+ Label {
+ Layout.fillWidth: true
+ Layout.bottomMargin: 10
+
+ wrapMode: Text.Wrap
+ text: "Description: flat model with roles 'collectionName',"
+ + " 'tokenName' and 'tokenValue' is filtered by token value"
+ + " and grouped by collection name"
+ }
+
+ Label {
+ text: "source model count: " + collectiblesModel.count
+ }
+
+ Label {
+ text: "filtered model count: " + sfpm.count
+ }
+
+ Label {
+ text: "grouped model count: " + listView.count
+ }
+
+ RowLayout {
+ Layout.fillHeight: false
+
+ Label {
+ text: "Token value threshold"
+ }
+
+ Slider {
+ id: slider
+
+ stepSize: 1
+ value: 100
+ from: 0
+ to: 200
+ }
+
+ Label {
+ text: slider.value
+ }
+ }
+
+ ListView {
+ id: listView
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ model: groupingModel
+ spacing: 5
+ clip: true
+
+ ScrollBar.vertical: ScrollBar {}
+
+ delegate: RowLayout {
+ id: delegateRoot
+
+ width: ListView.view.width
+
+ readonly property var collectibles: model.collectibles
+
+ Label {
+ text: model.collectionName
+ }
+
+ ListView {
+ clip: true
+ orientation: ListView.Horizontal
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ model: delegateRoot.collectibles
+
+ delegate: Label {
+ text: `${model.tokenName} (val: ${model.tokenValue})`
+ color: "darkred"
+ }
+ }
+ }
+ }
+ }
+}
+
+// category: Models
diff --git a/ui/StatusQ/CMakeLists.txt b/ui/StatusQ/CMakeLists.txt
index 7b94d7c93c..afd3d50599 100644
--- a/ui/StatusQ/CMakeLists.txt
+++ b/ui/StatusQ/CMakeLists.txt
@@ -100,6 +100,7 @@ add_library(StatusQ SHARED
include/StatusQ/fastexpressionsorter.h
include/StatusQ/formatteddoubleproperty.h
include/StatusQ/functionaggregator.h
+ include/StatusQ/groupingmodel.h
include/StatusQ/leftjoinmodel.h
include/StatusQ/modelentry.h
include/StatusQ/modelsyncedcontainer.h
@@ -109,9 +110,9 @@ add_library(StatusQ SHARED
include/StatusQ/permissionutilsinternal.h
include/StatusQ/rolesrenamingmodel.h
include/StatusQ/rxvalidator.h
+ include/StatusQ/singleroleaggregator.h
include/StatusQ/snapshotmodel.h
include/StatusQ/snapshotobject.h
- include/StatusQ/singleroleaggregator.h
include/StatusQ/statussyntaxhighlighter.h
include/StatusQ/statuswindow.h
include/StatusQ/stringutilsinternal.h
@@ -127,6 +128,7 @@ add_library(StatusQ SHARED
src/fastexpressionsorter.cpp
src/formatteddoubleproperty.cpp
src/functionaggregator.cpp
+ src/groupingmodel.cpp
src/leftjoinmodel.cpp
src/modelentry.cpp
src/modelutilsinternal.cpp
diff --git a/ui/StatusQ/include/StatusQ/groupingmodel.h b/ui/StatusQ/include/StatusQ/groupingmodel.h
new file mode 100644
index 0000000000..db1f53791b
--- /dev/null
+++ b/ui/StatusQ/include/StatusQ/groupingmodel.h
@@ -0,0 +1,81 @@
+#pragma once
+
+#include
+
+#include
+#include
+
+class RangeModel;
+
+class GroupingModel : public QAbstractProxyModel
+{
+ Q_OBJECT
+
+ Q_PROPERTY(QString groupingRoleName READ groupingRoleName
+ WRITE setGroupingRoleName NOTIFY groupingRoleNameChanged)
+ Q_PROPERTY(QString submodelRoleName READ submodelRoleName
+ WRITE setSubmodelRoleName NOTIFY submodelRoleNameChanged)
+
+public:
+ explicit GroupingModel(QObject* parent = nullptr);
+ ~GroupingModel();
+
+ void setSourceModel(QAbstractItemModel* sourceModel) override;
+
+ QModelIndex mapToSource(const QModelIndex& proxyIndex) const override;
+ QModelIndex mapFromSource(const QModelIndex& sourceIndex) const override;
+
+ void setGroupingRoleName(const QString& groupingRoleName);
+ const QString& groupingRoleName() const;
+
+ void setSubmodelRoleName(const QString& submodelRoleName);
+ const QString& submodelRoleName() const;
+
+ QVariant data(const QModelIndex &index, int role) const override;
+ QHash roleNames() const override;
+
+ int columnCount(const QModelIndex& parent = QModelIndex()) const override;
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+
+ QModelIndex index(int row, int column = 0, const QModelIndex& parent = QModelIndex()) const override;
+ QModelIndex parent(const QModelIndex& child) const override;
+
+signals:
+ void groupingRoleNameChanged();
+ void submodelRoleNameChanged();
+
+protected slots:
+ void resetInternalData();
+
+private:
+ static constexpr auto s_defaultSubmodelRoleName = "submodel";
+
+ struct Entry {
+ int submodel = 0; // index of submodel
+ int submodelIndex = 0; // index within submodel
+ int sourceIndex = 0; // index within source model
+ };
+
+ void init();
+ void initRoles();
+ void initSubmodelRole();
+
+ void connectSignals(QAbstractItemModel* model);
+
+ std::vector m_entries;
+ std::vector> m_submodels;
+
+ RangeModel* m_pendingMergeSubmodel = nullptr;
+ RangeModel* m_pendingRemovalSubmodel = nullptr;
+
+ QHash m_roleNames;
+ QString m_groupingRoleName;
+ QString m_submodelRoleName = s_defaultSubmodelRoleName;
+
+ std::optional m_groupingRole;
+
+ int m_submodelRole = -1;
+ bool m_rolesInitialized = false;
+
+ friend class RangeModel;
+};
diff --git a/ui/StatusQ/src/groupingmodel.cpp b/ui/StatusQ/src/groupingmodel.cpp
new file mode 100644
index 0000000000..7ac0c9e571
--- /dev/null
+++ b/ui/StatusQ/src/groupingmodel.cpp
@@ -0,0 +1,684 @@
+#include "StatusQ/groupingmodel.h"
+
+#include
+
+#include
+
+namespace {
+
+QVariant data(QAbstractItemModel* model, int row, int role) {
+ return model->data(model->index(row, 0), role);
+}
+
+} // unnamed namespace
+
+
+class RangeModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ explicit RangeModel(QAbstractItemModel* sourceModel,
+ const std::vector& entries, int from, int to)
+ : m_entries(entries), m_sourceModel(sourceModel), m_from(from), m_to(to)
+ {
+ }
+
+ using QAbstractListModel::beginInsertRows;
+ using QAbstractListModel::endInsertRows;
+ using QAbstractListModel::beginRemoveRows;
+ using QAbstractListModel::endRemoveRows;
+
+ QVariant data(const QModelIndex& index, int role) const override
+ {
+ if (!index.isValid())
+ return {};
+
+ auto sourceIndex = m_entries[index.row() + m_from].sourceIndex;
+
+ return m_sourceModel->data(m_sourceModel->index(sourceIndex,
+ index.column()), role);
+ }
+
+ QHash roleNames() const override
+ {
+ return m_sourceModel->roleNames();
+ }
+
+ int rowCount(const QModelIndex& parent = {}) const override
+ {
+ return m_to - m_from + 1;
+ }
+
+ int& from()
+ {
+ return m_from;
+ }
+
+ int& to() {
+ return m_to;
+ }
+
+ void shift(int offset)
+ {
+ m_from += offset;
+ m_to += offset;
+ }
+
+private:
+ QAbstractItemModel* m_sourceModel = nullptr;
+ const std::vector& m_entries;
+ int m_from = 0;
+ int m_to = 0;
+};
+
+GroupingModel::GroupingModel(QObject* parent)
+ : QAbstractProxyModel{parent}
+{
+}
+
+GroupingModel::~GroupingModel() = default;
+
+void GroupingModel::setSourceModel(QAbstractItemModel* model)
+{
+ if (sourceModel() == model)
+ return;
+
+ if (sourceModel() != nullptr)
+ sourceModel()->disconnect(this);
+
+ beginResetModel();
+
+ QAbstractProxyModel::setSourceModel(model);
+
+ if (model != nullptr)
+ connectSignals(model);
+
+ endResetModel();
+}
+
+QModelIndex GroupingModel::mapToSource(const QModelIndex& proxyIndex) const
+{
+ if (!sourceModel())
+ return {};
+
+ if (!proxyIndex.isValid())
+ return {};
+
+ if (proxyIndex.model() != sourceModel())
+ return {};
+
+ return index(m_submodels[proxyIndex.row()]->from());
+}
+
+QModelIndex GroupingModel::mapFromSource(const QModelIndex& sourceIndex) const
+{
+ if (!sourceIndex.isValid())
+ return {};
+
+ auto& entry = m_entries[sourceIndex.row()];
+ auto submodel = entry.submodel;
+ auto s = m_submodels[submodel].get();
+
+ return s->index(entry.submodelIndex, 0);
+}
+
+void GroupingModel::setGroupingRoleName(const QString& groupingRoleName)
+{
+ if (m_groupingRoleName == groupingRoleName)
+ return;
+
+ m_groupingRoleName = groupingRoleName;
+
+ initSubmodelRole();
+
+ if (m_groupingRole)
+ init();
+
+ emit groupingRoleNameChanged();
+}
+
+const QString &GroupingModel::groupingRoleName() const
+{
+ return m_groupingRoleName;
+}
+
+void GroupingModel::setSubmodelRoleName(const QString& submodelRoleName)
+{
+ if (m_submodelRoleName == submodelRoleName)
+ return;
+
+ if (!m_roleNames.isEmpty())
+ beginResetModel();
+
+ m_submodelRoleName = submodelRoleName;
+
+ if (!m_roleNames.isEmpty())
+ endResetModel();
+
+ emit submodelRoleNameChanged();
+}
+
+const QString& GroupingModel::submodelRoleName() const
+{
+ return m_submodelRoleName;
+}
+
+QVariant GroupingModel::data(const QModelIndex& index, int role) const
+{
+ if (!index.isValid() || index.row() >= m_submodels.size())
+ return {};
+
+ if (role == m_submodelRole)
+ return QVariant::fromValue(m_submodels[index.row()].get());
+
+ auto row = index.row();
+
+ auto destRow = m_submodels[row]->from();
+ auto srcRow = m_entries.at(destRow).sourceIndex;
+
+ return sourceModel()->data(sourceModel()->index(srcRow, index.column()), role);
+}
+
+QHash GroupingModel::roleNames() const
+{
+ return m_roleNames;
+}
+
+int GroupingModel::columnCount(const QModelIndex& parent) const
+{
+ if (parent.isValid())
+ return 0;
+
+ return 1;
+}
+
+int GroupingModel::rowCount(const QModelIndex &parent) const
+{
+ return m_submodels.size();
+}
+
+QModelIndex GroupingModel::index(int row, int column, const QModelIndex& parent) const
+{
+ if (parent.isValid())
+ return {};
+
+ if (row < 0 || column < 0 || row >= rowCount(parent)
+ || column >= columnCount(parent))
+ return {};
+
+ return createIndex(row, column);
+}
+
+QModelIndex GroupingModel::parent(const QModelIndex& child) const
+{
+ return {};
+}
+
+void GroupingModel::resetInternalData()
+{
+ QAbstractProxyModel::resetInternalData();
+
+ auto source = sourceModel();
+
+ m_rolesInitialized = false;
+ m_roleNames.clear();
+
+ m_entries.clear();
+ m_submodels.clear();
+
+ if (source == nullptr)
+ return;
+
+ if (source->rowCount() > 0) {
+ initRoles();
+ initSubmodelRole();
+
+ if (m_groupingRole)
+ init();
+ }
+}
+
+void GroupingModel::init()
+{
+ auto count = sourceModel()->rowCount();
+
+ Q_ASSERT(m_groupingRole.has_value());
+
+ QVariant previousVal;
+
+ // from/to pair for every group
+ std::vector> pairs;
+
+ m_entries.reserve(count);
+ int submodel = -1;
+
+ for (int i = 0; i < count; i++) {
+ auto val = sourceModel()->data(sourceModel()->index(i, 0), *m_groupingRole);
+
+ if (val != previousVal || pairs.empty()) {
+ submodel++;
+ pairs.emplace_back(i, i);
+ } else {
+ pairs.back().second++;
+ }
+
+ m_entries.push_back({submodel, pairs.back().second - pairs.back().first, i});
+
+ previousVal = val;
+ }
+
+ m_submodels.reserve(pairs.size());
+ std::transform(pairs.cbegin(), pairs.cend(), std::back_inserter(m_submodels),
+ [this] (auto& entry) {
+ return std::make_unique(sourceModel(), m_entries, entry.first, entry.second);
+ });
+}
+
+void GroupingModel::initRoles()
+{
+ auto roleNames = sourceModel()->roleNames();
+ auto roles = roleNames.keys();
+ auto maxIt = std::max_element(roles.cbegin(), roles.cend());
+
+ m_submodelRole = maxIt == roles.cend() ? 0 : *maxIt + 1;
+ roleNames.insert(m_submodelRole, m_submodelRoleName.toUtf8());
+
+ m_roleNames = std::move(roleNames);
+ m_rolesInitialized = true;
+}
+
+void GroupingModel::initSubmodelRole()
+{
+ auto groupingRole = m_roleNames.keys(m_groupingRoleName.toUtf8());
+
+ if (groupingRole.size())
+ m_groupingRole = groupingRole.first();
+ else
+ m_groupingRole.reset();
+}
+
+void GroupingModel::connectSignals(QAbstractItemModel* model)
+{
+ connect(model, &QAbstractItemModel::rowsInserted, this,
+ [this, model](const QModelIndex &parent, int first, int last) {
+
+ if (!m_rolesInitialized) {
+ initRoles();
+ initSubmodelRole(); // check order
+ }
+
+ auto insertCount = last - first + 1;
+
+ std::optional previousGroupingValue;
+
+ if (first - 1 >= 0)
+ previousGroupingValue = ::data(model, first - 1, *m_groupingRole);
+
+ std::optional nextGroupingValue;
+
+ if (last + 1 < m_entries.size() + insertCount)
+ nextGroupingValue = ::data(model, last + 1, *m_groupingRole);
+
+ int currentFirst = first;
+ int currentLast = last;
+
+ int appendToPrevious = 0;
+ int appendToNext = 0;
+
+ // count data belonging to the previous group
+ while (previousGroupingValue
+ && currentFirst <= currentLast
+ && ::data(model, currentFirst, *m_groupingRole) == *previousGroupingValue) {
+ currentFirst++;
+ appendToPrevious++;
+ }
+
+ // count data belonging to the following group
+ while (nextGroupingValue
+ && currentLast >= currentFirst
+ && ::data(model, currentLast, *m_groupingRole) == *nextGroupingValue) {
+ currentLast--;
+ appendToNext++;
+ }
+
+ int toNewGroups = insertCount - appendToPrevious - appendToNext;
+ int toRemove = 0;
+
+ // shift indexes to indicate old items for rowsAboutToBe* signals
+ for (auto i = first; i < m_entries.size(); i++)
+ m_entries[i].sourceIndex += last - first + 1;
+
+ if (toNewGroups > 0 && previousGroupingValue && nextGroupingValue
+ && previousGroupingValue == nextGroupingValue) {
+
+ int submodelIndex = m_entries[first - 1].submodel;
+ RangeModel* submodel = m_submodels[submodelIndex].get();
+
+ toRemove = submodel->rowCount() - m_entries[first - 1].submodelIndex - 1;
+
+ submodel->beginRemoveRows({}, m_entries[first - 1].submodelIndex + 1,
+ submodel->rowCount() - 1);
+ submodel->to() -= toRemove;
+ submodel->endRemoveRows();
+
+ toNewGroups += toRemove + appendToNext;
+ appendToNext = 0;
+ }
+
+ RangeModel* previousModel = nullptr;
+ RangeModel* nextModel = nullptr;
+
+ if (appendToPrevious) {
+ const Entry& entry = m_entries[first - 1];
+ int submodel = entry.submodel;
+ int offset = entry.submodelIndex + 1;
+
+ previousModel = m_submodels[submodel].get();
+ previousModel->beginInsertRows({}, offset, appendToPrevious + offset - 1);
+ }
+
+ // prepare new entries
+
+ QVariant previousVal;
+ std::vector> pairs;
+
+ int baseline = first + appendToPrevious;
+ int submodel = -1;
+
+ for (int i = baseline; i < baseline + toNewGroups; i++) {
+
+ Q_ASSERT(m_groupingRole);
+
+ auto val = sourceModel()->data(sourceModel()->index(i, 0), *m_groupingRole);
+
+ if (val != previousVal || pairs.empty()) {
+ submodel++;
+ pairs.emplace_back(i, i);
+ } else {
+ pairs.back().second++;
+ }
+
+ previousVal = val;
+ }
+
+ std::vector> newSubmodels;
+
+ std::transform(pairs.cbegin(), pairs.cend(), std::back_inserter(newSubmodels),
+ [this] (auto& entry) {
+ return std::make_unique(sourceModel(), m_entries, entry.first, entry.second);
+ });
+
+ if (newSubmodels.size()) {
+ int offset = first == 0 ? 0 : m_entries[first - 1].submodel + 1;
+ beginInsertRows({}, offset, offset + newSubmodels.size() - 1);
+ }
+
+ if (appendToNext) {
+ Q_ASSERT(first < m_entries.size());
+
+ int submodel = m_entries[first].submodel;
+
+ Q_ASSERT(submodel < m_submodels.size());
+
+ nextModel = m_submodels[submodel].get();
+ nextModel->beginInsertRows({}, 0, appendToNext - 1);
+ }
+
+ // ADJUST STRUCTURES
+
+ if (appendToPrevious) {
+ previousModel->to() += appendToPrevious;
+
+ const Entry& entry = m_entries[first - 1];
+ int submodel = entry.submodel;
+
+ for (int i = submodel + 1; i < m_submodels.size(); i++)
+ m_submodels[i]->shift(appendToPrevious);
+ }
+
+ if (newSubmodels.size()) {
+ int offset = first == 0 ? 0 : m_entries[first - 1].submodel + 1;
+
+ m_submodels.insert(m_submodels.begin() + offset,
+ std::make_move_iterator(newSubmodels.begin()),
+ std::make_move_iterator(newSubmodels.end()));
+
+ for (int i = offset + newSubmodels.size(); i < m_submodels.size(); i++)
+ m_submodels[i]->shift(toNewGroups - toRemove);
+ }
+
+ if (appendToNext) {
+ nextModel->to() += appendToNext;
+
+ int submodel = m_entries[first].submodel + newSubmodels.size();
+
+ for (int i = submodel + 1; i < m_submodels.size(); i++)
+ m_submodels[i]->shift(appendToNext);
+ }
+
+ m_entries.resize(m_entries.size() + insertCount);
+ int totalCounter = 0;
+
+ for (std::size_t i = 0; i < m_submodels.size(); i++) {
+ RangeModel* model = m_submodels[i].get();
+ int count = model->rowCount();
+
+ for (std::size_t j = 0; j < count; j++) {
+ Entry& entry = m_entries[totalCounter];
+ entry.submodel = i;
+ entry.submodelIndex = j;
+ entry.sourceIndex = totalCounter;
+
+ totalCounter++;
+ }
+ }
+
+ // EMIT SIGNALS
+ if (previousModel)
+ previousModel->endInsertRows();
+
+ if (newSubmodels.size())
+ endInsertRows();
+
+ if (nextModel) {
+ nextModel->endInsertRows();
+
+ auto dataChangedIdx = index(m_entries[last].submodel, 0);
+ auto roles = m_roleNames.keys();
+ roles.removeOne(m_submodelRole);
+ roles.removeOne(*m_groupingRole);
+
+ emit dataChanged(dataChangedIdx, dataChangedIdx, roles.toVector());
+ }
+ });
+
+ connect(model, &QAbstractItemModel::rowsAboutToBeRemoved, this,
+ [this, model](const QModelIndex &parent, int first, int last) {
+ int firstSubmodelToBeRemoved = -1;
+ int lastSubmodelToBeRemoved = -1;
+
+ bool mergeRequired = first > 0
+ && last < m_entries.size() - 1
+ && m_entries[first - 1].submodel != m_entries[last + 1].submodel
+ && ::data(model, first - 1, *m_groupingRole)
+ == ::data(model, last + 1, *m_groupingRole);
+
+ std::vector> removals;
+
+ for (int i = first; i <= last;) {
+ Entry& entry = m_entries[i];
+
+ auto submodel = entry.submodel;
+ auto submodelIndex = entry.submodelIndex;
+
+ RangeModel* s = m_submodels[submodel].get();
+ auto submodelCount = s->rowCount();
+
+ int remaining = last - i + 1;
+
+ if (submodelIndex == 0 && remaining >= submodelCount) {
+ if (firstSubmodelToBeRemoved == -1) {
+ firstSubmodelToBeRemoved = submodel;
+ lastSubmodelToBeRemoved = submodel;
+ } else {
+ lastSubmodelToBeRemoved++;
+ }
+ } else {
+ int removeFrom = submodelIndex;
+ int removeTo = std::min(removeFrom + remaining, submodelCount) - 1;
+ int countToRemove = removeTo - removeFrom + 1;
+
+ if (removeFrom == 0) {
+ if (!mergeRequired) {
+ s->beginRemoveRows({}, removeFrom, removeTo);
+ s->from() = s->from() + countToRemove;
+ s->endRemoveRows();
+ }
+ } else {
+ s->beginRemoveRows({}, removeFrom, removeTo);
+ s->to() = s->to() - countToRemove;
+
+ Q_ASSERT(m_pendingRemovalSubmodel == nullptr);
+
+ // removing tail of the submodel
+ if (removeTo == submodelCount - 1)
+ s->endRemoveRows();
+ // removing from the middle, must be deferred to keep correct
+ // intermediate state
+ else
+ m_pendingRemovalSubmodel = s;
+ }
+ }
+
+ i += submodelCount - submodelIndex;
+ }
+
+ auto mergeCount = 0;
+
+ if (mergeRequired) {
+ lastSubmodelToBeRemoved++;
+
+ auto submodel = m_entries[first - 1].submodel;
+ auto submodelToBeMerged = m_entries[last + 1].submodel;
+
+ mergeCount = m_submodels[submodelToBeMerged]->rowCount()
+ - m_entries[last + 1].submodelIndex;
+ }
+
+ if (firstSubmodelToBeRemoved != -1) {
+ beginRemoveRows({}, firstSubmodelToBeRemoved, lastSubmodelToBeRemoved);
+
+ m_submodels.erase(m_submodels.begin() + firstSubmodelToBeRemoved,
+ m_submodels.begin() + lastSubmodelToBeRemoved + 1);
+
+ endRemoveRows();
+ }
+
+ if (mergeRequired) {
+ auto submodel = m_entries[first - 1].submodel;
+
+ RangeModel* s = m_submodels[submodel].get();
+ s->beginInsertRows({}, s->rowCount(), s->rowCount() + mergeCount - 1);
+ s->to() = s->to() + mergeCount;
+
+ Q_ASSERT(m_pendingMergeSubmodel == nullptr);
+ m_pendingMergeSubmodel = s;
+ }
+ });
+
+ connect(model, &QAbstractItemModel::rowsRemoved, this,
+ [this, model](const QModelIndex &parent, int first, int last) {
+ int newSize = m_entries.size() - (last - first + 1);
+ m_entries.clear();
+ m_entries.reserve(newSize);
+
+ int sourceIndex = 0;
+
+ for (int i = 0; i < m_submodels.size(); i++) {
+ auto s = m_submodels[i].get();
+ auto count = s->rowCount();
+
+ s->from() = sourceIndex;
+ s->to() = sourceIndex + count - 1;
+
+ for (int j = 0; j < count; j++)
+ m_entries.push_back({i, j, sourceIndex++});
+ }
+
+ if (m_pendingMergeSubmodel) {
+ m_pendingMergeSubmodel->endInsertRows();
+ m_pendingMergeSubmodel = nullptr;
+ }
+
+ if (m_pendingRemovalSubmodel) {
+ m_pendingRemovalSubmodel->endRemoveRows();
+ m_pendingRemovalSubmodel = nullptr;
+ }
+
+ Q_ASSERT(m_entries.size() == newSize);
+ });
+
+ connect(model, &QAbstractItemModel::dataChanged, this, [this, model] (
+ const QModelIndex& topLeft, const QModelIndex& bottomRight,
+ const QVector& roles) {
+
+ if (!topLeft.isValid() || !bottomRight.isValid())
+ return;
+
+ auto sourceFirst = topLeft.row();
+ auto sourceLast = bottomRight.row();
+
+ auto destFirst = m_entries.at(sourceFirst).submodel;
+ auto destLast = m_entries.at(sourceLast).submodel;
+
+ // internal models
+ int changeSize = sourceLast - sourceFirst + 1;
+ int offset = m_entries.at(sourceFirst).submodelIndex;
+
+ for (auto i = destFirst; i <= destLast; i++) {
+ auto submodel = m_submodels[i].get();
+ auto sumodelChangeSize = std::min(changeSize,
+ submodel->rowCount() - offset);
+
+ emit submodel->dataChanged(submodel->index(offset),
+ submodel->index(offset + sumodelChangeSize - 1),
+ roles);
+
+ changeSize -= sumodelChangeSize;
+ offset = 0;
+ }
+
+ // external model
+ if (m_entries.at(sourceFirst).submodelIndex > 0)
+ destFirst++;
+
+ if (destLast < destFirst)
+ return;
+
+ const QVector& rolesAligned = roles.isEmpty()
+ ? model->roleNames().keys().toVector()
+ : roles;
+
+ emit this->dataChanged(this->index(destFirst), this->index(destLast),
+ rolesAligned);
+ });
+
+ connect(model, &QAbstractItemModel::modelAboutToBeReset, this, [this] {
+ this->beginResetModel();
+ });
+
+ connect(model, &QAbstractItemModel::modelReset, this, [this] {
+ this->endResetModel();
+ });
+
+ connect(model, &QAbstractItemModel::layoutAboutToBeChanged, this, [this] {
+ this->beginResetModel();
+ });
+
+ connect(model, &QAbstractItemModel::layoutChanged, this, [this] {
+ this->endResetModel();
+ });
+}
+
+#include "groupingmodel.moc"
diff --git a/ui/StatusQ/src/plugin.cpp b/ui/StatusQ/src/plugin.cpp
index 3b032acb02..cf7b7c08af 100644
--- a/ui/StatusQ/src/plugin.cpp
+++ b/ui/StatusQ/src/plugin.cpp
@@ -10,14 +10,15 @@
#include "StatusQ/fastexpressionsorter.h"
#include "StatusQ/formatteddoubleproperty.h"
#include "StatusQ/functionaggregator.h"
+#include "StatusQ/groupingmodel.h"
#include "StatusQ/leftjoinmodel.h"
+#include "StatusQ/modelentry.h"
#include "StatusQ/modelutilsinternal.h"
#include "StatusQ/movablemodel.h"
#include "StatusQ/objectproxymodel.h"
#include "StatusQ/permissionutilsinternal.h"
#include "StatusQ/rolesrenamingmodel.h"
#include "StatusQ/rxvalidator.h"
-#include "StatusQ/modelentry.h"
#include "StatusQ/snapshotobject.h"
#include "StatusQ/statussyntaxhighlighter.h"
#include "StatusQ/statuswindow.h"
@@ -46,6 +47,7 @@ public:
qmlRegisterType("StatusQ.Models", 0, 1, "ManageTokensController");
qmlRegisterType("StatusQ.Models", 0, 1, "ManageTokensModel");
+ qmlRegisterType("StatusQ", 0, 1, "GroupingModel");
qmlRegisterType("StatusQ", 0, 1, "SourceModel");
qmlRegisterType("StatusQ", 0, 1, "ConcatModel");
qmlRegisterType("StatusQ", 0, 1, "MovableModel");
diff --git a/ui/StatusQ/tests/CMakeLists.txt b/ui/StatusQ/tests/CMakeLists.txt
index e244fb5da2..9c246597d7 100644
--- a/ui/StatusQ/tests/CMakeLists.txt
+++ b/ui/StatusQ/tests/CMakeLists.txt
@@ -110,4 +110,8 @@ add_test(NAME ModelEntryTest COMMAND ModelEntryTest)
add_executable(SnapshotObjectTest tst_SnapshotObject.cpp)
target_link_libraries(SnapshotObjectTest PRIVATE StatusQ StatusQTestLib)
-add_test(NAME SnapshotObjectTest COMMAND SnapshotObjectTest)
\ No newline at end of file
+add_test(NAME SnapshotObjectTest COMMAND SnapshotObjectTest)
+
+add_executable(GroupingModelTest tst_GroupingModel.cpp)
+target_link_libraries(GroupingModelTest PRIVATE Qt5::Qml StatusQ StatusQTestLib)
+add_test(NAME GroupingModelTest COMMAND GroupingModelTest)
diff --git a/ui/StatusQ/tests/src/TestHelpers/listmodelwrapper.cpp b/ui/StatusQ/tests/src/TestHelpers/listmodelwrapper.cpp
index f24001e539..24c6dd26fa 100644
--- a/ui/StatusQ/tests/src/TestHelpers/listmodelwrapper.cpp
+++ b/ui/StatusQ/tests/src/TestHelpers/listmodelwrapper.cpp
@@ -29,6 +29,8 @@ ListModelWrapper::ListModelWrapper(QQmlEngine& engine, const QString& content)
m_model.reset(qobject_cast(
component.create(engine.rootContext())));
+
+ Q_ASSERT_X(m_model, "ListModelWrapper", "creating model failed!");
}
ListModelWrapper::ListModelWrapper(QQmlEngine& engine, const QJsonArray& content)
diff --git a/ui/StatusQ/tests/tst_GroupingModel.cpp b/ui/StatusQ/tests/tst_GroupingModel.cpp
new file mode 100644
index 0000000000..d7bb5552b4
--- /dev/null
+++ b/ui/StatusQ/tests/tst_GroupingModel.cpp
@@ -0,0 +1,2514 @@
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+
+
+#include
+#include
+#include
+
+
+class TestGroupingModel: public QObject
+{
+ Q_OBJECT
+
+ int roleForName(const QHash& roles, const QByteArray& name) const
+ {
+ auto keys = roles.keys(name);
+
+ if (keys.empty())
+ return -1;
+
+ return keys.first();
+ }
+
+ static constexpr QModelIndex InvalidIdx{};
+
+private slots:
+ void initializationTest()
+ {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine, R"([
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" },
+ { "name": "B", "subname": "b1" },
+ { "name": "C", "subname": "c1" },
+ { "name": "C", "subname": "c2" },
+ { "name": "C", "subname": "c3" },
+ { "name": "C", "subname": "c4" },
+ { "name": "D", "subname": "d1" },
+ { "name": "D", "subname": "d2" },
+ { "name": "D", "subname": "d3" },
+ { "name": "E", "subname": "e1" },
+ { "name": "E", "subname": "e2" }
+ ])");
+
+ ListModelWrapper expected(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "submodel": [
+ { "name": "C", "subname": "c1" },
+ { "name": "C", "subname": "c2" },
+ { "name": "C", "subname": "c3" },
+ { "name": "C", "subname": "c4" }
+ ]
+ },
+ {
+ "name": "D", "subname": "d1",
+ "submodel": [
+ { "name": "D", "subname": "d1" },
+ { "name": "D", "subname": "d2" },
+ { "name": "D", "subname": "d3" }
+ ]
+ },
+ {
+ "name": "E", "subname": "e1",
+ "submodel": [
+ { "name": "E", "subname": "e1" },
+ { "name": "E", "subname": "e2" }
+ ]
+ }
+ ])");
+
+ {
+ GroupingModel model;
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel);
+
+ QVERIFY(isSame(model, *expected.model()));
+ }
+
+ {
+ GroupingModel model;
+ model.setSourceModel(sourceModel);
+ model.setGroupingRoleName("name");
+
+ QVERIFY(isSame(model, *expected.model()));
+ }
+
+ {
+ GroupingModel model;
+ model.setSourceModel(sourceModel);
+ model.setGroupingRoleName("name_");
+
+ QCOMPARE(model.rowCount(), 0);
+ model.setGroupingRoleName("name");
+
+ QVERIFY(isSame(model, *expected.model()));
+ }
+ }
+
+ void sourceDataChangeTest()
+ {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine, R"([
+ { "name": "A", "subname": "a1" }, // 0 0
+ { "name": "A", "subname": "a2" }, // 1
+ { "name": "B", "subname": "b1" }, // 1 2
+ { "name": "C", "subname": "c1" }, // 2 3
+ { "name": "C", "subname": "c2" }, // 4
+ { "name": "C", "subname": "c3" }, // 5
+ { "name": "C", "subname": "c4" }, // 6
+ { "name": "D", "subname": "d1" }, // 3 7
+ { "name": "D", "subname": "d2" }, // 8
+ { "name": "D", "subname": "d3" }, // 9
+ { "name": "E", "subname": "e1" }, // 4 10
+ { "name": "E", "subname": "e2" } // 11
+ ])");
+
+ ListModelWrapper expected(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "submodel": [
+ { "name": "C", "subname": "c1" },
+ { "name": "C", "subname": "c2" },
+ { "name": "C", "subname": "c3" },
+ { "name": "C", "subname": "c4" }
+ ]
+ },
+ {
+ "name": "D", "subname": "d1",
+ "submodel": [
+ { "name": "D", "subname": "d1" },
+ { "name": "D", "subname": "d2" },
+ { "name": "D", "subname": "d3" }
+ ]
+ },
+ {
+ "name": "E", "subname": "e1",
+ "submodel": [
+ { "name": "E", "subname": "e1" },
+ { "name": "E", "subname": "e2" }
+ ]
+ }
+ ])");
+
+ GroupingModel model;
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel);
+
+ auto roles = model.roleNames();
+ auto nameRole = roleForName(roles, "name");
+ auto subnameRole = roleForName(roles, "subname");
+ auto submodelRole = roleForName(roles, "submodel");
+
+ auto submodel1 = model.data(model.index(0, 0), submodelRole).value();
+ auto submodel2 = model.data(model.index(1, 0), submodelRole).value();
+ auto submodel3 = model.data(model.index(2, 0), submodelRole).value();
+ auto submodel4 = model.data(model.index(3, 0), submodelRole).value();
+ auto submodel5 = model.data(model.index(4, 0), submodelRole).value();
+
+ {
+ ModelSignalsSpy signalsSpy(&model);
+ ModelSignalsSpy submodel1Spy(submodel1);
+ ModelSignalsSpy submodel2Spy(submodel2);
+ ModelSignalsSpy submodel3Spy(submodel3);
+ ModelSignalsSpy submodel4Spy(submodel4);
+ ModelSignalsSpy submodel5Spy(submodel5);
+
+ emit sourceModel.model()->dataChanged(sourceModel.model()->index(0, 0),
+ sourceModel.model()->index(0, 0));
+
+ QCOMPARE(signalsSpy.count(), 1);
+ QCOMPARE(signalsSpy.dataChangedSpy.count(), 1);
+ QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(0), model.index(0, 0));
+ QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(1), model.index(0, 0));
+
+ QSet expectedRoles{nameRole, subnameRole};
+ auto roles = signalsSpy.dataChangedSpy.at(0).at(2).value>();
+ QSet rolesSet(roles.cbegin(), roles.cend());
+
+ QCOMPARE(rolesSet, expectedRoles);
+
+ QCOMPARE(submodel1Spy.count(), 1);
+ QCOMPARE(submodel1Spy.dataChangedSpy.count(), 1);
+ QCOMPARE(submodel1Spy.dataChangedSpy.at(0).at(0), submodel1->index(0, 0));
+ QCOMPARE(submodel1Spy.dataChangedSpy.at(0).at(1), submodel1->index(0, 0));
+ QCOMPARE(submodel2Spy.count(), 0);
+ QCOMPARE(submodel3Spy.count(), 0);
+ QCOMPARE(submodel4Spy.count(), 0);
+ QCOMPARE(submodel5Spy.count(), 0);
+ }
+ {
+ ModelSignalsSpy signalsSpy(&model);
+ ModelSignalsSpy submodel1Spy(submodel1);
+ ModelSignalsSpy submodel2Spy(submodel2);
+ ModelSignalsSpy submodel3Spy(submodel3);
+ ModelSignalsSpy submodel4Spy(submodel4);
+ ModelSignalsSpy submodel5Spy(submodel5);
+
+ emit sourceModel.model()->dataChanged(sourceModel.model()->index(4, 0),
+ sourceModel.model()->index(7, 0),
+ { subnameRole });
+
+ QCOMPARE(signalsSpy.count(), 1);
+ QCOMPARE(signalsSpy.dataChangedSpy.count(), 1);
+ QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(0), model.index(3, 0));
+ QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(1), model.index(3, 0));
+
+ QVector expectedRoles{subnameRole};
+ auto roles = signalsSpy.dataChangedSpy.at(0).at(2).value>();
+
+ QCOMPARE(roles, expectedRoles);
+
+ QCOMPARE(submodel1Spy.count(), 0);
+ QCOMPARE(submodel2Spy.count(), 0);
+ QCOMPARE(submodel3Spy.count(), 1);
+ QCOMPARE(submodel3Spy.dataChangedSpy.count(), 1);
+ QCOMPARE(submodel3Spy.dataChangedSpy.at(0).at(0), submodel3->index(1, 0));
+ QCOMPARE(submodel3Spy.dataChangedSpy.at(0).at(1), submodel3->index(3, 0));
+
+ roles = submodel3Spy.dataChangedSpy.at(0).at(2).value>();
+ QCOMPARE(roles, expectedRoles);
+
+ QCOMPARE(submodel4Spy.count(), 1);
+ QCOMPARE(submodel4Spy.dataChangedSpy.count(), 1);
+ QCOMPARE(submodel4Spy.dataChangedSpy.at(0).at(0), submodel4->index(0, 0));
+ QCOMPARE(submodel4Spy.dataChangedSpy.at(0).at(1), submodel4->index(0, 0));
+
+ roles = submodel4Spy.dataChangedSpy.at(0).at(2).value>();
+ QCOMPARE(roles, expectedRoles);
+
+ QCOMPARE(submodel5Spy.count(), 0);
+ }
+ {
+ ModelSignalsSpy signalsSpy(&model);
+ ModelSignalsSpy submodel1Spy(submodel1);
+ ModelSignalsSpy submodel2Spy(submodel2);
+ ModelSignalsSpy submodel3Spy(submodel3);
+ ModelSignalsSpy submodel4Spy(submodel4);
+ ModelSignalsSpy submodel5Spy(submodel5);
+
+ emit sourceModel.model()->dataChanged(sourceModel.model()->index(4, 0),
+ sourceModel.model()->index(6, 0));
+
+ QCOMPARE(signalsSpy.count(), 0);
+ QCOMPARE(submodel1Spy.count(), 0);
+ QCOMPARE(submodel2Spy.count(), 0);
+ QCOMPARE(submodel3Spy.count(), 1);
+ QCOMPARE(submodel3Spy.dataChangedSpy.count(), 1);
+ QCOMPARE(submodel3Spy.dataChangedSpy.at(0).at(0), submodel3->index(1, 0));
+ QCOMPARE(submodel3Spy.dataChangedSpy.at(0).at(1), submodel3->index(3, 0));
+ QVERIFY(submodel3Spy.dataChangedSpy.at(0).at(2).value>().isEmpty());
+
+ QCOMPARE(submodel4Spy.count(), 0);
+ QCOMPARE(submodel5Spy.count(), 0);
+ }
+ {
+ ModelSignalsSpy signalsSpy(&model);
+ ModelSignalsSpy submodel1Spy(submodel1);
+ ModelSignalsSpy submodel2Spy(submodel2);
+ ModelSignalsSpy submodel3Spy(submodel3);
+ ModelSignalsSpy submodel4Spy(submodel4);
+ ModelSignalsSpy submodel5Spy(submodel5);
+
+ emit sourceModel.model()->dataChanged(sourceModel.model()->index(2, 0),
+ sourceModel.model()->index(7, 0),
+ { subnameRole });
+
+ QCOMPARE(signalsSpy.count(), 1);
+ QCOMPARE(signalsSpy.dataChangedSpy.count(), 1);
+ QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(0), model.index(1, 0));
+ QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(1), model.index(3, 0));
+
+ QVector expectedRoles{subnameRole};
+ auto roles = signalsSpy.dataChangedSpy.at(0).at(2).value>();
+
+ QCOMPARE(roles, expectedRoles);
+
+ QCOMPARE(submodel1Spy.count(), 0);
+ QCOMPARE(submodel2Spy.count(), 1);
+ QCOMPARE(submodel2Spy.dataChangedSpy.count(), 1);
+ QCOMPARE(submodel2Spy.dataChangedSpy.at(0).at(0), submodel2->index(0, 0));
+ QCOMPARE(submodel2Spy.dataChangedSpy.at(0).at(1), submodel2->index(0, 0));
+
+ QCOMPARE(submodel3Spy.count(), 1);
+ QCOMPARE(submodel3Spy.dataChangedSpy.count(), 1);
+ QCOMPARE(submodel3Spy.dataChangedSpy.at(0).at(0), submodel3->index(0, 0));
+ QCOMPARE(submodel3Spy.dataChangedSpy.at(0).at(1), submodel3->index(3, 0));
+
+ QCOMPARE(submodel4Spy.count(), 1);
+ QCOMPARE(submodel4Spy.dataChangedSpy.count(), 1);
+ QCOMPARE(submodel4Spy.dataChangedSpy.at(0).at(0), submodel4->index(0, 0));
+ QCOMPARE(submodel4Spy.dataChangedSpy.at(0).at(1), submodel4->index(0, 0));
+
+ QCOMPARE(submodel5Spy.count(), 0);
+ }
+
+ QVERIFY(isSame(model, *expected.model()));
+ }
+
+ void setSourceTest()
+ {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel1(engine);
+ ListModelWrapper sourceModel2(engine);
+
+ auto contains = [](auto roles, auto name) {
+ return std::find(roles.cbegin(), roles.cend(), name) != roles.cend();
+ };
+
+ GroupingModel model;
+ auto signalsSpy = std::make_unique(&model);
+
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel1.model()); // set empty source model
+
+ QCOMPARE(signalsSpy->count(), 2);
+ QCOMPARE(signalsSpy->modelAboutToBeResetSpy.count(), 1);
+ QCOMPARE(signalsSpy->modelResetSpy.count(), 1);
+
+ QCOMPARE(model.rowCount(), 0);
+
+ {
+ auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 0);
+ }
+
+ sourceModel1.append(QJsonArray { // append to empty source model
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a2" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }}
+ });
+
+ QCOMPARE(signalsSpy->count(), 4);
+ QCOMPARE(signalsSpy->rowsAboutToBeInsertedSpy.count(), 1);
+ QCOMPARE(signalsSpy->rowsInsertedSpy.count(), 1);
+ signalsSpy = std::make_unique(&model);
+
+ QCOMPARE(model.rowCount(), 2);
+
+ {
+ const auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+ QVERIFY(contains(roles, "name"));
+ QVERIFY(contains(roles, "subname"));
+ }
+
+ model.setSourceModel(sourceModel2); // set empty source model
+
+ QCOMPARE(signalsSpy->count(), 2);
+ QCOMPARE(signalsSpy->modelAboutToBeResetSpy.count(), 1);
+ QCOMPARE(signalsSpy->modelResetSpy.count(), 1);
+
+ QCOMPARE(model.rowCount(), 0);
+
+ {
+ const auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 0);
+ }
+
+ sourceModel2.append(QJsonArray { // append to empty source model
+ QJsonObject {{ "name", "A"}, { "subname_", "a1" }},
+ QJsonObject {{ "name", "A"}, { "subname_", "a2" }},
+ QJsonObject {{ "name", "B"}, { "subname_", "b1" }}
+ });
+
+ QCOMPARE(signalsSpy->count(), 4);
+ QCOMPARE(signalsSpy->rowsAboutToBeInsertedSpy.count(), 1);
+ QCOMPARE(signalsSpy->rowsInsertedSpy.count(), 1);
+ signalsSpy = std::make_unique(&model);
+
+ QCOMPARE(model.rowCount(), 2);
+
+ {
+ const auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+ QVERIFY(contains(roles, "name"));
+ QVERIFY(contains(roles, "subname_"));
+ }
+
+ model.setSourceModel(nullptr); // set null source model
+
+ QCOMPARE(signalsSpy->count(), 2);
+ QCOMPARE(signalsSpy->modelAboutToBeResetSpy.count(), 1);
+ QCOMPARE(signalsSpy->modelResetSpy.count(), 1);
+
+ QCOMPARE(model.rowCount(), 0);
+
+ model.setSourceModel(nullptr); // set null source model
+
+ QCOMPARE(signalsSpy->count(), 2);
+ signalsSpy = std::make_unique(&model);
+
+ {
+ const auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 0);
+ }
+
+ QCOMPARE(signalsSpy->count(), 0);
+
+ model.setSourceModel(sourceModel2); // set not empty source model
+
+ QCOMPARE(signalsSpy->count(), 2);
+ QCOMPARE(signalsSpy->modelAboutToBeResetSpy.count(), 1);
+ QCOMPARE(signalsSpy->modelResetSpy.count(), 1);
+ signalsSpy = std::make_unique(&model);
+
+ QCOMPARE(model.rowCount(), 2);
+
+ {
+ const auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+ QVERIFY(contains(roles, "name"));
+ QVERIFY(contains(roles, "subname_"));
+ }
+
+ model.setSourceModel(sourceModel1); // set not empty source model
+
+ QCOMPARE(signalsSpy->count(), 2);
+ QCOMPARE(signalsSpy->modelAboutToBeResetSpy.count(), 1);
+ QCOMPARE(signalsSpy->modelResetSpy.count(), 1);
+
+ QCOMPARE(model.rowCount(), 2);
+
+ {
+ const auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+ QVERIFY(contains(roles, "name"));
+ QVERIFY(contains(roles, "subname"));
+ }
+ }
+
+ void setSubmodelRoleTest()
+ {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a2" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b2" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c1" }}
+ });
+
+ ListModelWrapper expected(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "subnames": [
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "subnames": [
+ { "name": "B", "subname": "b1" },
+ { "name": "B", "subname": "b2" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "subnames": [
+ { "name": "C", "subname": "c1" }
+ ]
+ }
+ ])");
+
+ auto contains = [](auto roles, auto name) {
+ return std::find(roles.cbegin(), roles.cend(), name) != roles.cend();
+ };
+
+ {
+ GroupingModel model;
+ auto signalsSpy = std::make_unique(&model);
+
+ model.setGroupingRoleName("name");
+ model.setSubmodelRoleName("subnames");
+
+ model.setSourceModel(sourceModel.model());
+
+ QCOMPARE(signalsSpy->count(), 2);
+ QCOMPARE(signalsSpy->modelAboutToBeResetSpy.count(), 1);
+ QCOMPARE(signalsSpy->modelResetSpy.count(), 1);
+
+ QCOMPARE(model.rowCount(), 3);
+
+ const auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+ QVERIFY(contains(roles, "name"));
+ QVERIFY(contains(roles, "subname"));
+ QVERIFY(contains(roles, "subnames"));
+
+ QVERIFY(isSame(model, *expected.model()));
+ }
+
+ {
+ GroupingModel model;
+ auto signalsSpy = std::make_unique(&model);
+
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel.model());
+ model.setSubmodelRoleName("subnames");
+
+ QCOMPARE(signalsSpy->count(), 4);
+ QCOMPARE(signalsSpy->modelAboutToBeResetSpy.count(), 2);
+ QCOMPARE(signalsSpy->modelResetSpy.count(), 2);
+
+ QCOMPARE(model.rowCount(), 3);
+
+ const auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+ QVERIFY(contains(roles, "name"));
+ QVERIFY(contains(roles, "subname"));
+ QVERIFY(contains(roles, "subnames"));
+
+ QVERIFY(isSame(model, *expected.model()));
+ }
+ }
+
+ void preppendToFirstGroupTest()
+ {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a2" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b2" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c1" }}
+ });
+
+ GroupingModel model;
+
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel.model());
+
+ QCOMPARE(model.rowCount(), 3);
+
+ auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+
+ SnapshotModel snapshot(model);
+
+ auto submodelRole = roleForName(roles, "submodel");
+
+ auto submodel1 = model.data(model.index(0, 0), submodelRole)
+ .value();
+ auto submodel2 = model.data(model.index(1, 0), submodelRole)
+ .value();
+ auto submodel3 = model.data(model.index(2, 0), submodelRole)
+ .value();
+
+ ListModelWrapper expected(engine, R"([
+ {
+ "name": "A", "subname": "a0",
+ "submodel": [
+ { "name": "A", "subname": "a0" },
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" },
+ { "name": "B", "subname": "b2" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "submodel": [
+ { "name": "C", "subname": "c1" }
+ ]
+ }
+ ])");
+
+ QObject context;
+ connect(submodel1, &QAbstractItemModel::rowsAboutToBeInserted, &context,
+ [m = &model, s = &snapshot] {
+ QVERIFY(isSame(m, s));
+ });
+ connect(submodel1, &QAbstractItemModel::rowsInserted, &context,
+ [m = &model, expected = expected.model()] {
+ QVERIFY(isSame(m, expected));
+ });
+
+ ModelSignalsSpy signalsSpy(&model);
+ ModelSignalsSpy submodel1Spy(submodel1);
+ ModelSignalsSpy submodel2Spy(submodel2);
+ ModelSignalsSpy submodel3Spy(submodel3);
+
+ sourceModel.insert(0, QJsonObject {
+ { "name", "A"}, { "subname", "a0" }
+ });
+
+ QCOMPARE(signalsSpy.dataChangedSpy.count(), 1);
+ QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(0), model.index(0));
+ QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(1), model.index(0));
+ QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(2).value>(),
+ { roleForName(roles, "subname") });
+
+ QCOMPARE(submodel1Spy.rowsAboutToBeInsertedSpy.count(), 1);
+ QCOMPARE(submodel1Spy.rowsAboutToBeInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(submodel1Spy.rowsAboutToBeInsertedSpy.at(0).at(1), 0);
+ QCOMPARE(submodel1Spy.rowsAboutToBeInsertedSpy.at(0).at(2), 0);
+
+ QCOMPARE(submodel1Spy.rowsInsertedSpy.count(), 1);
+ QCOMPARE(submodel1Spy.rowsInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(submodel1Spy.rowsInsertedSpy.at(0).at(1), 0);
+ QCOMPARE(submodel1Spy.rowsInsertedSpy.at(0).at(2), 0);
+
+ QCOMPARE(signalsSpy.count(), 1);
+ QCOMPARE(submodel1Spy.count(), 2);
+ QCOMPARE(submodel2Spy.count(), 0);
+ QCOMPARE(submodel3Spy.count(), 0);
+
+ QVERIFY(isSame(model, *expected.model()));
+ }
+
+ void preppendToGroupTest()
+ {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a2" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b2" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c1" }}
+ });
+
+ GroupingModel model;
+
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel.model());
+
+ QCOMPARE(model.rowCount(), 3);
+
+ auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+
+ SnapshotModel snapshot(model);
+
+ auto submodelRole = roleForName(roles, "submodel");
+
+ auto submodel1 = model.data(model.index(0, 0), submodelRole)
+ .value();
+ auto submodel2 = model.data(model.index(1, 0), submodelRole)
+ .value();
+ auto submodel3 = model.data(model.index(2, 0), submodelRole)
+ .value();
+
+ ListModelWrapper expected(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b01",
+ "submodel": [
+ { "name": "B", "subname": "b01" },
+ { "name": "B", "subname": "b02" },
+ { "name": "B", "subname": "b1" },
+ { "name": "B", "subname": "b2" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "submodel": [
+ { "name": "C", "subname": "c1" }
+ ]
+ }
+ ])");
+
+ QObject context;
+ connect(submodel2, &QAbstractItemModel::rowsAboutToBeInserted, &context,
+ [m = &model, s = &snapshot] {
+ QVERIFY(isSame(m, s));
+ });
+ connect(submodel2, &QAbstractItemModel::rowsInserted, &context,
+ [m = &model, expected = expected.model()] {
+ QVERIFY(isSame(m, expected));
+ });
+
+ ModelSignalsSpy signalsSpy(&model);
+ ModelSignalsSpy submodel1Spy(submodel1);
+ ModelSignalsSpy submodel2Spy(submodel2);
+ ModelSignalsSpy submodel3Spy(submodel3);
+
+ sourceModel.insert(2, QJsonArray {
+ QJsonObject {{ "name", "B"}, { "subname", "b01" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b02" }}
+ });
+
+ QCOMPARE(signalsSpy.dataChangedSpy.count(), 1);
+ QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(0), model.index(1));
+ QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(1), model.index(1));
+ QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(2).value>(),
+ { roleForName(roles, "subname") });
+
+ QCOMPARE(submodel2Spy.rowsAboutToBeInsertedSpy.count(), 1);
+ QCOMPARE(submodel2Spy.rowsAboutToBeInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(submodel2Spy.rowsAboutToBeInsertedSpy.at(0).at(1), 0);
+ QCOMPARE(submodel2Spy.rowsAboutToBeInsertedSpy.at(0).at(2), 1);
+
+ QCOMPARE(submodel2Spy.rowsInsertedSpy.count(), 1);
+ QCOMPARE(submodel2Spy.rowsInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(submodel2Spy.rowsInsertedSpy.at(0).at(1), 0);
+ QCOMPARE(submodel2Spy.rowsInsertedSpy.at(0).at(2), 1);
+
+ QCOMPARE(signalsSpy.count(), 1);
+ QCOMPARE(submodel1Spy.count(), 0);
+ QCOMPARE(submodel2Spy.count(), 2);
+ QCOMPARE(submodel3Spy.count(), 0);
+
+ QVERIFY(isSame(model, *expected.model()));
+ }
+
+ void insertToGroupTest()
+ {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a2" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b2" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c1" }}
+ });
+
+ GroupingModel model;
+
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel.model());
+
+ QCOMPARE(model.rowCount(), 3);
+
+ auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+
+ SnapshotModel snapshot(model);
+
+ auto submodelRole = roleForName(roles, "submodel");
+
+ auto submodel1 = model.data(model.index(0, 0), submodelRole)
+ .value();
+ auto submodel2 = model.data(model.index(1, 0), submodelRole)
+ .value();
+ auto submodel3 = model.data(model.index(2, 0), submodelRole)
+ .value();
+
+ ListModelWrapper expected(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a11" },
+ { "name": "A", "subname": "a12" },
+ { "name": "A", "subname": "a2" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" },
+ { "name": "B", "subname": "b2" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "submodel": [
+ { "name": "C", "subname": "c1" }
+ ]
+ }
+ ])");
+
+ QObject context;
+ connect(submodel1, &QAbstractItemModel::rowsAboutToBeInserted, &context,
+ [m = &model, s = &snapshot] {
+ QVERIFY(isSame(m, s));
+ });
+
+ connect(submodel1, &QAbstractItemModel::rowsInserted, &context,
+ [m = &model, expected = expected.model()] {
+ QVERIFY(isSame(m, expected));
+ });
+
+ ModelSignalsSpy signalsSpy(&model);
+ ModelSignalsSpy submodel1Spy(submodel1);
+ ModelSignalsSpy submodel2Spy(submodel2);
+ ModelSignalsSpy submodel3Spy(submodel3);
+
+ sourceModel.insert(1, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a11" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a12" }}
+ });
+
+ QCOMPARE(submodel1Spy.rowsAboutToBeInsertedSpy.count(), 1);
+ QCOMPARE(submodel1Spy.rowsAboutToBeInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(submodel1Spy.rowsAboutToBeInsertedSpy.at(0).at(1), 1);
+ QCOMPARE(submodel1Spy.rowsAboutToBeInsertedSpy.at(0).at(2), 2);
+
+ QCOMPARE(submodel1Spy.rowsInsertedSpy.count(), 1);
+ QCOMPARE(submodel1Spy.rowsInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(submodel1Spy.rowsInsertedSpy.at(0).at(1), 1);
+ QCOMPARE(submodel1Spy.rowsInsertedSpy.at(0).at(2), 2);
+
+ QCOMPARE(signalsSpy.count(), 0);
+ QCOMPARE(submodel1Spy.count(), 2);
+ QCOMPARE(submodel2Spy.count(), 0);
+ QCOMPARE(submodel3Spy.count(), 0);
+
+ QVERIFY(isSame(model, *expected.model()));
+ }
+
+ void insertToAdjacentGroupsTest()
+ {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a2" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b2" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c1" }}
+ });
+
+ GroupingModel model;
+
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel.model());
+
+ QCOMPARE(model.rowCount(), 3);
+
+ auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+
+ SnapshotModel snapshot(model);
+
+ auto submodelRole = roleForName(roles, "submodel");
+
+ auto submodel1 = model.data(model.index(0, 0), submodelRole)
+ .value();
+ auto submodel2 = model.data(model.index(1, 0), submodelRole)
+ .value();
+ auto submodel3 = model.data(model.index(2, 0), submodelRole)
+ .value();
+
+ ListModelWrapper expected(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" },
+ { "name": "A", "subname": "a3" },
+ { "name": "A", "subname": "a4" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b01",
+ "submodel": [
+ { "name": "B", "subname": "b01" },
+ { "name": "B", "subname": "b02" },
+ { "name": "B", "subname": "b1" },
+ { "name": "B", "subname": "b2" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "submodel": [
+ { "name": "C", "subname": "c1" }
+ ]
+ }
+ ])");
+
+ QObject context;
+ connect(submodel1, &QAbstractItemModel::rowsAboutToBeInserted, &context,
+ [m = &model, s = &snapshot] {
+ QVERIFY(isSame(m, s));
+ });
+ connect(submodel1, &QAbstractItemModel::rowsInserted, &context,
+ [m = &model, expected = expected.model()] {
+ QVERIFY(isSame(m, expected));
+ });
+ connect(submodel2, &QAbstractItemModel::rowsAboutToBeInserted, &context,
+ [m = &model, s = &snapshot] {
+ QVERIFY(isSame(m, s));
+ });
+ connect(submodel2, &QAbstractItemModel::rowsInserted, &context,
+ [m = &model, expected = expected.model()] {
+ QVERIFY(isSame(*m, *expected));
+ });
+
+ ModelSignalsSpy signalsSpy(&model);
+ ModelSignalsSpy submodel1Spy(submodel1);
+ ModelSignalsSpy submodel2Spy(submodel2);
+ ModelSignalsSpy submodel3Spy(submodel3);
+
+ sourceModel.insert(2, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a3" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a4" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b01" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b02" }}
+ });
+
+ QCOMPARE(signalsSpy.dataChangedSpy.count(), 1);
+ QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(0), model.index(1));
+ QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(1), model.index(1));
+ QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(2).value>(),
+ { roleForName(roles, "subname") });
+
+ QCOMPARE(submodel1Spy.rowsAboutToBeInsertedSpy.count(), 1);
+ QCOMPARE(submodel1Spy.rowsAboutToBeInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(submodel1Spy.rowsAboutToBeInsertedSpy.at(0).at(1), 2);
+ QCOMPARE(submodel1Spy.rowsAboutToBeInsertedSpy.at(0).at(2), 3);
+
+ QCOMPARE(submodel1Spy.rowsInsertedSpy.count(), 1);
+ QCOMPARE(submodel1Spy.rowsInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(submodel1Spy.rowsInsertedSpy.at(0).at(1), 2);
+ QCOMPARE(submodel1Spy.rowsInsertedSpy.at(0).at(2), 3);
+
+ QCOMPARE(submodel2Spy.rowsAboutToBeInsertedSpy.count(), 1);
+ QCOMPARE(submodel2Spy.rowsAboutToBeInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(submodel2Spy.rowsAboutToBeInsertedSpy.at(0).at(1), 0);
+ QCOMPARE(submodel2Spy.rowsAboutToBeInsertedSpy.at(0).at(2), 1);
+
+ QCOMPARE(submodel2Spy.rowsInsertedSpy.count(), 1);
+ QCOMPARE(submodel2Spy.rowsInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(submodel2Spy.rowsInsertedSpy.at(0).at(1), 0);
+ QCOMPARE(submodel2Spy.rowsInsertedSpy.at(0).at(2), 1);
+
+ QCOMPARE(signalsSpy.count(), 1);
+ QCOMPARE(submodel1Spy.count(), 2);
+ QCOMPARE(submodel2Spy.count(), 2);
+ QCOMPARE(submodel3Spy.count(), 0);
+
+ QVERIFY(isSame(model, *expected.model()));
+ }
+
+ void insertNewGroupTest()
+ {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a2" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b2" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c1" }}
+ });
+
+ GroupingModel model;
+
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel.model());
+
+ QCOMPARE(model.rowCount(), 3);
+
+ auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+
+ SnapshotModel snapshot(model);
+
+ auto submodelRole = roleForName(roles, "submodel");
+
+ auto submodel1 = model.data(model.index(0, 0), submodelRole)
+ .value();
+ auto submodel2 = model.data(model.index(1, 0), submodelRole)
+ .value();
+ auto submodel3 = model.data(model.index(2, 0), submodelRole)
+ .value();
+
+ ListModelWrapper expected(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" }
+ ]
+ },
+ {
+ "name": "AA", "subname": "aa1",
+ "submodel": [
+ { "name": "AA", "subname": "aa1" },
+ { "name": "AA", "subname": "aa2" }
+ ]
+ },
+ {
+ "name": "AB", "subname": "ab1",
+ "submodel": [
+ { "name": "AB", "subname": "ab1" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" },
+ { "name": "B", "subname": "b2" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "submodel": [
+ { "name": "C", "subname": "c1" }
+ ]
+ }
+ ])");
+
+ QObject context;
+ connect(&model, &QAbstractItemModel::rowsAboutToBeInserted, &context,
+ [m = &model, s = &snapshot] {
+ QVERIFY(isSame(m, s));
+ });
+
+ connect(&model, &QAbstractItemModel::rowsInserted, &context,
+ [m = &model, expected = expected.model()] {
+ QVERIFY(isSame(m, expected));
+ });
+
+ ModelSignalsSpy signalsSpy(&model);
+ ModelSignalsSpy submodel1Spy(submodel1);
+ ModelSignalsSpy submodel2Spy(submodel2);
+ ModelSignalsSpy submodel3Spy(submodel3);
+
+ sourceModel.insert(2, QJsonArray {
+ QJsonObject {{ "name", "AA"}, { "subname", "aa1" }},
+ QJsonObject {{ "name", "AA"}, { "subname", "aa2" }},
+ QJsonObject {{ "name", "AB"}, { "subname", "ab1" }}
+ });
+
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.count(), 1);
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(1), 1);
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(2), 2);
+
+ QCOMPARE(signalsSpy.rowsInsertedSpy.count(), 1);
+ QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(1), 1);
+ QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(2), 2);
+
+ QCOMPARE(signalsSpy.count(), 2);
+ QCOMPARE(submodel1Spy.count(), 0);
+ QCOMPARE(submodel2Spy.count(), 0);
+ QCOMPARE(submodel3Spy.count(), 0);
+
+ QVERIFY(isSame(model, *expected.model()));
+ }
+
+ void insertToEmptyModelTest()
+ {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine);
+
+ GroupingModel model;
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel.model());
+
+ QCOMPARE(model.rowCount(), 0);
+
+ ListModelWrapper expected(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" }
+ ]
+ }
+ ])");
+
+ QObject context;
+ connect(&model, &QAbstractItemModel::rowsAboutToBeInserted, &context,
+ [m = &model] {
+ QCOMPARE(m->rowCount(), 0);
+ });
+
+ connect(&model, &QAbstractItemModel::rowsInserted, &context,
+ [m = &model, expected = expected.model()] {
+ QVERIFY(isSame(*m, *expected));
+ });
+
+ ModelSignalsSpy signalsSpy(&model);
+
+ sourceModel.append(QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a2" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }}
+ });
+
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.count(), 1);
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(1), 0);
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(2), 1);
+
+ QCOMPARE(signalsSpy.rowsInsertedSpy.count(), 1);
+ QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(1), 0);
+ QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(2), 1);
+
+ QCOMPARE(signalsSpy.count(), 2);
+ QVERIFY(isSame(model, *expected.model()));
+ }
+
+ void insertNewGroupAndSplitTest()
+ {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a2" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b2" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c1" }}
+ });
+
+ GroupingModel model;
+
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel.model());
+
+ QCOMPARE(model.rowCount(), 3);
+
+ auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+
+ SnapshotModel snapshot(model);
+
+ auto submodelRole = roleForName(roles, "submodel");
+
+ auto submodel1 = model.data(model.index(0, 0), submodelRole)
+ .value();
+ auto submodel2 = model.data(model.index(1, 0), submodelRole)
+ .value();
+ auto submodel3 = model.data(model.index(2, 0), submodelRole)
+ .value();
+
+ ListModelWrapper expected(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" }
+ ]
+ },
+ {
+ "name": "D", "subname": "d1",
+ "submodel": [
+ { "name": "D", "subname": "d1" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b2",
+ "submodel": [
+ { "name": "B", "subname": "b2" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "submodel": [
+ { "name": "C", "subname": "c1" }
+ ]
+ }
+ ])");
+
+ ListModelWrapper expectedIntermediate(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "submodel": [
+ { "name": "C", "subname": "c1" }
+ ]
+ }
+ ])");
+
+ QObject context;
+ connect(submodel2, &QAbstractItemModel::rowsAboutToBeRemoved, &context,
+ [m = &model, s = &snapshot] {
+ QVERIFY(isSame(m, s));
+ });
+ connect(submodel2, &QAbstractItemModel::rowsRemoved, &context,
+ [m = &model, e = expectedIntermediate.model()] {
+ QVERIFY(isSame(m, e));
+ });
+ connect(&model, &QAbstractItemModel::rowsAboutToBeInserted, &context,
+ [m = &model, e = expectedIntermediate.model()] {
+ QVERIFY(isSame(m, e));
+ });
+ connect(&model, &QAbstractItemModel::rowsInserted, &context,
+ [m = &model, e = expected.model()] {
+ QVERIFY(isSame(m, e));
+ });
+
+ ModelSignalsSpy signalsSpy(&model);
+ ModelSignalsSpy submodel1Spy(submodel1);
+ ModelSignalsSpy submodel2Spy(submodel2);
+ ModelSignalsSpy submodel3Spy(submodel3);
+
+ sourceModel.insert(3, QJsonArray {
+ QJsonObject {{ "name", "D"}, { "subname", "d1" }}
+ });
+
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.count(), 1);
+ QCOMPARE(signalsSpy.rowsInsertedSpy.count(), 1);
+
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.count(), 1);
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(1), 2);
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(2), 3);
+
+ QCOMPARE(signalsSpy.rowsInsertedSpy.count(), 1);
+ QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(1), 2);
+ QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(2), 3);
+
+ QCOMPARE(submodel2Spy.rowsAboutToBeRemovedSpy.count(), 1);
+ QCOMPARE(submodel2Spy.rowsAboutToBeRemovedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(submodel2Spy.rowsAboutToBeRemovedSpy.at(0).at(1), 1);
+ QCOMPARE(submodel2Spy.rowsAboutToBeRemovedSpy.at(0).at(2), 1);
+
+ QCOMPARE(submodel2Spy.rowsRemovedSpy.count(), 1);
+ QCOMPARE(submodel2Spy.rowsRemovedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(submodel2Spy.rowsRemovedSpy.at(0).at(1), 1);
+ QCOMPARE(submodel2Spy.rowsRemovedSpy.at(0).at(2), 1);
+
+ QCOMPARE(signalsSpy.count(), 2);
+ QCOMPARE(submodel1Spy.count(), 0);
+ QCOMPARE(submodel2Spy.count(), 2);
+ QCOMPARE(submodel3Spy.count(), 0);
+
+ QVERIFY(isSame(model, *expected.model()));
+ }
+
+ void insertToGroupAndSplitTest()
+ {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a2" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b2" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c1" }}
+ });
+
+ GroupingModel model;
+
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel.model());
+
+ QCOMPARE(model.rowCount(), 3);
+
+ auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+
+ SnapshotModel snapshot(model);
+
+ auto submodelRole = roleForName(roles, "submodel");
+
+ auto submodel1 = model.data(model.index(0, 0), submodelRole)
+ .value();
+ auto submodel2 = model.data(model.index(1, 0), submodelRole)
+ .value();
+ auto submodel3 = model.data(model.index(2, 0), submodelRole)
+ .value();
+
+ ListModelWrapper expected(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" },
+ { "name": "B", "subname": "b11" }
+ ]
+ },
+ {
+ "name": "D", "subname": "d1",
+ "submodel": [
+ { "name": "D", "subname": "d1" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b12",
+ "submodel": [
+ { "name": "B", "subname": "b12" },
+ { "name": "B", "subname": "b2" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "submodel": [
+ { "name": "C", "subname": "c1" }
+ ]
+ }
+ ])");
+
+ ListModelWrapper expectedIntermediate(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "submodel": [
+ { "name": "C", "subname": "c1" }
+ ]
+ }
+ ])");
+
+ QObject context;
+ connect(submodel2, &QAbstractItemModel::rowsAboutToBeRemoved, &context,
+ [m = &model, s = &snapshot] {
+ QVERIFY(isSame(m, s));
+ });
+ connect(submodel2, &QAbstractItemModel::rowsRemoved, &context,
+ [m = &model, e = expectedIntermediate.model()] {
+ QVERIFY(isSame(m, e));
+ });
+ connect(submodel2, &QAbstractItemModel::rowsAboutToBeInserted, &context,
+ [m = &model, e = expectedIntermediate.model()] {
+ QVERIFY(isSame(m, e));
+ });
+ connect(submodel2, &QAbstractItemModel::rowsInserted, &context,
+ [m = &model, e = expected.model()] {
+ QVERIFY(isSame(m, e));
+ });
+ connect(&model, &QAbstractItemModel::rowsAboutToBeInserted, &context,
+ [m = &model, e = expectedIntermediate.model()] {
+ QVERIFY(isSame(m, e));
+ });
+ connect(&model, &QAbstractItemModel::rowsInserted, &context,
+ [m = &model, e = expected.model()] {
+ QVERIFY(isSame(m, e));
+ });
+
+ ModelSignalsSpy signalsSpy(&model);
+ ModelSignalsSpy submodel1Spy(submodel1);
+ ModelSignalsSpy submodel2Spy(submodel2);
+ ModelSignalsSpy submodel3Spy(submodel3);
+
+ sourceModel.insert(3, QJsonArray {
+ QJsonObject {{ "name", "B"}, { "subname", "b11" }},
+ QJsonObject {{ "name", "D"}, { "subname", "d1" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b12" }}
+ });
+
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.count(), 1);
+ QCOMPARE(signalsSpy.rowsInsertedSpy.count(), 1);
+
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.count(), 1);
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(1), 2);
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(2), 3);
+
+ QCOMPARE(signalsSpy.rowsInsertedSpy.count(), 1);
+ QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(1), 2);
+ QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(2), 3);
+
+ QCOMPARE(submodel2Spy.rowsAboutToBeRemovedSpy.count(), 1);
+ QCOMPARE(submodel2Spy.rowsAboutToBeRemovedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(submodel2Spy.rowsAboutToBeRemovedSpy.at(0).at(1), 1);
+ QCOMPARE(submodel2Spy.rowsAboutToBeRemovedSpy.at(0).at(2), 1);
+
+ QCOMPARE(submodel2Spy.rowsRemovedSpy.count(), 1);
+ QCOMPARE(submodel2Spy.rowsRemovedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(submodel2Spy.rowsRemovedSpy.at(0).at(1), 1);
+ QCOMPARE(submodel2Spy.rowsRemovedSpy.at(0).at(2), 1);
+
+ QCOMPARE(submodel2Spy.rowsAboutToBeInsertedSpy.count(), 1);
+ QCOMPARE(submodel2Spy.rowsAboutToBeInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(submodel2Spy.rowsAboutToBeInsertedSpy.at(0).at(1), 1);
+ QCOMPARE(submodel2Spy.rowsAboutToBeInsertedSpy.at(0).at(2), 1);
+
+ QCOMPARE(submodel2Spy.rowsInsertedSpy.count(), 1);
+ QCOMPARE(submodel2Spy.rowsInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(submodel2Spy.rowsInsertedSpy.at(0).at(1), 1);
+ QCOMPARE(submodel2Spy.rowsInsertedSpy.at(0).at(2), 1);
+
+ QCOMPARE(signalsSpy.count(), 2);
+ QCOMPARE(submodel1Spy.count(), 0);
+ QCOMPARE(submodel2Spy.count(), 4);
+ QCOMPARE(submodel3Spy.count(), 0);
+
+ QVERIFY(isSame(model, *expected.model()));
+ }
+
+ void insertAtEndTest()
+ {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a2" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b2" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c1" }}
+ });
+
+ GroupingModel model;
+
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel.model());
+
+ QCOMPARE(model.rowCount(), 3);
+
+ auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+
+ SnapshotModel snapshot(model);
+
+ auto submodelRole = roleForName(roles, "submodel");
+
+ auto submodel1 = model.data(model.index(0, 0), submodelRole)
+ .value();
+ auto submodel2 = model.data(model.index(1, 0), submodelRole)
+ .value();
+ auto submodel3 = model.data(model.index(2, 0), submodelRole)
+ .value();
+
+ ListModelWrapper expected(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" },
+ { "name": "B", "subname": "b2" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "submodel": [
+ { "name": "C", "subname": "c1" },
+ { "name": "C", "subname": "c2" },
+ { "name": "C", "subname": "c3" }
+ ]
+ },
+ {
+ "name": "D", "subname": "d1",
+ "submodel": [
+ { "name": "D", "subname": "d1" }
+ ]
+ },
+ {
+ "name": "E", "subname": "e1",
+ "submodel": [
+ { "name": "E", "subname": "e1" }
+ ]
+ }
+ ])");
+
+ QObject context;
+ connect(submodel3, &QAbstractItemModel::rowsAboutToBeInserted, &context,
+ [m = &model, s = &snapshot] {
+ QVERIFY(isSame(m, s));
+ });
+ connect(submodel3, &QAbstractItemModel::rowsInserted, &context,
+ [m = &model, e = expected.model()] {
+ QVERIFY(isSame(m, e));
+ });
+ connect(&model, &QAbstractItemModel::rowsAboutToBeInserted, &context,
+ [m = &model, s = &snapshot] {
+ QVERIFY(isSame(m, s));
+ });
+ connect(&model, &QAbstractItemModel::rowsInserted, &context,
+ [m = &model, e = expected.model()] {
+ QVERIFY(isSame(m, e));
+ });
+
+ ModelSignalsSpy signalsSpy(&model);
+ ModelSignalsSpy submodel1Spy(submodel1);
+ ModelSignalsSpy submodel2Spy(submodel2);
+ ModelSignalsSpy submodel3Spy(submodel3);
+
+ sourceModel.append(QJsonArray {
+ QJsonObject {{ "name", "C"}, { "subname", "c2" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c3" }},
+ QJsonObject {{ "name", "D"}, { "subname", "d1" }},
+ QJsonObject {{ "name", "E"}, { "subname", "e1" }}
+ });
+
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.count(), 1);
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(1), 3);
+ QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.at(0).at(2), 4);
+
+ QCOMPARE(signalsSpy.rowsInsertedSpy.count(), 1);
+ QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(1), 3);
+ QCOMPARE(signalsSpy.rowsInsertedSpy.at(0).at(2), 4);
+
+ QCOMPARE(submodel3Spy.rowsAboutToBeInsertedSpy.count(), 1);
+ QCOMPARE(submodel3Spy.rowsAboutToBeInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(submodel3Spy.rowsAboutToBeInsertedSpy.at(0).at(1), 1);
+ QCOMPARE(submodel3Spy.rowsAboutToBeInsertedSpy.at(0).at(2), 2);
+
+ QCOMPARE(submodel3Spy.rowsInsertedSpy.count(), 1);
+ QCOMPARE(submodel3Spy.rowsInsertedSpy.at(0).at(0), InvalidIdx);
+ QCOMPARE(submodel3Spy.rowsInsertedSpy.at(0).at(1), 1);
+ QCOMPARE(submodel3Spy.rowsInsertedSpy.at(0).at(2), 2);
+
+ QCOMPARE(signalsSpy.count(), 2);
+ QCOMPARE(submodel1Spy.count(), 0);
+ QCOMPARE(submodel2Spy.count(), 0);
+ QCOMPARE(submodel3Spy.count(), 2);
+
+ QVERIFY(isSame(model, *expected.model()));
+ }
+
+ void removeFromBeginingTest()
+ {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }}, // to be removed
+ QJsonObject {{ "name", "A"}, { "subname", "a2" }}, // to be removed
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }}, // to be removed
+ QJsonObject {{ "name", "C"}, { "subname", "c1" }}, // to be removed
+ QJsonObject {{ "name", "C"}, { "subname", "c2" }}, // to be removed
+ QJsonObject {{ "name", "C"}, { "subname", "c3" }},
+ QJsonObject {{ "name", "D"}, { "subname", "d1" }}
+ });
+
+ GroupingModel model;
+
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel);
+
+ QCOMPARE(model.rowCount(), 4);
+
+ auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+
+ SnapshotModel snapshot(model);
+
+ auto submodelRole = roleForName(roles, "submodel");
+
+ QPointer submodel1
+ = model.data(model.index(0, 0), submodelRole).value();
+ QPointer submodel2
+ = model.data(model.index(1, 0), submodelRole).value();
+ QPointer submodel3
+ = model.data(model.index(2, 0), submodelRole).value();
+ QPointer submodel4
+ = model.data(model.index(3, 0), submodelRole).value();
+
+ ListModelWrapper expected(engine, R"([
+ {
+ "name": "C", "subname": "c3",
+ "submodel": [
+ { "name": "C", "subname": "c3" }
+ ]
+ },
+ {
+ "name": "D", "subname": "d1",
+ "submodel": [
+ { "name": "D", "subname": "d1" }
+ ]
+ }
+ ])");
+
+ ListModelWrapper expectedIntermediate(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c3",
+ "submodel": [
+ { "name": "C", "subname": "c3" }
+ ]
+ },
+ {
+ "name": "D", "subname": "d1",
+ "submodel": [
+ { "name": "D", "subname": "d1" }
+ ]
+ }
+ ])");
+
+ QVERIFY(isSame(model, snapshot));
+
+ QObject context;
+ connect(submodel3, &QAbstractItemModel::rowsAboutToBeRemoved, &context,
+ [m = &model, s = &snapshot] {
+ QVERIFY(isSame(m, s));
+ });
+ connect(submodel3, &QAbstractItemModel::rowsRemoved, &context,
+ [m = &model, e = expectedIntermediate.model()] {
+ QVERIFY(isSame(m, e));
+ });
+ connect(&model, &QAbstractItemModel::rowsAboutToBeRemoved, &context,
+ [m = &model, e = expectedIntermediate.model()] {
+ QVERIFY(isSame(m, e));
+ });
+ connect(&model, &QAbstractItemModel::rowsRemoved, &context,
+ [m = &model, e = expected.model()] {
+ QVERIFY(isSame(m, e));
+ });
+
+ ModelSignalsSpy signalsSpy(&model);
+ ModelSignalsSpy submodel1Spy(submodel1);
+ ModelSignalsSpy submodel2Spy(submodel2);
+ ModelSignalsSpy submodel3Spy(submodel3);
+ ModelSignalsSpy submodel4Spy(submodel4);
+
+ sourceModel.remove(0, 5);
+ QCOMPARE(sourceModel.count(), 2);
+
+ QVERIFY(submodel1.isNull());
+ QVERIFY(submodel2.isNull());
+ QVERIFY(!submodel3.isNull());
+ QVERIFY(!submodel4.isNull());
+
+ QCOMPARE(signalsSpy.count(), 2);
+ QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.count(), 1);
+ QCOMPARE(signalsSpy.rowsRemovedSpy.count(), 1);
+ QCOMPARE(submodel1Spy.count(), 0);
+ QCOMPARE(submodel2Spy.count(), 0);
+ QCOMPARE(submodel3Spy.count(), 2);
+ QCOMPARE(submodel3Spy.rowsAboutToBeRemovedSpy.count(), 1);
+ QCOMPARE(submodel3Spy.rowsRemovedSpy.count(), 1);
+ QCOMPARE(submodel4Spy.count(), 0);
+
+ QVERIFY(isSame(&model, expected));
+ }
+
+ void removeSingleInTheMiddleTest()
+ {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b2" }}, // to be removed
+ QJsonObject {{ "name", "B"}, { "subname", "b3" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c1" }}
+ });
+
+ GroupingModel model;
+
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel);
+
+ QCOMPARE(model.rowCount(), 3);
+
+ auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+
+ SnapshotModel snapshot(model);
+
+ auto submodelRole = roleForName(roles, "submodel");
+
+ QPointer submodel1
+ = model.data(model.index(0, 0), submodelRole).value();
+ QPointer submodel2
+ = model.data(model.index(1, 0), submodelRole).value();
+ QPointer submodel3
+ = model.data(model.index(2, 0), submodelRole).value();
+
+ ListModelWrapper expected(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" },
+ { "name": "B", "subname": "b3" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "submodel": [
+ { "name": "C", "subname": "c1" }
+ ]
+ }
+ ])");
+
+ QVERIFY(isSame(model, snapshot));
+
+ QObject context;
+ connect(submodel2, &QAbstractItemModel::rowsAboutToBeRemoved, &context,
+ [m = &model, s = &snapshot] {
+ QVERIFY(isSame(m, s));
+ });
+ connect(submodel2, &QAbstractItemModel::rowsRemoved, &context,
+ [m = &model, e = expected.model()] {
+ QVERIFY(isSame(m, e));
+ });
+
+ ModelSignalsSpy signalsSpy(&model);
+ ModelSignalsSpy submodel1Spy(submodel1);
+ ModelSignalsSpy submodel2Spy(submodel2);
+ ModelSignalsSpy submodel3Spy(submodel3);
+
+ sourceModel.remove(2, 1);
+ QCOMPARE(model.rowCount(), 3);
+
+ QVERIFY(!submodel1.isNull());
+ QVERIFY(!submodel2.isNull());
+ QVERIFY(!submodel3.isNull());
+
+ QCOMPARE(signalsSpy.count(), 0);
+ QCOMPARE(submodel1Spy.count(), 0);
+ QCOMPARE(submodel2Spy.count(), 2);
+ QCOMPARE(submodel2Spy.rowsAboutToBeRemovedSpy.count(), 1);
+ QCOMPARE(submodel2Spy.rowsRemovedSpy.count(), 1);
+ QCOMPARE(submodel3Spy.count(), 0);
+
+ QVERIFY(isSame(&model, expected));
+ }
+
+ void removeGroupInTheMiddleTest()
+ {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a2" }}, // to be removed
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }}, // to be removed
+ QJsonObject {{ "name", "C"}, { "subname", "c1" }}, // to be removed
+ QJsonObject {{ "name", "C"}, { "subname", "c2" }}, // to be removed
+ QJsonObject {{ "name", "C"}, { "subname", "c3" }},
+ QJsonObject {{ "name", "D"}, { "subname", "d1" }}
+ });
+
+ GroupingModel model;
+
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel);
+
+ QCOMPARE(model.rowCount(), 4);
+
+ auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+
+ SnapshotModel snapshot(model);
+
+ auto submodelRole = roleForName(roles, "submodel");
+
+ QPointer submodel1
+ = model.data(model.index(0, 0), submodelRole).value();
+ QPointer submodel2
+ = model.data(model.index(1, 0), submodelRole).value();
+ QPointer submodel3
+ = model.data(model.index(2, 0), submodelRole).value();
+ QPointer submodel4
+ = model.data(model.index(3, 0), submodelRole).value();
+
+ ListModelWrapper expected(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c3",
+ "submodel": [
+ { "name": "C", "subname": "c3" }
+ ]
+ },
+ {
+ "name": "D", "subname": "d1",
+ "submodel": [
+ { "name": "D", "subname": "d1" }
+ ]
+ }
+ ])");
+
+ ListModelWrapper expectedIntermediate1(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "submodel": [
+ { "name": "C", "subname": "c1" },
+ { "name": "C", "subname": "c2" },
+ { "name": "C", "subname": "c3" }
+ ]
+ },
+ {
+ "name": "D", "subname": "d1",
+ "submodel": [
+ { "name": "D", "subname": "d1" }
+ ]
+ }
+ ])");
+
+ ListModelWrapper expectedIntermediate2(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c3",
+ "submodel": [
+ { "name": "C", "subname": "c3" }
+ ]
+ },
+ {
+ "name": "D", "subname": "d1",
+ "submodel": [
+ { "name": "D", "subname": "d1" }
+ ]
+ }
+ ])");
+
+ QVERIFY(isSame(model, snapshot));
+
+ QObject context;
+ connect(submodel1, &QAbstractItemModel::rowsAboutToBeRemoved, &context,
+ [m = &model, s = &snapshot] {
+ QVERIFY(isSame(m, s));
+ });
+ connect(submodel1, &QAbstractItemModel::rowsRemoved, &context,
+ [m = &model, e = expectedIntermediate1.model()] {
+ QVERIFY(isSame(m, e));
+ });
+ connect(submodel3, &QAbstractItemModel::rowsAboutToBeRemoved, &context,
+ [m = &model, e = expectedIntermediate1.model()] {
+ QVERIFY(isSame(m, e));
+ });
+ connect(submodel3, &QAbstractItemModel::rowsRemoved, &context,
+ [m = &model, e = expectedIntermediate2.model()] {
+ QVERIFY(isSame(m, e));
+ });
+ connect(&model, &QAbstractItemModel::rowsAboutToBeRemoved, &context,
+ [m = &model, e = expectedIntermediate2.model()] {
+ QVERIFY(isSame(m, e));
+ });
+ connect(&model, &QAbstractItemModel::rowsRemoved, &context,
+ [m = &model, e = expected.model()] {
+ QVERIFY(isSame(m, e));
+ });
+
+ ModelSignalsSpy signalsSpy(&model);
+ ModelSignalsSpy submodel1Spy(submodel1);
+ ModelSignalsSpy submodel2Spy(submodel2);
+ ModelSignalsSpy submodel3Spy(submodel3);
+ ModelSignalsSpy submodel4Spy(submodel4);
+
+ sourceModel.remove(1, 4);
+ QCOMPARE(sourceModel.count(), 3);
+
+ QVERIFY(!submodel1.isNull());
+ QVERIFY(submodel2.isNull());
+ QVERIFY(!submodel3.isNull());
+ QVERIFY(!submodel4.isNull());
+
+ QCOMPARE(signalsSpy.count(), 2);
+ QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.count(), 1);
+ QCOMPARE(signalsSpy.rowsRemovedSpy.count(), 1);
+ QCOMPARE(submodel1Spy.count(), 2);
+ QCOMPARE(submodel1Spy.rowsAboutToBeRemovedSpy.count(), 1);
+ QCOMPARE(submodel1Spy.rowsRemovedSpy.count(), 1);
+ QCOMPARE(submodel2Spy.count(), 0);
+ QCOMPARE(submodel3Spy.count(), 2);
+ QCOMPARE(submodel3Spy.rowsAboutToBeRemovedSpy.count(), 1);
+ QCOMPARE(submodel3Spy.rowsRemovedSpy.count(), 1);
+ QCOMPARE(submodel4Spy.count(), 0);
+
+ QVERIFY(isSame(&model, expected));
+ }
+
+ void removeAllTest()
+ {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a2" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c1" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c2" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c3" }},
+ QJsonObject {{ "name", "D"}, { "subname", "d1" }}
+ });
+
+ GroupingModel model;
+
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel);
+
+ QCOMPARE(model.rowCount(), 4);
+
+ auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+
+ auto submodelRole = roleForName(roles, "submodel");
+
+ QPointer submodel1
+ = model.data(model.index(0, 0), submodelRole).value();
+ QPointer submodel2
+ = model.data(model.index(1, 0), submodelRole).value();
+ QPointer submodel3
+ = model.data(model.index(2, 0), submodelRole).value();
+ QPointer submodel4
+ = model.data(model.index(3, 0), submodelRole).value();
+
+ ModelSignalsSpy signalsSpy(&model);
+
+ sourceModel.remove(0, 7);
+ QCOMPARE(sourceModel.count(), 0);
+
+ QVERIFY(submodel1.isNull());
+ QVERIFY(submodel2.isNull());
+ QVERIFY(submodel3.isNull());
+ QVERIFY(submodel4.isNull());
+
+ QCOMPARE(signalsSpy.count(), 2);
+ QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.count(), 1);
+ QCOMPARE(signalsSpy.rowsRemovedSpy.count(), 1);
+ }
+
+ void removeManyAndMergeTest()
+ {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a2" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c1" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c2" }}, // to be removed
+ QJsonObject {{ "name", "D"}, { "subname", "d1" }}, // to be removed
+ QJsonObject {{ "name", "C"}, { "subname", "c3" }}, // to be removed
+ QJsonObject {{ "name", "C"}, { "subname", "c4" }}
+ });
+
+ GroupingModel model;
+
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel);
+
+ QCOMPARE(model.rowCount(), 5);
+
+ auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+
+ SnapshotModel snapshot(model);
+
+ auto submodelRole = roleForName(roles, "submodel");
+
+ QPointer submodel1
+ = model.data(model.index(0, 0), submodelRole).value();
+ QPointer submodel2
+ = model.data(model.index(1, 0), submodelRole).value();
+ QPointer submodel3
+ = model.data(model.index(2, 0), submodelRole).value();
+ QPointer submodel4
+ = model.data(model.index(3, 0), submodelRole).value();
+ QPointer submodel5
+ = model.data(model.index(4, 0), submodelRole).value();
+
+ ListModelWrapper expected(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "submodel": [
+ { "name": "C", "subname": "c1" },
+ { "name": "C", "subname": "c4" }
+ ]
+ }
+ ])");
+
+ ListModelWrapper expectedIntermediate1(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "submodel": [
+ { "name": "C", "subname": "c1" }
+ ]
+ },
+ {
+ "name": "D", "subname": "d1",
+ "submodel": [
+ { "name": "D", "subname": "d1" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c3",
+ "submodel": [
+ { "name": "C", "subname": "c3" },
+ { "name": "C", "subname": "c4" }
+ ]
+ }
+ ])");
+
+ ListModelWrapper expectedIntermediate2(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "submodel": [
+ { "name": "C", "subname": "c1" },
+ ]
+ }
+ ])");
+
+ QObject context;
+ connect(submodel3, &QAbstractItemModel::rowsAboutToBeRemoved, &context,
+ [m = &model, s = &snapshot] {
+ QVERIFY(isSame(m, s));
+ });
+ connect(submodel3, &QAbstractItemModel::rowsRemoved, &context,
+ [m = &model, e = expectedIntermediate1.model()] {
+ QVERIFY(isSame(m, e));
+ });
+ connect(&model, &QAbstractItemModel::rowsAboutToBeRemoved, &context,
+ [m = &model, e = expectedIntermediate1.model()] {
+ QVERIFY(isSame(m, e));
+ });
+ connect(&model, &QAbstractItemModel::rowsRemoved, &context,
+ [m = &model, e = expectedIntermediate2.model()] {
+ QVERIFY(isSame(m, e));
+ });
+ connect(submodel3, &QAbstractItemModel::rowsAboutToBeInserted, &context,
+ [m = &model, e = expectedIntermediate2.model()] {
+ QVERIFY(isSame(m, e));
+ });
+ connect(submodel3, &QAbstractItemModel::rowsInserted, &context,
+ [m = &model, e = expected.model()] {
+ QVERIFY(isSame(m, e));
+ });
+
+ ModelSignalsSpy signalsSpy(&model);
+ ModelSignalsSpy submodel1Spy(submodel1);
+ ModelSignalsSpy submodel2Spy(submodel2);
+ ModelSignalsSpy submodel3Spy(submodel3);
+ ModelSignalsSpy submodel4Spy(submodel4);
+ ModelSignalsSpy submodel5Spy(submodel5);
+
+ sourceModel.remove(4, 3);
+
+ QCOMPARE(model.rowCount(), 3);
+
+ QVERIFY(!submodel1.isNull());
+ QVERIFY(!submodel2.isNull());
+ QVERIFY(!submodel3.isNull());
+ QVERIFY(submodel4.isNull());
+ QVERIFY(submodel5.isNull());
+
+ QCOMPARE(signalsSpy.count(), 2);
+ QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.count(), 1);
+ QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(1), 3);
+ QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(2), 4);
+ QCOMPARE(signalsSpy.rowsRemovedSpy.count(), 1);
+
+ QCOMPARE(submodel1Spy.count(), 0);
+ QCOMPARE(submodel2Spy.count(), 0);
+
+ QCOMPARE(submodel3Spy.count(), 4);
+ QCOMPARE(submodel3Spy.rowsAboutToBeRemovedSpy.count(), 1);
+ QCOMPARE(submodel3Spy.rowsAboutToBeRemovedSpy.at(0).at(1), 1);
+ QCOMPARE(submodel3Spy.rowsAboutToBeRemovedSpy.at(0).at(2), 1);
+ QCOMPARE(submodel3Spy.rowsRemovedSpy.count(), 1);
+ QCOMPARE(submodel3Spy.rowsAboutToBeInsertedSpy.count(), 1);
+ QCOMPARE(submodel3Spy.rowsAboutToBeInsertedSpy.at(0).at(1), 1);
+ QCOMPARE(submodel3Spy.rowsAboutToBeInsertedSpy.at(0).at(2), 1);
+ QCOMPARE(submodel3Spy.rowsInsertedSpy.count(), 1);
+
+ QCOMPARE(submodel4Spy.count(), 0);
+ QCOMPARE(submodel5Spy.count(), 0);
+
+ QVERIFY(isSame(&model, expected));
+ }
+
+ void removeSingleAndMergeTest()
+ {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a2" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c1" }},
+ QJsonObject {{ "name", "D"}, { "subname", "d1" }}, // to be removed
+ QJsonObject {{ "name", "C"}, { "subname", "c2" }}
+ });
+
+ GroupingModel model;
+
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel);
+
+ QCOMPARE(model.rowCount(), 5);
+
+ auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+
+ SnapshotModel snapshot(model);
+
+ auto submodelRole = roleForName(roles, "submodel");
+
+ QPointer submodel1
+ = model.data(model.index(0, 0), submodelRole).value();
+ QPointer submodel2
+ = model.data(model.index(1, 0), submodelRole).value();
+ QPointer submodel3
+ = model.data(model.index(2, 0), submodelRole).value();
+ QPointer submodel4
+ = model.data(model.index(3, 0), submodelRole).value();
+ QPointer submodel5
+ = model.data(model.index(4, 0), submodelRole).value();
+
+ ListModelWrapper expected(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "submodel": [
+ { "name": "C", "subname": "c1" },
+ { "name": "C", "subname": "c2" }
+ ]
+ }
+ ])");
+
+ ListModelWrapper expectedIntermediate(engine, R"([
+ {
+ "name": "A", "subname": "a1",
+ "submodel": [
+ { "name": "A", "subname": "a1" },
+ { "name": "A", "subname": "a2" }
+ ]
+ },
+ {
+ "name": "B", "subname": "b1",
+ "submodel": [
+ { "name": "B", "subname": "b1" }
+ ]
+ },
+ {
+ "name": "C", "subname": "c1",
+ "submodel": [
+ { "name": "C", "subname": "c1" }
+ ]
+ }
+ ])");
+
+ QObject context;
+ connect(&model, &QAbstractItemModel::rowsAboutToBeRemoved, &context,
+ [m = &model, s = &snapshot] {
+ QVERIFY(isSame(m, s));
+ });
+ connect(&model, &QAbstractItemModel::rowsRemoved, &context,
+ [m = &model, e = expectedIntermediate.model()] {
+ QVERIFY(isSame(m, e));
+ });
+ connect(submodel3, &QAbstractItemModel::rowsAboutToBeInserted, &context,
+ [m = &model, e = expectedIntermediate.model()] {
+ QVERIFY(isSame(m, e));
+ });
+ connect(submodel3, &QAbstractItemModel::rowsInserted, &context,
+ [m = &model, e = expected.model()] {
+ QVERIFY(isSame(m, e));
+ });
+
+ ModelSignalsSpy signalsSpy(&model);
+ ModelSignalsSpy submodel1Spy(submodel1);
+ ModelSignalsSpy submodel2Spy(submodel2);
+ ModelSignalsSpy submodel3Spy(submodel3);
+ ModelSignalsSpy submodel4Spy(submodel4);
+ ModelSignalsSpy submodel5Spy(submodel5);
+
+ sourceModel.remove(4, 1);
+
+ QCOMPARE(model.rowCount(), 3);
+
+ QVERIFY(!submodel1.isNull());
+ QVERIFY(!submodel2.isNull());
+ QVERIFY(!submodel3.isNull());
+ QVERIFY(submodel4.isNull());
+ QVERIFY(submodel5.isNull());
+
+ QCOMPARE(signalsSpy.count(), 2);
+ QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.count(), 1);
+ QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(1), 3);
+ QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(2), 4);
+ QCOMPARE(signalsSpy.rowsRemovedSpy.count(), 1);
+
+ QCOMPARE(submodel1Spy.count(), 0);
+ QCOMPARE(submodel2Spy.count(), 0);
+
+ QCOMPARE(submodel3Spy.count(), 2);
+ QCOMPARE(submodel3Spy.rowsAboutToBeInsertedSpy.count(), 1);
+ QCOMPARE(submodel3Spy.rowsAboutToBeInsertedSpy.at(0).at(1), 1);
+ QCOMPARE(submodel3Spy.rowsAboutToBeInsertedSpy.at(0).at(2), 1);
+ QCOMPARE(submodel3Spy.rowsInsertedSpy.count(), 1);
+ QCOMPARE(submodel4Spy.count(), 0);
+ QCOMPARE(submodel5Spy.count(), 0);
+
+ QVERIFY(isSame(&model, expected));
+ }
+
+ void submodelsDeletionOnDestructionTest() {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a2" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c1" }}
+ });
+
+ QPointer submodel1, submodel2, submodel3;
+
+ {
+ GroupingModel model;
+
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel);
+
+ QCOMPARE(model.rowCount(), 3);
+
+ auto roles = model.roleNames();
+ auto submodelRole = roleForName(roles, "submodel");
+
+ submodel1 = model.data(model.index(0, 0), submodelRole).value();
+ submodel2 = model.data(model.index(1, 0), submodelRole).value();
+ submodel3 = model.data(model.index(2, 0), submodelRole).value();
+
+ QVERIFY(!submodel1.isNull());
+ QVERIFY(!submodel2.isNull());
+ QVERIFY(!submodel3.isNull());
+ }
+
+ QVERIFY(submodel1.isNull());
+ QVERIFY(submodel2.isNull());
+ QVERIFY(submodel3.isNull());
+ }
+
+ void submodelsDeletionOnResetTest() {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel(engine, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a2" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c1" }}
+ });
+
+ QPointer submodel1, submodel2, submodel3;
+
+ GroupingModel model;
+
+ model.setGroupingRoleName("name");
+ model.setSourceModel(sourceModel);
+
+ QCOMPARE(model.rowCount(), 3);
+
+ auto roles = model.roleNames();
+ auto submodelRole = roleForName(roles, "submodel");
+
+ submodel1 = model.data(model.index(0, 0), submodelRole).value();
+ submodel2 = model.data(model.index(1, 0), submodelRole).value();
+ submodel3 = model.data(model.index(2, 0), submodelRole).value();
+
+ QVERIFY(!submodel1.isNull());
+ QVERIFY(!submodel2.isNull());
+ QVERIFY(!submodel3.isNull());
+
+ model.setSourceModel(nullptr);
+
+ QVERIFY(submodel1.isNull());
+ QVERIFY(submodel2.isNull());
+ QVERIFY(submodel3.isNull());
+
+ QCOMPARE(model.rowCount(), 0);
+ }
+
+ void submodelResetTest() {
+ QQmlEngine engine;
+
+ ListModelWrapper sourceModel1(engine, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "subname", "a1" }},
+ QJsonObject {{ "name", "A"}, { "subname", "a2" }},
+ QJsonObject {{ "name", "B"}, { "subname", "b1" }},
+ QJsonObject {{ "name", "C"}, { "subname", "c1" }}
+ });
+
+ ListModelWrapper sourceModel2(engine, QJsonArray {
+ QJsonObject {{ "name", "A"}, { "other", "a1" }},
+ QJsonObject {{ "name", "A"}, { "other", "a2" }},
+ QJsonObject {{ "name", "B"}, { "other", "b1" }},
+ QJsonObject {{ "name", "C"}, { "other", "c1" }},
+ QJsonObject {{ "name", "C"}, { "other", "c2" }},
+ QJsonObject {{ "name", "D"}, { "other", "d1" }}
+ });
+
+ auto contains = [](auto roles, auto name) {
+ return std::find(roles.cbegin(), roles.cend(), name) != roles.cend();
+ };
+
+ QIdentityProxyModel identity;
+ identity.setSourceModel(sourceModel1);
+
+ GroupingModel model;
+ model.setGroupingRoleName("name");
+ model.setSourceModel(&identity);
+
+ {
+ const auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+ QVERIFY(contains(roles, "name"));
+ QVERIFY(contains(roles, "subname"));
+ QVERIFY(contains(roles, "submodel"));
+ }
+
+ QCOMPARE(model.rowCount(), 3);
+
+ ModelSignalsSpy signalsSpy(&model);
+ identity.setSourceModel(sourceModel2);
+
+ {
+ const auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 3);
+ QVERIFY(contains(roles, "name"));
+ QVERIFY(contains(roles, "other"));
+ QVERIFY(contains(roles, "submodel"));
+ }
+
+ QCOMPARE(signalsSpy.count(), 2);
+ QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1);
+ QCOMPARE(signalsSpy.modelResetSpy.count(), 1);
+
+ QCOMPARE(model.rowCount(), 4);
+
+ identity.setSourceModel(nullptr);
+
+ {
+ const auto roles = model.roleNames();
+ QCOMPARE(roles.size(), 0);
+ }
+
+ QCOMPARE(signalsSpy.count(), 4);
+ QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 2);
+ QCOMPARE(signalsSpy.modelResetSpy.count(), 2);
+
+ QCOMPARE(model.rowCount(), 0);
+
+ model.setSourceModel(nullptr);
+
+ ModelSignalsSpy signalsSpy2(&model);
+ identity.setSourceModel(sourceModel2);
+ QCOMPARE(signalsSpy2.count(), 0);
+ }
+};
+
+QTEST_MAIN(TestGroupingModel)
+#include "tst_GroupingModel.moc"