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

source code of qtdeclarative/src/labs/models/qqmlabstractcolumnmodel.cpp