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 const bool isExpanded = m_items.at(parentRow).expanded;
783 queueDataChanged(parentRow, parentRow, {HasChildrenRole});
784 item = m_items.at(parentRow);
785 // If the item not expanded earlier, and if expanded due to expand() triggered during
786 // new child added to the row (parentRow), then we don't want to continue with showing model
787 // child items (in showModelChildItems()) as its already expanded.
788 if ((!isExpanded && item.expanded) || !item.expanded) {
790 return;
791 }
792 } else if (parent == m_rootIndex) {
793 item = TreeItem(parent);
794 } else {
796 return;
797 }
798 showModelChildItems(item, start, end);
800}
801
802void QQmlTreeModelToTableModel::modelRowsAboutToBeRemoved(const QModelIndex & parent, int start, int end)
803{
805 enableSignalAggregation();
806 if (parent == m_rootIndex || childrenVisible(parent)) {
807 const QModelIndex &smi = m_model->index(start, 0, parent);
808 int startIndex = itemIndex(smi);
809 const QModelIndex &emi = m_model->index(end, 0, parent);
810 int endIndex = -1;
811 if (isExpanded(emi)) {
812 int rowCount = m_model->rowCount(emi);
813 if (rowCount > 0) {
814 const QModelIndex &idx = m_model->index(rowCount - 1, 0, emi);
815 endIndex = lastChildIndex(idx);
816 }
817 }
818 if (endIndex == -1)
819 endIndex = itemIndex(emi);
820
821 removeVisibleRows(startIndex, endIndex);
822 }
823
824 for (int r = start; r <= end; r++) {
825 const QModelIndex &cmi = m_model->index(r, 0, parent);
826 m_expandedItems.remove(cmi);
827 }
828}
829
830void QQmlTreeModelToTableModel::modelRowsRemoved(const QModelIndex & parent, int start, int end)
831{
832 Q_UNUSED(start)
833 Q_UNUSED(end)
834 int parentRow = itemIndex(parent);
835 if (parentRow >= 0)
836 queueDataChanged(parentRow, parentRow, {HasChildrenRole});
837 disableSignalAggregation();
839}
840
841void QQmlTreeModelToTableModel::modelRowsAboutToBeMoved(const QModelIndex & sourceParent, int sourceStart, int sourceEnd, const QModelIndex & destinationParent, int destinationRow)
842{
844 enableSignalAggregation();
845 m_visibleRowsMoved = false;
846 if (!childrenVisible(sourceParent))
847 return; // Do nothing now. See modelRowsMoved() below.
848
849 if (!childrenVisible(destinationParent)) {
850 modelRowsAboutToBeRemoved(sourceParent, sourceStart, sourceEnd);
851 /* If the destination parent has no children, we'll need to
852 * report a change on the HasChildrenRole */
853 if (isVisible(destinationParent) && m_model->rowCount(destinationParent) == 0) {
854 const int parentRow = itemIndex(destinationParent);
855 queueDataChanged(parentRow, parentRow, {HasChildrenRole});
856 }
857 } else {
858 int depthDifference = -1;
859 if (destinationParent.isValid()) {
860 int destParentIndex = itemIndex(destinationParent);
861 depthDifference = m_items.at(destParentIndex).depth;
862 }
863 if (sourceParent.isValid()) {
864 int sourceParentIndex = itemIndex(sourceParent);
865 depthDifference -= m_items.at(sourceParentIndex).depth;
866 } else {
867 depthDifference++;
868 }
869
870 int startIndex = itemIndex(m_model->index(sourceStart, 0, sourceParent));
871 const QModelIndex &emi = m_model->index(sourceEnd, 0, sourceParent);
872 int endIndex = -1;
873 if (isExpanded(emi)) {
874 int rowCount = m_model->rowCount(emi);
875 if (rowCount > 0)
876 endIndex = lastChildIndex(m_model->index(rowCount - 1, 0, emi));
877 }
878 if (endIndex == -1)
879 endIndex = itemIndex(emi);
880
881 int destIndex = -1;
882 if (destinationRow == m_model->rowCount(destinationParent)) {
883 const QModelIndex &emi = m_model->index(destinationRow - 1, 0, destinationParent);
884 destIndex = lastChildIndex(emi) + 1;
885 } else {
886 destIndex = itemIndex(m_model->index(destinationRow, 0, destinationParent));
887 }
888
889 int totalMovedCount = endIndex - startIndex + 1;
890
891 /* This beginMoveRows() is matched by a endMoveRows() in the
892 * modelRowsMoved() method below. */
893 m_visibleRowsMoved = startIndex != destIndex &&
894 beginMoveRows(QModelIndex(), startIndex, endIndex, QModelIndex(), destIndex);
895
896 const QList<TreeItem> &buffer = m_items.mid(startIndex, totalMovedCount);
897 int bufferCopyOffset;
898 if (destIndex > endIndex) {
899 for (int i = endIndex + 1; i < destIndex; i++) {
900 m_items.swapItemsAt(i, i - totalMovedCount); // Fast move from 1st to 2nd position
901 }
902 bufferCopyOffset = destIndex - totalMovedCount;
903 } else {
904 // NOTE: we will not enter this loop if startIndex == destIndex
905 for (int i = startIndex - 1; i >= destIndex; i--) {
906 m_items.swapItemsAt(i, i + totalMovedCount); // Fast move from 1st to 2nd position
907 }
908 bufferCopyOffset = destIndex;
909 }
910 for (int i = 0; i < buffer.size(); i++) {
911 TreeItem item = buffer.at(i);
912 item.depth += depthDifference;
913 m_items.replace(bufferCopyOffset + i, item);
914 }
915
916 /* If both source and destination items are visible, the indexes of
917 * all the items in between will change. If they share the same
918 * parent, then this is all; however, if they belong to different
919 * parents, their bottom siblings will also get displaced, so their
920 * index also needs to be updated.
921 * Given that the bottom siblings of the top moved elements are
922 * already included in the update (since they lie between the
923 * source and the dest elements), we only need to worry about the
924 * siblings of the bottom moved element.
925 */
926 const int top = qMin(startIndex, bufferCopyOffset);
927 int bottom = qMax(endIndex, bufferCopyOffset + totalMovedCount - 1);
928 if (sourceParent != destinationParent) {
929 const QModelIndex &bottomParent =
930 bottom == endIndex ? sourceParent : destinationParent;
931
932 const int rowCount = m_model->rowCount(bottomParent);
933 if (rowCount > 0)
934 bottom = qMax(bottom, lastChildIndex(m_model->index(rowCount - 1, 0, bottomParent)));
935 }
936 queueDataChanged(top, bottom, {ModelIndexRole});
937
938 if (depthDifference != 0)
939 queueDataChanged(bufferCopyOffset, bufferCopyOffset + totalMovedCount - 1, {DepthRole});
940 }
941}
942
943void QQmlTreeModelToTableModel::modelRowsMoved(const QModelIndex & sourceParent, int sourceStart, int sourceEnd, const QModelIndex & destinationParent, int destinationRow)
944{
945 if (!childrenVisible(sourceParent)) {
946 modelRowsInserted(destinationParent, destinationRow, destinationRow + sourceEnd - sourceStart);
947 } else if (!childrenVisible(destinationParent)) {
948 modelRowsRemoved(sourceParent, sourceStart, sourceEnd);
949 }
950
951 if (m_visibleRowsMoved)
952 endMoveRows();
953
954 if (isVisible(sourceParent) && m_model->rowCount(sourceParent) == 0) {
955 int parentRow = itemIndex(sourceParent);
956 collapseRow(parentRow);
957 queueDataChanged(parentRow, parentRow, {ExpandedRole, HasChildrenRole});
958 }
959
960 disableSignalAggregation();
961
963}
964
965void QQmlTreeModelToTableModel::modelColumnsAboutToBeInserted(const QModelIndex & parent, int start, int end)
966{
967 Q_UNUSED(parent);
968 beginInsertColumns({}, start, end);
969}
970
971void QQmlTreeModelToTableModel::modelColumnsAboutToBeRemoved(const QModelIndex & parent, int start, int end)
972{
973 Q_UNUSED(parent);
974 beginRemoveColumns({}, start, end);
975}
976
977void QQmlTreeModelToTableModel::modelColumnsInserted(const QModelIndex & parent, int start, int end)
978{
979 Q_UNUSED(parent);
980 Q_UNUSED(start);
981 Q_UNUSED(end);
982 endInsertColumns();
983 m_items.clear();
984 showModelTopLevelItems();
986}
987
988void QQmlTreeModelToTableModel::modelColumnsRemoved(const QModelIndex & parent, int start, int end)
989{
990 Q_UNUSED(parent);
991 Q_UNUSED(start);
992 Q_UNUSED(end);
993 endRemoveColumns();
994 m_items.clear();
995 showModelTopLevelItems();
997}
998
999void QQmlTreeModelToTableModel::dump() const
1000{
1001 if (!m_model)
1002 return;
1003 int count = m_items.size();
1004 if (count == 0)
1005 return;
1006 int countWidth = floor(log10(double(count))) + 1;
1007 qInfo() << "Dumping" << this;
1008 for (int i = 0; i < count; i++) {
1009 const TreeItem &item = m_items.at(i);
1010 bool hasChildren = m_model->hasChildren(item.index);
1011 int children = m_model->rowCount(item.index);
1012 qInfo().noquote().nospace()
1013 << QStringLiteral("%1 ").arg(i, countWidth) << QString(4 * item.depth, QChar::fromLatin1('.'))
1014 << QLatin1String(!hasChildren ? ".. " : item.expanded ? " v " : " > ")
1015 << item.index << children;
1016 }
1017}
1018
1019bool QQmlTreeModelToTableModel::testConsistency(bool dumpOnFail) const
1020{
1021 if (!m_model) {
1022 if (!m_items.isEmpty()) {
1023 qWarning() << "Model inconsistency: No model but stored visible items";
1024 return false;
1025 }
1026 if (!m_expandedItems.isEmpty()) {
1027 qWarning() << "Model inconsistency: No model but stored expanded items";
1028 return false;
1029 }
1030 return true;
1031 }
1032 QModelIndex parent = m_rootIndex;
1033 QStack<QModelIndex> ancestors;
1034 QModelIndex idx = m_model->index(0, 0, parent);
1035 for (int i = 0; i < m_items.size(); i++) {
1036 bool isConsistent = true;
1037 const TreeItem &item = m_items.at(i);
1038 if (item.index != idx) {
1039 qWarning() << "QModelIndex inconsistency" << i << item.index;
1040 qWarning() << " expected" << idx;
1041 isConsistent = false;
1042 }
1043 if (item.index.parent() != parent) {
1044 qWarning() << "Parent inconsistency" << i << item.index;
1045 qWarning() << " stored index parent" << item.index.parent() << "model parent" << parent;
1046 isConsistent = false;
1047 }
1048 if (item.depth != ancestors.size()) {
1049 qWarning() << "Depth inconsistency" << i << item.index;
1050 qWarning() << " item depth" << item.depth << "ancestors stack" << ancestors.size();
1051 isConsistent = false;
1052 }
1053 if (item.expanded && !m_expandedItems.contains(item.index)) {
1054 qWarning() << "Expanded inconsistency" << i << item.index;
1055 qWarning() << " set" << m_expandedItems.contains(item.index) << "item" << item.expanded;
1056 isConsistent = false;
1057 }
1058 if (!isConsistent) {
1059 if (dumpOnFail)
1060 dump();
1061 return false;
1062 }
1063 QModelIndex firstChildIndex;
1064 if (item.expanded)
1065 firstChildIndex = m_model->index(0, 0, idx);
1066 if (firstChildIndex.isValid()) {
1067 ancestors.push(parent);
1068 parent = idx;
1069 idx = m_model->index(0, 0, parent);
1070 } else {
1071 while (idx.row() == m_model->rowCount(parent) - 1) {
1072 if (ancestors.isEmpty())
1073 break;
1074 idx = parent;
1075 parent = ancestors.pop();
1076 }
1077 idx = m_model->index(idx.row() + 1, 0, parent);
1078 }
1079 }
1080
1081 return true;
1082}
1083
1084void QQmlTreeModelToTableModel::enableSignalAggregation() {
1085 m_signalAggregatorStack++;
1086}
1087
1088void QQmlTreeModelToTableModel::disableSignalAggregation() {
1089 m_signalAggregatorStack--;
1090 Q_ASSERT(m_signalAggregatorStack >= 0);
1091 if (m_signalAggregatorStack == 0) {
1092 emitQueuedSignals();
1093 }
1094}
1095
1096void QQmlTreeModelToTableModel::queueDataChanged(int top, int bottom,
1097 std::initializer_list<int> roles)
1098{
1099 if (isAggregatingSignals())
1100 m_queuedDataChanged.append(DataChangedParams { top, bottom, roles });
1101 else
1102 emit dataChanged(index(top, 0), index(bottom, 0), roles);
1103}
1104
1105void QQmlTreeModelToTableModel::emitQueuedSignals()
1106{
1107 QVarLengthArray<DataChangedParams> combinedUpdates;
1108 /* First, iterate through the queued updates and merge the overlapping ones
1109 * to reduce the number of updates.
1110 * We don't merge adjacent updates, because they are typically filed with a
1111 * different role (a parent row is next to its children).
1112 */
1113 for (const DataChangedParams &dataChange : std::as_const(m_queuedDataChanged)) {
1114 const int startRow = dataChange.top;
1115 const int endRow = dataChange.bottom;
1116 bool merged = false;
1117 for (DataChangedParams &combined : combinedUpdates) {
1118 int combinedStartRow = combined.top;
1119 int combinedEndRow = combined.bottom;
1120 if ((startRow <= combinedStartRow && endRow >= combinedStartRow) ||
1121 (startRow <= combinedEndRow && endRow >= combinedEndRow)) {
1122 if (startRow < combinedStartRow) {
1123 combined.top = dataChange.top;
1124 }
1125 if (endRow > combinedEndRow) {
1126 combined.bottom = dataChange.bottom;
1127 }
1128 for (int role : dataChange.roles) {
1129 if (!combined.roles.contains(role))
1130 combined.roles.append(role);
1131 }
1132 merged = true;
1133 break;
1134 }
1135 }
1136 if (!merged) {
1137 combinedUpdates.append(dataChange);
1138 }
1139 }
1140
1141 /* Finally, emit the dataChanged signals */
1142 for (const DataChangedParams &dataChange : combinedUpdates) {
1143 const QModelIndex topLeft = index(dataChange.top, 0);
1144 const QModelIndex bottomRight = index(dataChange.bottom, 0);
1145 emit dataChanged(topLeft, bottomRight, {dataChange.roles.begin(), dataChange.roles.end()});
1146 }
1147 m_queuedDataChanged.clear();
1148}
1149
1150QT_END_NAMESPACE
1151
1152#include "moc_qqmltreemodeltotablemodel_p_p.cpp"
Combined button and popup list for selecting options.
#define ASSERT_CONSISTENCY