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
17static const QString ROWS_PROPERTY_NAME = u"rows"_s;
18
19/*!
20 \qmltype TreeModel
21//! \nativetype QQmlTreeModel
22 \inqmlmodule Qt.labs.qmlmodels
23 \brief Encapsulates a simple tree model.
24 \since 6.10
25
26 The TreeModel type stores JavaScript/JSON objects as data for a tree
27 model that can be used with \l TreeView. It is intended to support
28 very simple models without requiring the creation of a custom
29 \l QAbstractItemModel subclass in C++.
30
31 \snippet qml/treemodel/treemodel-filesystem-basic.qml file
32
33 The model's initial data is set with either the \l rows property or by
34 calling \l appendRow(). Each column in the model is specified by declaring
35 a \l TableModelColumn instance, where the order of each instance determines
36 its column index. Once the model's \l Component::completed() signal has been
37 emitted, the columns and roles will have been established and are then
38 fixed for the lifetime of the model.
39
40 \section1 Supported Row Data Structures
41
42 Each row represents a node in the tree. Each node has the same type of
43 columns. The TreeModel is designed to work with JavaScript/JSON data so
44 each row is a list of simple key-value pairs:
45
46 \snippet qml/treemodel/treemodel-filesystem-basic.qml rows
47
48 A node can have child nodes and these will be stored in an array
49 associated with the "rows" key. "rows" is reserved for this purpose: only
50 the list of child nodes should be associated with this key.
51
52 The model is manipulated via \l {QModelIndex} {QModelIndices}. To access
53 a specific row/node, the \l getRow() function can be used. It's also
54 possible to access the model's JavaScript data directly via the \l rows
55 property, but it is not possible to modify the model data this way.
56
57 To add new rows, use \l appendRow(). To modify existing rows, use
58 \l setRow(), \l removeRow() and \l clear().
59*/
60
61QQmlTreeModel::QQmlTreeModel(QObject *parent)
62 : QQmlAbstractColumnModel(parent)
63{
64}
65
66QQmlTreeModel::~QQmlTreeModel() = default;
67
68/*!
69 \qmlproperty object TreeModel::rows
70
71 This property holds the model data in the form of an array of rows.
72
73 \sa getRow(), setRow(), appendRow(), clear(), columnCount
74*/
75QVariant QQmlTreeModel::rows() const
76{
77 QVariantList rowsAsVariant;
78 for (const auto &row : mRows)
79 rowsAsVariant.append(row->toVariant());
80
81 return rowsAsVariant;
82}
83
84void QQmlTreeModel::setRows(const QVariant &rows)
85{
86 if (rows.userType() != qMetaTypeId<QJSValue>()) {
87 qmlWarning(this) << "setRows(): \"rows\" must be an array; actual type is " << rows.typeName();
88 return;
89 }
90
91 const auto rowsAsJSValue = rows.value<QJSValue>();
92 const QVariantList rowsAsVariantList = rowsAsJSValue.toVariant().toList();
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 // We don't clear the column or role data, because a TreeModel should not be reused in that way.
128 // Once it has valid data, its columns and roles are fixed.
129 mRows.clear();
130
131 for (const auto &rowAsVariant : rowsAsVariantList)
132 mRows.push_back(std::make_unique<QQmlTreeRow>(rowAsVariant));
133
134 // Gather metadata the first time rows is set.
135 // If we call setrows on an empty model, mInitialRows will be empty, but mRows is not
136 if (firstTimeValidRowsHaveBeenSet && (!mRows.empty() || !mInitialRows.isEmpty()))
137 fetchColumnMetadata();
138
139 endResetModel();
140 emit rowsChanged();
141}
142
143QVariant QQmlTreeModel::dataPrivate(const QModelIndex &index, const QString &roleName) const
144{
145 const ColumnMetadata columnMetadata = mColumnMetadata.at(index.column());
146 const QString propertyName = columnMetadata.roles.value(roleName).name;
147 const auto *thisRow = static_cast<const QQmlTreeRow *>(index.internalPointer());
148 return thisRow->data(propertyName);
149}
150
151void QQmlTreeModel::setDataPrivate(const QModelIndex &index, const QString &roleName, QVariant value)
152{
153 auto *row = static_cast<QQmlTreeRow *>(index.internalPointer());
154 row->setField(roleName, value);
155}
156
157// TODO: Turn this into a snippet that compiles in CI
158/*!
159 \qmlmethod TreeModel::appendRow(QModelIndex parent, object treeRow)
160
161 Appends a new treeRow to \a parent, with the values (cells) in \a treeRow.
162
163 \code
164 treeModel.appendRow(index, {
165 checked: false,
166 size: "-",
167 type: "folder",
168 name: "Orders",
169 lastModified: "2025-07-02",
170 rows: [
171 {
172 checked: true,
173 size: "38 KB",
174 type: "file",
175 name: "monitors.xlsx",
176 lastModified: "2025-07-02"
177 },
178 {
179 checked: true,
180 size: "54 KB",
181 type: "file",
182 name: "notebooks.xlsx",
183 lastModified: "2025-07-02"
184 }
185 ]
186 });
187 \endcode
188
189 If \a parent is invalid, \a treeRow gets appended to the root node.
190
191 \sa setRow(), removeRow()
192*/
193void QQmlTreeModel::appendRow(QModelIndex parent, const QVariant &row)
194{
195 if (!validateNewRow("TreeModel::appendRow"_L1, row))
196 return;
197
198 const QVariant data = row.userType() == QMetaType::QVariantMap ? row : row.value<QJSValue>().toVariant();
199
200 if (parent.isValid()) {
201 auto *parentRow = static_cast<QQmlTreeRow *>(parent.internalPointer());
202 auto *newChild = new QQmlTreeRow(data);
203
204 beginInsertRows(parent, static_cast<int>(parentRow->rowCount()), static_cast<int>(parentRow->rowCount()));
205 parentRow->addChild(newChild);
206
207 // Gather metadata the first time a row is added.
208 if (mColumnMetadata.isEmpty())
209 fetchColumnMetadata();
210
211 endInsertRows();
212 } else {
213 qmlWarning(this) << "append: could not find any node at the specified index"
214 << " - the new row will be appended to root";
215
216 beginInsertRows(QModelIndex(),
217 static_cast<int>(mRows.size()),
218 static_cast<int>(mRows.size()));
219
220 mRows.push_back(std::make_unique<QQmlTreeRow>(data));
221
222 // Gather metadata the first time a row is added.
223 if (mColumnMetadata.isEmpty())
224 fetchColumnMetadata();
225
226 endInsertRows();
227 }
228
229 emit rowsChanged();
230}
231
232/*!
233 \qmlmethod TreeModel::appendRow(object treeRow)
234
235 Appends \a treeRow to the root node.
236
237 \sa setRow(), removeRow()
238*/
239void QQmlTreeModel::appendRow(const QVariant &row)
240{
241 appendRow({}, row);
242}
243
244/*!
245 \qmlmethod TreeModel::clear()
246
247 Removes all rows from the model.
248
249 \sa removeRow()
250*/
251void QQmlTreeModel::clear()
252{
253 QQmlEngine *engine = qmlEngine(this);
254 Q_ASSERT(engine);
255 setRows(QVariant::fromValue(engine->newArray()));
256}
257
258/*!
259 \qmlmethod object TreeModel::getRow(const QModelIndex &rowIndex)
260
261 Returns the treeRow at \a rowIndex in the model.
262
263 \note the returned object cannot be used to modify the contents of the
264 model; use setTreeRow() instead.
265
266 \sa setRow(), appendRow(), removeRow()
267*/
268QVariant QQmlTreeModel::getRow(const QModelIndex &rowIndex) const
269{
270 if (rowIndex.isValid())
271 return static_cast<QQmlTreeRow*>(rowIndex.internalPointer())->toVariant();
272
273 qmlWarning(this) << "getRow: could not find any node at the specified index";
274 return {};
275}
276
277QVariant QQmlTreeModel::firstRow() const
278{
279 return mRows.front().get()->data();
280}
281
282void QQmlTreeModel::setInitialRows()
283{
284 setRowsPrivate(mInitialRows);
285}
286
287/*!
288 \qmlmethod TreeModel::removeRow(QModelIndex rowIndex)
289
290 Removes the TreeRow referenced by \a rowIndex from the model.
291
292 \code
293 treeModel.removeTreeRow(rowIndex)
294 \endcode
295
296\sa clear()
297*/
298void QQmlTreeModel::removeRow(QModelIndex rowIndex)
299{
300 if (rowIndex.isValid()) {
301 QModelIndex mIndexParent = rowIndex.parent();
302
303 beginRemoveRows(mIndexParent, rowIndex.row(), rowIndex.row());
304
305 if (mIndexParent.isValid()) {
306 auto *parent = static_cast<QQmlTreeRow *>(mIndexParent.internalPointer());
307 parent->removeChildAt(rowIndex.row());
308 } else {
309 mRows.erase(std::next(mRows.begin(), rowIndex.row()));
310 }
311
312 endRemoveRows();
313 } else {
314 qmlWarning(this) << "TreeModel::removeRow could not find any node at the specified index";
315 return;
316 }
317
318 emit rowsChanged();
319}
320
321// TODO: Turn this into a snippet that compiles in CI
322
323/*!
324 \qmlmethod TreeModel::setRow(QModelIndex rowIndex, object treeRow)
325
326 Replaces the TreeRow at \a rowIndex in the model with \a treeRow.
327 A row with child rows will be rejected.
328
329 All columns/cells must be present in \c treeRow, and in the correct order.
330 The child rows of the row remain unaffected.
331
332 \code
333 treeModel.setRow(rowIndex, {
334 checked: true,
335 size: "-",
336 type: "folder",
337 name: "Subtitles",
338 lastModified: "2025-07-07",
339 iconColor: "blue"
340 });
341 \endcode
342
343 \sa appendRow()
344*/
345void QQmlTreeModel::setRow(QModelIndex rowIndex, const QVariant &rowData)
346{
347 if (!rowIndex.isValid()) {
348 qmlWarning(this) << "TreeModel::setRow: invalid modelIndex";
349 return;
350 }
351
352 const QVariantMap rowAsMap = rowData.toMap();
353 if (rowAsMap.contains(ROWS_PROPERTY_NAME) && rowAsMap[ROWS_PROPERTY_NAME].userType() == QMetaType::Type::QVariantList) {
354 qmlWarning(this) << "TreeModel::setRow: child rows are not allowed";
355 return;
356 }
357
358 if (!validateNewRow("TreeModel::setRow"_L1, rowData))
359 return;
360
361 const QVariant rowAsVariant = rowData.userType() == QMetaType::QVariantMap ? rowData : rowData.value<QJSValue>().toVariant();
362 auto *row = static_cast<QQmlTreeRow *>(rowIndex.internalPointer());
363 row->setData(rowAsVariant);
364
365 const QModelIndex topLeftModelIndex(createIndex(rowIndex.row(), 0, rowIndex.internalPointer()));
366 const QModelIndex bottomRightModelIndex(createIndex(rowIndex.row(), mColumnCount-1, rowIndex.internalPointer()));
367
368 emit dataChanged(topLeftModelIndex, bottomRightModelIndex);
369 emit rowsChanged();
370}
371
372/*!
373 \qmlmethod QModelIndex TreeModel::index(int row, int column, object parent)
374
375 Returns a \l QModelIndex object referencing the given \a row and \a column of
376 a given \a parent which can be passed to the data() function to get the data
377 from that cell, or to setData() to edit the contents of that cell.
378
379 \sa {QModelIndex and related Classes in QML}, data()
380*/
381QModelIndex QQmlTreeModel::index(int row, int column, const QModelIndex &parent) const
382{
383 if (!parent.isValid()){
384 if (static_cast<size_t>(row) >= mRows.size())
385 return {};
386
387 return createIndex(row, column, mRows.at(row).get());
388 }
389
390 const auto *treeRow = static_cast<const QQmlTreeRow *>(parent.internalPointer());
391 if (treeRow->rowCount() <= static_cast<size_t>(row))
392 return {};
393
394 return createIndex(row, column, treeRow->getRow(row));
395}
396
397/*!
398 \qmlmethod QModelIndex TreeModel::index(list<int> treeIndex, int column)
399
400 Returns a \l QModelIndex object referencing the given \a treeIndex and \a column,
401 which can be passed to the data() function to get the data from that cell,
402 or to setData() to edit the contents of that cell.
403
404 The first parameter \a treeIndex represents a path of row numbers tracing from
405 the root to the desired row and is used for navigation inside the tree.
406 This is best explained through an example.
407
408 \table
409
410 \row \li \inlineimage treemodel.svg
411 \li \list
412
413 \li The root of the tree is special, as it can be referenced by an invalid
414 \l QModelIndex.
415
416 \li Node A is the first child of the root and the corresponding \a treeIndex is \c [0].
417
418 \li Node B is the first child of node A. Since the \a treeIndex of A is \c [0]
419 the \a treeIndex of B will be \c [0,0].
420
421 \li Node C is the second child of A and its \a treeIndex is \c [0,1].
422
423 \li Node D is the third child of A and its \a treeIndex is \c [0,2].
424
425 \li Node E is the second child of the root and its \a treeIndex is \c [1].
426
427 \li Node F is the third child of the root and its \a treeIndex is \c [2].
428
429 \endlist
430
431 \endtable
432
433 With this overload it is possible to obtain a \l QModelIndex to a node without
434 having a \l QModelIndex to its parent node.
435
436 If no node is found by the list specified, an invalid model index is returned.
437 Please note that an invalid model index is referencing the root of the node.
438
439 \sa {QModelIndex and related Classes in QML}, data()
440*/
441QModelIndex QQmlTreeModel::index(const std::vector<int> &treeIndex, int column)
442{
443 QModelIndex mIndex;
444 QQmlTreeRow *row = getPointerToTreeRow(mIndex, treeIndex);
445
446 if (row)
447 return createIndex(treeIndex.back(), column, row);
448
449 qmlWarning(this) << "TreeModel::index: could not find any node at the specified index";
450 return {};
451}
452
453QModelIndex QQmlTreeModel::parent(const QModelIndex &index) const
454{
455 if (!index.isValid())
456 return {};
457
458 const auto *thisRow = static_cast<const QQmlTreeRow *>(index.internalPointer());
459 const QQmlTreeRow *parentRow = thisRow->parent();
460
461 if (!parentRow) // parent is root
462 return {};
463
464 const QQmlTreeRow *grandparentRow = parentRow->parent();
465
466 if (!grandparentRow) {// grandparent is root, parent is in mRows
467 for (size_t i = 0; i < mRows.size(); i++) {
468 if (mRows[i].get() == parentRow)
469 return createIndex(static_cast<int>(i), 0, parentRow);
470 }
471 Q_UNREACHABLE_RETURN(QModelIndex());
472 }
473
474 for (size_t i = 0; i < grandparentRow->rowCount(); i++) {
475 if (grandparentRow->getRow(static_cast<int>(i)) == parentRow)
476 return createIndex(static_cast<int>(i), 0, parentRow);
477 }
478 Q_UNREACHABLE_RETURN(QModelIndex());
479}
480
481
482int QQmlTreeModel::rowCount(const QModelIndex &parent) const
483{
484 if (!parent.isValid())
485 return static_cast<int>(mRows.size());
486
487 const auto *row = static_cast<const QQmlTreeRow *>(parent.internalPointer());
488 return static_cast<int>(row->rowCount());
489}
490
491/*!
492 \qmlproperty int TreeModel::columnCount
493 \readonly
494
495 This read-only property holds the number of columns in the model.
496
497 The number of columns is fixed for the lifetime of the model
498 after the \l rows property is set or \l appendRow() is called for the first
499 time.
500*/
501int QQmlTreeModel::columnCount(const QModelIndex &parent) const
502{
503 Q_UNUSED(parent);
504
505 return mColumnCount;
506}
507
508/*!
509 \qmlmethod variant TreeModel::data(QModelIndex index, string role)
510
511 Returns the data from the TreeModel at the given \a index belonging to the
512 given \a role.
513
514 \sa index(), setData()
515*/
516
517/*!
518 \qmlmethod bool TreeModel::setData(QModelIndex index, variant value, string role)
519
520 Inserts or updates the data field named by \a role in the TreeRow at the
521 given \a index with \a value. Returns true if successful, false if not.
522
523 \sa data(), index()
524*/
525
526bool QQmlTreeModel::validateNewRow(QLatin1StringView functionName, const QVariant &row,
527 NewRowOperationFlag operation) const
528{
529 const bool isVariantMap = (row.userType() == QMetaType::QVariantMap);
530 const QVariant rowAsVariant = operation == SetRowsOperation || isVariantMap
531 ? row : row.value<QJSValue>().toVariant();
532 const QVariantMap rowAsMap = rowAsVariant.toMap();
533 if (rowAsMap.contains(ROWS_PROPERTY_NAME) && rowAsMap[ROWS_PROPERTY_NAME].userType() == QMetaType::Type::QVariantList)
534 {
535 const QList<QVariant> variantList = rowAsMap[ROWS_PROPERTY_NAME].toList();
536 for (const QVariant &rowAsVariant : variantList)
537 if (!validateNewRow(functionName, rowAsVariant))
538 return false;
539 }
540
541 return QQmlAbstractColumnModel::validateNewRow(functionName, row, operation);
542}
543
544int QQmlTreeModel::treeSize() const
545{
546 int treeSize = 0;
547
548 for (const auto &treeRow : mRows)
549 treeSize += treeRow->subTreeSize();
550
551 return treeSize;
552}
553
554QQmlTreeRow *QQmlTreeModel::getPointerToTreeRow(QModelIndex &modIndex,
555 const std::vector<int> &rowIndex) const
556{
557 for (int r : rowIndex) {
558 modIndex = index(r, 0, modIndex);
559 if (!modIndex.isValid())
560 return nullptr;
561 }
562
563 return static_cast<QQmlTreeRow*>(modIndex.internalPointer());
564}
565
566QT_END_NAMESPACE
567
568#include "moc_qqmltreemodel_p.cpp"
static const QString ROWS_PROPERTY_NAME