1/*
2 SPDX-FileCopyrightText: 2015 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
3 SPDX-FileContributor: David Faure <david.faure@kdab.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "kextracolumnsproxymodel.h"
9#include "kitemmodels_debug.h"
10
11#include <QItemSelection>
12
13class KExtraColumnsProxyModelPrivate
14{
15 Q_DECLARE_PUBLIC(KExtraColumnsProxyModel)
16 KExtraColumnsProxyModel *const q_ptr;
17
18public:
19 KExtraColumnsProxyModelPrivate(KExtraColumnsProxyModel *model)
20 : q_ptr(model)
21 {
22 }
23
24 void _ec_sourceLayoutAboutToBeChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint);
25 void _ec_sourceLayoutChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint);
26
27 // Configuration (doesn't change once source model is plugged in)
28 QList<QString> m_extraHeaders;
29
30 // for layoutAboutToBeChanged/layoutChanged
31 QList<QPersistentModelIndex> layoutChangePersistentIndexes;
32 QList<int> layoutChangeProxyColumns;
33 QModelIndexList proxyIndexes;
34};
35
36KExtraColumnsProxyModel::KExtraColumnsProxyModel(QObject *parent)
37 : QIdentityProxyModel(parent)
38 , d_ptr(new KExtraColumnsProxyModelPrivate(this))
39{
40 // The handling of persistent model indexes assumes mapToSource can be called for any index
41 // This breaks for the extra column, so we'll have to do it ourselves
42#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
43 setHandleSourceLayoutChanges(false);
44#endif
45}
46
47KExtraColumnsProxyModel::~KExtraColumnsProxyModel()
48{
49}
50
51void KExtraColumnsProxyModel::appendColumn(const QString &header)
52{
53 Q_D(KExtraColumnsProxyModel);
54 d->m_extraHeaders.append(t: header);
55}
56
57void KExtraColumnsProxyModel::removeExtraColumn(int idx)
58{
59 Q_D(KExtraColumnsProxyModel);
60 d->m_extraHeaders.remove(i: idx);
61}
62
63bool KExtraColumnsProxyModel::setExtraColumnData(const QModelIndex &parent, int row, int extraColumn, const QVariant &data, int role)
64{
65 Q_UNUSED(parent);
66 Q_UNUSED(row);
67 Q_UNUSED(extraColumn);
68 Q_UNUSED(data);
69 Q_UNUSED(role);
70 return false;
71}
72
73void KExtraColumnsProxyModel::extraColumnDataChanged(const QModelIndex &parent, int row, int extraColumn, const QList<int> &roles)
74{
75 const QModelIndex idx = index(row, column: proxyColumnForExtraColumn(extraColumn), parent);
76 Q_EMIT dataChanged(topLeft: idx, bottomRight: idx, roles);
77}
78
79void KExtraColumnsProxyModel::setSourceModel(QAbstractItemModel *model)
80{
81 if (sourceModel()) {
82 disconnect(sender: sourceModel(),
83 SIGNAL(layoutAboutToBeChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)),
84 receiver: this,
85 SLOT(_ec_sourceLayoutAboutToBeChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)));
86 disconnect(sender: sourceModel(),
87 SIGNAL(layoutChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)),
88 receiver: this,
89 SLOT(_ec_sourceLayoutChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)));
90 }
91
92 QIdentityProxyModel::setSourceModel(model);
93
94 if (model) {
95 // The handling of persistent model indexes assumes mapToSource can be called for any index
96 // This breaks for the extra column, so we'll have to do it ourselves
97#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
98 disconnect(model,
99 SIGNAL(layoutAboutToBeChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)),
100 this,
101 SLOT(_q_sourceLayoutAboutToBeChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)));
102 disconnect(model,
103 SIGNAL(layoutChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)),
104 this,
105 SLOT(_q_sourceLayoutChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)));
106#endif
107 connect(sender: model,
108 SIGNAL(layoutAboutToBeChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)),
109 receiver: this,
110 SLOT(_ec_sourceLayoutAboutToBeChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)));
111 connect(sender: model,
112 SIGNAL(layoutChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)),
113 receiver: this,
114 SLOT(_ec_sourceLayoutChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)));
115 }
116}
117
118QModelIndex KExtraColumnsProxyModel::mapToSource(const QModelIndex &proxyIndex) const
119{
120 if (!proxyIndex.isValid()) { // happens in e.g. rowCount(mapToSource(parent))
121 return QModelIndex();
122 }
123 const int column = proxyIndex.column();
124 if (column >= sourceModel()->columnCount()) {
125 qCDebug(KITEMMODELS_LOG) << "Returning invalid index in mapToSource";
126 return QModelIndex();
127 }
128 return QIdentityProxyModel::mapToSource(proxyIndex);
129}
130
131QModelIndex KExtraColumnsProxyModel::buddy(const QModelIndex &proxyIndex) const
132{
133 if (sourceModel()) {
134 const int column = proxyIndex.column();
135 if (column >= sourceModel()->columnCount()) {
136 return proxyIndex;
137 }
138 }
139 return QIdentityProxyModel::buddy(index: proxyIndex);
140}
141
142QModelIndex KExtraColumnsProxyModel::sibling(int row, int column, const QModelIndex &idx) const
143{
144 if (row == idx.row() && column == idx.column()) {
145 return idx;
146 }
147 return index(row, column, parent: parent(child: idx));
148}
149
150QItemSelection KExtraColumnsProxyModel::mapSelectionToSource(const QItemSelection &selection) const
151{
152 QItemSelection sourceSelection;
153
154 if (!sourceModel()) {
155 return sourceSelection;
156 }
157
158 // mapToSource will give invalid index for our additional columns, so truncate the selection
159 // to the columns known by the source model
160 const int sourceColumnCount = sourceModel()->columnCount();
161 QItemSelection::const_iterator it = selection.constBegin();
162 const QItemSelection::const_iterator end = selection.constEnd();
163 for (; it != end; ++it) {
164 Q_ASSERT(it->model() == this);
165 QModelIndex topLeft = it->topLeft();
166 Q_ASSERT(topLeft.isValid());
167 Q_ASSERT(topLeft.model() == this);
168 topLeft = topLeft.sibling(arow: topLeft.row(), acolumn: 0);
169 QModelIndex bottomRight = it->bottomRight();
170 Q_ASSERT(bottomRight.isValid());
171 Q_ASSERT(bottomRight.model() == this);
172 if (bottomRight.column() >= sourceColumnCount) {
173 bottomRight = bottomRight.sibling(arow: bottomRight.row(), acolumn: sourceColumnCount - 1);
174 }
175 // This can lead to duplicate source indexes, so use merge().
176 const QItemSelectionRange range(mapToSource(proxyIndex: topLeft), mapToSource(proxyIndex: bottomRight));
177 QItemSelection newSelection;
178 newSelection << range;
179 sourceSelection.merge(other: newSelection, command: QItemSelectionModel::Select);
180 }
181
182 return sourceSelection;
183}
184
185int KExtraColumnsProxyModel::columnCount(const QModelIndex &parent) const
186{
187 Q_D(const KExtraColumnsProxyModel);
188 return QIdentityProxyModel::columnCount(parent) + d->m_extraHeaders.count();
189}
190
191QVariant KExtraColumnsProxyModel::data(const QModelIndex &index, int role) const
192{
193 Q_D(const KExtraColumnsProxyModel);
194 const int extraCol = extraColumnForProxyColumn(proxyColumn: index.column());
195 if (extraCol >= 0 && !d->m_extraHeaders.isEmpty()) {
196 return extraColumnData(parent: index.parent(), row: index.row(), extraColumn: extraCol, role);
197 }
198 return sourceModel()->data(index: mapToSource(proxyIndex: index), role);
199}
200
201bool KExtraColumnsProxyModel::setData(const QModelIndex &index, const QVariant &value, int role)
202{
203 Q_D(const KExtraColumnsProxyModel);
204 const int extraCol = extraColumnForProxyColumn(proxyColumn: index.column());
205 if (extraCol >= 0 && !d->m_extraHeaders.isEmpty()) {
206 return setExtraColumnData(parent: index.parent(), row: index.row(), extraColumn: extraCol, data: value, role);
207 }
208 return sourceModel()->setData(index: mapToSource(proxyIndex: index), value, role);
209}
210
211Qt::ItemFlags KExtraColumnsProxyModel::flags(const QModelIndex &index) const
212{
213 const int extraCol = extraColumnForProxyColumn(proxyColumn: index.column());
214 if (extraCol >= 0) {
215 // extra columns are readonly
216 return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
217 }
218 return sourceModel() != nullptr ? sourceModel()->flags(index: mapToSource(proxyIndex: index)) : Qt::NoItemFlags;
219}
220
221bool KExtraColumnsProxyModel::hasChildren(const QModelIndex &index) const
222{
223 if (index.column() > 0) {
224 return false;
225 }
226 return QIdentityProxyModel::hasChildren(parent: index);
227}
228
229QVariant KExtraColumnsProxyModel::headerData(int section, Qt::Orientation orientation, int role) const
230{
231 Q_D(const KExtraColumnsProxyModel);
232 if (orientation == Qt::Horizontal) {
233 const int extraCol = extraColumnForProxyColumn(proxyColumn: section);
234 if (extraCol >= 0) {
235 // Only text is supported, in headers for extra columns
236 if (role == Qt::DisplayRole) {
237 return d->m_extraHeaders.at(i: extraCol);
238 }
239 return QVariant();
240 }
241 }
242 return QIdentityProxyModel::headerData(section, orientation, role);
243}
244
245QModelIndex KExtraColumnsProxyModel::index(int row, int column, const QModelIndex &parent) const
246{
247 const int extraCol = extraColumnForProxyColumn(proxyColumn: column);
248 if (extraCol >= 0) {
249 // We store the internal pointer of the index for column 0 in the proxy index for extra columns.
250 // This will be useful in the parent method.
251 return createIndex(arow: row, acolumn: column, adata: QIdentityProxyModel::index(row, column: 0, parent).internalPointer());
252 }
253 return QIdentityProxyModel::index(row, column, parent);
254}
255
256QModelIndex KExtraColumnsProxyModel::parent(const QModelIndex &child) const
257{
258 const int extraCol = extraColumnForProxyColumn(proxyColumn: child.column());
259 if (extraCol >= 0) {
260 // Create an index for column 0 and use that to get the parent.
261 const QModelIndex proxySibling = createIndex(arow: child.row(), acolumn: 0, adata: child.internalPointer());
262 return QIdentityProxyModel::parent(child: proxySibling);
263 }
264 return QIdentityProxyModel::parent(child);
265}
266
267int KExtraColumnsProxyModel::extraColumnForProxyColumn(int proxyColumn) const
268{
269 if (sourceModel() != nullptr) {
270 const int sourceColumnCount = sourceModel()->columnCount();
271 if (proxyColumn >= sourceColumnCount) {
272 return proxyColumn - sourceColumnCount;
273 }
274 }
275 return -1;
276}
277
278int KExtraColumnsProxyModel::proxyColumnForExtraColumn(int extraColumn) const
279{
280 return sourceModel()->columnCount() + extraColumn;
281}
282
283void KExtraColumnsProxyModelPrivate::_ec_sourceLayoutAboutToBeChanged(const QList<QPersistentModelIndex> &sourceParents,
284 QAbstractItemModel::LayoutChangeHint hint)
285{
286 Q_Q(KExtraColumnsProxyModel);
287
288 QList<QPersistentModelIndex> parents;
289 parents.reserve(asize: sourceParents.size());
290 for (const QPersistentModelIndex &parent : sourceParents) {
291 if (!parent.isValid()) {
292 parents << QPersistentModelIndex();
293 continue;
294 }
295 const QModelIndex mappedParent = q->mapFromSource(sourceIndex: parent);
296 Q_ASSERT(mappedParent.isValid());
297 parents << mappedParent;
298 }
299
300 Q_EMIT q->layoutAboutToBeChanged(parents, hint);
301
302 const QModelIndexList persistentIndexList = q->persistentIndexList();
303 layoutChangePersistentIndexes.reserve(asize: persistentIndexList.size());
304 layoutChangeProxyColumns.reserve(asize: persistentIndexList.size());
305
306 for (QModelIndex proxyPersistentIndex : persistentIndexList) {
307 proxyIndexes << proxyPersistentIndex;
308 Q_ASSERT(proxyPersistentIndex.isValid());
309 const int column = proxyPersistentIndex.column();
310 layoutChangeProxyColumns << column;
311 if (column >= q->sourceModel()->columnCount()) {
312 proxyPersistentIndex = proxyPersistentIndex.sibling(arow: proxyPersistentIndex.row(), acolumn: 0);
313 }
314 const QPersistentModelIndex srcPersistentIndex = q->mapToSource(proxyIndex: proxyPersistentIndex);
315 Q_ASSERT(srcPersistentIndex.isValid());
316 layoutChangePersistentIndexes << srcPersistentIndex;
317 }
318}
319
320void KExtraColumnsProxyModelPrivate::_ec_sourceLayoutChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint)
321{
322 Q_Q(KExtraColumnsProxyModel);
323 for (int i = 0; i < proxyIndexes.size(); ++i) {
324 const QModelIndex proxyIdx = proxyIndexes.at(i);
325 QModelIndex newProxyIdx = q->mapFromSource(sourceIndex: layoutChangePersistentIndexes.at(i));
326 if (proxyIdx.column() >= q->sourceModel()->columnCount()) {
327 newProxyIdx = newProxyIdx.sibling(arow: newProxyIdx.row(), acolumn: layoutChangeProxyColumns.at(i));
328 }
329 q->changePersistentIndex(from: proxyIdx, to: newProxyIdx);
330 }
331
332 layoutChangePersistentIndexes.clear();
333 layoutChangeProxyColumns.clear();
334 proxyIndexes.clear();
335
336 QList<QPersistentModelIndex> parents;
337 parents.reserve(asize: sourceParents.size());
338 for (const QPersistentModelIndex &parent : sourceParents) {
339 if (!parent.isValid()) {
340 parents << QPersistentModelIndex();
341 continue;
342 }
343 const QModelIndex mappedParent = q->mapFromSource(sourceIndex: parent);
344 Q_ASSERT(mappedParent.isValid());
345 parents << mappedParent;
346 }
347
348 Q_EMIT q->layoutChanged(parents, hint);
349}
350
351#include "moc_kextracolumnsproxymodel.cpp"
352

source code of kitemmodels/src/core/kextracolumnsproxymodel.cpp