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