aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/qml/doc/snippets/qml/tablemodel/fruit-example-delegatechooser.qml125
-rw-r--r--src/qml/doc/snippets/qml/tablemodel/fruit-example-simpledelegate.qml114
-rw-r--r--src/qml/types/qqmlmodelsmodule.cpp2
-rw-r--r--src/qml/types/qqmltablemodel.cpp863
-rw-r--r--src/qml/types/qqmltablemodel_p.h145
-rw-r--r--src/qml/types/types.pri6
-rw-r--r--tests/auto/qml/qqmltablemodel/data/common.qml140
-rw-r--r--tests/auto/qml/qqmltablemodel/data/defaultDisplayRoles.qml61
-rw-r--r--tests/auto/qml/qqmltablemodel/data/empty.qml65
-rw-r--r--tests/auto/qml/qqmltablemodel/data/setDataThroughDelegate.qml97
-rw-r--r--tests/auto/qml/qqmltablemodel/data/setRowsMultipleTimes.qml96
-rw-r--r--tests/auto/qml/qqmltablemodel/qqmltablemodel.pro10
-rw-r--r--tests/auto/qml/qqmltablemodel/tst_qqmltablemodel.cpp864
-rw-r--r--tests/auto/quick/examples/tst_examples.cpp1
-rw-r--r--tests/manual/tableview/tablemodel/RowForm.qml114
-rw-r--r--tests/manual/tableview/tablemodel/main.cpp52
-rw-r--r--tests/manual/tableview/tablemodel/main.qml284
-rw-r--r--tests/manual/tableview/tablemodel/tablemodel.pro10
18 files changed, 3047 insertions, 2 deletions
diff --git a/src/qml/doc/snippets/qml/tablemodel/fruit-example-delegatechooser.qml b/src/qml/doc/snippets/qml/tablemodel/fruit-example-delegatechooser.qml
new file mode 100644
index 0000000000..3d44f61668
--- /dev/null
+++ b/src/qml/doc/snippets/qml/tablemodel/fruit-example-delegatechooser.qml
@@ -0,0 +1,125 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 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.Controls 2.5
+import Qt.labs.qmlmodels 1.0
+
+ApplicationWindow {
+ width: 400
+ height: 400
+ visible: true
+
+ TableView {
+ anchors.fill: parent
+ columnSpacing: 1
+ rowSpacing: 1
+ boundsBehavior: Flickable.StopAtBounds
+
+ model: TableModel {
+ // 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 }
+ ]
+ ]
+//![rows]
+ }
+//![delegate]
+ delegate: DelegateChooser {
+ DelegateChoice {
+ column: 0
+ delegate: CheckBox {
+ checked: model.checked
+ onToggled: model.checked = checked
+ }
+ }
+ DelegateChoice {
+ column: 1
+ delegate: SpinBox {
+ value: model.amount
+ onValueModified: model.amount = value
+ }
+ }
+ DelegateChoice {
+ delegate: TextField {
+ text: model.display
+ selectByMouse: true
+ implicitWidth: 140
+ onAccepted: model.display = text
+ }
+ }
+ }
+//![delegate]
+ }
+}
+//![file]
diff --git a/src/qml/doc/snippets/qml/tablemodel/fruit-example-simpledelegate.qml b/src/qml/doc/snippets/qml/tablemodel/fruit-example-simpledelegate.qml
new file mode 100644
index 0000000000..ae1f8d0b71
--- /dev/null
+++ b/src/qml/doc/snippets/qml/tablemodel/fruit-example-simpledelegate.qml
@@ -0,0 +1,114 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 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 {
+ // 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 }
+ ]
+ ]
+//![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/types/qqmlmodelsmodule.cpp b/src/qml/types/qqmlmodelsmodule.cpp
index 30915d96fd..b7b9c9ee1c 100644
--- a/src/qml/types/qqmlmodelsmodule.cpp
+++ b/src/qml/types/qqmlmodelsmodule.cpp
@@ -47,6 +47,7 @@
#include <private/qqmldelegatecomponent_p.h>
#endif
#include <private/qqmlobjectmodel_p.h>
+#include <private/qqmltablemodel_p.h>
QT_BEGIN_NAMESPACE
@@ -75,6 +76,7 @@ void QQmlModelsModule::defineLabsModule()
qmlRegisterUncreatableType<QQmlAbstractDelegateComponent>(uri, 1, 0, "AbstractDelegateComponent", QQmlAbstractDelegateComponent::tr("Cannot create instance of abstract class AbstractDelegateComponent."));
qmlRegisterType<QQmlDelegateChooser>(uri, 1, 0, "DelegateChooser");
qmlRegisterType<QQmlDelegateChoice>(uri, 1, 0, "DelegateChoice");
+ qmlRegisterType<QQmlTableModel>(uri, 1, 0, "TableModel");
}
QT_END_NAMESPACE
diff --git a/src/qml/types/qqmltablemodel.cpp b/src/qml/types/qqmltablemodel.cpp
new file mode 100644
index 0000000000..67b7639a38
--- /dev/null
+++ b/src/qml/types/qqmltablemodel.cpp
@@ -0,0 +1,863 @@
+/****************************************************************************
+**
+** 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 "qqmltablemodel_p.h"
+
+#include <QtCore/qloggingcategory.h>
+#include <QtQml/qqmlinfo.h>
+#include <QtQml/qqmlengine.h>
+
+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.
+
+ The following snippet shows the simplest use case for TableModel:
+
+ \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.
+
+ To access a specific row, the \l getRow() function can be used.
+ It's also possible to access the model's JavaScript data
+ directly via the \l rows property, but it is not possible to
+ modify the model data this way.
+
+ To add new rows, use \l appendRow() and \l insertRow(). To modify
+ existing rows, use \l setRow(), \l moveRow(), \l removeRow(), and
+ \l clear().
+
+ It is also possible to modify the model's data via the delegate,
+ as shown in the example above:
+
+ \snippet qml/tablemodel/fruit-example-simpledelegate.qml delegate
+
+ If the type of the data at the modified role does not match the type of the
+ 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 Using DelegateChooser with TableModel
+
+ For most real world use cases, it is recommended to use DelegateChooser
+ as the delegate of a TableView that uses TableModel. This allows you to
+ use specific roles in the relevant delegates. For example, the snippet
+ above can be rewritten to use DelegateChooser like so:
+
+ \snippet qml/tablemodel/fruit-example-delegatechooser.qml file
+
+ The most specific delegates are declared first: the columns at index \c 0
+ and \c 1 have \c bool and \c integer data types, so they use a
+ \l [QtQuickControls2]{CheckBox} and \l [QtQuickControls2]{SpinBox},
+ respectively. The remaining columns can simply use a
+ \l [QtQuickControls2]{TextField}, and so that delegate is declared
+ last as a fallback.
+
+ \sa QAbstractTableModel, TableView
+*/
+
+QQmlTableModel::QQmlTableModel(QObject *parent)
+ : QAbstractTableModel(parent)
+{
+}
+
+QQmlTableModel::~QQmlTableModel()
+{
+}
+
+/*!
+ \qmlproperty object TableModel::rows
+
+ This property holds the model data in the form of an array of rows:
+
+ \snippet qml/tablemodel/fruit-example-simpledelegate.qml rows
+
+ \sa getRow(), setRow(), moveRow(), appendRow(), insertRow(), clear(), rowCount, columnCount
+*/
+QVariant QQmlTableModel::rows() const
+{
+ return mRows;
+}
+
+void QQmlTableModel::setRows(const QVariant &rows)
+{
+ if (rows.userType() != qMetaTypeId<QJSValue>()) {
+ qmlWarning(this) << "setRows(): \"rows\" must be an array; actual type is " << rows.typeName();
+ return;
+ }
+
+ const QJSValue rowsAsJSValue = rows.value<QJSValue>();
+ const QVariantList rowsAsVariantList = rowsAsJSValue.toVariant().toList();
+ if (rowsAsVariantList == mRows) {
+ // No change.
+ 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 (firstRowAsVariant.type() != QVariant::List) {
+ qmlWarning(this) << "setRows(): each row in \"rows\" must be an array of objects";
+ return;
+ }
+
+ if (mColumnCount > 0) {
+ // 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;
+ }
+ }
+ }
+
+ const bool resettingModel = mRowCount != rowsAsVariantList.size();
+ const int oldRowCount = mRowCount;
+ const int oldColumnCount = mColumnCount;
+ if (resettingModel)
+ beginResetModel();
+
+ // We don't clear the column or role data, because a TableModel should not be reused in that way.
+ // Once it has valid data, its columns and roles are fixed.
+ mRows = rowsAsVariantList;
+ mRowCount = mRows.size();
+
+ if (mRowCount == 0) {
+ // No elements.
+ if (resettingModel)
+ endResetModel();
+
+ emit rowsChanged();
+
+ if (mRowCount != oldRowCount)
+ emit rowCountChanged();
+ if (mColumnCount != oldColumnCount)
+ emit columnCountChanged();
+ return;
+ }
+
+ if (mColumnCount == 0) {
+ // This is the first time the rows have been set, so establish the column count.
+ mColumnCount = firstRow.size();
+ }
+
+ bool explicitDisplayRoleIndex = false;
+
+ if (mRowCount > 0) {
+ // Go through each property of each cell in the first row
+ // and make a role name from it.
+ int roleKey = 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();
+
+ const int firstRoleForColumn = roleKey;
+ QVector<ColumnPropertyInfo> properties;
+ for (const QString &roleName : columnPropertyNames) {
+ // QML/JS supports utf8.
+ mRoleNames[roleKey] = roleName.toUtf8().constData();
+
+ if (!explicitDisplayRoleIndex && roleName == displayRoleName) {
+ explicitDisplayRoleIndex = true;
+ // The user explicitly declared a "display" role, so now they're on their own.
+ mDefaultDisplayRoles.clear();
+
+ qCDebug(lcTableModel).nospace() << "explicit \"display\" role found; "
+ << "clearing default display roles";
+ }
+
+ qCDebug(lcTableModel).nospace() << "added role "
+ << roleName << " with key " << roleKey << " found in column " << columnIndex;
+
+ const QVariant roleValue = column.value(roleName);
+ properties.append(ColumnPropertyInfo(roleName, roleValue.type(), QString::fromLatin1(roleValue.typeName())));
+
+ ++roleKey;
+ }
+
+ mColumnProperties.append(properties);
+
+ if (!explicitDisplayRoleIndex) {
+ // The "display" role wasn't specified for this column,
+ // so we use the first role that was declared for that column.
+ // TODO: make it possible to specify the display role?
+ // e.g. { myRoleName: 123, displayRole: "myRoleName" }
+ mDefaultDisplayRoles[columnIndex] = firstRoleForColumn;
+
+ qCDebug(lcTableModel).nospace() << "added implicit \"display\" role with key "
+ << int(Qt::DisplayRole) << " for column " << columnIndex
+ << " which will display values from the " << mRoleNames.value(firstRoleForColumn) << " role";
+ }
+ }
+
+ if (!explicitDisplayRoleIndex) {
+ // There was no "display" role declared by the user, so we can provide one for them.
+ mRoleNames[Qt::DisplayRole] = displayRoleName.toUtf8().constData();
+ }
+
+ endResetModel();
+ }
+
+ emit rowsChanged();
+
+ if (mRowCount != oldRowCount)
+ emit rowCountChanged();
+ if (mColumnCount != oldColumnCount)
+ emit columnCountChanged();
+}
+
+/*!
+ \qmlmethod TableModel::appendRow(object row)
+
+ Adds a new row to the end of the model, with the
+ values (cells) in \a row.
+
+ \code
+ model.appendRow([
+ { checkable: true, checked: false },
+ { amount: 1 },
+ { fruitType: "Pear" },
+ { fruitName: "Williams" },
+ { fruitPrice: 1.50 },
+ ])
+ \endcode
+
+ \sa insertRow(), setRow(), removeRow()
+*/
+void QQmlTableModel::appendRow(const QVariant &row)
+{
+ if (!validateNewRow("appendRow()", row, -1, AppendOperation))
+ return;
+
+ doInsert(mRowCount, row);
+}
+
+/*!
+ \qmlmethod TableModel::clear()
+
+ Removes all rows from the model.
+
+ \sa removeRow()
+*/
+void QQmlTableModel::clear()
+{
+ QQmlEngine *engine = qmlEngine(this);
+ Q_ASSERT(engine);
+ setRows(QVariant::fromValue(engine->newArray()));
+}
+
+/*!
+ \qmlmethod object TableModel::getRow(int rowIndex)
+
+ Returns the row at \a rowIndex in the model.
+
+ Note that this equivalent to accessing the row directly
+ through the \l rows property:
+
+ \code
+ Component.onCompleted: {
+ // These two lines are equivalent.
+ console.log(model.getRow(0).fruitName);
+ console.log(model.rows[0].fruitName);
+ }
+ \endcode
+
+ \note the returned object cannot be used to modify the contents of the
+ model; use setRow() instead.
+
+ \sa setRow(), appendRow(), insertRow(), removeRow(), moveRow()
+*/
+QVariant QQmlTableModel::getRow(int rowIndex)
+{
+ if (!validateRowIndex("getRow()", "rowIndex", rowIndex))
+ return QVariant();
+
+ return mRows.at(rowIndex);
+}
+
+/*!
+ \qmlmethod TableModel::insertRow(int rowIndex, object row)
+
+ Adds a new row to the list model at position \a rowIndex, with the
+ values (cells) in \a row.
+
+ \code
+ 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
+ the end of the list (equivalent to \l appendRow()).
+
+ \sa appendRow(), setRow(), removeRow(), rowCount
+*/
+void QQmlTableModel::insertRow(int rowIndex, const QVariant &row)
+{
+ if (!validateNewRow("insertRow()", row, rowIndex))
+ return;
+
+ doInsert(rowIndex, row);
+}
+
+void QQmlTableModel::doInsert(int rowIndex, const QVariant &row)
+{
+ beginInsertRows(QModelIndex(), rowIndex, rowIndex);
+
+ // Adding rowAsVariant.toList() will add each invidual variant in the list,
+ // which is definitely not what we want.
+ const QVariant rowAsVariant = row.value<QJSValue>().toVariant();
+ mRows.insert(rowIndex, rowAsVariant);
+ ++mRowCount;
+
+ qCDebug(lcTableModel).nospace() << "inserted the following row to the model at index"
+ << rowIndex << ":\n" << rowAsVariant.toList();
+
+ endInsertRows();
+ emit rowCountChanged();
+}
+
+/*!
+ \qmlmethod TableModel::moveRow(int fromRowIndex, int toRowIndex, int rows)
+
+ Moves \a rows from the index at \a fromRowIndex to the index at
+ \a toRowIndex.
+
+ The from and to ranges must exist; for example, to move the first 3 items
+ to the end of the list:
+
+ \code
+ model.moveRow(0, model.rowCount - 3, 3)
+ \endcode
+
+ \sa appendRow(), insertRow(), removeRow(), rowCount
+*/
+void QQmlTableModel::moveRow(int fromRowIndex, int toRowIndex, int rows)
+{
+ if (fromRowIndex == toRowIndex) {
+ qmlWarning(this) << "moveRow(): \"fromRowIndex\" cannot be equal to \"toRowIndex\"";
+ return;
+ }
+
+ if (rows <= 0) {
+ qmlWarning(this) << "moveRow(): \"rows\" is less than or equal to 0";
+ return;
+ }
+
+ if (!validateRowIndex("moveRow()", "fromRowIndex", fromRowIndex))
+ return;
+
+ if (!validateRowIndex("moveRow()", "toRowIndex", toRowIndex))
+ return;
+
+ if (fromRowIndex + rows > mRowCount) {
+ qmlWarning(this) << "moveRow(): \"fromRowIndex\" (" << fromRowIndex
+ << ") + \"rows\" (" << rows << ") = " << (fromRowIndex + rows)
+ << ", which is greater than rowCount() of " << mRowCount;
+ return;
+ }
+
+ if (toRowIndex + rows > mRowCount) {
+ qmlWarning(this) << "moveRow(): \"toRowIndex\" (" << toRowIndex
+ << ") + \"rows\" (" << rows << ") = " << (toRowIndex + rows)
+ << ", which is greater than rowCount() of " << mRowCount;
+ return;
+ }
+
+ qCDebug(lcTableModel).nospace() << "moving " << rows
+ << " row(s) from index " << fromRowIndex
+ << " to index " << toRowIndex;
+
+ // Based on the same call in QQmlListModel::moveRow().
+ beginMoveRows(QModelIndex(), fromRowIndex, fromRowIndex + rows - 1, QModelIndex(),
+ toRowIndex > fromRowIndex ? toRowIndex + rows : toRowIndex);
+
+ // Based on ListModel::moveRow().
+ if (fromRowIndex > toRowIndex) {
+ // Only move forwards - flip if moving backwards.
+ const int from = fromRowIndex;
+ const int to = toRowIndex;
+ fromRowIndex = to;
+ toRowIndex = to + rows;
+ rows = from - to;
+ }
+
+ QVector<QVariant> store;
+ store.reserve(rows);
+ for (int i = 0; i < (toRowIndex - fromRowIndex); ++i)
+ store.append(mRows.at(fromRowIndex + rows + i));
+ for (int i = 0; i < rows; ++i)
+ store.append(mRows.at(fromRowIndex + i));
+ for (int i = 0; i < store.size(); ++i)
+ mRows[fromRowIndex + i] = store[i];
+
+ qCDebug(lcTableModel).nospace() << "after moving, rows are:\n" << mRows;
+
+ endMoveRows();
+}
+
+/*!
+ \qmlmethod TableModel::removeRow(int rowIndex, int rows = 1)
+
+ Removes the row at \a rowIndex from the model.
+
+ \sa clear(), rowCount
+*/
+void QQmlTableModel::removeRow(int rowIndex, int rows)
+{
+ if (!validateRowIndex("removeRow()", "rowIndex", rowIndex))
+ return;
+
+ if (rows <= 0) {
+ qmlWarning(this) << "removeRow(): \"rows\" is less than or equal to zero";
+ return;
+ }
+
+ if (rowIndex + rows - 1 >= mRowCount) {
+ qmlWarning(this) << "removeRow(): \"rows\" " << rows
+ << " exceeds available rowCount() of " << mRowCount
+ << " when removing from \"rowIndex\" " << rowIndex;
+ return;
+ }
+
+ beginRemoveRows(QModelIndex(), rowIndex, rowIndex + rows - 1);
+
+ auto firstIterator = mRows.begin() + rowIndex;
+ // The "last" argument to erase() is exclusive, so we go one past the last item.
+ auto lastIterator = firstIterator + rows;
+ mRows.erase(firstIterator, lastIterator);
+ mRowCount -= rows;
+
+ endRemoveRows();
+ emit rowCountChanged();
+
+ qCDebug(lcTableModel).nospace() << "removed" << rows
+ << "items from the model, starting at index" << rowIndex;
+}
+
+/*!
+ \qmlmethod TableModel::setRow(int rowIndex, object row)
+
+ Changes the row at \a rowIndex in the model with \a row.
+
+ 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 },
+ ])
+ \endcode
+
+ If \a rowIndex is equal to \c rowCount(), then a new row is appended to the
+ model. Otherwise, \a rowIndex must point to an existing row in the model.
+
+ \sa appendRow(), insertRow(), rowCount
+*/
+void QQmlTableModel::setRow(int rowIndex, const QVariant &row)
+{
+ if (!validateNewRow("setRow()", row, rowIndex))
+ return;
+
+ if (rowIndex != mRowCount) {
+ // Setting an existing row.
+ mRows[rowIndex] = row;
+
+ // For now we just assume the whole row changed, as it's simpler.
+ const QModelIndex topLeftModelIndex(createIndex(rowIndex, 0));
+ const QModelIndex bottomRightModelIndex(createIndex(rowIndex, mColumnCount - 1));
+ emit dataChanged(topLeftModelIndex, bottomRightModelIndex);
+ } else {
+ // Appending a row.
+ doInsert(rowIndex, row);
+ }
+}
+
+QModelIndex QQmlTableModel::index(int row, int column, const QModelIndex &parent) const
+{
+ return row >= 0 && row < rowCount() && column >= 0 && column < columnCount() && !parent.isValid()
+ ? createIndex(row, column)
+ : QModelIndex();
+}
+
+/*!
+ \qmlproperty int TableModel::rowCount
+ \readonly
+
+ This read-only property holds the number of rows in the model.
+
+ This value changes whenever rows are added or removed from the model.
+*/
+int QQmlTableModel::rowCount(const QModelIndex &parent) const
+{
+ if (parent.isValid())
+ return 0;
+
+ return mRowCount;
+}
+
+/*!
+ \qmlproperty int TableModel::columnCount
+ \readonly
+
+ This read-only property holds the number of columns in the model.
+
+ The number of columns is fixed for the lifetime of the model
+ after the \l rows property is set or \l appendRow() is called for the first
+ time.
+*/
+int QQmlTableModel::columnCount(const QModelIndex &parent) const
+{
+ if (parent.isValid())
+ return 0;
+
+ return mColumnCount;
+}
+
+QVariant QQmlTableModel::data(const QModelIndex &index, int role) const
+{
+ const int row = index.row();
+ if (row < 0 || row >= rowCount())
+ return QVariant();
+
+ const int column = index.column();
+ if (column < 0 || column >= columnCount())
+ return QVariant();
+
+ if ((role < Qt::UserRole || role >= Qt::UserRole + mRoleNames.size()) && role != Qt::DisplayRole)
+ return QVariant();
+
+ const QVariantList rowData = mRows.at(index.row()).toList();
+ const QVariantMap columnData = rowData.at(index.column()).toMap();
+
+ int effectiveRole = role;
+ if (role == Qt::DisplayRole) {
+ // If the execution got to this point, then the user is requesting data for the display role,
+ // but didn't specify any role with the name "display".
+ // So, we give them the data of the implicit display role.
+ Q_ASSERT(mDefaultDisplayRoles.contains(index.column()));
+ effectiveRole = mDefaultDisplayRoles.value(index.column());
+ }
+
+ const QString propertyName = QString::fromUtf8(roleNames().value(effectiveRole));
+ const QVariant value = columnData.value(propertyName);
+ return value;
+}
+
+bool QQmlTableModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ const int row = index.row();
+ if (row < 0 || row >= rowCount())
+ return false;
+
+ const int column = index.column();
+ if (column < 0 || column >= columnCount())
+ return false;
+
+ if ((role < Qt::UserRole || role >= Qt::UserRole + mRoleNames.size()) && role != Qt::DisplayRole)
+ return false;
+
+ int effectiveRole = role;
+ if (role == Qt::DisplayRole) {
+ Q_ASSERT(mDefaultDisplayRoles.contains(index.column()));
+ effectiveRole = mDefaultDisplayRoles.value(index.column());
+ }
+
+ const QVariantList rowData = mRows.at(index.row()).toList();
+ const QString propertyName = QString::fromUtf8(roleNames().value(effectiveRole));
+
+ qCDebug(lcTableModel).nospace() << "setData() called with index "
+ << index << ", value " << value << " and role " << propertyName;
+
+ // Verify that the role exists for this column.
+ const ColumnPropertyInfo propertyInfo = findColumnPropertyInfo(index.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);
+ for (auto propertyInfo : availableProperties)
+ stream << " - " << propertyInfo.name << " (" << qPrintable(propertyInfo.typeName) << ")";
+
+ qmlWarning(this) << message;
+ 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.
+ QVariant effectiveValue = value;
+ if (value.type() != propertyInfo.type) {
+ if (!value.canConvert(int(propertyInfo.type))) {
+ qmlWarning(this).nospace() << "setData(): the value " << value
+ << " set at row " << row << " column " << column << " with role " << propertyName
+ << " cannot be converted to " << propertyInfo.typeName;
+ return false;
+ }
+
+ if (!effectiveValue.convert(int(propertyInfo.type))) {
+ qmlWarning(this).nospace() << "setData(): failed converting value " << value
+ << " set at row " << row << " column " << column << " with role " << propertyName
+ << " to " << propertyInfo.typeName;
+ return false;
+ }
+ }
+
+ QVariantMap modifiedColumn = rowData.at(index.column()).toMap();
+ modifiedColumn[propertyName] = value;
+
+ QVariantList modifiedRow = rowData;
+ modifiedRow[index.column()] = modifiedColumn;
+ mRows[index.row()] = modifiedRow;
+
+ QVector<int> rolesChanged;
+ rolesChanged.append(role);
+ emit dataChanged(index, index, rolesChanged);
+
+ return true;
+}
+
+QHash<int, QByteArray> QQmlTableModel::roleNames() const
+{
+ return mRoleNames;
+}
+
+QQmlTableModel::ColumnPropertyInfo::ColumnPropertyInfo()
+{
+}
+
+QQmlTableModel::ColumnPropertyInfo::ColumnPropertyInfo(
+ const QString &name, QVariant::Type type, const QString &typeName) :
+ name(name),
+ type(type),
+ typeName(typeName)
+{
+}
+
+bool QQmlTableModel::ColumnPropertyInfo::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";
+ 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";
+ return false;
+ }
+
+ return true;
+}
+
+bool QQmlTableModel::validateNewRow(const char *functionName, const QVariant &row,
+ int rowIndex, NewRowOperationFlag appendFlag) const
+{
+ if (!validateRowType(functionName, row))
+ return false;
+
+ if (appendFlag == OtherOperation) {
+ // Inserting/setting.
+ if (rowIndex < 0) {
+ qmlWarning(this) << functionName << ": \"rowIndex\" cannot be negative";
+ return false;
+ }
+
+ if (rowIndex > mRowCount) {
+ qmlWarning(this) << functionName << ": \"rowIndex\" " << rowIndex
+ << " is greater than rowCount() of " << mRowCount;
+ return false;
+ }
+ }
+
+ const QVariant rowAsVariant = row.value<QJSValue>().toVariant();
+ const QVariantList rowAsList = rowAsVariant.toList();
+
+ const int columnCount = rowAsList.size();
+ if (columnCount != mColumnCount) {
+ qmlWarning(this) << functionName << ": expected " << mColumnCount
+ << " columns, but 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;
+ }
+
+ return true;
+}
+
+bool QQmlTableModel::validateRowIndex(const char *functionName, const char *argumentName, int rowIndex) const
+{
+ if (rowIndex < 0) {
+ qmlWarning(this) << functionName << ": \"" << argumentName << "\" cannot be negative";
+ return false;
+ }
+
+ if (rowIndex >= mRowCount) {
+ qmlWarning(this) << functionName << ": \"" << argumentName
+ << "\" " << rowIndex << " is greater than or equal to rowCount() of " << mRowCount;
+ return false;
+ }
+
+ 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);
+
+ // 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);
+ for (int i = 0; i < properties.size(); ++i) {
+ const ColumnPropertyInfo &info = properties.at(i);
+ if (info.name == columnPropertyName)
+ return info;
+ }
+
+ return ColumnPropertyInfo();
+}
+
+QT_END_NAMESPACE
diff --git a/src/qml/types/qqmltablemodel_p.h b/src/qml/types/qqmltablemodel_p.h
new file mode 100644
index 0000000000..8da930872d
--- /dev/null
+++ b/src/qml/types/qqmltablemodel_p.h
@@ -0,0 +1,145 @@
+/****************************************************************************
+**
+** 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 QQMLTABLEMODEL_P_H
+#define QQMLTABLEMODEL_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 <QtCore/QAbstractTableModel>
+#include <QtQml/qqml.h>
+#include <QtQml/private/qtqmlglobal_p.h>
+#include <QtQml/QJSValue>
+
+QT_BEGIN_NAMESPACE
+
+class Q_QML_PRIVATE_EXPORT QQmlTableModel : public QAbstractTableModel
+{
+ 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)
+
+public:
+ QQmlTableModel(QObject *parent = nullptr);
+ ~QQmlTableModel() override;
+
+ QVariant rows() const;
+ void setRows(const QVariant &rows);
+
+ Q_INVOKABLE void appendRow(const QVariant &row);
+ Q_INVOKABLE void clear();
+ Q_INVOKABLE QVariant getRow(int rowIndex);
+ Q_INVOKABLE void insertRow(int rowIndex, const QVariant &row);
+ Q_INVOKABLE void moveRow(int fromRowIndex, int toRowIndex, int rows = 1);
+ Q_INVOKABLE void removeRow(int rowIndex, int rows = 1);
+ Q_INVOKABLE void setRow(int rowIndex, const QVariant &row);
+
+ QModelIndex index(int row, int column, const QModelIndex &parent) const override;
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ bool setData(const QModelIndex &index, const QVariant &value, int role) override;
+ QHash<int, QByteArray> roleNames() const override;
+
+Q_SIGNALS:
+ void columnCountChanged();
+ void rowCountChanged();
+ void rowsChanged();
+
+private:
+ class ColumnPropertyInfo
+ {
+ public:
+ ColumnPropertyInfo();
+ ColumnPropertyInfo(const QString &name, QVariant::Type type, const QString &typeName);
+
+ bool isValid() const;
+
+ QString name;
+ QVariant::Type type = QVariant::Invalid;
+ QString typeName;
+ };
+
+ enum NewRowOperationFlag {
+ OtherOperation, // insert(), set(), etc.
+ AppendOperation
+ };
+
+ bool validateRowType(const char *functionName, const QVariant &row) const;
+ bool validateNewRow(const char *functionName, const QVariant &row,
+ int rowIndex, NewRowOperationFlag appendFlag = 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 &columnPropertyName) const;
+
+ void doInsert(int rowIndex, const QVariant &row);
+
+ QVariantList mRows;
+ int mRowCount = 0;
+ int mColumnCount = 0;
+ QVector<QVector<ColumnPropertyInfo>> mColumnProperties;
+ // key = property index (0 to number of properties across all columns)
+ // value = role name
+ QHash<int, QByteArray> mRoleNames;
+ // Contains the role key to be used as the display role for each column
+ // when "display" isn't explicitly specified.
+ // key = column index
+ // value = index (key) into mRoleNames
+ QHash<int, int> mDefaultDisplayRoles;
+};
+
+QT_END_NAMESPACE
+
+QML_DECLARE_TYPE(QQmlTableModel)
+
+#endif // QQMLTABLEMODEL_P_H
diff --git a/src/qml/types/types.pri b/src/qml/types/types.pri
index e74c89b1f1..1765beb09e 100644
--- a/src/qml/types/types.pri
+++ b/src/qml/types/types.pri
@@ -6,7 +6,8 @@ SOURCES += \
$$PWD/qqmlobjectmodel.cpp \
$$PWD/qquickpackage.cpp \
$$PWD/qqmlinstantiator.cpp \
- $$PWD/qqmltableinstancemodel.cpp
+ $$PWD/qqmltableinstancemodel.cpp \
+ $$PWD/qqmltablemodel.cpp
HEADERS += \
$$PWD/qqmlbind_p.h \
@@ -17,7 +18,8 @@ HEADERS += \
$$PWD/qquickpackage_p.h \
$$PWD/qqmlinstantiator_p.h \
$$PWD/qqmlinstantiator_p_p.h \
- $$PWD/qqmltableinstancemodel_p.h
+ $$PWD/qqmltableinstancemodel_p.h \
+ $$PWD/qqmltablemodel_p.h
qtConfig(qml-worker-script) {
SOURCES += \
diff --git a/tests/auto/qml/qqmltablemodel/data/common.qml b/tests/auto/qml/qqmltablemodel/data/common.qml
new file mode 100644
index 0000000000..2ae9c50a1b
--- /dev/null
+++ b/tests/auto/qml/qqmltablemodel/data/common.qml
@@ -0,0 +1,140 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the test suite of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL-EXCEPT$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.12
+import Qt.labs.qmlmodels 1.0
+
+Item {
+ id: root
+ width: 200
+ height: 200
+
+ property alias testModel: testModel
+ property alias tableView: tableView
+
+ function appendRow(personName, personAge) {
+ testModel.appendRow([
+ { name: personName },
+ { age: personAge }
+ ])
+ }
+
+ function appendRowInvalid1() {
+ testModel.appendRow([
+ { name: "Foo" },
+ { age: 99 },
+ { nonExistentRole: 123 }
+ ])
+ }
+
+ function appendRowInvalid2() {
+ testModel.appendRow(123)
+ }
+
+ function appendRowInvalid3() {
+ testModel.appendRow([
+ { name: "Foo" },
+ { age: [] }
+ ])
+ }
+
+ function insertRow(personName, personAge, rowIndex) {
+ testModel.insertRow(rowIndex, [
+ { name: personName },
+ { age: personAge }]
+ )
+ }
+
+ function insertRowInvalid1() {
+ testModel.insertRow(0, [
+ { name: "Foo" },
+ { age: 99 },
+ { nonExistentRole: 123 }
+ ])
+ }
+
+ function insertRowInvalid2() {
+ testModel.insertRow(0, 123)
+ }
+
+ function insertRowInvalid3() {
+ testModel.insertRow(0, [
+ { name: "Foo" },
+ { age: [] }
+ ])
+ }
+
+ function setRow(rowIndex, personName, personAge) {
+ testModel.setRow(rowIndex, [
+ { name: personName },
+ { age: personAge }]
+ )
+ }
+
+ function setRowInvalid1() {
+ testModel.setRow(0, [
+ { name: "Foo" },
+ { age: 99 },
+ { nonExistentRole: 123 }
+ ])
+ }
+
+ function setRowInvalid2() {
+ testModel.setRow(0, 123)
+ }
+
+ function setRowInvalid3() {
+ testModel.setRow(0, [
+ { name: "Foo" },
+ { age: [] }
+ ])
+ }
+
+ TableModel {
+ id: testModel
+ objectName: "testModel"
+ rows: [
+ [
+ { name: "John" },
+ { age: 22 }
+ ],
+ [
+ { name: "Oliver" },
+ { age: 33 }
+ ]
+ ]
+ }
+ TableView {
+ id: tableView
+ anchors.fill: parent
+ model: testModel
+ delegate: Text {
+ text: model.display
+ }
+ }
+}
diff --git a/tests/auto/qml/qqmltablemodel/data/defaultDisplayRoles.qml b/tests/auto/qml/qqmltablemodel/data/defaultDisplayRoles.qml
new file mode 100644
index 0000000000..32daea61f9
--- /dev/null
+++ b/tests/auto/qml/qqmltablemodel/data/defaultDisplayRoles.qml
@@ -0,0 +1,61 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the test suite of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL-EXCEPT$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.12
+import Qt.labs.qmlmodels 1.0
+
+Item {
+ id: root
+ width: 200
+ height: 200
+
+ property alias testModel: testModel
+
+ TableModel {
+ id: testModel
+ objectName: "testModel"
+ rows: [
+ [
+ { name: "John", someOtherRole1: "foo" }, // column 0
+ { age: 22, someOtherRole2: "foo" } // column 1
+ ],
+ [
+ { name: "Oliver", someOtherRole1: "foo" }, // column 0
+ { age: 33, someOtherRole2: "foo" } // column 1
+ ]
+ ]
+ }
+ TableView {
+ anchors.fill: parent
+ model: testModel
+ delegate: Text {
+ id: textItem
+ text: model.display
+ }
+ }
+}
diff --git a/tests/auto/qml/qqmltablemodel/data/empty.qml b/tests/auto/qml/qqmltablemodel/data/empty.qml
new file mode 100644
index 0000000000..57f2f992d9
--- /dev/null
+++ b/tests/auto/qml/qqmltablemodel/data/empty.qml
@@ -0,0 +1,65 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the test suite of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL-EXCEPT$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.12
+import Qt.labs.qmlmodels 1.0
+
+Item {
+ id: root
+ width: 200
+ height: 200
+
+ property alias testModel: testModel
+ property alias tableView: tableView
+
+ function setRows() {
+ testModel.rows = [
+ [
+ { name: "John" },
+ { age: 22 }
+ ],
+ [
+ { name: "Oliver" },
+ { age: 33 }
+ ]
+ ]
+ }
+
+ TableModel {
+ id: testModel
+ objectName: "testModel"
+ }
+ TableView {
+ id: tableView
+ anchors.fill: parent
+ model: testModel
+ delegate: Text {
+ text: model.display
+ }
+ }
+}
diff --git a/tests/auto/qml/qqmltablemodel/data/setDataThroughDelegate.qml b/tests/auto/qml/qqmltablemodel/data/setDataThroughDelegate.qml
new file mode 100644
index 0000000000..7a419d81c6
--- /dev/null
+++ b/tests/auto/qml/qqmltablemodel/data/setDataThroughDelegate.qml
@@ -0,0 +1,97 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the test suite of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL-EXCEPT$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.12
+import Qt.labs.qmlmodels 1.0
+
+Item {
+ id: root
+ width: 200
+ height: 200
+
+ property alias testModel: testModel
+
+ signal shouldModify()
+ signal shouldModifyInvalidRole()
+ signal shouldModifyInvalidType()
+
+ function modify() {
+ shouldModify();
+ }
+
+ function modifyInvalidRole() {
+ shouldModifyInvalidRole();
+ }
+
+ function modifyInvalidType() {
+ shouldModifyInvalidType()
+ }
+
+ TableModel {
+ id: testModel
+ objectName: "testModel"
+ rows: [
+ [
+ { name: "John" },
+ { age: 22 }
+ ],
+ [
+ { name: "Oliver" },
+ { age: 33 }
+ ]
+ ]
+ }
+ TableView {
+ anchors.fill: parent
+ model: testModel
+ delegate: Text {
+ id: textItem
+ text: model.display
+
+ Connections {
+ target: root
+ enabled: column === 1
+ onShouldModify: model.age = 18
+ }
+
+ Connections {
+ target: root
+ enabled: column === 0
+ // Invalid: should be "name".
+ onShouldModifyInvalidRole: model.age = 100
+ }
+
+ Connections {
+ target: root
+ enabled: column === 1
+ // Invalid: should be string.
+ onShouldModifyInvalidType: model.age = "Whoops"
+ }
+ }
+ }
+}
diff --git a/tests/auto/qml/qqmltablemodel/data/setRowsMultipleTimes.qml b/tests/auto/qml/qqmltablemodel/data/setRowsMultipleTimes.qml
new file mode 100644
index 0000000000..15d52f93a6
--- /dev/null
+++ b/tests/auto/qml/qqmltablemodel/data/setRowsMultipleTimes.qml
@@ -0,0 +1,96 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the test suite of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL-EXCEPT$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.12
+import Qt.labs.qmlmodels 1.0
+
+Item {
+ id: root
+ width: 200
+ height: 200
+
+ property alias testModel: testModel
+ property alias tableView: tableView
+
+ function setRowsValid() {
+ testModel.rows = [
+ [
+ { 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 }
+ ]
+ ]
+ }
+
+ TableModel {
+ id: testModel
+ objectName: "testModel"
+ rows: [
+ [
+ { name: "John" },
+ { age: 22 }
+ ],
+ [
+ { name: "Oliver" },
+ { age: 33 }
+ ]
+ ]
+ }
+ TableView {
+ id: tableView
+ anchors.fill: parent
+ model: testModel
+ delegate: Text {
+ text: model.display
+ }
+ }
+}
diff --git a/tests/auto/qml/qqmltablemodel/qqmltablemodel.pro b/tests/auto/qml/qqmltablemodel/qqmltablemodel.pro
new file mode 100644
index 0000000000..11b11132aa
--- /dev/null
+++ b/tests/auto/qml/qqmltablemodel/qqmltablemodel.pro
@@ -0,0 +1,10 @@
+CONFIG += testcase
+TARGET = tst_qqmltablemodel
+
+SOURCES += tst_qqmltablemodel.cpp
+
+include (../../shared/util.pri)
+
+TESTDATA = data/*
+
+QT += core gui qml-private qml quick-private quick testlib
diff --git a/tests/auto/qml/qqmltablemodel/tst_qqmltablemodel.cpp b/tests/auto/qml/qqmltablemodel/tst_qqmltablemodel.cpp
new file mode 100644
index 0000000000..825017e811
--- /dev/null
+++ b/tests/auto/qml/qqmltablemodel/tst_qqmltablemodel.cpp
@@ -0,0 +1,864 @@
+/****************************************************************************
+**
+** 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$
+**
+****************************************************************************/
+
+#include <QtTest/qtest.h>
+#include <QtTest/qsignalspy.h>
+#include <QtCore/qregularexpression.h>
+#include <QtQml/private/qqmlengine_p.h>
+#include <QtQml/private/qqmltablemodel_p.h>
+#include <QtQml/qqmlcomponent.h>
+#include <QtQuick/qquickitem.h>
+#include <QtQuick/qquickview.h>
+#include <QtQuick/private/qquicktableview_p.h>
+
+#include "../../shared/util.h"
+
+class tst_QQmlTableModel : public QQmlDataTest
+{
+ Q_OBJECT
+
+public:
+ tst_QQmlTableModel() {}
+
+private slots:
+ void appendRemoveRow();
+ void clear();
+ void getRow();
+ void insertRow();
+ void moveRow();
+ void setRow();
+ void setDataThroughDelegate();
+ void setRowsImperatively();
+ void setRowsMultipleTimes();
+ void defaultDisplayRoles();
+};
+
+void tst_QQmlTableModel::appendRemoveRow()
+{
+ QQuickView view(testFileUrl("common.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(), 2);
+ QCOMPARE(model->columnCount(), 2);
+
+ QSignalSpy columnCountSpy(model, SIGNAL(columnCountChanged()));
+ QVERIFY(columnCountSpy.isValid());
+
+ QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged()));
+ QVERIFY(rowCountSpy.isValid());
+ int heightSignalEmissions = 0;
+
+ const QHash<int, QByteArray> roleNames = model->roleNames();
+ QCOMPARE(roleNames.size(), 3);
+ QVERIFY(roleNames.values().contains("name"));
+ QVERIFY(roleNames.values().contains("age"));
+ QVERIFY(roleNames.values().contains("display"));
+
+ 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);
+
+ // Call remove() with a negative rowIndex.
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*removeRow\\(\\): \"rowIndex\" cannot be negative"));
+ QVERIFY(QMetaObject::invokeMethod(model, "removeRow", Q_ARG(int, -1)));
+ QCOMPARE(model->rowCount(), 2);
+ QCOMPARE(model->columnCount(), 2);
+ QCOMPARE(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+
+ // Call remove() with an rowIndex that is too large.
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression(
+ ".*removeRow\\(\\): \"rowIndex\" 2 is greater than or equal to rowCount\\(\\) of 2"));
+ QVERIFY(QMetaObject::invokeMethod(model, "removeRow", Q_ARG(int, 2)));
+ QCOMPARE(model->rowCount(), 2);
+ QCOMPARE(model->columnCount(), 2);
+ QCOMPARE(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+
+ // Call remove() with a valid rowIndex but negative rows.
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*removeRow\\(\\): \"rows\" is less than or equal to zero"));
+ QVERIFY(QMetaObject::invokeMethod(model, "removeRow", Q_ARG(int, 0), Q_ARG(int, -1)));
+ QCOMPARE(model->rowCount(), 2);
+ QCOMPARE(model->columnCount(), 2);
+ QCOMPARE(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+
+ // Call remove() with a valid rowIndex but excessive rows.
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression(
+ ".*removeRow\\(\\): \"rows\" 3 exceeds available rowCount\\(\\) of 2 when removing from \"rowIndex\" 0"));
+ QVERIFY(QMetaObject::invokeMethod(model, "removeRow", Q_ARG(int, 0), Q_ARG(int, 3)));
+ QCOMPARE(model->rowCount(), 2);
+ QCOMPARE(model->columnCount(), 2);
+ QCOMPARE(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+
+ // 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), ++heightSignalEmissions);
+
+ // Call append() with a row that has a new (and hence unexpected) role.
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*appendRow\\(\\): expected 2 columns, but got 3"));
+ QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "appendRowInvalid1"));
+ // Nothing should change.
+ 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+
+ // Call append() with a row that is not an array.
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression(
+ ".*appendRow\\(\\): expected \"row\" argument to be an array, but got int instead"));
+ QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "appendRowInvalid2"));
+ // Nothing should change.
+ 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+
+ // Call append() with a row with a role that is of the wrong type.
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression(
+ ".*appendRow\\(\\): expected property with type int at column index 1, but got QVariantList instead"));
+ QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "appendRowInvalid3"));
+ // Nothing should change.
+ 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+
+ // 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->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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), ++heightSignalEmissions);
+
+ // Call remove() and specify rowIndex and rows, removing all remaining rows.
+ QVERIFY(QMetaObject::invokeMethod(model, "removeRow", Q_ARG(int, 0), Q_ARG(int, 2)));
+ 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), ++heightSignalEmissions);
+}
+
+void tst_QQmlTableModel::clear()
+{
+ QQuickView view(testFileUrl("common.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(), 2);
+ QCOMPARE(model->columnCount(), 2);
+
+ QSignalSpy columnCountSpy(model, SIGNAL(columnCountChanged()));
+ QVERIFY(columnCountSpy.isValid());
+
+ QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged()));
+ QVERIFY(rowCountSpy.isValid());
+
+ const QHash<int, QByteArray> roleNames = model->roleNames();
+ QCOMPARE(roleNames.size(), 3);
+ QVERIFY(roleNames.values().contains("name"));
+ QVERIFY(roleNames.values().contains("age"));
+ QVERIFY(roleNames.values().contains("display"));
+
+ QQuickTableView *tableView = view.rootObject()->property("tableView").value<QQuickTableView*>();
+ QVERIFY(tableView);
+ QCOMPARE(tableView->rows(), 2);
+ QCOMPARE(tableView->columns(), 2);
+
+ QVERIFY(QMetaObject::invokeMethod(model, "clear"));
+ 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), 1);
+ // Wait until updatePolish() gets called, which is where the size is recalculated.
+ QTRY_COMPARE(tableView->rows(), 0);
+ QCOMPARE(tableView->columns(), 2);
+}
+
+void tst_QQmlTableModel::getRow()
+{
+ QQuickView view(testFileUrl("common.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(), 2);
+ QCOMPARE(model->columnCount(), 2);
+
+ // Call get() with a negative row index.
+ QVariant returnValue;
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*getRow\\(\\): \"rowIndex\" cannot be negative"));
+ QVERIFY(QMetaObject::invokeMethod(model, "getRow", Q_RETURN_ARG(QVariant, returnValue), Q_ARG(int, -1)));
+ QVERIFY(!returnValue.isValid());
+
+ // Call get() with a row index that is too large.
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression(
+ ".*getRow\\(\\): \"rowIndex\" 2 is greater than or equal to rowCount\\(\\) of 2"));
+ QVERIFY(QMetaObject::invokeMethod(model, "getRow", Q_RETURN_ARG(QVariant, returnValue), Q_ARG(int, 2)));
+ QVERIFY(!returnValue.isValid());
+
+ // 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);
+}
+
+void tst_QQmlTableModel::insertRow()
+{
+ QQuickView view(testFileUrl("common.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(), 2);
+ QCOMPARE(model->columnCount(), 2);
+
+ QSignalSpy columnCountSpy(model, SIGNAL(columnCountChanged()));
+ QVERIFY(columnCountSpy.isValid());
+
+ QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged()));
+ QVERIFY(rowCountSpy.isValid());
+ int heightSignalEmissions = 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.
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression(
+ ".*insertRow\\(\\): \"rowIndex\" cannot be negative"));
+ QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "insertRow",
+ Q_ARG(QVariant, QLatin1String("Max")), Q_ARG(QVariant, 40), Q_ARG(QVariant, -1)));
+ 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+ QCOMPARE(tableView->rows(), 2);
+ QCOMPARE(tableView->columns(), 2);
+
+ // Try to insert past the last allowed index.
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression(
+ ".*insertRow\\(\\): \"rowIndex\" 3 is greater than rowCount\\(\\) of 2"));
+ QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+ 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"));
+ 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+ QCOMPARE(tableView->rows(), 2);
+ QCOMPARE(tableView->columns(), 2);
+
+ // Try to insert a row that is not an array.
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression(
+ ".*insertRow\\(\\): expected \"row\" argument to be an array, but got int 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+ 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(
+ ".*insertRow\\(\\): expected property with type int at column index 1, but got QVariantList instead"));
+ 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+ 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)));
+ 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), ++heightSignalEmissions);
+ QTRY_COMPARE(tableView->rows(), 3);
+ 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, 1)));
+ 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), ++heightSignalEmissions);
+ QTRY_COMPARE(tableView->rows(), 4);
+ QCOMPARE(tableView->columns(), 2);
+}
+
+void tst_QQmlTableModel::moveRow()
+{
+ QQuickView view(testFileUrl("common.qml"));
+ QCOMPARE(view.status(), QQuickView::Ready);
+ view.show();
+ QVERIFY(QTest::qWaitForWindowActive(&view));
+
+ QQmlTableModel *model = view.rootObject()->property("testModel").value<QQmlTableModel*>();
+ QVERIFY(model);
+ QCOMPARE(model->columnCount(), 2);
+ QCOMPARE(model->rowCount(), 2);
+
+ QSignalSpy columnCountSpy(model, SIGNAL(columnCountChanged()));
+ QVERIFY(columnCountSpy.isValid());
+
+ QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged()));
+ QVERIFY(rowCountSpy.isValid());
+ int heightSignalEmissions = 0;
+
+ const QHash<int, QByteArray> roleNames = model->roleNames();
+
+ // Append some rows.
+ QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "appendRow", Q_ARG(QVariant, QLatin1String("Max")), Q_ARG(QVariant, 40)));
+ QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "appendRow", Q_ARG(QVariant, QLatin1String("Daisy")), Q_ARG(QVariant, 30)));
+ 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+
+ // 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+
+ // 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+
+ // 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+
+ // 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+
+ // 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+
+ // 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+
+ // 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);
+}
+
+void tst_QQmlTableModel::setRow()
+{
+ QQuickView view(testFileUrl("common.qml"));
+ QCOMPARE(view.status(), QQuickView::Ready);
+ view.show();
+ QVERIFY(QTest::qWaitForWindowActive(&view));
+
+ QQmlTableModel *model = view.rootObject()->property("testModel").value<QQmlTableModel*>();
+ QVERIFY(model);
+ QCOMPARE(model->columnCount(), 2);
+ QCOMPARE(model->rowCount(), 2);
+
+ QSignalSpy columnCountSpy(model, SIGNAL(columnCountChanged()));
+ QVERIFY(columnCountSpy.isValid());
+
+ QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged()));
+ QVERIFY(rowCountSpy.isValid());
+ int heightSignalEmissions = 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.
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression(
+ ".*setRow\\(\\): \"rowIndex\" cannot be negative"));
+ QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRow",
+ Q_ARG(QVariant, -1), Q_ARG(QVariant, QLatin1String("Max")), Q_ARG(QVariant, 40)));
+ 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(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+ QCOMPARE(tableView->rows(), 2);
+ QCOMPARE(tableView->columns(), 2);
+
+ // Try to insert 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+ 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"));
+ 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+ 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"));
+ 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+ 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"));
+ 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+ QCOMPARE(tableView->rows(), 2);
+ QCOMPARE(tableView->columns(), 2);
+
+ // Set the first row.
+ QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+ QCOMPARE(tableView->rows(), 2);
+ QCOMPARE(tableView->columns(), 2);
+
+ // Set the last row.
+ QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), heightSignalEmissions);
+ QCOMPARE(tableView->rows(), 2);
+ QCOMPARE(tableView->columns(), 2);
+
+ // Append a row by passing an index that is equal to rowCount().
+ QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), ++heightSignalEmissions);
+ QTRY_COMPARE(tableView->rows(), 3);
+ QCOMPARE(tableView->columns(), 2);
+}
+
+void tst_QQmlTableModel::setDataThroughDelegate()
+{
+ QQuickView view(testFileUrl("setDataThroughDelegate.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(), 2);
+ QCOMPARE(model->columnCount(), 2);
+
+ QSignalSpy columnCountSpy(model, SIGNAL(columnCountChanged()));
+ QVERIFY(columnCountSpy.isValid());
+
+ QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged()));
+ QVERIFY(rowCountSpy.isValid());
+
+ const QHash<int, QByteArray> roleNames = model->roleNames();
+ QCOMPARE(roleNames.size(), 3);
+ QVERIFY(roleNames.values().contains("name"));
+ QVERIFY(roleNames.values().contains("age"));
+ QVERIFY(roleNames.values().contains("display"));
+ 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(), 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(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(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\""));
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*setData\\(\\): failed converting value QVariant\\(QString, \"Whoops\"\\) " \
+ "set at row 1 column 1 with role \"age\" 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), 0);
+}
+
+// Start off with empty rows and append to test widthChanged().
+void tst_QQmlTableModel::setRowsImperatively()
+{
+ 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->columnCount(), 0);
+ QCOMPARE(model->rowCount(), 0);
+
+ 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(), 0);
+
+ 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(rowCountSpy.count(), 1);
+ QTRY_COMPARE(tableView->rows(), 2);
+ QCOMPARE(tableView->columns(), 2);
+}
+
+void tst_QQmlTableModel::setRowsMultipleTimes()
+{
+ QQuickView view(testFileUrl("setRowsMultipleTimes.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(), 2);
+ 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(), 2);
+ QCOMPARE(tableView->columns(), 2);
+
+ // Set valid rows after they've already been declared.
+ QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRowsValid"));
+ 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(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"));
+ 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(columnCountSpy.count(), 0);
+ QCOMPARE(rowCountSpy.count(), 1);
+ QCOMPARE(tableView->rows(), 3);
+ QCOMPARE(tableView->columns(), 2);
+}
+
+void tst_QQmlTableModel::defaultDisplayRoles()
+{
+ QQuickView view(testFileUrl("defaultDisplayRoles.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(), 2);
+ QCOMPARE(model->columnCount(), 2);
+
+ QSignalSpy columnCountSpy(model, SIGNAL(columnCountChanged()));
+ QVERIFY(columnCountSpy.isValid());
+
+ QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged()));
+ QVERIFY(rowCountSpy.isValid());
+
+ 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(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);
+}
+
+QTEST_MAIN(tst_QQmlTableModel)
+
+#include "tst_qqmltablemodel.moc"
diff --git a/tests/auto/quick/examples/tst_examples.cpp b/tests/auto/quick/examples/tst_examples.cpp
index 9b3fa8fd2c..fdefa855e4 100644
--- a/tests/auto/quick/examples/tst_examples.cpp
+++ b/tests/auto/quick/examples/tst_examples.cpp
@@ -74,6 +74,7 @@ tst_examples::tst_examples()
{
// Add files to exclude here
excludedFiles << "snippets/qml/listmodel/listmodel.qml"; //Just a ListModel, no root QQuickItem
+ excludedFiles << "snippets/qml/tablemodel/fruit-example-delegatechooser.qml"; // Requires QtQuick.Controls import.
// Add directories you want excluded here
excludedDirs << "shared"; //Not an example
diff --git a/tests/manual/tableview/tablemodel/RowForm.qml b/tests/manual/tableview/tablemodel/RowForm.qml
new file mode 100644
index 0000000000..428682008a
--- /dev/null
+++ b/tests/manual/tableview/tablemodel/RowForm.qml
@@ -0,0 +1,114 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtQuick 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$
+**
+****************************************************************************/
+
+import QtQuick 2.12
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.11
+
+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) },
+ ]
+ }
+
+ default property alias content: gridLayout.children
+
+ GridLayout {
+ id: gridLayout
+ columns: 2
+
+ RowLayout {
+ Layout.columnSpan: 2
+
+ Label {
+ text: "checkable"
+ }
+ CheckBox {
+ id: checkableCheckBox
+ checked: true
+ }
+
+ Label {
+ text: "checked"
+ }
+ CheckBox {
+ id: checkedCheckBox
+ }
+ }
+
+ Label {
+ text: "amount"
+ }
+ SpinBox {
+ id: amountSpinBox
+ value: 1
+ }
+
+ Label {
+ text: "fruitType"
+ }
+ TextField {
+ id: fruitTypeTextField
+ text: "Pear"
+ }
+
+ Label {
+ text: "fruitName"
+ }
+ TextField {
+ id: fruitNameTextField
+ text: "Williams"
+ }
+
+ Label {
+ text: "fruitPrice"
+ }
+ TextField {
+ id: fruitPriceTextField
+ text: "1.50"
+ }
+ }
+}
diff --git a/tests/manual/tableview/tablemodel/main.cpp b/tests/manual/tableview/tablemodel/main.cpp
new file mode 100644
index 0000000000..2a3b90d392
--- /dev/null
+++ b/tests/manual/tableview/tablemodel/main.cpp
@@ -0,0 +1,52 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtQuick 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 <QGuiApplication>
+#include <QQmlApplicationEngine>
+
+int main(int argc, char *argv[])
+{
+ QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+ QGuiApplication app(argc, argv);
+
+ QQmlApplicationEngine engine;
+ engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
+
+ return app.exec();
+}
diff --git a/tests/manual/tableview/tablemodel/main.qml b/tests/manual/tableview/tablemodel/main.qml
new file mode 100644
index 0000000000..f50fbf8781
--- /dev/null
+++ b/tests/manual/tableview/tablemodel/main.qml
@@ -0,0 +1,284 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtQuick 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$
+**
+****************************************************************************/
+
+import QtQuick 2.12
+import QtQuick.Controls 2.5
+import QtQuick.Layouts 1.12
+import QtQuick.Window 2.12
+import Qt.labs.qmlmodels 1.0
+
+ApplicationWindow {
+ id: window
+ width: 800
+ height: 800
+ visible: true
+
+ ColumnLayout {
+ anchors.fill: parent
+
+ TableView {
+ id: tableView
+ boundsBehavior: Flickable.StopAtBounds
+
+ ScrollBar.horizontal: ScrollBar {}
+ ScrollBar.vertical: ScrollBar {}
+
+ Layout.minimumHeight: window.height / 2
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ model: TableModel {
+ // 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 }
+ ]
+ ]
+ }
+
+ delegate: DelegateChooser {
+ DelegateChoice {
+ column: 0
+ delegate: CheckBox {
+ objectName: "tableViewCheckBoxDelegate"
+ checked: model.checked
+ onToggled: model.checked = checked
+ }
+ }
+ DelegateChoice {
+ column: 1
+ delegate: SpinBox {
+ objectName: "tableViewSpinBoxDelegate"
+ value: model.amount
+ onValueModified: model.amount = value
+ }
+ }
+ DelegateChoice {
+ delegate: TextField {
+ objectName: "tableViewTextFieldDelegate"
+ text: model.display
+ selectByMouse: true
+ implicitWidth: 140
+ onAccepted: model.display = text
+ }
+ }
+ }
+ }
+
+ TabBar {
+ id: operationTabBar
+
+ Layout.fillWidth: true
+ Layout.preferredHeight: 40
+
+ TabButton {
+ text: "Append"
+ }
+ TabButton {
+ text: "Clear"
+ }
+ TabButton {
+ text: "Insert"
+ }
+ TabButton {
+ text: "Move"
+ }
+ TabButton {
+ text: "Remove"
+ }
+ TabButton {
+ text: "Set"
+ }
+ }
+
+ StackLayout {
+ currentIndex: operationTabBar.currentIndex
+
+ ColumnLayout {
+ RowForm {
+ id: appendRowForm
+
+ Layout.fillHeight: true
+ }
+
+ Button {
+ text: "Append"
+
+ Layout.alignment: Qt.AlignRight
+
+ onClicked: tableView.model.append(appendRowForm.inputAsRow())
+ }
+ }
+ ColumnLayout {
+ Button {
+ text: "Clear"
+ enabled: tableView.rows > 0
+
+ onClicked: tableView.model.clear()
+ }
+ }
+ ColumnLayout {
+ RowForm {
+ id: insertRowForm
+
+ Layout.fillHeight: true
+
+ Label {
+ text: "Insert index"
+ }
+ SpinBox {
+ id: insertIndexSpinBox
+ from: 0
+ to: tableView.rows
+ }
+ }
+
+ Button {
+ text: "Insert"
+
+ Layout.alignment: Qt.AlignRight
+
+ onClicked: tableView.model.insert(insertIndexSpinBox.value, insertRowForm.inputAsRow())
+ }
+ }
+ GridLayout {
+ columns: 2
+
+ Label {
+ text: "Move from index"
+ }
+ SpinBox {
+ id: moveFromIndexSpinBox
+ from: 0
+ to: tableView.rows > 0 ? tableView.rows - 1 : 0
+ }
+
+ Label {
+ text: "Move to index"
+ }
+ SpinBox {
+ id: moveToIndexSpinBox
+ from: 0
+ to: tableView.rows > 0 ? tableView.rows - 1 : 0
+ }
+
+ Label {
+ text: "Rows to move"
+ }
+ SpinBox {
+ id: rowsToMoveSpinBox
+ from: 1
+ to: tableView.rows
+ }
+
+ Button {
+ text: "Move"
+ enabled: tableView.rows > 0
+
+ Layout.alignment: Qt.AlignRight
+ Layout.columnSpan: 2
+
+ onClicked: tableView.model.move(moveFromIndexSpinBox.value, moveToIndexSpinBox.value, rowsToMoveSpinBox.value)
+ }
+ }
+ GridLayout {
+ Label {
+ text: "Remove index"
+ }
+ SpinBox {
+ id: removeIndexSpinBox
+ from: 0
+ to: tableView.rows > 0 ? tableView.rows - 1 : 0
+ }
+
+ Button {
+ text: "Remove"
+ enabled: tableView.rows > 0
+
+ Layout.alignment: Qt.AlignRight
+ Layout.columnSpan: 2
+
+ onClicked: tableView.model.remove(removeIndexSpinBox.value)
+ }
+ }
+ ColumnLayout {
+ RowForm {
+ id: setRowForm
+
+ Layout.fillHeight: true
+
+ Label {
+ text: "Set index"
+ }
+ SpinBox {
+ id: setIndexSpinBox
+ from: 0
+ to: tableView.rows > 0 ? tableView.rows - 1 : 0
+ }
+ }
+
+ Button {
+ text: "Set"
+
+ onClicked: tableView.model.set(setIndexSpinBox.value, setRowForm.inputAsRow());
+ }
+ }
+ }
+ }
+}
diff --git a/tests/manual/tableview/tablemodel/tablemodel.pro b/tests/manual/tableview/tablemodel/tablemodel.pro
new file mode 100644
index 0000000000..f6100276a1
--- /dev/null
+++ b/tests/manual/tableview/tablemodel/tablemodel.pro
@@ -0,0 +1,10 @@
+TEMPLATE = app
+TARGET = tableview_tablemodel
+QT += qml quick
+SOURCES += main.cpp
+RESOURCES += main.qml RowForm.qml
+
+# Default rules for deployment.
+qnx: target.path = /tmp/$${TARGET}/bin
+else: unix:!android: target.path = /opt/$${TARGET}/bin
+!isEmpty(target.path): INSTALLS += target