Revision control
Copy as Markdown
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qqmlabstractcolumnmodel_p.h"
#include <QtCore/qloggingcategory.h>
#include <QtQml/qqmlinfo.h>
#include <QtQml/qqmlengine.h>
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
Q_STATIC_LOGGING_CATEGORY(lcColumnModel, "qt.qml.columnmodel")
QQmlAbstractColumnModel::QQmlAbstractColumnModel(QObject *parent)
: QAbstractItemModel(parent)
{
}
QQmlListProperty<QQmlTableModelColumn> QQmlAbstractColumnModel::columns()
{
return {this, nullptr,
&QQmlAbstractColumnModel::columns_append,
&QQmlAbstractColumnModel::columns_count,
&QQmlAbstractColumnModel::columns_at,
&QQmlAbstractColumnModel::columns_clear,
&QQmlAbstractColumnModel::columns_replace,
&QQmlAbstractColumnModel::columns_removeLast};
}
void QQmlAbstractColumnModel::columns_append(QQmlListProperty<QQmlTableModelColumn> *property,
QQmlTableModelColumn *value)
{
auto *model = static_cast<QQmlAbstractColumnModel *>(property->object);
Q_ASSERT(value);
Q_ASSERT(model);
auto *column = qobject_cast<QQmlTableModelColumn *>(value);
if (column)
model->mColumns.append(column);
}
qsizetype QQmlAbstractColumnModel::columns_count(QQmlListProperty<QQmlTableModelColumn> *property)
{
auto *model = static_cast<QQmlAbstractColumnModel*>(property->object);
Q_ASSERT(model);
return model->mColumns.size();
}
QQmlTableModelColumn *QQmlAbstractColumnModel::columns_at(QQmlListProperty<QQmlTableModelColumn> *property, qsizetype index)
{
auto *model = static_cast<QQmlAbstractColumnModel*>(property->object);
Q_ASSERT(model);
return model->mColumns.at(index);
}
void QQmlAbstractColumnModel::columns_clear(QQmlListProperty<QQmlTableModelColumn> *property)
{
auto *model = static_cast<QQmlAbstractColumnModel *>(property->object);
Q_ASSERT(model);
model->mColumns.clear();
}
void QQmlAbstractColumnModel::columns_replace(QQmlListProperty<QQmlTableModelColumn> *property, qsizetype index, QQmlTableModelColumn *value)
{
auto *model = static_cast<QQmlAbstractColumnModel *>(property->object);
Q_ASSERT(model);
if (auto *column = qobject_cast<QQmlTableModelColumn *>(value))
model->mColumns.replace(index, column);
}
void QQmlAbstractColumnModel::columns_removeLast(QQmlListProperty<QQmlTableModelColumn> *property)
{
auto *model = static_cast<QQmlAbstractColumnModel *>(property->object);
Q_ASSERT(model);
model->mColumns.removeLast();
}
QVariant QQmlAbstractColumnModel::data(const QModelIndex &index, const QString &role) const
{
const int iRole = mRoleNames.key(role.toUtf8(), -1);
if (iRole >= 0)
return data(index, iRole);
return {};
}
QVariant QQmlAbstractColumnModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
qmlWarning(this) << "data(): invalid QModelIndex";
return {};
}
const int row = index.row();
if (row < 0 || row >= rowCount(parent(index))) {
qmlWarning(this) << "data(): invalid row specified in QModelIndex";
return {};
}
const int column = index.column();
if (column < 0 || column >= columnCount(parent(index))) {
qmlWarning(this) << "data(): invalid column specified in QModelIndex";
return {};
}
const ColumnMetadata columnMetadata = mColumnMetadata.at(column);
const QString roleName = QString::fromUtf8(mRoleNames.value(role));
if (!columnMetadata.roles.contains(roleName)) {
qmlWarning(this) << "data(): no role named " << roleName
<< " at column index " << column << ". The available roles for that column are: "
<< columnMetadata.roles.keys();
return {};
}
const ColumnRoleMetadata roleData = columnMetadata.roles.value(roleName);
if (roleData.columnRole == ColumnRole::StringRole) {
// We know the data structure, so we can get the data for the user.
return dataPrivate(index, roleName);
}
// 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();
}
bool QQmlAbstractColumnModel::setData(const QModelIndex &index, const QVariant &value, const QString &role)
{
const int intRole = mRoleNames.key(role.toUtf8(), -1);
if (intRole >= 0)
return setData(index, value, intRole);
return false;
}
bool QQmlAbstractColumnModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
Q_ASSERT(index.isValid());
const int row = index.row();
if (row < 0 || row >= rowCount(parent(index)))
return false;
const int column = index.column();
if (column < 0 || column >= columnCount(parent(index)))
return false;
const QString roleName = QString::fromUtf8(mRoleNames.value(role));
qCDebug(lcColumnModel).nospace() << "setData() called with index "
<< index << ", value " << value << " and role " << roleName;
// Verify that the role exists for this column.
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.userType() != roleData.type) {
if (!value.canConvert(QMetaType(roleData.type))) {
qmlWarning(this).nospace() << "setData(): the value " << value
<< " set at row " << row << " column " << column << " with role " << roleName
<< " cannot be converted to " << roleData.typeName;
return false;
}
if (!effectiveValue.convert(QMetaType(roleData.type))) {
qmlWarning(this).nospace() << "setData(): failed converting value " << value
<< " set at row " << row << " column " << column << " with role " << roleName
<< " to " << roleData.typeName;
return false;
}
}
if (roleData.columnRole == ColumnRole::StringRole) {
// We know the data structure, so we can set it for the user.
setDataPrivate(index, roleData.name, value);
} else {
qmlWarning(this).nospace() << "setData(): manipulation of complex row "
<< "structures is not supported";
return false;
}
QList<int> rolesChanged;
rolesChanged.append(role);
emit dataChanged(index, index, rolesChanged);
emit rowsChanged();
return true;
}
QHash<int, QByteArray> QQmlAbstractColumnModel::roleNames() const
{
return mRoleNames;
}
Qt::ItemFlags QQmlAbstractColumnModel::flags(const QModelIndex &index) const
{
Q_UNUSED(index)
return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable;
}
void QQmlAbstractColumnModel::classBegin()
{
}
void QQmlAbstractColumnModel::componentComplete()
{
mComponentCompleted = true;
mColumnCount = mColumns.size();
if (mColumnCount > 0)
emit columnCountChanged();
setInitialRows();
}
QQmlAbstractColumnModel::ColumnRoleMetadata::ColumnRoleMetadata()
= default;
QQmlAbstractColumnModel::ColumnRoleMetadata::ColumnRoleMetadata(
ColumnRole role, QString name, int type, QString typeName) :
columnRole(role),
name(std::move(name)),
type(type),
typeName(std::move(typeName))
{
}
bool QQmlAbstractColumnModel::ColumnRoleMetadata::isValid() const
{
return !name.isEmpty();
}
QQmlAbstractColumnModel::ColumnRoleMetadata QQmlAbstractColumnModel::fetchColumnRoleData(const QString &roleNameKey,
QQmlTableModelColumn *tableModelColumn, int columnIndex) const
{
const QVariant row = firstRow();
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 (row.userType() != QMetaType::QVariantMap) {
qmlWarning(this).quote() << "expected row for role "
<< roleNameKey << " of TableModelColumn at index "
<< columnIndex << " to be a simple object, but it's "
<< row.typeName() << " instead: " << row;
return roleData;
}
QString rolePropertyName = columnRoleGetter.toString();
const QVariant roleProperty = row.toMap().value(rolePropertyName);
roleData.columnRole = ColumnRole::StringRole;
roleData.type = roleProperty.userType();
roleData.typeName = QString::fromLatin1(roleProperty.typeName());
roleData.name = std::move(rolePropertyName);
} 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.columnRole = ColumnRole::FunctionRole;
roleData.type = cellData.userType();
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 QQmlAbstractColumnModel::fetchColumnMetadata()
{
qCDebug(lcColumnModel) << "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(lcColumnModel).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 == QMetaType::UnknownType) {
// This built-in role was not specified in this column.
continue;
}
qCDebug(lcColumnModel).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);
}
}
bool QQmlAbstractColumnModel::validateRowType(QLatin1StringView functionName, const QVariant &row) const
{
if (!row.canConvert<QJSValue>()) {
qmlWarning(this) << functionName << ": expected \"row\" argument to be a QJSValue,"
<< " but got " << row.typeName() << " instead:\n" << row;
return false;
}
const auto 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;
}
return true;
}
bool QQmlAbstractColumnModel::validateNewRow(QLatin1StringView functionName, const QVariant &row,
NewRowOperationFlag operation) const
{
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;
}
const bool isVariantMap = (row.userType() == QMetaType::QVariantMap);
const bool isVariantList = (row.userType() == QMetaType::QVariantList);
// Don't require each row to be a QJSValue when setting all rows,
// as they won't be; they'll be QVariantMap if given as object.
// Passing a QVariantList counts as attempted complex row manipulation.
if (operation != SetRowsOperation
&& !isVariantMap
&& !isVariantList
&& !validateRowType(functionName, row)) {
return false;
}
const QVariant rowAsVariant = operation == SetRowsOperation || isVariantMap
? row : row.value<QJSValue>().toVariant();
if (rowAsVariant.userType() != QMetaType::QVariantMap) {
qmlWarning(this) << functionName << ": row manipulation functions "
<< "do not support complex rows";
return false;
}
const QVariantMap rowAsMap = rowAsVariant.toMap();
const int columnCount = rowAsMap.size();
if (columnCount < mColumnCount) {
qmlWarning(this) << functionName << ": expected " << mColumnCount
<< " columns, but only got " << columnCount;
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.columnRole == ColumnRole::FunctionRole)
continue;
if (!rowAsMap.contains(roleData.name)) {
qmlWarning(this).noquote() << functionName << ": expected a property named \""
<< roleData.name << "\" in row";
return false;
}
const QVariant rolePropertyValue = rowAsMap.value(roleData.name);
if (rolePropertyValue.userType() != roleData.type) {
if (!rolePropertyValue.canConvert(QMetaType(roleData.type))) {
qmlWarning(this).noquote() << functionName << ": expected the property named \""
<< roleData.name << "\" to be of type \"" << roleData.typeName
<< "\", but got \"" << QString::fromLatin1(rolePropertyValue.typeName())
<< "\" instead";
return false;
}
QVariant effectiveValue = rolePropertyValue;
if (!effectiveValue.convert(QMetaType(roleData.type))) {
qmlWarning(this).noquote() << functionName << ": failed converting value \""
<< rolePropertyValue << "\" set at column " << columnIndex << " with role \""
<< QString::fromLatin1(rolePropertyValue.typeName()) << "\" to \""
<< roleData.typeName << "\"";
return false;
}
}
}
}
return true;
}
QLatin1StringView QQmlAbstractColumnModel::jsTypeName(const QJSValue &v)
{
if (v.isArray()) return "array"_L1;
if (v.isObject()) return "object"_L1;
if (v.isString()) return "string"_L1;
if (v.isNumber()) return "number"_L1;
if (v.isBool()) return "boolean"_L1;
if (v.isNull()) return "null"_L1;
if (v.isUndefined()) return "undefined"_L1;
return "unknown"_L1;
}
std::optional<QVariantList> QQmlAbstractColumnModel::validateRowsArgument(const QVariant &rows) const
{
if (rows.userType() != qMetaTypeId<QJSValue>()) {
qmlWarning(this)
<< "setRows(): \"rows\" must be an array; actual type is"
<< rows.typeName();
return std::nullopt;
}
const auto rowsAsJSValue = rows.value<QJSValue>();
if (!rowsAsJSValue.isArray()) {
qmlWarning(this)
<< "setRows(): the type of \"rows\" is"
<< jsTypeName(rowsAsJSValue)
<< "but an array is expected";
return std::nullopt;
}
return rowsAsJSValue.toVariant().toList();
}
QT_END_NAMESPACE
#include "moc_qqmlabstractcolumnmodel_p.cpp"