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: |
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 | |
36 | KExtraColumnsProxyModel::KExtraColumnsProxyModel(QObject *parent) |
37 | : QIdentityProxyModel(parent) |
38 | , d_ptr(new KExtraColumnsProxyModelPrivate(this)) |
39 | { |
40 | } |
41 | |
42 | KExtraColumnsProxyModel::~KExtraColumnsProxyModel() |
43 | { |
44 | } |
45 | |
46 | void KExtraColumnsProxyModel::appendColumn(const QString &) |
47 | { |
48 | Q_D(KExtraColumnsProxyModel); |
49 | d->m_extraHeaders.append(t: header); |
50 | } |
51 | |
52 | void KExtraColumnsProxyModel::removeExtraColumn(int idx) |
53 | { |
54 | Q_D(KExtraColumnsProxyModel); |
55 | d->m_extraHeaders.remove(i: idx); |
56 | } |
57 | |
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 | |
68 | void 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 | |
74 | void 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 | |
111 | 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 | |
124 | QModelIndex 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 | |
135 | QModelIndex 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 | |
143 | QItemSelection 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 | |
178 | int KExtraColumnsProxyModel::columnCount(const QModelIndex &parent) const |
179 | { |
180 | Q_D(const KExtraColumnsProxyModel); |
181 | return QIdentityProxyModel::columnCount(parent) + d->m_extraHeaders.count(); |
182 | } |
183 | |
184 | QVariant KExtraColumnsProxyModel::data(const QModelIndex &index, int role) const |
185 | { |
186 | Q_D(const KExtraColumnsProxyModel); |
187 | const int = 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 | |
194 | bool KExtraColumnsProxyModel::setData(const QModelIndex &index, const QVariant &value, int role) |
195 | { |
196 | Q_D(const KExtraColumnsProxyModel); |
197 | const int = 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 | |
204 | Qt::ItemFlags KExtraColumnsProxyModel::flags(const QModelIndex &index) const |
205 | { |
206 | const int = 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 | |
214 | bool KExtraColumnsProxyModel::hasChildren(const QModelIndex &index) const |
215 | { |
216 | if (index.column() > 0) { |
217 | return false; |
218 | } |
219 | return QIdentityProxyModel::hasChildren(parent: index); |
220 | } |
221 | |
222 | QVariant KExtraColumnsProxyModel::headerData(int section, Qt::Orientation orientation, int role) const |
223 | { |
224 | Q_D(const KExtraColumnsProxyModel); |
225 | if (orientation == Qt::Horizontal) { |
226 | const int = 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 | |
238 | QModelIndex KExtraColumnsProxyModel::index(int row, int column, const QModelIndex &parent) const |
239 | { |
240 | const int = 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 | |
249 | QModelIndex KExtraColumnsProxyModel::parent(const QModelIndex &child) const |
250 | { |
251 | const int = 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 | |
260 | int 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 | |
271 | int KExtraColumnsProxyModel::proxyColumnForExtraColumn(int extraColumn) const |
272 | { |
273 | return sourceModel()->columnCount() + extraColumn; |
274 | } |
275 | |
276 | void 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 | |
313 | void 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 | |