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

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