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 
13 class KExtraColumnsProxyModelPrivate
14 {
15     Q_DECLARE_PUBLIC(KExtraColumnsProxyModel)
16     KExtraColumnsProxyModel *const q_ptr;
17 
18 public:
KExtraColumnsProxyModelPrivate(KExtraColumnsProxyModel * model)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     QVector<QString> m_extraHeaders;
29 
30     // for layoutAboutToBeChanged/layoutChanged
31     QVector<QPersistentModelIndex> layoutChangePersistentIndexes;
32     QVector<int> layoutChangeProxyColumns;
33     QModelIndexList proxyIndexes;
34 };
35 
KExtraColumnsProxyModel(QObject * parent)36 KExtraColumnsProxyModel::KExtraColumnsProxyModel(QObject *parent)
37     : QIdentityProxyModel(parent)
38     , d_ptr(new KExtraColumnsProxyModelPrivate(this))
39 {
40 }
41 
~KExtraColumnsProxyModel()42 KExtraColumnsProxyModel::~KExtraColumnsProxyModel()
43 {
44 }
45 
appendColumn(const QString & header)46 void KExtraColumnsProxyModel::appendColumn(const QString &header)
47 {
48     Q_D(KExtraColumnsProxyModel);
49     d->m_extraHeaders.append(header);
50 }
51 
removeExtraColumn(int idx)52 void KExtraColumnsProxyModel::removeExtraColumn(int idx)
53 {
54     Q_D(KExtraColumnsProxyModel);
55     d->m_extraHeaders.remove(idx);
56 }
57 
setExtraColumnData(const QModelIndex & parent,int row,int extraColumn,const QVariant & data,int role)58 bool 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 
extraColumnDataChanged(const QModelIndex & parent,int row,int extraColumn,const QVector<int> & roles)68 void KExtraColumnsProxyModel::extraColumnDataChanged(const QModelIndex &parent, int row, int extraColumn, const QVector<int> &roles)
69 {
70     const QModelIndex idx = index(row, proxyColumnForExtraColumn(extraColumn), parent);
71     Q_EMIT dataChanged(idx, idx, roles);
72 }
73 
setSourceModel(QAbstractItemModel * model)74 void KExtraColumnsProxyModel::setSourceModel(QAbstractItemModel *model)
75 {
76     if (sourceModel()) {
77         disconnect(sourceModel(),
78                    SIGNAL(layoutAboutToBeChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)),
79                    this,
80                    SLOT(_ec_sourceLayoutAboutToBeChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)));
81         disconnect(sourceModel(),
82                    SIGNAL(layoutChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)),
83                    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(model,
93                    SIGNAL(layoutAboutToBeChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)),
94                    this,
95                    SLOT(_q_sourceLayoutAboutToBeChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)));
96         disconnect(model,
97                    SIGNAL(layoutChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)),
98                    this,
99                    SLOT(_q_sourceLayoutChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)));
100         connect(model,
101                 SIGNAL(layoutAboutToBeChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)),
102                 this,
103                 SLOT(_ec_sourceLayoutAboutToBeChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)));
104         connect(model,
105                 SIGNAL(layoutChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)),
106                 this,
107                 SLOT(_ec_sourceLayoutChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)));
108     }
109 }
110 
mapToSource(const QModelIndex & proxyIndex) const111 QModelIndex 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 
buddy(const QModelIndex & proxyIndex) const124 QModelIndex KExtraColumnsProxyModel::buddy(const QModelIndex &proxyIndex) const
125 {
126     const int column = proxyIndex.column();
127     if (column >= sourceModel()->columnCount()) {
128         return proxyIndex;
129     }
130     return QIdentityProxyModel::buddy(proxyIndex);
131 }
132 
sibling(int row,int column,const QModelIndex & idx) const133 QModelIndex KExtraColumnsProxyModel::sibling(int row, int column, const QModelIndex &idx) const
134 {
135     if (row == idx.row() && column == idx.column()) {
136         return idx;
137     }
138     return index(row, column, parent(idx));
139 }
140 
mapSelectionToSource(const QItemSelection & selection) const141 QItemSelection KExtraColumnsProxyModel::mapSelectionToSource(const QItemSelection &selection) const
142 {
143     QItemSelection sourceSelection;
144 
145     if (!sourceModel()) {
146         return sourceSelection;
147     }
148 
149     // mapToSource will give invalid index for our additional columns, so truncate the selection
150     // to the columns known by the source model
151     const int sourceColumnCount = sourceModel()->columnCount();
152     QItemSelection::const_iterator it = selection.constBegin();
153     const QItemSelection::const_iterator end = selection.constEnd();
154     for (; it != end; ++it) {
155         Q_ASSERT(it->model() == this);
156         QModelIndex topLeft = it->topLeft();
157         Q_ASSERT(topLeft.isValid());
158         Q_ASSERT(topLeft.model() == this);
159         topLeft = topLeft.sibling(topLeft.row(), 0);
160         QModelIndex bottomRight = it->bottomRight();
161         Q_ASSERT(bottomRight.isValid());
162         Q_ASSERT(bottomRight.model() == this);
163         if (bottomRight.column() >= sourceColumnCount) {
164             bottomRight = bottomRight.sibling(bottomRight.row(), sourceColumnCount - 1);
165         }
166         // This can lead to duplicate source indexes, so use merge().
167         const QItemSelectionRange range(mapToSource(topLeft), mapToSource(bottomRight));
168         QItemSelection newSelection;
169         newSelection << range;
170         sourceSelection.merge(newSelection, QItemSelectionModel::Select);
171     }
172 
173     return sourceSelection;
174 }
175 
columnCount(const QModelIndex & parent) const176 int KExtraColumnsProxyModel::columnCount(const QModelIndex &parent) const
177 {
178     Q_D(const KExtraColumnsProxyModel);
179     return QIdentityProxyModel::columnCount(parent) + d->m_extraHeaders.count();
180 }
181 
data(const QModelIndex & index,int role) const182 QVariant KExtraColumnsProxyModel::data(const QModelIndex &index, int role) const
183 {
184     Q_D(const KExtraColumnsProxyModel);
185     const int extraCol = extraColumnForProxyColumn(index.column());
186     if (extraCol >= 0 && !d->m_extraHeaders.isEmpty()) {
187         return extraColumnData(index.parent(), index.row(), extraCol, role);
188     }
189     return sourceModel()->data(mapToSource(index), role);
190 }
191 
setData(const QModelIndex & index,const QVariant & value,int role)192 bool KExtraColumnsProxyModel::setData(const QModelIndex &index, const QVariant &value, int role)
193 {
194     Q_D(const KExtraColumnsProxyModel);
195     const int extraCol = extraColumnForProxyColumn(index.column());
196     if (extraCol >= 0 && !d->m_extraHeaders.isEmpty()) {
197         return setExtraColumnData(index.parent(), index.row(), extraCol, value, role);
198     }
199     return sourceModel()->setData(mapToSource(index), value, role);
200 }
201 
flags(const QModelIndex & index) const202 Qt::ItemFlags KExtraColumnsProxyModel::flags(const QModelIndex &index) const
203 {
204     const int extraCol = extraColumnForProxyColumn(index.column());
205     if (extraCol >= 0) {
206         // extra columns are readonly
207         return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
208     }
209     return sourceModel() != nullptr ? sourceModel()->flags(mapToSource(index)) : Qt::NoItemFlags;
210 }
211 
hasChildren(const QModelIndex & index) const212 bool KExtraColumnsProxyModel::hasChildren(const QModelIndex &index) const
213 {
214     if (index.column() > 0) {
215         return false;
216     }
217     return QIdentityProxyModel::hasChildren(index);
218 }
219 
headerData(int section,Qt::Orientation orientation,int role) const220 QVariant KExtraColumnsProxyModel::headerData(int section, Qt::Orientation orientation, int role) const
221 {
222     Q_D(const KExtraColumnsProxyModel);
223     if (orientation == Qt::Horizontal) {
224         const int extraCol = extraColumnForProxyColumn(section);
225         if (extraCol >= 0) {
226             // Only text is supported, in headers for extra columns
227             if (role == Qt::DisplayRole) {
228                 return d->m_extraHeaders.at(extraCol);
229             }
230             return QVariant();
231         }
232     }
233     return QIdentityProxyModel::headerData(section, orientation, role);
234 }
235 
index(int row,int column,const QModelIndex & parent) const236 QModelIndex KExtraColumnsProxyModel::index(int row, int column, const QModelIndex &parent) const
237 {
238     const int extraCol = extraColumnForProxyColumn(column);
239     if (extraCol >= 0) {
240         // We store the internal pointer of the index for column 0 in the proxy index for extra columns.
241         // This will be useful in the parent method.
242         return createIndex(row, column, QIdentityProxyModel::index(row, 0, parent).internalPointer());
243     }
244     return QIdentityProxyModel::index(row, column, parent);
245 }
246 
parent(const QModelIndex & child) const247 QModelIndex KExtraColumnsProxyModel::parent(const QModelIndex &child) const
248 {
249     const int extraCol = extraColumnForProxyColumn(child.column());
250     if (extraCol >= 0) {
251         // Create an index for column 0 and use that to get the parent.
252         const QModelIndex proxySibling = createIndex(child.row(), 0, child.internalPointer());
253         return QIdentityProxyModel::parent(proxySibling);
254     }
255     return QIdentityProxyModel::parent(child);
256 }
257 
extraColumnForProxyColumn(int proxyColumn) const258 int KExtraColumnsProxyModel::extraColumnForProxyColumn(int proxyColumn) const
259 {
260     if (sourceModel() != nullptr) {
261         const int sourceColumnCount = sourceModel()->columnCount();
262         if (proxyColumn >= sourceColumnCount) {
263             return proxyColumn - sourceColumnCount;
264         }
265     }
266     return -1;
267 }
268 
proxyColumnForExtraColumn(int extraColumn) const269 int KExtraColumnsProxyModel::proxyColumnForExtraColumn(int extraColumn) const
270 {
271     return sourceModel()->columnCount() + extraColumn;
272 }
273 
_ec_sourceLayoutAboutToBeChanged(const QList<QPersistentModelIndex> & sourceParents,QAbstractItemModel::LayoutChangeHint hint)274 void KExtraColumnsProxyModelPrivate::_ec_sourceLayoutAboutToBeChanged(const QList<QPersistentModelIndex> &sourceParents,
275                                                                       QAbstractItemModel::LayoutChangeHint hint)
276 {
277     Q_Q(KExtraColumnsProxyModel);
278 
279     QList<QPersistentModelIndex> parents;
280     parents.reserve(sourceParents.size());
281     for (const QPersistentModelIndex &parent : sourceParents) {
282         if (!parent.isValid()) {
283             parents << QPersistentModelIndex();
284             continue;
285         }
286         const QModelIndex mappedParent = q->mapFromSource(parent);
287         Q_ASSERT(mappedParent.isValid());
288         parents << mappedParent;
289     }
290 
291     Q_EMIT q->layoutAboutToBeChanged(parents, hint);
292 
293     const QModelIndexList persistentIndexList = q->persistentIndexList();
294     layoutChangePersistentIndexes.reserve(persistentIndexList.size());
295     layoutChangeProxyColumns.reserve(persistentIndexList.size());
296 
297     for (QModelIndex proxyPersistentIndex : persistentIndexList) {
298         proxyIndexes << proxyPersistentIndex;
299         Q_ASSERT(proxyPersistentIndex.isValid());
300         const int column = proxyPersistentIndex.column();
301         layoutChangeProxyColumns << column;
302         if (column >= q->sourceModel()->columnCount()) {
303             proxyPersistentIndex = proxyPersistentIndex.sibling(proxyPersistentIndex.row(), 0);
304         }
305         const QPersistentModelIndex srcPersistentIndex = q->mapToSource(proxyPersistentIndex);
306         Q_ASSERT(srcPersistentIndex.isValid());
307         layoutChangePersistentIndexes << srcPersistentIndex;
308     }
309 }
310 
_ec_sourceLayoutChanged(const QList<QPersistentModelIndex> & sourceParents,QAbstractItemModel::LayoutChangeHint hint)311 void KExtraColumnsProxyModelPrivate::_ec_sourceLayoutChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint)
312 {
313     Q_Q(KExtraColumnsProxyModel);
314     for (int i = 0; i < proxyIndexes.size(); ++i) {
315         const QModelIndex proxyIdx = proxyIndexes.at(i);
316         QModelIndex newProxyIdx = q->mapFromSource(layoutChangePersistentIndexes.at(i));
317         if (proxyIdx.column() >= q->sourceModel()->columnCount()) {
318             newProxyIdx = newProxyIdx.sibling(newProxyIdx.row(), layoutChangeProxyColumns.at(i));
319         }
320         q->changePersistentIndex(proxyIdx, newProxyIdx);
321     }
322 
323     layoutChangePersistentIndexes.clear();
324     layoutChangeProxyColumns.clear();
325     proxyIndexes.clear();
326 
327     QList<QPersistentModelIndex> parents;
328     parents.reserve(sourceParents.size());
329     for (const QPersistentModelIndex &parent : sourceParents) {
330         if (!parent.isValid()) {
331             parents << QPersistentModelIndex();
332             continue;
333         }
334         const QModelIndex mappedParent = q->mapFromSource(parent);
335         Q_ASSERT(mappedParent.isValid());
336         parents << mappedParent;
337     }
338 
339     Q_EMIT q->layoutChanged(parents, hint);
340 }
341 
342 #include "moc_kextracolumnsproxymodel.cpp"
343