diff --git a/panels/dock/tray/CMakeLists.txt b/panels/dock/tray/CMakeLists.txt index b5fc161c4..bc46d9fb3 100644 --- a/panels/dock/tray/CMakeLists.txt +++ b/panels/dock/tray/CMakeLists.txt @@ -23,6 +23,10 @@ qt_add_qml_module(dock-tray trayitempositionmanager.h ksortfilterproxymodel.cpp ksortfilterproxymodel.h + kextracolumnsproxymodel.cpp + kextracolumnsproxymodel.h + listtotableproxymodel.cpp + listtotableproxymodel.h QML_FILES SurfacePopup.qml SurfaceSubPopup.qml diff --git a/panels/dock/tray/kextracolumnsproxymodel.cpp b/panels/dock/tray/kextracolumnsproxymodel.cpp new file mode 100644 index 000000000..2faedab20 --- /dev/null +++ b/panels/dock/tray/kextracolumnsproxymodel.cpp @@ -0,0 +1,349 @@ +/* + SPDX-FileCopyrightText: 2015 Klarälvdalens Datakonsult AB, a KDAB Group company + SPDX-FileContributor: David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later + + Source code extracted from KItemModels with minimized changes. +*/ + +#include "kextracolumnsproxymodel.h" + +#include + +class KExtraColumnsProxyModelPrivate +{ + Q_DECLARE_PUBLIC(KExtraColumnsProxyModel) + KExtraColumnsProxyModel *const q_ptr; + +public: + KExtraColumnsProxyModelPrivate(KExtraColumnsProxyModel *model) + : q_ptr(model) + { + } + + void _ec_sourceLayoutAboutToBeChanged(const QList &sourceParents, QAbstractItemModel::LayoutChangeHint hint); + void _ec_sourceLayoutChanged(const QList &sourceParents, QAbstractItemModel::LayoutChangeHint hint); + + // Configuration (doesn't change once source model is plugged in) + QVector m_extraHeaders; + + // for layoutAboutToBeChanged/layoutChanged + QVector layoutChangePersistentIndexes; + QVector layoutChangeProxyColumns; + QModelIndexList proxyIndexes; +}; + +KExtraColumnsProxyModel::KExtraColumnsProxyModel(QObject *parent) + : QIdentityProxyModel(parent) + , d_ptr(new KExtraColumnsProxyModelPrivate(this)) +{ +} + +KExtraColumnsProxyModel::~KExtraColumnsProxyModel() +{ +} + +void KExtraColumnsProxyModel::appendColumn(const QString &header) +{ + Q_D(KExtraColumnsProxyModel); + d->m_extraHeaders.append(header); +} + +void KExtraColumnsProxyModel::setExtraColumnTitle(int idx, QString title) +{ + Q_D(KExtraColumnsProxyModel); + d->m_extraHeaders[idx] = title; +} + +void KExtraColumnsProxyModel::removeExtraColumn(int idx) +{ + Q_D(KExtraColumnsProxyModel); + d->m_extraHeaders.remove(idx); +} + +bool KExtraColumnsProxyModel::setExtraColumnData(const QModelIndex &parent, int row, int extraColumn, const QVariant &data, int role) +{ + Q_UNUSED(parent); + Q_UNUSED(row); + Q_UNUSED(extraColumn); + Q_UNUSED(data); + Q_UNUSED(role); + return false; +} + +void KExtraColumnsProxyModel::extraColumnDataChanged(const QModelIndex &parent, int row, int extraColumn, const QVector &roles) +{ + const QModelIndex idx = index(row, proxyColumnForExtraColumn(extraColumn), parent); + Q_EMIT dataChanged(idx, idx, roles); +} + +void KExtraColumnsProxyModel::setSourceModel(QAbstractItemModel *model) +{ + if (sourceModel()) { + disconnect(sourceModel(), + SIGNAL(layoutAboutToBeChanged(QList, QAbstractItemModel::LayoutChangeHint)), + this, + SLOT(_ec_sourceLayoutAboutToBeChanged(QList, QAbstractItemModel::LayoutChangeHint))); + disconnect(sourceModel(), + SIGNAL(layoutChanged(QList, QAbstractItemModel::LayoutChangeHint)), + this, + SLOT(_ec_sourceLayoutChanged(QList, QAbstractItemModel::LayoutChangeHint))); + } + + QIdentityProxyModel::setSourceModel(model); + + if (model) { + // The handling of persistent model indexes assumes mapToSource can be called for any index + // This breaks for the extra column, so we'll have to do it ourselves + disconnect(model, + SIGNAL(layoutAboutToBeChanged(QList, QAbstractItemModel::LayoutChangeHint)), + this, + SLOT(_q_sourceLayoutAboutToBeChanged(QList, QAbstractItemModel::LayoutChangeHint))); + disconnect(model, + SIGNAL(layoutChanged(QList, QAbstractItemModel::LayoutChangeHint)), + this, + SLOT(_q_sourceLayoutChanged(QList, QAbstractItemModel::LayoutChangeHint))); + connect(model, + SIGNAL(layoutAboutToBeChanged(QList, QAbstractItemModel::LayoutChangeHint)), + this, + SLOT(_ec_sourceLayoutAboutToBeChanged(QList, QAbstractItemModel::LayoutChangeHint))); + connect(model, + SIGNAL(layoutChanged(QList, QAbstractItemModel::LayoutChangeHint)), + this, + SLOT(_ec_sourceLayoutChanged(QList, QAbstractItemModel::LayoutChangeHint))); + } +} + +QModelIndex KExtraColumnsProxyModel::mapToSource(const QModelIndex &proxyIndex) const +{ + if (!proxyIndex.isValid()) { // happens in e.g. rowCount(mapToSource(parent)) + return QModelIndex(); + } + const int column = proxyIndex.column(); + if (column >= sourceModel()->columnCount()) { + qDebug() << "Returning invalid index in mapToSource"; + return QModelIndex(); + } + return QIdentityProxyModel::mapToSource(proxyIndex); +} + +QModelIndex KExtraColumnsProxyModel::buddy(const QModelIndex &proxyIndex) const +{ + const int column = proxyIndex.column(); + if (column >= sourceModel()->columnCount()) { + return proxyIndex; + } + return QIdentityProxyModel::buddy(proxyIndex); +} + +QModelIndex KExtraColumnsProxyModel::sibling(int row, int column, const QModelIndex &idx) const +{ + if (row == idx.row() && column == idx.column()) { + return idx; + } + return index(row, column, parent(idx)); +} + +QItemSelection KExtraColumnsProxyModel::mapSelectionToSource(const QItemSelection &selection) const +{ + QItemSelection sourceSelection; + + if (!sourceModel()) { + return sourceSelection; + } + + // mapToSource will give invalid index for our additional columns, so truncate the selection + // to the columns known by the source model + const int sourceColumnCount = sourceModel()->columnCount(); + QItemSelection::const_iterator it = selection.constBegin(); + const QItemSelection::const_iterator end = selection.constEnd(); + for (; it != end; ++it) { + Q_ASSERT(it->model() == this); + QModelIndex topLeft = it->topLeft(); + Q_ASSERT(topLeft.isValid()); + Q_ASSERT(topLeft.model() == this); + topLeft = topLeft.sibling(topLeft.row(), 0); + QModelIndex bottomRight = it->bottomRight(); + Q_ASSERT(bottomRight.isValid()); + Q_ASSERT(bottomRight.model() == this); + if (bottomRight.column() >= sourceColumnCount) { + bottomRight = bottomRight.sibling(bottomRight.row(), sourceColumnCount - 1); + } + // This can lead to duplicate source indexes, so use merge(). + const QItemSelectionRange range(mapToSource(topLeft), mapToSource(bottomRight)); + QItemSelection newSelection; + newSelection << range; + sourceSelection.merge(newSelection, QItemSelectionModel::Select); + } + + return sourceSelection; +} + +int KExtraColumnsProxyModel::columnCount(const QModelIndex &parent) const +{ + Q_D(const KExtraColumnsProxyModel); + return QIdentityProxyModel::columnCount(parent) + d->m_extraHeaders.count(); +} + +QVariant KExtraColumnsProxyModel::data(const QModelIndex &index, int role) const +{ + Q_D(const KExtraColumnsProxyModel); + const int extraCol = extraColumnForProxyColumn(index.column()); + if (extraCol >= 0 && !d->m_extraHeaders.isEmpty()) { + return extraColumnData(index.parent(), index.row(), extraCol, role); + } + return sourceModel()->data(mapToSource(index), role); +} + +bool KExtraColumnsProxyModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + Q_D(const KExtraColumnsProxyModel); + const int extraCol = extraColumnForProxyColumn(index.column()); + if (extraCol >= 0 && !d->m_extraHeaders.isEmpty()) { + return setExtraColumnData(index.parent(), index.row(), extraCol, value, role); + } + return sourceModel()->setData(mapToSource(index), value, role); +} + +Qt::ItemFlags KExtraColumnsProxyModel::flags(const QModelIndex &index) const +{ + const int extraCol = extraColumnForProxyColumn(index.column()); + if (extraCol >= 0) { + // extra columns are readonly + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; + } + return sourceModel() != nullptr ? sourceModel()->flags(mapToSource(index)) : Qt::NoItemFlags; +} + +bool KExtraColumnsProxyModel::hasChildren(const QModelIndex &index) const +{ + if (index.column() > 0) { + return false; + } + return QIdentityProxyModel::hasChildren(index); +} + +QVariant KExtraColumnsProxyModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + Q_D(const KExtraColumnsProxyModel); + if (orientation == Qt::Horizontal) { + const int extraCol = extraColumnForProxyColumn(section); + if (extraCol >= 0) { + // Only text is supported, in headers for extra columns + if (role == Qt::DisplayRole) { + return d->m_extraHeaders.at(extraCol); + } + return QVariant(); + } + } + return QIdentityProxyModel::headerData(section, orientation, role); +} + +QModelIndex KExtraColumnsProxyModel::index(int row, int column, const QModelIndex &parent) const +{ + const int extraCol = extraColumnForProxyColumn(column); + if (extraCol >= 0) { + // We store the internal pointer of the index for column 0 in the proxy index for extra columns. + // This will be useful in the parent method. + return createIndex(row, column, QIdentityProxyModel::index(row, 0, parent).internalPointer()); + } + return QIdentityProxyModel::index(row, column, parent); +} + +QModelIndex KExtraColumnsProxyModel::parent(const QModelIndex &child) const +{ + const int extraCol = extraColumnForProxyColumn(child.column()); + if (extraCol >= 0) { + // Create an index for column 0 and use that to get the parent. + const QModelIndex proxySibling = createIndex(child.row(), 0, child.internalPointer()); + return QIdentityProxyModel::parent(proxySibling); + } + return QIdentityProxyModel::parent(child); +} + +int KExtraColumnsProxyModel::extraColumnForProxyColumn(int proxyColumn) const +{ + if (sourceModel() != nullptr) { + const int sourceColumnCount = sourceModel()->columnCount(); + if (proxyColumn >= sourceColumnCount) { + return proxyColumn - sourceColumnCount; + } + } + return -1; +} + +int KExtraColumnsProxyModel::proxyColumnForExtraColumn(int extraColumn) const +{ + return sourceModel()->columnCount() + extraColumn; +} + +void KExtraColumnsProxyModelPrivate::_ec_sourceLayoutAboutToBeChanged(const QList &sourceParents, + QAbstractItemModel::LayoutChangeHint hint) +{ + Q_Q(KExtraColumnsProxyModel); + + QList parents; + parents.reserve(sourceParents.size()); + for (const QPersistentModelIndex &parent : sourceParents) { + if (!parent.isValid()) { + parents << QPersistentModelIndex(); + continue; + } + const QModelIndex mappedParent = q->mapFromSource(parent); + Q_ASSERT(mappedParent.isValid()); + parents << mappedParent; + } + + Q_EMIT q->layoutAboutToBeChanged(parents, hint); + + const QModelIndexList persistentIndexList = q->persistentIndexList(); + layoutChangePersistentIndexes.reserve(persistentIndexList.size()); + layoutChangeProxyColumns.reserve(persistentIndexList.size()); + + for (QModelIndex proxyPersistentIndex : persistentIndexList) { + proxyIndexes << proxyPersistentIndex; + Q_ASSERT(proxyPersistentIndex.isValid()); + const int column = proxyPersistentIndex.column(); + layoutChangeProxyColumns << column; + if (column >= q->sourceModel()->columnCount()) { + proxyPersistentIndex = proxyPersistentIndex.sibling(proxyPersistentIndex.row(), 0); + } + const QPersistentModelIndex srcPersistentIndex = q->mapToSource(proxyPersistentIndex); + Q_ASSERT(srcPersistentIndex.isValid()); + layoutChangePersistentIndexes << srcPersistentIndex; + } +} + +void KExtraColumnsProxyModelPrivate::_ec_sourceLayoutChanged(const QList &sourceParents, QAbstractItemModel::LayoutChangeHint hint) +{ + Q_Q(KExtraColumnsProxyModel); + for (int i = 0; i < proxyIndexes.size(); ++i) { + const QModelIndex proxyIdx = proxyIndexes.at(i); + QModelIndex newProxyIdx = q->mapFromSource(layoutChangePersistentIndexes.at(i)); + if (proxyIdx.column() >= q->sourceModel()->columnCount()) { + newProxyIdx = newProxyIdx.sibling(newProxyIdx.row(), layoutChangeProxyColumns.at(i)); + } + q->changePersistentIndex(proxyIdx, newProxyIdx); + } + + layoutChangePersistentIndexes.clear(); + layoutChangeProxyColumns.clear(); + proxyIndexes.clear(); + + QList parents; + parents.reserve(sourceParents.size()); + for (const QPersistentModelIndex &parent : sourceParents) { + if (!parent.isValid()) { + parents << QPersistentModelIndex(); + continue; + } + const QModelIndex mappedParent = q->mapFromSource(parent); + Q_ASSERT(mappedParent.isValid()); + parents << mappedParent; + } + + Q_EMIT q->layoutChanged(parents, hint); +} + +#include "moc_kextracolumnsproxymodel.cpp" diff --git a/panels/dock/tray/kextracolumnsproxymodel.h b/panels/dock/tray/kextracolumnsproxymodel.h new file mode 100644 index 000000000..fb7e03d66 --- /dev/null +++ b/panels/dock/tray/kextracolumnsproxymodel.h @@ -0,0 +1,146 @@ +/* + SPDX-FileCopyrightText: 2015 Klarälvdalens Datakonsult AB, a KDAB Group company + SPDX-FileContributor: David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later + + Source code extracted from KItemModels with minimized changes. +*/ + +#ifndef KEXTRACOLUMNSPROXYMODEL_H +#define KEXTRACOLUMNSPROXYMODEL_H + +#include + +#include + +class KExtraColumnsProxyModelPrivate; + +/** + * @class KExtraColumnsProxyModel kextracolumnsproxymodel.h KExtraColumnsProxyModel + * + * This proxy appends extra columns (after all existing columns). + * + * The proxy supports source models that have a tree structure. + * It also supports editing, and propagating changes from the source model. + * Row insertion/removal, column insertion/removal in the source model are supported. + * + * Not supported: adding/removing extra columns at runtime; having a different number of columns in subtrees; + * drag-n-drop support in the extra columns; moving columns. + * + * Derive from KExtraColumnsProxyModel, call appendColumn (typically in the constructor) for each extra column, + * and reimplement extraColumnData() to allow KExtraColumnsProxyModel to retrieve the data to show in the extra columns. + * + * If you want your new column(s) to be somewhere else than at the right of the existing columns, you can + * use a KRearrangeColumnsProxyModel on top. + * + * Author: David Faure, KDAB + * @since 5.13 + */ +class KExtraColumnsProxyModel : public QIdentityProxyModel +{ + Q_OBJECT +public: + /** + * Base class constructor. + * Remember to call setSourceModel afterwards, and appendColumn. + */ + explicit KExtraColumnsProxyModel(QObject *parent = nullptr); + /** + * Destructor. + */ + ~KExtraColumnsProxyModel() override; + + // API + + /** + * Appends an extra column. + * @param header an optional text for the horizontal header + * This does not emit any signals - do it in the initial setup phase + */ + void appendColumn(const QString &header = QString()); + + void setExtraColumnTitle(int idx, QString title); + /** + * Removes an extra column. + * @param idx index of the extra column (starting from 0). + * This does not emit any signals - do it in the initial setup phase + * @since 5.24 + */ + void removeExtraColumn(int idx); + + /** + * This method is called by data() for extra columns. + * Reimplement this method to return the data for the extra columns. + * + * @param parent the parent model index in the proxy model (only useful in tree models) + * @param row the row number for which the proxy model is querying for data (child of @p parent, if set) + * @param extraColumn the number of the extra column, starting at 0 (this doesn't require knowing how many columns the source model has) + * @param role the role being queried + * @return the data at @p row and @p extraColumn + */ + virtual QVariant extraColumnData(const QModelIndex &parent, int row, int extraColumn, int role = Qt::DisplayRole) const = 0; + + // KF6 TODO: add extraColumnFlags() virtual method + + /** + * This method is called by setData() for extra columns. + * Reimplement this method to set the data for the extra columns, if editing is supported. + * Remember to call extraColumnDataChanged() after changing the data storage. + * The default implementation returns false. + */ + virtual bool setExtraColumnData(const QModelIndex &parent, int row, int extraColumn, const QVariant &data, int role = Qt::EditRole); + + /** + * This method can be called by your derived class when the data in an extra column has changed. + * The use case is data that changes "by itself", unrelated to setData. + */ + void extraColumnDataChanged(const QModelIndex &parent, int row, int extraColumn, const QVector &roles); + + /** + * Returns the extra column number (0, 1, ...) for a given column number of the proxymodel. + * This basically means subtracting the amount of columns in the source model. + */ + int extraColumnForProxyColumn(int proxyColumn) const; + /** + * Returns the proxy column number for a given extra column number (starting at 0). + * This basically means adding the amount of columns in the source model. + */ + int proxyColumnForExtraColumn(int extraColumn) const; + + // Implementation + /// @reimp + void setSourceModel(QAbstractItemModel *model) override; + /// @reimp + QModelIndex mapToSource(const QModelIndex &proxyIndex) const override; + /// @reimp + QItemSelection mapSelectionToSource(const QItemSelection &selection) const override; + /// @reimp + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + /// @reimp + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + /// @reimp + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + /// @reimp + QModelIndex sibling(int row, int column, const QModelIndex &idx) const override; + /// @reimp + QModelIndex buddy(const QModelIndex &index) const override; + /// @reimp + Qt::ItemFlags flags(const QModelIndex &index) const override; + /// @reimp + bool hasChildren(const QModelIndex &index) const override; + /// @reimp + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + /// @reimp + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + /// @reimp + QModelIndex parent(const QModelIndex &child) const override; + +private: + Q_DECLARE_PRIVATE(KExtraColumnsProxyModel) + Q_PRIVATE_SLOT(d_func(), void _ec_sourceLayoutAboutToBeChanged(const QList &, QAbstractItemModel::LayoutChangeHint)) + Q_PRIVATE_SLOT(d_func(), void _ec_sourceLayoutChanged(const QList &, QAbstractItemModel::LayoutChangeHint)) + std::unique_ptr const d_ptr; +}; + +#endif diff --git a/panels/dock/tray/listtotableproxymodel.cpp b/panels/dock/tray/listtotableproxymodel.cpp new file mode 100644 index 000000000..e4f6343aa --- /dev/null +++ b/panels/dock/tray/listtotableproxymodel.cpp @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include "listtotableproxymodel.h" + +ListToTableProxyModel::ListToTableProxyModel(QObject *parent) + : KExtraColumnsProxyModel(parent) +{ + connect(this, &ListToTableProxyModel::rolesChanged, this, [this](){ + for (int role : std::as_const(m_roles)) { + QByteArray fallbackName(QByteArray::number(role)); + QByteArray roleName(sourceModel() ? sourceModel()->roleNames().value(role, fallbackName) : fallbackName); + appendColumn(QString(roleName)); + } + }); + + connect(this, &ListToTableProxyModel::sourceModelChanged, this, [this](){ + for (int idx = 0; idx < m_roles.count(); idx++) { + int role = m_roles[idx]; + QByteArray fallbackName(QByteArray::number(role)); + QByteArray roleName(sourceModel() ? sourceModel()->roleNames().value(role, fallbackName) : fallbackName); + setExtraColumnTitle(idx, QString(roleName)); + } + }); + + connect(this, &ListToTableProxyModel::dataChanged, this, + [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, + const QList &roles = QList()){ + // TODO: lazy solution, we should use dataChanged() to notify extra column changed; + beginResetModel(); + endResetModel(); + }); +} + +QVariant ListToTableProxyModel::extraColumnData(const QModelIndex &parent, int row, int extraColumn, int role) const +{ + QVariant result(data(index(row, m_sourceColumn, parent), m_roles[extraColumn])); + if (!result.isValid()) return QStringLiteral(""); + return data(index(row, m_sourceColumn, parent), m_roles[extraColumn]); +} diff --git a/panels/dock/tray/listtotableproxymodel.h b/panels/dock/tray/listtotableproxymodel.h new file mode 100644 index 000000000..66d1695c5 --- /dev/null +++ b/panels/dock/tray/listtotableproxymodel.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-2.0-or-later + +#pragma once + +#include "kextracolumnsproxymodel.h" +#include + +class ListToTableProxyModel : public KExtraColumnsProxyModel +{ + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(QList roles MEMBER m_roles NOTIFY rolesChanged) + Q_PROPERTY(int sourceColumn MEMBER m_sourceColumn NOTIFY sourceColumnChanged) +public: + explicit ListToTableProxyModel(QObject *parent = nullptr); + + QVariant extraColumnData(const QModelIndex &parent, int row, int extraColumn, int role = Qt::DisplayRole) const override; + +signals: + void rolesChanged(QList roles); + void sourceColumnChanged(int sourceColum); + +private: + QList m_roles; + int m_sourceColumn = 0; +};