1 | // Copyright (C) 2021 The Qt Company Ltd. |
---|---|
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include <math.h> |
5 | #include <QtCore/qstack.h> |
6 | #include <QtCore/qdebug.h> |
7 | |
8 | #include "qqmltreemodeltotablemodel_p_p.h" |
9 | |
10 | QT_BEGIN_NAMESPACE |
11 | |
12 | //#define QQMLTREEMODELADAPTOR_DEBUG |
13 | #if defined(QQMLTREEMODELADAPTOR_DEBUG) && !defined(QT_TESTLIB_LIB) |
14 | # define ASSERT_CONSISTENCY() Q_ASSERT_X(testConsistency(true /* dumpOnFail */), Q_FUNC_INFO, "Consistency test failed") |
15 | #else |
16 | # define ASSERT_CONSISTENCY qt_noop |
17 | #endif |
18 | |
19 | QQmlTreeModelToTableModel::QQmlTreeModelToTableModel(QObject *parent) |
20 | : QAbstractItemModel(parent) |
21 | { |
22 | } |
23 | |
24 | QAbstractItemModel *QQmlTreeModelToTableModel::model() const |
25 | { |
26 | return m_model; |
27 | } |
28 | |
29 | void QQmlTreeModelToTableModel::connectToModel() |
30 | { |
31 | m_connections = { |
32 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::destroyed, |
33 | context: this, slot: &QQmlTreeModelToTableModel::modelHasBeenDestroyed), |
34 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::modelReset, |
35 | context: this, slot: &QQmlTreeModelToTableModel::modelHasBeenReset), |
36 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::dataChanged, |
37 | context: this, slot: &QQmlTreeModelToTableModel::modelDataChanged), |
38 | |
39 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::layoutAboutToBeChanged, |
40 | context: this, slot: &QQmlTreeModelToTableModel::modelLayoutAboutToBeChanged), |
41 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::layoutChanged, |
42 | context: this, slot: &QQmlTreeModelToTableModel::modelLayoutChanged), |
43 | |
44 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::rowsAboutToBeInserted, |
45 | context: this, slot: &QQmlTreeModelToTableModel::modelRowsAboutToBeInserted), |
46 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::rowsInserted, |
47 | context: this, slot: &QQmlTreeModelToTableModel::modelRowsInserted), |
48 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::rowsAboutToBeRemoved, |
49 | context: this, slot: &QQmlTreeModelToTableModel::modelRowsAboutToBeRemoved), |
50 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::rowsRemoved, |
51 | context: this, slot: &QQmlTreeModelToTableModel::modelRowsRemoved), |
52 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::rowsAboutToBeMoved, |
53 | context: this, slot: &QQmlTreeModelToTableModel::modelRowsAboutToBeMoved), |
54 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::rowsMoved, |
55 | context: this, slot: &QQmlTreeModelToTableModel::modelRowsMoved), |
56 | |
57 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::columnsAboutToBeInserted, |
58 | context: this, slot: &QQmlTreeModelToTableModel::modelColumnsAboutToBeInserted), |
59 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::columnsAboutToBeRemoved, |
60 | context: this, slot: &QQmlTreeModelToTableModel::modelColumnsAboutToBeRemoved), |
61 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::columnsInserted, |
62 | context: this, slot: &QQmlTreeModelToTableModel::modelColumnsInserted), |
63 | QObject::connect(sender: m_model, signal: &QAbstractItemModel::columnsRemoved, |
64 | context: this, slot: &QQmlTreeModelToTableModel::modelColumnsRemoved) |
65 | }; |
66 | } |
67 | |
68 | void QQmlTreeModelToTableModel::setModel(QAbstractItemModel *arg) |
69 | { |
70 | if (m_model != arg) { |
71 | if (m_model) { |
72 | for (const auto &c : m_connections) |
73 | QObject::disconnect(c); |
74 | m_connections.fill(u: {}); |
75 | } |
76 | |
77 | clearModelData(); |
78 | m_model = arg; |
79 | |
80 | if (m_rootIndex.isValid() && m_rootIndex.model() != m_model) |
81 | m_rootIndex = QModelIndex(); |
82 | |
83 | if (m_model) { |
84 | connectToModel(); |
85 | showModelTopLevelItems(); |
86 | } |
87 | |
88 | emit modelChanged(model: arg); |
89 | } |
90 | } |
91 | |
92 | void QQmlTreeModelToTableModel::clearModelData() |
93 | { |
94 | beginResetModel(); |
95 | m_items.clear(); |
96 | m_expandedItems.clear(); |
97 | endResetModel(); |
98 | } |
99 | |
100 | QModelIndex QQmlTreeModelToTableModel::rootIndex() const |
101 | { |
102 | return m_rootIndex; |
103 | } |
104 | |
105 | void QQmlTreeModelToTableModel::setRootIndex(const QModelIndex &idx) |
106 | { |
107 | if (m_rootIndex == idx) |
108 | return; |
109 | |
110 | if (m_model) |
111 | clearModelData(); |
112 | m_rootIndex = idx; |
113 | if (m_model) |
114 | showModelTopLevelItems(); |
115 | emit rootIndexChanged(); |
116 | } |
117 | |
118 | void QQmlTreeModelToTableModel::resetRootIndex() |
119 | { |
120 | setRootIndex(QModelIndex()); |
121 | } |
122 | |
123 | QModelIndex QQmlTreeModelToTableModel::index(int row, int column, const QModelIndex &parent) const |
124 | { |
125 | return hasIndex(row, column, parent) ? createIndex(arow: row, acolumn: column) : QModelIndex(); |
126 | } |
127 | |
128 | QModelIndex QQmlTreeModelToTableModel::parent(const QModelIndex &child) const |
129 | { |
130 | Q_UNUSED(child) |
131 | return QModelIndex(); |
132 | } |
133 | |
134 | QHash<int, QByteArray> QQmlTreeModelToTableModel::roleNames() const |
135 | { |
136 | if (!m_model) |
137 | return QHash<int, QByteArray>(); |
138 | return m_model->roleNames(); |
139 | } |
140 | |
141 | int QQmlTreeModelToTableModel::rowCount(const QModelIndex &) const |
142 | { |
143 | if (!m_model) |
144 | return 0; |
145 | return m_items.size(); |
146 | } |
147 | |
148 | int QQmlTreeModelToTableModel::columnCount(const QModelIndex &parent) const |
149 | { |
150 | if (!m_model) |
151 | return 0; |
152 | return m_model->columnCount(parent); |
153 | } |
154 | |
155 | QVariant QQmlTreeModelToTableModel::data(const QModelIndex &index, int role) const |
156 | { |
157 | if (!m_model) |
158 | return QVariant(); |
159 | |
160 | return m_model->data(index: mapToModel(index), role); |
161 | } |
162 | |
163 | bool QQmlTreeModelToTableModel::setData(const QModelIndex &index, const QVariant &value, int role) |
164 | { |
165 | if (!m_model) |
166 | return false; |
167 | |
168 | return m_model->setData(index: mapToModel(index), value, role); |
169 | } |
170 | |
171 | QVariant QQmlTreeModelToTableModel::headerData(int section, Qt::Orientation orientation, int role) const |
172 | { |
173 | return m_model->headerData(section, orientation, role); |
174 | } |
175 | |
176 | Qt::ItemFlags QQmlTreeModelToTableModel::flags(const QModelIndex &index) const |
177 | { |
178 | return m_model->flags(index: mapToModel(index)); |
179 | } |
180 | |
181 | int QQmlTreeModelToTableModel::depthAtRow(int row) const |
182 | { |
183 | if (row < 0 || row >= m_items.size()) |
184 | return 0; |
185 | return m_items.at(i: row).depth; |
186 | } |
187 | |
188 | int QQmlTreeModelToTableModel::itemIndex(const QModelIndex &index) const |
189 | { |
190 | // This is basically a plagiarism of QTreeViewPrivate::viewIndex() |
191 | if (!index.isValid() || index == m_rootIndex || m_items.isEmpty()) |
192 | return -1; |
193 | |
194 | const int totalCount = m_items.size(); |
195 | |
196 | // We start nearest to the lastViewedItem |
197 | int localCount = qMin(a: m_lastItemIndex - 1, b: totalCount - m_lastItemIndex); |
198 | |
199 | for (int i = 0; i < localCount; ++i) { |
200 | const TreeItem &item1 = m_items.at(i: m_lastItemIndex + i); |
201 | if (item1.index == index) { |
202 | m_lastItemIndex = m_lastItemIndex + i; |
203 | return m_lastItemIndex; |
204 | } |
205 | const TreeItem &item2 = m_items.at(i: m_lastItemIndex - i - 1); |
206 | if (item2.index == index) { |
207 | m_lastItemIndex = m_lastItemIndex - i - 1; |
208 | return m_lastItemIndex; |
209 | } |
210 | } |
211 | |
212 | for (int j = qMax(a: 0, b: m_lastItemIndex + localCount); j < totalCount; ++j) { |
213 | const TreeItem &item = m_items.at(i: j); |
214 | if (item.index == index) { |
215 | m_lastItemIndex = j; |
216 | return j; |
217 | } |
218 | } |
219 | |
220 | for (int j = qMin(a: totalCount, b: m_lastItemIndex - localCount) - 1; j >= 0; --j) { |
221 | const TreeItem &item = m_items.at(i: j); |
222 | if (item.index == index) { |
223 | m_lastItemIndex = j; |
224 | return j; |
225 | } |
226 | } |
227 | |
228 | // nothing found |
229 | return -1; |
230 | } |
231 | |
232 | bool QQmlTreeModelToTableModel::isVisible(const QModelIndex &index) |
233 | { |
234 | return itemIndex(index) != -1; |
235 | } |
236 | |
237 | bool QQmlTreeModelToTableModel::childrenVisible(const QModelIndex &index) |
238 | { |
239 | return (index == m_rootIndex && !m_items.isEmpty()) |
240 | || (m_expandedItems.contains(value: index) && isVisible(index)); |
241 | } |
242 | |
243 | QModelIndex QQmlTreeModelToTableModel::mapToModel(const QModelIndex &index) const |
244 | { |
245 | if (!index.isValid()) |
246 | return QModelIndex(); |
247 | |
248 | const int row = index.row(); |
249 | if (row < 0 || row > m_items.size() - 1) |
250 | return QModelIndex(); |
251 | |
252 | const QModelIndex sourceIndex = m_items.at(i: row).index; |
253 | return m_model->index(row: sourceIndex.row(), column: index.column(), parent: sourceIndex.parent()); |
254 | } |
255 | |
256 | QModelIndex QQmlTreeModelToTableModel::mapFromModel(const QModelIndex &index) const |
257 | { |
258 | if (!index.isValid()) |
259 | return QModelIndex(); |
260 | |
261 | int row = -1; |
262 | for (int i = 0; i < m_items.size(); ++i) { |
263 | const QModelIndex proxyIndex = m_items[i].index; |
264 | if (proxyIndex.row() == index.row() && proxyIndex.parent() == index.parent()) { |
265 | row = i; |
266 | break; |
267 | } |
268 | } |
269 | |
270 | if (row == -1) |
271 | return QModelIndex(); |
272 | |
273 | return this->index(row, column: index.column()); |
274 | } |
275 | |
276 | QModelIndex QQmlTreeModelToTableModel::mapToModel(int row) const |
277 | { |
278 | if (row < 0 || row >= m_items.size()) |
279 | return QModelIndex(); |
280 | return m_items.at(i: row).index; |
281 | } |
282 | |
283 | QItemSelection QQmlTreeModelToTableModel::selectionForRowRange(const QModelIndex &fromIndex, const QModelIndex &toIndex) const |
284 | { |
285 | int from = itemIndex(index: fromIndex); |
286 | int to = itemIndex(index: toIndex); |
287 | if (from == -1) { |
288 | if (to == -1) |
289 | return QItemSelection(); |
290 | return QItemSelection(toIndex, toIndex); |
291 | } |
292 | |
293 | to = qMax(a: to, b: 0); |
294 | if (from > to) |
295 | qSwap(value1&: from, value2&: to); |
296 | |
297 | typedef QPair<QModelIndex, QModelIndex> MIPair; |
298 | typedef QHash<QModelIndex, MIPair> MI2MIPairHash; |
299 | MI2MIPairHash ranges; |
300 | QModelIndex firstIndex = m_items.at(i: from).index; |
301 | QModelIndex lastIndex = firstIndex; |
302 | QModelIndex previousParent = firstIndex.parent(); |
303 | bool selectLastRow = false; |
304 | for (int i = from + 1; i <= to || (selectLastRow = true); i++) { |
305 | // We run an extra iteration to make sure the last row is |
306 | // added to the selection. (And also to avoid duplicating |
307 | // the insertion code.) |
308 | QModelIndex index; |
309 | QModelIndex parent; |
310 | if (!selectLastRow) { |
311 | index = m_items.at(i).index; |
312 | parent = index.parent(); |
313 | } |
314 | if (selectLastRow || previousParent != parent) { |
315 | const MI2MIPairHash::iterator &it = ranges.find(key: previousParent); |
316 | if (it == ranges.end()) |
317 | ranges.insert(key: previousParent, value: MIPair(firstIndex, lastIndex)); |
318 | else |
319 | it->second = lastIndex; |
320 | |
321 | if (selectLastRow) |
322 | break; |
323 | |
324 | firstIndex = index; |
325 | previousParent = parent; |
326 | } |
327 | lastIndex = index; |
328 | } |
329 | |
330 | QItemSelection sel; |
331 | sel.reserve(asize: ranges.size()); |
332 | for (const MIPair &pair : std::as_const(t&: ranges)) |
333 | sel.append(t: QItemSelectionRange(pair.first, pair.second)); |
334 | |
335 | return sel; |
336 | } |
337 | |
338 | void QQmlTreeModelToTableModel::showModelTopLevelItems(bool doInsertRows) |
339 | { |
340 | if (!m_model) |
341 | return; |
342 | |
343 | if (m_model->hasChildren(parent: m_rootIndex) && m_model->canFetchMore(parent: m_rootIndex)) |
344 | m_model->fetchMore(parent: m_rootIndex); |
345 | const long topLevelRowCount = m_model->rowCount(parent: m_rootIndex); |
346 | if (topLevelRowCount == 0) |
347 | return; |
348 | |
349 | showModelChildItems(parent: TreeItem(m_rootIndex), start: 0, end: topLevelRowCount - 1, doInsertRows); |
350 | } |
351 | |
352 | void QQmlTreeModelToTableModel::showModelChildItems(const TreeItem &parentItem, int start, int end, bool doInsertRows, bool doExpandPendingRows) |
353 | { |
354 | const QModelIndex &parentIndex = parentItem.index; |
355 | int rowIdx = parentIndex.isValid() && parentIndex != m_rootIndex ? itemIndex(index: parentIndex) + 1 : 0; |
356 | Q_ASSERT(rowIdx == 0 || parentItem.expanded); |
357 | if (parentIndex.isValid() && parentIndex != m_rootIndex && (rowIdx == 0 || !parentItem.expanded)) |
358 | return; |
359 | |
360 | if (m_model->rowCount(parent: parentIndex) == 0) { |
361 | if (m_model->hasChildren(parent: parentIndex) && m_model->canFetchMore(parent: parentIndex)) |
362 | m_model->fetchMore(parent: parentIndex); |
363 | return; |
364 | } |
365 | |
366 | int insertCount = end - start + 1; |
367 | int startIdx; |
368 | if (start == 0) { |
369 | startIdx = rowIdx; |
370 | } else { |
371 | // Prefer to insert before next sibling instead of after last child of previous, as |
372 | // the latter is potentially buggy, see QTBUG-66062 |
373 | const QModelIndex &nextSiblingIdx = m_model->index(row: end + 1, column: 0, parent: parentIndex); |
374 | if (nextSiblingIdx.isValid()) { |
375 | startIdx = itemIndex(index: nextSiblingIdx); |
376 | } else { |
377 | const QModelIndex &prevSiblingIdx = m_model->index(row: start - 1, column: 0, parent: parentIndex); |
378 | startIdx = lastChildIndex(index: prevSiblingIdx) + 1; |
379 | } |
380 | } |
381 | |
382 | int rowDepth = rowIdx == 0 ? 0 : parentItem.depth + 1; |
383 | if (doInsertRows) |
384 | beginInsertRows(parent: QModelIndex(), first: startIdx, last: startIdx + insertCount - 1); |
385 | m_items.reserve(asize: m_items.size() + insertCount); |
386 | |
387 | for (int i = 0; i < insertCount; i++) { |
388 | const QModelIndex &cmi = m_model->index(row: start + i, column: 0, parent: parentIndex); |
389 | const bool expanded = m_expandedItems.contains(value: cmi); |
390 | const TreeItem treeItem(cmi, rowDepth, expanded); |
391 | m_items.insert(i: startIdx + i, t: treeItem); |
392 | |
393 | if (expanded) |
394 | m_itemsToExpand.append(t: treeItem); |
395 | } |
396 | |
397 | if (doInsertRows) |
398 | endInsertRows(); |
399 | |
400 | if (doExpandPendingRows) |
401 | expandPendingRows(doInsertRows); |
402 | } |
403 | |
404 | |
405 | void QQmlTreeModelToTableModel::expand(const QModelIndex &idx) |
406 | { |
407 | ASSERT_CONSISTENCY(); |
408 | if (!m_model) |
409 | return; |
410 | |
411 | Q_ASSERT(!idx.isValid() || idx.model() == m_model); |
412 | |
413 | if (!idx.isValid() || !m_model->hasChildren(parent: idx)) |
414 | return; |
415 | if (m_expandedItems.contains(value: idx)) |
416 | return; |
417 | |
418 | int row = itemIndex(index: idx); |
419 | if (row != -1) |
420 | expandRow(n: row); |
421 | else |
422 | m_expandedItems.insert(value: idx); |
423 | ASSERT_CONSISTENCY(); |
424 | |
425 | emit expanded(index: idx); |
426 | } |
427 | |
428 | void QQmlTreeModelToTableModel::collapse(const QModelIndex &idx) |
429 | { |
430 | ASSERT_CONSISTENCY(); |
431 | if (!m_model) |
432 | return; |
433 | |
434 | Q_ASSERT(!idx.isValid() || idx.model() == m_model); |
435 | |
436 | if (!idx.isValid() || !m_model->hasChildren(parent: idx)) |
437 | return; |
438 | if (!m_expandedItems.contains(value: idx)) |
439 | return; |
440 | |
441 | int row = itemIndex(index: idx); |
442 | if (row != -1) |
443 | collapseRow(n: row); |
444 | else |
445 | m_expandedItems.remove(value: idx); |
446 | ASSERT_CONSISTENCY(); |
447 | |
448 | emit collapsed(index: idx); |
449 | } |
450 | |
451 | bool QQmlTreeModelToTableModel::isExpanded(const QModelIndex &index) const |
452 | { |
453 | ASSERT_CONSISTENCY(); |
454 | if (!m_model) |
455 | return false; |
456 | |
457 | Q_ASSERT(!index.isValid() || index.model() == m_model); |
458 | return !index.isValid() || m_expandedItems.contains(value: index); |
459 | } |
460 | |
461 | bool QQmlTreeModelToTableModel::isExpanded(int row) const |
462 | { |
463 | if (row < 0 || row >= m_items.size()) |
464 | return false; |
465 | return m_items.at(i: row).expanded; |
466 | } |
467 | |
468 | bool QQmlTreeModelToTableModel::hasChildren(int row) const |
469 | { |
470 | if (row < 0 || row >= m_items.size()) |
471 | return false; |
472 | return m_model->hasChildren(parent: m_items[row].index); |
473 | } |
474 | |
475 | bool QQmlTreeModelToTableModel::hasSiblings(int row) const |
476 | { |
477 | const QModelIndex &index = mapToModel(row); |
478 | return index.row() != m_model->rowCount(parent: index.parent()) - 1; |
479 | } |
480 | |
481 | void QQmlTreeModelToTableModel::expandRow(int n) |
482 | { |
483 | if (!m_model || isExpanded(row: n)) |
484 | return; |
485 | |
486 | TreeItem &item = m_items[n]; |
487 | if ((item.index.flags() & Qt::ItemNeverHasChildren) || !m_model->hasChildren(parent: item.index)) |
488 | return; |
489 | item.expanded = true; |
490 | m_expandedItems.insert(value: item.index); |
491 | QVector<int> changedRole(1, ExpandedRole); |
492 | emit dataChanged(topLeft: index(row: n, column: m_column), bottomRight: index(row: n, column: m_column), roles: changedRole); |
493 | |
494 | m_itemsToExpand.append(t: item); |
495 | expandPendingRows(); |
496 | } |
497 | |
498 | void QQmlTreeModelToTableModel::expandRecursively(int row, int depth) |
499 | { |
500 | Q_ASSERT(depth == -1 || depth > 0); |
501 | const int startDepth = depthAtRow(row); |
502 | |
503 | auto expandHelp = [this, depth, startDepth] (const auto expandHelp, const QModelIndex &index) -> void { |
504 | const int rowToExpand = itemIndex(index); |
505 | if (!m_expandedItems.contains(value: index)) |
506 | expandRow(n: rowToExpand); |
507 | |
508 | if (depth != -1 && depthAtRow(row: rowToExpand) == startDepth + depth - 1) |
509 | return; |
510 | |
511 | const int childCount = m_model->rowCount(parent: index); |
512 | for (int childRow = 0; childRow < childCount; ++childRow) { |
513 | const QModelIndex childIndex = m_model->index(row: childRow, column: 0, parent: index); |
514 | if (m_model->hasChildren(parent: childIndex)) |
515 | expandHelp(expandHelp, childIndex); |
516 | } |
517 | }; |
518 | |
519 | const QModelIndex index = m_items[row].index; |
520 | if (index.isValid()) |
521 | expandHelp(expandHelp, index); |
522 | } |
523 | |
524 | void QQmlTreeModelToTableModel::expandPendingRows(bool doInsertRows) |
525 | { |
526 | while (!m_itemsToExpand.isEmpty()) { |
527 | const TreeItem item = m_itemsToExpand.takeFirst(); |
528 | Q_ASSERT(item.expanded); |
529 | const QModelIndex &index = item.index; |
530 | int childrenCount = m_model->rowCount(parent: index); |
531 | if (childrenCount == 0) { |
532 | if (m_model->hasChildren(parent: index) && m_model->canFetchMore(parent: index)) |
533 | m_model->fetchMore(parent: index); |
534 | continue; |
535 | } |
536 | |
537 | // TODO Pre-compute the total number of items made visible |
538 | // so that we only call a single beginInsertRows()/endInsertRows() |
539 | // pair per expansion (same as we do for collapsing). |
540 | showModelChildItems(parentItem: item, start: 0, end: childrenCount - 1, doInsertRows, doExpandPendingRows: false); |
541 | } |
542 | } |
543 | |
544 | void QQmlTreeModelToTableModel::collapseRecursively(int row) |
545 | { |
546 | auto collapseHelp = [this] (const auto collapseHelp, const QModelIndex &index) -> void { |
547 | if (m_expandedItems.contains(value: index)) { |
548 | const int rowToCollapse = itemIndex(index); |
549 | if (rowToCollapse != -1) |
550 | collapseRow(n: rowToCollapse); |
551 | else |
552 | m_expandedItems.remove(value: index); |
553 | } |
554 | |
555 | const int childCount = m_model->rowCount(parent: index); |
556 | for (int childRow = 0; childRow < childCount; ++childRow) { |
557 | const QModelIndex childIndex = m_model->index(row: childRow, column: 0, parent: index); |
558 | if (m_model->hasChildren(parent: childIndex)) |
559 | collapseHelp(collapseHelp, childIndex); |
560 | } |
561 | }; |
562 | |
563 | const QModelIndex index = m_items[row].index; |
564 | if (index.isValid()) |
565 | collapseHelp(collapseHelp, index); |
566 | } |
567 | |
568 | void QQmlTreeModelToTableModel::collapseRow(int n) |
569 | { |
570 | if (!m_model || !isExpanded(row: n)) |
571 | return; |
572 | |
573 | SignalFreezer aggregator(this); |
574 | |
575 | TreeItem &item = m_items[n]; |
576 | item.expanded = false; |
577 | m_expandedItems.remove(value: item.index); |
578 | QVector<int> changedRole(1, ExpandedRole); |
579 | queueDataChanged(topLeft: index(row: n, column: m_column), bottomRight: index(row: n, column: m_column), roles: changedRole); |
580 | int childrenCount = m_model->rowCount(parent: item.index); |
581 | if ((item.index.flags() & Qt::ItemNeverHasChildren) || !m_model->hasChildren(parent: item.index) || childrenCount == 0) |
582 | return; |
583 | |
584 | const QModelIndex &emi = m_model->index(row: childrenCount - 1, column: 0, parent: item.index); |
585 | int lastIndex = lastChildIndex(index: emi); |
586 | removeVisibleRows(startIndex: n + 1, endIndex: lastIndex); |
587 | } |
588 | |
589 | int QQmlTreeModelToTableModel::lastChildIndex(const QModelIndex &index) const |
590 | { |
591 | // The purpose of this function is to return the row of the last decendant of a node N. |
592 | // But note: index should point to the last child of N, and not N itself! |
593 | // This means that if index is not expanded, the last child will simply be index itself. |
594 | // Otherwise, since the tree underneath index can be of any depth, it will instead find |
595 | // the first sibling of N, get its table row, and simply return the row above. |
596 | if (!m_expandedItems.contains(value: index)) |
597 | return itemIndex(index); |
598 | |
599 | QModelIndex parent = index.parent(); |
600 | QModelIndex nextSiblingIndex; |
601 | while (parent.isValid()) { |
602 | nextSiblingIndex = parent.sibling(arow: parent.row() + 1, acolumn: 0); |
603 | if (nextSiblingIndex.isValid()) |
604 | break; |
605 | parent = parent.parent(); |
606 | } |
607 | |
608 | int firstIndex = nextSiblingIndex.isValid() ? itemIndex(index: nextSiblingIndex) : m_items.size(); |
609 | return firstIndex - 1; |
610 | } |
611 | |
612 | void QQmlTreeModelToTableModel::removeVisibleRows(int startIndex, int endIndex, bool doRemoveRows) |
613 | { |
614 | if (startIndex < 0 || endIndex < 0 || startIndex > endIndex) |
615 | return; |
616 | |
617 | if (doRemoveRows) |
618 | beginRemoveRows(parent: QModelIndex(), first: startIndex, last: endIndex); |
619 | m_items.erase(abegin: m_items.begin() + startIndex, aend: m_items.begin() + endIndex + 1); |
620 | if (doRemoveRows) { |
621 | endRemoveRows(); |
622 | |
623 | /* We need to update the model index for all the items below the removed ones */ |
624 | int lastIndex = m_items.size() - 1; |
625 | if (startIndex <= lastIndex) { |
626 | const QModelIndex &topLeft = index(row: startIndex, column: 0, parent: QModelIndex()); |
627 | const QModelIndex &bottomRight = index(row: lastIndex, column: 0, parent: QModelIndex()); |
628 | const QVector<int> changedRole(1, ModelIndexRole); |
629 | queueDataChanged(topLeft, bottomRight, roles: changedRole); |
630 | } |
631 | } |
632 | } |
633 | |
634 | void QQmlTreeModelToTableModel::modelHasBeenDestroyed() |
635 | { |
636 | // The model has been deleted. This should behave as if no model was set |
637 | clearModelData(); |
638 | emit modelChanged(model: nullptr); |
639 | } |
640 | |
641 | void QQmlTreeModelToTableModel::modelHasBeenReset() |
642 | { |
643 | clearModelData(); |
644 | |
645 | showModelTopLevelItems(); |
646 | ASSERT_CONSISTENCY(); |
647 | } |
648 | |
649 | void QQmlTreeModelToTableModel::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) |
650 | { |
651 | Q_ASSERT(topLeft.parent() == bottomRight.parent()); |
652 | const QModelIndex &parent = topLeft.parent(); |
653 | if (parent.isValid() && !childrenVisible(index: parent)) { |
654 | ASSERT_CONSISTENCY(); |
655 | return; |
656 | } |
657 | |
658 | int topIndex = itemIndex(index: topLeft.siblingAtColumn(acolumn: 0)); |
659 | if (topIndex == -1) // 'parent' is not visible anymore, though it's been expanded previously |
660 | return; |
661 | for (int i = topLeft.row(); i <= bottomRight.row(); i++) { |
662 | // Group items with same parent to minize the number of 'dataChanged()' emits |
663 | int bottomIndex = topIndex; |
664 | while (bottomIndex < m_items.size()) { |
665 | const QModelIndex &idx = m_items.at(i: bottomIndex).index; |
666 | if (idx.parent() != parent) { |
667 | --bottomIndex; |
668 | break; |
669 | } |
670 | if (idx.row() == bottomRight.row()) |
671 | break; |
672 | ++bottomIndex; |
673 | } |
674 | emit dataChanged(topLeft: index(row: topIndex, column: topLeft.column()), bottomRight: index(row: bottomIndex, column: bottomRight.column()), roles); |
675 | |
676 | i += bottomIndex - topIndex; |
677 | if (i == bottomRight.row()) |
678 | break; |
679 | topIndex = bottomIndex + 1; |
680 | while (topIndex < m_items.size() |
681 | && m_items.at(i: topIndex).index.parent() != parent) |
682 | topIndex++; |
683 | } |
684 | ASSERT_CONSISTENCY(); |
685 | } |
686 | |
687 | void QQmlTreeModelToTableModel::modelLayoutAboutToBeChanged(const QList<QPersistentModelIndex> &parents, QAbstractItemModel::LayoutChangeHint hint) |
688 | { |
689 | Q_UNUSED(hint) |
690 | |
691 | // Since the m_items is a list of TreeItems that contains QPersistentModelIndexes, we |
692 | // cannot wait until we get a modelLayoutChanged() before we remove the affected rows |
693 | // from that list. After the layout has changed, the list (or, the persistent indexes |
694 | // that it contains) is no longer in sync with the model (after all, that is what we're |
695 | // supposed to correct in modelLayoutChanged()). |
696 | // This means that vital functions, like itemIndex(index), cannot be trusted at that point. |
697 | // Therefore we need to do the update in two steps; First remove all the affected rows |
698 | // from here (while we're still in sync with the model), and then add back the |
699 | // affected rows, and notify about it, from modelLayoutChanged(). |
700 | m_modelLayoutChanged = false; |
701 | |
702 | if (parents.isEmpty() || !parents[0].isValid()) { |
703 | // Update entire model |
704 | emit layoutAboutToBeChanged(); |
705 | m_modelLayoutChanged = true; |
706 | m_items.clear(); |
707 | return; |
708 | } |
709 | |
710 | for (const QPersistentModelIndex &pmi : parents) { |
711 | if (!m_expandedItems.contains(value: pmi)) |
712 | continue; |
713 | const int row = itemIndex(index: pmi); |
714 | if (row == -1) |
715 | continue; |
716 | const int rowCount = m_model->rowCount(parent: pmi); |
717 | if (rowCount == 0) |
718 | continue; |
719 | |
720 | if (!m_modelLayoutChanged) { |
721 | emit layoutAboutToBeChanged(); |
722 | m_modelLayoutChanged = true; |
723 | } |
724 | |
725 | const QModelIndex &lmi = m_model->index(row: rowCount - 1, column: 0, parent: pmi); |
726 | const int lastRow = lastChildIndex(index: lmi); |
727 | removeVisibleRows(startIndex: row + 1, endIndex: lastRow, doRemoveRows: false /*doRemoveRows*/); |
728 | } |
729 | |
730 | ASSERT_CONSISTENCY(); |
731 | } |
732 | |
733 | void QQmlTreeModelToTableModel::modelLayoutChanged(const QList<QPersistentModelIndex> &parents, QAbstractItemModel::LayoutChangeHint hint) |
734 | { |
735 | Q_UNUSED(hint) |
736 | |
737 | if (!m_modelLayoutChanged) { |
738 | // No relevant changes done from modelLayoutAboutToBeChanged() |
739 | return; |
740 | } |
741 | |
742 | if (m_items.isEmpty()) { |
743 | // Entire model has changed. Add back all rows. |
744 | showModelTopLevelItems(doInsertRows: false /*doInsertRows*/); |
745 | const QModelIndex &mi = m_model->index(row: 0, column: 0); |
746 | const int columnCount = m_model->columnCount(parent: mi); |
747 | emit dataChanged(topLeft: index(row: 0, column: 0), bottomRight: index(row: m_items.size() - 1, column: columnCount - 1)); |
748 | emit layoutChanged(); |
749 | return; |
750 | } |
751 | |
752 | for (const QPersistentModelIndex &pmi : parents) { |
753 | if (!m_expandedItems.contains(value: pmi)) |
754 | continue; |
755 | const int row = itemIndex(index: pmi); |
756 | if (row == -1) |
757 | continue; |
758 | const int rowCount = m_model->rowCount(parent: pmi); |
759 | if (rowCount == 0) |
760 | continue; |
761 | |
762 | const QModelIndex &lmi = m_model->index(row: rowCount - 1, column: 0, parent: pmi); |
763 | const int columnCount = m_model->columnCount(parent: lmi); |
764 | showModelChildItems(parentItem: m_items.at(i: row), start: 0, end: rowCount - 1, doInsertRows: false /*doInsertRows*/); |
765 | const int lastRow = lastChildIndex(index: lmi); |
766 | emit dataChanged(topLeft: index(row: row + 1, column: 0), bottomRight: index(row: lastRow, column: columnCount - 1)); |
767 | } |
768 | |
769 | emit layoutChanged(); |
770 | |
771 | ASSERT_CONSISTENCY(); |
772 | } |
773 | |
774 | void QQmlTreeModelToTableModel::modelRowsAboutToBeInserted(const QModelIndex & parent, int start, int end) |
775 | { |
776 | Q_UNUSED(parent) |
777 | Q_UNUSED(start) |
778 | Q_UNUSED(end) |
779 | ASSERT_CONSISTENCY(); |
780 | } |
781 | |
782 | void QQmlTreeModelToTableModel::modelRowsInserted(const QModelIndex & parent, int start, int end) |
783 | { |
784 | TreeItem item; |
785 | int parentRow = itemIndex(index: parent); |
786 | if (parentRow >= 0) { |
787 | const QModelIndex& parentIndex = index(row: parentRow, column: m_column); |
788 | QVector<int> changedRole(1, HasChildrenRole); |
789 | queueDataChanged(topLeft: parentIndex, bottomRight: parentIndex, roles: changedRole); |
790 | item = m_items.at(i: parentRow); |
791 | if (!item.expanded) { |
792 | ASSERT_CONSISTENCY(); |
793 | return; |
794 | } |
795 | } else if (parent == m_rootIndex) { |
796 | item = TreeItem(parent); |
797 | } else { |
798 | ASSERT_CONSISTENCY(); |
799 | return; |
800 | } |
801 | showModelChildItems(parentItem: item, start, end); |
802 | ASSERT_CONSISTENCY(); |
803 | } |
804 | |
805 | void QQmlTreeModelToTableModel::modelRowsAboutToBeRemoved(const QModelIndex & parent, int start, int end) |
806 | { |
807 | ASSERT_CONSISTENCY(); |
808 | enableSignalAggregation(); |
809 | if (parent == m_rootIndex || childrenVisible(index: parent)) { |
810 | const QModelIndex &smi = m_model->index(row: start, column: 0, parent); |
811 | int startIndex = itemIndex(index: smi); |
812 | const QModelIndex &emi = m_model->index(row: end, column: 0, parent); |
813 | int endIndex = -1; |
814 | if (isExpanded(index: emi)) { |
815 | int rowCount = m_model->rowCount(parent: emi); |
816 | if (rowCount > 0) { |
817 | const QModelIndex &idx = m_model->index(row: rowCount - 1, column: 0, parent: emi); |
818 | endIndex = lastChildIndex(index: idx); |
819 | } |
820 | } |
821 | if (endIndex == -1) |
822 | endIndex = itemIndex(index: emi); |
823 | |
824 | removeVisibleRows(startIndex, endIndex); |
825 | } |
826 | |
827 | for (int r = start; r <= end; r++) { |
828 | const QModelIndex &cmi = m_model->index(row: r, column: 0, parent); |
829 | m_expandedItems.remove(value: cmi); |
830 | } |
831 | } |
832 | |
833 | void QQmlTreeModelToTableModel::modelRowsRemoved(const QModelIndex & parent, int start, int end) |
834 | { |
835 | Q_UNUSED(start) |
836 | Q_UNUSED(end) |
837 | int parentRow = itemIndex(index: parent); |
838 | if (parentRow >= 0) { |
839 | const QModelIndex& parentIndex = index(row: parentRow, column: m_column); |
840 | QVector<int> changedRole(1, HasChildrenRole); |
841 | queueDataChanged(topLeft: parentIndex, bottomRight: parentIndex, roles: changedRole); |
842 | } |
843 | disableSignalAggregation(); |
844 | ASSERT_CONSISTENCY(); |
845 | } |
846 | |
847 | void QQmlTreeModelToTableModel::modelRowsAboutToBeMoved(const QModelIndex & sourceParent, int sourceStart, int sourceEnd, const QModelIndex & destinationParent, int destinationRow) |
848 | { |
849 | ASSERT_CONSISTENCY(); |
850 | enableSignalAggregation(); |
851 | m_visibleRowsMoved = false; |
852 | if (!childrenVisible(index: sourceParent)) |
853 | return; // Do nothing now. See modelRowsMoved() below. |
854 | |
855 | if (!childrenVisible(index: destinationParent)) { |
856 | modelRowsAboutToBeRemoved(parent: sourceParent, start: sourceStart, end: sourceEnd); |
857 | /* If the destination parent has no children, we'll need to |
858 | * report a change on the HasChildrenRole */ |
859 | if (isVisible(index: destinationParent) && m_model->rowCount(parent: destinationParent) == 0) { |
860 | const QModelIndex &topLeft = index(row: itemIndex(index: destinationParent), column: 0, parent: QModelIndex()); |
861 | const QModelIndex &bottomRight = topLeft; |
862 | const QVector<int> changedRole(1, HasChildrenRole); |
863 | queueDataChanged(topLeft, bottomRight, roles: changedRole); |
864 | } |
865 | } else { |
866 | int depthDifference = -1; |
867 | if (destinationParent.isValid()) { |
868 | int destParentIndex = itemIndex(index: destinationParent); |
869 | depthDifference = m_items.at(i: destParentIndex).depth; |
870 | } |
871 | if (sourceParent.isValid()) { |
872 | int sourceParentIndex = itemIndex(index: sourceParent); |
873 | depthDifference -= m_items.at(i: sourceParentIndex).depth; |
874 | } else { |
875 | depthDifference++; |
876 | } |
877 | |
878 | int startIndex = itemIndex(index: m_model->index(row: sourceStart, column: 0, parent: sourceParent)); |
879 | const QModelIndex &emi = m_model->index(row: sourceEnd, column: 0, parent: sourceParent); |
880 | int endIndex = -1; |
881 | if (isExpanded(index: emi)) { |
882 | int rowCount = m_model->rowCount(parent: emi); |
883 | if (rowCount > 0) |
884 | endIndex = lastChildIndex(index: m_model->index(row: rowCount - 1, column: 0, parent: emi)); |
885 | } |
886 | if (endIndex == -1) |
887 | endIndex = itemIndex(index: emi); |
888 | |
889 | int destIndex = -1; |
890 | if (destinationRow == m_model->rowCount(parent: destinationParent)) { |
891 | const QModelIndex &emi = m_model->index(row: destinationRow - 1, column: 0, parent: destinationParent); |
892 | destIndex = lastChildIndex(index: emi) + 1; |
893 | } else { |
894 | destIndex = itemIndex(index: m_model->index(row: destinationRow, column: 0, parent: destinationParent)); |
895 | } |
896 | |
897 | int totalMovedCount = endIndex - startIndex + 1; |
898 | |
899 | /* This beginMoveRows() is matched by a endMoveRows() in the |
900 | * modelRowsMoved() method below. */ |
901 | m_visibleRowsMoved = startIndex != destIndex && |
902 | beginMoveRows(sourceParent: QModelIndex(), sourceFirst: startIndex, sourceLast: endIndex, destinationParent: QModelIndex(), destinationRow: destIndex); |
903 | |
904 | const QList<TreeItem> &buffer = m_items.mid(pos: startIndex, len: totalMovedCount); |
905 | int bufferCopyOffset; |
906 | if (destIndex > endIndex) { |
907 | for (int i = endIndex + 1; i < destIndex; i++) { |
908 | m_items.swapItemsAt(i, j: i - totalMovedCount); // Fast move from 1st to 2nd position |
909 | } |
910 | bufferCopyOffset = destIndex - totalMovedCount; |
911 | } else { |
912 | // NOTE: we will not enter this loop if startIndex == destIndex |
913 | for (int i = startIndex - 1; i >= destIndex; i--) { |
914 | m_items.swapItemsAt(i, j: i + totalMovedCount); // Fast move from 1st to 2nd position |
915 | } |
916 | bufferCopyOffset = destIndex; |
917 | } |
918 | for (int i = 0; i < buffer.size(); i++) { |
919 | TreeItem item = buffer.at(i); |
920 | item.depth += depthDifference; |
921 | m_items.replace(i: bufferCopyOffset + i, t: item); |
922 | } |
923 | |
924 | /* If both source and destination items are visible, the indexes of |
925 | * all the items in between will change. If they share the same |
926 | * parent, then this is all; however, if they belong to different |
927 | * parents, their bottom siblings will also get displaced, so their |
928 | * index also needs to be updated. |
929 | * Given that the bottom siblings of the top moved elements are |
930 | * already included in the update (since they lie between the |
931 | * source and the dest elements), we only need to worry about the |
932 | * siblings of the bottom moved element. |
933 | */ |
934 | const int top = qMin(a: startIndex, b: bufferCopyOffset); |
935 | int bottom = qMax(a: endIndex, b: bufferCopyOffset + totalMovedCount - 1); |
936 | if (sourceParent != destinationParent) { |
937 | const QModelIndex &bottomParent = |
938 | bottom == endIndex ? sourceParent : destinationParent; |
939 | |
940 | const int rowCount = m_model->rowCount(parent: bottomParent); |
941 | if (rowCount > 0) |
942 | bottom = qMax(a: bottom, b: lastChildIndex(index: m_model->index(row: rowCount - 1, column: 0, parent: bottomParent))); |
943 | } |
944 | const QModelIndex &topLeft = index(row: top, column: 0, parent: QModelIndex()); |
945 | const QModelIndex &bottomRight = index(row: bottom, column: 0, parent: QModelIndex()); |
946 | const QVector<int> changedRole(1, ModelIndexRole); |
947 | queueDataChanged(topLeft, bottomRight, roles: changedRole); |
948 | |
949 | if (depthDifference != 0) { |
950 | const QModelIndex &topLeft = index(row: bufferCopyOffset, column: 0, parent: QModelIndex()); |
951 | const QModelIndex &bottomRight = index(row: bufferCopyOffset + totalMovedCount - 1, column: 0, parent: QModelIndex()); |
952 | const QVector<int> changedRole(1, DepthRole); |
953 | queueDataChanged(topLeft, bottomRight, roles: changedRole); |
954 | } |
955 | } |
956 | } |
957 | |
958 | void QQmlTreeModelToTableModel::modelRowsMoved(const QModelIndex & sourceParent, int sourceStart, int sourceEnd, const QModelIndex & destinationParent, int destinationRow) |
959 | { |
960 | if (!childrenVisible(index: sourceParent)) { |
961 | modelRowsInserted(parent: destinationParent, start: destinationRow, end: destinationRow + sourceEnd - sourceStart); |
962 | } else if (!childrenVisible(index: destinationParent)) { |
963 | modelRowsRemoved(parent: sourceParent, start: sourceStart, end: sourceEnd); |
964 | } |
965 | |
966 | if (m_visibleRowsMoved) |
967 | endMoveRows(); |
968 | |
969 | if (isVisible(index: sourceParent) && m_model->rowCount(parent: sourceParent) == 0) { |
970 | int parentRow = itemIndex(index: sourceParent); |
971 | collapseRow(n: parentRow); |
972 | const QModelIndex &topLeft = index(row: parentRow, column: 0, parent: QModelIndex()); |
973 | const QModelIndex &bottomRight = topLeft; |
974 | const QVector<int> changedRole { ExpandedRole, HasChildrenRole }; |
975 | queueDataChanged(topLeft, bottomRight, roles: changedRole); |
976 | } |
977 | |
978 | disableSignalAggregation(); |
979 | |
980 | ASSERT_CONSISTENCY(); |
981 | } |
982 | |
983 | void QQmlTreeModelToTableModel::modelColumnsAboutToBeInserted(const QModelIndex & parent, int start, int end) |
984 | { |
985 | Q_UNUSED(parent); |
986 | beginInsertColumns(parent: {}, first: start, last: end); |
987 | } |
988 | |
989 | void QQmlTreeModelToTableModel::modelColumnsAboutToBeRemoved(const QModelIndex & parent, int start, int end) |
990 | { |
991 | Q_UNUSED(parent); |
992 | beginRemoveColumns(parent: {}, first: start, last: end); |
993 | } |
994 | |
995 | void QQmlTreeModelToTableModel::modelColumnsInserted(const QModelIndex & parent, int start, int end) |
996 | { |
997 | Q_UNUSED(parent); |
998 | Q_UNUSED(start); |
999 | Q_UNUSED(end); |
1000 | endInsertColumns(); |
1001 | m_items.clear(); |
1002 | showModelTopLevelItems(); |
1003 | ASSERT_CONSISTENCY(); |
1004 | } |
1005 | |
1006 | void QQmlTreeModelToTableModel::modelColumnsRemoved(const QModelIndex & parent, int start, int end) |
1007 | { |
1008 | Q_UNUSED(parent); |
1009 | Q_UNUSED(start); |
1010 | Q_UNUSED(end); |
1011 | endRemoveColumns(); |
1012 | m_items.clear(); |
1013 | showModelTopLevelItems(); |
1014 | ASSERT_CONSISTENCY(); |
1015 | } |
1016 | |
1017 | void QQmlTreeModelToTableModel::dump() const |
1018 | { |
1019 | if (!m_model) |
1020 | return; |
1021 | int count = m_items.size(); |
1022 | if (count == 0) |
1023 | return; |
1024 | int countWidth = floor(x: log10(x: double(count))) + 1; |
1025 | qInfo() << "Dumping"<< this; |
1026 | for (int i = 0; i < count; i++) { |
1027 | const TreeItem &item = m_items.at(i); |
1028 | bool hasChildren = m_model->hasChildren(parent: item.index); |
1029 | int children = m_model->rowCount(parent: item.index); |
1030 | qInfo().noquote().nospace() |
1031 | << QStringLiteral("%1 ").arg(a: i, fieldWidth: countWidth) << QString(4 * item.depth, QChar::fromLatin1(c: '.')) |
1032 | << QLatin1String(!hasChildren ? ".. ": item.expanded ? " v ": " > ") |
1033 | << item.index << children; |
1034 | } |
1035 | } |
1036 | |
1037 | bool QQmlTreeModelToTableModel::testConsistency(bool dumpOnFail) const |
1038 | { |
1039 | if (!m_model) { |
1040 | if (!m_items.isEmpty()) { |
1041 | qWarning() << "Model inconsistency: No model but stored visible items"; |
1042 | return false; |
1043 | } |
1044 | if (!m_expandedItems.isEmpty()) { |
1045 | qWarning() << "Model inconsistency: No model but stored expanded items"; |
1046 | return false; |
1047 | } |
1048 | return true; |
1049 | } |
1050 | QModelIndex parent = m_rootIndex; |
1051 | QStack<QModelIndex> ancestors; |
1052 | QModelIndex idx = m_model->index(row: 0, column: 0, parent); |
1053 | for (int i = 0; i < m_items.size(); i++) { |
1054 | bool isConsistent = true; |
1055 | const TreeItem &item = m_items.at(i); |
1056 | if (item.index != idx) { |
1057 | qWarning() << "QModelIndex inconsistency"<< i << item.index; |
1058 | qWarning() << " expected"<< idx; |
1059 | isConsistent = false; |
1060 | } |
1061 | if (item.index.parent() != parent) { |
1062 | qWarning() << "Parent inconsistency"<< i << item.index; |
1063 | qWarning() << " stored index parent"<< item.index.parent() << "model parent"<< parent; |
1064 | isConsistent = false; |
1065 | } |
1066 | if (item.depth != ancestors.size()) { |
1067 | qWarning() << "Depth inconsistency"<< i << item.index; |
1068 | qWarning() << " item depth"<< item.depth << "ancestors stack"<< ancestors.size(); |
1069 | isConsistent = false; |
1070 | } |
1071 | if (item.expanded && !m_expandedItems.contains(value: item.index)) { |
1072 | qWarning() << "Expanded inconsistency"<< i << item.index; |
1073 | qWarning() << " set"<< m_expandedItems.contains(value: item.index) << "item"<< item.expanded; |
1074 | isConsistent = false; |
1075 | } |
1076 | if (!isConsistent) { |
1077 | if (dumpOnFail) |
1078 | dump(); |
1079 | return false; |
1080 | } |
1081 | QModelIndex firstChildIndex; |
1082 | if (item.expanded) |
1083 | firstChildIndex = m_model->index(row: 0, column: 0, parent: idx); |
1084 | if (firstChildIndex.isValid()) { |
1085 | ancestors.push(t: parent); |
1086 | parent = idx; |
1087 | idx = m_model->index(row: 0, column: 0, parent); |
1088 | } else { |
1089 | while (idx.row() == m_model->rowCount(parent) - 1) { |
1090 | if (ancestors.isEmpty()) |
1091 | break; |
1092 | idx = parent; |
1093 | parent = ancestors.pop(); |
1094 | } |
1095 | idx = m_model->index(row: idx.row() + 1, column: 0, parent); |
1096 | } |
1097 | } |
1098 | |
1099 | return true; |
1100 | } |
1101 | |
1102 | void QQmlTreeModelToTableModel::enableSignalAggregation() { |
1103 | m_signalAggregatorStack++; |
1104 | } |
1105 | |
1106 | void QQmlTreeModelToTableModel::disableSignalAggregation() { |
1107 | m_signalAggregatorStack--; |
1108 | Q_ASSERT(m_signalAggregatorStack >= 0); |
1109 | if (m_signalAggregatorStack == 0) { |
1110 | emitQueuedSignals(); |
1111 | } |
1112 | } |
1113 | |
1114 | void QQmlTreeModelToTableModel::queueDataChanged(const QModelIndex &topLeft, |
1115 | const QModelIndex &bottomRight, |
1116 | const QVector<int> &roles) |
1117 | { |
1118 | if (isAggregatingSignals()) { |
1119 | m_queuedDataChanged.append(t: DataChangedParams { .topLeft: topLeft, .bottomRight: bottomRight, .roles: roles }); |
1120 | } else { |
1121 | emit dataChanged(topLeft, bottomRight, roles); |
1122 | } |
1123 | } |
1124 | |
1125 | void QQmlTreeModelToTableModel::emitQueuedSignals() |
1126 | { |
1127 | QVector<DataChangedParams> combinedUpdates; |
1128 | /* First, iterate through the queued updates and merge the overlapping ones |
1129 | * to reduce the number of updates. |
1130 | * We don't merge adjacent updates, because they are typically filed with a |
1131 | * different role (a parent row is next to its children). |
1132 | */ |
1133 | for (const DataChangedParams &dataChange : std::as_const(t&: m_queuedDataChanged)) { |
1134 | int startRow = dataChange.topLeft.row(); |
1135 | int endRow = dataChange.bottomRight.row(); |
1136 | bool merged = false; |
1137 | for (DataChangedParams &combined : combinedUpdates) { |
1138 | int combinedStartRow = combined.topLeft.row(); |
1139 | int combinedEndRow = combined.bottomRight.row(); |
1140 | if ((startRow <= combinedStartRow && endRow >= combinedStartRow) || |
1141 | (startRow <= combinedEndRow && endRow >= combinedEndRow)) { |
1142 | if (startRow < combinedStartRow) { |
1143 | combined.topLeft = dataChange.topLeft; |
1144 | } |
1145 | if (endRow > combinedEndRow) { |
1146 | combined.bottomRight = dataChange.bottomRight; |
1147 | } |
1148 | for (int role : dataChange.roles) { |
1149 | if (!combined.roles.contains(t: role)) |
1150 | combined.roles.append(t: role); |
1151 | } |
1152 | merged = true; |
1153 | break; |
1154 | } |
1155 | } |
1156 | if (!merged) { |
1157 | combinedUpdates.append(t: dataChange); |
1158 | } |
1159 | } |
1160 | |
1161 | /* Finally, emit the dataChanged signals */ |
1162 | for (const DataChangedParams &dataChange : combinedUpdates) { |
1163 | emit dataChanged(topLeft: dataChange.topLeft, bottomRight: dataChange.bottomRight, roles: dataChange.roles); |
1164 | } |
1165 | m_queuedDataChanged.clear(); |
1166 | } |
1167 | |
1168 | QT_END_NAMESPACE |
1169 | |
1170 | #include "moc_qqmltreemodeltotablemodel_p_p.cpp" |
1171 |
Definitions
- QQmlTreeModelToTableModel
- model
- connectToModel
- setModel
- clearModelData
- rootIndex
- setRootIndex
- resetRootIndex
- index
- parent
- roleNames
- rowCount
- columnCount
- data
- setData
- headerData
- flags
- depthAtRow
- itemIndex
- isVisible
- childrenVisible
- mapToModel
- mapFromModel
- mapToModel
- selectionForRowRange
- showModelTopLevelItems
- showModelChildItems
- expand
- collapse
- isExpanded
- isExpanded
- hasChildren
- hasSiblings
- expandRow
- expandRecursively
- expandPendingRows
- collapseRecursively
- collapseRow
- lastChildIndex
- removeVisibleRows
- modelHasBeenDestroyed
- modelHasBeenReset
- modelDataChanged
- modelLayoutAboutToBeChanged
- modelLayoutChanged
- modelRowsAboutToBeInserted
- modelRowsInserted
- modelRowsAboutToBeRemoved
- modelRowsRemoved
- modelRowsAboutToBeMoved
- modelRowsMoved
- modelColumnsAboutToBeInserted
- modelColumnsAboutToBeRemoved
- modelColumnsInserted
- modelColumnsRemoved
- dump
- testConsistency
- enableSignalAggregation
- disableSignalAggregation
- queueDataChanged
Learn Advanced QML with KDAB
Find out more