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 | //! \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 | |
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.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 | |
434 | void QQmlTableModel::classBegin() |
435 | { |
436 | } |
437 | |
438 | void 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 | */ |
464 | void 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 | */ |
535 | void 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 | */ |
589 | void 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 | |
608 | QQmlListProperty<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 | |
619 | void 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 | |
628 | qsizetype QQmlTableModel::columns_count(QQmlListProperty<QQmlTableModelColumn> *property) |
629 | { |
630 | const QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object); |
631 | return model->mColumns.size(); |
632 | } |
633 | |
634 | QQmlTableModelColumn *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 | |
640 | void QQmlTableModel::columns_clear(QQmlListProperty<QQmlTableModelColumn> *property) |
641 | { |
642 | QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object); |
643 | return model->mColumns.clear(); |
644 | } |
645 | |
646 | void 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 | |
653 | void 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. |
694 | QModelIndex 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 | */ |
709 | int 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 | */ |
727 | int 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 | */ |
743 | QVariant 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 | |
751 | QVariant 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 | */ |
796 | bool 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 | |
804 | bool 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 | |
908 | QHash<int, QByteArray> QQmlTableModel::roleNames() const |
909 | { |
910 | return mRoleNames; |
911 | } |
912 | |
913 | QQmlTableModel::ColumnRoleMetadata::ColumnRoleMetadata() |
914 | { |
915 | } |
916 | |
917 | QQmlTableModel::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 | |
926 | bool QQmlTableModel::ColumnRoleMetadata::isValid() const |
927 | { |
928 | return !name.isEmpty(); |
929 | } |
930 | |
931 | bool 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 | |
949 | bool 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 | |
1040 | bool 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 | |
1056 | Qt::ItemFlags QQmlTableModel::flags(const QModelIndex &index) const |
1057 | { |
1058 | Q_UNUSED(index) |
1059 | return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable; |
1060 | } |
1061 | |
1062 | QT_END_NAMESPACE |
1063 | |
1064 | #include "moc_qqmltablemodel_p.cpp" |
1065 |
Definitions
- lcTableModel
- QQmlTableModel
- ~QQmlTableModel
- rows
- setRows
- doSetRows
- fetchColumnRoleData
- fetchColumnMetadata
- appendRow
- clear
- getRow
- insertRow
- doInsert
- classBegin
- componentComplete
- moveRow
- removeRow
- setRow
- columns
- columns_append
- columns_count
- columns_at
- columns_clear
- columns_replace
- columns_removeLast
- index
- rowCount
- columnCount
- data
- data
- setData
- setData
- roleNames
- ColumnRoleMetadata
- ColumnRoleMetadata
- isValid
- validateRowType
- validateNewRow
- validateRowIndex
Learn to use CMake with our Intro Training
Find out more