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#include <QtQml/qqmlinfo.h>
8#include <QtQml/qqmlengine.h>
9
10QT_BEGIN_NAMESPACE
11
12Q_LOGGING_CATEGORY(lcTableModel, "qt.qml.tablemodel")
13
14/*!
15 \qmltype TableModel
16//! \instantiates QQmlTableModel
17 \inqmlmodule Qt.labs.qmlmodels
18 \brief Encapsulates a simple table model.
19 \since 5.14
20
21 The TableModel type stores JavaScript/JSON objects as data for a table
22 model that can be used with \l TableView. It is intended to support
23 very simple models without requiring the creation of a custom
24 QAbstractTableModel subclass in C++.
25
26 \snippet qml/tablemodel/fruit-example-simpledelegate.qml file
27
28 The model's initial row data is set with either the \l rows property or by
29 calling \l appendRow(). Each column in the model is specified by declaring
30 a \l TableModelColumn instance, where the order of each instance determines
31 its column index. Once the model's \l Component::completed() signal has been
32 emitted, the columns and roles will have been established and are then
33 fixed for the lifetime of the model.
34
35 To access a specific row, the \l getRow() function can be used.
36 It's also possible to access the model's JavaScript data
37 directly via the \l rows property, but it is not possible to
38 modify the model data this way.
39
40 To add new rows, use \l appendRow() and \l insertRow(). To modify
41 existing rows, use \l setRow(), \l moveRow(), \l removeRow(), and
42 \l clear().
43
44 It is also possible to modify the model's data via the delegate,
45 as shown in the example above:
46
47 \snippet qml/tablemodel/fruit-example-simpledelegate.qml delegate
48
49 If the type of the data at the modified role does not match the type of the
50 data that is set, it will be automatically converted via
51 \l {QVariant::canConvert()}{QVariant}.
52
53 \section1 Supported Row Data Structures
54
55 TableModel is designed to work with JavaScript/JSON data, where each row
56 is a simple key-pair object:
57
58 \code
59 {
60 // Each property is one cell/column.
61 checked: false,
62 amount: 1,
63 fruitType: "Apple",
64 fruitName: "Granny Smith",
65 fruitPrice: 1.50
66 },
67 // ...
68 \endcode
69
70 As model manipulation in Qt is done via row and column indices,
71 and because object keys are unordered, each column must be specified via
72 TableModelColumn. This allows mapping Qt's built-in roles to any property
73 in each row object.
74
75 Complex row structures are supported, but with limited functionality.
76 As TableModel has no way of knowing how each row is structured,
77 it cannot manipulate it. As a consequence of this, the copy of the
78 model data that TableModel has stored in \l rows is not kept in sync
79 with the source data that was set in QML. For these reasons, TableModel
80 relies on the user to handle simple data manipulation.
81
82 For example, suppose you wanted to have several roles per column. One way
83 of doing this is to use a data source where each row is an array and each
84 cell is an object. To use this data source with TableModel, define a
85 getter and setter:
86
87 \code
88 TableModel {
89 TableModelColumn {
90 display: function(modelIndex) { return rows[modelIndex.row][0].checked }
91 setDisplay: function(modelIndex, cellData) {
92 rows[modelIndex.row][0].checked = cellData
93 }
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 : QAbstractTableModel(parent)
136{
137}
138
139QQmlTableModel::~QQmlTableModel()
140{
141}
142
143/*!
144 \qmlproperty object TableModel::rows
145
146 This property holds the model data in the form of an array of rows:
147
148 \snippet qml/tablemodel/fruit-example-simpledelegate.qml rows
149
150 \sa getRow(), setRow(), moveRow(), appendRow(), insertRow(), clear(), rowCount, columnCount
151*/
152QVariant QQmlTableModel::rows() const
153{
154 return mRows;
155}
156
157void QQmlTableModel::setRows(const QVariant &rows)
158{
159 if (rows.userType() != qMetaTypeId<QJSValue>()) {
160 qmlWarning(me: this) << "setRows(): \"rows\" must be an array; actual type is " << rows.typeName();
161 return;
162 }
163
164 const QJSValue rowsAsJSValue = rows.value<QJSValue>();
165 const QVariantList rowsAsVariantList = rowsAsJSValue.toVariant().toList();
166 if (rowsAsVariantList == mRows) {
167 // No change.
168 return;
169 }
170
171 if (!componentCompleted) {
172 // Store the rows until we can call doSetRows() after component completion.
173 mRows = rowsAsVariantList;
174 return;
175 }
176
177 doSetRows(rowsAsVariantList);
178}
179
180void QQmlTableModel::doSetRows(const QVariantList &rowsAsVariantList)
181{
182 Q_ASSERT(componentCompleted);
183
184 // By now, all TableModelColumns should have been set.
185 if (mColumns.isEmpty()) {
186 qmlWarning(me: this) << "No TableModelColumns were set; model will be empty";
187 return;
188 }
189
190 const bool firstTimeValidRowsHaveBeenSet = mColumnMetadata.isEmpty();
191 if (!firstTimeValidRowsHaveBeenSet) {
192 // This is not the first time rows have been set; validate each one.
193 for (int rowIndex = 0; rowIndex < rowsAsVariantList.size(); ++rowIndex) {
194 // validateNewRow() expects a QVariant wrapping a QJSValue, so to
195 // simplify the code, just create one here.
196 const QVariant row = QVariant::fromValue(value: rowsAsVariantList.at(i: rowIndex));
197 if (!validateNewRow(functionName: "setRows()", row, rowIndex, operation: SetRowsOperation))
198 return;
199 }
200 }
201
202 const int oldRowCount = mRowCount;
203 const int oldColumnCount = mColumnCount;
204
205 beginResetModel();
206
207 // We don't clear the column or role data, because a TableModel should not be reused in that way.
208 // Once it has valid data, its columns and roles are fixed.
209 mRows = rowsAsVariantList;
210 mRowCount = mRows.size();
211
212 // Gather metadata the first time rows is set.
213 if (firstTimeValidRowsHaveBeenSet && !mRows.isEmpty())
214 fetchColumnMetadata();
215
216 endResetModel();
217
218 emit rowsChanged();
219
220 if (mRowCount != oldRowCount)
221 emit rowCountChanged();
222 if (mColumnCount != oldColumnCount)
223 emit columnCountChanged();
224}
225
226QQmlTableModel::ColumnRoleMetadata QQmlTableModel::fetchColumnRoleData(const QString &roleNameKey,
227 QQmlTableModelColumn *tableModelColumn, int columnIndex) const
228{
229 const QVariant firstRow = mRows.first();
230 ColumnRoleMetadata roleData;
231
232 QJSValue columnRoleGetter = tableModelColumn->getterAtRole(roleName: roleNameKey);
233 if (columnRoleGetter.isUndefined()) {
234 // This role is not defined, which is fine; just skip it.
235 return roleData;
236 }
237
238 if (columnRoleGetter.isString()) {
239 // The role is set as a string, so we assume the row is a simple object.
240 if (firstRow.userType() != QMetaType::QVariantMap) {
241 qmlWarning(me: this).quote() << "expected row for role "
242 << roleNameKey << " of TableModelColumn at index "
243 << columnIndex << " to be a simple object, but it's "
244 << firstRow.typeName() << " instead: " << firstRow;
245 return roleData;
246 }
247 const QVariantMap firstRowAsMap = firstRow.toMap();
248 const QString rolePropertyName = columnRoleGetter.toString();
249 const QVariant roleProperty = firstRowAsMap.value(key: rolePropertyName);
250
251 roleData.isStringRole = true;
252 roleData.name = rolePropertyName;
253 roleData.type = roleProperty.userType();
254 roleData.typeName = QString::fromLatin1(ba: roleProperty.typeName());
255 } else if (columnRoleGetter.isCallable()) {
256 // The role is provided via a function, which means the row is complex and
257 // the user needs to provide the data for it.
258 const auto modelIndex = index(row: 0, column: columnIndex);
259 const auto args = QJSValueList() << qmlEngine(this)->toScriptValue(value: modelIndex);
260 const QVariant cellData = columnRoleGetter.call(args).toVariant();
261
262 // We don't know the property name since it's provided through the function.
263 // roleData.name = ???
264 roleData.isStringRole = false;
265 roleData.type = cellData.userType();
266 roleData.typeName = QString::fromLatin1(ba: cellData.typeName());
267 } else {
268 // Invalid role.
269 qmlWarning(me: this) << "TableModelColumn role for column at index "
270 << columnIndex << " must be either a string or a function; actual type is: "
271 << columnRoleGetter.toString();
272 }
273
274 return roleData;
275}
276
277void QQmlTableModel::fetchColumnMetadata()
278{
279 qCDebug(lcTableModel) << "gathering metadata for" << mColumnCount << "columns from first row:";
280
281 static const auto supportedRoleNames = QQmlTableModelColumn::supportedRoleNames();
282
283 // Since we support different data structures at the row level, we require that there
284 // is a TableModelColumn for each column.
285 // Collect and cache metadata for each column. This makes data lookup faster.
286 for (int columnIndex = 0; columnIndex < mColumns.size(); ++columnIndex) {
287 QQmlTableModelColumn *column = mColumns.at(i: columnIndex);
288 qCDebug(lcTableModel).nospace() << "- column " << columnIndex << ":";
289
290 ColumnMetadata metaData;
291 const auto builtInRoleKeys = supportedRoleNames.keys();
292 for (const int builtInRoleKey : builtInRoleKeys) {
293 const QString builtInRoleName = supportedRoleNames.value(key: builtInRoleKey);
294 ColumnRoleMetadata roleData = fetchColumnRoleData(roleNameKey: builtInRoleName, tableModelColumn: column, columnIndex);
295 if (roleData.type == QMetaType::UnknownType) {
296 // This built-in role was not specified in this column.
297 continue;
298 }
299
300 qCDebug(lcTableModel).nospace() << " - added metadata for built-in role "
301 << builtInRoleName << " at column index " << columnIndex
302 << ": name=" << roleData.name << " typeName=" << roleData.typeName
303 << " type=" << roleData.type;
304
305 // This column now supports this specific built-in role.
306 metaData.roles.insert(key: builtInRoleName, value: roleData);
307 // Add it if it doesn't already exist.
308 mRoleNames[builtInRoleKey] = builtInRoleName.toLatin1();
309 }
310 mColumnMetadata.insert(i: columnIndex, t: metaData);
311 }
312}
313
314/*!
315 \qmlmethod TableModel::appendRow(object row)
316
317 Adds a new row to the end of the model, with the
318 values (cells) in \a row.
319
320 \code
321 model.appendRow({
322 checkable: true,
323 amount: 1,
324 fruitType: "Pear",
325 fruitName: "Williams",
326 fruitPrice: 1.50,
327 })
328 \endcode
329
330 \sa insertRow(), setRow(), removeRow()
331*/
332void QQmlTableModel::appendRow(const QVariant &row)
333{
334 if (!validateNewRow(functionName: "appendRow()", row, rowIndex: -1, operation: AppendOperation))
335 return;
336
337 doInsert(rowIndex: mRowCount, row);
338}
339
340/*!
341 \qmlmethod TableModel::clear()
342
343 Removes all rows from the model.
344
345 \sa removeRow()
346*/
347void QQmlTableModel::clear()
348{
349 QQmlEngine *engine = qmlEngine(this);
350 Q_ASSERT(engine);
351 setRows(QVariant::fromValue(value: engine->newArray()));
352}
353
354/*!
355 \qmlmethod object TableModel::getRow(int rowIndex)
356
357 Returns the row at \a rowIndex in the model.
358
359 Note that this equivalent to accessing the row directly
360 through the \l rows property:
361
362 \code
363 Component.onCompleted: {
364 // These two lines are equivalent.
365 console.log(model.getRow(0).display);
366 console.log(model.rows[0].fruitName);
367 }
368 \endcode
369
370 \note the returned object cannot be used to modify the contents of the
371 model; use setRow() instead.
372
373 \sa setRow(), appendRow(), insertRow(), removeRow(), moveRow()
374*/
375QVariant QQmlTableModel::getRow(int rowIndex)
376{
377 if (!validateRowIndex(functionName: "getRow()", argumentName: "rowIndex", rowIndex))
378 return QVariant();
379
380 return mRows.at(i: rowIndex);
381}
382
383/*!
384 \qmlmethod TableModel::insertRow(int rowIndex, object row)
385
386 Adds a new row to the list model at position \a rowIndex, with the
387 values (cells) in \a row.
388
389 \code
390 model.insertRow(2, {
391 checkable: true, checked: false,
392 amount: 1,
393 fruitType: "Pear",
394 fruitName: "Williams",
395 fruitPrice: 1.50,
396 })
397 \endcode
398
399 The \a rowIndex must be to an existing item in the list, or one past
400 the end of the list (equivalent to \l appendRow()).
401
402 \sa appendRow(), setRow(), removeRow(), rowCount
403*/
404void QQmlTableModel::insertRow(int rowIndex, const QVariant &row)
405{
406 if (!validateNewRow(functionName: "insertRow()", row, rowIndex))
407 return;
408
409 doInsert(rowIndex, row);
410}
411
412void QQmlTableModel::doInsert(int rowIndex, const QVariant &row)
413{
414 beginInsertRows(parent: QModelIndex(), first: rowIndex, last: rowIndex);
415
416 // Adding rowAsVariant.toList() will add each invidual variant in the list,
417 // which is definitely not what we want.
418 const QVariant rowAsVariant = row.value<QJSValue>().toVariant();
419 mRows.insert(i: rowIndex, t: rowAsVariant);
420 ++mRowCount;
421
422 qCDebug(lcTableModel).nospace() << "inserted the following row to the model at index "
423 << rowIndex << ":\n" << rowAsVariant.toMap();
424
425 // Gather metadata the first time a row is added.
426 if (mColumnMetadata.isEmpty())
427 fetchColumnMetadata();
428
429 endInsertRows();
430 emit rowCountChanged();
431}
432
433void QQmlTableModel::classBegin()
434{
435}
436
437void QQmlTableModel::componentComplete()
438{
439 componentCompleted = true;
440
441 mColumnCount = mColumns.size();
442 if (mColumnCount > 0)
443 emit columnCountChanged();
444
445 doSetRows(rowsAsVariantList: mRows);
446}
447
448/*!
449 \qmlmethod TableModel::moveRow(int fromRowIndex, int toRowIndex, int rows)
450
451 Moves \a rows from the index at \a fromRowIndex to the index at
452 \a toRowIndex.
453
454 The from and to ranges must exist; for example, to move the first 3 items
455 to the end of the list:
456
457 \code
458 model.moveRow(0, model.rowCount - 3, 3)
459 \endcode
460
461 \sa appendRow(), insertRow(), removeRow(), rowCount
462*/
463void QQmlTableModel::moveRow(int fromRowIndex, int toRowIndex, int rows)
464{
465 if (fromRowIndex == toRowIndex) {
466 qmlWarning(me: this) << "moveRow(): \"fromRowIndex\" cannot be equal to \"toRowIndex\"";
467 return;
468 }
469
470 if (rows <= 0) {
471 qmlWarning(me: this) << "moveRow(): \"rows\" is less than or equal to 0";
472 return;
473 }
474
475 if (!validateRowIndex(functionName: "moveRow()", argumentName: "fromRowIndex", rowIndex: fromRowIndex))
476 return;
477
478 if (!validateRowIndex(functionName: "moveRow()", argumentName: "toRowIndex", rowIndex: toRowIndex))
479 return;
480
481 if (fromRowIndex + rows > mRowCount) {
482 qmlWarning(me: this) << "moveRow(): \"fromRowIndex\" (" << fromRowIndex
483 << ") + \"rows\" (" << rows << ") = " << (fromRowIndex + rows)
484 << ", which is greater than rowCount() of " << mRowCount;
485 return;
486 }
487
488 if (toRowIndex + rows > mRowCount) {
489 qmlWarning(me: this) << "moveRow(): \"toRowIndex\" (" << toRowIndex
490 << ") + \"rows\" (" << rows << ") = " << (toRowIndex + rows)
491 << ", which is greater than rowCount() of " << mRowCount;
492 return;
493 }
494
495 qCDebug(lcTableModel).nospace() << "moving " << rows
496 << " row(s) from index " << fromRowIndex
497 << " to index " << toRowIndex;
498
499 // Based on the same call in QQmlListModel::moveRow().
500 beginMoveRows(sourceParent: QModelIndex(), sourceFirst: fromRowIndex, sourceLast: fromRowIndex + rows - 1, destinationParent: QModelIndex(),
501 destinationRow: toRowIndex > fromRowIndex ? toRowIndex + rows : toRowIndex);
502
503 // Based on ListModel::moveRow().
504 if (fromRowIndex > toRowIndex) {
505 // Only move forwards - flip if moving backwards.
506 const int from = fromRowIndex;
507 const int to = toRowIndex;
508 fromRowIndex = to;
509 toRowIndex = to + rows;
510 rows = from - to;
511 }
512
513 QVector<QVariant> store;
514 store.reserve(size: rows);
515 for (int i = 0; i < (toRowIndex - fromRowIndex); ++i)
516 store.append(t: mRows.at(i: fromRowIndex + rows + i));
517 for (int i = 0; i < rows; ++i)
518 store.append(t: mRows.at(i: fromRowIndex + i));
519 for (int i = 0; i < store.size(); ++i)
520 mRows[fromRowIndex + i] = store[i];
521
522 qCDebug(lcTableModel).nospace() << "after moving, rows are:\n" << mRows;
523
524 endMoveRows();
525}
526
527/*!
528 \qmlmethod TableModel::removeRow(int rowIndex, int rows = 1)
529
530 Removes a number of \a rows at \a rowIndex from the model.
531
532 \sa clear(), rowCount
533*/
534void QQmlTableModel::removeRow(int rowIndex, int rows)
535{
536 if (!validateRowIndex(functionName: "removeRow()", argumentName: "rowIndex", rowIndex))
537 return;
538
539 if (rows <= 0) {
540 qmlWarning(me: this) << "removeRow(): \"rows\" is less than or equal to zero";
541 return;
542 }
543
544 if (rowIndex + rows - 1 >= mRowCount) {
545 qmlWarning(me: this) << "removeRow(): \"rows\" " << rows
546 << " exceeds available rowCount() of " << mRowCount
547 << " when removing from \"rowIndex\" " << rowIndex;
548 return;
549 }
550
551 beginRemoveRows(parent: QModelIndex(), first: rowIndex, last: rowIndex + rows - 1);
552
553 auto firstIterator = mRows.begin() + rowIndex;
554 // The "last" argument to erase() is exclusive, so we go one past the last item.
555 auto lastIterator = firstIterator + rows;
556 mRows.erase(begin: firstIterator, end: lastIterator);
557 mRowCount -= rows;
558
559 endRemoveRows();
560 emit rowCountChanged();
561
562 qCDebug(lcTableModel).nospace() << "removed " << rows
563 << " items from the model, starting at index " << rowIndex;
564}
565
566/*!
567 \qmlmethod TableModel::setRow(int rowIndex, object row)
568
569 Changes the row at \a rowIndex in the model with \a row.
570
571 All columns/cells must be present in \c row, and in the correct order.
572
573 \code
574 model.setRow(0, {
575 checkable: true,
576 amount: 1,
577 fruitType: "Pear",
578 fruitName: "Williams",
579 fruitPrice: 1.50,
580 })
581 \endcode
582
583 If \a rowIndex is equal to \c rowCount(), then a new row is appended to the
584 model. Otherwise, \a rowIndex must point to an existing row in the model.
585
586 \sa appendRow(), insertRow(), rowCount
587*/
588void QQmlTableModel::setRow(int rowIndex, const QVariant &row)
589{
590 if (!validateNewRow(functionName: "setRow()", row, rowIndex))
591 return;
592
593 if (rowIndex != mRowCount) {
594 // Setting an existing row.
595 mRows[rowIndex] = row;
596
597 // For now we just assume the whole row changed, as it's simpler.
598 const QModelIndex topLeftModelIndex(createIndex(arow: rowIndex, acolumn: 0));
599 const QModelIndex bottomRightModelIndex(createIndex(arow: rowIndex, acolumn: mColumnCount - 1));
600 emit dataChanged(topLeft: topLeftModelIndex, bottomRight: bottomRightModelIndex);
601 } else {
602 // Appending a row.
603 doInsert(rowIndex, row);
604 }
605}
606
607QQmlListProperty<QQmlTableModelColumn> QQmlTableModel::columns()
608{
609 return QQmlListProperty<QQmlTableModelColumn>(this, nullptr,
610 &QQmlTableModel::columns_append,
611 &QQmlTableModel::columns_count,
612 &QQmlTableModel::columns_at,
613 &QQmlTableModel::columns_clear,
614 &QQmlTableModel::columns_replace,
615 &QQmlTableModel::columns_removeLast);
616}
617
618void QQmlTableModel::columns_append(QQmlListProperty<QQmlTableModelColumn> *property,
619 QQmlTableModelColumn *value)
620{
621 QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object);
622 QQmlTableModelColumn *column = qobject_cast<QQmlTableModelColumn*>(object: value);
623 if (column)
624 model->mColumns.append(t: column);
625}
626
627qsizetype QQmlTableModel::columns_count(QQmlListProperty<QQmlTableModelColumn> *property)
628{
629 const QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object);
630 return model->mColumns.size();
631}
632
633QQmlTableModelColumn *QQmlTableModel::columns_at(QQmlListProperty<QQmlTableModelColumn> *property, qsizetype index)
634{
635 const QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object);
636 return model->mColumns.at(i: index);
637}
638
639void QQmlTableModel::columns_clear(QQmlListProperty<QQmlTableModelColumn> *property)
640{
641 QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object);
642 return model->mColumns.clear();
643}
644
645void QQmlTableModel::columns_replace(QQmlListProperty<QQmlTableModelColumn> *property, qsizetype index, QQmlTableModelColumn *value)
646{
647 QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object);
648 if (QQmlTableModelColumn *column = qobject_cast<QQmlTableModelColumn*>(object: value))
649 return model->mColumns.replace(i: index, t: column);
650}
651
652void QQmlTableModel::columns_removeLast(QQmlListProperty<QQmlTableModelColumn> *property)
653{
654 QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object);
655 model->mColumns.removeLast();
656}
657
658/*!
659 \qmlmethod QModelIndex TableModel::index(int row, int column)
660
661 Returns a \l QModelIndex object referencing the given \a row and \a column,
662 which can be passed to the data() function to get the data from that cell,
663 or to setData() to edit the contents of that cell.
664
665 \code
666 import QtQml 2.14
667 import Qt.labs.qmlmodels 1.0
668
669 TableModel {
670 id: model
671
672 TableModelColumn { display: "fruitType" }
673 TableModelColumn { display: "fruitPrice" }
674
675 rows: [
676 { fruitType: "Apple", fruitPrice: 1.50 },
677 { fruitType: "Orange", fruitPrice: 2.50 }
678 ]
679
680 Component.onCompleted: {
681 for (var r = 0; r < model.rowCount; ++r) {
682 console.log("An " + model.data(model.index(r, 0)).display +
683 " costs " + model.data(model.index(r, 1)).display.toFixed(2))
684 }
685 }
686 }
687 \endcode
688
689 \sa {QModelIndex and related Classes in QML}, data()
690*/
691// Note: we don't document the parent argument, because you never need it, because
692// cells in a TableModel don't have parents. But it is there because this function is an override.
693QModelIndex QQmlTableModel::index(int row, int column, const QModelIndex &parent) const
694{
695 return row >= 0 && row < rowCount() && column >= 0 && column < columnCount() && !parent.isValid()
696 ? createIndex(arow: row, acolumn: column)
697 : QModelIndex();
698}
699
700/*!
701 \qmlproperty int TableModel::rowCount
702 \readonly
703
704 This read-only property holds the number of rows in the model.
705
706 This value changes whenever rows are added or removed from the model.
707*/
708int QQmlTableModel::rowCount(const QModelIndex &parent) const
709{
710 if (parent.isValid())
711 return 0;
712
713 return mRowCount;
714}
715
716/*!
717 \qmlproperty int TableModel::columnCount
718 \readonly
719
720 This read-only property holds the number of columns in the model.
721
722 The number of columns is fixed for the lifetime of the model
723 after the \l rows property is set or \l appendRow() is called for the first
724 time.
725*/
726int QQmlTableModel::columnCount(const QModelIndex &parent) const
727{
728 if (parent.isValid())
729 return 0;
730
731 return mColumnCount;
732}
733
734/*!
735 \qmlmethod variant TableModel::data(QModelIndex index, string role)
736
737 Returns the data from the table cell at the given \a index belonging to the
738 given \a role.
739
740 \sa index()
741*/
742QVariant QQmlTableModel::data(const QModelIndex &index, const QString &role) const
743{
744 const int iRole = mRoleNames.key(value: role.toUtf8(), defaultKey: -1);
745 if (iRole >= 0)
746 return data(index, role: iRole);
747 return QVariant();
748}
749
750QVariant QQmlTableModel::data(const QModelIndex &index, int role) const
751{
752 const int row = index.row();
753 if (row < 0 || row >= rowCount())
754 return QVariant();
755
756 const int column = index.column();
757 if (column < 0 || column >= columnCount())
758 return QVariant();
759
760 const ColumnMetadata columnMetadata = mColumnMetadata.at(i: index.column());
761 const QString roleName = QString::fromUtf8(ba: mRoleNames.value(key: role));
762 if (!columnMetadata.roles.contains(key: roleName)) {
763 qmlWarning(me: this) << "setData(): no role named " << roleName
764 << " at column index " << column << ". The available roles for that column are: "
765 << columnMetadata.roles.keys();
766 return QVariant();
767 }
768
769 const ColumnRoleMetadata roleData = columnMetadata.roles.value(key: roleName);
770 if (roleData.isStringRole) {
771 // We know the data structure, so we can get the data for the user.
772 const QVariantMap rowData = mRows.at(i: row).toMap();
773 const QString propertyName = columnMetadata.roles.value(key: roleName).name;
774 const QVariant value = rowData.value(key: propertyName);
775 return value;
776 }
777
778 // We don't know the data structure, so the user has to modify their data themselves.
779 // First, find the getter for this column and role.
780 QJSValue getter = mColumns.at(i: column)->getterAtRole(roleName);
781
782 // Then, call it and return what it returned.
783 const auto args = QJSValueList() << qmlEngine(this)->toScriptValue(value: index);
784 return getter.call(args).toVariant();
785}
786
787/*!
788 \qmlmethod bool TableModel::setData(QModelIndex index, string role, variant value)
789
790 Inserts or updates the data field named by \a role in the table cell at the
791 given \a index with \a value. Returns true if sucessful, false if not.
792
793 \sa index()
794*/
795bool QQmlTableModel::setData(const QModelIndex &index, const QString &role, const QVariant &value)
796{
797 const int intRole = mRoleNames.key(value: role.toUtf8(), defaultKey: -1);
798 if (intRole >= 0)
799 return setData(index, value, role: intRole);
800 return false;
801}
802
803bool QQmlTableModel::setData(const QModelIndex &index, const QVariant &value, int role)
804{
805 const int row = index.row();
806 if (row < 0 || row >= rowCount())
807 return false;
808
809 const int column = index.column();
810 if (column < 0 || column >= columnCount())
811 return false;
812
813 const QString roleName = QString::fromUtf8(ba: mRoleNames.value(key: role));
814
815 qCDebug(lcTableModel).nospace() << "setData() called with index "
816 << index << ", value " << value << " and role " << roleName;
817
818 // Verify that the role exists for this column.
819 const ColumnMetadata columnMetadata = mColumnMetadata.at(i: index.column());
820 if (!columnMetadata.roles.contains(key: roleName)) {
821 qmlWarning(me: this) << "setData(): no role named \"" << roleName
822 << "\" at column index " << column << ". The available roles for that column are: "
823 << columnMetadata.roles.keys();
824 return false;
825 }
826
827 // Verify that the type of the value is what we expect.
828 // If the value set is not of the expected type, we can try to convert it automatically.
829 const ColumnRoleMetadata roleData = columnMetadata.roles.value(key: roleName);
830 QVariant effectiveValue = value;
831 if (value.userType() != roleData.type) {
832 if (!value.canConvert(targetType: QMetaType(roleData.type))) {
833 qmlWarning(me: this).nospace() << "setData(): the value " << value
834 << " set at row " << row << " column " << column << " with role " << roleName
835 << " cannot be converted to " << roleData.typeName;
836 return false;
837 }
838
839 if (!effectiveValue.convert(type: QMetaType(roleData.type))) {
840 qmlWarning(me: this).nospace() << "setData(): failed converting value " << value
841 << " set at row " << row << " column " << column << " with role " << roleName
842 << " to " << roleData.typeName;
843 return false;
844 }
845 }
846
847 if (roleData.isStringRole) {
848 // We know the data structure, so we can set it for the user.
849 QVariantMap modifiedRow = mRows.at(i: row).toMap();
850 modifiedRow[roleData.name] = value;
851
852 mRows[row] = modifiedRow;
853 } else {
854 // We don't know the data structure, so the user has to modify their data themselves.
855 auto engine = qmlEngine(this);
856 auto args = QJSValueList()
857 // arg 0: modelIndex.
858 << engine->toScriptValue(value: index)
859 // arg 1: cellData.
860 << engine->toScriptValue(value);
861 // Do the actual setting.
862 QJSValue setter = mColumns.at(i: column)->setterAtRole(roleName);
863 setter.call(args);
864
865 /*
866 The chain of events so far:
867
868 - User did e.g.: model.edit = textInput.text
869 - setData() is called
870 - setData() calls the setter
871 (remember that we need to emit the dataChanged() signal,
872 which is why the user can't just set the data directly in the delegate)
873
874 Now the user's setter function has modified *their* copy of the
875 data, but *our* copy of the data is old. Imagine the getters and setters looked like this:
876
877 display: function(modelIndex) { return rows[modelIndex.row][1].amount }
878 setDisplay: function(modelIndex, cellData) { rows[modelIndex.row][1].amount = cellData }
879
880 We don't know the structure of the user's data, so we can't just do
881 what we do above for the isStringRole case:
882
883 modifiedRow[column][roleName] = value
884
885 This means that, besides getting the implicit row count when rows is initially set,
886 our copy of the data is unused when it comes to complex columns.
887
888 Another point to note is that we can't pass rowData in to the getter as a convenience,
889 because we would be passing in *our* copy of the row, which is not up-to-date.
890 Since the user already has access to the data, it's not a big deal for them to do:
891
892 display: function(modelIndex) { return rows[modelIndex.row][1].amount }
893
894 instead of:
895
896 display: function(modelIndex, rowData) { return rowData[1].amount }
897 */
898 }
899
900 QVector<int> rolesChanged;
901 rolesChanged.append(t: role);
902 emit dataChanged(topLeft: index, bottomRight: index, roles: rolesChanged);
903
904 return true;
905}
906
907QHash<int, QByteArray> QQmlTableModel::roleNames() const
908{
909 return mRoleNames;
910}
911
912QQmlTableModel::ColumnRoleMetadata::ColumnRoleMetadata()
913{
914}
915
916QQmlTableModel::ColumnRoleMetadata::ColumnRoleMetadata(
917 bool isStringRole, const QString &name, int type, const QString &typeName) :
918 isStringRole(isStringRole),
919 name(name),
920 type(type),
921 typeName(typeName)
922{
923}
924
925bool QQmlTableModel::ColumnRoleMetadata::isValid() const
926{
927 return !name.isEmpty();
928}
929
930bool QQmlTableModel::validateRowType(const char *functionName, const QVariant &row) const
931{
932 if (!row.canConvert<QJSValue>()) {
933 qmlWarning(me: this) << functionName << ": expected \"row\" argument to be a QJSValue,"
934 << " but got " << row.typeName() << " instead:\n" << row;
935 return false;
936 }
937
938 const QJSValue rowAsJSValue = row.value<QJSValue>();
939 if (!rowAsJSValue.isObject() && !rowAsJSValue.isArray()) {
940 qmlWarning(me: this) << functionName << ": expected \"row\" argument "
941 << "to be an object or array, but got:\n" << rowAsJSValue.toString();
942 return false;
943 }
944
945 return true;
946}
947
948bool QQmlTableModel::validateNewRow(const char *functionName, const QVariant &row,
949 int rowIndex, NewRowOperationFlag operation) const
950{
951 if (mColumnMetadata.isEmpty()) {
952 // There is no column metadata, so we have nothing to validate the row against.
953 // Rows have to be added before we can gather metadata from them, so just this
954 // once we'll return true to allow the rows to be added.
955 return true;
956 }
957
958 // Don't require each row to be a QJSValue when setting all rows,
959 // as they won't be; they'll be QVariantMap.
960 if (operation != SetRowsOperation && !validateRowType(functionName, row))
961 return false;
962
963 if (operation == OtherOperation) {
964 // Inserting/setting.
965 if (rowIndex < 0) {
966 qmlWarning(me: this) << functionName << ": \"rowIndex\" cannot be negative";
967 return false;
968 }
969
970 if (rowIndex > mRowCount) {
971 qmlWarning(me: this) << functionName << ": \"rowIndex\" " << rowIndex
972 << " is greater than rowCount() of " << mRowCount;
973 return false;
974 }
975 }
976
977 const QVariant rowAsVariant = operation == SetRowsOperation
978 ? row : row.value<QJSValue>().toVariant();
979 if (rowAsVariant.userType() != QMetaType::QVariantMap) {
980 qmlWarning(me: this) << functionName << ": row manipulation functions "
981 << "do not support complex rows (row index: " << rowIndex << ")";
982 return false;
983 }
984
985 const QVariantMap rowAsMap = rowAsVariant.toMap();
986 const int columnCount = rowAsMap.size();
987 if (columnCount < mColumnCount) {
988 qmlWarning(me: this) << functionName << ": expected " << mColumnCount
989 << " columns, but only got " << columnCount;
990 return false;
991 }
992
993 // We can't validate complex structures, but we can make sure that
994 // each simple string-based role in each column is correct.
995 for (int columnIndex = 0; columnIndex < mColumns.size(); ++columnIndex) {
996 QQmlTableModelColumn *column = mColumns.at(i: columnIndex);
997 const QHash<QString, QJSValue> getters = column->getters();
998 const auto roleNames = getters.keys();
999 const ColumnMetadata columnMetadata = mColumnMetadata.at(i: columnIndex);
1000 for (const QString &roleName : roleNames) {
1001 const ColumnRoleMetadata roleData = columnMetadata.roles.value(key: roleName);
1002 if (!roleData.isStringRole)
1003 continue;
1004
1005 if (!rowAsMap.contains(key: roleData.name)) {
1006 qmlWarning(me: this).quote() << functionName << ": expected a property named "
1007 << roleData.name << " in row at index " << rowIndex << ", but couldn't find one";
1008 return false;
1009 }
1010
1011 const QVariant rolePropertyValue = rowAsMap.value(key: roleData.name);
1012
1013 if (rolePropertyValue.userType() != roleData.type) {
1014 if (!rolePropertyValue.canConvert(targetType: QMetaType(roleData.type))) {
1015 qmlWarning(me: this).quote() << functionName << ": expected the property named "
1016 << roleData.name << " to be of type " << roleData.typeName
1017 << ", but got " << QString::fromLatin1(ba: rolePropertyValue.typeName())
1018 << " instead";
1019 return false;
1020 }
1021
1022 QVariant effectiveValue = rolePropertyValue;
1023 if (!effectiveValue.convert(type: QMetaType(roleData.type))) {
1024 qmlWarning(me: this).nospace() << functionName << ": failed converting value "
1025 << rolePropertyValue << " set at column " << columnIndex << " with role "
1026 << QString::fromLatin1(ba: rolePropertyValue.typeName()) << " to "
1027 << roleData.typeName;
1028 return false;
1029 }
1030 }
1031 }
1032 }
1033
1034 return true;
1035}
1036
1037bool QQmlTableModel::validateRowIndex(const char *functionName, const char *argumentName, int rowIndex) const
1038{
1039 if (rowIndex < 0) {
1040 qmlWarning(me: this) << functionName << ": \"" << argumentName << "\" cannot be negative";
1041 return false;
1042 }
1043
1044 if (rowIndex >= mRowCount) {
1045 qmlWarning(me: this) << functionName << ": \"" << argumentName
1046 << "\" " << rowIndex << " is greater than or equal to rowCount() of " << mRowCount;
1047 return false;
1048 }
1049
1050 return true;
1051}
1052
1053Qt::ItemFlags QQmlTableModel::flags(const QModelIndex &index) const
1054{
1055 Q_UNUSED(index)
1056 return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable;
1057}
1058
1059QT_END_NAMESPACE
1060
1061#include "moc_qqmltablemodel_p.cpp"
1062

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