diff options
22 files changed, 1671 insertions, 1059 deletions
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 <private/qqmlobjectmodel_p.h> #include <private/qqmltablemodel_p.h> +#include <private/qqmltablemodelcolumn_p.h> QT_BEGIN_NAMESPACE @@ -77,6 +78,7 @@ void QQmlModelsModule::defineLabsModule() qmlRegisterType<QQmlDelegateChooser>(uri, 1, 0, "DelegateChooser"); qmlRegisterType<QQmlDelegateChoice>(uri, 1, 0, "DelegateChoice"); qmlRegisterType<QQmlTableModel>(uri, 1, 0, "TableModel"); + qmlRegisterType<QQmlTableModelColumn>(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<QQmlTableModelColumn> QQmlTableModel::columns() { - return mRoleDataProvider; + return QQmlListProperty<QQmlTableModelColumn>(this, nullptr, + &QQmlTableModel::columns_append, + &QQmlTableModel::columns_count, + &QQmlTableModel::columns_at, + &QQmlTableModel::columns_clear); } -void QQmlTableModel::setRoleDataProvider(QJSValue roleDataProvider) +void QQmlTableModel::columns_append(QQmlListProperty<QQmlTableModelColumn> *property, + QQmlTableModelColumn *value) { - if (roleDataProvider.strictlyEquals(mRoleDataProvider)) - return; + QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object); + QQmlTableModelColumn *column = qobject_cast<QQmlTableModelColumn*>(value); + if (column) + model->mColumns.append(column); +} - mRoleDataProvider = roleDataProvider; - emit roleDataProviderChanged(); +int QQmlTableModel::columns_count(QQmlListProperty<QQmlTableModelColumn> *property) +{ + const QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object); + return model->mColumns.count(); +} + +QQmlTableModelColumn *QQmlTableModel::columns_at(QQmlListProperty<QQmlTableModelColumn> *property, int index) +{ + const QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object); + return model->mColumns.at(index); +} + +void QQmlTableModel::columns_clear(QQmlListProperty<QQmlTableModelColumn> *property) +{ + QQmlTableModel *model = static_cast<QQmlTableModel*>(property->object); + return model->mColumns.clear(); } /*! @@ -574,14 +687,19 @@ void QQmlTableModel::setRoleDataProvider(QJSValue roleDataProvider) TableModel { id: model + + TableModelColumn { display: "fruitType" } + TableModelColumn { display: "fruitPrice" } + rows: [ - [{ fruitType: "Apple" }, { fruitPrice: 1.50 }], - [{ fruitType: "Orange" }, { fruitPrice: 2.50 }] + { fruitType: "Apple", fruitPrice: 1.50 }, + { fruitType: "Orange", fruitPrice: 2.50 } ] + Component.onCompleted: { for (var r = 0; r < model.rowCount; ++r) { - console.log("An " + model.data(model.index(r, 0)).fruitType + - " costs " + model.data(model.index(r, 1)).fruitPrice.toFixed(2)) + console.log("An " + model.data(model.index(r, 0)).display + + " costs " + model.data(model.index(r, 1)).display.toFixed(2)) } } } @@ -658,27 +776,31 @@ QVariant QQmlTableModel::data(const QModelIndex &index, int role) const if (column < 0 || column >= columnCount()) return QVariant(); - if (!mRoleNames.contains(role)) + const ColumnMetadata columnMetadata = mColumnMetadata.at(index.column()); + const QString roleName = QString::fromUtf8(mRoleNames.value(role)); + if (!columnMetadata.roles.contains(roleName)) { + qmlWarning(this) << "setData(): no role named " << roleName + << " at column index " << column << ". The available roles for that column are: " + << columnMetadata.roles.keys(); return QVariant(); + } - const QVariantList rowData = mRows.at(row).toList(); - - if (mRoleDataProvider.isCallable()) { - auto engine = qmlEngine(this); - const auto args = QJSValueList() << - engine->toScriptValue(index) << - QString::fromUtf8(mRoleNames.value(role)) << - engine->toScriptValue(rowData.at(column)); - return const_cast<QQmlTableModel*>(this)->mRoleDataProvider.call(args).toVariant(); + const ColumnRoleMetadata roleData = columnMetadata.roles.value(roleName); + if (roleData.isStringRole) { + // We know the data structure, so we can get the data for the user. + const QVariantMap rowData = mRows.at(row).toMap(); + const QString propertyName = columnMetadata.roles.value(roleName).name; + const QVariant value = rowData.value(propertyName); + return value; } - // TODO: should we also allow this code to be executed if roleDataProvider doesn't - // handle the role/column, so that it only has to handle the case where there is - // more than one role in a column? - const QVariantMap columnData = rowData.at(column).toMap(); - const QString propertyName = columnPropertyNameFromRole(column, role); - const QVariant value = columnData.value(propertyName); - return value; + // We don't know the data structure, so the user has to modify their data themselves. + // First, find the getter for this column and role. + QJSValue getter = mColumns.at(column)->getterAtRole(roleName); + + // Then, call it and return what it returned. + const auto args = QJSValueList() << qmlEngine(this)->toScriptValue(index); + return getter.call(args).toVariant(); } /*! @@ -691,9 +813,9 @@ QVariant QQmlTableModel::data(const QModelIndex &index, int role) const */ bool QQmlTableModel::setData(const QModelIndex &index, const QString &role, const QVariant &value) { - const int iRole = mRoleNames.key(role.toUtf8(), -1); - if (iRole >= 0) - return setData(index, value, iRole); + const int intRole = mRoleNames.key(role.toUtf8(), -1); + if (intRole >= 0) + return setData(index, value, intRole); return false; } @@ -707,56 +829,92 @@ bool QQmlTableModel::setData(const QModelIndex &index, const QVariant &value, in if (column < 0 || column >= columnCount()) return false; - if (!mRoleNames.contains(role)) - return false; - - const QVariantList rowData = mRows.at(row).toList(); - const QString propertyName = columnPropertyNameFromRole(column, role); + const QString roleName = QString::fromUtf8(mRoleNames.value(role)); qCDebug(lcTableModel).nospace() << "setData() called with index " - << index << ", value " << value << " and role " << propertyName; + << index << ", value " << value << " and role " << roleName; // Verify that the role exists for this column. - const ColumnPropertyInfo propertyInfo = findColumnPropertyInfo(column, propertyName); - if (!propertyInfo.isValid()) { - QString message; - QDebug stream(&message); - stream.nospace() << "setData(): no role named " << propertyName - << " at column index " << column << ". The available roles for that column are:\n"; - - const QVector<ColumnPropertyInfo> availableProperties = mColumnProperties.at(column).infoForProperties; - for (auto propertyInfo : availableProperties) - stream << " - " << propertyInfo.name << " (" << qPrintable(propertyInfo.typeName) << ")"; - - qmlWarning(this) << message; + const ColumnMetadata columnMetadata = mColumnMetadata.at(index.column()); + if (!columnMetadata.roles.contains(roleName)) { + qmlWarning(this) << "setData(): no role named \"" << roleName + << "\" at column index " << column << ". The available roles for that column are: " + << columnMetadata.roles.keys(); return false; } // Verify that the type of the value is what we expect. // If the value set is not of the expected type, we can try to convert it automatically. + const ColumnRoleMetadata roleData = columnMetadata.roles.value(roleName); QVariant effectiveValue = value; - if (value.type() != propertyInfo.type) { - if (!value.canConvert(int(propertyInfo.type))) { + if (value.type() != roleData.type) { + if (!value.canConvert(int(roleData.type))) { qmlWarning(this).nospace() << "setData(): the value " << value - << " set at row " << row << " column " << column << " with role " << propertyName - << " cannot be converted to " << propertyInfo.typeName; + << " set at row " << row << " column " << column << " with role " << roleName + << " cannot be converted to " << roleData.typeName; return false; } - if (!effectiveValue.convert(int(propertyInfo.type))) { + if (!effectiveValue.convert(int(roleData.type))) { qmlWarning(this).nospace() << "setData(): failed converting value " << value - << " set at row " << row << " column " << column << " with role " << propertyName - << " to " << propertyInfo.typeName; + << " set at row " << row << " column " << column << " with role " << roleName + << " to " << roleData.typeName; return false; } } - QVariantMap modifiedColumn = rowData.at(column).toMap(); - modifiedColumn[propertyName] = value; + if (roleData.isStringRole) { + // We know the data structure, so we can set it for the user. + QVariantMap modifiedRow = mRows.at(row).toMap(); + modifiedRow[roleData.name] = value; + + mRows[row] = modifiedRow; + } else { + // We don't know the data structure, so the user has to modify their data themselves. + auto engine = qmlEngine(this); + auto args = QJSValueList() + // arg 0: modelIndex. + << engine->toScriptValue(index) + // arg 1: cellData. + << engine->toScriptValue(value); + // Do the actual setting. + QJSValue setter = mColumns.at(column)->setterAtRole(roleName); + setter.call(args); + + /* + The chain of events so far: - QVariantList modifiedRow = rowData; - modifiedRow[column] = modifiedColumn; - mRows[row] = modifiedRow; + - User did e.g.: model.edit = textInput.text + - setData() is called + - setData() calls the setter + (remember that we need to emit the dataChanged() signal, + which is why the user can't just set the data directly in the delegate) + + Now the user's setter function has modified *their* copy of the + data, but *our* copy of the data is old. Imagine the getters and setters looked like this: + + display: function(modelIndex) { return rows[modelIndex.row][1].amount } + setDisplay: function(modelIndex, cellData) { rows[modelIndex.row][1].amount = cellData } + + We don't know the structure of the user's data, so we can't just do + what we do above for the isStringRole case: + + modifiedRow[column][roleName] = value + + This means that, besides getting the implicit row count when rows is initially set, + our copy of the data is unused when it comes to complex columns. + + Another point to note is that we can't pass rowData in to the getter as a convenience, + because we would be passing in *our* copy of the row, which is not up-to-date. + Since the user already has access to the data, it's not a big deal for them to do: + + display: function(modelIndex) { return rows[modelIndex.row][1].amount } + + instead of: + + display: function(modelIndex, rowData) { return rowData[1].amount } + */ + } QVector<int> rolesChanged; rolesChanged.append(role); @@ -770,35 +928,36 @@ QHash<int, QByteArray> QQmlTableModel::roleNames() const return mRoleNames; } -QQmlTableModel::ColumnPropertyInfo::ColumnPropertyInfo() +QQmlTableModel::ColumnRoleMetadata::ColumnRoleMetadata() { } -QQmlTableModel::ColumnPropertyInfo::ColumnPropertyInfo( - const QString &name, QVariant::Type type, const QString &typeName) : +QQmlTableModel::ColumnRoleMetadata::ColumnRoleMetadata( + bool isStringRole, const QString &name, QVariant::Type type, const QString &typeName) : + isStringRole(isStringRole), name(name), type(type), typeName(typeName) { } -bool QQmlTableModel::ColumnPropertyInfo::isValid() const +bool QQmlTableModel::ColumnRoleMetadata::isValid() const { return !name.isEmpty(); } bool QQmlTableModel::validateRowType(const char *functionName, const QVariant &row) const { - if (row.userType() != qMetaTypeId<QJSValue>()) { - qmlWarning(this) << functionName << ": expected \"row\" argument to be an array," - << " but got " << row.typeName() << " instead"; + if (!row.canConvert<QJSValue>()) { + qmlWarning(this) << functionName << ": expected \"row\" argument to be a QJSValue," + << " but got " << row.typeName() << " instead:\n" << row; return false; } - const QVariant rowAsVariant = row.value<QJSValue>().toVariant(); - if (rowAsVariant.type() != QVariant::List) { - qmlWarning(this) << functionName << ": expected \"row\" argument to be an array," - << " but got " << row.typeName() << " instead"; + const QJSValue rowAsJSValue = row.value<QJSValue>(); + if (!rowAsJSValue.isObject() && !rowAsJSValue.isArray()) { + qmlWarning(this) << functionName << ": expected \"row\" argument " + << "to be an object or array, but got:\n" << rowAsJSValue.toString(); return false; } @@ -806,12 +965,21 @@ bool QQmlTableModel::validateRowType(const char *functionName, const QVariant &r } bool QQmlTableModel::validateNewRow(const char *functionName, const QVariant &row, - int rowIndex, NewRowOperationFlag appendFlag) const + int rowIndex, NewRowOperationFlag operation) const { - if (!validateRowType(functionName, row)) + if (mColumnMetadata.isEmpty()) { + // There is no column metadata, so we have nothing to validate the row against. + // Rows have to be added before we can gather metadata from them, so just this + // once we'll return true to allow the rows to be added. + return true; + } + + // Don't require each row to be a QJSValue when setting all rows, + // as they won't be; they'll be QVariantMap. + if (operation != SetRowsOperation && !validateRowType(functionName, row)) return false; - if (appendFlag == OtherOperation) { + if (operation == OtherOperation) { // Inserting/setting. if (rowIndex < 0) { qmlWarning(this) << functionName << ": \"rowIndex\" cannot be negative"; @@ -825,29 +993,48 @@ bool QQmlTableModel::validateNewRow(const char *functionName, const QVariant &ro } } - const QVariant rowAsVariant = row.value<QJSValue>().toVariant(); - const QVariantList rowAsList = rowAsVariant.toList(); + const QVariant rowAsVariant = operation == SetRowsOperation + ? row : row.value<QJSValue>().toVariant(); + if (rowAsVariant.type() != QVariant::Map) { + qmlWarning(this) << functionName << ": row manipulation functions " + << "do not support complex rows (row index: " << rowIndex << ")"; + return false; + } - const int columnCount = rowAsList.size(); - if (columnCount != mColumnCount) { + const QVariantMap rowAsMap = rowAsVariant.toMap(); + const int columnCount = rowAsMap.size(); + if (columnCount < mColumnCount) { qmlWarning(this) << functionName << ": expected " << mColumnCount - << " columns, but got " << columnCount; + << " columns, but only got " << columnCount; return false; } - // Verify that the row's columns and their roles match the name and type of existing data. - // This iterates across the columns in the row. For example: - // [ - // { checkable: true, checked: false }, // columnIndex == 0 - // { amount: 1 }, // columnIndex == 1 - // { fruitType: "Orange" }, // etc. - // { fruitName: "Navel" }, - // { fruitPrice: 2.50 } - // ], - for (int columnIndex = 0; columnIndex < mColumnCount; ++columnIndex) { - const QVariantMap column = rowAsList.at(columnIndex).toMap(); - if (!validateColumnPropertyTypes(functionName, column, columnIndex)) - return false; + // We can't validate complex structures, but we can make sure that + // each simple string-based role in each column is correct. + for (int columnIndex = 0; columnIndex < mColumns.size(); ++columnIndex) { + QQmlTableModelColumn *column = mColumns.at(columnIndex); + const QHash<QString, QJSValue> getters = column->getters(); + const auto roleNames = getters.keys(); + const ColumnMetadata columnMetadata = mColumnMetadata.at(columnIndex); + for (const QString &roleName : roleNames) { + const ColumnRoleMetadata roleData = columnMetadata.roles.value(roleName); + if (!roleData.isStringRole) + continue; + + if (!rowAsMap.contains(roleData.name)) { + qmlWarning(this).quote() << functionName << ": expected a property named " + << roleData.name << " in row at index " << rowIndex << ", but couldn't find one"; + return false; + } + + const QVariant rolePropertyValue = rowAsMap.value(roleData.name); + if (rolePropertyValue.type() != roleData.type) { + qmlWarning(this).quote() << functionName << ": expected the property named " + << roleData.name << " to be of type " << roleData.typeName + << ", but got " << QString::fromLatin1(rolePropertyValue.typeName()) << " instead"; + return false; + } + } } return true; @@ -869,82 +1056,4 @@ bool QQmlTableModel::validateRowIndex(const char *functionName, const char *argu return true; } -bool QQmlTableModel::validateColumnPropertyTypes(const char *functionName, - const QVariantMap &column, int columnIndex) const -{ - // Actual - const QVariantList columnProperties = column.values(); - const QStringList propertyNames = column.keys(); - // Expected - const QVector<ColumnPropertyInfo> properties = mColumnProperties.at(columnIndex).infoForProperties; - - // This iterates across the properties in the column. For example: - // 0 1 2 - // { foo: "A", bar: 1, baz: true }, - for (int propertyIndex = 0; propertyIndex < properties.size(); ++propertyIndex) { - const QString propertyName = propertyNames.at(propertyIndex); - const QVariant propertyValue = columnProperties.at(propertyIndex); - const ColumnPropertyInfo expectedPropertyFormat = properties.at(propertyIndex); - - if (!validateColumnPropertyType(functionName, propertyName, - propertyValue, expectedPropertyFormat, columnIndex)) { - return false; - } - } - - return true; -} - -bool QQmlTableModel::validateColumnPropertyType(const char *functionName, const QString &propertyName, - const QVariant &propertyValue, const ColumnPropertyInfo &expectedPropertyFormat, int columnIndex) const -{ - if (propertyName != expectedPropertyFormat.name) { - qmlWarning(this) << functionName - << ": expected property named " << expectedPropertyFormat.name - << " at column index " << columnIndex - << ", but got " << propertyName << " instead"; - return false; - } - - if (propertyValue.type() != expectedPropertyFormat.type) { - qmlWarning(this) << functionName - << ": expected property with type " << expectedPropertyFormat.typeName - << " at column index " << columnIndex - << ", but got " << propertyValue.typeName() << " instead"; - return false; - } - - return true; -} - -QQmlTableModel::ColumnPropertyInfo QQmlTableModel::findColumnPropertyInfo( - int columnIndex, const QString &columnPropertyName) const -{ - // TODO: check if a hash with its string-based lookup is faster, - // keeping in mind that we may be doing index-based lookups too. - const QVector<ColumnPropertyInfo> properties = mColumnProperties.at(columnIndex).infoForProperties; - for (int i = 0; i < properties.size(); ++i) { - const ColumnPropertyInfo &info = properties.at(i); - if (info.name == columnPropertyName) - return info; - } - - return ColumnPropertyInfo(); -} - -QString QQmlTableModel::columnPropertyNameFromRole(int columnIndex, int role) const -{ - QString propertyName; - if (role == Qt::DisplayRole && mColumnProperties.at(columnIndex).explicitDisplayRoleIndex == -1) { - // The user is getting or setting data for the display role, - // but didn't specify any role with the name "display" in this column. - // So, we give them the implicit display role, aka the first property we find. - propertyName = mColumnProperties.at(columnIndex).infoForProperties.first().name; - } else { - // QML/JS supports utf8. - propertyName = QString::fromUtf8(mRoleNames.value(role)); - } - return propertyName; -} - QT_END_NAMESPACE 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 <QtCore/QAbstractTableModel> #include <QtQml/qqml.h> #include <QtQml/private/qtqmlglobal_p.h> +#include <QtQml/private/qqmltablemodelcolumn_p.h> #include <QtQml/QJSValue> +#include <QtQml/QQmlListProperty> 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<QQmlTableModelColumn> 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<QQmlTableModelColumn> columns(); + + static void columns_append(QQmlListProperty<QQmlTableModelColumn> *property, QQmlTableModelColumn *value); + static int columns_count(QQmlListProperty<QQmlTableModelColumn> *property); + static QQmlTableModelColumn *columns_at(QQmlListProperty<QQmlTableModelColumn> *property, int index); + static void columns_clear(QQmlListProperty<QQmlTableModelColumn> *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<ColumnPropertyInfo> 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<QString, ColumnRoleMetadata> 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<QQmlTableModelColumn *> mColumns; int mRowCount = 0; int mColumnCount = 0; // Each entry contains information about the properties of the column at that index. - QVector<ColumnProperties> mColumnProperties; + QVector<ColumnMetadata> mColumnMetadata; // key = property index (0 to number of properties across all columns) // value = role name QHash<int, QByteArray> 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 <QtQml/qqmlinfo.h> + +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<QString, QJSValue> QQmlTableModelColumn::getters() const +{ + return mGetters; +} + +const QHash<int, QString> QQmlTableModelColumn::supportedRoleNames() +{ + QHash<int, QString> 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 <QtCore/QObject> +#include <QtQml/qqml.h> +#include <QtQml/private/qtqmlglobal_p.h> +#include <QtQml/qjsvalue.h> + +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<QString, QJSValue> getters() const; + + static const QHash<int, QString> 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<QString, QJSValue> mGetters; + QHash<QString, QJSValue> 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/roleDataProvider.qml b/tests/auto/qml/qqmltablemodel/data/complex.qml index 2706ea54fd..dbf53bac7e 100644 --- a/tests/auto/qml/qqmltablemodel/data/roleDataProvider.qml +++ b/tests/auto/qml/qqmltablemodel/data/complex.qml @@ -26,40 +26,43 @@ ** ****************************************************************************/ -import QtQuick 2.12 +import QtQuick 2.13 import Qt.labs.qmlmodels 1.0 -Item { - id: root - width: 200 - height: 200 +TableView { + width: 100 + height: 100 + delegate: Item { + implicitWidth: 50 + implicitHeight: 50 - property alias testModel: testModel - - TableModel { + Text { + text: model.display + anchors.centerIn: parent + } + } + model: 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; + + TableModelColumn { + display: function(modelIndex) { return testModel.rows[modelIndex.row][0].name } + setDisplay: function(modelIndex, cellData) { testModel.rows[modelIndex.row][0].name = cellData } } - } - TableView { - anchors.fill: parent - model: testModel - delegate: Text { - id: textItem - text: model.display + 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/omitTableModelColumnIndex.qml index 510a62e74b..86bcb08fa2 100644 --- a/tests/auto/qml/qqmltablemodel/data/explicitDisplayRole.qml +++ b/tests/auto/qml/qqmltablemodel/data/omitTableModelColumnIndex.qml @@ -28,14 +28,20 @@ import Qt.labs.qmlmodels 1.0 -import "TestUtils.js" as TestUtils - TableModel { - id: testModel + objectName: "testModel" + + TableModelColumn { display: "name" } + TableModelColumn { display: "age" } + rows: [ - [ - { name: "John", display: "foo" }, - { age: 22, display: "bar" } - ] + { + name: "John", + age: 22 + }, + { + name: "Oliver", + age: 33 + } ] } 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<int, QByteArray> 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<QQmlTableModel*>(); + 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<QQuickTableView*>(); + 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<int, QByteArray> 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<int, QByteArray> 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<QQuickTableView*>(); 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<QQuickTableView*>(); QVERIFY(tableView); @@ -300,12 +343,12 @@ void tst_QQmlTableModel::insertRow() QCOMPARE(model->columnCount(), 2); QCOMPARE(model->rowCount(), 2); const QHash<int, QByteArray> 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<int, QByteArray> 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<QQuickTableView*>(); 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<int, QByteArray> 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<int, QByteArray> 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<QQmlTableModel*>(); 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<QQuickTableView*>(); 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<int, QByteArray> 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<int, QByteArray> 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<int>("row"); - QTest::addColumn<int>("column"); - QTest::addColumn<QByteArray>("roleName"); - QTest::addColumn<QVariant>("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<QQmlTableModel> model(qobject_cast<QQmlTableModel*>(component.create())); + QQmlTableModel *model = view.rootObject()->property("model").value<QQmlTableModel*>(); QVERIFY(model); - QCOMPARE(model->rowCount(), 2); - QCOMPARE(model->columnCount(), 2); const QHash<int, QByteArray> 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<QQmlTableModel> model(qobject_cast<QQmlTableModel*>(component.create())); QVERIFY(model); - QCOMPARE(model->rowCount(), 1); + QCOMPARE(model->rowCount(), 2); QCOMPARE(model->columnCount(), 2); - const QHash<int, QByteArray> 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<QQmlTableModel*>(); - QVERIFY(model); const QHash<int, QByteArray> 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<QQmlTableModel*>(); + QQuickTableView *tableView = qobject_cast<QQuickTableView*>(view.rootObject()); + QVERIFY(tableView); + QCOMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); + + QQmlTableModel *model = tableView->model().value<QQmlTableModel*>(); QVERIFY(model); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); const QHash<int, QByteArray> 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 { |