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
qqmltreemodel.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 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 reason:default
4
7
8#include <QtCore/qloggingcategory.h>
9
10#include <QtQml/qqmlinfo.h>
11#include <QtQml/qqmlengine.h>
12
14
15using namespace Qt::StringLiterals;
16
17Q_STATIC_LOGGING_CATEGORY(lcTreeModel, "qt.qml.treemodel")
18
19static const QString ROWS_PROPERTY_NAME = u"rows"_s;
20
21/*!
22 \qmltype TreeModel
23//! \nativetype QQmlTreeModel
24 \inqmlmodule Qt.labs.qmlmodels
25 \brief Encapsulates a simple tree model.
26 \since 6.10
27
28 The TreeModel type stores JavaScript/JSON objects as data for a tree
29 model that can be used with \l TreeView. It is intended to support
30 very simple models without requiring the creation of a custom
31 \l QAbstractItemModel subclass in C++.
32
33 \snippet qml/treemodel/treemodel-filesystem-basic.qml file
34
35 The model's initial data is set with either the \l rows property or by
36 calling \l appendRow(). Each column in the model is specified by declaring
37 a \l TableModelColumn instance, where the order of each instance determines
38 its column index. Once the model's \l Component::completed() signal has been
39 emitted, the columns and roles will have been established and are then
40 fixed for the lifetime of the model.
41
42 \section1 Supported Row Data Structures
43
44 Each row represents a node in the tree. Each node has the same type of
45 columns. The TreeModel is designed to work with JavaScript/JSON data so
46 each row is a list of simple key-value pairs:
47
48 \snippet qml/treemodel/treemodel-filesystem-basic.qml rows
49
50 A node can have child nodes and these will be stored in an array
51 associated with the "rows" key. "rows" is reserved for this purpose: only
52 the list of child nodes should be associated with this key.
53
54 The model is manipulated via \l {QModelIndex} {QModelIndices}. To access
55 a specific row/node, the \l getRow() function can be used. It's also
56 possible to access the model's JavaScript data directly via the \l rows
57 property, but it is not possible to modify the model data this way.
58
59 To add new rows, use \l appendRow(). To modify existing rows, use
60 \l setRow(), \l removeRow() and \l clear().
61*/
62
63QQmlTreeModel::QQmlTreeModel(QObject *parent)
64 : QQmlAbstractColumnModel(parent)
65{
66}
67
68QQmlTreeModel::~QQmlTreeModel() = default;
69
70/*!
71 \qmlproperty var TreeModel::rows
72
73 This property holds the model data in the form of an array of rows.
74
75 \sa getRow(), setRow(), appendRow(), clear(), columnCount
76*/
77QVariant QQmlTreeModel::rows() const
78{
79 QVariantList rowsAsVariant;
80 for (const auto &row : mRows)
81 rowsAsVariant.append(row->toVariant());
82
83 return rowsAsVariant;
84}
85
86void QQmlTreeModel::setRows(const QVariant &rows)
87{
88 const std::optional<QVariantList> validated = validateRowsArgument(rows);
89 if (!validated)
90 return;
91
92 const QVariantList rowsAsVariantList = *validated;
93
94 if (!mComponentCompleted) {
95 // Store the rows until we can call setRowsPrivate() after component completion.
96 mInitialRows = rowsAsVariantList;
97 return;
98 }
99
100 setRowsPrivate(rowsAsVariantList);
101}
102
103void QQmlTreeModel::setRowsPrivate(const QVariantList &rowsAsVariantList)
104{
105 Q_ASSERT(mComponentCompleted);
106
107 // By now, all TableModelColumns should have been set.
108 if (mColumns.isEmpty()) {
109 qmlWarning(this) << "No TableModelColumns were set; model will be empty";
110 return;
111 }
112
113 const bool firstTimeValidRowsHaveBeenSet = mColumnMetadata.isEmpty();
114 if (!firstTimeValidRowsHaveBeenSet) {
115 // This is not the first time rows have been set; validate each one.
116 for (const auto &row : rowsAsVariantList) {
117 // validateNewRow() expects a QVariant wrapping a QJSValue, so to
118 // simplify the code, just create one here.
119 const QVariant wrappedRow = QVariant::fromValue(row);
120 if (!validateNewRow("TreeModel::setRows"_L1, wrappedRow, SetRowsOperation))
121 return;
122 }
123 }
124
125 beginResetModel();
126
127 // In case the model is empty and we cannot insert any new rows in the loop below,
128 // we don't want to emit rowsChanged
129 const bool wasEmpty = mRows.empty();
130
131 // We don't clear the column or role data, because a TreeModel should not be reused in that way.
132 // Once it has valid data, its columns and roles are fixed.
133 mRows.clear();
134
135 for (const auto &rowAsVariant : rowsAsVariantList) {
136 if (rowAsVariant.canConvert<QVariantMap>())
137 mRows.push_back(std::make_unique<QQmlTreeRow>(rowAsVariant));
138 else
139 qmlWarning(this) << "Cannot create tree row as the row does not contain "
140 << "key-value pairs";
141 }
142
143 // Gather metadata the first time rows is set.
144 // If we call setrows on an empty model, mInitialRows will be empty, but mRows is not
145 if (firstTimeValidRowsHaveBeenSet && (!mRows.empty() || !mInitialRows.isEmpty()))
146 fetchColumnMetadata();
147
148 endResetModel();
149
150 // was empty, still empty => no emit
151 // was empty, now non-empty => emit
152 // was not empty, now empty => emit
153 // was not empty, now non-empty => emit (there was a clear in-between)
154
155 if (!wasEmpty || !mRows.empty())
156 emit rowsChanged();
157}
158
159QVariant QQmlTreeModel::dataPrivate(const QModelIndex &index, const QString &roleName) const
160{
161 const ColumnMetadata columnMetadata = mColumnMetadata.at(index.column());
162 const QString propertyName = columnMetadata.roles.value(roleName).name;
163 const auto *thisRow = static_cast<const QQmlTreeRow *>(index.internalPointer());
164 return thisRow->data(propertyName);
165}
166
167void QQmlTreeModel::setDataPrivate(const QModelIndex &index, const QString &roleName, QVariant value)
168{
169 auto *row = static_cast<QQmlTreeRow *>(index.internalPointer());
170 row->setField(roleName, value);
171}
172
173// TODO: Turn this into a snippet that compiles in CI
174/*!
175 \qmlmethod void TreeModel::appendRow(parent, var treeRow)
176
177 Appends a new treeRow to \a parent, with the values (cells) in \a treeRow.
178
179 \code
180 treeModel.appendRow(index, {
181 checked: false,
182 size: "-",
183 type: "folder",
184 name: "Orders",
185 lastModified: "2025-07-02",
186 rows: [
187 {
188 checked: true,
189 size: "38 KB",
190 type: "file",
191 name: "monitors.xlsx",
192 lastModified: "2025-07-02"
193 },
194 {
195 checked: true,
196 size: "54 KB",
197 type: "file",
198 name: "notebooks.xlsx",
199 lastModified: "2025-07-02"
200 }
201 ]
202 });
203 \endcode
204
205 \a parent is an anonymous QML type backed by \l QModelIndex.
206 If \a parent is invalid, \a treeRow gets appended to the root node.
207
208 \sa setRow(), removeRow()
209*/
210void QQmlTreeModel::appendRow(QModelIndex parent, const QVariant &row)
211{
212 if (!validateNewRow("TreeModel::appendRow"_L1, row))
213 return;
214
215 const QVariant data = row.userType() == QMetaType::QVariantMap ? row : row.value<QJSValue>().toVariant();
216
217 if (parent.isValid()) {
218 auto *parentRow = static_cast<QQmlTreeRow *>(parent.internalPointer());
219 auto *newChild = new QQmlTreeRow(data);
220
221 beginInsertRows(parent, static_cast<int>(parentRow->rowCount()), static_cast<int>(parentRow->rowCount()));
222 parentRow->addChild(newChild);
223
224 // Gather metadata the first time a row is added.
225 if (mColumnMetadata.isEmpty())
226 fetchColumnMetadata();
227
228 endInsertRows();
229 } else {
230 qmlWarning(this) << "append: could not find any node at the specified index"
231 << " - the new row will be appended to root";
232
233 if (data.canConvert<QVariantMap>()) {
234 beginInsertRows(QModelIndex(),
235 static_cast<int>(mRows.size()),
236 static_cast<int>(mRows.size()));
237
238 mRows.push_back(std::make_unique<QQmlTreeRow>(data));
239
240 // Gather metadata the first time a row is added.
241 if (mColumnMetadata.isEmpty())
242 fetchColumnMetadata();
243
244 endInsertRows();
245 } else {
246 qmlWarning(this) << "Cannot create tree row as the row does not contain "
247 << "key-value pairs";
248 return;
249 }
250 }
251
252 emit rowsChanged();
253}
254
255/*!
256 \qmlmethod void TreeModel::appendRow(var treeRow)
257
258 Appends \a treeRow to the root node.
259
260 \sa setRow(), removeRow()
261*/
262void QQmlTreeModel::appendRow(const QVariant &row)
263{
264 appendRow({}, row);
265}
266
267/*!
268 \qmlmethod void TreeModel::clear()
269
270 Removes all rows from the model.
271
272 \sa removeRow()
273*/
274void QQmlTreeModel::clear()
275{
276 QQmlEngine *engine = qmlEngine(this);
277 Q_ASSERT(engine);
278 setRows(QVariant::fromValue(engine->newArray()));
279}
280
281/*!
282 \qmlmethod var TreeModel::getRow(rowIndex)
283
284 Returns the treeRow at specified index in the model.
285 \a rowIndex is an anonymous QML type backed by \l QModelIndex.
286
287 \note the returned object cannot be used to modify the contents of the
288 model; use setTreeRow() instead.
289
290 \sa setRow(), appendRow(), removeRow()
291*/
292QVariant QQmlTreeModel::getRow(const QModelIndex &rowIndex) const
293{
294 if (rowIndex.isValid())
295 return static_cast<QQmlTreeRow*>(rowIndex.internalPointer())->toVariant();
296
297 qmlWarning(this) << "getRow: could not find any node at the specified index";
298 return {};
299}
300
301/*!
302 \qmlmethod void TreeModel::insertRow(int rowIndex, QModelIndex parent, object row)
303 \since 6.12
304
305 Inserts a new row to the \a parent at position \a rowIndex, with the
306 values (cells) in \a row.
307
308 \code
309 model.insertRow(2, parentIndex, {
310 checkable: true, checked: false,
311 amount: 1,
312 fruitType: "Pear",
313 fruitName: "Williams",
314 fruitPrice: 1.50,
315 })
316 \endcode
317
318 \sa appendRow(), setRow(), removeRow()
319*/
320
321void QQmlTreeModel::insertRow(int rowIndex, QModelIndex parent, const QVariant &row)
322{
323 if (rowIndex < 0) {
324 qmlWarning(this).noquote() << "insertRow(): rowIndex cannot be negative";
325 return;
326 }
327 if (rowIndex > rowCount(parent)) {
328 qmlWarning(this).noquote() << "insertRow(): rowIndex " << rowIndex
329 << " is greater than rowCount() of "
330 << rowCount(parent);
331 return;
332 }
333 if (!validateNewRow("insertRow()"_L1, row))
334 return;
335
336 doInsert(parent, rowIndex, row);
337}
338
339/*!
340 \qmlmethod void TreeModel::insertRow(int rowIndex, object row)
341 \since 6.12
342
343 Inserts a new row to the root item at position \a rowIndex, with the
344 values (cells) in \a row.
345
346 \sa appendRow(), setRow(), removeRow()
347*/
348void QQmlTreeModel::insertRow(int rowIndex, const QVariant &row)
349{
350 insertRow(rowIndex, {}, row);
351}
352
353void QQmlTreeModel::doInsert(const QModelIndex &parent, int rowIndex, const QVariant &row)
354{
355 beginInsertRows(parent, rowIndex, rowIndex);
356
357 const QVariant data =
358 row.userType() == QMetaType::QVariantMap
359 ? row
360 : row.value<QJSValue>().toVariant();
361
362 auto *newChild = new QQmlTreeRow(data);
363 if (parent.isValid())
364 static_cast<QQmlTreeRow *>(parent.internalPointer())->insertChild(rowIndex, newChild);
365 else
366 mRows.insert(mRows.begin() + rowIndex, std::unique_ptr<QQmlTreeRow>(newChild));
367
368 qCDebug(lcTreeModel).nospace() << "inserted the following row to the row "
369 << parent << " at index "
370 << rowIndex << ":\n" << data.toMap();
371
372 // Gather metadata the first time a row is added.
373 if (mColumnMetadata.isEmpty())
374 fetchColumnMetadata();
375
376 endInsertRows();
377 emit rowsChanged();
378}
379
380QVariant QQmlTreeModel::firstRow() const
381{
382 return mRows.front().get()->data();
383}
384
385void QQmlTreeModel::setInitialRows()
386{
387 setRowsPrivate(mInitialRows);
388}
389
390/*!
391 \qmlmethod void TreeModel::removeRow(rowIndex)
392
393 Removes the TreeRow referenced by \a rowIndex from the model.
394 \a rowIndex is an anonymous QML type backed by \l QModelIndex.
395
396 \code
397 treeModel.removeTreeRow(rowIndex)
398 \endcode
399
400\sa clear()
401*/
402void QQmlTreeModel::removeRow(QModelIndex rowIndex)
403{
404 if (rowIndex.isValid()) {
405 QModelIndex mIndexParent = rowIndex.parent();
406
407 beginRemoveRows(mIndexParent, rowIndex.row(), rowIndex.row());
408
409 if (mIndexParent.isValid()) {
410 auto *parent = static_cast<QQmlTreeRow *>(mIndexParent.internalPointer());
411 parent->removeChildAt(rowIndex.row());
412 } else {
413 mRows.erase(std::next(mRows.begin(), rowIndex.row()));
414 }
415
416 endRemoveRows();
417 } else {
418 qmlWarning(this) << "TreeModel::removeRow could not find any node at the specified index";
419 return;
420 }
421
422 emit rowsChanged();
423}
424
425// TODO: Turn this into a snippet that compiles in CI
426
427/*!
428 \qmlmethod void TreeModel::setRow(rowIndex, var treeRow)
429
430 Replaces the TreeRow at \a rowIndex in the model with \a treeRow.
431 \a rowIndex is an anonymous QML type back by \l QModelIndex.
432
433 A row with child rows will be rejected.
434
435 All columns/cells must be present in \c treeRow, and in the correct order.
436 The child rows of the row remain unaffected.
437
438 \code
439 treeModel.setRow(rowIndex, {
440 checked: true,
441 size: "-",
442 type: "folder",
443 name: "Subtitles",
444 lastModified: "2025-07-07",
445 iconColor: "blue"
446 });
447 \endcode
448
449 \sa appendRow()
450*/
451void QQmlTreeModel::setRow(QModelIndex rowIndex, const QVariant &rowData)
452{
453 if (!rowIndex.isValid()) {
454 qmlWarning(this) << "TreeModel::setRow: invalid modelIndex";
455 return;
456 }
457
458 const QVariantMap rowAsMap = rowData.toMap();
459 if (rowAsMap.contains(ROWS_PROPERTY_NAME) && rowAsMap[ROWS_PROPERTY_NAME].userType() == QMetaType::Type::QVariantList) {
460 qmlWarning(this) << "TreeModel::setRow: child rows are not allowed";
461 return;
462 }
463
464 if (!validateNewRow("TreeModel::setRow"_L1, rowData))
465 return;
466
467 const QVariant rowAsVariant = rowData.userType() == QMetaType::QVariantMap ? rowData : rowData.value<QJSValue>().toVariant();
468 auto *row = static_cast<QQmlTreeRow *>(rowIndex.internalPointer());
469 row->setData(rowAsVariant);
470
471 const QModelIndex topLeftModelIndex(createIndex(rowIndex.row(), 0, rowIndex.internalPointer()));
472 const QModelIndex bottomRightModelIndex(createIndex(rowIndex.row(), mColumnCount-1, rowIndex.internalPointer()));
473
474 emit dataChanged(topLeftModelIndex, bottomRightModelIndex);
475 emit rowsChanged();
476}
477
478/*!
479 \qmlmethod QModelIndex TreeModel::index(int row, int column, var parent)
480
481 Returns an object referencing the given \a row and \a column of
482 a given \a parent which can be passed to the data() function to get the data
483 from that cell, or to setData() to edit the contents of that cell.
484
485 The returned object is of an anonymous QML type backed by \l QModelIndex.
486
487 \sa {QModelIndex and related Classes in QML}, data()
488*/
489QModelIndex QQmlTreeModel::index(int row, int column, const QModelIndex &parent) const
490{
491 if (!parent.isValid()){
492 if (static_cast<size_t>(row) >= mRows.size())
493 return {};
494
495 return createIndex(row, column, mRows.at(row).get());
496 }
497
498 const auto *treeRow = static_cast<const QQmlTreeRow *>(parent.internalPointer());
499 if (treeRow->rowCount() <= static_cast<size_t>(row))
500 return {};
501
502 return createIndex(row, column, treeRow->getRow(row));
503}
504
505/*!
506 \qmlmethod QModelIndex TreeModel::index(list<int> treeIndex, int column)
507
508 Returns an object referencing the given \a treeIndex and \a column,
509 which can be passed to the data() function to get the data from that cell,
510 or to setData() to edit the contents of that cell.
511
512 The returned object is of an anonymous QML type backed by \l QModelIndex.
513
514 The first parameter \a treeIndex represents a path of row numbers tracing from
515 the root to the desired row and is used for navigation inside the tree.
516 This is best explained through an example.
517
518 \table
519
520 \row \li \inlineimage treemodel.svg
521 \li \list
522
523 \li The root of the tree is special, as it can be referenced by an invalid
524 \l QModelIndex.
525
526 \li Node A is the first child of the root and the corresponding \a treeIndex is \c [0].
527
528 \li Node B is the first child of node A. Since the \a treeIndex of A is \c [0]
529 the \a treeIndex of B will be \c [0,0].
530
531 \li Node C is the second child of A and its \a treeIndex is \c [0,1].
532
533 \li Node D is the third child of A and its \a treeIndex is \c [0,2].
534
535 \li Node E is the second child of the root and its \a treeIndex is \c [1].
536
537 \li Node F is the third child of the root and its \a treeIndex is \c [2].
538
539 \endlist
540
541 \endtable
542
543 With this overload it is possible to obtain a \l QModelIndex to a node without
544 having a \l QModelIndex to its parent node.
545
546 If no node is found by the list specified, an invalid model index is returned.
547 Please note that an invalid model index is referencing the root of the node.
548
549 \sa {QModelIndex and related Classes in QML}, data()
550*/
551QModelIndex QQmlTreeModel::index(const std::vector<int> &treeIndex, int column)
552{
553 QModelIndex mIndex;
554 QQmlTreeRow *row = getPointerToTreeRow(mIndex, treeIndex);
555
556 if (row)
557 return createIndex(treeIndex.back(), column, row);
558
559 qmlWarning(this) << "TreeModel::index: could not find any node at the specified index: "
560 << treeIndex;
561 return {};
562}
563
564QModelIndex QQmlTreeModel::parent(const QModelIndex &index) const
565{
566 if (!index.isValid())
567 return {};
568
569 const auto *thisRow = static_cast<const QQmlTreeRow *>(index.internalPointer());
570 const QQmlTreeRow *parentRow = thisRow->parent();
571
572 if (!parentRow) // parent is root
573 return {};
574
575 const QQmlTreeRow *grandparentRow = parentRow->parent();
576
577 if (!grandparentRow) {// grandparent is root, parent is in mRows
578 for (size_t i = 0; i < mRows.size(); i++) {
579 if (mRows[i].get() == parentRow)
580 return createIndex(static_cast<int>(i), 0, parentRow);
581 }
582 Q_UNREACHABLE_RETURN(QModelIndex());
583 }
584
585 for (size_t i = 0; i < grandparentRow->rowCount(); i++) {
586 if (grandparentRow->getRow(static_cast<int>(i)) == parentRow)
587 return createIndex(static_cast<int>(i), 0, parentRow);
588 }
589 Q_UNREACHABLE_RETURN(QModelIndex());
590}
591
592
593int QQmlTreeModel::rowCount(const QModelIndex &parent) const
594{
595 if (!parent.isValid())
596 return static_cast<int>(mRows.size());
597
598 const auto *row = static_cast<const QQmlTreeRow *>(parent.internalPointer());
599 return static_cast<int>(row->rowCount());
600}
601
602/*!
603 \qmlproperty int TreeModel::columnCount
604 \readonly
605
606 This read-only property holds the number of columns in the model.
607
608 The number of columns is fixed for the lifetime of the model
609 after the \l rows property is set or \l appendRow() is called for the first
610 time.
611*/
612int QQmlTreeModel::columnCount(const QModelIndex &parent) const
613{
614 Q_UNUSED(parent);
615
616 return mColumnCount;
617}
618
619/*!
620 \qmlmethod variant TreeModel::data(index, string role)
621
622 Returns the data from the TreeModel at the given \a index belonging to the
623 given \a role.
624
625 \a index is an anonymous QML type backed by \l QModelIndex.
626
627 \sa index(), setData()
628*/
629
630/*!
631 \qmlmethod bool TreeModel::setData(index, variant value, string role)
632
633 Inserts or updates the data field named by \a role in the TreeRow at the
634 given \a index with \a value. Returns true if successful, false if not.
635
636 \a index is an anonymous QML type backed by \l QModelIndex.
637
638 \sa data(), index()
639*/
640
641bool QQmlTreeModel::validateNewRow(QLatin1StringView functionName, const QVariant &row,
642 NewRowOperationFlag operation) const
643{
644 const bool isVariantMap = (row.userType() == QMetaType::QVariantMap);
645 const QVariant rowAsVariant = operation == SetRowsOperation || isVariantMap
646 ? row : row.value<QJSValue>().toVariant();
647 const QVariantMap rowAsMap = rowAsVariant.toMap();
648 if (rowAsMap.contains(ROWS_PROPERTY_NAME) && rowAsMap[ROWS_PROPERTY_NAME].userType() == QMetaType::Type::QVariantList)
649 {
650 const QList<QVariant> variantList = rowAsMap[ROWS_PROPERTY_NAME].toList();
651 for (const QVariant &rowAsVariant : variantList)
652 if (!validateNewRow(functionName, rowAsVariant))
653 return false;
654 }
655
656 return QQmlAbstractColumnModel::validateNewRow(functionName, row, operation);
657}
658
659int QQmlTreeModel::treeSize() const
660{
661 int treeSize = 0;
662
663 for (const auto &treeRow : mRows)
664 treeSize += treeRow->subTreeSize();
665
666 return treeSize;
667}
668
669QQmlTreeRow *QQmlTreeModel::getPointerToTreeRow(QModelIndex &modIndex,
670 const std::vector<int> &rowIndex) const
671{
672 for (int r : rowIndex) {
673 modIndex = index(r, 0, modIndex);
674 if (!modIndex.isValid())
675 return nullptr;
676 }
677
678 return static_cast<QQmlTreeRow*>(modIndex.internalPointer());
679}
680
681QT_END_NAMESPACE
682
683#include "moc_qqmltreemodel_p.cpp"
Combined button and popup list for selecting options.
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)
static const QString ROWS_PROPERTY_NAME