feat(StatusQ): GroupingModel for grouping rows with the same key into
submodels Closes: #12683
This commit is contained in:
parent
f038d83281
commit
a2b8eb333d
|
@ -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: "<b>Description</b>: 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
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
#pragma once
|
||||
|
||||
#include <QAbstractProxyModel>
|
||||
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
|
||||
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<int, QByteArray> 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<Entry> m_entries;
|
||||
std::vector<std::unique_ptr<RangeModel>> m_submodels;
|
||||
|
||||
RangeModel* m_pendingMergeSubmodel = nullptr;
|
||||
RangeModel* m_pendingRemovalSubmodel = nullptr;
|
||||
|
||||
QHash<int, QByteArray> m_roleNames;
|
||||
QString m_groupingRoleName;
|
||||
QString m_submodelRoleName = s_defaultSubmodelRoleName;
|
||||
|
||||
std::optional<int> m_groupingRole;
|
||||
|
||||
int m_submodelRole = -1;
|
||||
bool m_rolesInitialized = false;
|
||||
|
||||
friend class RangeModel;
|
||||
};
|
|
@ -0,0 +1,684 @@
|
|||
#include "StatusQ/groupingmodel.h"
|
||||
|
||||
#include <iterator>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
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<GroupingModel::Entry>& 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<int, QByteArray> 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<GroupingModel::Entry>& 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<int, QByteArray> 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<std::pair<int, int>> 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<RangeModel>(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<QVariant> previousGroupingValue;
|
||||
|
||||
if (first - 1 >= 0)
|
||||
previousGroupingValue = ::data(model, first - 1, *m_groupingRole);
|
||||
|
||||
std::optional<QVariant> 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<std::pair<int, int>> 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<std::unique_ptr<RangeModel>> newSubmodels;
|
||||
|
||||
std::transform(pairs.cbegin(), pairs.cend(), std::back_inserter(newSubmodels),
|
||||
[this] (auto& entry) {
|
||||
return std::make_unique<RangeModel>(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<std::tuple<RangeModel*, int, int>> 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<int>& 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<int>& 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"
|
|
@ -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<ManageTokensController>("StatusQ.Models", 0, 1, "ManageTokensController");
|
||||
qmlRegisterType<ManageTokensModel>("StatusQ.Models", 0, 1, "ManageTokensModel");
|
||||
|
||||
qmlRegisterType<GroupingModel>("StatusQ", 0, 1, "GroupingModel");
|
||||
qmlRegisterType<SourceModel>("StatusQ", 0, 1, "SourceModel");
|
||||
qmlRegisterType<ConcatModel>("StatusQ", 0, 1, "ConcatModel");
|
||||
qmlRegisterType<MovableModel>("StatusQ", 0, 1, "MovableModel");
|
||||
|
|
|
@ -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)
|
||||
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)
|
||||
|
|
|
@ -29,6 +29,8 @@ ListModelWrapper::ListModelWrapper(QQmlEngine& engine, const QString& content)
|
|||
|
||||
m_model.reset(qobject_cast<QAbstractItemModel*>(
|
||||
component.create(engine.rootContext())));
|
||||
|
||||
Q_ASSERT_X(m_model, "ListModelWrapper", "creating model failed!");
|
||||
}
|
||||
|
||||
ListModelWrapper::ListModelWrapper(QQmlEngine& engine, const QJsonArray& content)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue