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 | |
10 | QT_BEGIN_NAMESPACE |
11 | |
12 | Q_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 | |
134 | QQmlTableModel::QQmlTableModel(QObject *parent) |
135 | : QAbstractTableModel(parent) |
136 | { |
137 | } |
138 | |
139 | QQmlTableModel::~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 | */ |
152 | QVariant QQmlTableModel::rows() const |
153 | { |
154 | return mRows; |
155 | } |
156 | |
157 | void 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 | |
180 | void 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 | |
226 | QQmlTableModel::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 | |
277 | void 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 | */ |
332 | void 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 | */ |
347 | void 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 | */ |
375 | QVariant 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 | */ |
404 | void QQmlTableModel::insertRow(int rowIndex, const QVariant &row) |
405 | { |
406 | if (!validateNewRow(functionName: "insertRow()" , row, rowIndex)) |
407 | return; |
408 | |
409 | doInsert(rowIndex, row); |
410 | } |
411 | |
412 | void 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 | |
433 | void QQmlTableModel::classBegin() |
434 | { |
435 | } |
436 | |
437 | void 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 | */ |
463 | void 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 | */ |
534 | void 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 | */ |
588 | void 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 | |
607 | QQmlListProperty<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 | |
618 | void 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 | |
627 | qsizetype QQmlTableModel::columns_count(QQmlListProperty<QQmlTableModelColumn> *property) |
628 | { |
629 | const QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object); |
630 | return model->mColumns.size(); |
631 | } |
632 | |
633 | QQmlTableModelColumn *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 | |
639 | void QQmlTableModel::columns_clear(QQmlListProperty<QQmlTableModelColumn> *property) |
640 | { |
641 | QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object); |
642 | return model->mColumns.clear(); |
643 | } |
644 | |
645 | void 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 | |
652 | void 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. |
693 | QModelIndex 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 | */ |
708 | int 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 | */ |
726 | int 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 | */ |
742 | QVariant 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 | |
750 | QVariant 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 | */ |
795 | bool 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 | |
803 | bool 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 | |
907 | QHash<int, QByteArray> QQmlTableModel::roleNames() const |
908 | { |
909 | return mRoleNames; |
910 | } |
911 | |
912 | QQmlTableModel::ColumnRoleMetadata::ColumnRoleMetadata() |
913 | { |
914 | } |
915 | |
916 | QQmlTableModel::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 | |
925 | bool QQmlTableModel::ColumnRoleMetadata::isValid() const |
926 | { |
927 | return !name.isEmpty(); |
928 | } |
929 | |
930 | bool 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 | |
948 | bool 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 | |
1037 | bool 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 | |
1053 | Qt::ItemFlags QQmlTableModel::flags(const QModelIndex &index) const |
1054 | { |
1055 | Q_UNUSED(index) |
1056 | return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable; |
1057 | } |
1058 | |
1059 | QT_END_NAMESPACE |
1060 | |
1061 | #include "moc_qqmltablemodel_p.cpp" |
1062 | |