From 7a303424f2095c53889f8102f115ec38013ef8d9 Mon Sep 17 00:00:00 2001 From: Mitch Curtis Date: Thu, 7 Mar 2019 10:21:53 +0100 Subject: Add TableModelColumn This allows us to support simple object rows by default, which we expect to be the most common use case for TableModel. Complex rows are supported, but with a limited subset of functionality. Things that could be improved: - Would be nice if we could get arbitrary/dynamic properties like ListModel has, without the complex code that comes with it. That way we could get rid of all of the role properties and users could have their own custom roles. The limitation of only having built-in roles becomes too restrictive very quickly. Change-Id: Icbdb6b39665851c55c69c0b79e0aa523c5d46dfe Reviewed-by: Venugopal Shivashankar Reviewed-by: Richard Moe Gustavsen --- .../qml/tablemodel/fruit-example-complex.qml | 134 ++++ .../tablemodel/fruit-example-delegatechooser.qml | 61 +- .../tablemodel/fruit-example-simpledelegate.qml | 54 +- .../snippets/qml/tablemodel/roleDataProvider.qml | 83 --- src/qml/types/qqmlmodelsmodule.cpp | 2 + src/qml/types/qqmltablemodel.cpp | 731 ++++++++++++--------- src/qml/types/qqmltablemodel_p.h | 56 +- src/qml/types/qqmltablemodelcolumn.cpp | 200 ++++++ src/qml/types/qqmltablemodelcolumn_p.h | 224 +++++++ src/qml/types/types.pri | 6 +- tests/auto/qml/qqmltablemodel/data/TestModel.qml | 21 +- .../auto/qml/qqmltablemodel/data/builtInRoles.qml | 47 -- tests/auto/qml/qqmltablemodel/data/common.qml | 95 +-- tests/auto/qml/qqmltablemodel/data/complex.qml | 68 ++ .../qml/qqmltablemodel/data/dataAndSetData.qml | 19 +- tests/auto/qml/qqmltablemodel/data/empty.qml | 36 +- .../qqmltablemodel/data/explicitDisplayRole.qml | 41 -- .../data/omitTableModelColumnIndex.qml | 47 ++ .../qml/qqmltablemodel/data/roleDataProvider.qml | 65 -- .../qqmltablemodel/data/setDataThroughDelegate.qml | 25 +- .../qqmltablemodel/data/setRowsMultipleTimes.qml | 48 +- .../auto/qml/qqmltablemodel/tst_qqmltablemodel.cpp | 713 ++++++++++---------- tests/manual/tableview/tablemodel/form/RowForm.qml | 36 +- tests/manual/tableview/tablemodel/form/main.qml | 60 +- 24 files changed, 1742 insertions(+), 1130 deletions(-) create mode 100644 src/qml/doc/snippets/qml/tablemodel/fruit-example-complex.qml delete mode 100644 src/qml/doc/snippets/qml/tablemodel/roleDataProvider.qml create mode 100644 src/qml/types/qqmltablemodelcolumn.cpp create mode 100644 src/qml/types/qqmltablemodelcolumn_p.h delete mode 100644 tests/auto/qml/qqmltablemodel/data/builtInRoles.qml create mode 100644 tests/auto/qml/qqmltablemodel/data/complex.qml delete mode 100644 tests/auto/qml/qqmltablemodel/data/explicitDisplayRole.qml create mode 100644 tests/auto/qml/qqmltablemodel/data/omitTableModelColumnIndex.qml delete mode 100644 tests/auto/qml/qqmltablemodel/data/roleDataProvider.qml diff --git a/src/qml/doc/snippets/qml/tablemodel/fruit-example-complex.qml b/src/qml/doc/snippets/qml/tablemodel/fruit-example-complex.qml new file mode 100644 index 0000000000..104a2209d7 --- /dev/null +++ b/src/qml/doc/snippets/qml/tablemodel/fruit-example-complex.qml @@ -0,0 +1,134 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +//![file] +import QtQuick 2.12 +import QtQuick.Window 2.12 +import Qt.labs.qmlmodels 1.0 + +Window { + width: 400 + height: 400 + visible: true + + TableView { + anchors.fill: parent + columnSpacing: 1 + rowSpacing: 1 + boundsBehavior: Flickable.StopAtBounds + + model: TableModel { + TableModelColumn { + display: function(modelIndex) { return rows[modelIndex.row][0].checked } + setDisplay: function(modelIndex, cellData) { rows[modelIndex.row][0].checked = cellData } + } + TableModelColumn { + display: function(modelIndex) { return rows[modelIndex.row][1].amount } + setDisplay: function(modelIndex, cellData) { rows[modelIndex.row][1].amount = cellData } + } + TableModelColumn { + display: function(modelIndex) { return rows[modelIndex.row][2].fruitType } + setDisplay: function(modelIndex, cellData) { rows[modelIndex.row][2].fruitType = cellData } + } + TableModelColumn { + display: function(modelIndex) { return rows[modelIndex.row][3].fruitName } + setDisplay: function(modelIndex, cellData) { rows[modelIndex.row][3].fruitName = cellData } + } + TableModelColumn { + display: function(modelIndex) { return rows[modelIndex.row][4].fruitPrice } + setDisplay: function(modelIndex, cellData) { rows[modelIndex.row][4].fruitPrice = cellData } + } + + // Each row is one type of fruit that can be ordered +//![rows] + rows: [ + [ + // Each object (line) is one cell/column. + { checked: false, checkable: true }, + { amount: 1 }, + { fruitType: "Apple" }, + { fruitName: "Granny Smith" }, + { fruitPrice: 1.50 } + ], + [ + { checked: true, checkable: true }, + { amount: 4 }, + { fruitType: "Orange" }, + { fruitName: "Navel" }, + { fruitPrice: 2.50 } + ], + [ + { checked: false, checkable: false }, + { amount: 1 }, + { fruitType: "Banana" }, + { fruitName: "Cavendish" }, + { fruitPrice: 3.50 } + ] + ] +//![rows] + } +//![delegate] + delegate: TextInput { + text: model.display + padding: 12 + selectByMouse: true + + onAccepted: model.display = text + + Rectangle { + anchors.fill: parent + color: "#efefef" + z: -1 + } + } +//![delegate] + } +} +//![file] diff --git a/src/qml/doc/snippets/qml/tablemodel/fruit-example-delegatechooser.qml b/src/qml/doc/snippets/qml/tablemodel/fruit-example-delegatechooser.qml index 3d44f61668..d3f6176c70 100644 --- a/src/qml/doc/snippets/qml/tablemodel/fruit-example-delegatechooser.qml +++ b/src/qml/doc/snippets/qml/tablemodel/fruit-example-delegatechooser.qml @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2018 The Qt Company Ltd. +** Copyright (C) 2019 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the documentation of the Qt Toolkit. @@ -65,32 +65,37 @@ ApplicationWindow { boundsBehavior: Flickable.StopAtBounds model: TableModel { + TableModelColumn { display: "checked" } + TableModelColumn { display: "amount" } + TableModelColumn { display: "fruitType" } + TableModelColumn { display: "fruitName" } + TableModelColumn { display: "fruitPrice" } + // Each row is one type of fruit that can be ordered //![rows] rows: [ - [ - // Each object (line) is one cell/column, - // and each property in that object is a role. - { checked: false, checkable: true }, - { amount: 1 }, - { fruitType: "Apple" }, - { fruitName: "Granny Smith" }, - { fruitPrice: 1.50 } - ], - [ - { checked: true, checkable: true }, - { amount: 4 }, - { fruitType: "Orange" }, - { fruitName: "Navel" }, - { fruitPrice: 2.50 } - ], - [ - { checked: false, checkable: true }, - { amount: 1 }, - { fruitType: "Banana" }, - { fruitName: "Cavendish" }, - { fruitPrice: 3.50 } - ] + { + // Each property is one cell/column. + checked: false, + amount: 1, + fruitType: "Apple", + fruitName: "Granny Smith", + fruitPrice: 1.50 + }, + { + checked: true, + amount: 4, + fruitType: "Orange", + fruitName: "Navel", + fruitPrice: 2.50 + }, + { + checked: false, + amount: 1, + fruitType: "Banana", + fruitName: "Cavendish", + fruitPrice: 3.50 + } ] //![rows] } @@ -99,15 +104,15 @@ ApplicationWindow { DelegateChoice { column: 0 delegate: CheckBox { - checked: model.checked - onToggled: model.checked = checked + checked: model.display + onToggled: model.display = checked } } DelegateChoice { column: 1 delegate: SpinBox { - value: model.amount - onValueModified: model.amount = value + value: model.display + onValueModified: model.display = value } } DelegateChoice { diff --git a/src/qml/doc/snippets/qml/tablemodel/fruit-example-simpledelegate.qml b/src/qml/doc/snippets/qml/tablemodel/fruit-example-simpledelegate.qml index 5f00eb484b..f51c1818c3 100644 --- a/src/qml/doc/snippets/qml/tablemodel/fruit-example-simpledelegate.qml +++ b/src/qml/doc/snippets/qml/tablemodel/fruit-example-simpledelegate.qml @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2018 The Qt Company Ltd. +** Copyright (C) 2019 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the documentation of the Qt Toolkit. @@ -65,32 +65,37 @@ Window { boundsBehavior: Flickable.StopAtBounds model: TableModel { + TableModelColumn { display: "checked" } + TableModelColumn { display: "amount" } + TableModelColumn { display: "fruitType" } + TableModelColumn { display: "fruitName" } + TableModelColumn { display: "fruitPrice" } + // Each row is one type of fruit that can be ordered //![rows] rows: [ - [ - // Each object (line) is one cell/column, - // and each property in that object is a role. - { checked: false, checkable: true }, - { amount: 1 }, - { fruitType: "Apple" }, - { fruitName: "Granny Smith" }, - { fruitPrice: 1.50 } - ], - [ - { checked: true, checkable: true }, - { amount: 4 }, - { fruitType: "Orange" }, - { fruitName: "Navel" }, - { fruitPrice: 2.50 } - ], - [ - { checked: false, checkable: true }, - { amount: 1 }, - { fruitType: "Banana" }, - { fruitName: "Cavendish" }, - { fruitPrice: 3.50 } - ] + { + // Each property is one cell/column. + checked: false, + amount: 1, + fruitType: "Apple", + fruitName: "Granny Smith", + fruitPrice: 1.50 + }, + { + checked: true, + amount: 4, + fruitType: "Orange", + fruitName: "Navel", + fruitPrice: 2.50 + }, + { + checked: false, + amount: 1, + fruitType: "Banana", + fruitName: "Cavendish", + fruitPrice: 3.50 + } ] //![rows] } @@ -100,7 +105,6 @@ Window { padding: 12 selectByMouse: true - // TODO: the property used here is undefined onAccepted: model.display = text Rectangle { diff --git a/src/qml/doc/snippets/qml/tablemodel/roleDataProvider.qml b/src/qml/doc/snippets/qml/tablemodel/roleDataProvider.qml deleted file mode 100644 index 63978a370d..0000000000 --- a/src/qml/doc/snippets/qml/tablemodel/roleDataProvider.qml +++ /dev/null @@ -1,83 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2019 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the documentation of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:BSD$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** BSD License Usage -** Alternatively, you may use this file under the terms of the BSD license -** as follows: -** -** "Redistribution and use in source and binary forms, with or without -** modification, are permitted provided that the following conditions are -** met: -** * Redistributions of source code must retain the above copyright -** notice, this list of conditions and the following disclaimer. -** * Redistributions in binary form must reproduce the above copyright -** notice, this list of conditions and the following disclaimer in -** the documentation and/or other materials provided with the -** distribution. -** * Neither the name of The Qt Company Ltd nor the names of its -** contributors may be used to endorse or promote products derived -** from this software without specific prior written permission. -** -** -** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." -** -** $QT_END_LICENSE$ -** -****************************************************************************/ -//![0] -import QtQuick 2.12 -import Qt.labs.qmlmodels 1.0 - -TableView { - columnSpacing: 1; rowSpacing: 1 - model: TableModel { - property real taxPercent: 10 - rows: [ - [{ fruitType: "Apple" }, { fruitPrice: 1.50 }], - [{ fruitType: "Orange" }, { fruitPrice: 2.50 }] - ] - roleDataProvider: function(index, role, cellData) { - if (role === "display") { - if (cellData.hasOwnProperty("fruitPrice")) { - console.log("row", index.row, "taxing your fruit", JSON.stringify(cellData)) - return (cellData.fruitPrice * (1 + taxPercent / 100)).toFixed(2); - } - else if (cellData.hasOwnProperty("fruitType")) - return cellData.fruitType; - } - return cellData; - } - } - delegate: Rectangle { - implicitWidth: 150; implicitHeight: 30 - color: "lightsteelblue" - Text { - x: 6; anchors.verticalCenter: parent.verticalCenter - text: display - } - } -} -//![0] diff --git a/src/qml/types/qqmlmodelsmodule.cpp b/src/qml/types/qqmlmodelsmodule.cpp index b7b9c9ee1c..119ede256f 100644 --- a/src/qml/types/qqmlmodelsmodule.cpp +++ b/src/qml/types/qqmlmodelsmodule.cpp @@ -48,6 +48,7 @@ #endif #include #include +#include QT_BEGIN_NAMESPACE @@ -77,6 +78,7 @@ void QQmlModelsModule::defineLabsModule() qmlRegisterType(uri, 1, 0, "DelegateChooser"); qmlRegisterType(uri, 1, 0, "DelegateChoice"); qmlRegisterType(uri, 1, 0, "TableModel"); + qmlRegisterType(uri, 1, 0, "TableModelColumn"); } QT_END_NAMESPACE 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 QQmlTableModel::columns() { - return mRoleDataProvider; + return QQmlListProperty(this, nullptr, + &QQmlTableModel::columns_append, + &QQmlTableModel::columns_count, + &QQmlTableModel::columns_at, + &QQmlTableModel::columns_clear); } -void QQmlTableModel::setRoleDataProvider(QJSValue roleDataProvider) +void QQmlTableModel::columns_append(QQmlListProperty *property, + QQmlTableModelColumn *value) { - if (roleDataProvider.strictlyEquals(mRoleDataProvider)) - return; + QQmlTableModel *model = static_cast(property->object); + QQmlTableModelColumn *column = qobject_cast(value); + if (column) + model->mColumns.append(column); +} - mRoleDataProvider = roleDataProvider; - emit roleDataProviderChanged(); +int QQmlTableModel::columns_count(QQmlListProperty *property) +{ + const QQmlTableModel *model = static_cast(property->object); + return model->mColumns.count(); +} + +QQmlTableModelColumn *QQmlTableModel::columns_at(QQmlListProperty *property, int index) +{ + const QQmlTableModel *model = static_cast(property->object); + return model->mColumns.at(index); +} + +void QQmlTableModel::columns_clear(QQmlListProperty *property) +{ + QQmlTableModel *model = static_cast(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(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 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 rolesChanged; rolesChanged.append(role); @@ -770,35 +928,36 @@ QHash 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()) { - qmlWarning(this) << functionName << ": expected \"row\" argument to be an array," - << " but got " << row.typeName() << " instead"; + if (!row.canConvert()) { + qmlWarning(this) << functionName << ": expected \"row\" argument to be a QJSValue," + << " but got " << row.typeName() << " instead:\n" << row; return false; } - const QVariant rowAsVariant = row.value().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(); + 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().toVariant(); - const QVariantList rowAsList = rowAsVariant.toList(); + const QVariant rowAsVariant = operation == SetRowsOperation + ? row : row.value().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 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 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 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 diff --git a/src/qml/types/qqmltablemodel_p.h b/src/qml/types/qqmltablemodel_p.h index 33b2189fcd..a1bb97e7d4 100644 --- a/src/qml/types/qqmltablemodel_p.h +++ b/src/qml/types/qqmltablemodel_p.h @@ -55,17 +55,21 @@ #include #include #include +#include #include +#include QT_BEGIN_NAMESPACE -class Q_QML_PRIVATE_EXPORT QQmlTableModel : public QAbstractTableModel +class Q_QML_PRIVATE_EXPORT QQmlTableModel : public QAbstractTableModel, public QQmlParserStatus { Q_OBJECT Q_PROPERTY(int columnCount READ columnCount NOTIFY columnCountChanged FINAL) Q_PROPERTY(int rowCount READ rowCount NOTIFY rowCountChanged FINAL) Q_PROPERTY(QVariant rows READ rows WRITE setRows NOTIFY rowsChanged FINAL) - Q_PROPERTY(QJSValue roleDataProvider READ roleDataProvider WRITE setRoleDataProvider NOTIFY roleDataProviderChanged) + Q_PROPERTY(QQmlListProperty columns READ columns CONSTANT FINAL) + Q_INTERFACES(QQmlParserStatus) + Q_CLASSINFO("DefaultProperty", "columns") public: QQmlTableModel(QObject *parent = nullptr); @@ -82,8 +86,12 @@ public: Q_INVOKABLE void removeRow(int rowIndex, int rows = 1); Q_INVOKABLE void setRow(int rowIndex, const QVariant &row); - QJSValue roleDataProvider() const; - void setRoleDataProvider(QJSValue roleDataProvider); + QQmlListProperty columns(); + + static void columns_append(QQmlListProperty *property, QQmlTableModelColumn *value); + static int columns_count(QQmlListProperty *property); + static QQmlTableModelColumn *columns_at(QQmlListProperty *property, int index); + static void columns_clear(QQmlListProperty *property); QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; @@ -98,57 +106,61 @@ Q_SIGNALS: void columnCountChanged(); void rowCountChanged(); void rowsChanged(); - void roleDataProviderChanged(); private: - class ColumnPropertyInfo + class ColumnRoleMetadata { public: - ColumnPropertyInfo(); - ColumnPropertyInfo(const QString &name, QVariant::Type type, const QString &typeName); + ColumnRoleMetadata(); + ColumnRoleMetadata(bool isStringRole, const QString &name, QVariant::Type type, const QString &typeName); bool isValid() const; + // If this is false, it's a function role. + bool isStringRole = false; QString name; QVariant::Type type = QVariant::Invalid; QString typeName; }; - struct ColumnProperties + struct ColumnMetadata { - QVector infoForProperties; - // If there was a display role found in this column, it'll be stored here. - // The index is into infoForProperties. - int explicitDisplayRoleIndex = -1; + // Key = role name that will be made visible to the delegate + // Value = metadata about that role, including actual name in the model data, type, etc. + QHash roles; }; enum NewRowOperationFlag { OtherOperation, // insert(), set(), etc. + SetRowsOperation, AppendOperation }; + void doSetRows(const QVariantList &rowsAsVariantList); + ColumnRoleMetadata fetchColumnRoleData(const QString &roleNameKey, + QQmlTableModelColumn *tableModelColumn, int columnIndex) const; + void fetchColumnMetadata(); + bool validateRowType(const char *functionName, const QVariant &row) const; bool validateNewRow(const char *functionName, const QVariant &row, - int rowIndex, NewRowOperationFlag appendFlag = OtherOperation) const; + int rowIndex, NewRowOperationFlag operation = OtherOperation) const; bool validateRowIndex(const char *functionName, const char *argumentName, int rowIndex) const; - bool validateColumnPropertyTypes(const char *functionName, const QVariantMap &column, int columnIndex) const; - bool validateColumnPropertyType(const char *functionName, const QString &propertyName, - const QVariant &propertyValue, const ColumnPropertyInfo &expectedPropertyFormat, int columnIndex) const; - - ColumnPropertyInfo findColumnPropertyInfo(int columnIndex, const QString &columnPropertyNameFromRole) const; - QString columnPropertyNameFromRole(int columnIndex, int role) const; void doInsert(int rowIndex, const QVariant &row); + void classBegin() override; + void componentComplete() override; + + bool componentCompleted = false; QVariantList mRows; + QList mColumns; int mRowCount = 0; int mColumnCount = 0; // Each entry contains information about the properties of the column at that index. - QVector mColumnProperties; + QVector mColumnMetadata; // key = property index (0 to number of properties across all columns) // value = role name QHash mRoleNames; - QJSValue mRoleDataProvider; }; QT_END_NAMESPACE diff --git a/src/qml/types/qqmltablemodelcolumn.cpp b/src/qml/types/qqmltablemodelcolumn.cpp new file mode 100644 index 0000000000..93da0642de --- /dev/null +++ b/src/qml/types/qqmltablemodelcolumn.cpp @@ -0,0 +1,200 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qqmltablemodelcolumn_p.h" + +#include + +QT_BEGIN_NAMESPACE + +/*! + \qmltype TableModelColumn + \instantiates QQmlTableModelColumn + \inqmlmodule Qt.labs.qmlmodels + \brief Represents a column in a model. + \since 5.14 + + \section1 Supported Roles + + TableModelColumn supports all of \l {Qt::ItemDataRole}{Qt's roles}, + with the exception of \c Qt::InitialSortOrderRole. + + \sa TableModel, TableView +*/ + +static const QString displayRoleName = QStringLiteral("display"); +static const QString decorationRoleName = QStringLiteral("decoration"); +static const QString editRoleName = QStringLiteral("edit"); +static const QString toolTipRoleName = QStringLiteral("toolTip"); +static const QString statusTipRoleName = QStringLiteral("statusTip"); +static const QString whatsThisRoleName = QStringLiteral("whatsThis"); + +static const QString fontRoleName = QStringLiteral("font"); +static const QString textAlignmentRoleName = QStringLiteral("textAlignment"); +static const QString backgroundRoleName = QStringLiteral("background"); +static const QString foregroundRoleName = QStringLiteral("foreground"); +static const QString checkStateRoleName = QStringLiteral("checkState"); + +static const QString accessibleTextRoleName = QStringLiteral("accessibleText"); +static const QString accessibleDescriptionRoleName = QStringLiteral("accessibleDescription"); + +static const QString sizeHintRoleName = QStringLiteral("sizeHint"); + + +QQmlTableModelColumn::QQmlTableModelColumn(QObject *parent) + : QObject(parent) +{ +} + +QQmlTableModelColumn::~QQmlTableModelColumn() +{ +} + +#define DEFINE_ROLE_PROPERTIES(getterGetterName, getterSetterName, getterSignal, setterGetterName, setterSetterName, setterSignal, roleName) \ +QJSValue QQmlTableModelColumn::getterGetterName() const \ +{ \ + return mGetters.value(roleName); \ +} \ +\ +void QQmlTableModelColumn::getterSetterName(const QJSValue &stringOrFunction) \ +{ \ + if (!stringOrFunction.isString() && !stringOrFunction.isCallable()) { \ + qmlWarning(this).quote() << "getter for " << roleName << " must be a function"; \ + return; \ + } \ + if (stringOrFunction.strictlyEquals(decoration())) \ + return; \ +\ + mGetters[roleName] = stringOrFunction; \ + emit decorationChanged(); \ +} \ +\ +QJSValue QQmlTableModelColumn::setterGetterName() const \ +{ \ + return mSetters.value(roleName); \ +} \ +\ +void QQmlTableModelColumn::setterSetterName(const QJSValue &function) \ +{ \ + if (!function.isCallable()) { \ + qmlWarning(this).quote() << "setter for " << roleName << " must be a function"; \ + return; \ + } \ +\ + if (function.strictlyEquals(getSetDisplay())) \ + return; \ +\ + mSetters[roleName] = function; \ + emit setDisplayChanged(); \ +} + +DEFINE_ROLE_PROPERTIES(display, setDisplay, displayChanged, + getSetDisplay, setSetDisplay, setDisplayChanged, displayRoleName) +DEFINE_ROLE_PROPERTIES(decoration, setDecoration, decorationChanged, + getSetDecoration, setSetDecoration, setDecorationChanged, decorationRoleName) +DEFINE_ROLE_PROPERTIES(edit, setEdit, editChanged, + getSetEdit, setSetEdit, setEditChanged, editRoleName) +DEFINE_ROLE_PROPERTIES(toolTip, setToolTip, toolTipChanged, + getSetToolTip, setSetToolTip, setToolTipChanged, toolTipRoleName) +DEFINE_ROLE_PROPERTIES(statusTip, setStatusTip, statusTipChanged, + getSetStatusTip, setSetStatusTip, setStatusTipChanged, statusTipRoleName) +DEFINE_ROLE_PROPERTIES(whatsThis, setWhatsThis, whatsThisChanged, + getSetWhatsThis, setSetWhatsThis, setWhatsThisChanged, whatsThisRoleName) + +DEFINE_ROLE_PROPERTIES(font, setFont, fontChanged, + getSetFont, setSetFont, setFontChanged, fontRoleName) +DEFINE_ROLE_PROPERTIES(textAlignment, setTextAlignment, textAlignmentChanged, + getSetTextAlignment, setSetTextAlignment, setTextAlignmentChanged, textAlignmentRoleName) +DEFINE_ROLE_PROPERTIES(background, setBackground, backgroundChanged, + getSetBackground, setSetBackground, setBackgroundChanged, backgroundRoleName) +DEFINE_ROLE_PROPERTIES(foreground, setForeground, foregroundChanged, + getSetForeground, setSetForeground, setForegroundChanged, foregroundRoleName) +DEFINE_ROLE_PROPERTIES(checkState, setCheckState, checkStateChanged, + getSetCheckState, setSetCheckState, setCheckStateChanged, checkStateRoleName) + +DEFINE_ROLE_PROPERTIES(accessibleText, setAccessibleText, accessibleTextChanged, + getSetAccessibleText, setSetAccessibleText, setAccessibleTextChanged, accessibleTextRoleName) +DEFINE_ROLE_PROPERTIES(accessibleDescription, setAccessibleDescription, accessibleDescriptionChanged, + getSetAccessibleDescription, setSetAccessibleDescription, setAccessibleDescriptionChanged, accessibleDescriptionRoleName) + +DEFINE_ROLE_PROPERTIES(sizeHint, setSizeHint, sizeHintChanged, + getSetSizeHint, setSetSizeHint, setSizeHintChanged, sizeHintRoleName) + +QJSValue QQmlTableModelColumn::getterAtRole(const QString &roleName) +{ + auto it = mGetters.find(roleName); + if (it == mGetters.end()) + return QJSValue(); + return *it; +} + +QJSValue QQmlTableModelColumn::setterAtRole(const QString &roleName) +{ + auto it = mSetters.find(roleName); + if (it == mSetters.end()) + return QJSValue(); + return *it; +} + +const QHash QQmlTableModelColumn::getters() const +{ + return mGetters; +} + +const QHash QQmlTableModelColumn::supportedRoleNames() +{ + QHash names; + names[Qt::DisplayRole] = QLatin1String("display"); + names[Qt::DecorationRole] = QLatin1String("decoration"); + names[Qt::EditRole] = QLatin1String("edit"); + names[Qt::ToolTipRole] = QLatin1String("toolTip"); + names[Qt::StatusTipRole] = QLatin1String("statusTip"); + names[Qt::WhatsThisRole] = QLatin1String("whatsThis"); + names[Qt::FontRole] = QLatin1String("font"); + names[Qt::TextAlignmentRole] = QLatin1String("textAlignment"); + names[Qt::BackgroundRole] = QLatin1String("background"); + names[Qt::ForegroundRole] = QLatin1String("foreground"); + names[Qt::CheckStateRole] = QLatin1String("checkState"); + names[Qt::AccessibleTextRole] = QLatin1String("accessibleText"); + names[Qt::AccessibleDescriptionRole] = QLatin1String("accessibleDescription"); + names[Qt::SizeHintRole] = QLatin1String("sizeHint"); + return names; +} + +QT_END_NAMESPACE diff --git a/src/qml/types/qqmltablemodelcolumn_p.h b/src/qml/types/qqmltablemodelcolumn_p.h new file mode 100644 index 0000000000..41c02482c0 --- /dev/null +++ b/src/qml/types/qqmltablemodelcolumn_p.h @@ -0,0 +1,224 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQMLTABLEMODELCOLUMN_P_H +#define QQMLTABLEMODELCOLUMN_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class Q_QML_AUTOTEST_EXPORT QQmlTableModelColumn : public QObject +{ + Q_OBJECT + Q_PROPERTY(QJSValue display READ display WRITE setDisplay NOTIFY displayChanged FINAL) + Q_PROPERTY(QJSValue setDisplay READ getSetDisplay WRITE setSetDisplay NOTIFY setDisplayChanged) + Q_PROPERTY(QJSValue decoration READ decoration WRITE setDecoration NOTIFY decorationChanged FINAL) + Q_PROPERTY(QJSValue setDecoration READ getSetDecoration WRITE setSetDecoration NOTIFY setDecorationChanged FINAL) + Q_PROPERTY(QJSValue edit READ edit WRITE setEdit NOTIFY editChanged FINAL) + Q_PROPERTY(QJSValue setEdit READ getSetEdit WRITE setSetEdit NOTIFY setEditChanged FINAL) + Q_PROPERTY(QJSValue toolTip READ toolTip WRITE setToolTip NOTIFY toolTipChanged FINAL) + Q_PROPERTY(QJSValue setToolTip READ getSetToolTip WRITE setSetToolTip NOTIFY setToolTipChanged FINAL) + Q_PROPERTY(QJSValue statusTip READ statusTip WRITE setStatusTip NOTIFY statusTipChanged FINAL) + Q_PROPERTY(QJSValue setStatusTip READ getSetStatusTip WRITE setSetStatusTip NOTIFY setStatusTipChanged FINAL) + Q_PROPERTY(QJSValue whatsThis READ whatsThis WRITE setWhatsThis NOTIFY whatsThisChanged FINAL) + Q_PROPERTY(QJSValue setWhatsThis READ getSetWhatsThis WRITE setSetWhatsThis NOTIFY setWhatsThisChanged FINAL) + + Q_PROPERTY(QJSValue font READ font WRITE setFont NOTIFY fontChanged FINAL) + Q_PROPERTY(QJSValue setFont READ getSetFont WRITE setSetFont NOTIFY setFontChanged FINAL) + Q_PROPERTY(QJSValue textAlignment READ textAlignment WRITE setTextAlignment NOTIFY textAlignmentChanged FINAL) + Q_PROPERTY(QJSValue setTextAlignment READ getSetTextAlignment WRITE setSetTextAlignment NOTIFY setTextAlignmentChanged FINAL) + Q_PROPERTY(QJSValue background READ background WRITE setBackground NOTIFY backgroundChanged FINAL) + Q_PROPERTY(QJSValue setBackground READ getSetBackground WRITE setSetBackground NOTIFY setBackgroundChanged FINAL) + Q_PROPERTY(QJSValue foreground READ foreground WRITE setForeground NOTIFY foregroundChanged FINAL) + Q_PROPERTY(QJSValue setForeground READ getSetForeground WRITE setSetForeground NOTIFY setForegroundChanged FINAL) + Q_PROPERTY(QJSValue checkState READ checkState WRITE setCheckState NOTIFY checkStateChanged FINAL) + Q_PROPERTY(QJSValue setCheckState READ getSetCheckState WRITE setSetCheckState NOTIFY setCheckStateChanged FINAL) + + Q_PROPERTY(QJSValue accessibleText READ accessibleText WRITE setAccessibleText NOTIFY accessibleTextChanged FINAL) + Q_PROPERTY(QJSValue setAccessibleText READ getSetAccessibleText WRITE setSetAccessibleText NOTIFY setAccessibleTextChanged FINAL) + Q_PROPERTY(QJSValue accessibleDescription READ accessibleDescription + WRITE setAccessibleDescription NOTIFY accessibleDescriptionChanged FINAL) + Q_PROPERTY(QJSValue setAccessibleDescription READ getSetAccessibleDescription + WRITE setSetAccessibleDescription NOTIFY setAccessibleDescriptionChanged FINAL) + + Q_PROPERTY(QJSValue sizeHint READ sizeHint WRITE setSizeHint NOTIFY sizeHintChanged FINAL) + Q_PROPERTY(QJSValue setSizeHint READ getSetSizeHint WRITE setSetSizeHint NOTIFY setSizeHintChanged FINAL) + +public: + QQmlTableModelColumn(QObject *parent = nullptr); + ~QQmlTableModelColumn() override; + + QJSValue display() const; + void setDisplay(const QJSValue &stringOrFunction); + QJSValue getSetDisplay() const; + void setSetDisplay(const QJSValue &function); + + QJSValue decoration() const; + void setDecoration(const QJSValue &stringOrFunction); + QJSValue getSetDecoration() const; + void setSetDecoration(const QJSValue &function); + + QJSValue edit() const; + void setEdit(const QJSValue &stringOrFunction); + QJSValue getSetEdit() const; + void setSetEdit(const QJSValue &function); + + QJSValue toolTip() const; + void setToolTip(const QJSValue &stringOrFunction); + QJSValue getSetToolTip() const; + void setSetToolTip(const QJSValue &function); + + QJSValue statusTip() const; + void setStatusTip(const QJSValue &stringOrFunction); + QJSValue getSetStatusTip() const; + void setSetStatusTip(const QJSValue &function); + + QJSValue whatsThis() const; + void setWhatsThis(const QJSValue &stringOrFunction); + QJSValue getSetWhatsThis() const; + void setSetWhatsThis(const QJSValue &function); + + QJSValue font() const; + void setFont(const QJSValue &stringOrFunction); + QJSValue getSetFont() const; + void setSetFont(const QJSValue &function); + + QJSValue textAlignment() const; + void setTextAlignment(const QJSValue &stringOrFunction); + QJSValue getSetTextAlignment() const; + void setSetTextAlignment(const QJSValue &function); + + QJSValue background() const; + void setBackground(const QJSValue &stringOrFunction); + QJSValue getSetBackground() const; + void setSetBackground(const QJSValue &function); + + QJSValue foreground() const; + void setForeground(const QJSValue &stringOrFunction); + QJSValue getSetForeground() const; + void setSetForeground(const QJSValue &function); + + QJSValue checkState() const; + void setCheckState(const QJSValue &stringOrFunction); + QJSValue getSetCheckState() const; + void setSetCheckState(const QJSValue &function); + + QJSValue accessibleText() const; + void setAccessibleText(const QJSValue &stringOrFunction); + QJSValue getSetAccessibleText() const; + void setSetAccessibleText(const QJSValue &function); + + QJSValue accessibleDescription() const; + void setAccessibleDescription(const QJSValue &stringOrFunction); + QJSValue getSetAccessibleDescription() const; + void setSetAccessibleDescription(const QJSValue &function); + + QJSValue sizeHint() const; + void setSizeHint(const QJSValue &stringOrFunction); + QJSValue getSetSizeHint() const; + void setSetSizeHint(const QJSValue &function); + + QJSValue getterAtRole(const QString &roleName); + QJSValue setterAtRole(const QString &roleName); + + const QHash getters() const; + + static const QHash supportedRoleNames(); + +Q_SIGNALS: + void indexChanged(); + void displayChanged(); + void setDisplayChanged(); + void decorationChanged(); + void setDecorationChanged(); + void editChanged(); + void setEditChanged(); + void toolTipChanged(); + void setToolTipChanged(); + void statusTipChanged(); + void setStatusTipChanged(); + void whatsThisChanged(); + void setWhatsThisChanged(); + + void fontChanged(); + void setFontChanged(); + void textAlignmentChanged(); + void setTextAlignmentChanged(); + void backgroundChanged(); + void setBackgroundChanged(); + void foregroundChanged(); + void setForegroundChanged(); + void checkStateChanged(); + void setCheckStateChanged(); + + void accessibleTextChanged(); + void setAccessibleTextChanged(); + void accessibleDescriptionChanged(); + void setAccessibleDescriptionChanged(); + void sizeHintChanged(); + void setSizeHintChanged(); + +private: + int mIndex = -1; + + // We store these in hashes because QQuickTableModel needs string-based lookup in certain situations. + QHash mGetters; + QHash mSetters; +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQmlTableModelColumn) + +#endif // QQMLTABLEMODELCOLUMN_P_H diff --git a/src/qml/types/types.pri b/src/qml/types/types.pri index 1765beb09e..5a56208dc4 100644 --- a/src/qml/types/types.pri +++ b/src/qml/types/types.pri @@ -7,7 +7,8 @@ SOURCES += \ $$PWD/qquickpackage.cpp \ $$PWD/qqmlinstantiator.cpp \ $$PWD/qqmltableinstancemodel.cpp \ - $$PWD/qqmltablemodel.cpp + $$PWD/qqmltablemodel.cpp \ + $$PWD/qqmltablemodelcolumn.cpp HEADERS += \ $$PWD/qqmlbind_p.h \ @@ -19,7 +20,8 @@ HEADERS += \ $$PWD/qqmlinstantiator_p.h \ $$PWD/qqmlinstantiator_p_p.h \ $$PWD/qqmltableinstancemodel_p.h \ - $$PWD/qqmltablemodel_p.h + $$PWD/qqmltablemodel_p.h \ + $$PWD/qqmltablemodelcolumn_p.h qtConfig(qml-worker-script) { SOURCES += \ diff --git a/tests/auto/qml/qqmltablemodel/data/TestModel.qml b/tests/auto/qml/qqmltablemodel/data/TestModel.qml index 7aeb5d03f4..00e1fa65a7 100644 --- a/tests/auto/qml/qqmltablemodel/data/TestModel.qml +++ b/tests/auto/qml/qqmltablemodel/data/TestModel.qml @@ -33,15 +33,18 @@ import "TestUtils.js" as TestUtils TableModel { id: testModel objectName: "testModel" - roleDataProvider: TestUtils.testModelRoleDataProvider + + TableModelColumn { display: "name" } + TableModelColumn { display: "age" } + rows: [ - [ - { name: "John" }, - { age: 22 } - ], - [ - { name: "Oliver" }, - { age: 33 } - ] + { + name: "John", + age: 22 + }, + { + name: "Oliver", + age: 33 + } ] } diff --git a/tests/auto/qml/qqmltablemodel/data/builtInRoles.qml b/tests/auto/qml/qqmltablemodel/data/builtInRoles.qml deleted file mode 100644 index d9882e4dea..0000000000 --- a/tests/auto/qml/qqmltablemodel/data/builtInRoles.qml +++ /dev/null @@ -1,47 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2019 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -import Qt.labs.qmlmodels 1.0 - -import "TestUtils.js" as TestUtils - -TableModel { - id: testModel - objectName: "testModel" - roleDataProvider: TestUtils.testModelRoleDataProvider - rows: [ - [ - { name: "John", someOtherRole1: "foo" }, // column 0 - { age: 22, someOtherRole2: "foo" } // column 1 - ], - [ - { name: "Oliver", someOtherRole1: "foo" }, // column 0 - { age: 33, someOtherRole2: "foo" } // column 1 - ] - ] -} diff --git a/tests/auto/qml/qqmltablemodel/data/common.qml b/tests/auto/qml/qqmltablemodel/data/common.qml index aec796bd4f..2f8b0c072b 100644 --- a/tests/auto/qml/qqmltablemodel/data/common.qml +++ b/tests/auto/qml/qqmltablemodel/data/common.qml @@ -26,7 +26,7 @@ ** ****************************************************************************/ -import QtQuick 2.12 +import QtQuick 2.13 import Qt.labs.qmlmodels 1.0 Item { @@ -38,80 +38,101 @@ Item { property alias tableView: tableView function appendRow(personName, personAge) { - testModel.appendRow([ - { name: personName }, - { age: personAge } - ]) + testModel.appendRow({ + name: personName, + age: personAge + }) + } + + function appendRowExtraData() { + testModel.appendRow({ + name: "Foo", + age: 99, + nonExistentRole: 123 + }) } function appendRowInvalid1() { - testModel.appendRow([ - { name: "Foo" }, - { age: 99 }, - { nonExistentRole: 123 } - ]) + testModel.appendRow(123) } function appendRowInvalid2() { - testModel.appendRow(123) + testModel.appendRow({ + name: "Foo", + age: [] + }) } function appendRowInvalid3() { testModel.appendRow([ - { name: "Foo" }, - { age: [] } + { name: "Bar" }, + { age: "111" } ]) } function insertRow(personName, personAge, rowIndex) { - testModel.insertRow(rowIndex, [ - { name: personName }, - { age: personAge }] - ) + testModel.insertRow(rowIndex, { + name: personName, + age: personAge + }) + } + + function insertRowExtraData() { + testModel.insertRow(0, { + name: "Foo", + age: 99, + nonExistentRole: 123 + }) } function insertRowInvalid1() { - testModel.insertRow(0, [ - { name: "Foo" }, - { age: 99 }, - { nonExistentRole: 123 } - ]) + testModel.insertRow(0, 123) } function insertRowInvalid2() { - testModel.insertRow(0, 123) + testModel.insertRow(0, { + name: "Foo", + age: [] + }) } function insertRowInvalid3() { testModel.insertRow(0, [ - { name: "Foo" }, - { age: [] } + { name: "Bar" }, + { age: "111" } ]) } function setRow(rowIndex, personName, personAge) { - testModel.setRow(rowIndex, [ - { name: personName }, - { age: personAge }] - ) + testModel.setRow(rowIndex, { + name: personName, + age: personAge + }) + } + + function setRowExtraData() { + testModel.setRow(0, { + name: "Foo", + age: 99, + nonExistentRole: 123 + }) } function setRowInvalid1() { - testModel.setRow(0, [ - { name: "Foo" }, - { age: 99 }, - { nonExistentRole: 123 } - ]) + testModel.setRow(0, 123) } function setRowInvalid2() { - testModel.setRow(0, 123) + testModel.setRow(0, { + name: "Foo", + age: [] + }) } function setRowInvalid3() { testModel.setRow(0, [ - { name: "Foo" }, - { age: [] } + { name: "Bar" }, + { age: "111" } ]) } diff --git a/tests/auto/qml/qqmltablemodel/data/complex.qml b/tests/auto/qml/qqmltablemodel/data/complex.qml new file mode 100644 index 0000000000..dbf53bac7e --- /dev/null +++ b/tests/auto/qml/qqmltablemodel/data/complex.qml @@ -0,0 +1,68 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.13 +import Qt.labs.qmlmodels 1.0 + +TableView { + width: 100 + height: 100 + delegate: Item { + implicitWidth: 50 + implicitHeight: 50 + + Text { + text: model.display + anchors.centerIn: parent + } + } + model: TableModel { + id: testModel + objectName: "testModel" + + TableModelColumn { + display: function(modelIndex) { return testModel.rows[modelIndex.row][0].name } + setDisplay: function(modelIndex, cellData) { testModel.rows[modelIndex.row][0].name = cellData } + } + TableModelColumn { + display: function(modelIndex) { return testModel.rows[modelIndex.row][1].age } + setDisplay: function(modelIndex, cellData) { testModel.rows[modelIndex.row][1].age = cellData } + } + + rows: [ + [ + { name: "John" }, + { age: 22 } + ], + [ + { name: "Oliver" }, + { age: 33 } + ] + ] + } +} diff --git a/tests/auto/qml/qqmltablemodel/data/dataAndSetData.qml b/tests/auto/qml/qqmltablemodel/data/dataAndSetData.qml index d61c50ba2c..d3f726bfa1 100644 --- a/tests/auto/qml/qqmltablemodel/data/dataAndSetData.qml +++ b/tests/auto/qml/qqmltablemodel/data/dataAndSetData.qml @@ -31,36 +31,25 @@ import Qt.labs.qmlmodels 1.0 TableView { width: 200; height: 200 - model: TableModel { + model: TestModel { id: testModel - objectName: "testModel" - rows: [ - [ - { name: "John", someOtherRole1: "foo" }, // column 0 - { age: 22, someOtherRole2: "foo" } // column 1 - ], - [ - { name: "Oliver", someOtherRole1: "foo" }, // column 0 - { age: 33, someOtherRole2: "foo" } // column 1 - ] - ] // This is silly: in real life, store the birthdate instead of the age, // and let the delegate calculate the age, so it won't need updating function happyBirthday(dude) { var row = -1; for (var r = 0; row < 0 && r < testModel.rowCount; ++r) - if (testModel.data(testModel.index(r, 0), "name") === dude) + if (testModel.data(testModel.index(r, 0), "display") === dude) row = r; var index = testModel.index(row, 1) - testModel.setData(index, "age", testModel.data(index, "age") + 1) + testModel.setData(index, "display", testModel.data(index, "display") + 1) } } delegate: Text { id: textItem text: model.display TapHandler { - onTapped: testModel.happyBirthday(testModel.data(testModel.index(row, 0), "name")) + onTapped: testModel.happyBirthday(testModel.data(testModel.index(row, 0), "display")) } } } diff --git a/tests/auto/qml/qqmltablemodel/data/empty.qml b/tests/auto/qml/qqmltablemodel/data/empty.qml index 6e66b99145..f5afbd1d27 100644 --- a/tests/auto/qml/qqmltablemodel/data/empty.qml +++ b/tests/auto/qml/qqmltablemodel/data/empty.qml @@ -29,8 +29,6 @@ import QtQuick 2.12 import Qt.labs.qmlmodels 1.0 -import "TestUtils.js" as TestUtils - Item { id: root width: 200 @@ -41,21 +39,37 @@ Item { function setRows() { testModel.rows = [ - [ - { name: "John" }, - { age: 22 } - ], - [ - { name: "Oliver" }, - { age: 33 } - ] + { + name: "John", + age: 22 + }, + { + name: "Oliver", + age: 33 + } ] } + function appendJohn() { + testModel.appendRow({ + name: "John", + age: 22 + }) + } + + function appendOliver() { + testModel.appendRow({ + name: "Oliver", + age: 33 + }) + } + TableModel { id: testModel objectName: "testModel" - roleDataProvider: TestUtils.testModelRoleDataProvider + + TableModelColumn { display: "name" } + TableModelColumn { display: "age" } } TableView { id: tableView diff --git a/tests/auto/qml/qqmltablemodel/data/explicitDisplayRole.qml b/tests/auto/qml/qqmltablemodel/data/explicitDisplayRole.qml deleted file mode 100644 index 510a62e74b..0000000000 --- a/tests/auto/qml/qqmltablemodel/data/explicitDisplayRole.qml +++ /dev/null @@ -1,41 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2019 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -import Qt.labs.qmlmodels 1.0 - -import "TestUtils.js" as TestUtils - -TableModel { - id: testModel - rows: [ - [ - { name: "John", display: "foo" }, - { age: 22, display: "bar" } - ] - ] -} diff --git a/tests/auto/qml/qqmltablemodel/data/omitTableModelColumnIndex.qml b/tests/auto/qml/qqmltablemodel/data/omitTableModelColumnIndex.qml new file mode 100644 index 0000000000..86bcb08fa2 --- /dev/null +++ b/tests/auto/qml/qqmltablemodel/data/omitTableModelColumnIndex.qml @@ -0,0 +1,47 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import Qt.labs.qmlmodels 1.0 + +TableModel { + objectName: "testModel" + + TableModelColumn { display: "name" } + TableModelColumn { display: "age" } + + rows: [ + { + name: "John", + age: 22 + }, + { + name: "Oliver", + age: 33 + } + ] +} diff --git a/tests/auto/qml/qqmltablemodel/data/roleDataProvider.qml b/tests/auto/qml/qqmltablemodel/data/roleDataProvider.qml deleted file mode 100644 index 2706ea54fd..0000000000 --- a/tests/auto/qml/qqmltablemodel/data/roleDataProvider.qml +++ /dev/null @@ -1,65 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2019 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -import QtQuick 2.12 -import Qt.labs.qmlmodels 1.0 - -Item { - id: root - width: 200 - height: 200 - - property alias testModel: testModel - - TableModel { - id: testModel - objectName: "testModel" - rows: [ - [ { name: "Rex" }, { age: 3 } ], - [ { name: "Buster" }, { age: 5 } ] - ] - roleDataProvider: function(index, role, cellData) { - if (role === "display") { - // Age will now be in dog years - if (cellData.hasOwnProperty("age")) - return (cellData.age * 7); - else if (index.column === 0) - return (cellData.name); - } - return cellData; - } - } - TableView { - anchors.fill: parent - model: testModel - delegate: Text { - id: textItem - text: model.display - } - } -} diff --git a/tests/auto/qml/qqmltablemodel/data/setDataThroughDelegate.qml b/tests/auto/qml/qqmltablemodel/data/setDataThroughDelegate.qml index 5f849c3350..ebfe4ed930 100644 --- a/tests/auto/qml/qqmltablemodel/data/setDataThroughDelegate.qml +++ b/tests/auto/qml/qqmltablemodel/data/setDataThroughDelegate.qml @@ -41,11 +41,11 @@ Item { signal shouldModifyInvalidType() function modify() { - shouldModify(); + shouldModify() } function modifyInvalidRole() { - shouldModifyInvalidRole(); + shouldModifyInvalidRole() } function modifyInvalidType() { @@ -54,37 +54,24 @@ Item { TableView { anchors.fill: parent - model: TableModel { + model: TestModel { id: testModel - objectName: "testModel" - rows: [ - [ - { name: "John" }, - { age: 22 } - ], - [ - { name: "Oliver" }, - { age: 33 } - ] - ] } delegate: Text { id: textItem - // TODO: this is currently random when no roleDataProvider handles it - // we should allow roleDataProvider to be used to handle specific roles only text: model.display Connections { target: root enabled: column === 1 - onShouldModify: model.age = 18 + onShouldModify: model.display = 18 } Connections { target: root enabled: column === 0 - // Invalid: should be "name". + // Invalid: should be "display". onShouldModifyInvalidRole: model.age = 100 } @@ -92,7 +79,7 @@ Item { target: root enabled: column === 1 // Invalid: should be string. - onShouldModifyInvalidType: model.age = "Whoops" + onShouldModifyInvalidType: model.display = "Whoops" } } } diff --git a/tests/auto/qml/qqmltablemodel/data/setRowsMultipleTimes.qml b/tests/auto/qml/qqmltablemodel/data/setRowsMultipleTimes.qml index 6aaf79f2d4..01ec40270c 100644 --- a/tests/auto/qml/qqmltablemodel/data/setRowsMultipleTimes.qml +++ b/tests/auto/qml/qqmltablemodel/data/setRowsMultipleTimes.qml @@ -39,35 +39,35 @@ Item { function setRowsValid() { testModel.rows = [ - [ - { name: "Max" }, - { age: 20 } - ], - [ - { name: "Imum" }, - { age: 41 } - ], - [ - { name: "Power" }, - { age: 89 } - ] + { + name: "Max", + age: 20 + }, + { + name: "Imum", + age: 41 + }, + { + name: "Power", + age: 89 + } ] } function setRowsInvalid() { testModel.rows = [ - [ - { nope: "Nope" }, - { age: 20 } - ], - [ - { nope: "Nah" }, - { age: 41 } - ], - [ - { nope: "No" }, - { age: 89 } - ] + { + nope: "Nope", + age: 20 + }, + { + nope: "Nah", + age: 41 + }, + { + nope: "No", + age: 89 + } ] } diff --git a/tests/auto/qml/qqmltablemodel/tst_qqmltablemodel.cpp b/tests/auto/qml/qqmltablemodel/tst_qqmltablemodel.cpp index 059ce082d9..113a27494d 100644 --- a/tests/auto/qml/qqmltablemodel/tst_qqmltablemodel.cpp +++ b/tests/auto/qml/qqmltablemodel/tst_qqmltablemodel.cpp @@ -47,6 +47,7 @@ public: private slots: void appendRemoveRow(); + void appendRowToEmptyModel(); void clear(); void getRow(); void insertRow(); @@ -55,15 +56,11 @@ private slots: void setDataThroughDelegate(); void setRowsImperatively(); void setRowsMultipleTimes(); - void builtInRoles_data(); - void builtInRoles(); - void explicitDisplayRole(); - void roleDataProvider(); void dataAndEditing(); + void omitTableModelColumnIndex(); + void complexRow(); }; -static const int builtInRoleCount = 6; - void tst_QQmlTableModel::appendRemoveRow() { QQuickView view(testFileUrl("common.qml")); @@ -81,22 +78,15 @@ void tst_QQmlTableModel::appendRemoveRow() QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged())); QVERIFY(rowCountSpy.isValid()); - int heightSignalEmissions = 0; + int rowCountSignalEmissions = 0; const QHash roleNames = model->roleNames(); - QCOMPARE(roleNames.size(), 2 + builtInRoleCount); - QVERIFY(roleNames.values().contains("name")); - QVERIFY(roleNames.values().contains("age")); + QCOMPARE(roleNames.size(), 1); QVERIFY(roleNames.values().contains("display")); - QVERIFY(roleNames.values().contains("decoration")); - QVERIFY(roleNames.values().contains("edit")); - QVERIFY(roleNames.values().contains("toolTip")); - QVERIFY(roleNames.values().contains("statusTip")); - QVERIFY(roleNames.values().contains("whatsThis")); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); // Call remove() with a negative rowIndex. QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*removeRow\\(\\): \"rowIndex\" cannot be negative")); @@ -104,7 +94,7 @@ void tst_QQmlTableModel::appendRemoveRow() QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); // Call remove() with an rowIndex that is too large. QTest::ignoreMessage(QtWarningMsg, QRegularExpression( @@ -113,7 +103,7 @@ void tst_QQmlTableModel::appendRemoveRow() QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); // Call remove() with a valid rowIndex but negative rows. QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*removeRow\\(\\): \"rows\" is less than or equal to zero")); @@ -121,7 +111,7 @@ void tst_QQmlTableModel::appendRemoveRow() QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); // Call remove() with a valid rowIndex but excessive rows. QTest::ignoreMessage(QtWarningMsg, QRegularExpression( @@ -130,71 +120,125 @@ void tst_QQmlTableModel::appendRemoveRow() QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); // Call remove() without specifying the number of rows to remove; it should remove one row. QVERIFY(QMetaObject::invokeMethod(model, "removeRow", Q_ARG(int, 0))); QCOMPARE(model->rowCount(), 1); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), ++heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), ++rowCountSignalEmissions); - // Call append() with a row that has a new (and hence unexpected) role. - QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*appendRow\\(\\): expected 2 columns, but got 3")); + // Call append() with a row that has an unexpected role; the row should be added and the extra data ignored. + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "appendRowExtraData")); + // Nothing should change. + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Foo")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 99); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), ++rowCountSignalEmissions); + + // Call append() with a row that is an int. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression( + ".*appendRow\\(\\): expected \"row\" argument to be a QJSValue, but got int instead:\nQVariant\\(int, 123\\)")); QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "appendRowInvalid1")); // Nothing should change. - QCOMPARE(model->rowCount(), 1); + QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Foo")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 99); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); - // Call append() with a row that is not an array. + // Call append() with a row with a role of the wrong type. QTest::ignoreMessage(QtWarningMsg, QRegularExpression( - ".*appendRow\\(\\): expected \"row\" argument to be an array, but got int instead")); + ".*appendRow\\(\\): expected the property named \"age\" to be of type \"int\", but got \"QVariantList\" instead")); QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "appendRowInvalid2")); // Nothing should change. - QCOMPARE(model->rowCount(), 1); + QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Foo")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 99); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); - // Call append() with a row with a role that is of the wrong type. + // Call append() with a row that is an array instead of a simple object. QTest::ignoreMessage(QtWarningMsg, QRegularExpression( - ".*appendRow\\(\\): expected property with type int at column index 1, but got QVariantList instead")); + ".*appendRow\\(\\): row manipulation functions do not support complex rows \\(row index: -1\\)")); QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "appendRowInvalid3")); // Nothing should change. - QCOMPARE(model->rowCount(), 1); + QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); // Call append() to insert one row. QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "appendRow", Q_ARG(QVariant, QLatin1String("Max")), Q_ARG(QVariant, 40))); - QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->rowCount(), 3); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Foo")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 99); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("display")).toInt(), 40); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), ++heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), ++rowCountSignalEmissions); // Call remove() and specify rowIndex and rows, removing all remaining rows. - QVERIFY(QMetaObject::invokeMethod(model, "removeRow", Q_ARG(int, 0), Q_ARG(int, 2))); + QVERIFY(QMetaObject::invokeMethod(model, "removeRow", Q_ARG(int, 0), Q_ARG(int, 3))); QCOMPARE(model->rowCount(), 0); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")), QVariant()); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")), QVariant()); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")), QVariant()); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")), QVariant()); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), ++heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), ++rowCountSignalEmissions); +} + +void tst_QQmlTableModel::appendRowToEmptyModel() +{ + QQuickView view(testFileUrl("empty.qml")); + QCOMPARE(view.status(), QQuickView::Ready); + view.show(); + QVERIFY(QTest::qWaitForWindowActive(&view)); + + QQmlTableModel *model = view.rootObject()->property("testModel").value(); + QVERIFY(model); + QCOMPARE(model->rowCount(), 0); + QCOMPARE(model->columnCount(), 2); + + QSignalSpy columnCountSpy(model, SIGNAL(columnCountChanged())); + QVERIFY(columnCountSpy.isValid()); + + QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged())); + QVERIFY(rowCountSpy.isValid()); + + QQuickTableView *tableView = view.rootObject()->property("tableView").value(); + QVERIFY(tableView); + QCOMPARE(tableView->rows(), 0); + QCOMPARE(tableView->columns(), 2); + + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "appendJohn")); + QCOMPARE(model->rowCount(), 1); + QCOMPARE(model->columnCount(), 2); + const QHash roleNames = model->roleNames(); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), 1); + QTRY_COMPARE(tableView->rows(), 1); + QCOMPARE(tableView->columns(), 2); } void tst_QQmlTableModel::clear() @@ -216,9 +260,8 @@ void tst_QQmlTableModel::clear() QVERIFY(rowCountSpy.isValid()); const QHash roleNames = model->roleNames(); - QVERIFY(roleNames.values().contains("name")); - QVERIFY(roleNames.values().contains("age")); - QCOMPARE(roleNames.size(), 2 + builtInRoleCount); + QVERIFY(roleNames.values().contains("display")); + QCOMPARE(roleNames.size(), 1); QQuickTableView *tableView = view.rootObject()->property("tableView").value(); QVERIFY(tableView); @@ -263,9 +306,9 @@ void tst_QQmlTableModel::getRow() // Call get() with a valid row index. QVERIFY(QMetaObject::invokeMethod(model, "getRow", Q_RETURN_ARG(QVariant, returnValue), Q_ARG(int, 0))); - const QVariantList rowAsVariantList = returnValue.toList(); - QCOMPARE(rowAsVariantList.at(0).toMap().value(QLatin1String("name")), QLatin1String("John")); - QCOMPARE(rowAsVariantList.at(1).toMap().value(QLatin1String("age")), 22); + const QVariantMap rowAsVariantMap = returnValue.toMap(); + QCOMPARE(rowAsVariantMap.value(QLatin1String("name")).toString(), QLatin1String("John")); + QCOMPARE(rowAsVariantMap.value(QLatin1String("age")).toInt(), 22); } void tst_QQmlTableModel::insertRow() @@ -285,7 +328,7 @@ void tst_QQmlTableModel::insertRow() QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged())); QVERIFY(rowCountSpy.isValid()); - int heightSignalEmissions = 0; + int rowCountSignalEmissions = 0; QQuickTableView *tableView = view.rootObject()->property("tableView").value(); QVERIFY(tableView); @@ -300,12 +343,12 @@ void tst_QQmlTableModel::insertRow() QCOMPARE(model->columnCount(), 2); QCOMPARE(model->rowCount(), 2); const QHash roleNames = model->roleNames(); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); QCOMPARE(tableView->rows(), 2); QCOMPARE(tableView->columns(), 2); @@ -316,92 +359,112 @@ void tst_QQmlTableModel::insertRow() Q_ARG(QVariant, QLatin1String("Max")), Q_ARG(QVariant, 40), Q_ARG(QVariant, 3))); QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); QCOMPARE(tableView->rows(), 2); QCOMPARE(tableView->columns(), 2); - // Try to insert a row that has a new (and hence unexpected) role. - QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*insertRow\\(\\): expected 2 columns, but got 3")); + // Call insert() with a row that is an int. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression( + ".*insertRow\\(\\): expected \"row\" argument to be a QJSValue, but got int instead:\nQVariant\\(int, 123\\)")); QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "insertRowInvalid1")); - QCOMPARE(model->columnCount(), 2); QCOMPARE(model->rowCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); QCOMPARE(tableView->rows(), 2); QCOMPARE(tableView->columns(), 2); - // Try to insert a row that is not an array. + // Try to insert a row with a role of the wrong type. QTest::ignoreMessage(QtWarningMsg, QRegularExpression( - ".*insertRow\\(\\): expected \"row\" argument to be an array, but got int instead")); + ".*insertRow\\(\\): expected the property named \"age\" to be of type \"int\", but got \"QVariantList\" instead")); QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "insertRowInvalid2")); QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); QCOMPARE(tableView->rows(), 2); QCOMPARE(tableView->columns(), 2); - // Try to insert a row with a role that is of the wrong type. + // Try to insert a row that is an array instead of a simple object. QTest::ignoreMessage(QtWarningMsg, QRegularExpression( - ".*insertRow\\(\\): expected property with type int at column index 1, but got QVariantList instead")); + ".*insertRow\\(\\): row manipulation functions do not support complex rows \\(row index: 0\\)")); QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "insertRowInvalid3")); QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); QCOMPARE(tableView->rows(), 2); QCOMPARE(tableView->columns(), 2); - // Insert a row at the bottom of the table. - QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "insertRow", - Q_ARG(QVariant, QLatin1String("Max")), Q_ARG(QVariant, 40), Q_ARG(QVariant, 2))); + // Try to insert a row has an unexpected role; the row should be added and the extra data ignored. + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "insertRowExtraData")); QCOMPARE(model->rowCount(), 3); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); - QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); - QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Foo")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 99); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), ++heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), ++rowCountSignalEmissions); QTRY_COMPARE(tableView->rows(), 3); QCOMPARE(tableView->columns(), 2); - // Insert a row in the middle of the table. + // Insert a row at the bottom of the table. QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "insertRow", - Q_ARG(QVariant, QLatin1String("Daisy")), Q_ARG(QVariant, 30), Q_ARG(QVariant, 1))); + Q_ARG(QVariant, QLatin1String("Max")), Q_ARG(QVariant, 40), Q_ARG(QVariant, 3))); QCOMPARE(model->rowCount(), 4); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Daisy")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 30); - QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); - QCOMPARE(model->data(model->index(3, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); - QCOMPARE(model->data(model->index(3, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Foo")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 99); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); + QCOMPARE(model->data(model->index(3, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(3, 1, QModelIndex()), roleNames.key("display")).toInt(), 40); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), ++heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), ++rowCountSignalEmissions); QTRY_COMPARE(tableView->rows(), 4); QCOMPARE(tableView->columns(), 2); + + // Insert a row in the middle of the table. + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "insertRow", + Q_ARG(QVariant, QLatin1String("Daisy")), Q_ARG(QVariant, 30), Q_ARG(QVariant, 2))); + QCOMPARE(model->rowCount(), 5); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Foo")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 99); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Daisy")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("display")).toInt(), 30); + QCOMPARE(model->data(model->index(3, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(3, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); + QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("display")).toInt(), 40); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), ++rowCountSignalEmissions); + QTRY_COMPARE(tableView->rows(), 5); + QCOMPARE(tableView->columns(), 2); } void tst_QQmlTableModel::moveRow() @@ -421,7 +484,7 @@ void tst_QQmlTableModel::moveRow() QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged())); QVERIFY(rowCountSpy.isValid()); - int heightSignalEmissions = 0; + int rowCountSignalEmissions = 0; const QHash roleNames = model->roleNames(); @@ -431,105 +494,105 @@ void tst_QQmlTableModel::moveRow() QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "appendRow", Q_ARG(QVariant, QLatin1String("Trev")), Q_ARG(QVariant, 48))); QCOMPARE(model->rowCount(), 5); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); - QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); - QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); - QCOMPARE(model->data(model->index(3, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Daisy")); - QCOMPARE(model->data(model->index(3, 1, QModelIndex()), roleNames.key("age")).toInt(), 30); - QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Trev")); - QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("age")).toInt(), 48); - heightSignalEmissions = 3; + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("display")).toInt(), 40); + QCOMPARE(model->data(model->index(3, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Daisy")); + QCOMPARE(model->data(model->index(3, 1, QModelIndex()), roleNames.key("display")).toInt(), 30); + QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Trev")); + QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("display")).toInt(), 48); + rowCountSignalEmissions = 3; QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); // Try to move with a fromRowIndex that is negative. QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*moveRow\\(\\): \"fromRowIndex\" cannot be negative")); QVERIFY(QMetaObject::invokeMethod(model, "moveRow", Q_ARG(int, -1), Q_ARG(int, 1))); // Shouldn't have changed. - QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Trev")); - QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("age")).toInt(), 48); + QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Trev")); + QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("display")).toInt(), 48); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); // Try to move with a fromRowIndex that is too large. QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*moveRow\\(\\): \"fromRowIndex\" 5 is greater than or equal to rowCount\\(\\)")); QVERIFY(QMetaObject::invokeMethod(model, "moveRow", Q_ARG(int, 5), Q_ARG(int, 1))); - QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Trev")); - QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("age")).toInt(), 48); + QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Trev")); + QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("display")).toInt(), 48); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); // Try to move with a toRowIndex that is negative. QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*moveRow\\(\\): \"toRowIndex\" cannot be negative")); QVERIFY(QMetaObject::invokeMethod(model, "moveRow", Q_ARG(int, 0), Q_ARG(int, -1))); // Shouldn't have changed. - QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Trev")); - QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("age")).toInt(), 48); + QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Trev")); + QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("display")).toInt(), 48); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); // Try to move with a toRowIndex that is too large. QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*moveRow\\(\\): \"toRowIndex\" 5 is greater than or equal to rowCount\\(\\)")); QVERIFY(QMetaObject::invokeMethod(model, "moveRow", Q_ARG(int, 0), Q_ARG(int, 5))); - QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Trev")); - QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("age")).toInt(), 48); + QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Trev")); + QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("display")).toInt(), 48); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); // Move the first row to the end. QVERIFY(QMetaObject::invokeMethod(model, "moveRow", Q_ARG(int, 0), Q_ARG(int, 4))); // The counts shouldn't have changed. QCOMPARE(model->rowCount(), 5); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); - QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Daisy")); - QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("age")).toInt(), 30); - QCOMPARE(model->data(model->index(3, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Trev")); - QCOMPARE(model->data(model->index(3, 1, QModelIndex()), roleNames.key("age")).toInt(), 48); - QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 40); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Daisy")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("display")).toInt(), 30); + QCOMPARE(model->data(model->index(3, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Trev")); + QCOMPARE(model->data(model->index(3, 1, QModelIndex()), roleNames.key("display")).toInt(), 48); + QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); // Move it back again. QVERIFY(QMetaObject::invokeMethod(model, "moveRow", Q_ARG(int, 4), Q_ARG(int, 0))); QCOMPARE(model->rowCount(), 5); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); - QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); - QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); - QCOMPARE(model->data(model->index(3, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Daisy")); - QCOMPARE(model->data(model->index(3, 1, QModelIndex()), roleNames.key("age")).toInt(), 30); - QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Trev")); - QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("age")).toInt(), 48); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("display")).toInt(), 40); + QCOMPARE(model->data(model->index(3, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Daisy")); + QCOMPARE(model->data(model->index(3, 1, QModelIndex()), roleNames.key("display")).toInt(), 30); + QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Trev")); + QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("display")).toInt(), 48); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); // Move the first row down one by one row. QVERIFY(QMetaObject::invokeMethod(model, "moveRow", Q_ARG(int, 0), Q_ARG(int, 1))); QCOMPARE(model->rowCount(), 5); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); - QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); - QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); - QCOMPARE(model->data(model->index(3, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Daisy")); - QCOMPARE(model->data(model->index(3, 1, QModelIndex()), roleNames.key("age")).toInt(), 30); - QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Trev")); - QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("age")).toInt(), 48); - QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("display")).toInt(), 40); + QCOMPARE(model->data(model->index(3, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Daisy")); + QCOMPARE(model->data(model->index(3, 1, QModelIndex()), roleNames.key("display")).toInt(), 30); + QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Trev")); + QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("display")).toInt(), 48); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); } void tst_QQmlTableModel::setRow() @@ -549,14 +612,14 @@ void tst_QQmlTableModel::setRow() QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged())); QVERIFY(rowCountSpy.isValid()); - int heightSignalEmissions = 0; + int rowCountSignalEmissions = 0; QQuickTableView *tableView = view.rootObject()->property("tableView").value(); QVERIFY(tableView); QCOMPARE(tableView->rows(), 2); QCOMPARE(tableView->columns(), 2); - // Try to insert with a negative index. + // Try to set with a negative index. QTest::ignoreMessage(QtWarningMsg, QRegularExpression( ".*setRow\\(\\): \"rowIndex\" cannot be negative")); QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRow", @@ -564,72 +627,86 @@ void tst_QQmlTableModel::setRow() QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); const QHash roleNames = model->roleNames(); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); QCOMPARE(tableView->rows(), 2); QCOMPARE(tableView->columns(), 2); - // Try to insert past the last allowed index. + // Try to set at an index past the last allowed index. QTest::ignoreMessage(QtWarningMsg, QRegularExpression( ".*setRow\\(\\): \"rowIndex\" 3 is greater than rowCount\\(\\) of 2")); QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRow", Q_ARG(QVariant, 3), Q_ARG(QVariant, QLatin1String("Max")), Q_ARG(QVariant, 40))); QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); QCOMPARE(tableView->rows(), 2); QCOMPARE(tableView->columns(), 2); - // Try to insert a row that has a new (and hence unexpected) role. - QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*setRow\\(\\): expected 2 columns, but got 3")); - QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRowInvalid1")); + // Try to set a row that has an unexpected role; the row should be set and the extra data ignored. + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRowExtraData")); QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Foo")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 99); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); QCOMPARE(tableView->rows(), 2); QCOMPARE(tableView->columns(), 2); // Try to insert a row that is not an array. QTest::ignoreMessage(QtWarningMsg, QRegularExpression( - ".*setRow\\(\\): expected \"row\" argument to be an array, but got int instead")); - QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRowInvalid2")); + ".*setRow\\(\\): expected \"row\" argument to be a QJSValue, but got int instead:\nQVariant\\(int, 123\\)")); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRowInvalid1")); QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Foo")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 99); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); QCOMPARE(tableView->rows(), 2); QCOMPARE(tableView->columns(), 2); // Try to insert a row with a role that is of the wrong type. QTest::ignoreMessage(QtWarningMsg, QRegularExpression( - ".*setRow\\(\\): expected property with type int at column index 1, but got QVariantList instead")); + ".*setRow\\(\\): expected the property named \"age\" to be of type \"int\", but got \"QVariantList\" instead")); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRowInvalid2")); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Foo")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 99); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); + QCOMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); + + // Try to insert a row that is an array instead of a simple object. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression( + ".*setRow\\(\\): row manipulation functions do not support complex rows \\(row index: 0\\)")); QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRowInvalid3")); QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Foo")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 99); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); QCOMPARE(tableView->rows(), 2); QCOMPARE(tableView->columns(), 2); @@ -638,12 +715,12 @@ void tst_QQmlTableModel::setRow() Q_ARG(QVariant, 0), Q_ARG(QVariant, QLatin1String("Max")), Q_ARG(QVariant, 40))); QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 40); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); QCOMPARE(tableView->rows(), 2); QCOMPARE(tableView->columns(), 2); @@ -652,12 +729,12 @@ void tst_QQmlTableModel::setRow() Q_ARG(QVariant, 1), Q_ARG(QVariant, QLatin1String("Daisy")), Q_ARG(QVariant, 30))); QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Daisy")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 30); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 40); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Daisy")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 30); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), rowCountSignalEmissions); QCOMPARE(tableView->rows(), 2); QCOMPARE(tableView->columns(), 2); @@ -666,14 +743,14 @@ void tst_QQmlTableModel::setRow() Q_ARG(QVariant, 2), Q_ARG(QVariant, QLatin1String("Wot")), Q_ARG(QVariant, 99))); QCOMPARE(model->rowCount(), 3); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Daisy")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 30); - QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Wot")); - QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("age")).toInt(), 99); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 40); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Daisy")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 30); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Wot")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("display")).toInt(), 99); QCOMPARE(columnCountSpy.count(), 0); - QCOMPARE(rowCountSpy.count(), ++heightSignalEmissions); + QCOMPARE(rowCountSpy.count(), ++rowCountSignalEmissions); QTRY_COMPARE(tableView->rows(), 3); QCOMPARE(tableView->columns(), 2); } @@ -697,56 +774,50 @@ void tst_QQmlTableModel::setDataThroughDelegate() QVERIFY(rowCountSpy.isValid()); const QHash roleNames = model->roleNames(); - QCOMPARE(roleNames.size(), 2 + builtInRoleCount); - QVERIFY(roleNames.values().contains("name")); - QVERIFY(roleNames.values().contains("age")); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(roleNames.size(), 1); + QVERIFY(roleNames.values().contains("display")); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); QCOMPARE(columnCountSpy.count(), 0); QCOMPARE(rowCountSpy.count(), 0); QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "modify")); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 18); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 18); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 18); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 18); QCOMPARE(columnCountSpy.count(), 0); QCOMPARE(rowCountSpy.count(), 0); // Test setting a role that doesn't exist for a certain column. - const auto invalidRoleRegEx = QRegularExpression(".*setData\\(\\): no role named \"age\" at column index 0. " \ - "The available roles for that column are:[\r\n] - \"name\" \\(QString\\)"); - // There are two rows, so two delegates respond to the signal, which means we need to ignore two warnings. - QTest::ignoreMessage(QtWarningMsg, invalidRoleRegEx); - QTest::ignoreMessage(QtWarningMsg, invalidRoleRegEx); QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "modifyInvalidRole")); // Should be unchanged. - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 18); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 18); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 18); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 18); QCOMPARE(columnCountSpy.count(), 0); QCOMPARE(rowCountSpy.count(), 0); // Test setting a role with a value of the wrong type. // There are two rows, so two delegates respond to the signal, which means we need to ignore two warnings. QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*setData\\(\\): failed converting value QVariant\\(QString, \"Whoops\"\\) " \ - "set at row 0 column 1 with role \"age\" to \"int\"")); + "set at row 0 column 1 with role \"display\" to \"int\"")); QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*setData\\(\\): failed converting value QVariant\\(QString, \"Whoops\"\\) " \ - "set at row 1 column 1 with role \"age\" to \"int\"")); + "set at row 1 column 1 with role \"display\" to \"int\"")); QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "modifyInvalidType")); // Should be unchanged. - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 18); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 18); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 18); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 18); QCOMPARE(columnCountSpy.count(), 0); QCOMPARE(rowCountSpy.count(), 0); } -// Start off with empty rows and append to test widthChanged(). +// Start off with empty rows and then set them to test rowCountChanged(). void tst_QQmlTableModel::setRowsImperatively() { QQuickView view(testFileUrl("empty.qml")); @@ -756,8 +827,8 @@ void tst_QQmlTableModel::setRowsImperatively() QQmlTableModel *model = view.rootObject()->property("testModel").value(); QVERIFY(model); - QCOMPARE(model->columnCount(), 0); QCOMPARE(model->rowCount(), 0); + QCOMPARE(model->columnCount(), 2); QSignalSpy columnCountSpy(model, SIGNAL(columnCountChanged())); QVERIFY(columnCountSpy.isValid()); @@ -768,17 +839,17 @@ void tst_QQmlTableModel::setRowsImperatively() QQuickTableView *tableView = view.rootObject()->property("tableView").value(); QVERIFY(tableView); QCOMPARE(tableView->rows(), 0); - QCOMPARE(tableView->columns(), 0); + QCOMPARE(tableView->columns(), 2); QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRows")); QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); const QHash roleNames = model->roleNames(); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); - QCOMPARE(columnCountSpy.count(), 1); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); + QCOMPARE(columnCountSpy.count(), 0); QCOMPARE(rowCountSpy.count(), 1); QTRY_COMPARE(tableView->rows(), 2); QCOMPARE(tableView->columns(), 2); @@ -812,134 +883,96 @@ void tst_QQmlTableModel::setRowsMultipleTimes() QCOMPARE(model->rowCount(), 3); QCOMPARE(model->columnCount(), 2); const QHash roleNames = model->roleNames(); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 20); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Imum")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 41); - QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Power")); - QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("age")).toInt(), 89); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 20); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Imum")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 41); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Power")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("display")).toInt(), 89); QCOMPARE(columnCountSpy.count(), 0); QCOMPARE(rowCountSpy.count(), 1); QTRY_COMPARE(tableView->rows(), 3); QCOMPARE(tableView->columns(), 2); // Set invalid rows; we should get a warning and nothing should change. - // TODO: add quotes to the warning message QTest::ignoreMessage(QtWarningMsg, QRegularExpression( - ".*setRows\\(\\): expected property named name at column index 0, but got nope instead")); + ".*setRows\\(\\): expected a property named \"name\" in row at index 0, but couldn't find one")); QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRowsInvalid")); QCOMPARE(model->rowCount(), 3); QCOMPARE(model->columnCount(), 2); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 20); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Imum")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 41); - QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Power")); - QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("age")).toInt(), 89); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 20); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Imum")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 41); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Power")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("display")).toInt(), 89); QCOMPARE(columnCountSpy.count(), 0); QCOMPARE(rowCountSpy.count(), 1); QCOMPARE(tableView->rows(), 3); QCOMPARE(tableView->columns(), 2); } -void tst_QQmlTableModel::builtInRoles_data() -{ - QTest::addColumn("row"); - QTest::addColumn("column"); - QTest::addColumn("roleName"); - QTest::addColumn("expectedValue"); - - const QByteArray displayRole = "display"; - - QTest::addRow("display(0,0)") << 0 << 0 << displayRole << QVariant(QLatin1String("John")); - QTest::addRow("display(0,1)") << 0 << 1 << displayRole << QVariant(QLatin1String("22")); - QTest::addRow("display(1,0)") << 1 << 0 << displayRole << QVariant(QLatin1String("Oliver")); - QTest::addRow("display(1,1)") << 1 << 1 << displayRole << QVariant(QLatin1String("33")); -} - -void tst_QQmlTableModel::builtInRoles() +void tst_QQmlTableModel::dataAndEditing() { - QFETCH(int, row); - QFETCH(int, column); - QFETCH(QByteArray, roleName); - QFETCH(QVariant, expectedValue); - - QQmlEngine engine; - QQmlComponent component(&engine, testFileUrl("builtInRoles.qml")); - QCOMPARE(component.status(), QQmlComponent::Ready); + QQuickView view(testFileUrl("dataAndSetData.qml")); + QCOMPARE(view.status(), QQuickView::Ready); + view.show(); + QVERIFY(QTest::qWaitForWindowActive(&view)); - QScopedPointer model(qobject_cast(component.create())); + QQmlTableModel *model = view.rootObject()->property("model").value(); QVERIFY(model); - QCOMPARE(model->rowCount(), 2); - QCOMPARE(model->columnCount(), 2); const QHash roleNames = model->roleNames(); - QCOMPARE(roleNames.size(), 4 + builtInRoleCount); QVERIFY(roleNames.values().contains("display")); - QVERIFY(roleNames.values().contains("decoration")); - QVERIFY(roleNames.values().contains("edit")); - QVERIFY(roleNames.values().contains("toolTip")); - QVERIFY(roleNames.values().contains("statusTip")); - QVERIFY(roleNames.values().contains("whatsThis")); - QVERIFY(roleNames.values().contains("name")); - QVERIFY(roleNames.values().contains("age")); - QVERIFY(roleNames.values().contains("someOtherRole1")); - QVERIFY(roleNames.values().contains("someOtherRole2")); - QCOMPARE(model->data(model->index(row, column, QModelIndex()), roleNames.key(roleName)), expectedValue); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); + QVERIFY(QMetaObject::invokeMethod(model, "happyBirthday", Q_ARG(QVariant, QLatin1String("Oliver")))); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 34); } -void tst_QQmlTableModel::explicitDisplayRole() +void tst_QQmlTableModel::omitTableModelColumnIndex() { QQmlEngine engine; - QQmlComponent component(&engine, testFileUrl("explicitDisplayRole.qml")); + QQmlComponent component(&engine, testFileUrl("omitTableModelColumnIndex.qml")); QCOMPARE(component.status(), QQmlComponent::Ready); QScopedPointer model(qobject_cast(component.create())); QVERIFY(model); - QCOMPARE(model->rowCount(), 1); + QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); - const QHash roleNames = model->roleNames(); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("foo")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("bar")); -} - -void tst_QQmlTableModel::roleDataProvider() -{ - QQuickView view(testFileUrl("roleDataProvider.qml")); - QCOMPARE(view.status(), QQuickView::Ready); - view.show(); - QVERIFY(QTest::qWaitForWindowActive(&view)); - - QQmlTableModel *model = view.rootObject()->property("testModel").value(); - QVERIFY(model); const QHash roleNames = model->roleNames(); - QVERIFY(roleNames.values().contains("display")); - QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Rex")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 3 * 7); - QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Buster")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 5 * 7); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); } -void tst_QQmlTableModel::dataAndEditing() +void tst_QQmlTableModel::complexRow() { - QQuickView view(testFileUrl("dataAndSetData.qml")); + QQuickView view(testFileUrl("complex.qml")); QCOMPARE(view.status(), QQuickView::Ready); view.show(); QVERIFY(QTest::qWaitForWindowActive(&view)); - QQmlTableModel *model = view.rootObject()->property("model").value(); + QQuickTableView *tableView = qobject_cast(view.rootObject()); + QVERIFY(tableView); + QCOMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); + + QQmlTableModel *model = tableView->model().value(); QVERIFY(model); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); const QHash roleNames = model->roleNames(); - QVERIFY(roleNames.values().contains("display")); - QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); - QVERIFY(QMetaObject::invokeMethod(model, "happyBirthday", Q_ARG(QVariant, QLatin1String("Oliver")))); QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); - QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 34); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); } QTEST_MAIN(tst_QQmlTableModel) diff --git a/tests/manual/tableview/tablemodel/form/RowForm.qml b/tests/manual/tableview/tablemodel/form/RowForm.qml index 428682008a..bb03e685c0 100644 --- a/tests/manual/tableview/tablemodel/form/RowForm.qml +++ b/tests/manual/tableview/tablemodel/form/RowForm.qml @@ -45,13 +45,13 @@ ScrollView { clip: true function inputAsRow() { - return [ - { checkable: checkableCheckBox.checked, checked: checkedCheckBox.checked }, - { amount: amountSpinBox.value }, - { fruitType: fruitTypeTextField.text }, - { fruitName: fruitNameTextField.text }, - { fruitPrice: parseFloat(fruitPriceTextField.text) }, - ] + return { + checked: checkedCheckBox.checked, + amount: amountSpinBox.value, + fruitType: fruitTypeTextField.text, + fruitName: fruitNameTextField.text, + fruitPrice: parseFloat(fruitPriceTextField.text) + } } default property alias content: gridLayout.children @@ -60,23 +60,11 @@ ScrollView { id: gridLayout columns: 2 - RowLayout { - Layout.columnSpan: 2 - - Label { - text: "checkable" - } - CheckBox { - id: checkableCheckBox - checked: true - } - - Label { - text: "checked" - } - CheckBox { - id: checkedCheckBox - } + Label { + text: "checked" + } + CheckBox { + id: checkedCheckBox } Label { diff --git a/tests/manual/tableview/tablemodel/form/main.qml b/tests/manual/tableview/tablemodel/form/main.qml index 21ecd8edbb..6c6874fb4b 100644 --- a/tests/manual/tableview/tablemodel/form/main.qml +++ b/tests/manual/tableview/tablemodel/form/main.qml @@ -64,31 +64,37 @@ ApplicationWindow { Layout.fillHeight: true model: TableModel { + TableModelColumn { display: "checked" } + TableModelColumn { display: "amount" } + TableModelColumn { display: "fruitType" } + TableModelColumn { display: "fruitName" } + TableModelColumn { display: "fruitPrice" } + // One row = one type of fruit that can be ordered rows: [ - [ - // Each object (line) is one cell/column, - // and each property in that object is a role. - { checked: false, checkable: true }, - { amount: 1 }, - { fruitType: "Apple" }, - { fruitName: "Granny Smith" }, - { fruitPrice: 1.50 } - ], - [ - { checked: true, checkable: true }, - { amount: 4 }, - { fruitType: "Orange" }, - { fruitName: "Navel" }, - { fruitPrice: 2.50 } - ], - [ - { checked: false, checkable: true }, - { amount: 1 }, - { fruitType: "Banana" }, - { fruitName: "Cavendish" }, - { fruitPrice: 3.50 } - ] + { + // Each object (line) is one column, + // and each property in that object represents a role. + checked: false, + amount: 1, + fruitType: "Apple", + fruitName: "Granny Smith", + fruitPrice: 1.50 + }, + { + checked: true, + amount: 4, + fruitType: "Orange", + fruitName: "Navel", + fruitPrice: 2.50 + }, + { + checked: false, + amount: 1, + fruitType: "Banana", + fruitName: "Cavendish", + fruitPrice: 3.50 + } ] } @@ -97,16 +103,16 @@ ApplicationWindow { column: 0 delegate: CheckBox { objectName: "tableViewCheckBoxDelegate" - checked: model.checked - onToggled: model.checked = checked + checked: model.display + onToggled: model.display = display } } DelegateChoice { column: 1 delegate: SpinBox { objectName: "tableViewSpinBoxDelegate" - value: model.amount - onValueModified: model.amount = value + value: model.display + onValueModified: model.display = value } } DelegateChoice { -- cgit v1.2.3