-
Notifications
You must be signed in to change notification settings - Fork 55
feat: introduce RoleGroupModel #1124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,255 @@ | ||
| // SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. | ||
| // | ||
| // SPDX-License-Identifier: GPL-3.0-or-later | ||
|
|
||
| #include "rolegroupmodel.h" | ||
|
|
||
| #include <QList> | ||
|
|
||
| RoleGroupModel::RoleGroupModel(QAbstractItemModel *sourceModel, int role, QObject *parent) | ||
| : QAbstractProxyModel(parent) | ||
| , m_roleForDeduplication(role) | ||
| { | ||
| RoleGroupModel::setSourceModel(sourceModel); | ||
| } | ||
|
|
||
| void RoleGroupModel::setDeduplicationRole(const int &role) | ||
| { | ||
| if (role != m_roleForDeduplication) { | ||
| m_roleForDeduplication = role; | ||
| rebuildTreeSource(); | ||
| } | ||
| } | ||
|
|
||
| int RoleGroupModel::deduplicationRole() const | ||
| { | ||
| return m_roleForDeduplication; | ||
| } | ||
|
|
||
| void RoleGroupModel::setSourceModel(QAbstractItemModel *model) | ||
| { | ||
| if (sourceModel()) { | ||
| sourceModel()->disconnect(this); | ||
| } | ||
|
|
||
| QAbstractProxyModel::setSourceModel(model); | ||
|
|
||
| rebuildTreeSource(); | ||
| if (sourceModel() == nullptr) | ||
| return; | ||
|
|
||
| connect(sourceModel(), &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) { | ||
| adjustMap(first, (last - first) + 1); | ||
|
|
||
| for (int i = first; i <= last; i++) { | ||
| auto sourceIndex = sourceModel()->index(i, 0); | ||
| auto data = sourceIndex.data(m_roleForDeduplication).toString(); | ||
| if (data.isEmpty()) { | ||
| continue; | ||
| } | ||
|
|
||
| auto list = m_map.value(data, nullptr); | ||
| if (nullptr == list) { | ||
| beginInsertRows(QModelIndex(), m_rowMap.size(), m_rowMap.size()); | ||
| list = new QList<int>(); | ||
| m_map.insert(data, list); | ||
| m_rowMap.append(list); | ||
| endInsertRows(); | ||
| } | ||
| beginInsertRows(index(m_rowMap.indexOf(list), 0), list->size(), list->size()); | ||
| list->append(i); | ||
| endInsertRows(); | ||
| } | ||
| }); | ||
|
|
||
| connect(sourceModel(), &QAbstractItemModel::rowsRemoved, this, [this](const QModelIndex &parent, int first, int last) { | ||
| for (int i = 0; i < m_rowMap.count(); ++i) { | ||
| auto sourceRows = m_rowMap.value(i); | ||
| for (int j = 0; j < sourceRows->size(); ++j) { | ||
| if (first <= sourceRows->value(j) && last >= sourceRows->value(j)) { | ||
| beginRemoveRows(index(m_rowMap.indexOf(sourceRows), 0), j, j); | ||
| sourceRows->removeAt(j); | ||
| endRemoveRows(); | ||
| --j; | ||
| } | ||
| } | ||
| if (sourceRows->size() == 0) { | ||
| beginRemoveRows(QModelIndex(), m_rowMap.indexOf(sourceRows), m_rowMap.indexOf(sourceRows)); | ||
| m_map.remove(m_map.key(sourceRows)); | ||
| m_rowMap.removeOne(sourceRows); | ||
| delete sourceRows; | ||
| endRemoveRows(); | ||
| } | ||
| } | ||
| adjustMap(first, -((last - first) + 1)); | ||
| }); | ||
|
|
||
| connect(sourceModel(), &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles) { | ||
| // TODO: if roles contains m_roleForDeduplication, need update from topLeft 2 bottomRight or just send dataChanged | ||
| if (roles.contains(m_roleForDeduplication)) { | ||
| rebuildTreeSource(); | ||
| return; | ||
| } | ||
|
|
||
| for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { | ||
| auto data = sourceModel()->index(i, 0).data(m_roleForDeduplication).toString(); | ||
| auto list = m_map.value(data, nullptr); | ||
| if (list == nullptr) | ||
| continue; | ||
|
|
||
| auto index = createIndex(list->indexOf(i), 0, m_rowMap.indexOf(list)); | ||
| Q_EMIT dataChanged(index, index, roles); | ||
| } | ||
| }); | ||
|
|
||
| connect(sourceModel(), &QAbstractItemModel::modelReset, this, [this]() { | ||
| rebuildTreeSource(); | ||
| }); | ||
| } | ||
|
|
||
| int RoleGroupModel::rowCount(const QModelIndex &parent) const | ||
| { | ||
| if (parent.isValid()) { | ||
| auto list = m_rowMap.value(parent.row(), nullptr); | ||
| return nullptr == list ? 0 : list->size(); | ||
| } | ||
|
|
||
| return m_rowMap.size(); | ||
| } | ||
|
|
||
| int RoleGroupModel::columnCount(const QModelIndex &parent) const | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): columnCount() should check for a valid sourceModel pointer. Add a null check for sourceModel() and return 0 if it’s unset to prevent a crash. |
||
| { | ||
| if (sourceModel()) { | ||
| return sourceModel()->columnCount(parent); | ||
| } | ||
| return 0; | ||
| } | ||
|
|
||
| QHash<int, QByteArray> RoleGroupModel::roleNames() const | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): roleNames() should check for a valid sourceModel pointer. Add a null check and return an empty QHash if sourceModel() is null. |
||
| { | ||
| if (sourceModel()) { | ||
| return sourceModel()->roleNames(); | ||
| } | ||
| return QAbstractProxyModel::roleNames(); | ||
| } | ||
|
|
||
| QVariant RoleGroupModel::data(const QModelIndex &index, int role) const | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: data() should validate the index and sourceModel pointer. Return a default QVariant when the index is invalid or sourceModel() is null to prevent crashes. |
||
| { | ||
| auto parentPos = static_cast<int>(index.internalId()); | ||
| auto list = m_rowMap.value(index.row(), nullptr); | ||
| if (parentPos == -1) { | ||
| if (nullptr == list || list->size() == 0) { | ||
| return QVariant(); | ||
| } | ||
| return sourceModel()->index(list->first(), 0).data(role); | ||
| } | ||
|
|
||
| list = m_rowMap.value(parentPos); | ||
| if (list == nullptr) { | ||
| return QVariant(); | ||
| } | ||
|
|
||
| return sourceModel()->index(list->value(index.row()), 0).data(role); | ||
| } | ||
|
|
||
| QModelIndex RoleGroupModel::index(int row, int column, const QModelIndex &parent) const | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): index() should validate row and column bounds. Check that row and column fall within valid ranges to avoid out-of-bounds indexes. |
||
| { | ||
| auto list = m_map.value(parent.data(m_roleForDeduplication).toString(), nullptr); | ||
| if (parent.isValid() && nullptr != list) { | ||
| return createIndex(row, column, m_rowMap.indexOf(list)); | ||
| } | ||
|
|
||
| return createIndex(row, column, -1); | ||
| } | ||
|
|
||
| QModelIndex RoleGroupModel::parent(const QModelIndex &child) const | ||
| { | ||
| auto pos = static_cast<int>(child.internalId()); | ||
| if (pos == -1) | ||
| return QModelIndex(); | ||
|
|
||
| return createIndex(pos, 0); | ||
| } | ||
|
|
||
| QModelIndex RoleGroupModel::mapToSource(const QModelIndex &proxyIndex) const | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: mapToSource() should validate proxyIndex and sourceModel pointer. Return a default QModelIndex when proxyIndex is invalid or sourceModel() is null to prevent crashes. |
||
| { | ||
| auto parentIndex = proxyIndex.parent(); | ||
| QList<int> *list = nullptr; | ||
|
|
||
| if (parentIndex.isValid()) { | ||
| list = m_rowMap.value(parentIndex.row()); | ||
| } else { | ||
| list = m_map.value(proxyIndex.data(m_roleForDeduplication).toString()); | ||
| } | ||
|
|
||
| if (nullptr == list) | ||
| return QModelIndex(); | ||
|
|
||
| if (parentIndex.isValid()) { | ||
| return sourceModel()->index(list->value(proxyIndex.row()), 0); | ||
| } | ||
|
|
||
| return sourceModel()->index(list->value(0), 0); | ||
| } | ||
|
|
||
| QModelIndex RoleGroupModel::mapFromSource(const QModelIndex &sourceIndex) const | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): mapFromSource() should validate sourceIndex and sourceModel pointer. Return a default QModelIndex if sourceIndex is invalid or sourceModel() is null to avoid crashes. |
||
| { | ||
| auto data = sourceIndex.data(m_roleForDeduplication).toString(); | ||
| if (data.isEmpty()) { | ||
| return QModelIndex(); | ||
| } | ||
|
|
||
| auto list = m_map.value(data, nullptr); | ||
| if (nullptr == list || 0 == list->size()) { | ||
| return {}; | ||
| } | ||
|
|
||
| if (sourceIndex.row() == list->first()) { | ||
| return createIndex(m_rowMap.indexOf(list), 0, -1); | ||
| } | ||
|
|
||
| auto pos = list->indexOf(sourceIndex.row()); | ||
| return createIndex(pos, 0, m_rowMap.indexOf(list)); | ||
| } | ||
|
|
||
| void RoleGroupModel::rebuildTreeSource() | ||
| { | ||
| beginResetModel(); | ||
| qDeleteAll(m_rowMap); | ||
| m_map.clear(); | ||
| m_rowMap.clear(); | ||
|
|
||
| if (sourceModel() == nullptr) { | ||
| endResetModel(); | ||
| return; | ||
| } | ||
|
|
||
| for (int i = 0; i < sourceModel()->rowCount(); i++) { | ||
| auto index = sourceModel()->index(i, 0); | ||
| auto data = index.data(m_roleForDeduplication).toString(); | ||
| if (data.isEmpty()) { | ||
| continue; | ||
| } | ||
|
|
||
| auto list = m_map.value(data, nullptr); | ||
| if (nullptr == list) { | ||
| list = new QList<int>(); | ||
| m_map.insert(data, list); | ||
| m_rowMap.append(list); | ||
| } | ||
| list->append(i); | ||
| } | ||
| endResetModel(); | ||
| } | ||
|
|
||
| void RoleGroupModel::adjustMap(int base, int offset) | ||
| { | ||
| for (int i = 0; i < m_rowMap.count(); ++i) { | ||
| auto sourceRows = m_rowMap.value(i); | ||
| for (int j = 0; j < sourceRows->size(); ++j) { | ||
| if (sourceRows->value(j) < base) | ||
| continue; | ||
| (*sourceRows)[j] += offset; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| // SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. | ||
| // | ||
| // SPDX-License-Identifier: GPL-3.0-or-later | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <QAbstractProxyModel> | ||
|
|
||
| class RoleGroupModel : public QAbstractProxyModel | ||
| { | ||
| Q_OBJECT | ||
|
|
||
| public: | ||
| explicit RoleGroupModel(QAbstractItemModel *sourceModel, int role, QObject *parent = nullptr); | ||
| void setSourceModel(QAbstractItemModel *sourceModel) override; | ||
|
|
||
| void setDeduplicationRole(const int &role); | ||
| int deduplicationRole() const; | ||
|
|
||
| QHash<int, QByteArray> roleNames() const override; | ||
| int rowCount(const QModelIndex &parent = QModelIndex()) const override; | ||
| int columnCount(const QModelIndex &parent = QModelIndex()) const override; | ||
| QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; | ||
|
Check warning on line 23 in panels/dock/taskmanager/rolegroupmodel.h
|
||
|
|
||
| bool hasIndex(int row, int column, const QModelIndex &parent = QModelIndex()) const; | ||
| Q_INVOKABLE virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; | ||
| Q_INVOKABLE virtual QModelIndex parent(const QModelIndex &child) const override; | ||
|
|
||
| QModelIndex mapToSource(const QModelIndex &proxyIndex) const override; | ||
| QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override; | ||
|
|
||
| Q_SIGNALS: | ||
| void deduplicationRoleChanged(int role); | ||
|
|
||
| private: | ||
| void rebuildTreeSource(); | ||
| void adjustMap(int base, int offset); | ||
|
|
||
| private: | ||
| int m_roleForDeduplication; | ||
|
|
||
| // for order | ||
| QList<QList<int> *> m_rowMap; | ||
|
|
||
| // data 2 source row | ||
| QHash<QString, QList<int> *> m_map; | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): Disconnecting all signals from the previous source model may unintentionally break other connections.
disconnect(this) removes all sourceModel→this connections, risking other slots. Instead, disconnect only the signals you connected or store QMetaObject::Connection objects to manage them.