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

source code of qtdeclarative/src/labs/models/qqmltreemodel.cpp