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

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