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