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

source code of qtdeclarative/src/qmlmodels/qqmltreemodeltotablemodel.cpp