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

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