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//! \nativetype 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.userType() == QMetaType::QVariantMap ? row : row.value<QJSValue>().toVariant();
419
420 mRows.insert(i: rowIndex, t: rowAsVariant);
421 ++mRowCount;
422
423 qCDebug(lcTableModel).nospace() << "inserted the following row to the model at index "
424 << rowIndex << ":\n" << rowAsVariant.toMap();
425
426 // Gather metadata the first time a row is added.
427 if (mColumnMetadata.isEmpty())
428 fetchColumnMetadata();
429
430 endInsertRows();
431 emit rowCountChanged();
432}
433
434void QQmlTableModel::classBegin()
435{
436}
437
438void QQmlTableModel::componentComplete()
439{
440 componentCompleted = true;
441
442 mColumnCount = mColumns.size();
443 if (mColumnCount > 0)
444 emit columnCountChanged();
445
446 doSetRows(rowsAsVariantList: mRows);
447}
448
449/*!
450 \qmlmethod TableModel::moveRow(int fromRowIndex, int toRowIndex, int rows)
451
452 Moves \a rows from the index at \a fromRowIndex to the index at
453 \a toRowIndex.
454
455 The from and to ranges must exist; for example, to move the first 3 items
456 to the end of the list:
457
458 \code
459 model.moveRow(0, model.rowCount - 3, 3)
460 \endcode
461
462 \sa appendRow(), insertRow(), removeRow(), rowCount
463*/
464void QQmlTableModel::moveRow(int fromRowIndex, int toRowIndex, int rows)
465{
466 if (fromRowIndex == toRowIndex) {
467 qmlWarning(me: this) << "moveRow(): \"fromRowIndex\" cannot be equal to \"toRowIndex\"";
468 return;
469 }
470
471 if (rows <= 0) {
472 qmlWarning(me: this) << "moveRow(): \"rows\" is less than or equal to 0";
473 return;
474 }
475
476 if (!validateRowIndex(functionName: "moveRow()", argumentName: "fromRowIndex", rowIndex: fromRowIndex))
477 return;
478
479 if (!validateRowIndex(functionName: "moveRow()", argumentName: "toRowIndex", rowIndex: toRowIndex))
480 return;
481
482 if (fromRowIndex + rows > mRowCount) {
483 qmlWarning(me: this) << "moveRow(): \"fromRowIndex\" (" << fromRowIndex
484 << ") + \"rows\" (" << rows << ") = " << (fromRowIndex + rows)
485 << ", which is greater than rowCount() of " << mRowCount;
486 return;
487 }
488
489 if (toRowIndex + rows > mRowCount) {
490 qmlWarning(me: this) << "moveRow(): \"toRowIndex\" (" << toRowIndex
491 << ") + \"rows\" (" << rows << ") = " << (toRowIndex + rows)
492 << ", which is greater than rowCount() of " << mRowCount;
493 return;
494 }
495
496 qCDebug(lcTableModel).nospace() << "moving " << rows
497 << " row(s) from index " << fromRowIndex
498 << " to index " << toRowIndex;
499
500 // Based on the same call in QQmlListModel::moveRow().
501 beginMoveRows(sourceParent: QModelIndex(), sourceFirst: fromRowIndex, sourceLast: fromRowIndex + rows - 1, destinationParent: QModelIndex(),
502 destinationRow: toRowIndex > fromRowIndex ? toRowIndex + rows : toRowIndex);
503
504 // Based on ListModel::moveRow().
505 if (fromRowIndex > toRowIndex) {
506 // Only move forwards - flip if moving backwards.
507 const int from = fromRowIndex;
508 const int to = toRowIndex;
509 fromRowIndex = to;
510 toRowIndex = to + rows;
511 rows = from - to;
512 }
513
514 QVector<QVariant> store;
515 store.reserve(size: rows);
516 for (int i = 0; i < (toRowIndex - fromRowIndex); ++i)
517 store.append(t: mRows.at(i: fromRowIndex + rows + i));
518 for (int i = 0; i < rows; ++i)
519 store.append(t: mRows.at(i: fromRowIndex + i));
520 for (int i = 0; i < store.size(); ++i)
521 mRows[fromRowIndex + i] = store[i];
522
523 qCDebug(lcTableModel).nospace() << "after moving, rows are:\n" << mRows;
524
525 endMoveRows();
526}
527
528/*!
529 \qmlmethod TableModel::removeRow(int rowIndex, int rows = 1)
530
531 Removes a number of \a rows at \a rowIndex from the model.
532
533 \sa clear(), rowCount
534*/
535void QQmlTableModel::removeRow(int rowIndex, int rows)
536{
537 if (!validateRowIndex(functionName: "removeRow()", argumentName: "rowIndex", rowIndex))
538 return;
539
540 if (rows <= 0) {
541 qmlWarning(me: this) << "removeRow(): \"rows\" is less than or equal to zero";
542 return;
543 }
544
545 if (rowIndex + rows - 1 >= mRowCount) {
546 qmlWarning(me: this) << "removeRow(): \"rows\" " << rows
547 << " exceeds available rowCount() of " << mRowCount
548 << " when removing from \"rowIndex\" " << rowIndex;
549 return;
550 }
551
552 beginRemoveRows(parent: QModelIndex(), first: rowIndex, last: rowIndex + rows - 1);
553
554 auto firstIterator = mRows.begin() + rowIndex;
555 // The "last" argument to erase() is exclusive, so we go one past the last item.
556 auto lastIterator = firstIterator + rows;
557 mRows.erase(begin: firstIterator, end: lastIterator);
558 mRowCount -= rows;
559
560 endRemoveRows();
561 emit rowCountChanged();
562
563 qCDebug(lcTableModel).nospace() << "removed " << rows
564 << " items from the model, starting at index " << rowIndex;
565}
566
567/*!
568 \qmlmethod TableModel::setRow(int rowIndex, object row)
569
570 Changes the row at \a rowIndex in the model with \a row.
571
572 All columns/cells must be present in \c row, and in the correct order.
573
574 \code
575 model.setRow(0, {
576 checkable: true,
577 amount: 1,
578 fruitType: "Pear",
579 fruitName: "Williams",
580 fruitPrice: 1.50,
581 })
582 \endcode
583
584 If \a rowIndex is equal to \c rowCount(), then a new row is appended to the
585 model. Otherwise, \a rowIndex must point to an existing row in the model.
586
587 \sa appendRow(), insertRow(), rowCount
588*/
589void QQmlTableModel::setRow(int rowIndex, const QVariant &row)
590{
591 if (!validateNewRow(functionName: "setRow()", row, rowIndex))
592 return;
593
594 if (rowIndex != mRowCount) {
595 // Setting an existing row.
596 mRows[rowIndex] = row;
597
598 // For now we just assume the whole row changed, as it's simpler.
599 const QModelIndex topLeftModelIndex(createIndex(arow: rowIndex, acolumn: 0));
600 const QModelIndex bottomRightModelIndex(createIndex(arow: rowIndex, acolumn: mColumnCount - 1));
601 emit dataChanged(topLeft: topLeftModelIndex, bottomRight: bottomRightModelIndex);
602 } else {
603 // Appending a row.
604 doInsert(rowIndex, row);
605 }
606}
607
608QQmlListProperty<QQmlTableModelColumn> QQmlTableModel::columns()
609{
610 return QQmlListProperty<QQmlTableModelColumn>(this, nullptr,
611 &QQmlTableModel::columns_append,
612 &QQmlTableModel::columns_count,
613 &QQmlTableModel::columns_at,
614 &QQmlTableModel::columns_clear,
615 &QQmlTableModel::columns_replace,
616 &QQmlTableModel::columns_removeLast);
617}
618
619void QQmlTableModel::columns_append(QQmlListProperty<QQmlTableModelColumn> *property,
620 QQmlTableModelColumn *value)
621{
622 QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object);
623 QQmlTableModelColumn *column = qobject_cast<QQmlTableModelColumn*>(object: value);
624 if (column)
625 model->mColumns.append(t: column);
626}
627
628qsizetype QQmlTableModel::columns_count(QQmlListProperty<QQmlTableModelColumn> *property)
629{
630 const QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object);
631 return model->mColumns.size();
632}
633
634QQmlTableModelColumn *QQmlTableModel::columns_at(QQmlListProperty<QQmlTableModelColumn> *property, qsizetype index)
635{
636 const QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object);
637 return model->mColumns.at(i: index);
638}
639
640void QQmlTableModel::columns_clear(QQmlListProperty<QQmlTableModelColumn> *property)
641{
642 QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object);
643 return model->mColumns.clear();
644}
645
646void QQmlTableModel::columns_replace(QQmlListProperty<QQmlTableModelColumn> *property, qsizetype index, QQmlTableModelColumn *value)
647{
648 QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object);
649 if (QQmlTableModelColumn *column = qobject_cast<QQmlTableModelColumn*>(object: value))
650 return model->mColumns.replace(i: index, t: column);
651}
652
653void QQmlTableModel::columns_removeLast(QQmlListProperty<QQmlTableModelColumn> *property)
654{
655 QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object);
656 model->mColumns.removeLast();
657}
658
659/*!
660 \qmlmethod QModelIndex TableModel::index(int row, int column)
661
662 Returns a \l QModelIndex object referencing the given \a row and \a column,
663 which can be passed to the data() function to get the data from that cell,
664 or to setData() to edit the contents of that cell.
665
666 \code
667 import QtQml 2.14
668 import Qt.labs.qmlmodels 1.0
669
670 TableModel {
671 id: model
672
673 TableModelColumn { display: "fruitType" }
674 TableModelColumn { display: "fruitPrice" }
675
676 rows: [
677 { fruitType: "Apple", fruitPrice: 1.50 },
678 { fruitType: "Orange", fruitPrice: 2.50 }
679 ]
680
681 Component.onCompleted: {
682 for (var r = 0; r < model.rowCount; ++r) {
683 console.log("An " + model.data(model.index(r, 0)).display +
684 " costs " + model.data(model.index(r, 1)).display.toFixed(2))
685 }
686 }
687 }
688 \endcode
689
690 \sa {QModelIndex and related Classes in QML}, data()
691*/
692// Note: we don't document the parent argument, because you never need it, because
693// cells in a TableModel don't have parents. But it is there because this function is an override.
694QModelIndex QQmlTableModel::index(int row, int column, const QModelIndex &parent) const
695{
696 return row >= 0 && row < rowCount() && column >= 0 && column < columnCount() && !parent.isValid()
697 ? createIndex(arow: row, acolumn: column)
698 : QModelIndex();
699}
700
701/*!
702 \qmlproperty int TableModel::rowCount
703 \readonly
704
705 This read-only property holds the number of rows in the model.
706
707 This value changes whenever rows are added or removed from the model.
708*/
709int QQmlTableModel::rowCount(const QModelIndex &parent) const
710{
711 if (parent.isValid())
712 return 0;
713
714 return mRowCount;
715}
716
717/*!
718 \qmlproperty int TableModel::columnCount
719 \readonly
720
721 This read-only property holds the number of columns in the model.
722
723 The number of columns is fixed for the lifetime of the model
724 after the \l rows property is set or \l appendRow() is called for the first
725 time.
726*/
727int QQmlTableModel::columnCount(const QModelIndex &parent) const
728{
729 if (parent.isValid())
730 return 0;
731
732 return mColumnCount;
733}
734
735/*!
736 \qmlmethod variant TableModel::data(QModelIndex index, string role)
737
738 Returns the data from the table cell at the given \a index belonging to the
739 given \a role.
740
741 \sa index()
742*/
743QVariant QQmlTableModel::data(const QModelIndex &index, const QString &role) const
744{
745 const int iRole = mRoleNames.key(value: role.toUtf8(), defaultKey: -1);
746 if (iRole >= 0)
747 return data(index, role: iRole);
748 return QVariant();
749}
750
751QVariant QQmlTableModel::data(const QModelIndex &index, int role) const
752{
753 const int row = index.row();
754 if (row < 0 || row >= rowCount())
755 return QVariant();
756
757 const int column = index.column();
758 if (column < 0 || column >= columnCount())
759 return QVariant();
760
761 const ColumnMetadata columnMetadata = mColumnMetadata.at(i: index.column());
762 const QString roleName = QString::fromUtf8(ba: mRoleNames.value(key: role));
763 if (!columnMetadata.roles.contains(key: roleName)) {
764 qmlWarning(me: this) << "setData(): no role named " << roleName
765 << " at column index " << column << ". The available roles for that column are: "
766 << columnMetadata.roles.keys();
767 return QVariant();
768 }
769
770 const ColumnRoleMetadata roleData = columnMetadata.roles.value(key: roleName);
771 if (roleData.isStringRole) {
772 // We know the data structure, so we can get the data for the user.
773 const QVariantMap rowData = mRows.at(i: row).toMap();
774 const QString propertyName = columnMetadata.roles.value(key: roleName).name;
775 const QVariant value = rowData.value(key: propertyName);
776 return value;
777 }
778
779 // We don't know the data structure, so the user has to modify their data themselves.
780 // First, find the getter for this column and role.
781 QJSValue getter = mColumns.at(i: column)->getterAtRole(roleName);
782
783 // Then, call it and return what it returned.
784 const auto args = QJSValueList() << qmlEngine(this)->toScriptValue(value: index);
785 return getter.call(args).toVariant();
786}
787
788/*!
789 \qmlmethod bool TableModel::setData(QModelIndex index, string role, variant value)
790
791 Inserts or updates the data field named by \a role in the table cell at the
792 given \a index with \a value. Returns true if sucessful, false if not.
793
794 \sa index()
795*/
796bool QQmlTableModel::setData(const QModelIndex &index, const QString &role, const QVariant &value)
797{
798 const int intRole = mRoleNames.key(value: role.toUtf8(), defaultKey: -1);
799 if (intRole >= 0)
800 return setData(index, value, role: intRole);
801 return false;
802}
803
804bool QQmlTableModel::setData(const QModelIndex &index, const QVariant &value, int role)
805{
806 const int row = index.row();
807 if (row < 0 || row >= rowCount())
808 return false;
809
810 const int column = index.column();
811 if (column < 0 || column >= columnCount())
812 return false;
813
814 const QString roleName = QString::fromUtf8(ba: mRoleNames.value(key: role));
815
816 qCDebug(lcTableModel).nospace() << "setData() called with index "
817 << index << ", value " << value << " and role " << roleName;
818
819 // Verify that the role exists for this column.
820 const ColumnMetadata columnMetadata = mColumnMetadata.at(i: index.column());
821 if (!columnMetadata.roles.contains(key: roleName)) {
822 qmlWarning(me: this) << "setData(): no role named \"" << roleName
823 << "\" at column index " << column << ". The available roles for that column are: "
824 << columnMetadata.roles.keys();
825 return false;
826 }
827
828 // Verify that the type of the value is what we expect.
829 // If the value set is not of the expected type, we can try to convert it automatically.
830 const ColumnRoleMetadata roleData = columnMetadata.roles.value(key: roleName);
831 QVariant effectiveValue = value;
832 if (value.userType() != roleData.type) {
833 if (!value.canConvert(targetType: QMetaType(roleData.type))) {
834 qmlWarning(me: this).nospace() << "setData(): the value " << value
835 << " set at row " << row << " column " << column << " with role " << roleName
836 << " cannot be converted to " << roleData.typeName;
837 return false;
838 }
839
840 if (!effectiveValue.convert(type: QMetaType(roleData.type))) {
841 qmlWarning(me: this).nospace() << "setData(): failed converting value " << value
842 << " set at row " << row << " column " << column << " with role " << roleName
843 << " to " << roleData.typeName;
844 return false;
845 }
846 }
847
848 if (roleData.isStringRole) {
849 // We know the data structure, so we can set it for the user.
850 QVariantMap modifiedRow = mRows.at(i: row).toMap();
851 modifiedRow[roleData.name] = value;
852
853 mRows[row] = modifiedRow;
854 } else {
855 // We don't know the data structure, so the user has to modify their data themselves.
856 auto engine = qmlEngine(this);
857 auto args = QJSValueList()
858 // arg 0: modelIndex.
859 << engine->toScriptValue(value: index)
860 // arg 1: cellData.
861 << engine->toScriptValue(value);
862 // Do the actual setting.
863 QJSValue setter = mColumns.at(i: column)->setterAtRole(roleName);
864 setter.call(args);
865
866 /*
867 The chain of events so far:
868
869 - User did e.g.: model.edit = textInput.text
870 - setData() is called
871 - setData() calls the setter
872 (remember that we need to emit the dataChanged() signal,
873 which is why the user can't just set the data directly in the delegate)
874
875 Now the user's setter function has modified *their* copy of the
876 data, but *our* copy of the data is old. Imagine the getters and setters looked like this:
877
878 display: function(modelIndex) { return rows[modelIndex.row][1].amount }
879 setDisplay: function(modelIndex, cellData) { rows[modelIndex.row][1].amount = cellData }
880
881 We don't know the structure of the user's data, so we can't just do
882 what we do above for the isStringRole case:
883
884 modifiedRow[column][roleName] = value
885
886 This means that, besides getting the implicit row count when rows is initially set,
887 our copy of the data is unused when it comes to complex columns.
888
889 Another point to note is that we can't pass rowData in to the getter as a convenience,
890 because we would be passing in *our* copy of the row, which is not up-to-date.
891 Since the user already has access to the data, it's not a big deal for them to do:
892
893 display: function(modelIndex) { return rows[modelIndex.row][1].amount }
894
895 instead of:
896
897 display: function(modelIndex, rowData) { return rowData[1].amount }
898 */
899 }
900
901 QVector<int> rolesChanged;
902 rolesChanged.append(t: role);
903 emit dataChanged(topLeft: index, bottomRight: index, roles: rolesChanged);
904
905 return true;
906}
907
908QHash<int, QByteArray> QQmlTableModel::roleNames() const
909{
910 return mRoleNames;
911}
912
913QQmlTableModel::ColumnRoleMetadata::ColumnRoleMetadata()
914{
915}
916
917QQmlTableModel::ColumnRoleMetadata::ColumnRoleMetadata(
918 bool isStringRole, const QString &name, int type, const QString &typeName) :
919 isStringRole(isStringRole),
920 name(name),
921 type(type),
922 typeName(typeName)
923{
924}
925
926bool QQmlTableModel::ColumnRoleMetadata::isValid() const
927{
928 return !name.isEmpty();
929}
930
931bool QQmlTableModel::validateRowType(const char *functionName, const QVariant &row) const
932{
933 if (!row.canConvert<QJSValue>()) {
934 qmlWarning(me: this) << functionName << ": expected \"row\" argument to be a QJSValue,"
935 << " but got " << row.typeName() << " instead:\n" << row;
936 return false;
937 }
938
939 const QJSValue rowAsJSValue = row.value<QJSValue>();
940 if (!rowAsJSValue.isObject() && !rowAsJSValue.isArray()) {
941 qmlWarning(me: this) << functionName << ": expected \"row\" argument "
942 << "to be an object or array, but got:\n" << rowAsJSValue.toString();
943 return false;
944 }
945
946 return true;
947}
948
949bool QQmlTableModel::validateNewRow(const char *functionName, const QVariant &row,
950 int rowIndex, NewRowOperationFlag operation) const
951{
952 if (mColumnMetadata.isEmpty()) {
953 // There is no column metadata, so we have nothing to validate the row against.
954 // Rows have to be added before we can gather metadata from them, so just this
955 // once we'll return true to allow the rows to be added.
956 return true;
957 }
958
959 const bool isVariantMap = (row.userType() == QMetaType::QVariantMap);
960
961 // Don't require each row to be a QJSValue when setting all rows,
962 // as they won't be; they'll be QVariantMap.
963 if (operation != SetRowsOperation && (!isVariantMap && !validateRowType(functionName, row)))
964 return false;
965
966 if (operation == OtherOperation) {
967 // Inserting/setting.
968 if (rowIndex < 0) {
969 qmlWarning(me: this) << functionName << ": \"rowIndex\" cannot be negative";
970 return false;
971 }
972
973 if (rowIndex > mRowCount) {
974 qmlWarning(me: this) << functionName << ": \"rowIndex\" " << rowIndex
975 << " is greater than rowCount() of " << mRowCount;
976 return false;
977 }
978 }
979
980 const QVariant rowAsVariant = operation == SetRowsOperation || isVariantMap
981 ? row : row.value<QJSValue>().toVariant();
982 if (rowAsVariant.userType() != QMetaType::QVariantMap) {
983 qmlWarning(me: this) << functionName << ": row manipulation functions "
984 << "do not support complex rows (row index: " << rowIndex << ")";
985 return false;
986 }
987
988 const QVariantMap rowAsMap = rowAsVariant.toMap();
989 const int columnCount = rowAsMap.size();
990 if (columnCount < mColumnCount) {
991 qmlWarning(me: this) << functionName << ": expected " << mColumnCount
992 << " columns, but only got " << columnCount;
993 return false;
994 }
995
996 // We can't validate complex structures, but we can make sure that
997 // each simple string-based role in each column is correct.
998 for (int columnIndex = 0; columnIndex < mColumns.size(); ++columnIndex) {
999 QQmlTableModelColumn *column = mColumns.at(i: columnIndex);
1000 const QHash<QString, QJSValue> getters = column->getters();
1001 const auto roleNames = getters.keys();
1002 const ColumnMetadata columnMetadata = mColumnMetadata.at(i: columnIndex);
1003 for (const QString &roleName : roleNames) {
1004 const ColumnRoleMetadata roleData = columnMetadata.roles.value(key: roleName);
1005 if (!roleData.isStringRole)
1006 continue;
1007
1008 if (!rowAsMap.contains(key: roleData.name)) {
1009 qmlWarning(me: this).quote() << functionName << ": expected a property named "
1010 << roleData.name << " in row at index " << rowIndex << ", but couldn't find one";
1011 return false;
1012 }
1013
1014 const QVariant rolePropertyValue = rowAsMap.value(key: roleData.name);
1015
1016 if (rolePropertyValue.userType() != roleData.type) {
1017 if (!rolePropertyValue.canConvert(targetType: QMetaType(roleData.type))) {
1018 qmlWarning(me: this).quote() << functionName << ": expected the property named "
1019 << roleData.name << " to be of type " << roleData.typeName
1020 << ", but got " << QString::fromLatin1(ba: rolePropertyValue.typeName())
1021 << " instead";
1022 return false;
1023 }
1024
1025 QVariant effectiveValue = rolePropertyValue;
1026 if (!effectiveValue.convert(type: QMetaType(roleData.type))) {
1027 qmlWarning(me: this).nospace() << functionName << ": failed converting value "
1028 << rolePropertyValue << " set at column " << columnIndex << " with role "
1029 << QString::fromLatin1(ba: rolePropertyValue.typeName()) << " to "
1030 << roleData.typeName;
1031 return false;
1032 }
1033 }
1034 }
1035 }
1036
1037 return true;
1038}
1039
1040bool QQmlTableModel::validateRowIndex(const char *functionName, const char *argumentName, int rowIndex) const
1041{
1042 if (rowIndex < 0) {
1043 qmlWarning(me: this) << functionName << ": \"" << argumentName << "\" cannot be negative";
1044 return false;
1045 }
1046
1047 if (rowIndex >= mRowCount) {
1048 qmlWarning(me: this) << functionName << ": \"" << argumentName
1049 << "\" " << rowIndex << " is greater than or equal to rowCount() of " << mRowCount;
1050 return false;
1051 }
1052
1053 return true;
1054}
1055
1056Qt::ItemFlags QQmlTableModel::flags(const QModelIndex &index) const
1057{
1058 Q_UNUSED(index)
1059 return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable;
1060}
1061
1062QT_END_NAMESPACE
1063
1064#include "moc_qqmltablemodel_p.cpp"
1065

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

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