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
qqmltablemodel.cpp
Go to the documentation of this file.
1// Copyright (C) 2019 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
6
7#include <QtCore/qloggingcategory.h>
8
9#include <QtQml/qqmlinfo.h>
10#include <QtQml/qqmlengine.h>
11
13
14using namespace Qt::StringLiterals;
15
16Q_STATIC_LOGGING_CATEGORY(lcTableModel, "qt.qml.tablemodel")
17
18/*!
19 \qmltype TableModel
20//! \nativetype QQmlTableModel
21 \inqmlmodule Qt.labs.qmlmodels
22 \brief Encapsulates a simple table model.
23 \since 5.14
24
25 The TableModel type stores JavaScript/JSON objects as data for a table
26 model that can be used with \l TableView. It is intended to support
27 very simple models without requiring the creation of a custom
28 QAbstractTableModel subclass in C++.
29
30 \snippet qml/tablemodel/fruit-example-simpledelegate.qml file
31
32 The model's initial row 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 To access a specific row, the \l getRow() function can be used.
40 It's also possible to access the model's JavaScript data
41 directly via the \l rows property, but it is not possible to
42 modify the model data this way.
43
44 To add new rows, use \l appendRow() and \l insertRow(). To modify
45 existing rows, use \l setRow(), \l moveRow(), \l removeRow(), and
46 \l clear().
47
48 It is also possible to modify the model's data via the delegate,
49 as shown in the example above:
50
51 \snippet qml/tablemodel/fruit-example-simpledelegate.qml delegate
52
53 If the type of the data at the modified role does not match the type of the
54 data that is set, it will be automatically converted via
55 \l {QVariant::canConvert()}{QVariant}.
56
57 \section1 Supported Row Data Structures
58
59 TableModel is designed to work with JavaScript/JSON data, where each row
60 is a list of simple key-value pairs:
61
62 \code
63 {
64 // Each property is one cell/column.
65 checked: false,
66 amount: 1,
67 fruitType: "Apple",
68 fruitName: "Granny Smith",
69 fruitPrice: 1.50
70 },
71 // ...
72 \endcode
73
74 As model manipulation in Qt is done via row and column indices,
75 and because object keys are unordered, each column must be specified via
76 TableModelColumn. This allows mapping Qt's built-in roles to any property
77 in each row object.
78
79 Complex row structures are supported, but with limited functionality.
80 As TableModel has no way of knowing how each row is structured,
81 it cannot manipulate it. As a consequence of this, the copy of the
82 model data that TableModel has stored in \l rows is not kept in sync
83 with the source data that was set in QML. For these reasons, manipulation
84 of the data is not supported.
85
86 For example, suppose you wanted to use a data source where each row is an
87 array and each cell is an object. To use this data source with TableModel,
88 define a getter:
89
90 \code
91 TableModel {
92 TableModelColumn {
93 display: function(modelIndex) { return rows[modelIndex.row][0].checked }
94 }
95 // ...
96
97 rows: [
98 [
99 { checked: false, checkable: true },
100 { amount: 1 },
101 { fruitType: "Apple" },
102 { fruitName: "Granny Smith" },
103 { fruitPrice: 1.50 }
104 ]
105 // ...
106 ]
107 }
108 \endcode
109
110 The row above is one example of a complex row.
111
112 \note Row manipulation functions such as \l appendRow(), \l removeRow(),
113 etc. are not supported when using complex rows.
114
115 \section1 Using DelegateChooser with TableModel
116
117 For most real world use cases, it is recommended to use DelegateChooser
118 as the delegate of a TableView that uses TableModel. This allows you to
119 use specific roles in the relevant delegates. For example, the snippet
120 above can be rewritten to use DelegateChooser like so:
121
122 \snippet qml/tablemodel/fruit-example-delegatechooser.qml file
123
124 The most specific delegates are declared first: the columns at index \c 0
125 and \c 1 have \c bool and \c integer data types, so they use a
126 \l [QtQuickControls2]{CheckBox} and \l [QtQuickControls2]{SpinBox},
127 respectively. The remaining columns can simply use a
128 \l [QtQuickControls2]{TextField}, and so that delegate is declared
129 last as a fallback.
130
131 \sa TableModelColumn, TableView, QAbstractTableModel
132*/
133
134QQmlTableModel::QQmlTableModel(QObject *parent)
135 : QQmlAbstractColumnModel(parent)
136{
137}
138
139QQmlTableModel::~QQmlTableModel()
140 = default;
141
142/*!
143 \qmlproperty object TableModel::rows
144
145 This property holds the model data in the form of an array of rows:
146
147 \snippet qml/tablemodel/fruit-example-simpledelegate.qml rows
148
149 \sa getRow(), setRow(), moveRow(), appendRow(), insertRow(), clear(), rowCount, columnCount
150*/
151QVariant QQmlTableModel::rows() const
152{
153 return mRows;
154}
155
156void QQmlTableModel::setRows(const QVariant &rows)
157{
158 if (rows.userType() != qMetaTypeId<QJSValue>()) {
159 qmlWarning(this) << "setRows(): \"rows\" must be an array; actual type is " << rows.typeName();
160 return;
161 }
162
163 const auto rowsAsJSValue = rows.value<QJSValue>();
164 const QVariantList rowsAsVariantList = rowsAsJSValue.toVariant().toList();
165 if (rowsAsVariantList == mRows) {
166 // No change.
167 return;
168 }
169
170 if (!mComponentCompleted) {
171 // Store the rows until we can call setRowsPrivate() after component completion.
172 mRows = rowsAsVariantList;
173 return;
174 }
175
176 setRowsPrivate(rowsAsVariantList);
177}
178
179void QQmlTableModel::setRowsPrivate(const QVariantList &rowsAsVariantList)
180{
181 Q_ASSERT(mComponentCompleted);
182
183 // By now, all TableModelColumns should have been set.
184 if (mColumns.isEmpty()) {
185 qmlWarning(this) << "No TableModelColumns were set; model will be empty";
186 return;
187 }
188
189 const bool firstTimeValidRowsHaveBeenSet = mColumnMetadata.isEmpty();
190 if (!firstTimeValidRowsHaveBeenSet) {
191 // This is not the first time rows have been set; validate each one.
192 for (int rowIndex = 0; rowIndex < rowsAsVariantList.size(); ++rowIndex) {
193 // validateNewRow() expects a QVariant wrapping a QJSValue, so to
194 // simplify the code, just create one here.
195 const QVariant row = QVariant::fromValue(rowsAsVariantList.at(rowIndex));
196 if (!validateNewRow("setRows()"_L1, row, SetRowsOperation))
197 return;
198 }
199 }
200
201 const int oldRowCount = mRowCount;
202
203 beginResetModel();
204
205 // We don't clear the column or role data, because a TableModel should not be reused in that way.
206 // Once it has valid data, its columns and roles are fixed.
207 mRows = rowsAsVariantList;
208 mRowCount = mRows.size();
209
210 // Gather metadata the first time rows is set.
211 if (firstTimeValidRowsHaveBeenSet && !mRows.isEmpty())
212 fetchColumnMetadata();
213
214 endResetModel();
215 emit rowsChanged();
216
217 if (mRowCount != oldRowCount)
218 emit rowCountChanged();
219}
220
221QVariant QQmlTableModel::dataPrivate(const QModelIndex &index, const QString &roleName) const
222{
223 const ColumnMetadata columnMetadata = mColumnMetadata.at(index.column());
224 const QString propertyName = columnMetadata.roles.value(roleName).name;
225 const QVariantMap rowData = mRows.at(index.row()).toMap();
226 return rowData.value(propertyName);
227}
228
229void QQmlTableModel::setDataPrivate(const QModelIndex &index, const QString &roleName, QVariant value)
230{
231 int row = index.row();
232 QVariantMap modifiedRow = mRows.at(row).toMap();
233 modifiedRow[roleName] = value;
234 mRows[row] = modifiedRow;
235}
236
237// TODO: Turn this into a snippet that compiles in CI
238/*!
239 \qmlmethod TableModel::appendRow(object row)
240
241 Adds a new row to the end of the model, with the
242 values (cells) in \a row.
243
244 \code
245 model.appendRow({
246 checkable: true,
247 amount: 1,
248 fruitType: "Pear",
249 fruitName: "Williams",
250 fruitPrice: 1.50,
251 })
252 \endcode
253
254 \sa insertRow(), setRow(), removeRow()
255*/
256void QQmlTableModel::appendRow(const QVariant &row)
257{
258 if (!validateNewRow("appendRow()"_L1, row, AppendOperation))
259 return;
260
261 doInsert(mRowCount, row);
262}
263
264/*!
265 \qmlmethod TableModel::clear()
266
267 Removes all rows from the model.
268
269 \sa removeRow()
270*/
271void QQmlTableModel::clear()
272{
273 QQmlEngine *engine = qmlEngine(this);
274 Q_ASSERT(engine);
275 setRows(QVariant::fromValue(engine->newArray()));
276}
277
278/*!
279 \qmlmethod object TableModel::getRow(int rowIndex)
280
281 Returns the row at \a rowIndex in the model.
282
283 Note that this equivalent to accessing the row directly
284 through the \l rows property:
285
286 \code
287 Component.onCompleted: {
288 // These two lines are equivalent.
289 console.log(model.getRow(0).display);
290 console.log(model.rows[0].fruitName);
291 }
292 \endcode
293
294 \note the returned object cannot be used to modify the contents of the
295 model; use setRow() instead.
296
297 \sa setRow(), appendRow(), insertRow(), removeRow(), moveRow()
298*/
299QVariant QQmlTableModel::getRow(int rowIndex)
300{
301 if (!validateRowIndex("getRow()"_L1, "rowIndex"_L1, rowIndex, NeedsExisting))
302 return QVariant();
303 return mRows.at(rowIndex);
304}
305
306/*!
307 \qmlmethod TableModel::insertRow(int rowIndex, object row)
308
309 Adds a new row to the model at position \a rowIndex, with the
310 values (cells) in \a row.
311
312 \code
313 model.insertRow(2, {
314 checkable: true, checked: false,
315 amount: 1,
316 fruitType: "Pear",
317 fruitName: "Williams",
318 fruitPrice: 1.50,
319 })
320 \endcode
321
322 The \a rowIndex must point to an existing item in the table, or one past
323 the end of the table (equivalent to \l appendRow()).
324
325 \sa appendRow(), setRow(), removeRow(), rowCount
326*/
327void QQmlTableModel::insertRow(int rowIndex, const QVariant &row)
328{
329 if (!validateNewRow("insertRow()"_L1, row) ||
330 !validateRowIndex("insertRow()"_L1, "rowIndex"_L1, rowIndex, CanAppend))
331 return;
332
333 doInsert(rowIndex, row);
334}
335
336void QQmlTableModel::doInsert(int rowIndex, const QVariant &row)
337{
338 beginInsertRows(QModelIndex(), rowIndex, rowIndex);
339
340 // Adding rowAsVariant.toList() will add each invidual variant in the list,
341 // which is definitely not what we want.
342 const QVariant rowAsVariant = row.userType() == QMetaType::QVariantMap ? row : row.value<QJSValue>().toVariant();
343
344 mRows.insert(rowIndex, rowAsVariant);
345 ++mRowCount;
346
347 qCDebug(lcTableModel).nospace() << "inserted the following row to the model at index "
348 << rowIndex << ":\n" << rowAsVariant.toMap();
349
350 // Gather metadata the first time a row is added.
351 if (mColumnMetadata.isEmpty())
352 fetchColumnMetadata();
353
354 endInsertRows();
355 emit rowCountChanged();
356 emit rowsChanged();
357}
358
359/*!
360 \qmlmethod TableModel::moveRow(int fromRowIndex, int toRowIndex, int rows)
361
362 Moves \a rows from the index at \a fromRowIndex to the index at
363 \a toRowIndex.
364
365 The from and to ranges must exist; for example, to move the first 3 items
366 to the end of the list:
367
368 \code
369 model.moveRow(0, model.rowCount - 3, 3)
370 \endcode
371
372 \sa appendRow(), insertRow(), removeRow(), rowCount
373*/
374void QQmlTableModel::moveRow(int fromRowIndex, int toRowIndex, int rows)
375{
376 if (fromRowIndex == toRowIndex) {
377 qmlWarning(this) << "moveRow(): \"fromRowIndex\" cannot be equal to \"toRowIndex\"";
378 return;
379 }
380
381 if (rows <= 0) {
382 qmlWarning(this) << "moveRow(): \"rows\" is less than or equal to 0";
383 return;
384 }
385
386 if (!validateRowIndex("moveRow()"_L1, "fromRowIndex"_L1, fromRowIndex, NeedsExisting))
387 return;
388
389 if (!validateRowIndex("moveRow()"_L1, "toRowIndex"_L1, toRowIndex, NeedsExisting))
390 return;
391
392 if (fromRowIndex + rows > mRowCount) {
393 qmlWarning(this) << "moveRow(): \"fromRowIndex\" (" << fromRowIndex
394 << ") + \"rows\" (" << rows << ") = " << (fromRowIndex + rows)
395 << ", which is greater than rowCount() of " << mRowCount;
396 return;
397 }
398
399 if (toRowIndex + rows > mRowCount) {
400 qmlWarning(this) << "moveRow(): \"toRowIndex\" (" << toRowIndex
401 << ") + \"rows\" (" << rows << ") = " << (toRowIndex + rows)
402 << ", which is greater than rowCount() of " << mRowCount;
403 return;
404 }
405
406 qCDebug(lcTableModel).nospace() << "moving " << rows
407 << " row(s) from index " << fromRowIndex
408 << " to index " << toRowIndex;
409
410 // Based on the same call in QQmlListModel::moveRow().
411 beginMoveRows(QModelIndex(), fromRowIndex, fromRowIndex + rows - 1, QModelIndex(),
412 toRowIndex > fromRowIndex ? toRowIndex + rows : toRowIndex);
413
414 // Based on ListModel::moveRow().
415 if (fromRowIndex > toRowIndex) {
416 // Only move forwards - flip if moving backwards.
417 const int from = fromRowIndex;
418 const int to = toRowIndex;
419 fromRowIndex = to;
420 toRowIndex = to + rows;
421 rows = from - to;
422 }
423
424 QVector<QVariant> store;
425 store.reserve(rows);
426 for (int i = 0; i < (toRowIndex - fromRowIndex); ++i)
427 store.append(mRows.at(fromRowIndex + rows + i));
428 for (int i = 0; i < rows; ++i)
429 store.append(mRows.at(fromRowIndex + i));
430 for (int i = 0; i < store.size(); ++i)
431 mRows[fromRowIndex + i] = store[i];
432
433 qCDebug(lcTableModel).nospace() << "after moving, rows are:\n" << mRows;
434
435 endMoveRows();
436 emit rowsChanged();
437}
438
439/*!
440 \qmlmethod TableModel::removeRow(int rowIndex, int rows = 1)
441
442 Removes a number of \a rows at \a rowIndex from the model.
443
444 \sa clear(), rowCount
445*/
446void QQmlTableModel::removeRow(int rowIndex, int rows)
447{
448 if (!validateRowIndex("removeRow()"_L1, "rowIndex"_L1, rowIndex, NeedsExisting))
449 return;
450
451 if (rows <= 0) {
452 qmlWarning(this) << "removeRow(): \"rows\" is less than or equal to zero";
453 return;
454 }
455
456 if (rowIndex + rows - 1 >= mRowCount) {
457 qmlWarning(this) << "removeRow(): \"rows\" " << rows
458 << " exceeds available rowCount() of " << mRowCount
459 << " when removing from \"rowIndex\" " << rowIndex;
460 return;
461 }
462
463 beginRemoveRows(QModelIndex(), rowIndex, rowIndex + rows - 1);
464
465 auto firstIterator = mRows.begin() + rowIndex;
466 // The "last" argument to erase() is exclusive, so we go one past the last item.
467 auto lastIterator = firstIterator + rows;
468 mRows.erase(firstIterator, lastIterator);
469 mRowCount -= rows;
470
471 endRemoveRows();
472 emit rowCountChanged();
473 emit rowsChanged();
474
475 qCDebug(lcTableModel).nospace() << "removed " << rows
476 << " items from the model, starting at index " << rowIndex;
477}
478
479/*!
480 \qmlmethod TableModel::setRow(int rowIndex, object row)
481
482 Changes the row at \a rowIndex in the model with \a row.
483
484 All columns/cells must be present in \c row, and in the correct order.
485
486 \code
487 model.setRow(0, {
488 checkable: true,
489 amount: 1,
490 fruitType: "Pear",
491 fruitName: "Williams",
492 fruitPrice: 1.50,
493 })
494 \endcode
495
496 If \a rowIndex is equal to \c rowCount(), then a new row is appended to the
497 model. Otherwise, \a rowIndex must point to an existing row in the model.
498
499 \sa appendRow(), insertRow(), rowCount
500*/
501void QQmlTableModel::setRow(int rowIndex, const QVariant &row)
502{
503 if (!validateNewRow("setRow()"_L1, row) ||
504 !validateRowIndex("setRow()"_L1, "rowIndex"_L1, rowIndex, CanAppend))
505 return;
506
507 if (rowIndex != mRowCount) {
508 // Setting an existing row.
509 mRows[rowIndex] = row;
510
511 // For now we just assume the whole row changed, as it's simpler.
512 const QModelIndex topLeftModelIndex(createIndex(rowIndex, 0));
513 const QModelIndex bottomRightModelIndex(createIndex(rowIndex, mColumnCount - 1));
514 emit dataChanged(topLeftModelIndex, bottomRightModelIndex);
515 emit rowsChanged();
516 } else {
517 // Appending a row.
518 doInsert(rowIndex, row);
519 }
520}
521
522
523QVariant QQmlTableModel::firstRow() const
524{
525 return mRows.first();
526}
527
528void QQmlTableModel::setInitialRows()
529{
530 setRowsPrivate(mRows);
531}
532
533/*!
534 \qmlmethod QModelIndex TableModel::index(int row, int column)
535
536 Returns a \l QModelIndex object referencing the given \a row and \a column,
537 which can be passed to the data() function to get the data from that cell,
538 or to setData() to edit the contents of that cell.
539
540 \code
541 import QtQml 2.14
542 import Qt.labs.qmlmodels 1.0
543
544 TableModel {
545 id: model
546
547 TableModelColumn { display: "fruitType" }
548 TableModelColumn { display: "fruitPrice" }
549
550 rows: [
551 { fruitType: "Apple", fruitPrice: 1.50 },
552 { fruitType: "Orange", fruitPrice: 2.50 }
553 ]
554
555 Component.onCompleted: {
556 for (var r = 0; r < model.rowCount; ++r) {
557 console.log("An " + model.data(model.index(r, 0)).display +
558 " costs " + model.data(model.index(r, 1)).display.toFixed(2))
559 }
560 }
561 }
562 \endcode
563
564 \sa {QModelIndex and related Classes in QML}, data()
565*/
566// Note: we don't document the parent argument, because you never need it, because
567// cells in a TableModel don't have parents. But it is there because this function is an override.
568QModelIndex QQmlTableModel::index(int row, int column, const QModelIndex &parent) const
569{
570 return row >= 0 && row < rowCount() && column >= 0 && column < columnCount() && !parent.isValid()
571 ? createIndex(row, column)
572 : QModelIndex();
573}
574
575QModelIndex QQmlTableModel::parent(const QModelIndex &index) const
576{
577 Q_UNUSED(index);
578 return {};
579}
580
581/*!
582 \qmlproperty int TableModel::rowCount
583 \readonly
584
585 This read-only property holds the number of rows in the model.
586
587 This value changes whenever rows are added or removed from the model.
588*/
589int QQmlTableModel::rowCount(const QModelIndex &parent) const
590{
591 if (parent.isValid())
592 return 0;
593
594 return mRowCount;
595}
596
597/*!
598 \qmlproperty int TableModel::columnCount
599 \readonly
600
601 This read-only property holds the number of columns in the model.
602
603 The number of columns is fixed for the lifetime of the model
604 after the \l rows property is set or \l appendRow() is called for the first
605 time.
606*/
607int QQmlTableModel::columnCount(const QModelIndex &parent) const
608{
609 Q_UNUSED(parent);
610
611 return mColumnCount;
612}
613
614/*!
615 \qmlmethod variant TableModel::data(QModelIndex index, string role)
616
617 Returns the data from the table cell at the given \a index belonging to the
618 given \a role.
619
620 \sa index(), setData()
621*/
622
623/*!
624 \qmlmethod bool TableModel::setData(QModelIndex index, variant value, string role)
625
626 Inserts or updates the data field named by \a role in the table cell at the
627 given \a index with \a value. Returns true if successful, false if not.
628
629 \sa data(), index()
630*/
631
632bool QQmlTableModel::validateRowIndex(QLatin1StringView functionName, QLatin1StringView argumentName,
633 int rowIndex, RowOption operation) const
634{
635 if (rowIndex < 0) {
636 qmlWarning(this).noquote() << functionName << ": \"" << argumentName << "\" cannot be negative";
637 return false;
638 }
639
640 if (operation == NeedsExisting) {
641 if (rowIndex >= mRowCount) {
642 qmlWarning(this).noquote() << functionName << ": \"" << argumentName
643 << "\" " << rowIndex << " is greater than or equal to rowCount() of " << mRowCount;
644 return false;
645 }
646 } else {
647 if (rowIndex > mRowCount) {
648 qmlWarning(this).noquote() << functionName << ": \"" << argumentName
649 << "\" " << rowIndex << " is greater than rowCount() of " << mRowCount;
650 return false;
651 }
652 }
653
654 return true;
655}
656
657QT_END_NAMESPACE
658
659#include "moc_qqmltablemodel_p.cpp"
Q_STATIC_LOGGING_CATEGORY(lcAccessibilityCore, "qt.accessibility.core")