diff options
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 |