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(header); |
50 | } |
51 | |
52 | void KExtraColumnsProxyModel::removeExtraColumn(int idx) |
53 | { |
54 | Q_D(KExtraColumnsProxyModel); |
55 | d->m_extraHeaders.remove(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, proxyColumnForExtraColumn(extraColumn), parent); |
71 | Q_EMIT dataChanged(idx, idx, roles); |
72 | } |
73 | |
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 | |
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 | const int column = proxyIndex.column(); |
127 | if (column >= sourceModel()->columnCount()) { |
128 | return proxyIndex; |
129 | } |
130 | return QIdentityProxyModel::buddy(proxyIndex); |
131 | } |
132 | |
133 | 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 | |
141 | 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 | |
176 | int KExtraColumnsProxyModel::columnCount(const QModelIndex &parent) const |
177 | { |
178 | Q_D(const KExtraColumnsProxyModel); |
179 | return QIdentityProxyModel::columnCount(parent) + d->m_extraHeaders.count(); |
180 | } |
181 | |
182 | QVariant KExtraColumnsProxyModel::data(const QModelIndex &index, int role) const |
183 | { |
184 | Q_D(const KExtraColumnsProxyModel); |
185 | const int = extraColumnForProxyColumn(proxyColumn: 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 | |
192 | bool KExtraColumnsProxyModel::setData(const QModelIndex &index, const QVariant &value, int role) |
193 | { |
194 | Q_D(const KExtraColumnsProxyModel); |
195 | const int = extraColumnForProxyColumn(proxyColumn: 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 | |
202 | Qt::ItemFlags KExtraColumnsProxyModel::flags(const QModelIndex &index) const |
203 | { |
204 | const int = extraColumnForProxyColumn(proxyColumn: 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 | |
212 | bool KExtraColumnsProxyModel::hasChildren(const QModelIndex &index) const |
213 | { |
214 | if (index.column() > 0) { |
215 | return false; |
216 | } |
217 | return QIdentityProxyModel::hasChildren(index); |
218 | } |
219 | |
220 | 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 = extraColumnForProxyColumn(proxyColumn: 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 | |
236 | QModelIndex KExtraColumnsProxyModel::index(int row, int column, const QModelIndex &parent) const |
237 | { |
238 | const int = extraColumnForProxyColumn(proxyColumn: 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 | |
247 | QModelIndex KExtraColumnsProxyModel::parent(const QModelIndex &child) const |
248 | { |
249 | const int = extraColumnForProxyColumn(proxyColumn: 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 | |
258 | 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 | |
269 | int KExtraColumnsProxyModel::proxyColumnForExtraColumn(int extraColumn) const |
270 | { |
271 | return sourceModel()->columnCount() + extraColumn; |
272 | } |
273 | |
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 | |
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 | |