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

source code of qtdeclarative/src/imports/labsmodels/qqmltablemodel.cpp