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

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

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