summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMitch Curtis <mitch.curtis@qt.io>2018-06-22 16:54:27 +0200
committerMitch Curtis <mitch.curtis@qt.io>2019-02-08 15:46:09 +0000
commit7fa5ef9e1ce87d9602c12d26442427c8604472ef (patch)
treec520467c672cf1e99bb51fd025fbbffa470d7176
parentfb2fb95f8b6d23715728b702ccdb9ec5587872a9 (diff)
Add TableModel
This is a QML type that can be used as a model for the new TableView. The model data is set by assigning a JavaScript array to the rows property (or by calling appendRow()). After data has been assigned for the first time, the available columns and roles are fixed for the lifetime of the model, as opposed to ListModel where the dynamicRoles property could be used. This is done intentionally to simplify the code. The API is designed to be familiar to users of ListModel: - To add new rows, use appendRow() and insertRow(). - To modify existing rows, use setRow(), moveRow(), removeRow(), and clear(). [ChangeLog][Qt Labs QML Models] Added the TableModel QML type, a JavaScript-based model for the new TableView. Fixes: QTBUG-70334 Change-Id: I55387a08b122227c5624f78af3d450b7695d974a Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
-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