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 | |
46 | QT_BEGIN_NAMESPACE |
47 | |
48 | Q_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 | |
168 | QQmlTableModel::QQmlTableModel(QObject *parent) |
169 | : QAbstractTableModel(parent) |
170 | { |
171 | } |
172 | |
173 | QQmlTableModel::~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 | */ |
186 | QVariant QQmlTableModel::rows() const |
187 | { |
188 | return mRows; |
189 | } |
190 | |
191 | void 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 | |
214 | void 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 | |
260 | QQmlTableModel::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 | |
311 | void 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 | */ |
366 | void 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 | */ |
381 | void 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 | */ |
409 | QVariant 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 | */ |
438 | void QQmlTableModel::insertRow(int rowIndex, const QVariant &row) |
439 | { |
440 | if (!validateNewRow(functionName: "insertRow()" , row, rowIndex)) |
441 | return; |
442 | |
443 | doInsert(rowIndex, row); |
444 | } |
445 | |
446 | void 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 | |
467 | void QQmlTableModel::classBegin() |
468 | { |
469 | } |
470 | |
471 | void 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 | */ |
497 | void 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 | */ |
568 | void 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 | */ |
622 | void 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 | |
641 | QQmlListProperty<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 | |
652 | void 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 | |
661 | int QQmlTableModel::columns_count(QQmlListProperty<QQmlTableModelColumn> *property) |
662 | { |
663 | const QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object); |
664 | return model->mColumns.count(); |
665 | } |
666 | |
667 | QQmlTableModelColumn *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 | |
673 | void QQmlTableModel::columns_clear(QQmlListProperty<QQmlTableModelColumn> *property) |
674 | { |
675 | QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object); |
676 | return model->mColumns.clear(); |
677 | } |
678 | |
679 | void 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 | |
686 | void 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. |
727 | QModelIndex 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 | */ |
742 | int 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 | */ |
760 | int 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 | */ |
776 | QVariant 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 | |
784 | QVariant 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 | */ |
829 | bool 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 | |
837 | bool 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 | |
941 | QHash<int, QByteArray> QQmlTableModel::roleNames() const |
942 | { |
943 | return mRoleNames; |
944 | } |
945 | |
946 | QQmlTableModel::ColumnRoleMetadata::ColumnRoleMetadata() |
947 | { |
948 | } |
949 | |
950 | QQmlTableModel::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 | |
959 | bool QQmlTableModel::ColumnRoleMetadata::isValid() const |
960 | { |
961 | return !name.isEmpty(); |
962 | } |
963 | |
964 | bool 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 | |
982 | bool 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 | |
1071 | bool 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 | |
1087 | QT_END_NAMESPACE |
1088 | |