diff options
Diffstat (limited to 'src/qml/types/qqmltablemodel.cpp')
-rw-r--r-- | src/qml/types/qqmltablemodel.cpp | 731 |
1 files changed, 420 insertions, 311 deletions
diff --git a/src/qml/types/qqmltablemodel.cpp b/src/qml/types/qqmltablemodel.cpp index 6068155f5a..4a96e7a46b 100644 --- a/src/qml/types/qqmltablemodel.cpp +++ b/src/qml/types/qqmltablemodel.cpp @@ -47,27 +47,26 @@ QT_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(lcTableModel, "qt.qml.tablemodel") -static const QString lengthPropertyName = QStringLiteral("length"); -static const QString displayRoleName = QStringLiteral("display"); - /*! \qmltype TableModel \instantiates QQmlTableModel \inqmlmodule Qt.labs.qmlmodels \brief Encapsulates a simple table model. - \since 5.12 - - The TableModel type stores JavaScript objects as data for a table model - that can be used with \l TableView. + \since 5.14 - The following snippet shows the simplest use case for TableModel: + The TableModel type stores JavaScript/JSON objects as data for a table + model that can be used with \l TableView. It is intended to support + very simple models without requiring the creation of a custom + QAbstractTableModel subclass in C++. \snippet qml/tablemodel/fruit-example-simpledelegate.qml file - The model's initial data is set with either the \l rows property or by - calling \l appendRow(). Once the first row has been added to the table, the - columns and roles are established and will be fixed for the lifetime of the - model. + The model's initial row data is set with either the \l rows property or by + calling \l appendRow(). Each column in the model is specified by declaring + a \l TableModelColumn instance, where the order of each instance determines + its column index. Once the model's \l Component.completed() signal has been + emitted, the columns and roles will have been established and are then + fixed for the lifetime of the model. To access a specific row, the \l getRow() function can be used. It's also possible to access the model's JavaScript data @@ -87,14 +86,65 @@ static const QString displayRoleName = QStringLiteral("display"); data that is set, it will be automatically converted via \l {QVariant::canConvert()}{QVariant}. - For convenience, TableModel provides the \c display role if it is not - explicitly specified in any column. When a column only has one role - declared, that role will be used used as the display role. However, when - there is more than one role in a column, which role will be used is - undefined. This is because JavaScript does not guarantee that properties - within an object can be accessed according to the order in which they were - declared. This is why \c checkable may be used as the display role for the - first column even though \c checked is declared before it, for example. + \section1 Supported Row Data Structures + + TableModel is designed to work with JavaScript/JSON data, where each row + is a simple key-pair object: + + \code + { + // Each property is one cell/column. + checked: false, + amount: 1, + fruitType: "Apple", + fruitName: "Granny Smith", + fruitPrice: 1.50 + }, + // ... + \endcode + + As model manipulation in Qt is done via row and column indices, + and because object keys are unordered, each column must be specified via + TableModelColumn. This allows mapping Qt's built-in roles to any property + in each row object. + + Complex row structures are supported, but with limited functionality. + As TableModel has no way of knowing how each row is structured, + it cannot manipulate it. As a consequence of this, the copy of the + model data that TableModel has stored in \l rows is not kept in sync + with the source data that was set in QML. For these reasons, TableModel + relies on the user to handle simple data manipulation. + + For example, suppose you wanted to have several roles per column. One way + of doing this is to use a data source where each row is an array and each + cell is an object. To use this data source with TableModel, define a + getter and setter: + + \code + TableModel { + TableModelColumn { + display: function(modelIndex) { return rows[modelIndex.row][0].checked } + setDisplay: function(modelIndex, cellData) { rows[modelIndex.row][0].checked = cellData } + } + // ... + + rows: [ + [ + { checked: false, checkable: true }, + { amount: 1 }, + { fruitType: "Apple" }, + { fruitName: "Granny Smith" }, + { fruitPrice: 1.50 } + ] + // ... + ] + } + \endcode + + The row above is one example of a complex row. + + \note Row manipulation functions such as \l appendRow(), \l removeRow(), + etc. are not supported when using complex rows. \section1 Using DelegateChooser with TableModel @@ -112,13 +162,12 @@ static const QString displayRoleName = QStringLiteral("display"); \l [QtQuickControls2]{TextField}, and so that delegate is declared last as a fallback. - \sa QAbstractTableModel, TableView + \sa TableModelColumn, TableView, QAbstractTableModel */ QQmlTableModel::QQmlTableModel(QObject *parent) : QAbstractTableModel(parent) { - mRoleNames = QAbstractTableModel::roleNames(); } QQmlTableModel::~QQmlTableModel() @@ -153,31 +202,34 @@ void QQmlTableModel::setRows(const QVariant &rows) return; } - QVariant firstRowAsVariant; - QVariantList firstRow; - if (!rowsAsVariantList.isEmpty()) { - // There are rows to validate. If they're not valid, - // we'll return early without changing anything. - firstRowAsVariant = rowsAsVariantList.first(); - firstRow = firstRowAsVariant.toList(); + if (!componentCompleted) { + // Store the rows until we can call doSetRows() after component completion. + mRows = rowsAsVariantList; + return; + } - if (firstRowAsVariant.type() != QVariant::List) { - qmlWarning(this) << "setRows(): each row in \"rows\" must be an array of objects"; - return; - } + doSetRows(rowsAsVariantList); +} - if (mColumnCount > 0) { - qCDebug(lcTableModel) << "validating" << rowsAsVariantList.size() - << "rows against existing metadata"; - - // This is not the first time the rows have been set; validate the new columns. - for (int i = 0; i < rowsAsVariantList.size(); ++i) { - // validateNewRow() expects a QVariant wrapping a QJSValue, so to - // simplify the code, just create one here. - const QVariant row = QVariant::fromValue(rowsAsJSValue.property(i)); - if (!validateNewRow("setRows()", row, i)) - return; - } +void QQmlTableModel::doSetRows(const QVariantList &rowsAsVariantList) +{ + Q_ASSERT(componentCompleted); + + // By now, all TableModelColumns should have been set. + if (mColumns.isEmpty()) { + qmlWarning(this) << "No TableModelColumns were set; model will be empty"; + return; + } + + const bool firstTimeValidRowsHaveBeenSet = mColumnMetadata.isEmpty(); + if (!firstTimeValidRowsHaveBeenSet) { + // This is not the first time rows have been set; validate each one. + for (int rowIndex = 0; rowIndex < rowsAsVariantList.size(); ++rowIndex) { + // validateNewRow() expects a QVariant wrapping a QJSValue, so to + // simplify the code, just create one here. + const QVariant row = QVariant::fromValue(rowsAsVariantList.at(rowIndex)); + if (!validateNewRow("setRows()", row, rowIndex, SetRowsOperation)) + return; } } @@ -191,59 +243,9 @@ void QQmlTableModel::setRows(const QVariant &rows) mRows = rowsAsVariantList; mRowCount = mRows.size(); - const bool isFirstTimeSet = mColumnCount == 0; - if (isFirstTimeSet && mRowCount > 0) { - // This is the first time the rows have been set, so establish - // the column count and gather column metadata. - mColumnCount = firstRow.size(); - qCDebug(lcTableModel) << "gathering metadata for" << mColumnCount << "columns from first row:"; - - // Go through each property of each cell in the first row - // and make a role name from it. - int userRoleKey = Qt::UserRole; - for (int columnIndex = 0; columnIndex < mColumnCount; ++columnIndex) { - // We need it as a QVariantMap because we need to get - // the name of the property, which we can't do with QJSValue's API. - const QVariantMap column = firstRow.at(columnIndex).toMap(); - const QStringList columnPropertyNames = column.keys(); - ColumnProperties properties; - int propertyInfoIndex = 0; - - qCDebug(lcTableModel).nospace() << "- column " << columnIndex << ":"; - - for (const QString &roleName : columnPropertyNames) { - // QML/JS supports utf8. - const QByteArray roleNameUtf8 = roleName.toUtf8(); - if (!mRoleNames.values().contains(roleNameUtf8)) { - // We don't already have this role name, so it's a user role. - mRoleNames[userRoleKey] = roleName.toUtf8().constData(); - qCDebug(lcTableModel) << " - added new user role" << roleName << "with key" << userRoleKey; - ++userRoleKey; - } else { - qCDebug(lcTableModel) << " - found existing role" << roleName; - } - - if (properties.explicitDisplayRoleIndex == -1 && roleName == displayRoleName) { - // The user explicitly declared a "display" role, - // so now we don't need to make it the first role in the column for them. - properties.explicitDisplayRoleIndex = propertyInfoIndex; - } - - // Keep track of the type of property so we can use it to validate new rows later on. - const QVariant roleValue = column.value(roleName); - const auto propertyInfo = ColumnPropertyInfo(roleName, roleValue.type(), - QString::fromLatin1(roleValue.typeName())); - properties.infoForProperties.append(propertyInfo); - - qCDebug(lcTableModel) << " - column property" << propertyInfo.name - << "has type" << propertyInfo.typeName; - - ++propertyInfoIndex; - } - - mColumnProperties.append(properties); - } - } + // Gather metadata the first time rows is set. + if (firstTimeValidRowsHaveBeenSet && !mRows.isEmpty()) + fetchColumnMetadata(); endResetModel(); @@ -255,6 +257,94 @@ void QQmlTableModel::setRows(const QVariant &rows) emit columnCountChanged(); } +QQmlTableModel::ColumnRoleMetadata QQmlTableModel::fetchColumnRoleData(const QString &roleNameKey, + QQmlTableModelColumn *tableModelColumn, int columnIndex) const +{ + const QVariant firstRow = mRows.first(); + ColumnRoleMetadata roleData; + + QJSValue columnRoleGetter = tableModelColumn->getterAtRole(roleNameKey); + if (columnRoleGetter.isUndefined()) { + // This role is not defined, which is fine; just skip it. + return roleData; + } + + if (columnRoleGetter.isString()) { + // The role is set as a string, so we assume the row is a simple object. + if (firstRow.type() != QVariant::Map) { + qmlWarning(this).quote() << "expected row for role " + << roleNameKey << " of TableModelColumn at index " + << columnIndex << " to be a simple object, but it's " + << firstRow.typeName() << " instead: " << firstRow; + return roleData; + } + const QVariantMap firstRowAsMap = firstRow.toMap(); + const QString rolePropertyName = columnRoleGetter.toString(); + const QVariant roleProperty = firstRowAsMap.value(rolePropertyName); + + roleData.isStringRole = true; + roleData.name = rolePropertyName; + roleData.type = roleProperty.type(); + roleData.typeName = QString::fromLatin1(roleProperty.typeName()); + } else if (columnRoleGetter.isCallable()) { + // The role is provided via a function, which means the row is complex and + // the user needs to provide the data for it. + const auto modelIndex = index(0, columnIndex); + const auto args = QJSValueList() << qmlEngine(this)->toScriptValue(modelIndex); + const QVariant cellData = columnRoleGetter.call(args).toVariant(); + + // We don't know the property name since it's provided through the function. + // roleData.name = ??? + roleData.isStringRole = false; + roleData.type = cellData.type(); + roleData.typeName = QString::fromLatin1(cellData.typeName()); + } else { + // Invalid role. + qmlWarning(this) << "TableModelColumn role for column at index " + << columnIndex << " must be either a string or a function; actual type is: " + << columnRoleGetter.toString(); + } + + return roleData; +} + +void QQmlTableModel::fetchColumnMetadata() +{ + qCDebug(lcTableModel) << "gathering metadata for" << mColumnCount << "columns from first row:"; + + static const auto supportedRoleNames = QQmlTableModelColumn::supportedRoleNames(); + + // Since we support different data structures at the row level, we require that there + // is a TableModelColumn for each column. + // Collect and cache metadata for each column. This makes data lookup faster. + for (int columnIndex = 0; columnIndex < mColumns.size(); ++columnIndex) { + QQmlTableModelColumn *column = mColumns.at(columnIndex); + qCDebug(lcTableModel).nospace() << "- column " << columnIndex << ":"; + + ColumnMetadata metaData; + const auto builtInRoleKeys = supportedRoleNames.keys(); + for (const int builtInRoleKey : builtInRoleKeys) { + const QString builtInRoleName = supportedRoleNames.value(builtInRoleKey); + ColumnRoleMetadata roleData = fetchColumnRoleData(builtInRoleName, column, columnIndex); + if (roleData.type == QVariant::Invalid) { + // This built-in role was not specified in this column. + continue; + } + + qCDebug(lcTableModel).nospace() << " - added metadata for built-in role " + << builtInRoleName << " at column index " << columnIndex + << ": name=" << roleData.name << " typeName=" << roleData.typeName + << " type=" << roleData.type; + + // This column now supports this specific built-in role. + metaData.roles.insert(builtInRoleName, roleData); + // Add it if it doesn't already exist. + mRoleNames[builtInRoleKey] = builtInRoleName.toLatin1(); + } + mColumnMetadata.insert(columnIndex, metaData); + } +} + /*! \qmlmethod TableModel::appendRow(object row) @@ -262,13 +352,13 @@ void QQmlTableModel::setRows(const QVariant &rows) values (cells) in \a row. \code - model.appendRow([ - { checkable: true, checked: false }, - { amount: 1 }, - { fruitType: "Pear" }, - { fruitName: "Williams" }, - { fruitPrice: 1.50 }, - ]) + model.appendRow({ + checkable: true, + amount: 1, + fruitType: "Pear", + fruitName: "Williams", + fruitPrice: 1.50, + }) \endcode \sa insertRow(), setRow(), removeRow() @@ -306,7 +396,7 @@ void QQmlTableModel::clear() \code Component.onCompleted: { // These two lines are equivalent. - console.log(model.getRow(0).fruitName); + console.log(model.getRow(0).display); console.log(model.rows[0].fruitName); } \endcode @@ -331,13 +421,13 @@ QVariant QQmlTableModel::getRow(int rowIndex) values (cells) in \a row. \code - model.insertRow(2, [ - { checkable: true, checked: false }, - { amount: 1 }, - { fruitType: "Pear" }, - { fruitName: "Williams" }, - { fruitPrice: 1.50 }, - ]) + model.insertRow(2, { + checkable: true, checked: false, + amount: 1, + fruitType: "Pear", + fruitName: "Williams", + fruitPrice: 1.50, + }) \endcode The \a rowIndex must be to an existing item in the list, or one past @@ -363,13 +453,32 @@ void QQmlTableModel::doInsert(int rowIndex, const QVariant &row) mRows.insert(rowIndex, rowAsVariant); ++mRowCount; - qCDebug(lcTableModel).nospace() << "inserted the following row to the model at index" - << rowIndex << ":\n" << rowAsVariant.toList(); + qCDebug(lcTableModel).nospace() << "inserted the following row to the model at index " + << rowIndex << ":\n" << rowAsVariant.toMap(); + + // Gather metadata the first time a row is added. + if (mColumnMetadata.isEmpty()) + fetchColumnMetadata(); endInsertRows(); emit rowCountChanged(); } +void QQmlTableModel::classBegin() +{ +} + +void QQmlTableModel::componentComplete() +{ + componentCompleted = true; + + mColumnCount = mColumns.size(); + if (mColumnCount > 0) + emit columnCountChanged(); + + doSetRows(mRows); +} + /*! \qmlmethod TableModel::moveRow(int fromRowIndex, int toRowIndex, int rows) @@ -496,13 +605,13 @@ void QQmlTableModel::removeRow(int rowIndex, int rows) All columns/cells must be present in \c row, and in the correct order. \code - model.setRow(0, [ - { checkable: true, checked: false }, - { amount: 1 }, - { fruitType: "Pear" }, - { fruitName: "Williams" }, - { fruitPrice: 1.50 }, - ]) + model.setRow(0, { + checkable: true, + amount: 1, + fruitType: "Pear", + fruitName: "Williams", + fruitPrice: 1.50, + }) \endcode If \a rowIndex is equal to \c rowCount(), then a new row is appended to the @@ -529,36 +638,40 @@ void QQmlTableModel::setRow(int rowIndex, const QVariant &row) } } -/*! - \qmlproperty var TableModel::roleDataProvider - - This property can hold a function that will map roles to values. - - When assigned, it will be called each time data() is called, to enable - extracting arbitrary values, converting the data in arbitrary ways, or even - doing calculations. It takes 3 arguments: \c index (\l QModelIndex), - \c role (string), and \c cellData (object), which is the complete data that - is stored in the given cell. (If the cell contains a JS object with - multiple named values, the entire object will be given in \c cellData.) - The function that you define must return the value to be used; for example - a typical delegate will display the value returned for the \c display role, - so you can check whether that is the role and return data in a form that is - suitable for the delegate to show: - - \snippet qml/tablemodel/roleDataProvider.qml 0 -*/ -QJSValue QQmlTableModel::roleDataProvider() const +QQmlListProperty<QQmlTableModelColumn> QQmlTableModel::columns() { - return mRoleDataProvider; + return QQmlListProperty<QQmlTableModelColumn>(this, nullptr, + &QQmlTableModel::columns_append, + &QQmlTableModel::columns_count, + &QQmlTableModel::columns_at, + &QQmlTableModel::columns_clear); } -void QQmlTableModel::setRoleDataProvider(QJSValue roleDataProvider) +void QQmlTableModel::columns_append(QQmlListProperty<QQmlTableModelColumn> *property, + QQmlTableModelColumn *value) { - if (roleDataProvider.strictlyEquals(mRoleDataProvider)) - return; + QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object); + QQmlTableModelColumn *column = qobject_cast<QQmlTableModelColumn*>(value); + if (column) + model->mColumns.append(column); +} - mRoleDataProvider = roleDataProvider; - emit roleDataProviderChanged(); +int QQmlTableModel::columns_count(QQmlListProperty<QQmlTableModelColumn> *property) +{ + const QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object); + return model->mColumns.count(); +} + +QQmlTableModelColumn *QQmlTableModel::columns_at(QQmlListProperty<QQmlTableModelColumn> *property, int index) +{ + const QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object); + return model->mColumns.at(index); +} + +void QQmlTableModel::columns_clear(QQmlListProperty<QQmlTableModelColumn> *property) +{ + QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object); + return model->mColumns.clear(); } /*! @@ -574,14 +687,19 @@ void QQmlTableModel::setRoleDataProvider(QJSValue roleDataProvider) TableModel { id: model + + TableModelColumn { display: "fruitType" } + TableModelColumn { display: "fruitPrice" } + rows: [ - [{ fruitType: "Apple" }, { fruitPrice: 1.50 }], - [{ fruitType: "Orange" }, { fruitPrice: 2.50 }] + { fruitType: "Apple", fruitPrice: 1.50 }, + { fruitType: "Orange", fruitPrice: 2.50 } ] + Component.onCompleted: { for (var r = 0; r < model.rowCount; ++r) { - console.log("An " + model.data(model.index(r, 0)).fruitType + - " costs " + model.data(model.index(r, 1)).fruitPrice.toFixed(2)) + console.log("An " + model.data(model.index(r, 0)).display + + " costs " + model.data(model.index(r, 1)).display.toFixed(2)) } } } @@ -658,27 +776,31 @@ QVariant QQmlTableModel::data(const QModelIndex &index, int role) const if (column < 0 || column >= columnCount()) return QVariant(); - if (!mRoleNames.contains(role)) + const ColumnMetadata columnMetadata = mColumnMetadata.at(index.column()); + const QString roleName = QString::fromUtf8(mRoleNames.value(role)); + if (!columnMetadata.roles.contains(roleName)) { + qmlWarning(this) << "setData(): no role named " << roleName + << " at column index " << column << ". The available roles for that column are: " + << columnMetadata.roles.keys(); return QVariant(); + } - const QVariantList rowData = mRows.at(row).toList(); - - if (mRoleDataProvider.isCallable()) { - auto engine = qmlEngine(this); - const auto args = QJSValueList() << - engine->toScriptValue(index) << - QString::fromUtf8(mRoleNames.value(role)) << - engine->toScriptValue(rowData.at(column)); - return const_cast<QQmlTableModel*>(this)->mRoleDataProvider.call(args).toVariant(); + const ColumnRoleMetadata roleData = columnMetadata.roles.value(roleName); + if (roleData.isStringRole) { + // We know the data structure, so we can get the data for the user. + const QVariantMap rowData = mRows.at(row).toMap(); + const QString propertyName = columnMetadata.roles.value(roleName).name; + const QVariant value = rowData.value(propertyName); + return value; } - // TODO: should we also allow this code to be executed if roleDataProvider doesn't - // handle the role/column, so that it only has to handle the case where there is - // more than one role in a column? - const QVariantMap columnData = rowData.at(column).toMap(); - const QString propertyName = columnPropertyNameFromRole(column, role); - const QVariant value = columnData.value(propertyName); - return value; + // We don't know the data structure, so the user has to modify their data themselves. + // First, find the getter for this column and role. + QJSValue getter = mColumns.at(column)->getterAtRole(roleName); + + // Then, call it and return what it returned. + const auto args = QJSValueList() << qmlEngine(this)->toScriptValue(index); + return getter.call(args).toVariant(); } /*! @@ -691,9 +813,9 @@ QVariant QQmlTableModel::data(const QModelIndex &index, int role) const */ bool QQmlTableModel::setData(const QModelIndex &index, const QString &role, const QVariant &value) { - const int iRole = mRoleNames.key(role.toUtf8(), -1); - if (iRole >= 0) - return setData(index, value, iRole); + const int intRole = mRoleNames.key(role.toUtf8(), -1); + if (intRole >= 0) + return setData(index, value, intRole); return false; } @@ -707,56 +829,92 @@ bool QQmlTableModel::setData(const QModelIndex &index, const QVariant &value, in if (column < 0 || column >= columnCount()) return false; - if (!mRoleNames.contains(role)) - return false; - - const QVariantList rowData = mRows.at(row).toList(); - const QString propertyName = columnPropertyNameFromRole(column, role); + const QString roleName = QString::fromUtf8(mRoleNames.value(role)); qCDebug(lcTableModel).nospace() << "setData() called with index " - << index << ", value " << value << " and role " << propertyName; + << index << ", value " << value << " and role " << roleName; // Verify that the role exists for this column. - const ColumnPropertyInfo propertyInfo = findColumnPropertyInfo(column, propertyName); - if (!propertyInfo.isValid()) { - QString message; - QDebug stream(&message); - stream.nospace() << "setData(): no role named " << propertyName - << " at column index " << column << ". The available roles for that column are:\n"; - - const QVector<ColumnPropertyInfo> availableProperties = mColumnProperties.at(column).infoForProperties; - for (auto propertyInfo : availableProperties) - stream << " - " << propertyInfo.name << " (" << qPrintable(propertyInfo.typeName) << ")"; - - qmlWarning(this) << message; + const ColumnMetadata columnMetadata = mColumnMetadata.at(index.column()); + if (!columnMetadata.roles.contains(roleName)) { + qmlWarning(this) << "setData(): no role named \"" << roleName + << "\" at column index " << column << ". The available roles for that column are: " + << columnMetadata.roles.keys(); return false; } // Verify that the type of the value is what we expect. // If the value set is not of the expected type, we can try to convert it automatically. + const ColumnRoleMetadata roleData = columnMetadata.roles.value(roleName); QVariant effectiveValue = value; - if (value.type() != propertyInfo.type) { - if (!value.canConvert(int(propertyInfo.type))) { + if (value.type() != roleData.type) { + if (!value.canConvert(int(roleData.type))) { qmlWarning(this).nospace() << "setData(): the value " << value - << " set at row " << row << " column " << column << " with role " << propertyName - << " cannot be converted to " << propertyInfo.typeName; + << " set at row " << row << " column " << column << " with role " << roleName + << " cannot be converted to " << roleData.typeName; return false; } - if (!effectiveValue.convert(int(propertyInfo.type))) { + if (!effectiveValue.convert(int(roleData.type))) { qmlWarning(this).nospace() << "setData(): failed converting value " << value - << " set at row " << row << " column " << column << " with role " << propertyName - << " to " << propertyInfo.typeName; + << " set at row " << row << " column " << column << " with role " << roleName + << " to " << roleData.typeName; return false; } } - QVariantMap modifiedColumn = rowData.at(column).toMap(); - modifiedColumn[propertyName] = value; + if (roleData.isStringRole) { + // We know the data structure, so we can set it for the user. + QVariantMap modifiedRow = mRows.at(row).toMap(); + modifiedRow[roleData.name] = value; + + mRows[row] = modifiedRow; + } else { + // We don't know the data structure, so the user has to modify their data themselves. + auto engine = qmlEngine(this); + auto args = QJSValueList() + // arg 0: modelIndex. + << engine->toScriptValue(index) + // arg 1: cellData. + << engine->toScriptValue(value); + // Do the actual setting. + QJSValue setter = mColumns.at(column)->setterAtRole(roleName); + setter.call(args); + + /* + The chain of events so far: - QVariantList modifiedRow = rowData; - modifiedRow[column] = modifiedColumn; - mRows[row] = modifiedRow; + - User did e.g.: model.edit = textInput.text + - setData() is called + - setData() calls the setter + (remember that we need to emit the dataChanged() signal, + which is why the user can't just set the data directly in the delegate) + + Now the user's setter function has modified *their* copy of the + data, but *our* copy of the data is old. Imagine the getters and setters looked like this: + + display: function(modelIndex) { return rows[modelIndex.row][1].amount } + setDisplay: function(modelIndex, cellData) { rows[modelIndex.row][1].amount = cellData } + + We don't know the structure of the user's data, so we can't just do + what we do above for the isStringRole case: + + modifiedRow[column][roleName] = value + + This means that, besides getting the implicit row count when rows is initially set, + our copy of the data is unused when it comes to complex columns. + + Another point to note is that we can't pass rowData in to the getter as a convenience, + because we would be passing in *our* copy of the row, which is not up-to-date. + Since the user already has access to the data, it's not a big deal for them to do: + + display: function(modelIndex) { return rows[modelIndex.row][1].amount } + + instead of: + + display: function(modelIndex, rowData) { return rowData[1].amount } + */ + } QVector<int> rolesChanged; rolesChanged.append(role); @@ -770,35 +928,36 @@ QHash<int, QByteArray> QQmlTableModel::roleNames() const return mRoleNames; } -QQmlTableModel::ColumnPropertyInfo::ColumnPropertyInfo() +QQmlTableModel::ColumnRoleMetadata::ColumnRoleMetadata() { } -QQmlTableModel::ColumnPropertyInfo::ColumnPropertyInfo( - const QString &name, QVariant::Type type, const QString &typeName) : +QQmlTableModel::ColumnRoleMetadata::ColumnRoleMetadata( + bool isStringRole, const QString &name, QVariant::Type type, const QString &typeName) : + isStringRole(isStringRole), name(name), type(type), typeName(typeName) { } -bool QQmlTableModel::ColumnPropertyInfo::isValid() const +bool QQmlTableModel::ColumnRoleMetadata::isValid() const { return !name.isEmpty(); } bool QQmlTableModel::validateRowType(const char *functionName, const QVariant &row) const { - if (row.userType() != qMetaTypeId<QJSValue>()) { - qmlWarning(this) << functionName << ": expected \"row\" argument to be an array," - << " but got " << row.typeName() << " instead"; + if (!row.canConvert<QJSValue>()) { + qmlWarning(this) << functionName << ": expected \"row\" argument to be a QJSValue," + << " but got " << row.typeName() << " instead:\n" << row; return false; } - const QVariant rowAsVariant = row.value<QJSValue>().toVariant(); - if (rowAsVariant.type() != QVariant::List) { - qmlWarning(this) << functionName << ": expected \"row\" argument to be an array," - << " but got " << row.typeName() << " instead"; + const QJSValue rowAsJSValue = row.value<QJSValue>(); + if (!rowAsJSValue.isObject() && !rowAsJSValue.isArray()) { + qmlWarning(this) << functionName << ": expected \"row\" argument " + << "to be an object or array, but got:\n" << rowAsJSValue.toString(); return false; } @@ -806,12 +965,21 @@ bool QQmlTableModel::validateRowType(const char *functionName, const QVariant &r } bool QQmlTableModel::validateNewRow(const char *functionName, const QVariant &row, - int rowIndex, NewRowOperationFlag appendFlag) const + int rowIndex, NewRowOperationFlag operation) const { - if (!validateRowType(functionName, row)) + if (mColumnMetadata.isEmpty()) { + // There is no column metadata, so we have nothing to validate the row against. + // Rows have to be added before we can gather metadata from them, so just this + // once we'll return true to allow the rows to be added. + return true; + } + + // Don't require each row to be a QJSValue when setting all rows, + // as they won't be; they'll be QVariantMap. + if (operation != SetRowsOperation && !validateRowType(functionName, row)) return false; - if (appendFlag == OtherOperation) { + if (operation == OtherOperation) { // Inserting/setting. if (rowIndex < 0) { qmlWarning(this) << functionName << ": \"rowIndex\" cannot be negative"; @@ -825,29 +993,48 @@ bool QQmlTableModel::validateNewRow(const char *functionName, const QVariant &ro } } - const QVariant rowAsVariant = row.value<QJSValue>().toVariant(); - const QVariantList rowAsList = rowAsVariant.toList(); + const QVariant rowAsVariant = operation == SetRowsOperation + ? row : row.value<QJSValue>().toVariant(); + if (rowAsVariant.type() != QVariant::Map) { + qmlWarning(this) << functionName << ": row manipulation functions " + << "do not support complex rows (row index: " << rowIndex << ")"; + return false; + } - const int columnCount = rowAsList.size(); - if (columnCount != mColumnCount) { + const QVariantMap rowAsMap = rowAsVariant.toMap(); + const int columnCount = rowAsMap.size(); + if (columnCount < mColumnCount) { qmlWarning(this) << functionName << ": expected " << mColumnCount - << " columns, but got " << columnCount; + << " columns, but only got " << columnCount; return false; } - // Verify that the row's columns and their roles match the name and type of existing data. - // This iterates across the columns in the row. For example: - // [ - // { checkable: true, checked: false }, // columnIndex == 0 - // { amount: 1 }, // columnIndex == 1 - // { fruitType: "Orange" }, // etc. - // { fruitName: "Navel" }, - // { fruitPrice: 2.50 } - // ], - for (int columnIndex = 0; columnIndex < mColumnCount; ++columnIndex) { - const QVariantMap column = rowAsList.at(columnIndex).toMap(); - if (!validateColumnPropertyTypes(functionName, column, columnIndex)) - return false; + // We can't validate complex structures, but we can make sure that + // each simple string-based role in each column is correct. + for (int columnIndex = 0; columnIndex < mColumns.size(); ++columnIndex) { + QQmlTableModelColumn *column = mColumns.at(columnIndex); + const QHash<QString, QJSValue> getters = column->getters(); + const auto roleNames = getters.keys(); + const ColumnMetadata columnMetadata = mColumnMetadata.at(columnIndex); + for (const QString &roleName : roleNames) { + const ColumnRoleMetadata roleData = columnMetadata.roles.value(roleName); + if (!roleData.isStringRole) + continue; + + if (!rowAsMap.contains(roleData.name)) { + qmlWarning(this).quote() << functionName << ": expected a property named " + << roleData.name << " in row at index " << rowIndex << ", but couldn't find one"; + return false; + } + + const QVariant rolePropertyValue = rowAsMap.value(roleData.name); + if (rolePropertyValue.type() != roleData.type) { + qmlWarning(this).quote() << functionName << ": expected the property named " + << roleData.name << " to be of type " << roleData.typeName + << ", but got " << QString::fromLatin1(rolePropertyValue.typeName()) << " instead"; + return false; + } + } } return true; @@ -869,82 +1056,4 @@ bool QQmlTableModel::validateRowIndex(const char *functionName, const char *argu return true; } -bool QQmlTableModel::validateColumnPropertyTypes(const char *functionName, - const QVariantMap &column, int columnIndex) const -{ - // Actual - const QVariantList columnProperties = column.values(); - const QStringList propertyNames = column.keys(); - // Expected - const QVector<ColumnPropertyInfo> properties = mColumnProperties.at(columnIndex).infoForProperties; - - // This iterates across the properties in the column. For example: - // 0 1 2 - // { foo: "A", bar: 1, baz: true }, - for (int propertyIndex = 0; propertyIndex < properties.size(); ++propertyIndex) { - const QString propertyName = propertyNames.at(propertyIndex); - const QVariant propertyValue = columnProperties.at(propertyIndex); - const ColumnPropertyInfo expectedPropertyFormat = properties.at(propertyIndex); - - if (!validateColumnPropertyType(functionName, propertyName, - propertyValue, expectedPropertyFormat, columnIndex)) { - return false; - } - } - - return true; -} - -bool QQmlTableModel::validateColumnPropertyType(const char *functionName, const QString &propertyName, - const QVariant &propertyValue, const ColumnPropertyInfo &expectedPropertyFormat, int columnIndex) const -{ - if (propertyName != expectedPropertyFormat.name) { - qmlWarning(this) << functionName - << ": expected property named " << expectedPropertyFormat.name - << " at column index " << columnIndex - << ", but got " << propertyName << " instead"; - return false; - } - - if (propertyValue.type() != expectedPropertyFormat.type) { - qmlWarning(this) << functionName - << ": expected property with type " << expectedPropertyFormat.typeName - << " at column index " << columnIndex - << ", but got " << propertyValue.typeName() << " instead"; - return false; - } - - return true; -} - -QQmlTableModel::ColumnPropertyInfo QQmlTableModel::findColumnPropertyInfo( - int columnIndex, const QString &columnPropertyName) const -{ - // TODO: check if a hash with its string-based lookup is faster, - // keeping in mind that we may be doing index-based lookups too. - const QVector<ColumnPropertyInfo> properties = mColumnProperties.at(columnIndex).infoForProperties; - for (int i = 0; i < properties.size(); ++i) { - const ColumnPropertyInfo &info = properties.at(i); - if (info.name == columnPropertyName) - return info; - } - - return ColumnPropertyInfo(); -} - -QString QQmlTableModel::columnPropertyNameFromRole(int columnIndex, int role) const -{ - QString propertyName; - if (role == Qt::DisplayRole && mColumnProperties.at(columnIndex).explicitDisplayRoleIndex == -1) { - // The user is getting or setting data for the display role, - // but didn't specify any role with the name "display" in this column. - // So, we give them the implicit display role, aka the first property we find. - propertyName = mColumnProperties.at(columnIndex).infoForProperties.first().name; - } else { - // QML/JS supports utf8. - propertyName = QString::fromUtf8(mRoleNames.value(role)); - } - return propertyName; -} - QT_END_NAMESPACE |