Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qqmlabstractcolumnmodel.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
6
7#include <QtCore/qloggingcategory.h>
8
9#include <QtQml/qqmlinfo.h>
10#include <QtQml/qqmlengine.h>
11
13
14using namespace Qt::StringLiterals;
15
16Q_STATIC_LOGGING_CATEGORY(lcColumnModel, "qt.qml.columnmodel")
17
18QQmlAbstractColumnModel::QQmlAbstractColumnModel(QObject *parent)
19 : QAbstractItemModel(parent)
20{
21}
22
23QQmlListProperty<QQmlTableModelColumn> QQmlAbstractColumnModel::columns()
24{
25 return {this, nullptr,
26 &QQmlAbstractColumnModel::columns_append,
27 &QQmlAbstractColumnModel::columns_count,
28 &QQmlAbstractColumnModel::columns_at,
29 &QQmlAbstractColumnModel::columns_clear,
30 &QQmlAbstractColumnModel::columns_replace,
31 &QQmlAbstractColumnModel::columns_removeLast};
32}
33
34void QQmlAbstractColumnModel::columns_append(QQmlListProperty<QQmlTableModelColumn> *property,
35 QQmlTableModelColumn *value)
36{
37 auto *model = static_cast<QQmlAbstractColumnModel *>(property->object);
38 Q_ASSERT(value);
39 Q_ASSERT(model);
40 auto *column = qobject_cast<QQmlTableModelColumn *>(value);
41 if (column)
42 model->mColumns.append(column);
43}
44
45qsizetype QQmlAbstractColumnModel::columns_count(QQmlListProperty<QQmlTableModelColumn> *property)
46{
47 auto *model = static_cast<QQmlAbstractColumnModel*>(property->object);
48 Q_ASSERT(model);
49 return model->mColumns.size();
50}
51
52QQmlTableModelColumn *QQmlAbstractColumnModel::columns_at(QQmlListProperty<QQmlTableModelColumn> *property, qsizetype index)
53{
54 auto *model = static_cast<QQmlAbstractColumnModel*>(property->object);
55 Q_ASSERT(model);
56 return model->mColumns.at(index);
57}
58
59void QQmlAbstractColumnModel::columns_clear(QQmlListProperty<QQmlTableModelColumn> *property)
60{
61 auto *model = static_cast<QQmlAbstractColumnModel *>(property->object);
62 Q_ASSERT(model);
63 return model->mColumns.clear();
64}
65
66void QQmlAbstractColumnModel::columns_replace(QQmlListProperty<QQmlTableModelColumn> *property, qsizetype index, QQmlTableModelColumn *value)
67{
68 auto *model = static_cast<QQmlAbstractColumnModel *>(property->object);
69 Q_ASSERT(model);
70 if (auto *column = qobject_cast<QQmlTableModelColumn *>(value))
71 return model->mColumns.replace(index, column);
72}
73
74void QQmlAbstractColumnModel::columns_removeLast(QQmlListProperty<QQmlTableModelColumn> *property)
75{
76 auto *model = static_cast<QQmlAbstractColumnModel *>(property->object);
77 Q_ASSERT(model);
78 model->mColumns.removeLast();
79}
80
81QVariant QQmlAbstractColumnModel::data(const QModelIndex &index, const QString &role) const
82{
83 const int iRole = mRoleNames.key(role.toUtf8(), -1);
84 if (iRole >= 0)
85 return data(index, iRole);
86 return {};
87}
88
89QVariant QQmlAbstractColumnModel::data(const QModelIndex &index, int role) const
90{
91 if (!index.isValid()) {
92 qmlWarning(this) << "data(): invalid QModelIndex";
93 return {};
94 }
95
96 const int row = index.row();
97 if (row < 0 || row >= rowCount(parent(index))) {
98 qmlWarning(this) << "data(): invalid row specified in QModelIndex";
99 return {};
100 }
101
102 const int column = index.column();
103 if (column < 0 || column >= columnCount(parent(index))) {
104 qmlWarning(this) << "data(): invalid column specified in QModelIndex";
105 return {};
106 }
107
108 const ColumnMetadata columnMetadata = mColumnMetadata.at(column);
109 const QString roleName = QString::fromUtf8(mRoleNames.value(role));
110 if (!columnMetadata.roles.contains(roleName)) {
111 qmlWarning(this) << "data(): no role named " << roleName
112 << " at column index " << column << ". The available roles for that column are: "
113 << columnMetadata.roles.keys();
114 return {};
115 }
116
117 const ColumnRoleMetadata roleData = columnMetadata.roles.value(roleName);
118 if (roleData.columnRole == ColumnRole::StringRole) {
119 // We know the data structure, so we can get the data for the user.
120 return dataPrivate(index, roleName);
121 }
122
123 // We don't know the data structure, so the user has to modify their data themselves.
124 // First, find the getter for this column and role.
125 QJSValue getter = mColumns.at(column)->getterAtRole(roleName);
126
127 // Then, call it and return what it returned.
128 const auto args = QJSValueList() << qmlEngine(this)->toScriptValue(index);
129 return getter.call(args).toVariant();
130}
131
132bool QQmlAbstractColumnModel::setData(const QModelIndex &index, const QVariant &value, const QString &role)
133{
134 const int intRole = mRoleNames.key(role.toUtf8(), -1);
135 if (intRole >= 0)
136 return setData(index, value, intRole);
137 return false;
138}
139
140bool QQmlAbstractColumnModel::setData(const QModelIndex &index, const QVariant &value, int role)
141{
142 Q_ASSERT(index.isValid());
143
144 const int row = index.row();
145 if (row < 0 || row >= rowCount(parent(index)))
146 return false;
147
148 const int column = index.column();
149 if (column < 0 || column >= columnCount(parent(index)))
150 return false;
151
152 const QString roleName = QString::fromUtf8(mRoleNames.value(role));
153
154 qCDebug(lcColumnModel).nospace() << "setData() called with index "
155 << index << ", value " << value << " and role " << roleName;
156
157 // Verify that the role exists for this column.
158 const ColumnMetadata columnMetadata = mColumnMetadata.at(index.column());
159 if (!columnMetadata.roles.contains(roleName)) {
160 qmlWarning(this) << "setData(): no role named \"" << roleName
161 << "\" at column index " << column << ". The available roles for that column are: "
162 << columnMetadata.roles.keys();
163 return false;
164 }
165
166 // Verify that the type of the value is what we expect.
167 // If the value set is not of the expected type, we can try to convert it automatically.
168 const ColumnRoleMetadata roleData = columnMetadata.roles.value(roleName);
169 QVariant effectiveValue = value;
170 if (value.userType() != roleData.type) {
171 if (!value.canConvert(QMetaType(roleData.type))) {
172 qmlWarning(this).nospace() << "setData(): the value " << value
173 << " set at row " << row << " column " << column << " with role " << roleName
174 << " cannot be converted to " << roleData.typeName;
175 return false;
176 }
177
178 if (!effectiveValue.convert(QMetaType(roleData.type))) {
179 qmlWarning(this).nospace() << "setData(): failed converting value " << value
180 << " set at row " << row << " column " << column << " with role " << roleName
181 << " to " << roleData.typeName;
182 return false;
183 }
184 }
185
186 if (roleData.columnRole == ColumnRole::StringRole) {
187 // We know the data structure, so we can set it for the user.
188 setDataPrivate(index, roleData.name, value);
189 } else {
190 qmlWarning(this).nospace() << "setData(): manipulation of complex row "
191 << "structures is not supported";
192 return false;
193 }
194
195 QVector<int> rolesChanged;
196 rolesChanged.append(role);
197 emit dataChanged(index, index, rolesChanged);
198 emit rowsChanged();
199
200 return true;
201}
202
203QHash<int, QByteArray> QQmlAbstractColumnModel::roleNames() const
204{
205 return mRoleNames;
206}
207
208Qt::ItemFlags QQmlAbstractColumnModel::flags(const QModelIndex &index) const
209{
210 Q_UNUSED(index)
211 return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable;
212}
213
214void QQmlAbstractColumnModel::classBegin()
215{
216}
217
218void QQmlAbstractColumnModel::componentComplete()
219{
220 mComponentCompleted = true;
221
222 mColumnCount = mColumns.size();
223 if (mColumnCount > 0)
224 emit columnCountChanged();
225
226 setInitialRows();
227}
228
229
230QQmlAbstractColumnModel::ColumnRoleMetadata::ColumnRoleMetadata()
231 = default;
232
233QQmlAbstractColumnModel::ColumnRoleMetadata::ColumnRoleMetadata(
234 ColumnRole role, QString name, int type, QString typeName) :
235 columnRole(role),
236 name(std::move(name)),
237 type(type),
238 typeName(std::move(typeName))
239{
240}
241
242bool QQmlAbstractColumnModel::ColumnRoleMetadata::isValid() const
243{
244 return !name.isEmpty();
245}
246
247QQmlAbstractColumnModel::ColumnRoleMetadata QQmlAbstractColumnModel::fetchColumnRoleData(const QString &roleNameKey,
248 QQmlTableModelColumn *tableModelColumn, int columnIndex) const
249{
250 const QVariant row = firstRow();
251 ColumnRoleMetadata roleData;
252
253 QJSValue columnRoleGetter = tableModelColumn->getterAtRole(roleNameKey);
254 if (columnRoleGetter.isUndefined()) {
255 // This role is not defined, which is fine; just skip it.
256 return roleData;
257 }
258
259 if (columnRoleGetter.isString()) {
260 // The role is set as a string, so we assume the row is a simple object.
261 if (row.userType() != QMetaType::QVariantMap) {
262 qmlWarning(this).quote() << "expected row for role "
263 << roleNameKey << " of TableModelColumn at index "
264 << columnIndex << " to be a simple object, but it's "
265 << row.typeName() << " instead: " << row;
266 return roleData;
267 }
268
269 QString rolePropertyName = columnRoleGetter.toString();
270 const QVariant roleProperty = row.toMap().value(rolePropertyName);
271
272 roleData.columnRole = ColumnRole::StringRole;
273 roleData.type = roleProperty.userType();
274 roleData.typeName = QString::fromLatin1(roleProperty.typeName());
275 roleData.name = std::move(rolePropertyName);
276 } else if (columnRoleGetter.isCallable()) {
277 // The role is provided via a function, which means the row is complex and
278 // the user needs to provide the data for it.
279 const auto modelIndex = index(0, columnIndex);
280 const auto args = QJSValueList() << qmlEngine(this)->toScriptValue(modelIndex);
281 const QVariant cellData = columnRoleGetter.call(args).toVariant();
282
283 // We don't know the property name since it's provided through the function.
284 // roleData.name = ???
285 roleData.columnRole = ColumnRole::FunctionRole;
286 roleData.type = cellData.userType();
287 roleData.typeName = QString::fromLatin1(cellData.typeName());
288 } else {
289 // Invalid role.
290 qmlWarning(this) << "TableModelColumn role for column at index "
291 << columnIndex << " must be either a string or a function; actual type is: "
292 << columnRoleGetter.toString();
293 }
294
295 return roleData;
296}
297
298void QQmlAbstractColumnModel::fetchColumnMetadata()
299{
300 qCDebug(lcColumnModel) << "gathering metadata for" << mColumnCount << "columns from first row:";
301
302 static const auto supportedRoleNames = QQmlTableModelColumn::supportedRoleNames();
303
304 // Since we support different data structures at the row level, we require that there
305 // is a TableModelColumn for each column.
306 // Collect and cache metadata for each column. This makes data lookup faster.
307 for (int columnIndex = 0; columnIndex < mColumns.size(); ++columnIndex) {
308 QQmlTableModelColumn *column = mColumns.at(columnIndex);
309 qCDebug(lcColumnModel).nospace() << "- column " << columnIndex << ":";
310
311 ColumnMetadata metaData;
312 const auto builtInRoleKeys = supportedRoleNames.keys();
313 for (const int builtInRoleKey : builtInRoleKeys) {
314 const QString builtInRoleName = supportedRoleNames.value(builtInRoleKey);
315 ColumnRoleMetadata roleData = fetchColumnRoleData(builtInRoleName, column, columnIndex);
316 if (roleData.type == QMetaType::UnknownType) {
317 // This built-in role was not specified in this column.
318 continue;
319 }
320
321 qCDebug(lcColumnModel).nospace() << " - added metadata for built-in role "
322 << builtInRoleName << " at column index " << columnIndex
323 << ": name=" << roleData.name << " typeName=" << roleData.typeName
324 << " type=" << roleData.type;
325
326 // This column now supports this specific built-in role.
327 metaData.roles.insert(builtInRoleName, roleData);
328 // Add it if it doesn't already exist.
329 mRoleNames[builtInRoleKey] = builtInRoleName.toLatin1();
330 }
331 mColumnMetadata.insert(columnIndex, metaData);
332 }
333}
334
335bool QQmlAbstractColumnModel::validateRowType(QLatin1StringView functionName, const QVariant &row) const
336{
337 if (!row.canConvert<QJSValue>()) {
338 qmlWarning(this) << functionName << ": expected \"row\" argument to be a QJSValue,"
339 << " but got " << row.typeName() << " instead:\n" << row;
340 return false;
341 }
342
343 const auto rowAsJSValue = row.value<QJSValue>();
344 if (!rowAsJSValue.isObject() && !rowAsJSValue.isArray()) {
345 qmlWarning(this) << functionName << ": expected \"row\" argument "
346 << "to be an object or array, but got:\n" << rowAsJSValue.toString();
347 return false;
348 }
349
350 return true;
351}
352
353bool QQmlAbstractColumnModel::validateNewRow(QLatin1StringView functionName, const QVariant &row,
354 NewRowOperationFlag operation) const
355{
356 if (mColumnMetadata.isEmpty()) {
357 // There is no column metadata, so we have nothing to validate the row against.
358 // Rows have to be added before we can gather metadata from them, so just this
359 // once we'll return true to allow the rows to be added.
360 return true;
361 }
362
363 const bool isVariantMap = (row.userType() == QMetaType::QVariantMap);
364
365 // Don't require each row to be a QJSValue when setting all rows,
366 // as they won't be; they'll be QVariantMap.
367 if (operation != SetRowsOperation && (!isVariantMap && !validateRowType(functionName, row)))
368 return false;
369
370 const QVariant rowAsVariant = operation == SetRowsOperation || isVariantMap
371 ? row : row.value<QJSValue>().toVariant();
372 if (rowAsVariant.userType() != QMetaType::QVariantMap) {
373 qmlWarning(this) << functionName << ": row manipulation functions "
374 << "do not support complex rows";
375 return false;
376 }
377
378 const QVariantMap rowAsMap = rowAsVariant.toMap();
379 const int columnCount = rowAsMap.size();
380 if (columnCount < mColumnCount) {
381 qmlWarning(this) << functionName << ": expected " << mColumnCount
382 << " columns, but only got " << columnCount;
383 return false;
384 }
385
386 // We can't validate complex structures, but we can make sure that
387 // each simple string-based role in each column is correct.
388 for (int columnIndex = 0; columnIndex < mColumns.size(); ++columnIndex) {
389 QQmlTableModelColumn *column = mColumns.at(columnIndex);
390 const QHash<QString, QJSValue> getters = column->getters();
391 const auto roleNames = getters.keys();
392 const ColumnMetadata columnMetadata = mColumnMetadata.at(columnIndex);
393 for (const QString &roleName : roleNames) {
394 const ColumnRoleMetadata roleData = columnMetadata.roles.value(roleName);
395 if (roleData.columnRole == ColumnRole::FunctionRole)
396 continue;
397
398 if (!rowAsMap.contains(roleData.name)) {
399 qmlWarning(this).noquote() << functionName << ": expected a property named \""
400 << roleData.name << "\" in row";
401 return false;
402 }
403
404 const QVariant rolePropertyValue = rowAsMap.value(roleData.name);
405
406 if (rolePropertyValue.userType() != roleData.type) {
407 if (!rolePropertyValue.canConvert(QMetaType(roleData.type))) {
408 qmlWarning(this).noquote() << functionName << ": expected the property named \""
409 << roleData.name << "\" to be of type \"" << roleData.typeName
410 << "\", but got \"" << QString::fromLatin1(rolePropertyValue.typeName())
411 << "\" instead";
412 return false;
413 }
414
415 QVariant effectiveValue = rolePropertyValue;
416 if (!effectiveValue.convert(QMetaType(roleData.type))) {
417 qmlWarning(this).noquote() << functionName << ": failed converting value \""
418 << rolePropertyValue << "\" set at column " << columnIndex << " with role \""
419 << QString::fromLatin1(rolePropertyValue.typeName()) << "\" to \""
420 << roleData.typeName << "\"";
421 return false;
422 }
423 }
424 }
425 }
426
427 return true;
428}
429
430
431QT_END_NAMESPACE
432
433#include "moc_qqmlabstractcolumnmodel_p.cpp"
Q_STATIC_LOGGING_CATEGORY(lcAccessibilityCore, "qt.accessibility.core")